Skip to content

Reuse one AttributesIterator per validator; drop Enumerable#2726

Open
ericproulx wants to merge 1 commit into
masterfrom
refactor/stateless-attributes-iterator
Open

Reuse one AttributesIterator per validator; drop Enumerable#2726
ericproulx wants to merge 1 commit into
masterfrom
refactor/stateless-attributes-iterator

Conversation

@ericproulx
Copy link
Copy Markdown
Contributor

Summary

AttributesIterator's attrs/scope are static per validator; only params varies per request. Yet a fresh iterator was allocated per validator per request in validate!, with the request-derived state (@original_params/@params) computed in the constructor.

This builds the iterator once in Base#initialize (frozen) and threads params through #each, so the instance carries no per-request state and can be reused. It also drops the unused Enumerable mixin.

What changed

  • AttributesIterator
    • Removed include Enumerable — dead and misleading: #each never returned an Enumerator (no enum_for fallback) and nothing in the codebase used map/select/to_a/etc. Only #each is ever called.
    • Removed attr_reader :scope — zero callers in lib/ or spec/.
    • Constructor is now (attrs, scope). params flows through each(params)do_each(..., original_params, ...) as an argument instead of the @original_params/@params ivars.
  • Base — overridable private iterator_class (default SingleAttributeIterator); @iterator = iterator_class.new(@attrs, @scope).freeze built in #initialize; #validate! uses @iterator.each(params).
  • MultipleParamsBase — overrides iterator_classMultipleAttributesIterator; #validate! now builds array_errors lazily (nil||= []) like Base, avoiding an empty-array allocation on the no-error path.
  • DefaultValidator — uses the shared @iterator.
  • Iterator specs updated to new(attrs, scope) + each(params, &b).

The reused instance holds only the static, frozen @attrs/@scope and writes no ivars during traversal — safe under Grape's frozen, thread-shared validators.

Benchmark

Real pre-change implementation (from master, identical do_each machinery) vs. the new one, one validator traversal:

i/s memory objects
before: new per request 1.22M 280 B 6
after: reuse instance 1.31M 200 B 5

Speed is neutral (ips delta within noise — scope.params/Array.wrap still run per request, just in each instead of the ctor). The reliable win is one fewer object (~80 B) per validator per request — a GC-pressure reduction that scales with #validators × throughput, in the spirit of #2689/#2690. This is an allocation/clarity refactor, not a throughput optimization.

Testing

  • spec/grape/validations + spec/grape/endpoint_spec.rb + spec/grape/api_spec.rb: 0 failures
  • RuboCop: clean

🤖 Generated with Claude Code

AttributesIterator's attrs/scope are static per validator; only params
varies per request. Build the iterator once in Base#initialize (frozen)
via an overridable iterator_class, and thread params through #each
instead of storing request-derived state in @original_params/@params.
The reused instance is shared across threads, so it must hold no
mutable per-request state.

- Remove `include Enumerable` (dead + misleading: #each never returned
  an Enumerator and nothing used map/select/etc.) and the unused
  `attr_reader :scope`.
- MultipleParamsBase#validate! now lazily builds array_errors
  (`nil` -> `||= []`) like Base, avoiding an empty-array allocation
  on the no-error path.

Allocation: one fewer object (~80 B) per validator per request;
speed-neutral. Full validation/endpoint suites green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the refactor/stateless-attributes-iterator branch from b7da330 to a55438b Compare May 16, 2026 10:50
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

Danger Report

No issues found.

View run

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