Skip to content

v5.7.1 - Cache-Invalidation Hooks, Handler Isolation, Transaction Rollback - #38

Open
AdrianCurtin wants to merge 3 commits into
mainfrom
v0571
Open

v5.7.1 - Cache-Invalidation Hooks, Handler Isolation, Transaction Rollback#38
AdrianCurtin wants to merge 3 commits into
mainfrom
v0571

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Cache-invalidation webhooks stop starving application hooks

A fix release for webhook dispatch and transaction rollback. The SDK's own cache-invalidation triggers raised on every _User, _Role, and _Session webhook and took every application handler for the same trigger down with them, whether one failing after_* handler stops the rest is now an explicit setting, and a failed Parse::Object.transaction now actually restores the object it rolled back.

Changes

Cache-invalidation webhooks no longer break every application hook for the same trigger

  • FIXED: Parse::Cache::Invalidation raised NoMethodError: undefined method 'guard' on every _User, _Role, and _Session trigger it registered, and the failure was not contained. A webhook handler block is bound to the payload before it runs, so the bare guard / bump_subject / subject_id calls in the handler bodies resolved against Parse::Webhooks::Payload, which defines none of them. guard's own rescue never ran, because it lives inside the body that was never entered, so the error escaped into the route dispatcher. The dispatcher folds a trigger's handlers with Array#map, which abandons the collection on the first raise, and these triggers install during Parse.setup and therefore sit ahead of every application handler for the same trigger. One unresolvable method name silently prevented an application's own after_save "_User" hook from running at all, so any work downstream of it stopped without a visible cause. The handlers now call the module on an explicit receiver captured in a local, which is independent of whatever self the dispatcher binds. Applications that set cache_invalidation_hooks: false to work around this can remove it.
  • FIXED: The invalidation tests dispatched handlers with a plain Proc#call, which leaves self bound to the module the block closed over, so they passed against handlers that could not run in production. They now dispatch through the real handler invocation and fail against the broken code.

Whether one failing after_* handler stops the rest is now a setting

  • NEW: Parse::Webhooks.abort_after_callbacks_on_error decides whether an exception raised by one after_* handler prevents the remaining handlers for that trigger from running. It defaults to true, which is the existing behavior, so nothing changes on upgrade. Set it to false to isolate handlers from each other: each one runs regardless of what an earlier one raised, and the failure is reported with a warning and a parse.webhooks.handler_error notification instead of being swallowed silently. This was previously not a decision at all but a side effect of folding handlers with Array#map, which abandons the collection on the first raise. Registration order is not fully under an application's control, since the SDK's own cache-invalidation triggers install during Parse.setup and therefore sit ahead of handlers registered by application files loaded later, so a single raising handler could silently prevent an application's own hooks from running with nothing logged to say they had been skipped.

Failed transactions restore the object they rolled back

  • FIXED: A failed Parse::Object.transaction left the in-memory object holding its modified values. The rollback snapshotted Parse::Object#attributes and restored it, but that method returns a schema map of field name to type symbol rather than values, so nothing was ever restored. Property values live in @<field> instance variables; those are now what the rollback captures and restores, including values nested inside arrays and hashes, relation operation queues, and properties whose ivar did not exist before the transaction. State is captured when the object first enters the transaction rather than at batch.add, so the documented pattern of mutating an object and adding it afterwards rolls back correctly.
  • FIXED: A failed transaction also left the object with broken change tracking. Restoring the schema map defined @attributes, which is the instance variable ActiveModel::Dirty keys its behavior on: once defined it builds an AttributeMutationTracker over that hash instead of the ForcedMutationTracker a Parse::Object needs, and clear_changes! then called forgetting_assignment on the [key, value] pairs Hash#map yields. Both call sites rescue and warn, so every rollback quietly downgraded the object rather than failing. The rollback no longer defines @attributes.

A silently skipped integration test actually runs again

  • FIXED: The mongo-direct role-graph integration test gated on ANALYTICS_DATABASE_URI, a production variable name the test stack never sets, so both of its traversal assertions skipped on every run while the per-file reporter still reported the file as passing. It now reads PARSE_TEST_MONGO_URI like every other mongo-direct integration file and still honors ANALYTICS_DATABASE_URI as an override.

Behavior Notes

  • abort_after_callbacks_on_error governs only whether later handlers still run. Neither mode reverts anything: an after_* trigger fires once the write has already committed, so there is no version of this setting that can undo a save.
  • before_* dispatch is untouched. Only the accumulating, non-rejectable triggers (after_save, after_delete, after_logout) can hold more than one handler in the first place, and a rejectable before_* trigger must deny if any handler denies, so its raise must continue to abort.

Code Example

# Default: a raising after_* handler still aborts the remaining handlers.
Parse::Webhooks.abort_after_callbacks_on_error = true

# Opt in to isolation: every handler for the trigger runs, and a raise is
# reported rather than propagated.
Parse::Webhooks.abort_after_callbacks_on_error = false

