Skip to content

Fix runTx throwing instead of waiting when called while another transaction is active - #468

Merged
isoos merged 1 commit into
isoos:masterfrom
xvrh:fix-runtx-concurrent-callers
Sep 24, 2026
Merged

isoos merged 1 commit into
isoos:masterfrom
xvrh:fix-runtx-concurrent-callers

Conversation

@xvrh

@xvrh xvrh commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

3.5.16 added a check to runTx so that a call made from inside its own callback fails fast instead of deadlocking. That part is good.

The check is _activeTransaction != null, though. That flag is set by any active transaction on the connection. So a runTx issued from a different async flow now throws too. It is not re-entrant, and before 3.5.16 it waited on _operationLock. That is also what the runTx doc says: "other invocations on a [Connection] are blocked while a transaction is active".

The outcome now depends on timing. The existing test "Make sure two simultaneous transactions cannot be interwoven" still passes only because both calls start before the first one sets _activeTransaction. If the second call comes one query later, it throws:

final started = Completer<void>();
final first = conn.runTx((c) async {
  await c.execute('SELECT 1');
  started.complete();
  await c.execute('SELECT pg_sleep(0.2)');
});
await started.future;
await conn.runTx((c) => c.execute('SELECT 2')); // throws on 3.5.16
await first;
Case Before 3.5.16 3.5.16 This PR
Two runTx calls started in the same microtask waits waits waits
runTx from outside, after another transaction began waits throws waits
runTx from inside its own callback deadlock throws throws

We hit this in a code generator that shares one Connection between concurrent workers, each doing a short runTx.

Fix

runTx now runs its callback in a zone that records which connections have a transaction callback running. It throws only when the current zone says it is inside this connection's own callback. Every other call waits on _operationLock again, as before 3.5.16. Nested runTx calls on different connections still work.

…nsaction is active on the same connection.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.08%. Comparing base (2439394) to head (ebf06da).
⚠️ Report is 62 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #468      +/-   ##
==========================================
+ Coverage   84.65%   86.08%   +1.43%     
==========================================
  Files          33       33              
  Lines        3415     3824     +409     
==========================================
+ Hits         2891     3292     +401     
- Misses        524      532       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@isoos

isoos commented Sep 23, 2026

Copy link
Copy Markdown
Owner

So a runTx issued from a different async flow now throws too.

I'm not sure what the expectation here would be. A single postgres connection is only associated to a single transaction, cannot be in two. Are the users of the package aware of that and would like the library to queue these? Or are they expecting them to overlap?

For queueing, the connection pool would be a better place. In fact, I think it should the main place people use. So enabling this may be good, but it also may enable a call pattern which may fail the expectations of the users.

What do you think about these concerns?

@xvrh

xvrh commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

We expect them to queue, not overlap. The PR keeps one transaction per connection. The second runTx waits for the first to finish.

In 3.5.16, the behaviour changed and it now depends on timing. If two runTx calls start in the same microtask, the second waits. That's what the "cannot be interwoven" test covers. If the second call starts one await later, it throws.

Queuing in every case or throwing in every case would both be fine with me.

@isoos

isoos commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Okay, I'm sold on this.

@isoos
isoos merged commit dbdf827 into isoos:master Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants