Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
184 commits
Select commit Hold shift + click to select a range
845a595
feat(events): persist recorded events to an append-only log (tier 3)
abelonogov-ld Sep 19, 2026
0c5c675
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 24, 2026
1307ed4
test: point the unserializable-event specs at the event store
abelonogov-ld Sep 24, 2026
7df86a3
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 25, 2026
87d3926
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 25, 2026
183ee21
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 25, 2026
312dbb6
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 25, 2026
a952a9b
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 25, 2026
95b56af
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 28, 2026
ebc8aef
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 29, 2026
b5a88dd
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 29, 2026
2c46b87
Encode held events only where they are persisted; record evaluations …
abelonogov-ld Sep 29, 2026
126db1c
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Sep 29, 2026
87ec45b
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 2, 2026
4bac993
tier 3 fixes
abelonogov-ld Oct 7, 2026
e995b26
fixes
abelonogov-ld Oct 7, 2026
7f803e4
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 7, 2026
3fcd1f5
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 8, 2026
6406d5d
fix(events): Keep stored events whose read failed, and deliver what a…
abelonogov-ld Oct 8, 2026
0c8ea66
fix(events): Report a flush false when an event the SDK accepted coul…
abelonogov-ld Oct 8, 2026
6a310fa
test(events): Match Android's coverage of the recoverable failures
abelonogov-ld Oct 8, 2026
f767a89
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 8, 2026
ca5078e
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 9, 2026
fe918c6
format
abelonogov-ld Oct 9, 2026
2b892df
Assert lock ownership instead of encoding it in method names
abelonogov-ld Oct 9, 2026
af218b1
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 9, 2026
fd73b60
tests
abelonogov-ld Oct 9, 2026
2b25a35
fix(events): Answer every flush off the caller's thread
abelonogov-ld Oct 9, 2026
e5a4e43
fix(events): Give each process a log of its own
abelonogov-ld Oct 10, 2026
61ba495
Lock the open event log on a Mac, where instances share a process name
abelonogov-ld Oct 10, 2026
12cb3e2
split
abelonogov-ld Oct 10, 2026
a76eda9
rename to recordLock
abelonogov-ld Oct 11, 2026
a901782
use withLock
abelonogov-ld Oct 11, 2026
8bffc83
Keep the batch paths off the disk, and out of ioLock, when persistenc…
abelonogov-ld Oct 11, 2026
81a7856
Answer a flush for the batch another delivery is holding
abelonogov-ld Oct 11, 2026
425aefb
simplify
abelonogov-ld Oct 12, 2026
12cc8f0
Take the frames a failed write left back off the log, so they are not…
abelonogov-ld Oct 12, 2026
a65b8fe
Rename OpenEventLog to DiskEventLog, and make EventLogFile the open f…
abelonogov-ld Oct 12, 2026
ea815e2
Count an evaluation through the default subscript, so a context is lo…
abelonogov-ld Oct 12, 2026
35ae12e
Move NullEventStore into a file of its own
abelonogov-ld Oct 12, 2026
4fcc3e0
Group the EventReporter fields by the lock or queue that guards them
abelonogov-ld Oct 12, 2026
d32c669
Look a context up once even at the limit, through its index there
abelonogov-ld Oct 12, 2026
40ab485
Go back to looking a context up through its index, which reads more p…
abelonogov-ld Oct 12, 2026
20203d9
Admit an evaluation's events without building an array for them
abelonogov-ld Oct 12, 2026
4ab5f5d
Rename EventReporter to DirectEventProcessor, as Android names it, wi…
abelonogov-ld Oct 12, 2026
8d9c131
Name the processor's capacity eventCapacity, after the setting it com…
abelonogov-ld Oct 12, 2026
65b2bbf
Move summarizing and encoding into OutboundEventBuffer, as Android ha…
abelonogov-ld Oct 12, 2026
79ddb32
Move posting a batch and reading the response into AnalyticsEventSend…
abelonogov-ld Oct 12, 2026
ab53b77
Count a commit's events while they are being written, so a slow write…
abelonogov-ld Oct 12, 2026
1db8cc7
Lay out long tier-3 signatures one parameter per line
abelonogov-ld Oct 12, 2026
3668890
Fix lint warnings in files the event durability tiers touched
abelonogov-ld Oct 12, 2026
d6b1e95
Run the process assertion specs only where the platform has assertions
abelonogov-ld Oct 12, 2026
f2a8bd6
Take a delivered batch out of the store before telling observers abou…
abelonogov-ld Oct 12, 2026
6a6afc8
Make the unit tests build and pass on watchOS
abelonogov-ld Oct 12, 2026
50ebcd0
Keep the closed-batch count equal to the batches it counts
abelonogov-ld Oct 12, 2026
8ef1c2a
Document that the event capacity counts an earlier launch's events on…
abelonogov-ld Oct 12, 2026
f289900
Give the processor specs' waits room for a loaded CI simulator
abelonogov-ld Oct 12, 2026
d95893a
Fix compiler warnings in tests the event durability tiers added
abelonogov-ld Oct 12, 2026
2f180e4
Say which lock guards the outbound buffer's encoder
abelonogov-ld Oct 12, 2026
409b076
List the no-directory store's batches oldest first
abelonogov-ld Oct 12, 2026
b85328d
Stop counting a batch against capacity while its request is out
abelonogov-ld Oct 12, 2026
b71a6cb
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 12, 2026
7c3aab6
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Oct 12, 2026
53af9ee
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 12, 2026
35453a8
disabled unit testd
abelonogov-ld Oct 12, 2026
8bf63c6
don't touch disk in disabled
abelonogov-ld Oct 12, 2026
7dbc8ea
EventDirectory
abelonogov-ld Oct 12, 2026
1ae965d
Keep events counted while a held batch is being assembled
abelonogov-ld Oct 12, 2026
60c1965
Stop re-sending a delivered batch whose file will not delete
abelonogov-ld Oct 12, 2026
eaa67a4
Raise the default event capacity to 2000
abelonogov-ld Oct 12, 2026
8d59683
Write on flush at .deferred too
abelonogov-ld Oct 12, 2026
d4f6e06
Leave scheduling writes to the event processor
abelonogov-ld Oct 12, 2026
109372d
Count a batch's events in its file name
abelonogov-ld Oct 12, 2026
3aaa3b8
unreadable batch
abelonogov-ld Oct 13, 2026
9d0b789
Write on entering the background at .deferred too
abelonogov-ld Oct 13, 2026
ea0b069
Read the summarizer under its lock in tests
abelonogov-ld Oct 13, 2026
49a4c56
Do not take a payload id ending in zero-led digits for a count
abelonogov-ld Oct 13, 2026
1baabd6
Say what a dropped event-publish completion costs now
abelonogov-ld Oct 13, 2026
35163da
Wait for the after-identify hooks in the hook forwarding specs
abelonogov-ld Oct 13, 2026
19192a3
Go online before staging the batch the unreadable-batch spec damages
abelonogov-ld Oct 13, 2026
51edd86
Make the summary-count spec commit during recording however it is sch…
abelonogov-ld Oct 13, 2026
690f52e
Merge branch 'v11' into andrey/event-durability-tier3-persistence
abelonogov-ld Oct 16, 2026
188bb59
Bound stored events by bytes and by age
abelonogov-ld Oct 20, 2026
4be95a8
Checksum every frame of the event log
abelonogov-ld Oct 21, 2026
a8362df
Move the in-memory CRC advance out of the vendored compression file
abelonogov-ld Oct 21, 2026
8a6af04
experimental
abelonogov-ld Oct 21, 2026
067e676
Name each event log by a UUID and recover every log no live process h…
abelonogov-ld Oct 21, 2026
1cdd573
Stop handing the process name to the event store
abelonogov-ld Oct 21, 2026
4d33b28
Name an event log pending- rather than open-
abelonogov-ld Oct 21, 2026
e29e9e2
Log events from a previous run as prepared, not recovered
abelonogov-ld Oct 21, 2026
82322ad
Rename recoverInterruptedLog to preparePayloadsFromInterruptedLogs
abelonogov-ld Oct 21, 2026
0533f11
Send a batch's events no more than once when a failed write stays on …
abelonogov-ld Oct 21, 2026
5ad00a1
Log when events from a previous run cannot be moved for delivery
abelonogov-ld Oct 21, 2026
b21ef40
Keep every batch within 2 MiB, so no request is too large to accept
abelonogov-ld Oct 21, 2026
95957b9
Hold a batch being delivered twice at most, and stop copying every frame
abelonogov-ld Oct 21, 2026
edb95e7
Tell the next flush about a batch lost where nobody was waiting
abelonogov-ld Oct 21, 2026
91a301a
Say when a read that failed is tried again
abelonogov-ld Oct 21, 2026
e3c64b8
Leave a batch out of the count only while it is still counted
abelonogov-ld Oct 21, 2026
2fa7afc
Assemble a batch's payload in the bytes its file was read into
abelonogov-ld Oct 21, 2026
1e7616c
Name batches ready-<payloadId>.<count>, read by position
abelonogov-ld Oct 22, 2026
8004c28
Test that a flush of more than a batch sends no request over the batc…
abelonogov-ld Oct 22, 2026
d4f35f3
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 22, 2026
2a7cf5b
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 22, 2026
0586bdf
Report a flush as failed when the stored events cannot be listed
abelonogov-ld Oct 22, 2026
f4adfaf
Keep a log whose rename failed open, and report the flush as failed
abelonogov-ld Oct 22, 2026
2b1e380
Report events a damaged batch could not send as lost
abelonogov-ld Oct 22, 2026
cc200a6
Read the logs a previous run left without holding the store's I/O lock
abelonogov-ld Oct 22, 2026
4c30257
Describe the pending threshold as a trigger, not a ceiling
abelonogov-ld Oct 22, 2026
1b0a80d
Keep the disk out of flushAndWait's caller thread
abelonogov-ld Oct 22, 2026
4e049fd
Keep expiry away from batches any live store or process is sending
abelonogov-ld Oct 22, 2026
d506d34
Close no batch behind one that is blocked
abelonogov-ld Oct 22, 2026
5d04131
Bound summaries by bytes where events stay off the disk
abelonogov-ld Oct 22, 2026
051ff2b
Leave summaries out of the event count
abelonogov-ld Oct 22, 2026
288a27b
Back off failed event deliveries per RETRY instead of dropping and go…
abelonogov-ld Oct 22, 2026
b55c839
Drop a batch after two 400s in a row so it cannot block the rest
abelonogov-ld Oct 22, 2026
0124fc8
Deliver one batch at a time
abelonogov-ld Oct 22, 2026
307eaa7
Pin that a delivery's own loss is told to its caller
abelonogov-ld Oct 22, 2026
d5ca597
Remove a log whose first write failed and could not be cut back
abelonogov-ld Oct 22, 2026
6506c92
Count a batch deleted unsent as lost where no other process shares th…
abelonogov-ld Oct 23, 2026
5c1e5b8
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Oct 23, 2026
6093548
Pin context redaction in the payload the processor posts
abelonogov-ld Oct 23, 2026
bd3b796
Count the events of an open log deleted unsent as lost
abelonogov-ld Oct 23, 2026
b94ce8e
Put each frame's checksum after its payload
abelonogov-ld Oct 23, 2026
c438c9e
fix(events): let the delivery at close post through the backoff
abelonogov-ld Oct 27, 2026
455028b
fix(events): drop a batch after two 413, 415 or 422 refusals, as for 400
abelonogov-ld Oct 27, 2026
68ce4c4
Name and list every file in the event directory in EventDirectory
abelonogov-ld Oct 27, 2026
24ba5b2
test: pace the lister so closes are not starved of the store's lock
abelonogov-ld Oct 27, 2026
8319688
Drop the part of EventDirectory's comment about EventStore's concerns
abelonogov-ld Oct 27, 2026
5028134
fix: count a batch read to count it by the bytes that were read
abelonogov-ld Oct 27, 2026
0083526
fix: never read a stored file larger than this version writes
abelonogov-ld Oct 28, 2026
8676b9f
feat: tell the event processor when a flag request starts and settles
abelonogov-ld Oct 28, 2026
874c11a
feat: give flag requests the network ahead of a backlog of events
abelonogov-ld Oct 28, 2026
8b6e2d6
feat: share the byte capacity equally across the configured environments
abelonogov-ld Oct 28, 2026
07ed8c5
fix: count the wait for a flag request from when it started, so the l…
abelonogov-ld Oct 28, 2026
e6514b9
fix: count the wait for a flag request from when it started, so no pa…
abelonogov-ld Oct 28, 2026
bc29c3d
fix: going offline stops a delivery before its next post, its retry's…
abelonogov-ld Oct 28, 2026
2887cd3
fix: leave flag requests untracked and FlagTraffic unbuilt where even…
abelonogov-ld Oct 28, 2026
b1a7ed1
feat: version batch names, leaving a batch another version named to e…
abelonogov-ld Oct 28, 2026
a015c89
refactor: give way to flag values by time alone, without tracking fla…
abelonogov-ld Oct 28, 2026
cedff6a
refactor: move recording and commits out of DirectEventProcessor into…
abelonogov-ld Oct 28, 2026
53eb687
Move delivery queueing and pacing out of DirectEventProcessor
abelonogov-ld Oct 28, 2026
f2b515e
Rename DeliveryScheduler to EventDeliveryScheduler
abelonogov-ld Oct 28, 2026
9e629b6
Rename DeliveryRun to EventDeliveryRun
abelonogov-ld Oct 28, 2026
9913306
Hold the report timer's deliveries until the launch gate opens
abelonogov-ld Oct 28, 2026
8a8a670
fix: flushAndWait posts through the backoff's wait, as close does
abelonogov-ld Oct 30, 2026
bcd41ae
fix: a post that gets no response leaves the run of payload refusals …
abelonogov-ld Oct 30, 2026
0d4e4a4
fix: bound full events by bytes where events are kept off the disk
abelonogov-ld Oct 30, 2026
fdaf882
fix: count an uncounted batch this version cannot read as lost rather…
abelonogov-ld Oct 30, 2026
8d97c41
fix: count an uncounted batch damaged from its first frame as one eve…
abelonogov-ld Oct 30, 2026
af90a9c
fix: pass by a stored batch whose read fails, and drop it after three…
abelonogov-ld Oct 30, 2026
f059653
fix: give each event directory its own BatchLocks lock
abelonogov-ld Oct 30, 2026
6877ac1
fix: split a summary too large for a log frame rather than dropping it
abelonogov-ld Oct 30, 2026
2a1d1a8
Stop .deferred track and identify from scheduling a commit each (ATR3…
abelonogov-ld Oct 30, 2026
5ea2749
Group concurrent .immediate commit points into one commit, as Android…
abelonogov-ld Oct 30, 2026
9bda454
Commit at close off the caller's thread within a budget, as Android does
abelonogov-ld Oct 30, 2026
18e73a6
A failed post through the backoff leaves the wait as it was
abelonogov-ld Oct 30, 2026
7117c2f
Space the read failures that drop a batch a flush interval apart
abelonogov-ld Oct 30, 2026
013d2b7
Stop splitting a summary that is mostly context
abelonogov-ld Oct 30, 2026
8e6abfa
Pack an oversized summary's flags into parts that fit beside the context
abelonogov-ld Oct 31, 2026
d62cd58
Measure a summary's flags with a writer of their own, so its parts re…
abelonogov-ld Oct 31, 2026
93f6ed5
Split an oversized summary on the commit queue rather than the caller…
abelonogov-ld Oct 31, 2026
4ae9292
Split an oversized summary on the flushing thread again, so a flush w…
abelonogov-ld Nov 1, 2026
5c61258
Write only its own event for a track or identify at immediate persist…
abelonogov-ld Nov 1, 2026
9173f03
Write only its own frame for a track at immediate persistence, from a…
abelonogov-ld Nov 2, 2026
d0ee14b
Drop a summary larger than a frame rather than splitting it
abelonogov-ld Nov 2, 2026
9721730
Commit everything held at a track or identify at immediate persistenc…
abelonogov-ld Nov 2, 2026
739a951
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Nov 4, 2026
e3684a8
Mark the experimental persistence levels with a warning callout
abelonogov-ld Nov 4, 2026
98ad649
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Nov 4, 2026
635daf1
Build the event persistence test helpers in every configuration, not …
abelonogov-ld Nov 4, 2026
f2793c1
Keep the lock ownership check to Debug builds
abelonogov-ld Nov 4, 2026
6c68152
Guard the diagnostic cache mock with a lock, as the SDK calls it from…
abelonogov-ld Nov 4, 2026
6d6875c
Make the event delivery delay configurable, and hold the diagnostic i…
abelonogov-ld Nov 4, 2026
b55af5d
Log undelivered events at suspension at info where nothing persists, …
abelonogov-ld Nov 5, 2026
5711315
Gather the platform differences in backgrounding into SystemCapabilit…
abelonogov-ld Nov 5, 2026
7bebd16
Merge branch 'andrey/event-durability-tier2-bounded-flush' into andre…
abelonogov-ld Nov 5, 2026
b5e81ef
Build the event pipeline's test accessors in Debug only
abelonogov-ld Nov 6, 2026
7fa137c
One background observer for the events of every environment
abelonogov-ld Nov 6, 2026
d052e1c
One way to flush several environments and collect their outcomes
abelonogov-ld Nov 6, 2026
793c3b9
Give up on an environment's answer after the background assertion's cap
abelonogov-ld Nov 6, 2026
bcbde8a
Drop the long-timeout log from flushAndWait
abelonogov-ld Nov 6, 2026
5a04c20
fix: pace the delivery a backoff's wait sets when it ends (R3-3)
abelonogov-ld Nov 6, 2026
b3c9ff3
Recover a previous run's events at utility
abelonogov-ld Nov 6, 2026
3f8bab5
Close the clients outside instancesQueue
abelonogov-ld Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 170 additions & 14 deletions LaunchDarkly.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

81 changes: 32 additions & 49 deletions LaunchDarkly/GeneratedCode/mocks.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,53 +38,6 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider {
}
}

