Skip to content

Commit 01d39d2

Browse files
authored
Refactored operatingMode related code. (#328)
* Refactoring of flush completion. * Catching places where groups/completions/semaphores might not get hit. * Fixed linux tests * Removed debug line; Added fix for long-running ios task.
1 parent d43c7a8 commit 01d39d2

File tree

9 files changed

+164
-104
lines changed

9 files changed

+164
-104
lines changed

.github/workflows/swift.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ jobs:
5151
needs: cancel_previous
5252
runs-on: macos-14
5353
steps:
54+
- name: Install yeetd
55+
run: |
56+
wget https://github.com/biscuitehh/yeetd/releases/download/1.0/yeetd-normal.pkg
57+
sudo installer -pkg yeetd-normal.pkg -target /
58+
yeetd &
5459
- uses: maxim-lobanov/setup-xcode@v1
5560
with:
5661
xcode-version: "15.2"

Sources/Segment/Analytics.swift

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -228,56 +228,23 @@ extension Analytics {
228228
/// called when flush has completed.
229229
public func flush(completion: (() -> Void)? = nil) {
230230
// only flush if we're enabled.
231-
guard enabled == true else { return }
232-
233-
let flushGroup = DispatchGroup()
234-
// gotta call enter at least once before we ask to be notified.
235-
flushGroup.enter()
231+
guard enabled == true else { completion?(); return }
236232

233+
let completionGroup = CompletionGroup(queue: configuration.values.flushQueue)
237234
apply { plugin in
238-
// we want to enter as soon as possible. waiting to do it from
239-
// another queue just takes too long.
240-
operatingMode.run(queue: configuration.values.flushQueue) {
235+
completionGroup.add { group in
241236
if let p = plugin as? FlushCompletion {
242-
// flush handles the groups enter/leave calls
243-
p.flush(group: flushGroup) { plugin in
244-
// we don't really care about the plugin value .. yet.
245-
}
237+
p.flush(group: group)
246238
} else if let p = plugin as? EventPlugin {
247-
flushGroup.enter()
248-
// we have no idea if this will be async or not, assume it's sync.
239+
group.enter()
249240
p.flush()
250-
flushGroup.leave()
241+
group.leave()
251242
}
252243
}
253244
}
254245

255-
flushGroup.leave() // matches our initial enter().
256-
257-
// if we ARE in sync mode, we need to wait on the group.
258-
// This effectively ends up being a `sync` operation.
259-
if operatingMode == .synchronous {
260-
flushGroup.wait()
261-
// we need to call completion on our own since
262-
// we skipped setting up notify. we don't need to do it on
263-
// .main since we are in synchronous mode.
264-
if let completion { completion() }
265-
} else if operatingMode == .asynchronous {
266-
// if we're not, flip over to our serial queue, tell it to wait on the flush
267-
// group to complete if we have a completion to hit. Otherwise, no need to
268-
// wait on completion.
269-
if let completion {
270-
// NOTE: DispatchGroup's `notify` method on linux ended up getting called
271-
// before the tasks have actually completed, so we went with this instead.
272-
OperatingMode.defaultQueue.async { [weak self] in
273-
let timedOut = flushGroup.wait(timeout: .now() + 15 /*seconds*/)
274-
if timedOut == .timedOut {
275-
self?.log(message: "flush(completion:) timed out waiting for completion.")
276-
}
277-
completion()
278-
//DispatchQueue.main.async { completion() }
279-
}
280-
}
246+
completionGroup.run(mode: operatingMode) {
247+
completion?()
281248
}
282249
}
283250

Sources/Segment/Plugins.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public protocol VersionedPlugin {
6363
}
6464

6565
public protocol FlushCompletion {
66-
func flush(group: DispatchGroup, completion: @escaping (DestinationPlugin) -> Void)
66+
func flush(group: DispatchGroup)
6767
}
6868

6969
// For internal platform-specific bits

Sources/Segment/Plugins/SegmentDestination.swift

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,15 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion
123123
// unused .. see flush(group:completion:)
124124
}
125125

126-
public func flush(group: DispatchGroup, completion: @escaping (DestinationPlugin) -> Void) {
126+
public func flush(group: DispatchGroup) {
127+
group.enter()
128+
defer { group.leave() }
129+
127130
guard let storage = self.storage else { return }
128131
guard let analytics = self.analytics else { return }
129132

130133
// don't flush if analytics is disabled.
131134
guard analytics.enabled == true else { return }
132-
133-
// enter for the high level flush, allow us time to run through any existing files..
134-
group.enter()
135135

136136
eventCount = 0
137137
cleanupUploads()
@@ -143,25 +143,19 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion
143143

144144
if pendingUploads == 0 {
145145
if type == .file, hasData {
146-
flushFiles(group: group, completion: completion)
146+
flushFiles(group: group)
147147
} else if type == .data, hasData {
148148
// we know it's a data-based transaction as opposed to file I/O
149-
flushData(group: group, completion: completion)
150-
} else {
151-
// there was nothing to do ...
152-
completion(self)
149+
flushData(group: group)
153150
}
154151
} else {
155152
analytics.log(message: "Skipping processing; Uploads in progress.")
156153
}
157-
158-
// leave for the high level flush
159-
group.leave()
160154
}
161155
}
162156

163157
extension SegmentDestination {
164-
private func flushFiles(group: DispatchGroup, completion: @escaping (DestinationPlugin) -> Void) {
158+
private func flushFiles(group: DispatchGroup) {
165159
guard let storage = self.storage else { return }
166160
guard let analytics = self.analytics else { return }
167161
guard let httpClient = self.httpClient else { return }
@@ -175,6 +169,9 @@ extension SegmentDestination {
175169

176170
// set up the task
177171
let uploadTask = httpClient.startBatchUpload(writeKey: analytics.configuration.values.writeKey, batch: url) { [weak self] result in
172+
defer {
173+
group.leave()
174+
}
178175
guard let self else { return }
179176
switch result {
180177
case .success(_):
@@ -195,20 +192,19 @@ extension SegmentDestination {
195192
// make sure it gets removed and it's cleanup() called rather
196193
// than waiting on the next flush to come around.
197194
cleanupUploads()
198-
// call the completion
199-
completion(self)
200-
// leave for the url we kicked off.
201-
group.leave()
202195
}
203196

204197
// we have a legit upload in progress now, so add it to our list.
205198
if let upload = uploadTask {
206199
add(uploadTask: UploadTaskInfo(url: url, data: nil, task: upload))
200+
} else {
201+
// we couldn't get a task, so we need to leave the group or things will hang.
202+
group.leave()
207203
}
208204
}
209205
}
210206

211-
private func flushData(group: DispatchGroup, completion: @escaping (DestinationPlugin) -> Void) {
207+
private func flushData(group: DispatchGroup) {
212208
// DO NOT CALL THIS FROM THE MAIN THREAD, IT BLOCKS!
213209
// Don't make me add a check here; i'll be sad you didn't follow directions.
214210
guard let storage = self.storage else { return }
@@ -239,6 +235,12 @@ extension SegmentDestination {
239235

240236
// set up the task
241237
let uploadTask = httpClient.startBatchUpload(writeKey: analytics.configuration.values.writeKey, data: data) { [weak self] result in
238+
defer {
239+
// leave for the url we kicked off.
240+
group.leave()
241+
semaphore.signal()
242+
}
243+
242244
guard let self else { return }
243245
switch result {
244246
case .success(_):
@@ -259,16 +261,15 @@ extension SegmentDestination {
259261
// make sure it gets removed and it's cleanup() called rather
260262
// than waiting on the next flush to come around.
261263
cleanupUploads()
262-
// call the completion
263-
completion(self)
264-
// leave for the url we kicked off.
265-
group.leave()
266-
semaphore.signal()
267264
}
268265

269266
// we have a legit upload in progress now, so add it to our list.
270267
if let upload = uploadTask {
271268
add(uploadTask: UploadTaskInfo(url: nil, data: data, task: upload))
269+
} else {
270+
// we couldn't get a task, so we need to leave the group or things will hang.
271+
group.leave()
272+
semaphore.signal()
272273
}
273274

274275
_ = semaphore.wait(timeout: .distantFuture)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// CompletionGroup.swift
3+
//
4+
//
5+
// Created by Brandon Sneed on 4/17/24.
6+
//
7+
8+
import Foundation
9+
10+
class CompletionGroup {
11+
let queue: DispatchQueue
12+
var items = [(DispatchGroup) -> Void]()
13+
14+
init(queue: DispatchQueue) {
15+
self.queue = queue
16+
}
17+
18+
func add(workItem: @escaping (DispatchGroup) -> Void) {
19+
items.append(workItem)
20+
}
21+
22+
func run(mode: OperatingMode, completion: @escaping () -> Void) {
23+
// capture self strongly on purpose
24+
let task: () -> Void = { [self] in
25+
let group = DispatchGroup()
26+
group.enter()
27+
group.notify(queue: queue) { [weak self] in
28+
completion()
29+
self?.items.removeAll()
30+
}
31+
32+
for item in items {
33+
item(group)
34+
}
35+
36+
group.leave()
37+
38+
if mode == .synchronous {
39+
group.wait()
40+
}
41+
}
42+
43+
switch mode {
44+
case .synchronous:
45+
queue.sync {
46+
task()
47+
}
48+
case .asynchronous:
49+
queue.async {
50+
task()
51+
}
52+
}
53+
}
54+
}

Sources/Segment/Utilities/Utils.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,3 @@ internal func eventStorageDirectory(writeKey: String) -> URL {
8585
return segmentURL
8686
}
8787

88-

Tests/LinuxMain.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.

Tests/Segment-Tests/Analytics_Tests.swift

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,6 @@ final class Analytics_Tests: XCTestCase {
745745
let shared2 = Analytics.shared()
746746
XCTAssertFalse(alive2 === shared2)
747747
XCTAssertTrue(shared2 === shared)
748-
749748
}
750749

751750
func testAsyncOperatingMode() throws {
@@ -755,20 +754,6 @@ final class Analytics_Tests: XCTestCase {
755754
.flushAt(9999)
756755
.operatingMode(.asynchronous))
757756

758-
// set the httpclient to use our blocker session
759-
let segment = analytics.find(pluginType: SegmentDestination.self)
760-
let configuration = URLSessionConfiguration.ephemeral
761-
configuration.allowsCellularAccess = true
762-
configuration.timeoutIntervalForResource = 30
763-
configuration.timeoutIntervalForRequest = 60
764-
configuration.httpMaximumConnectionsPerHost = 2
765-
configuration.protocolClasses = [BlockNetworkCalls.self]
766-
configuration.httpAdditionalHeaders = ["Content-Type": "application/json; charset=utf-8",
767-
"Authorization": "Basic test",
768-
"User-Agent": "analytics-ios/\(Analytics.version())"]
769-
let blockSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
770-
segment?.httpClient?.session = blockSession
771-
772757
waitUntilStarted(analytics: analytics)
773758

774759
analytics.storage.hardReset(doYouKnowHowToUseThis: true)
@@ -777,13 +762,16 @@ final class Analytics_Tests: XCTestCase {
777762

778763
// put an event in the pipe ...
779764
analytics.track(name: "completion test1")
765+
766+
RunLoop.main.run(until: .distantPast)
767+
780768
// flush it, that'll get us an upload going
781769
analytics.flush {
782770
// verify completion is called.
783771
expectation.fulfill()
784772
}
785773

786-
wait(for: [expectation], timeout: 5)
774+
wait(for: [expectation], timeout: 10)
787775

788776
XCTAssertNil(analytics.pendingUploads)
789777
}
@@ -795,20 +783,6 @@ final class Analytics_Tests: XCTestCase {
795783
.flushAt(9999)
796784
.operatingMode(.synchronous))
797785

798-
// set the httpclient to use our blocker session
799-
let segment = analytics.find(pluginType: SegmentDestination.self)
800-
let configuration = URLSessionConfiguration.ephemeral
801-
configuration.allowsCellularAccess = true
802-
configuration.timeoutIntervalForResource = 30
803-
configuration.timeoutIntervalForRequest = 60
804-
configuration.httpMaximumConnectionsPerHost = 2
805-
configuration.protocolClasses = [BlockNetworkCalls.self]
806-
configuration.httpAdditionalHeaders = ["Content-Type": "application/json; charset=utf-8",
807-
"Authorization": "Basic test",
808-
"User-Agent": "analytics-ios/\(Analytics.version())"]
809-
let blockSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
810-
segment?.httpClient?.session = blockSession
811-
812786
waitUntilStarted(analytics: analytics)
813787

814788
analytics.storage.hardReset(doYouKnowHowToUseThis: true)
@@ -822,7 +796,7 @@ final class Analytics_Tests: XCTestCase {
822796
expectation.fulfill()
823797
}
824798

825-
wait(for: [expectation], timeout: 1)
799+
wait(for: [expectation], timeout: 10)
826800

827801
XCTAssertNil(analytics.pendingUploads)
828802

0 commit comments

Comments
 (0)