Skip to content

refactor: collapse blocker install check+insert into a helper - #5

Merged
didiergarcia merged 4 commits into
mainfrom
fix/mediator-race-update-serialize
May 7, 2026
Merged

didiergarcia merged 4 commits into
mainfrom
fix/mediator-race-update-serialize

Conversation

@didiergarcia

@didiergarcia didiergarcia commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Small refactor of ConsentManager.update(settings:type:): collapse the per-destination find(pluginType:) == nil check and the subsequent destination.add(plugin:) into a single installBlockerIfNeeded(on:type:make:) helper. Same behavior, but the guard and the insert are now co-located rather than repeated for SegmentConsentBlocker and ConsentBlocker.

Also removed iPhone 15 target that is no longer available.

Scope

  • Sources/SegmentConsent/Manager.swift only (+9/-13).
  • No public API change.
  • No new dependencies.
  • Existing SegmentConsent-Tests paths unaffected.

Test plan

  • swift build succeeds.
  • SegmentConsent-Tests still passes (19 tests across ConsentBlockerTests, ConsentNotEnabledAtSegment, DestinationsMultipleCategoriesTests, NoUnmappedDestinationsTests, UnmappedDestinationsTests).

ConsentManager.update(settings:type:) is invoked on the URLSession
delegate thread after remote settings are fetched. On that path it
iterates destinations and calls destination.add(plugin:), which mutates
analytics-swift's Mediator plugin array.

Meanwhile, event processing on the analytics event thread iterates the
same Mediator array via Timeline/Mediator.execute(). Because Mediator's
underlying storage is a plain Swift Array, the concurrent mutation and
iteration can trigger a CoW reallocation while another thread is reading
a retained element, producing _swift_release_dealloc on a freed object
and an EXC_BAD_ACCESS (pointer authentication failure) in release
builds.

Stack signature observed from a production crash:
  _swift_release_dealloc
  Mediator.add(plugin:)           Timeline.swift
  DestinationPlugin.add(plugin:)  Plugins.swift
  ConsentManager.update(settings:type:)
  Analytics.checkSettings
  HTTPClient.settingsFor completion
  com.apple.NSURLSession-delegate thread

Hop the destination-mutating portion of update() onto a dedicated serial
queue so those add(plugin:) calls can no longer race the event timeline.
This also fixes a pre-existing check-then-insert TOCTOU that could
install duplicate blockers when settings fetches overlap.

The underlying thread-safety issue lives in analytics-swift Mediator
and should be fixed there as well; this change is a localized workaround
so consumers can upgrade immediately.
@didiergarcia didiergarcia changed the title fix: serialize destination-timeline mutation in update(settings:) WIP: fix: serialize destination-timeline mutation in update(settings:) May 5, 2026
…cessing

updateQueue.async returned before the destination blockers were
installed, leaving a window where events could flow through destinations
unguarded after checkSettings completion called resumeEventProcessing.
Use .sync so update(settings:) blocks until blockers are in place,
matching the original ordering contract. The serial queue still
eliminates the writer/writer race on Mediator under concurrent
checkSettings completions.

Also drops the inline rationale comments per review feedback.
@didiergarcia didiergarcia changed the title WIP: fix: serialize destination-timeline mutation in update(settings:) fix: serialize destination-timeline mutation in update(settings:) May 6, 2026
macos-latest runner image no longer ships an iPhone 15 simulator, so
xcodebuild fails destination lookup before compiling. Pin to iPhone 17
and let Xcode pick whichever iOS version the image has installed.
Remove the updateQueue serialization introduced earlier in this
branch. The underlying race lives in analytics-swift Mediator and is
addressed there directly. Once consumers upgrade to the analytics-swift
version with that fix, Mediator protects concurrent add(plugin:)
calls without any additional coordination from this plugin.

Keep the installBlockerIfNeeded(on:type:make:) helper, which closes a
pre-existing check-then-insert TOCTOU in update(settings:type:) where
overlapping settings fetches could each pass the find() == nil guard
and install duplicate blockers.
@didiergarcia didiergarcia changed the title fix: serialize destination-timeline mutation in update(settings:) refactor: collapse blocker install check+insert into a helper May 7, 2026
@didiergarcia
didiergarcia merged commit b95b3f2 into main May 7, 2026
10 checks passed
@didiergarcia
didiergarcia deleted the fix/mediator-race-update-serialize branch May 7, 2026 21:15
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