Skip to content

perf: cache A2uiValidator instance on A2uiCatalog.validator property#1972

Open
brucearctor wants to merge 2 commits into
a2ui-project:mainfrom
brucearctor:fix/cache-validator-for-async-contexts
Open

perf: cache A2uiValidator instance on A2uiCatalog.validator property#1972
brucearctor wants to merge 2 commits into
a2ui-project:mainfrom
brucearctor:fix/cache-validator-for-async-contexts

Conversation

@brucearctor

@brucearctor brucearctor commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Cache the A2uiValidator instance returned by A2uiCatalog.validator using functools.cached_property to avoid reconstructing it on every access.

Motivation

A2uiCatalog.validator was a @property that created a new A2uiValidator (including its delegated A2uiValidatorWrapperV10, Draft202012Validator, Registry, and FormatChecker) on every call. Since A2uiCatalog is a frozen dataclass, the validator for a given catalog is deterministic and can be safely cached.

Relationship to #1971

Issue #1971 reported blocking I/O during validator construction that hung Temporal workflows. That root cause has already been resolved on main — the os.path file I/O calls in the referencing/registry chain were replaced with urllib.parse.urljoin string operations, and Registry is now built entirely from in-memory schemas via Resource.from_contents().

Verified empirically: patching builtins.open, os.path.exists, os.path.isfile, os.path.isdir, os.path.abspath, and os.path.realpath during validator construction and validation confirms zero file I/O operations on current main.

This PR is scoped as a pure performance optimization — avoiding repeated construction of the validator, registry, and format checker objects on every .validator access.

Changes

agent_sdks/python/a2ui_agent/src/a2ui/schema/catalog.py

  • Replace @property with @cached_property on A2uiCatalog.validator
  • Add from functools import cached_property import
  • cached_property writes directly to __dict__, bypassing __setattr__ on the frozen dataclass — no global state, no memory leaks, no id()-reuse hazards

agent_sdks/python/a2ui_agent/tests/schema/test_validator_caching.py (new)

7 tests covering:

  • Cache identity (same instance returned on repeated access)
  • Cache invalidation (clearing cached_property creates new instance)
  • Import restriction survival (cached validator works with referencing blocked)
  • Valid payload handling on cached validator
  • Invalid payload rejection on cached validator (using pytest.raises)
  • Cache isolation (different catalog instances get different validators)
  • Multiple validate calls reuse same instance

Test Results

438 passed (existing + 7 new), 0 failed

Refs #1971

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces caching for A2uiValidator instances on A2uiCatalog to avoid redundant construction and blocking I/O in async contexts like Temporal workflows, accompanied by a comprehensive test suite. The reviewer identified a critical memory leak issue caused by the global _VALIDATOR_CACHE dictionary holding strong references to catalog instances via id(self). To resolve this, the reviewer recommended replacing the global cache with Python's standard functools.cached_property on the A2uiCatalog class, which naturally handles garbage collection. Additionally, the reviewer suggested updating the test helper to clear the cached property using object.__delattr__ and refactoring a generic try-except block in the tests to use pytest.raises for more robust error handling.

Comment on lines +146 to +150
# Module-level cache for A2uiValidator instances, keyed by catalog id().
# Avoids re-constructing the jsonschema Registry (which does os.path I/O)
# on every .validator access. Critical for async contexts like Temporal
# workflows where blocking I/O halts the event loop.
_VALIDATOR_CACHE: Dict[int, Any] = {}

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.

high

Using a global dictionary _VALIDATOR_CACHE to cache validators by id(self) introduces a severe memory leak. Because _VALIDATOR_CACHE holds a strong reference to the A2uiValidator instance, and the validator holds a strong reference to the A2uiCatalog instance, neither the validator nor the catalog can ever be garbage collected. In long-running applications (such as Temporal workers or web servers) where catalogs are dynamically loaded or pruned, this will lead to an eventual Out Of Memory (OOM) error.

Instead, we can use functools.cached_property, which is the standard, idiomatic Python way to cache properties on objects. It works perfectly on frozen dataclasses (since they have a __dict__ and cached_property bypasses __setattr__ by writing directly to __dict__). This ensures that the cached validator is garbage collected along with the catalog instance.

