Repository navigation
fix(hindsight): guard client lifecycle with a leaf lock - #117236
Closed
yingliang-zhang wants to merge 2 commits into
Closed
yingliang-zhang wants to merge 2 commits into
yingliang-zhang wants to merge 2 commits into
Conversation
…ration (NousResearch#64745) The background prefetch worker published its recall into the session slot unconditionally; a worker outliving on_session_switch's 3s join wrote the old session's memories into the new session's slot. queue_prefetch also spawned unbounded threads with the last finisher winning the slot. Workers now capture a slot generation at spawn, queue_prefetch bumps it and skips while a prior worker runs, on_session_switch/shutdown bump it to fence late publishers, and the publish + recall are gated on the current generation.
Collaborator
|
Closing: the bundled Hindsight provider this PR patches has moved out of this repo. Thanks @yingliang-zhang for this contribution. In #119888 (merge Triage notes:
If you believe this was closed in error, comment and we will reopen. (Bulk-closed in the hindsight-move close pass.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HindsightMemoryProvider._clientwas read and written from at least three threads — the retain writer thread, the background prefetch worker, and the turn/tool thread — with no lock._get_client()is check-then-act (if self._client is None: self._client = <constructor>), and the embedded constructor takes seconds (runtime check, optional dependency install, daemon spawn). Two threads hitting a cold or just-nulled client both construct, one wins, and the loser'sHindsightEmbeddedis orphaned with an aiohttpClientSessionthat is never closed —HindsightEmbedded.__del__is deliberately neutered and_close_clientonly ever closed the current client. That orphan is a direct generator of theUnclosed client session/Unclosed connectornoise in #11923.The stale-daemon retry path in
_run_hindsight_operation(self._client = Nonethen_get_client()) widened the window from microseconds to seconds. The race pre-dates #64745 by five months (0ba6471dd1); #64745's single-prefetch-worker gate narrowed the prefetch side but leaves the writer, tool and daemon-start paths exposed.The fix (+43/−15 in one file)
self._client_lock, a leaf lock:_get_client()takes it only on the construction path; the fast path stays lock-free; the lock is never held across_run_sync/operation(client)and never nests with_prefetch_lockor_pending_retain_ops_lock._get_client(*, recreate=False)— double-checked construction;recreateretires the client only if it is still the one the caller observed as broken (identity CAS), so a sibling thread's fresh rebuild is returned as-is instead of being orphaned._broken_clientand rebuilds via the CAS. The loser is not closed inline (closing against a dead daemon can hang) — onlyshutdown()closes.shutdown()retires the client under the lock before closing it, so a concurrent_get_client()rebuilds instead of racing the close._close_client→_close_client_of(client)(parameterised; single caller, grep-verified).Tests
Three new tests in
tests/plugins/memory/test_hindsight_provider.py:test_client_created_once_under_concurrent_first_access— 8 threads against a 0.2 s stubbed factory; exactly one construction, one shared object.test_retry_does_not_orphan_a_sibling_client— retriable-failure retry with 4 concurrent readers against a tracking factory; exactly one replacement client, zero orphans.test_shutdown_closes_retired_client_and_allows_rebuild— retired client closed exactly once,_client is None, and a post-shutdown_get_client()rebuilds.Verification
tests/plugins/memory/test_hindsight_provider.pytests/plugins/memory/+tests/agent/test_memory_provider.py44e607be61(2 ×test_mem0_v3No module named 'mem0', 1 ×test_openviking_optional_peer— pre-existing, unrelated)_get_clientto check-then-act makestest_client_created_once_under_concurrent_first_accessRED (8 constructions); restoring makes it greengit diff --checkFollow-up from the #64745 rewrite. Independent of it — this is a client-lifecycle invariant, not a prefetch one — but the two are complementary: #64745 fences what gets published, this fences what gets built.