ActiveSupport::Notifications.subscribe("parse.webhooks.handler_error") do |*, payload|
  # payload[:trigger] is the trigger symbol, payload[:error] the error class name.
  Rails.logger.error("webhook #{payload[:trigger]} raised #{payload[:error]}")
end

# This now runs even when an earlier handler for the same trigger raised.
Parse::Webhooks.route(:after_save, "_User") do
  Rails.logger.info("user saved: #{parse_object.id}")
end

Capture object state at first mutation inside `Parse::Object.transaction` instead of waiting for `batch.add`, so mutate-then-add flows roll back correctly. The snapshot and restore logic now covers undefined ivars, nested mutable values, relation proxy internals, ACL and dirty-tracking ivars, and newly created transaction objects. Added focused rollback tests for deep mutation isolation, relation operation arrays, preexisting dirty state, failed creates, and transaction context cleanup.
Refactor cache invalidation webhook handlers to call helper methods through an explicit module receiver instead of relying on `self` inside webhook blocks. This prevents `NoMethodError` in `_User`, `_Role`, and `_Session` triggers and avoids aborting later application handlers on the same route.

Update cache invalidation tests to invoke handlers through `Parse::Webhooks.invoke_handler`, add coverage for payload-bound dispatch behavior, and add an end-to-end assertion that SDK hooks cannot starve app hooks. Also bump the gem version to 5.7.1 and add the corresponding changelog entry.
Introduce `Parse::Webhooks.abort_after_callbacks_on_error` to control whether a raised `after_*` handler aborts remaining handlers. The default is `true` to preserve existing behavior; when set to `false`, dispatch continues, logs a warning, and emits `parse.webhooks.handler_error` notifications instead of propagating the exception. Refactor composed handler execution into `dispatch_composed`, add error reporting helper logic, and add focused tests covering default abort behavior, isolation behavior, reporting, and unaffected `before_*` semantics. Update CHANGELOG with the new setting and behavior notes.
Copilot AI review requested due to automatic review settings August 2, 2026 18:33
@AdrianCurtin AdrianCurtin changed the title V0571 v5.7.1 - Cache-Invalidation Hooks, Handler Isolation, Transaction Rollback Aug 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bumps the gem to 5.7.1 and delivers three related reliability fixes: (1) webhook handler execution is made resilient/controllable when an after_* handler raises, (2) cache invalidation webhooks are corrected to not depend on self being the registering module, and (3) failed transactions now correctly roll back in-memory object state (including nested/mutable values and dirty-tracking state).

Changes:

  • Add Parse::Webhooks.abort_after_callbacks_on_error and route array-dispatch logic to optionally isolate failing after_* handlers while still reporting errors.
  • Fix cache-invalidation webhook handlers to call invalidation helpers on an explicit receiver (independent of dispatcher self binding) and strengthen tests to dispatch via the real invoke_handler.
  • Rework transaction rollback snapshotting/restoration to capture instance-variable-backed property values (deeply), relation proxy mutation queues, and key dirty-tracking ivars; capture state before first mutation inside the transaction.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/lib/parse/webhook_handler_isolation_test.rb New unit tests covering abort-vs-isolate semantics for composed after_* webhook handlers.
test/lib/parse/transaction_rollback_state_test.rb Expanded rollback tests for deep-dup snapshots, undefined ivars, relation queues, and transaction-context restoration.
test/lib/parse/cache_invalidation_test.rb Updates tests to dispatch handlers through the real dispatcher binding (invoke_handler) and adds a regression for “SDK handler shouldn’t starve app handler”.
lib/parse/webhooks.rb Introduces configurable composed-dispatch behavior, adds error reporting via warn + notifications, and routes composed triggers through the new dispatcher.
lib/parse/stack/version.rb Version bump to 5.7.1.
lib/parse/model/object.rb Hooks object initialization and ACL mutation into transaction state capture.
lib/parse/model/core/properties.rb Captures transaction state before getter materializes defaults/mutable values.
lib/parse/model/core/fetching.rb Captures transaction state before dirty-tracking marks/replaces values via setters.
lib/parse/model/core/actions.rb Implements fiber-local transaction snapshotting, deep duplication, restoration (including undefined ivars), and revised transaction rollback behavior.
lib/parse/model/associations/has_many.rb Captures transaction state before returning/materializing mutable collection proxies.
lib/parse/model/associations/collection_proxy.rb Captures transaction state when collection proxies are mutated (before dirty notification).
lib/parse/model/associations/belongs_to.rb Captures transaction state after lazy hydration but before callers can mutate returned association objects.
lib/parse/cache/invalidation.rb Refactors webhook handlers to invoke invalidation methods on an explicit receiver (no reliance on bound self).
Gemfile.lock Updates locked local gem version to 5.7.1.
CHANGELOG.md Documents the 5.7.1 fixes and behavioral notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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