Suggested change
# Module-level cache for A2uiValidator instances, keyed by catalog id().
# Avoids re-constructing the jsonschema Registry (which does os.path I/O)
# on every .validator access. Critical for async contexts like Temporal
# workflows where blocking I/O halts the event loop.
_VALIDATOR_CACHE: Dict[int, Any] = {}
from functools import cached_property

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

WDYT @nan-yu?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

went ahead and addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ditman @nan-yu — addressed all review feedback. The global _VALIDATOR_CACHE dict has been replaced with functools.cached_property, which eliminates the memory leak and id()-reuse hazard.

Also rebased on current main, updated the PR description to properly scope this as a performance optimization (the blocking I/O from #1971 was already resolved upstream — verified empirically), and updated tests to use TransportFormat instead of the deprecated A2uiSchemaManager.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR looks great, thanks!

Just as a heads-up: we have a major refactor coming up for Catalog and Validators. We're moving away from caching the validator inside the A2uiCatalog class. Instead, developers will need to instantiate the validator independently, which will give them complete control over the validator instance.

Comment thread agent_sdks/python/a2ui_agent/src/a2ui/schema/catalog.py Outdated
Comment thread agent_sdks/python/a2ui_agent/tests/schema/test_validator_caching.py Outdated
Comment thread agent_sdks/python/a2ui_agent/tests/schema/test_validator_caching.py Outdated
@github-actions github-actions Bot added the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 10, 2026
@github-actions github-actions Bot removed the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 21, 2026
@brucearctor

Copy link
Copy Markdown
Contributor Author

I just noticed comment, which then alerted me to the gemini review. Can address.

@brucearctor
brucearctor force-pushed the fix/cache-validator-for-async-contexts branch from 4bc13ca to bd193d9 Compare July 21, 2026 03:46
@brucearctor

Copy link
Copy Markdown
Contributor Author

Looks like some things for me to cleanup

@brucearctor
brucearctor force-pushed the fix/cache-validator-for-async-contexts branch from bd193d9 to 69fa97d Compare July 21, 2026 17:47
@github-actions github-actions Bot added the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 22, 2026
@polina-c polina-c added status: waiting-for-user-response and removed status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs labels Jul 22, 2026
@github-actions github-actions Bot added the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 22, 2026
@brucearctor

Copy link
Copy Markdown
Contributor Author

I think this is OK now?

@github-actions github-actions Bot removed the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 23, 2026
@ditman
ditman requested a review from nan-yu July 23, 2026 18:22
@ditman

ditman commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Can you PTAL when you have some time @nan-yu?

@ditman

ditman commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

@brucearctor one last thing I forgot to point out! Do you mind adding a short entry under "Unreleased" on the CHANGELOG.md of the package?

https://github.com/a2ui-project/a2ui/blob/main/agent_sdks/python/a2ui_agent/CHANGELOG.md

That way our on-call person will know that there's changes that need to be released to pypi.

Thanks again, and apologies for the delay!

@brucearctor
brucearctor force-pushed the fix/cache-validator-for-async-contexts branch from 85cec49 to f9aee2e Compare July 24, 2026 20:51
@brucearctor

Copy link
Copy Markdown
Contributor Author

Done — added a CHANGELOG entry under "Unreleased". Thanks for the reminder, and no worries on the delay!

A2uiCatalog.validator is a @Property that creates a new A2uiValidator
on every access. This is wasteful — the validator for a given catalog is
deterministic and can be safely cached.

Use functools.cached_property to cache the validator directly on the
instance. This is the idiomatic Python approach for frozen dataclasses:
cached_property writes to __dict__ directly, bypassing __setattr__,
and ties cache lifetime to the object — no global state, no memory
leaks, no id()-reuse hazards.

Includes 7 new tests covering:
- Cache identity (same instance returned)
- Cache isolation (different catalogs get different validators)
- Import restriction survival
- Valid/invalid payload handling on cached validators

Refs a2ui-project#1971
@brucearctor
brucearctor force-pushed the fix/cache-validator-for-async-contexts branch from f9aee2e to 24e9562 Compare July 25, 2026 02:35
@github-actions github-actions Bot added status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs and removed status: waiting-for-user-response labels Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants