AL+

Language: RO (română) EN (English)

Email us: paul@alplustech.com

Rails CurrentAttributes needs a lifecycle that clears incidental state

Rails changed CurrentAttributes cleanup in June 2025. Separate declared request state from incidental instance variables and give shared caches another owner.

Citiți articolul în română

Request-local state needs a defined end. If a current-user object forgets its declared user attribute but retains an incidental cached value derived from that user, cleanup has not fulfilled the application's isolation contract. Rails' June 2025 CurrentAttributes change addressed that gap by abandoning instances at lifecycle boundaries rather than relying only on resetting their declared attributes.

Looking back from November 2026, the design lesson is to keep request state explicit and put deliberately shared caches under a separate owner. A convenient place to find the current user should not quietly become a store for values with several different lifetimes. Those lifetimes affect correctness even when every individual query is valid.

The discussion followed the implementation

The Rails 6 June 2025 weekly update highlighted regular instance variables that could leak between requests. The underlying pull request merged on 2 June. The weekly post discussed work already merged into main; it did not establish that every released Rails version already included it.

The diff changes executor entry and completion hooks to call clear_all. That operation resets existing instances and clears their registry. Later access obtains a fresh instance. This is more specific than saying that every call to an individual object's reset now erases all its Ruby instance variables.

For an application decision, establish the behavior of the installed Rails version and the execution wrapper actually in use. Custom execution outside the usual request lifecycle needs its own scrutiny. The historical merge is evidence of the change, rather than a substitute for checking where the deployed application creates and ends its execution context.

Two requests expose the ownership mistake

Consider a proposed regression scenario, not a test run for this article. One request identifies user A and computes a value from that user's account. The computation stores the result in an ordinary instance variable on the CurrentAttributes instance. The lifecycle then resets declared attributes, and another request on the reused execution context identifies user B.

If the same object remains and its incidental variable is not cleared, a memoized accessor can return A's derived value during B's request. The user attribute itself can be correct while the derived value is wrong. Looking only at the declared current user would therefore miss the relevant failure.

The exact consequence depends on what the cached value controls. A stale display preference and a stale account selection have different severity. Neither should be assumed from the mechanism alone. Review the values the application actually derives and the actions that consume them before describing the business risk.

This also explains why a test should observe the derived result. Asserting that the current-user attribute becomes empty confirms only one part of the contract. The important assertion is that the second request receives its own result, including after the first request takes an error path.

Declare short-lived state deliberately

Declared attributes make intended request state visible to maintainers and participate in the framework's attribute reset behavior. That is a reason to use them for values whose lifecycle matches the current context. It does not mean every convenient piece of application data belongs in a globally accessible Current class.

A small explicit set is easier to reason about. If a method receives all the values it needs directly, introducing request state may add hidden dependencies without a useful benefit. When CurrentAttributes does serve a clear purpose, document that purpose in the interface and keep derived values consistent with their declared inputs.

Resetting a reference also does not destroy an object that another part of the application still holds. If a mutable object escapes into a long-lived service, clearing the CurrentAttributes registry cannot revoke the other reference. The lifecycle change narrows a specific retention path. It is not a universal guarantee that request data cannot escape anywhere else.

The same reasoning applies to jobs. Decide which identity a job should use and establish it inside that job's actual execution lifecycle. A job running later cannot safely infer its business authority merely from whatever state happened to be present when it was scheduled.

Give shared caches a different home

The strongest counterargument is performance. Reusing a computed value can avoid repeated work, and a team may intentionally want data to survive across requests. Discarding every object at the end of a request would defeat that purpose if the object were actually meant to be shared.

That is a reason to separate owners. A shared cache needs an explicit key, invalidation behavior and concurrency assumptions. If a value varies by account, those assumptions need to preserve that distinction. Hiding the cache inside a request-state object makes it difficult to see whether survival is intended or accidental.

For a value needed only within one request, recomputation or explicit declared state may be enough. For a costly shared value, use a component designed for that lifetime. The decision should follow the meaning and cost of the value, rather than a blanket ban on caching or a blanket preference for memoization.

Verify the real boundary

A bounded regression check should execute two distinct identities through the application's real lifecycle on a reused execution context. Observe the consequential derived value, then repeat with a failure in the first operation. Keep the test focused on the retention risk instead of asserting private registry details that may change.

For software maintenance, ask which values belong to a request, which are deliberately shared and where cleanup occurs in the deployed version. This is a state-lifetime problem. Correct cleanup does not replace authorization, and a successful ordinary request does not prove that incidental state is gone before the next one begins.