9009 dxox: Transaction support (formerly flexion#10705)#6283
9009 dxox: Transaction support (formerly flexion#10705)#6283
Conversation
…nnection because they haven't looked at the pool config, that's a them issue
| } | ||
|
|
||
| // Callbacks to run once the commit is successful | ||
| export function onTransactionCommit(cb: () => Promise<void>) { |
There was a problem hiding this comment.
We do not want to update OpenSearch when we make a query unless we commit the transaction. onTransactionCommit allows us to say, "Ok, do X--like update OpenSearch--once the transaction commits."
There was a problem hiding this comment.
One thing to test: if we queue up a ton of OpenSearch updates, will we run into memory limits?
There was a problem hiding this comment.
Other potential use cases for onTransactionCommit: do not generate these PDFs or send these emails until we've updated the DB.
…ally, and make connections more robust so that we could increase our maximum pool size without breaking everything
…s commit attempts to improve the situation
…out our connection handling incorrectly. I have renamed it.
| let transactionStore: ConnectionInfo = {} as ConnectionInfo; | ||
|
|
||
| // Start the transaction; Kysely handles BEGIN/COMMIT/ROLLBACK. | ||
| const result = await db.transaction().execute(async trx => { |
There was a problem hiding this comment.
Note that kysely.transaction 1) guarantees the same connection throughout the transaction lifecycle and 2) handles the mutex lock so that no two transactions will ever run on the same connection.
|
this has successfully been deployed to exp8 without error https://app.circleci.com/pipelines/github/ustaxcourt/ef-cms/16910/workflows/cfa2326f-9e4a-4d4a-89bf-ccced17d69cd |
This PR has two fundamental goals:
Details:
I think our current connection code has a few issues.
getConnectionandestablishConnection... but these are not getting a connection or establishing a connection. They are returning a kysely wrapper object around a client pool. Behind the scenes, kysely is creating/acquiring/releasing/removing connections when we call, e.g.,.execute.We have two connection pools, one forNope. This is complicated.getConnectionand one forgetLockingConnection. For the latter especially, we abuse the pool: we say, "Make sure there is only ever one connection in your pool so that we don't accidentally create a lock on one connection and try to remove it with another, which won't work!" But this is not what a connection pool is for. Instead, we should have one connection pool and get an honest-to-goodness connection from the pool and use that throughout the lifetime of the lock.En route to transaction support, this PR addresses those issues as follows:
getConnectionandestablishConnectionetc. If we think of these objects as connections, we will confuse things.We have one connection pool. The equivalent ofNope.getLockingConnection,acquireOneDbConnection, now simply gets a particular connection from the existing pool and uses it throughout the lifetime of the locking code.AsyncLocalStorageas needed. This means that we can in principle have two asynchronous connections at once that each know which transaction they are a part of. It also means we don't have to try passing around connections manually, which would be a huge mess. In other words, we can extend the number of clients in our pool and everything should "just work" because we have isolated connections per node process-chain.Implementation details:
transactions.ts, which defineswithTransactionand a couple of helpersinTransaction(answers the question "Am I currently executing as part of a transaction?") andonTransactionCommit(queues up work to be executed upon a successful transaction).database.ts, which now uses the helpersinTransactionandonTransactionCommit. IfinTransaction()isfalseingetDbWriter, we send OpenSearch index messages directly after writing to Postgres, as we did before. If it istrue, we cache the messages viaonTransactionCommitto send only once the transaction is committed.getConnection.ts, renamed todatabaseConnection.ts, which now defines context we can send to child processes.getDb, the rough equivalent to our previousgetConnection, now first checks "Do I have a connection passed in from my parent process that I should be using?" If so, it returns that. This allows us to pass a transaction (or, in the future, other connection stuff) through an arbitrarily complex child callback chain.