// MARK: - DiagnosticCachingMock
final class DiagnosticCachingMock: DiagnosticCaching {

var getDiagnosticIdCallCount = 0
var getDiagnosticIdCallback: (() throws -> Void)?
var getDiagnosticIdReturnValue: DiagnosticId!
func getDiagnosticId() -> DiagnosticId {
getDiagnosticIdCallCount += 1
try! getDiagnosticIdCallback?()
return getDiagnosticIdReturnValue
}

var getCurrentStatsAndResetCallCount = 0
var getCurrentStatsAndResetCallback: (() throws -> Void)?
var getCurrentStatsAndResetReturnValue: DiagnosticStats!
func getCurrentStatsAndReset() -> DiagnosticStats {
getCurrentStatsAndResetCallCount += 1
try! getCurrentStatsAndResetCallback?()
return getCurrentStatsAndResetReturnValue
}

var incrementDroppedEventCountCallCount = 0
var incrementDroppedEventCountCallback: (() throws -> Void)?
func incrementDroppedEventCount() {
incrementDroppedEventCountCallCount += 1
try! incrementDroppedEventCountCallback?()
}

var recordEventsInLastBatchCallCount = 0
var recordEventsInLastBatchCallback: (() throws -> Void)?
var recordEventsInLastBatchReceivedEventsInLastBatch: Int?
func recordEventsInLastBatch(eventsInLastBatch: Int) {
recordEventsInLastBatchCallCount += 1
recordEventsInLastBatchReceivedEventsInLastBatch = eventsInLastBatch
try! recordEventsInLastBatchCallback?()
}

var addStreamInitCallCount = 0
var addStreamInitCallback: (() throws -> Void)?
var addStreamInitReceivedStreamInit: DiagnosticStreamInit?
func addStreamInit(streamInit: DiagnosticStreamInit) {
addStreamInitCallCount += 1
addStreamInitReceivedStreamInit = streamInit
try! addStreamInitCallback?()
}
}

