MONGOID-5782 Add block-based Timeless API to fix cascaded timestamp leak - #6165
Merged
Conversation
The counter-based timeless mechanism was consumed by the first timestamp
callback in a save, so cascaded embedded children (and nested children,
whose callbacks run more than once) lost suppression and had their
updated_at bumped.
Introduce a block-based API, timeless { ... }, backed by a thread/fiber
nesting depth that is only cleared when the block exits. This suppresses
timestamping for everything persisted in the block, at any cascade depth,
and is easier to reason about than the implicit next-operation scope.
The block-less chained form still works but is deprecated (removal in
Mongoid 10.0) via Mongoid::Deprecation. process_touch_option now uses the
block form internally so touch: false updates no longer leak on nested
embeds and no longer emit an internal deprecation warning.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a deterministic, block-scoped timeless API to prevent timestamp suppression from “leaking” away during cascaded embedded callbacks (MONGOID-5782), and deprecates the older chained/block-less form.
Changes:
- Introduces
timeless { ... }(instance and class forms) backed by a thread/fiber-local nesting depth, and updatestimeless?to honor both the new scope and legacy counters. - Deprecates chained/block-less
timelessusage viaMongoid::Deprecationwarnings. - Updates the internal
touch: falseupdate path to use the new block-based timeless scope, preventing nested-embed leaks and avoiding internal deprecation warnings; adds extensive regression and deprecation specs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| spec/mongoid/timestamps/timeless_spec.rb | Adds block-API, nesting/exception-safety, deprecation-warning, and MONGOID-5782 regression coverage. |
| lib/mongoid/timestamps/timeless.rb | Implements block-scoped timeless suppression, keeps legacy behavior for one operation, and adds deprecation warnings. |
| lib/mongoid/persistable/updatable.rb | Switches touch: false handling to use the block-based timeless scope for correct cascaded behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
comandeo-mongo
approved these changes
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mongoid::Timestamps::Timelessskipscreated_at/updated_atwrites for a single persistence operation. The mechanism is a per-class, thread/fiber-local counter that is consumed by the first timestamp callback in a save. When atimelesssave cascades to embedded children — and especially nested embedded children, whose callbacks run more than once — the counter is decremented away before those children are processed, so theirupdated_atgets bumped anyway. That is the leak reported in MONGOID-5782.The fix
Introduce a block-based API whose scope is explicit and deterministic:
The block establishes a thread/fiber-local nesting depth on entry and tears it down in an
ensure. While the block is active, timestamping is suppressed for everything persisted on that thread/fiber, at any cascade depth — so embedded and nested-embedded children are covered without any per-instance flagging or counter bookkeeping. The depth is never decremented by persistence callbacks, which is what fixes the leak.Timestamps::Timeless#timeless?now returns true when either the block scope is active or the legacy per-class counter is set.Persistable::Updatable#process_touch_optionnow uses the block form internally, sosave(touch: false)no longer leaks on nested embeds (and no longer emits an internal deprecation warning).Deprecation
The block-less chained form (
person.timeless.save,Person.timeless.create) still works but now emits a deprecation warning viaMongoid::Deprecation. It is slated for removal in Mongoid 10.0. Migration is mechanical:x.timeless.savebecomesx.timeless { x.save }.Testing
touch: falsepath does not warn.cascade_callbacks, using Timecop to assert both that the change persists and that timestamps are suppressed at every depth.All specs pass against a local replica set; RuboCop is clean.
Summary
The method-chainable
timelessmethod is deprecated in favor of a more robust block-based version.The deprecated syntax will be removed in a future major release.