Skip to content

Fix PoseHeaderCache race under concurrent Pose.read#239

Merged
AmitMY merged 2 commits into
masterfrom
fix-header-cache-race
Jul 23, 2026
Merged

Fix PoseHeaderCache race under concurrent Pose.read#239
AmitMY merged 2 commits into
masterfrom
fix-header-cache-race

Conversation

@AmitMY

@AmitMY AmitMY commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

PoseHeaderCache is global mutable state updated non-atomically, which makes Pose.read unsafe to call from multiple threads when the files have different headers:

  • PoseHeader.read re-read PoseHeaderCache.end_offset after check_cache returned — a concurrent set_cache in between leaves the reader at another file's body offset.
  • set_cache assigned header/offsets before computing hash, so a concurrent check_cache could match the stale hash and return the newly-assigned wrong header.

Downstream this is the intermittent TypeError: buffer is too small for requested array seen in spoken-to-signed-translation CI (its lookup_sequence reads lexicon poses in a ThreadPoolExecutor; 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.read uses end_offset as a read-ahead hint, where a torn read is harmless).

Test plan

  • New regression test 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-existing test_unpack_tensorflow, which needs tensorflow installed)
  • External repro script against this branch: 0 failures in 4,800 concurrent reads

🤖 Generated with Claude Code

autoresearch and others added 2 commits July 23, 2026 08:05
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>
@AmitMY

AmitMY commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Reworked to a plain threading.Lock as suggested (stdlib, so no new wrapt dependency). One subtlety: the lock can't only guard set_cache/clear_cache — the reader is the other half of the race. check_cache reads hash/start_offset/end_offset/header as four separate attribute reads, and PoseHeader.read then re-read end_offset after the check, so a locked writer can still be observed mid-update by an unlocked reader. So check_cache takes the same lock and returns (header, end_offset) from inside the critical section. Re-verified: the concurrency regression test passes and the external repro is clean (0 failures in 4,800 concurrent reads; 122 exceptions + 153 wrong headers before the fix).

@AmitMY
AmitMY merged commit 7a36fcf into master Jul 23, 2026
8 checks passed
@AmitMY
AmitMY deleted the fix-header-cache-race branch July 23, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant