Allow running arbitrary async code in a transaction?
See original GitHub issueHi! Thanks for creating this library! It has been very nice to use.
At work we have this use case:
- Start transaction
UPDATE scheduler.schedule SET sent_date = @now WHERE id = @id- Publish a message to RabbitMQ (quick async operation that can fail)
- Commit transaction, or rollback if publishing to RabbitMQ failed
We couldn’t find a way to do this, so we rolled our own. Since we’re not super experienced F#/dotnet developers, we simply copied executeTransactionAsync and modified it slightly to do what we needed. Here’s the result:
type QueryOrAsync =
| SqlQuery of string * List<List<string * SqlValue>>
| CustomAsync of Async<Result<unit, string>>
// Based on: https://github.com/Zaid-Ajaj/Npgsql.FSharp/blob/dad861b4f2e5a27300747854052bd033a2438bf3/src/Sql.fs#L554-L589
// Changed `(props: SqlProps)` to `(connectionString: string)` as a workaround for `SqlProps` being kinda private.
let executeTransactionAsyncWithCustomAsyncs (queries: List<QueryOrAsync>) (connectionString: string) =
async {
try
let! token = Async.CancellationToken
// Changed `props.CancellationToken` to `CancellationToken.None` because private.
use mergedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, CancellationToken.None)
let mergedToken = mergedTokenSource.Token
if List.isEmpty queries then
return Ok []
else
// Changed from `getConnection props` because private.
let connection = new NpgsqlConnection(connectionString)
try
// Removed `if x then connection.Open()` because we got "Connection already open" errors otherwise.
// Doesn’t make sense to first open synchronously and then asynchronously on the next line?
do! Async.AwaitTask(connection.OpenAsync mergedToken)
use transaction = connection.BeginTransaction()
let affectedRowsByQuery = ResizeArray<int>()
for queryOrAsync in queries do
match queryOrAsync with
| SqlQuery(query, parameterSets) ->
if List.isEmpty parameterSets then
use command = new NpgsqlCommand(query, connection, transaction)
let! affectedRows = Async.AwaitTask(command.ExecuteNonQueryAsync mergedToken)
affectedRowsByQuery.Add affectedRows
else
for parameterSet in parameterSets do
use command = new NpgsqlCommand(query, connection, transaction)
// We had to copy the entire `populateRow` function into our code
// since `poulateRow` is private...
populateRow command parameterSet
let! affectedRows = Async.AwaitTask(command.ExecuteNonQueryAsync mergedToken)
affectedRowsByQuery.Add affectedRows
| CustomAsync asyncResult ->
// Please excuse our ugly async code.
let theAsync =
async.Bind
(asyncResult,
function
| Ok() -> async.Return()
| Error message -> raise (System.Exception(message)))
do! theAsync
do! Async.AwaitTask(transaction.CommitAsync mergedToken)
return Ok(List.ofSeq affectedRowsByQuery)
finally
connection.Dispose()
with error -> return Error error
}
The above allows us to do the following:
sendEvent now publishScheduledEvent connectionString eventToTrigger =
connectionString
|> executeTransactionAsyncWithCustomAsyncs
[ SqlQuery
("UPDATE scheduler.schedule SET sent_date = @now WHERE id = @id",
[ [ ("now", Sql.timestamptz now)
("id", Sql.int64 eventToTrigger.id) ] ])
CustomAsync(publishScheduledEvent eventToTrigger.payload) ]
We’ve tried it and it works: If publishScheduledEvent fails, the scheduler.schedule table isn’t updated.
Would you be interested in adding something like this to your library? You might be able to come up with a much nicer API than we did (we didn’t spend much time on it). As long as we can fulfil our use case we’d be happy!
Issue Analytics
- State:
- Created 4 years ago
- Reactions:3
- Comments:23 (8 by maintainers)
Top Results From Across the Web
How to deal with asynchronous code in JavaScript
Running checkIfItsDone() will execute the doSomething() promise and will wait for it to resolve, using the then callback, and if there is an...
Read more >Is it OK to pass an async function to setImmediate in order ...
I want to call a given function asynchronously. The wrapper function tryCallAsync is one way of doing this. This approach works. However, it ......
Read more >Documentation: 15: 30.4. Asynchronous Commit
Asynchronous commit is an option that allows transactions to complete more quickly, at the cost that the most recent transactions may be lost...
Read more >Tools for Handling Async Code
So far, we've learned how to use an XMLHttpRequest object to make our API call. To handle the asynchrony of the request, or,...
Read more >Node.js, MySQL and async/await
In JavaScript, you have three options to write asynchronous code: ... which executes arbitrary asynchronous code within a transaction:
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found

Alright, so it took awhile for me to come up with a serious API to tackle this problem that I am actually happy with. Although I liked the continuation style by @jukkhop, it meant that the library has to have specific transaction command and non-transaction commands.
Instead, I came up with the following API as of v3.12 (please see new docs) which allows to run the SQL commands or any arbitrary code in the context of a transaction. You basically create the transaction yourself and re-use it across commands as follows:
The magic happens when you provide the transaction via the
Sql.transactionfunction.If you don’t like creating the connection yourself because you want to use the builder API, you can instead let the library create the connection as follows:
Please give this a try and let me know if you encounter any problem
This is on my list of stuff to try out – I’ll let you know once I get to it! 👍