Fix PoseHeaderCache race under concurrent Pose.read#239
Merged
Conversation
PoseHeaderCache was global mutable state updated non-atomically: check_cache
could match the old hash while another thread's set_cache had already
replaced the header, and PoseHeader.read re-read end_offset after the check.
Concurrent Pose.read calls on files with different headers could crash
("buffer is too small for requested array") or silently parse a pose with
another file's header.
The cache is now an immutable (hash, header, start_offset, end_offset) tuple
swapped atomically; readers take a single snapshot. The legacy class
attributes are kept in sync for backward compatibility.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review: a plain threading.Lock over check_cache/set_cache/clear_cache is easier to follow than the snapshot tuple. check_cache still returns (header, end_offset) -- the offset must come from the same critical section, otherwise the caller re-reading the class attribute races again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
|
Reworked to a plain |
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
PoseHeaderCacheis global mutable state updated non-atomically, which makesPose.readunsafe to call from multiple threads when the files have different headers:PoseHeader.readre-readPoseHeaderCache.end_offsetaftercheck_cachereturned — a concurrentset_cachein between leaves the reader at another file's body offset.set_cacheassignedheader/offsets before computinghash, so a concurrentcheck_cachecould match the stale hash and return the newly-assigned wrong header.Downstream this is the intermittent
TypeError: buffer is too small for requested arrayseen in spoken-to-signed-translation CI (itslookup_sequencereads lexicon poses in aThreadPoolExecutor; see e.g. sign-language-processing/spoken-to-signed-translation#52 discussion). The nastier failure mode is silent: a pose parsed with a different file's header and no exception.Repro (6 lexicon files, 8 threads, 4,800 reads): 122 exceptions + 153 silently-wrong headers before the fix; 0 + 0 after.
Fix
The authoritative cache is now a single immutable
(hash, header, start_offset, end_offset)tuple swapped atomically (CPython attribute assignment); readers take one snapshot and use only it. No locks needed. The legacy class attributes are kept in sync for backward compatibility (Pose.readusesend_offsetas a read-ahead hint, where a torn read is harmless).Test plan
test_concurrent_read_of_different_files_is_safe(fails in <1s on the old code, passes with the fix)pytest tests/pose_test.py pose_format/utils/reader_test.py— all pass (except pre-existingtest_unpack_tensorflow, which needs tensorflow installed)🤖 Generated with Claude Code