Files
bitchat/bitchatTests/Services
jackandClaude Fable 5 a55a16adcd Fix identity-cache save race and deinit deadlock in forceSave
The previous commit moved cryptographicIdentities into the persisted
IdentityCache, which made the pre-existing off-queue cache access in
the save path fatal instead of merely racy: forceSave() -> performSave()
read and JSON-encoded `cache` on the caller's thread while a concurrent
`queue.async(.barrier)` writer mutated the same dictionary. ThreadSanitizer
flags this as a data race in saveIdentityCache(), and because JSONEncoder
walks the dictionary storage, an interleaved mutation can spin forever —
which is what hung the CI "Run Swift Tests (app)" job (killed at the
watchdog timeout).

The naive fix (snapshot `cache` under `queue.sync` in forceSave) instead
introduced a deadlock: forceSave() is reachable from deinit, and the
object's final release can run *on* the identity queue (the fire-and-forget
barrier saves capture self), so queue.sync there is a re-entrant same-queue
wait -> SIGTRAP. That matched the intermittent crash the hang investigation
surfaced.

Fix:
- Split persistence into persist(snapshot:) which encodes/seals/writes a
  by-value IdentityCache snapshot, decoupled from reading `cache`.
- saveIdentityCache() (only ever called inside a barrier writer) passes
  `cache` directly — already serialized, race-free.
- forceSave() now snapshots + persists inside `queue.async(flags:.barrier)`
  (async, never sync): the read is on the barrier context so it never races
  an in-flight write, and being async it can't deadlock when invoked from
  deinit running on the queue. Durability is unaffected: every mutating API
  already persists inline within its own barrier, so forceSave is a
  belt-and-suspenders flush.

Test: test_concurrentUpsertsAndForceSaveDoNotRaceOrHang hammers concurrent
upserts interleaved with forceSave; it reproduces the data race under
`--sanitize=thread` on the old code (SecureIdentityStateManager.swift:250)
and passes cleanly with the fix. A lock-guarded LockedKeychain double is
used so the test exercises the manager's own cache race rather than the
non-thread-safe MockKeychain. Verified green over repeated
`swift test --parallel` runs both with and without --enable-code-coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 00:25:14 +02:00
..