// MARK: - DiagnosticReportingMock
final class DiagnosticReportingMock: DiagnosticReporting {

Expand Down Expand Up @@ -174,8 +127,8 @@ final class EnvironmentReportingMock: EnvironmentReporting {
}
}

// MARK: - EventReportingMock
final class EventReportingMock: EventReporting {
// MARK: - EventProcessorMock
final class EventProcessorMock: EventProcessor {

var isOnlineSetCount = 0
var setIsOnlineCallback: (() throws -> Void)?
Expand Down Expand Up @@ -230,6 +183,36 @@ final class EventReportingMock: EventReporting {
flushReportingOutcomeReceivedCompletion = completion
try! flushReportingOutcomeCallback?()
}

var flushBypassingBackoffCallCount = 0
var flushBypassingBackoffCallback: (() throws -> Void)?
var flushBypassingBackoffReceivedCompletion: (FlushOutcomeClosure)?
func flushBypassingBackoff(completion: @escaping FlushOutcomeClosure) {
flushBypassingBackoffCallCount += 1
flushBypassingBackoffReceivedCompletion = completion
try! flushBypassingBackoffCallback?()
}

var flushAtCloseCallCount = 0
var flushAtCloseCallback: (() throws -> Void)?
func flushAtClose() {
flushAtCloseCallCount += 1
try! flushAtCloseCallback?()
}

var commitRecordedEventsCallCount = 0
var commitRecordedEventsCallback: (() throws -> Void)?
func commitRecordedEvents() {
commitRecordedEventsCallCount += 1
try! commitRecordedEventsCallback?()
}

var commitWherePersistingCallCount = 0
var commitWherePersistingCallback: (() throws -> Void)?
func commitWherePersisting() {
commitWherePersistingCallCount += 1
try! commitWherePersistingCallback?()
}
}

// MARK: - FeatureFlagCachingMock
Expand Down
15 changes: 15 additions & 0 deletions LaunchDarkly/LaunchDarkly/Extensions/Data+CRC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

extension Crc32 {
/// Advance the current checksum with bytes already in memory, without wrapping them in `Data` first.
/// - parameter bytes: bytes to advance the checksum
mutating func advance(withBytes bytes: UnsafeRawBufferPointer) {
guard let base = bytes.baseAddress?.assumingMemoryBound(to: UInt8.self)
else { return }
if let fastCrc32 = Crc32.zLibCrc32 {
checksum = fastCrc32(checksum, base, UInt32(bytes.count))
} else {
checksum = slowCrc32(start: checksum, data: Data(bytes))
}
}
}
6 changes: 3 additions & 3 deletions LaunchDarkly/LaunchDarkly/Extensions/Data+Compression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,12 @@ internal extension Data {

/// Struct based type representing a Crc32 checksum.
struct Crc32: CustomStringConvertible {
private static let zLibCrc32: ZLibCrc32FuncPtr? = loadCrc32fromZLib()
static let zLibCrc32: ZLibCrc32FuncPtr? = loadCrc32fromZLib()

init() {}

// C convention function pointer type matching the signature of `libz::crc32`
private typealias ZLibCrc32FuncPtr = @convention(c) (
typealias ZLibCrc32FuncPtr = @convention(c) (
_ cks: UInt32,
_ buf: UnsafePointer<UInt8>,
_ len: UInt32
Expand Down Expand Up @@ -300,7 +300,7 @@ struct Crc32: CustomStringConvertible {
/// Rudimentary fallback implementation of the crc32 checksum. This is only a backup used
/// when zlib can't be found under '/usr/lib/libz.dylib'.
/// - returns: crc32 checksum (4 byte)
private func slowCrc32(start: UInt32, data: Data) -> UInt32 {
func slowCrc32(start: UInt32, data: Data) -> UInt32 {
return ~data.reduce(~start) { (crc: UInt32, next: UInt8) -> UInt32 in
let tableOffset = (crc ^ UInt32(next)) & 0xff
return lookUpTable[Int(tableOffset)] ^ crc >> 8
Expand Down
85 changes: 46 additions & 39 deletions LaunchDarkly/LaunchDarkly/LDClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class LDClient {
let oldValue = _isOnline
_isOnline = newValue
flagSynchronizer.isOnline = _isOnline
eventReporter.isOnline = _isOnline
eventProcessor.isOnline = _isOnline
diagnosticReporter.setMode(runMode, online: _isOnline)
if _isOnline != oldValue {
connectionInformation = ConnectionInformation.onlineSetCheck(connectionInformation: connectionInformation, ldClient: self, config: config, online: _isOnline)
Expand Down Expand Up @@ -247,39 +247,25 @@ public class LDClient {
There is almost no reason to stop the LDClient. Normally, set the LDClient offline to stop communication with the LaunchDarkly servers. Stop the LDClient to stop recording events. There is no need to stop the LDClient prior to suspending, moving to the background, or terminating the app. The SDK will respond to these events as the system requires and as configured in LDConfig.
*/
public func close() {
LDClient.instancesQueue.sync(flags: .barrier) {
LDClient.instances?.forEach { $1.internalClose() }
LDClient.instances = nil
// Closed outside `instancesQueue`: each close can wait on its final event commit, and every other call that
// reads `instances` would wait behind it.
let clients = LDClient.instancesQueue.sync(flags: .barrier) { () -> [LDClient] in
defer { LDClient.instances = nil }
return LDClient.instances.map { Array($0.values) } ?? []
}
clients.forEach { $0.internalClose() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close overlaps a concurrent client start

Medium Severity

close() now clears instances before internalClose() runs, so another thread can observe get() as nil and call start() while the old clients are still committing and delivering. Two EventStores then share one directory: the new client can recover and send a batch the closing client is still posting, duplicating events. Previously start() waited on instancesQueue until teardown finished, so this overlap could not happen.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3f8bab5. Configure here.

}

private func internalClose() {
os_log("%s stopping", log: config.logger, type: .debug, typeName(and: #function))
internalFlush()
eventProcessor.flushAtClose()
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
internalSetOnline(false)
hasStarted = false
os_log("%s stopped", log: config.logger, type: .debug, typeName(and: #function))
}

@objc private func didEnterBackground() {
os_log("%s", log: config.logger, type: .debug, typeName(and: #function))
// A backgrounded process is suspended once idle, so deliver inside an activity assertion. Only where suspension
// is real: macOS posts this notification whenever the application loses focus, and never suspends it for that.
#if os(iOS) || os(tvOS)
backgroundDelivery.run { [weak self] finished in
guard let self = self
else {
finished()
return
}
self.eventReporter.flushReportingOutcome { delivered in
if !delivered {
os_log("%s events did not reach LaunchDarkly before suspension", log: self.config.logger, type: .debug, self.typeName(and: #function))
}
finished()
}
}
#endif
Thread.performOnMain {
runMode = .background
}
Expand All @@ -292,9 +278,6 @@ public class LDClient {
}
}

/// One assertion per client, however many times the application is backgrounded while a delivery is running.
private let backgroundDelivery: BackgroundActivity

let config: LDConfig
/// Identifies this client's environment to a hook, without handing it the mobile key that identifies the
/// environment to LaunchDarkly. Hashed once here rather than per evaluation, because a deduping hook asks for it on
Expand Down Expand Up @@ -475,7 +458,7 @@ public class LDClient {

internalIdentifyQueue.sync {
if self.context == updatedContext {
self.eventReporter.record(IdentifyEvent(context: self.context))
self.eventProcessor.record(IdentifyEvent(context: self.context))
completion?()
return
}
Expand Down Expand Up @@ -525,7 +508,7 @@ public class LDClient {
onSyncComplete: self.onFlagSyncComplete)

if self.hasStarted {
self.eventReporter.record(IdentifyEvent(context: self.context))
self.eventProcessor.record(IdentifyEvent(context: self.context))
}

if self.isOnline && self.isInSupportedRunMode {
Expand Down Expand Up @@ -769,7 +752,7 @@ public class LDClient {
key,
String(describing: data),
String(describing: metricValue))
eventReporter.record(event)
eventProcessor.record(event)

executeAfterTrackHooks(key: key, data: data, metricValue: metricValue)
}
Expand Down Expand Up @@ -979,7 +962,7 @@ public class LDClient {
private(set) var flagCache: FeatureFlagCaching
private(set) var flagSynchronizer: LDFlagSynchronizing
var flagChangeNotifier: FlagChangeNotifying
private(set) var eventReporter: EventReporting
private(set) var eventProcessor: EventProcessor
private(set) var environmentReporter: EnvironmentReporting
private(set) var throttler: Throttling
private(set) var diagnosticReporter: DiagnosticReporting
Expand Down Expand Up @@ -1035,14 +1018,13 @@ public class LDClient {
throttler = self.serviceFactory.makeThrottler(environmentReporter: environmentReporter)

config = configuration
backgroundDelivery = BackgroundActivity(reason: "LaunchDarkly event delivery", logger: configuration.logger)
context = startContext ?? LDContext()

if config.autoEnvAttributes {
context = AutoEnvContextModifier(environmentReporter: environmentReporter, logger: config.logger).modifyContext(context)
}

var hookState: IdentifyHookState? = nil
var hookState: IdentifyHookState?
if !hooks.isEmpty {
let seriesContext = IdentifySeriesContext(context: context, methodName: "init")
let seriesData = hooks.map { hook in hook.beforeIdentify(seriesContext: seriesContext, seriesData: EvaluationSeriesData()) }
Expand All @@ -1054,7 +1036,7 @@ public class LDClient {
// Reports over the logger rather than over this client, which is not usable until every stored property
// below has a value, so that the reporter can be built once rather than built and replaced.
let logger = configuration.logger
eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service) { result in
eventProcessor = self.serviceFactory.makeEventProcessor(config: config, service: service) { result in
LDClient.logEventSyncComplete(result, logger)
}
connectionInformation = self.serviceFactory.makeConnectionInformation()
Expand All @@ -1069,6 +1051,7 @@ public class LDClient {
if let backgroundNotification = SystemCapabilities.backgroundNotification {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil)
}
LDClient.observeBackgroundingForEvents()
if let foregroundNotification = SystemCapabilities.foregroundNotification {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil)
}
Expand All @@ -1078,12 +1061,13 @@ public class LDClient {
service.resetFlagResponseCache(etag: cachedData.etag)
// Reports to this client, so it could not be given at construction: `self` is only usable from here.
flagSynchronizer.onSyncComplete = onFlagSyncComplete
giveFlagValuesTheNetworkFirst()

if let cachedFlags = cachedData.items, !cachedFlags.isEmpty {
flagStore.replaceStore(newStoredItems: cachedFlags)
}

eventReporter.record(IdentifyEvent(context: context))
eventProcessor.record(IdentifyEvent(context: context))
self.connectionInformation = ConnectionInformation.uncacheConnectionInformation(config: config, ldClient: self, clientServiceFactory: self.serviceFactory)

internalSetOnline(configuration.startOnline) {
Expand All @@ -1098,12 +1082,35 @@ public class LDClient {

extension LDClient: TypeIdentifying { }

private extension LDClient {
func onEventSyncComplete(result: SynchronizingError?) {
if let synchronizingError = result {
os_log("%s result: %s", log: config.logger, type: .debug, typeName(and: #function), String(describing: synchronizingError))
} else {
os_log("%s result: success", log: config.logger, type: .debug, typeName(and: #function))
extension LDClient {
/// Has the diagnostic init event wait for the event processor's launch gate, as a previous run's events do, whether
/// or not events persist.
fileprivate func giveFlagValuesTheNetworkFirst() {
if let eventProcessor = eventProcessor as? DirectEventProcessor {
(diagnosticReporter as? DiagnosticReporter)?.sendsInitAfter = eventProcessor.whenLaunchGateOpens
}
}

/// Observes the notification once for the events of every environment, however many clients are made.
private static let backgroundEvents: BackgroundEventDelivery? = {
guard let backgroundNotification = SystemCapabilities.backgroundNotification
else { return nil }
let delivery = BackgroundEventDelivery(environments: LDClient.currentEnvironments)
// Run on the posting thread, so that every environment has committed by the time the notification returns.
NotificationCenter.default.addObserver(forName: backgroundNotification, object: nil, queue: nil) { _ in
delivery.applicationDidEnterBackground()
}
return delivery
}()

fileprivate static func observeBackgroundingForEvents() {
_ = backgroundEvents
}

private static func currentEnvironments() -> [BackgroundEventDelivery.Environment] {
let clients = instancesQueue.sync { instances } ?? [:]
return clients.map { name, client in
BackgroundEventDelivery.Environment(name: name, eventProcessor: client.eventProcessor, logger: client.config.logger)
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
}
Expand Down
Loading
Loading