Skip to content

Refactor SchemaValidator caches to be per-resolver#1185

Open
p1c2u wants to merge 1 commit into
masterfrom
refactor/per-resolver-validator-cache
Open

Refactor SchemaValidator caches to be per-resolver#1185
p1c2u wants to merge 1 commit into
masterfrom
refactor/per-resolver-validator-cache

Conversation

@p1c2u
Copy link
Copy Markdown
Collaborator

@p1c2u p1c2u commented May 16, 2026

The class-level _needs_state_cache was keyed on SchemaPath, but SchemaPath equality (inherited from pathable.BasePath) is path-only: two distinct OpenAPI specs that share a JSON-pointer path collide, returning stale answers across specs. The bug is silent in production because validation typically runs against a single spec at a time, but bites in any host that loads more than one spec, and in test suites where fresh SchemaPath.from_dict() calls produce short colliding paths.

Replace the cache with a per-resolver registry (one cache per loaded spec), keyed on the resolver's identity and evicted via weakref.finalize when the resolver is garbage-collected. Inner cache keys are id(content_dict), which is safe within a single spec (the cache only lives as long as the resolver does, so id() reuse cannot cross spec boundaries).

Perf-neutral on the bench (357 vs 348 ops/sec, within noise) because _schema_needs_state is only consulted during state-building, not on the per-value hot path.

Adds a regression test that two specs with colliding paths return correct independent answers, and a GC test that the per-resolver cache slot is released when the spec is collected.

The class-level _needs_state_cache was keyed on SchemaPath, but
SchemaPath equality (inherited from pathable.BasePath) is path-only:
two distinct OpenAPI specs that share a JSON-pointer path collide,
returning stale answers across specs. The bug is silent in production
because validation typically runs against a single spec at a time,
but bites in any host that loads more than one spec, and in test
suites where fresh SchemaPath.from_dict() calls produce short
colliding paths.

Replace the cache with a per-resolver registry (one cache per loaded
spec), keyed on the resolver's identity and evicted via
weakref.finalize when the resolver is garbage-collected. Inner cache
keys are id(content_dict), which is safe within a single spec (the
cache only lives as long as the resolver does, so id() reuse cannot
cross spec boundaries).

Perf-neutral on the bench (357 vs 348 ops/sec, within noise) because
_schema_needs_state is only consulted during state-building, not on
the per-value hot path.

Adds a regression test that two specs with colliding paths return
correct independent answers, and a GC test that the per-resolver
cache slot is released when the spec is collected.
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