Repository navigation
perf: cache SDK version metadata - #302
Linxiushen wants to merge 11 commits into
Conversation
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Reviewed 8d8edf0 against main (df47679), with the repo's AGENTS.md guidance and the changed files in the workspace.
What the change does
get_sdk_version() now memoizes importlib.metadata.version("openhands-sdk") process-wide via @lru_cache(maxsize=1), plus a new regression test in tests/test_version.py.
Material finding: the cache is never invalidated, which silently breaks the existing "SDK not installed" contract
get_server_version_info() exists specifically to degrade gracefully when package metadata is unavailable, and /sdk-version plus /server_info are specified to return 503 in that case (openhands/automation/app.py:288, :302, both wrapped in except PackageNotFoundError). Two existing tests encode that contract: tests/test_health.py::TestSdkVersionEndpoint::test_returns_503_when_package_not_found and tests/test_health.py::TestServerInfoEndpoint::test_returns_503_when_sdk_package_not_found. Both patch openhands.automation.utils.version.importlib.metadata.version to raise PackageNotFoundError.
With the process-lifetime cache, the first successful read permanently pins the value, so a later PackageNotFoundError never reaches the except clause. I reproduced this two ways in the workspace:
- Restoring the base file at
HEAD~1makestests/test_health.pypass 9/9; on this head it is7 passed, 2 failed(the two 503 tests, both returning200instead of503). - Directly: call
get_sdk_version()once (1.39.1), then swapimportlib.metadata.versionfor a function that raisesPackageNotFoundError; the second call still returns1.39.1from cache instead of raising.
The unit-level consequence is that the documented "package not found" -> 503 state is no longer reachable once anything has read the version. The merge-relevant consequence is that the test suite now fails on this head.
CI
On the exact head 8d8edf0, the Run tests, ci, Docker, and PR Artifacts workflow runs all completed with failure; only pr (PR title lint) passed. The 503 regressions above match the unit-test failures. These did not fail at df47679.
Suggested direction
Either keep the lookup cache-free, or make invalidation real (e.g. don't cache, or expose a way to refresh, and update callers and tests together so the not-installed state stays observable), and ensure tests/test_health.py - not just the new tests/test_version.py - is green before merge. The new test clears the cache in both directions, which is good practice, but it also masks the fact that in production nothing ever calls cache_clear().
The +24 lines are otherwise straightforward and the perf intent (avoid a synchronous metadata lookup per telemetry event) is reasonable; the blocker is solely the lost PackageNotFoundError handling and the resulting red test suite.
🔄 CHANGES REQUESTED
| sdk_version: str | ||
|
|
||
|
|
||
| @lru_cache(maxsize=1) |
There was a problem hiding this comment.
This process-lifetime cache makes the PackageNotFoundError path in get_server_version_info() (lines 26-31) unreachable after the first successful read, so /sdk-version (app.py:288) and /server_info (app.py:302) can no longer return their documented 503 "openhands-sdk package not found" response.
Confirmed on this head: uv run pytest tests/test_health.py -q gives 7 passed, 2 failed, both failures being test_returns_503_when_package_not_found (returns 200 instead of 503). The same file passes 9/9 when the pre-PR version of this module is restored, so the regression is introduced by the @lru_cache decorator. Either make the cached value invalidatable / avoid pinning a stale result, or update these callers and tests together so the not-installed state remains representable.
adadd04 to
1b86e28
Compare
|
Addressed in 1b86e28: Verified: |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Reviewed head 1b86e28 against main, applying the repo's AGENTS.md guidance to the changed files, and re-read the linked issue #289 (open, labeled enhancement).
Scope
This is a perf change to the telemetry path of the automation service itself and belongs in this repository. No product/architecture decision is required.
What changed
The earlier revision cached the lookup inside get_sdk_version() and broke the documented "SDK not installed" contract on /sdk-version and /server_info. This revision takes the lower-risk approach: it reverts openhands/automation/utils/version.py to the cache-free version and confines the memoization to telemetry via a private _installed_sdk_version() (@lru_cache(maxsize=1)) plus _server_version_properties() in telemetry.py. That directly addresses the reported issue — _base_properties() no longer does an uncached importlib.metadata filesystem scan per captured event — while leaving the version helpers' error semantics untouched.
Verification on the current head
tests/test_health.pyandtests/test_telemetry.py: 26 passed. The previously failingTestSdkVersionEndpoint/TestServerInfoEndpoint503 cases now pass, and/sdk-version+/server_infostill surfacePackageNotFoundErroras 503.- The two new telemetry tests are meaningful and cover the important edge:
lru_cachedoes not memoize exceptions, so a missing SDK is not pinned and a later successful lookup is still observed. That is the correct behavior for "unknown" degradation. ruff checkandruff format --checkclean on the changed files.- GitHub Actions on the exact head:
Run tests,ci,Docker,pr,PR Artifacts, andPR Description Checkall completedsuccess; the unit-tests check run is green.
Note: an unrelated tests/test_capabilities_router.py error appeared in my local run because this sandbox has no Docker daemon; that test is not part of this diff and its check run is green in CI.
Minor, non-blocking
_server_version_properties() can call get_sdk_version() twice in the rare missing-SDK path (once via _installed_sdk_version(), once via get_server_version_info()). This is a cold, exceptional path and has no measurable impact, so it does not block merge.
No material bugs, security problems, or design flaws found on this head.
✅ APPROVED
Move the memoization out of get_sdk_version() so /sdk-version and /server_info still observe PackageNotFoundError and return 503. The telemetry emitter caches successful get_sdk_version() results via lru_cache, which does not memoize exceptions, so a missing SDK is never pinned as the cached value; package_version is still read live.
1b86e28 to
a889eb4
Compare
|
I have brought this PR, #304, and #305 up to main at 356b3ad with ordinary merge commits, preserving each original fix and its regression tests. The descriptions now reflect the current implementation and validation. All three currently show APPROVED / CLEAN, with the required unit-tests and full pre-commit CI passing on these exact heads:
Could a maintainer take the final merge pass when convenient? The previous behind-main gate is now cleared. This maintenance and validation follow-up was performed with AI coding assistance. |
enyst
left a comment
There was a problem hiding this comment.
I'm an AI agent (Claude, based on Opus 5.5) helping Engel Nyst (@enyst) with project work.
Approve. I agree with all-hands-bot's approval of this head, and I found nothing new that blocks it.
Since that review, main has gained only #549 (the SDK 1.51.0 bump). This branch merges cleanly with it. It also merges cleanly with #304, which edits the same two files: the result is the same tree in either order. If the up-to-date rule still applies, it needs one more update-branch.
On cache lifetime: _installed_sdk_version() caches for the life of the process. That matches how the SDK is loaded. The image pins it and imports it once, so a new SDK version means a new image and a new process with an empty cache.
Telemetry performs a synchronous SDK package-metadata lookup for each event. This change caches successful SDK-version reads within telemetry while preserving the uncached
/sdk-versionand/server_infoendpoints and their HTTP 503 response when SDK metadata is missing. Exceptions and theunknownfallback are not cached. Fixes #289.The branch includes upstream main at
b43294c09eb439f2e6f4045761fae9b43d8664acthrough an ordinary merge to satisfy the required up-to-date check. The original PR patch is unchanged (verified by stable patch ID and source/test blob comparison).Validation for current head
55d889c6ab10dfa95e1952fb62bd88d5cb3064e9:Disclosure: this follow-up, review, and validation were performed by an AI coding agent. No human-testing attestation is implied.