Skip to content

MONGOID-5782 Add block-based Timeless API to fix cascaded timestamp leak - #6165

Merged
jamis merged 4 commits into
mongodb:masterfrom
jamis:5782-timeless-leak
Jul 20, 2026
Merged

MONGOID-5782 Add block-based Timeless API to fix cascaded timestamp leak#6165
jamis merged 4 commits into
mongodb:masterfrom
jamis:5782-timeless-leak

Conversation

@jamis

@jamis jamis commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Mongoid::Timestamps::Timeless skips created_at/updated_at writes 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 a timeless save 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 their updated_at gets bumped anyway. That is the leak reported in MONGOID-5782.

The fix

Introduce a block-based API whose scope is explicit and deterministic:

person.timeless { person.save }

Person.timeless do
  Person.create(title: "Sir")
end

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_option now uses the block form internally, so save(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 via Mongoid::Deprecation. It is slated for removal in Mongoid 10.0. Migration is mechanical:
x.timeless.save becomes x.timeless { x.save }.

Testing

  • Block-API coverage: instance/class forms, return value, resumption after the block, nesting, exception-safety, and global-on-thread scope.
  • Deprecation coverage: chained form warns, block form does not, and the internal touch: false path does not warn.
  • MONGOID-5782 regression: parent → embedded → nested embedded, all with 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 timeless method is deprecated in favor of a more robust block-based version.

# instead of the method chaining version
person.timeless.save!

# use the block-based version
person.timeless { person.save! }

The deprecated syntax will be removed in a future major release.

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.
@jamis
jamis requested a review from a team as a code owner July 9, 2026 17:07
@jamis
jamis requested review from comandeo-mongo and Copilot July 9, 2026 17:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 updates timeless? to honor both the new scope and legacy counters.
  • Deprecates chained/block-less timeless usage via Mongoid::Deprecation warnings.
  • Updates the internal touch: false update 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.

Comment thread lib/mongoid/timestamps/timeless.rb Outdated
Comment thread lib/mongoid/timestamps/timeless.rb
@jamis jamis added the feature Adds a new feature, without breaking compatibility label Jul 9, 2026
@jamis
jamis merged commit 7150a40 into mongodb:master Jul 20, 2026
76 checks passed
@jamis
jamis deleted the 5782-timeless-leak branch July 20, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds a new feature, without breaking compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants