Skip to content

connect before dirtying a transaction with a timeout - #45

Merged
maths22 merged 1 commit into
instructure:mainfrom
augiethornton:fix-nil-raw-connection-with-timeout
Aug 11, 2026
Merged

connect before dirtying a transaction with a timeout#45
maths22 merged 1 commit into
instructure:mainfrom
augiethornton:fix-nil-raw-connection-with-timeout

Conversation

@augiethornton

Copy link
Copy Markdown
Contributor

Setting an explicit timeout calls dirty!, which makes the transaction unrestorable. That disables Rails' lazy connect — with_raw_connection only connects when reconnect_can_restore_state? is true — so on a connection that has never been used, the first statement is BEGIN (issued by RealTransaction#materialize!) and it runs against a nil raw connection:

NoMethodError: undefined method 'async_exec' for nil

Hit in production in Canvas, where canvas_partman wraps on-demand partition creation in with_statement_timeout and can land on a GuardRail role connection that the request has not otherwise touched. Applies to all three TIMEOUTS, not just statement_timeout.

The ordering is load-bearing

Connecting after dirty! also stops the crash, but is wrong. reset_transaction only carries the transaction manager across a reconnect while it is still restorable?:

old_state = @transaction_manager if restore && @transaction_manager&.restorable?

Once the transaction is dirty that is false, so connect!verify!reconnect!(restore_transactions: true) installs a fresh empty manager and never swaps back. The transaction is silently discarded along with the timeout just set — no BEGIN, no SET LOCAL, and the block runs with no timeout at all. That is why there are two specs: the first passes under either ordering, the second only under this one.

Verification

  • Both specs fail on main with the production error above.
  • 107 examples, 0 failures on Rails 8.1, 8.0, and 7.2; bin/rubocop clean.
  • Verified end to end against the original Canvas code path, which fails without the change and succeeds with it.

Follow-up

dirty! unless ::Rails.version < '7.1' is now dead — 93c38f5 raised the floor to Rails 7.2 but left that check and two more in spec_helper.rb behind. Kept out of this PR so the fix stays reviewable; follow-up to come.

🤖 Generated with Claude Code

Setting an explicit timeout calls dirty!, which makes the
transaction unrestorable. That disables Rails' lazy connect --
with_raw_connection only connects when
reconnect_can_restore_state? is true -- so on a connection that
had never been used, the first statement (BEGIN, issued by
RealTransaction#materialize!) ran against a nil raw connection:

  NoMethodError: undefined method 'async_exec' for nil

Connect before dirty! rather than after. reset_transaction only
carries the transaction manager across a reconnect while it is
still restorable, so connecting once the transaction is dirty
silently discards it along with the timeout just set -- no
BEGIN, no SET LOCAL, and the block runs with no timeout at all.
The second spec covers that, since the crash alone is fixed by
either ordering.
@maths22
maths22 merged commit 9b4693d into instructure:main Aug 11, 2026
14 checks 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.

2 participants