Skip to content

fix: Guard EventParser.lastEventId against a data race#108

Merged
keelerm84 merged 2 commits into
4.xfrom
mk/SDK-2633/get-last-event-id-race
Jul 9, 2026
Merged

fix: Guard EventParser.lastEventId against a data race#108
keelerm84 merged 2 commits into
4.xfrom
mk/SDK-2633/get-last-event-id-race

Conversation

@keelerm84

@keelerm84 keelerm84 commented Jul 1, 2026

Copy link
Copy Markdown
Member

getLastEventId() reads EventParser.lastEventId from arbitrary caller threads (via
EventSource.getLastEventId()), while it is written on the serialized delegate
queue during event dispatch. Reading a String concurrently with a write is
undefined behavior (a torn copy-on-write buffer), not merely a stale value; the
@unchecked Sendable delegate suppressed the compiler check.

Guard the field with an NSLock: lastEventId is now a computed property whose get
and set lock a private backing store, so the cross-thread read is safe regardless
of the writing thread. parse()/reset()/dispatch remain serialized on the delegate
queue and are unchanged. Mark EventParser @unchecked Sendable to document that
split (serialized mutation + lock-guarded cross-thread read).

Adds a concurrency test that hammers getLastEventId() from one task while another
drives events, asserting correctness under load. Run under --sanitize=thread to
catch a regression (TSan is unavailable on the Linux toolchain here; the macOS CI
leg is TSan-capable).


Note

Medium Risk
Touches hot-path SSE state read from arbitrary threads; the lock is narrow but incorrect locking could still affect reconnect Last-Event-Id behavior under load.

Overview
Fixes a data race on EventParser.lastEventId: getLastEventId() can run on caller threads (via EventSource) while dispatch updates the field on the serialized delegate queue. Concurrent String read/write is undefined behavior, not just stale values.

lastEventId is now backed by _lastEventId behind an NSLock via a computed property; parse/reset/dispatch stay on the delegate queue. EventParser is @unchecked Sendable with comments describing that split.

Adds getLastEventIdIsSafeDuringConcurrentParsing (writer parses events, reader hammers getLastEventId()). CI gains test-thread-sanitizer on Ubuntu (swift:6.1) running swift test --sanitize=thread with TSAN_OPTIONS=halt_on_error=1 so races fail the job.

Reviewed by Cursor Bugbot for commit efa6daf. Bugbot is set up for automated code reviews on this repo. Configure here.

getLastEventId() reads EventParser.lastEventId from arbitrary caller threads (via
EventSource.getLastEventId()), while it is written on the serialized delegate
queue during event dispatch. Reading a String concurrently with a write is
undefined behavior (a torn copy-on-write buffer), not merely a stale value; the
@unchecked Sendable delegate suppressed the compiler check.

Guard the field with an NSLock: lastEventId is now a computed property whose get
and set lock a private backing store, so the cross-thread read is safe regardless
of the writing thread. parse()/reset()/dispatch remain serialized on the delegate
queue and are unchanged. Mark EventParser @unchecked Sendable to document that
split (serialized mutation + lock-guarded cross-thread read).

Adds a concurrency test that hammers getLastEventId() from one task while another
drives events, asserting correctness under load. Run under `--sanitize=thread` to
catch a regression (TSan is unavailable on the Linux toolchain here; the macOS CI
leg is TSan-capable).
@keelerm84 keelerm84 requested a review from a team as a code owner July 1, 2026 17:58
@keelerm84 keelerm84 force-pushed the mk/SDK-2633/get-last-event-id-race branch 2 times, most recently from 3845aa0 to b8cd872 Compare July 1, 2026 18:43
Runs the suite under `swift test --sanitize=thread` on macOS so data races (like
the getLastEventId one this fixes) are caught in CI. Sets TSAN_OPTIONS=halt_on_error=1
so a detected race aborts with a non-zero exit and fails the job — without it TSan
only prints a warning and the step stays green.

The Linux toolchain doesn't support -sanitize=thread, so this macOS job is where
the getLastEventIdIsSafeDuringConcurrentParsing test's race coverage actually runs.
@keelerm84 keelerm84 force-pushed the mk/SDK-2633/get-last-event-id-race branch from b8cd872 to efa6daf Compare July 1, 2026 18:51
@keelerm84 keelerm84 merged commit 0dc0c44 into 4.x Jul 9, 2026
16 checks passed
@keelerm84 keelerm84 deleted the mk/SDK-2633/get-last-event-id-race branch July 9, 2026 13:27
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.

2 participants