Skip to content

fix: preserve post-commit errors in v0.14.6 - #62

Merged
cardmagic merged 1 commit into
mainfrom
fix/post-commit-errors
Sep 12, 2026
Merged

fix: preserve post-commit errors in v0.14.6#62
cardmagic merged 1 commit into
mainfrom
fix/post-commit-errors

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

An Active Record after_commit callback registered through a commit action could raise after application rows, actor state, and message completion had committed. The executor then restored its old snapshot and tried failure finalization against the deleted claim, replacing the original error with LostActivation: message claim changed.

Track actual SQL commitment before deadline translation and SQLite retries. An internal CommittedTransactionError carries the committed phase through executor and coordination rescue handlers; synchronous caller assistance and workers re-raise the original exception object with its backtrace and cause. Preserve completed state/results, prevent false retries/rejections/dead letters, and let subsequent messages progress. Callback-raised Rejected, LostActivation, record-lookup, deadline, and busy errors follow the same boundary. Pre-commit rollback, retry, and domain rejection remain unchanged.

Bump the gem to 0.14.6, date the changelog, and regenerate Gemfile.lock and inline RBS signatures. Update the correctness and architecture documentation. No migration, Statesman dependency, authorization change, or new delivery API is required. This is an Active Record-specific surface; native JS commit actions have no Rails after-commit callback registration API.

Error-delivery boundary

A separate waiting caller may observe the durable result before a worker callback raises. Callback errors are exposed in the executing process; completed history is not rewritten and failure cannot be delivered retroactively. The callback itself is not durable work and Rails may skip later callbacks after an exception. Use an idempotent effect when callback work needs independent retries.

TDD evidence

The first independent core regression used a real Active Record transaction and model callback registered through a commit action. Before implementation it verified committed application/actor/message state, then failed with:

Expected #<SolidObjects::LostActivation: message claim changed>
to be an instance of PostCommitTest::CallbackFailure,
not SolidObjects::LostActivation.

Further red tests exposed rejection finalization, worker lease-error handling, and SQLite deadline/busy retries after commit. Temporarily reversing only the six implementation files produced 12 runs, 69 assertions, 8 failures, 0 errors, 0 skips, including the original missing-claim failure. Restoring those files byte-for-byte produced 12 runs, 125 assertions, 0 failures/errors/skips.

The regression suite covers successful callbacks, failure in before_commit after the fenced block finishes, rollback/retry/rejection, original exception identity/backtrace/cause, completed rows, no duplicate execution or false dead letters, activation cleanup, subsequent execution, and a real worker on a separate connection with a waiting caller. Concurrency uses queue barriers, not sleeps.

Validation

Ruby 3.3.9, Active Record 8.1.3.1. The full bundle exec rake pipeline passed after the version bump: Minitest, Standard, RuboCop, RBS generation/validation, Steep, and Brakeman (zero security warnings).

The full adapter matrix below passed against the same implementation before the metadata-only version bump. Every database was a dedicated disposable fixture; both containers were removed afterward.

Core command Runs Assertions Failures/errors Skips
bundle exec rake (SQLite) 623 2,108 0 / 0 15
SOLID_OBJECTS_DATABASE_URL='postgresql://postgres@127.0.0.1:55449/solid_objects_post_commit?pool=20' bundle exec rake test TESTOPTS=--verbose 623 2,092 0 / 0 15
SOLID_OBJECTS_DATABASE_URL='mysql2://root@127.0.0.1:53319/solid_objects_post_commit?pool=20' bundle exec rake test TESTOPTS=--verbose 623 2,072 0 / 0 24
SOLID_OBJECTS_DATABASE_URL='trilogy://root@127.0.0.1:53319/solid_objects_post_commit_trilogy?pool=20' bundle exec rake test TESTOPTS=--verbose 623 2,072 0 / 0 24

PostgreSQL was 18.4; MySQL was 8.4.11. Skips are existing adapter-specific tests; none of the 12 new cases skipped on any adapter. The focused regression suite also passed with Rails 7.1.6.

The separate Statesman prototype checkout was updated to expect the original RuntimeError, "failed after_commit", preserving all persistence and subsequent-transition assertions. Its SQLite/PostgreSQL/MySQL matrix passed 28 tests, 136 assertions per adapter, no failures/errors/skips, and its full SQLite lint/type pipeline passed. Those sibling-repository changes are outside this Ruby PR.

Track SQL commitment before retry and failure handling so Rails callback
errors cannot relabel a completed turn or mask its original exception.
Carry the committed phase through coordination handlers and restore the
original error at synchronous caller and worker boundaries.

Cover real transaction callbacks, rollback, domain rejection, busy and
deadline failures, waiting callers, and subsequent execution with strict
red/green and implementation-reversal evidence. Document the error
boundary and bump the version and generated lockfile to 0.14.6.
@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR distinguishes exceptions raised after an Active Record transaction has durably committed from ordinary pre-commit failures.

  • Tracks the actual Active Record transaction state before deadline translation and SQLite retry handling.
  • Carries post-commit exceptions through executor and coordination layers without restoring state or finalizing an already completed message as failed.
  • Re-raises the original callback exception, preserving its identity, backtrace, and cause.
  • Adds integration coverage for callback success, rollback, retry, rejection, worker execution, caller assistance, deadline, lock, and record-lookup errors.
  • Updates the architecture and correctness documentation and releases version 0.14.6.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete correctness, security, or repository-rule violations identified.

The transaction-state checks consistently separate rolled-back work from errors raised after durable commitment, public boundaries preserve the original exception, and the regression tests cover the principal failure and cleanup paths.

Important Files Changed

Filename Overview
lib/solid_objects/database_adapter.rb Captures the active transaction and distinguishes errors raised after durable commitment from deadline failures.
lib/solid_objects/database_adapters/sqlite.rb Prevents committed transaction errors from entering SQLite busy or deadline retry paths.
lib/solid_objects/errors.rb Adds an internal wrapper that retains and re-raises the original post-commit exception.
lib/solid_objects/executor.rb Preserves committed actor and message state when completion unwinding raises after SQL commit.
lib/solid_objects/synchronous_invocation.rb Unwraps post-commit failures at the synchronous caller boundary.
lib/solid_objects/worker.rb Unwraps post-commit failures at the worker boundary without applying ordinary failure handling.
test/integration/post_commit_test.rb Adds comprehensive transaction-boundary regression coverage for callers, workers, retries, callbacks, and exception preservation.

Reviews (1): Last reviewed commit: "fix: preserve committed turns in v0.14.6" | Re-trigger Greptile

@cardmagic
cardmagic merged commit 395be30 into main Sep 12, 2026
41 checks passed
@cardmagic
cardmagic deleted the fix/post-commit-errors branch September 12, 2026 14:12
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.

1 participant