You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
get_named_validator returns one validator shared across threads; concurrent validation fails with _RefResolutionError #1434
adcp.validation.schema_loader.get_named_validator caches one Draft7Validator per schema, built on jsonschema's legacy RefResolver, and returns that same instance to every caller. RefResolver keeps a mutable scope stack: following a $ref into another document pushes that document's scope, and leaving pops it. When two threads validate with the shared validator at the same time, one thread can resolve #/definitions/... against the other thread's document and fail with _RefResolutionError: Unresolvable JSON pointer.
Run on a single thread, the same declaration validates with no errors.
Impact
Any threaded server that validates with get_named_validator (Flask, or WSGI under a threaded worker) fails intermittently under concurrent requests. We hit it in production-like traffic on a management API that validates overlay_support declarations.
The task validators in schema_loader are built and cached the same way (state.compiled plus RefResolver). We haven't reproduced it there, but they may share the problem.
Suggested fix
Any of these would work:
Build the validator on referencing.Registry (jsonschema ≥ 4.18), which keeps no per-validation mutable state.
Cache only the schema and store, and return a validator with a fresh RefResolver per call.
Cache validators per thread.
Workaround
We build a validator per call, with its own resolver over the cached validator's schema, base URI and store (the store is only read):
Classification: Bug Bucket(s): validation (no validation label in repo — gap noted for maintainers) Status: ready-to-implement
What the experts said:
code-reviewer: Both get_named_validator (L765) and get_validator (L693) cache a Draft7Validator holding a shared mutable RefResolver._scopes_stack; concurrent iter_errors() corrupts the scope stack — blocker, fix both in one patch.
ad-tech-protocol-expert: Confirmed production blocker on all threaded WSGI stacks (Flask, gunicorn --threads, uWSGI enable-threads); fresh RefResolver per call is unconditionally correct since the scope stack is local to one traversal and the store is read-only; non-breaking semver patch.
My take: This is a hard crash under any concurrent validation workload, not an edge case. Both functions must be fixed together — patching only get_named_validator leaves the higher-traffic get_validator path (called on every validate_request / validate_response) broken.
Implementation scope:
File:src/adcp/validation/schema_loader.py only. Add a _ValidatorSpec(schema, base_uri, store, format_checker) dataclass; cache it in state.compiled / state.named_compiled instead of the validator. Have both get_* functions call spec.make() — _make_ref_resolver(…) + Draft7Validator(…) — on each return. Schema I/O and _reachable_schema_store graph walk stay cached; only the O(1) RefResolver + validator construction moves to call time. Preserve referrer=schema (not {}) in every fresh RefResolver — load-bearing for fragment-only $defs refs.
Test: Add a concurrent test (4+ threads × ≥100 iterations each, core/targeting-overlay-support.json) confirming zero _RefResolutionError exceptions.
Activity
bokelley commentedon Oct 7, 2026
Triage
Classification: Bug
Bucket(s): validation (no
validationlabel in repo — gap noted for maintainers)Status: ready-to-implement
What the experts said:
get_named_validator(L765) andget_validator(L693) cache aDraft7Validatorholding a shared mutableRefResolver._scopes_stack; concurrentiter_errors()corrupts the scope stack — blocker, fix both in one patch.--threads, uWSGIenable-threads); freshRefResolverper call is unconditionally correct since the scope stack is local to one traversal and the store is read-only; non-breaking semver patch.My take: This is a hard crash under any concurrent validation workload, not an edge case. Both functions must be fixed together — patching only
get_named_validatorleaves the higher-trafficget_validatorpath (called on everyvalidate_request/validate_response) broken.Implementation scope:
src/adcp/validation/schema_loader.pyonly. Add a_ValidatorSpec(schema, base_uri, store, format_checker)dataclass; cache it instate.compiled/state.named_compiledinstead of the validator. Have bothget_*functions callspec.make()—_make_ref_resolver(…)+Draft7Validator(…)— on each return. Schema I/O and_reachable_schema_storegraph walk stay cached; only the O(1) RefResolver + validator construction moves to call time. Preservereferrer=schema(not{}) in every freshRefResolver— load-bearing for fragment-only$defsrefs.core/targeting-overlay-support.json) confirming zero_RefResolutionErrorexceptions.pytest tests/·mypy src/·ruff check .·black --check .Draft7Validatorbehavior is identical; object identity across calls was never documented.Triaged by Claude Code. Session: https://claude.ai/code/session_015eyJqam8c1ZMC8TvovbLu4
Generated by Claude Code