Skip to content

Commit b4f5f4d

Browse files
authored
Merge pull request #18 from instana/feature/add_rate_limiter
Feature/add rate limiter
2 parents 492117f + 5c10563 commit b4f5f4d

File tree

18 files changed

+504
-213
lines changed

18 files changed

+504
-213
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/InstanaAgentIntegrationTests.xcscheme

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
<Test
8484
Identifier = "InstanaPersistableQueueTests">
8585
</Test>
86+
<Test
87+
Identifier = "InstanaPropertiesTests">
88+
</Test>
8689
<Test
8790
Identifier = "InstanaPropertyHandlerTests">
8891
</Test>
@@ -104,6 +107,9 @@
104107
<Test
105108
Identifier = "NetworkUtilityTests">
106109
</Test>
110+
<Test
111+
Identifier = "ReporterRateLimitTests">
112+
</Test>
107113
<Test
108114
Identifier = "ReporterTests">
109115
</Test>

.swiftpm/xcode/xcshareddata/xcschemes/InstanaAgentTests.xcscheme

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
<Test
3939
Identifier = "ReporterIntegrationTests">
4040
</Test>
41-
<Test
42-
Identifier = "ReporterTests">
43-
</Test>
4441
</SkippedTests>
4542
</TestableReference>
4643
</Testables>

Dev/iOSAgentExample.xcodeproj/project.pbxproj

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
728CF9D82387F01200BEE2F9 /* Frameworks */,
149149
728CF9D92387F01200BEE2F9 /* Resources */,
150150
7298779024C6080000E92B48 /* Reset Config */,
151+
7255C2122539D79B005B1DBA /* Reset Simulator */,
151152
);
152153
buildRules = (
153154
);
@@ -169,6 +170,7 @@
169170
72C7A9F323E04D6300041782 /* Sources */,
170171
72C7A9F423E04D6300041782 /* Frameworks */,
171172
72C7A9F523E04D6300041782 /* Resources */,
173+
7255C20D2539D723005B1DBA /* Reset Simulators */,
172174
);
173175
buildRules = (
174176
);
@@ -242,6 +244,42 @@
242244
/* End PBXResourcesBuildPhase section */
243245

244246
/* Begin PBXShellScriptBuildPhase section */
247+
7255C20D2539D723005B1DBA /* Reset Simulators */ = {
248+
isa = PBXShellScriptBuildPhase;
249+
buildActionMask = 2147483647;
250+
files = (
251+
);
252+
inputFileListPaths = (
253+
);
254+
inputPaths = (
255+
);
256+
name = "Reset Simulators";
257+
outputFileListPaths = (
258+
);
259+
outputPaths = (
260+
);
261+
runOnlyForDeploymentPostprocessing = 0;
262+
shellPath = /bin/sh;
263+
shellScript = "echo \"Shutdown and reset Simulators\"\nxcrun simctl shutdown all\nxcrun simctl erase all\n";
264+
};
265+
7255C2122539D79B005B1DBA /* Reset Simulator */ = {
266+
isa = PBXShellScriptBuildPhase;
267+
buildActionMask = 2147483647;
268+
files = (
269+
);
270+
inputFileListPaths = (
271+
);
272+
inputPaths = (
273+
);
274+
name = "Reset Simulator";
275+
outputFileListPaths = (
276+
);
277+
outputPaths = (
278+
);
279+
runOnlyForDeploymentPostprocessing = 0;
280+
shellPath = /bin/sh;
281+
shellScript = "echo \"Shutdown and reset Simulators\"\nxcrun simctl shutdown all\nxcrun simctl erase all\n";
282+
};
245283
7298779024C6080000E92B48 /* Reset Config */ = {
246284
isa = PBXShellScriptBuildPhase;
247285
buildActionMask = 2147483647;
@@ -258,7 +296,7 @@
258296
);
259297
runOnlyForDeploymentPostprocessing = 0;
260298
shellPath = /bin/sh;
261-
shellScript = "if [ -f ../../.env-vars ]\nthen\n echo \"Import secure variables\"\n source ../../.env-vars\n sed -i '' 's,'\\\"${INSTANA_REPORTING_KEY}\\\"',\\\"KEY\\\",g' ${PRODUCT_NAME}/Config.swift\n sed -i '' 's,'\\\"${INSTANA_REPORTING_URL}\\\"',\\\"URL\\\",g' ${PRODUCT_NAME}/Config.swift\nfi\n";
299+
shellScript = "if [ -f ../../.env-vars ]\nthen\n echo \"Import secure variables\"\n source ../../.env-vars\n sed -i '' 's,'${INSTANA_REPORTING_KEY}',INSTANA_REPORTING_KEY,g' ${PRODUCT_NAME}/Config.swift\n sed -i '' 's,'${INSTANA_REPORTING_URL}',INSTANA_REPORTING_URL,g' ${PRODUCT_NAME}/Config.swift\nfi\n";
262300
};
263301
72C7A9F123E04B2000041782 /* Set Config */ = {
264302
isa = PBXShellScriptBuildPhase;
@@ -276,7 +314,7 @@
276314
);
277315
runOnlyForDeploymentPostprocessing = 0;
278316
shellPath = /bin/sh;
279-
shellScript = "if [ -f ../../.env-vars ]\nthen\n echo \"Import secure variables\"\n source ../../.env-vars\n sed -i '' 's,\\\"KEY\\\",'\\\"${INSTANA_REPORTING_KEY}\\\"',g' ${PRODUCT_NAME}/Config.swift\n sed -i '' 's,\\\"URL\\\",'\\\"${INSTANA_REPORTING_URL}\\\"',g' ${PRODUCT_NAME}/Config.swift\nfi\n";
317+
shellScript = "if [ -f ../../.env-vars ]\nthen\n echo \"Import secure variables\"\n source ../../.env-vars\n sed -i '' 's,INSTANA_REPORTING_KEY,'${INSTANA_REPORTING_KEY}',g' ${PRODUCT_NAME}/Config.swift\n sed -i '' 's,INSTANA_REPORTING_URL,'${INSTANA_REPORTING_URL}',g' ${PRODUCT_NAME}/Config.swift\nfi\n";
280318
};
281319
72E6C5D223E2EA1D009E118E /* SwiftLint */ = {
282320
isa = PBXShellScriptBuildPhase;

Dev/iOSAgentExample/Config.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
import Foundation
1010

11-
let InstanaKey = "KEY"
12-
let InstanaURL = "URL"
11+
let InstanaKey = "INSTANA_REPORTING_KEY"
12+
let InstanaURL = "INSTANA_REPORTING_URL"

Sources/InstanaAgent/Beacons/Beacons Types/Beacon.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Foundation
22

33
/// Base class for Beacon.
44
class Beacon {
5-
65
let id = UUID()
76
var timestamp: Instana.Types.Milliseconds = Date().millisecondsSince1970
87
let viewName: String?

Sources/InstanaAgent/Beacons/Reporter.swift

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ public class Reporter {
99
typealias NetworkLoader = (URLRequest, @escaping (InstanaNetworking.Result) -> Void) -> Void
1010
var completionHandler = [Completion]()
1111
let queue: InstanaPersistableQueue<CoreBeacon>
12-
private let backgroundQueue = DispatchQueue(label: "com.instana.ios.agent.background", qos: .background)
12+
private let dispatchQueue = DispatchQueue(label: "com.instana.ios.agent.reporter", qos: .utility)
1313
private let send: NetworkLoader
14+
private let rateLimiter: ReporterRateLimiter
1415
private let batterySafeForNetworking: () -> Bool
1516
private let networkUtility: NetworkUtility
1617
private var suspendReporting: Set<InstanaConfiguration.SuspendReporting> { session.configuration.suspendReporting }
@@ -29,12 +30,14 @@ public class Reporter {
2930
init(_ session: InstanaSession,
3031
batterySafeForNetworking: @escaping () -> Bool = { InstanaSystemUtils.battery.safeForNetworking },
3132
networkUtility: NetworkUtility = NetworkUtility.shared,
33+
rateLimiter: ReporterRateLimiter? = nil,
3234
queue: InstanaPersistableQueue<CoreBeacon>? = nil,
3335
send: @escaping NetworkLoader = InstanaNetworking().send(request:completion:)) {
3436
self.networkUtility = networkUtility
3537
self.session = session
3638
self.batterySafeForNetworking = batterySafeForNetworking
3739
self.send = send
40+
self.rateLimiter = rateLimiter ?? ReporterRateLimiter(configs: session.configuration.reporterRateLimits)
3841
self.queue = queue ?? InstanaPersistableQueue<CoreBeacon>(identifier: "com.instana.ios.mainqueue", maxItems: session.configuration.maxBeaconsPerRequest)
3942
networkUtility.connectionUpdateHandler = { [weak self] connectionType in
4043
guard let self = self else { return }
@@ -44,24 +47,30 @@ public class Reporter {
4447
}
4548
InstanaApplicationStateHandler.shared.listen { [weak self] state in
4649
guard let self = self else { return }
47-
if state == .background {
50+
if state == .background, !ProcessInfo.isRunningTests {
4851
self.runBackgroundFlush()
4952
}
5053
}
51-
backgroundQueue.asyncAfter(deadline: .now() + session.configuration.preQueueUsageTime, execute: emptyPreQueueIfNeeded)
54+
dispatchQueue.asyncAfter(deadline: .now() + session.configuration.preQueueUsageTime, execute: emptyPreQueueIfNeeded)
5255
}
5356

5457
func submit(_ beacon: Beacon, _ completion: (() -> Void)? = nil) {
55-
if mustUsePrequeue {
56-
backgroundQueue.sync {
58+
dispatchQueue.async { [weak self] in
59+
guard let self = self else { return }
60+
guard self.rateLimiter.canSubmit() else {
61+
self.session.logger.add("Rate Limit reached - Beacon will be discarded", level: .warning)
62+
completion?()
63+
return
64+
}
65+
if self.mustUsePrequeue {
5766
self.preQueue.append(beacon)
67+
completion?()
68+
return
5869
}
59-
completion?()
60-
return
61-
}
62-
backgroundQueue.async(qos: .background) {
70+
6371
guard !self.queue.isFull else {
6472
self.session.logger.add("Queue is full - Beacon will be discarded", level: .warning)
73+
completion?()
6574
return
6675
}
6776
let start = Date()
@@ -102,7 +111,7 @@ public class Reporter {
102111
flushWorkItem = workItem
103112
var interval = batterySafeForNetworking() ? session.configuration.transmissionDelay : session.configuration.transmissionLowBatteryDelay
104113
interval = queue.isFull ? 0.0 : interval
105-
backgroundQueue.asyncAfter(deadline: .now() + interval, execute: workItem)
114+
dispatchQueue.asyncAfter(deadline: .now() + interval, execute: workItem)
106115
}
107116

108117
func flushQueue() {
@@ -126,23 +135,32 @@ public class Reporter {
126135
complete([], .failure(error))
127136
return
128137
}
129-
send(request) { [weak self] result in
130-
guard let self = self else { return }
131-
self.session.logger.add("Did transfer beacon\n \(beaconsAsString)")
132-
switch result {
133-
case let .failure(error):
134-
self.complete(beacons, .failure(error))
135-
case .success:
136-
self.complete(beacons, .success)
137-
}
138+
let sentSemaphore = DispatchSemaphore(value: 0)
139+
var result: InstanaNetworking.Result?
140+
send(request) { sentResult in
141+
result = sentResult
142+
sentSemaphore.signal()
143+
}
144+
_ = sentSemaphore.wait(timeout: .now() + request.timeoutInterval + 1)
145+
session.logger.add("Did transfer beacon\n \(beaconsAsString)")
146+
switch result {
147+
case let .failure(error):
148+
complete(beacons, .failure(error))
149+
case .success:
150+
complete(beacons, .success)
151+
case .none:
152+
complete(beacons, .failure(HTTPError.timeout))
138153
}
139154
}
140155

141156
func runBackgroundFlush() {
142-
guard !queue.items.isEmpty else { return }
143-
ProcessInfo.processInfo.performExpiringActivity(withReason: "BackgroundFlush") { expired in
144-
guard !expired else { return }
145-
self.flushQueue()
157+
dispatchQueue.async { [weak self] in
158+
guard let self = self else { return }
159+
guard !self.queue.items.isEmpty else { return }
160+
ProcessInfo.processInfo.performExpiringActivity(withReason: "BackgroundFlush") { expired in
161+
guard !expired else { return }
162+
self.flushQueue()
163+
}
146164
}
147165
}
148166

@@ -189,3 +207,45 @@ extension Reporter {
189207
return urlRequest
190208
}
191209
}
210+
211+
class ReporterRateLimiter {
212+
class Limiter {
213+
let maxItems: Int
214+
let timeout: TimeInterval
215+
var current = 0
216+
private lazy var queue = DispatchQueue(label: "com.instana.ios.agent.reporterratelimit.\(maxItems).\(timeout)")
217+
218+
var exceeds: Bool { current > maxItems }
219+
220+
init(maxItems: Int, timeout: TimeInterval) {
221+
self.maxItems = maxItems
222+
self.timeout = timeout
223+
scheduleReset()
224+
}
225+
226+
func scheduleReset() {
227+
queue.asyncAfter(deadline: .now() + timeout) { [weak self] in
228+
guard let self = self else { return }
229+
self.current = 0
230+
self.scheduleReset()
231+
}
232+
}
233+
234+
func signal() -> Bool {
235+
queue.sync {
236+
current += 1
237+
return exceeds
238+
}
239+
}
240+
}
241+
242+
let limiters: [Limiter]
243+
244+
init(configs: [InstanaConfiguration.ReporterRateLimitConfig]) {
245+
limiters = configs.map { Limiter(maxItems: $0.maxItems, timeout: $0.timeout) }
246+
}
247+
248+
func canSubmit() -> Bool {
249+
limiters.map { $0.signal() }.filter { $0 == true }.isEmpty
250+
}
251+
}

Sources/InstanaAgent/Configuration/InstanaConfiguraton.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ struct InstanaConfiguration: Equatable {
1919
static let defaults: Set<SuspendReporting> = []
2020
}
2121

22+
struct ReporterRateLimitConfig: Equatable {
23+
let timeout: TimeInterval
24+
let maxItems: Int
25+
}
26+
2227
enum MonitorTypes: Hashable {
2328
case http
2429
case memoryWarning
@@ -37,6 +42,8 @@ struct InstanaConfiguration: Equatable {
3742
static let gzipReport = ProcessInfo.ignoreZIPReporting ? false : true
3843
static let maxBeaconsPerRequest = 100
3944
static let preQueueUsageTime: TimeInterval = 2.0
45+
static let reporterRateLimits = [ReporterRateLimitConfig(timeout: 10, maxItems: 20),
46+
ReporterRateLimitConfig(timeout: 60 * 5, maxItems: 500)]
4047
}
4148

4249
var reportingURL: URL
@@ -49,6 +56,7 @@ struct InstanaConfiguration: Equatable {
4956
var gzipReport: Bool
5057
var maxBeaconsPerRequest: Int
5158
var preQueueUsageTime: TimeInterval
59+
var reporterRateLimits: [ReporterRateLimitConfig]
5260
var isValid: Bool { !key.isEmpty && !reportingURL.absoluteString.isEmpty }
5361

5462
static var empty: InstanaConfiguration {
@@ -65,6 +73,7 @@ struct InstanaConfiguration: Equatable {
6573
transmissionLowBatteryDelay: Defaults.transmissionLowBatteryDelay,
6674
gzipReport: Defaults.gzipReport,
6775
maxBeaconsPerRequest: Defaults.maxBeaconsPerRequest,
68-
preQueueUsageTime: Defaults.preQueueUsageTime)
76+
preQueueUsageTime: Defaults.preQueueUsageTime,
77+
reporterRateLimits: Defaults.reporterRateLimits)
6978
}
7079
}

Sources/InstanaAgent/Monitors/HTTP/InstanaURLProtocol.swift

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class InstanaURLProtocol: URLProtocol {
1414
private(set) lazy var sessionConfiguration: URLSessionConfiguration = { .default }()
1515
var marker: HTTPMarker?
1616
private var incomingTask: URLSessionTask?
17+
let markerQueue = DispatchQueue(label: "com.instana.ios.agent.InstanaURLProtocol", qos: .default)
1718

1819
convenience init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
1920
guard let request = task.originalRequest else { self.init(); return }
@@ -43,16 +44,20 @@ class InstanaURLProtocol: URLProtocol {
4344
}
4445

4546
override func startLoading() {
46-
if InstanaURLProtocol.mode == .enabled, canMark {
47-
marker = try? Instana.current?.monitors.http?.mark(request)
47+
markerQueue.sync {
48+
if InstanaURLProtocol.mode == .enabled, canMark {
49+
marker = try? Instana.current?.monitors.http?.mark(request)
50+
}
51+
let task = session.dataTask(with: request)
52+
task.resume()
4853
}
49-
let task = session.dataTask(with: request)
50-
task.resume()
5154
}
5255

5356
override func stopLoading() {
54-
session.invalidateAndCancel()
55-
if let marker = marker, case .started = marker.state { marker.cancel() }
57+
markerQueue.sync {
58+
session.invalidateAndCancel()
59+
if let marker = marker, case .started = marker.state { marker.cancel() }
60+
}
5661
}
5762
}
5863

@@ -63,11 +68,15 @@ extension InstanaURLProtocol: URLSessionTaskDelegate {
6368
} else {
6469
client?.urlProtocolDidFinishLoading(self)
6570
}
66-
marker?.finish(response: task.response, error: error)
71+
markerQueue.sync {
72+
marker?.finish(response: task.response, error: error)
73+
}
6774
}
6875

6976
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
70-
marker?.set(responseSize: HTTPMarker.Size(response: task.response ?? URLResponse(), transactionMetrics: metrics.transactionMetrics))
77+
markerQueue.sync {
78+
marker?.set(responseSize: HTTPMarker.Size(response: task.response ?? URLResponse(), transactionMetrics: metrics.transactionMetrics))
79+
}
7180
}
7281
}
7382

@@ -89,10 +98,12 @@ extension InstanaURLProtocol: URLSessionDataDelegate {
8998
willPerformHTTPRedirection response: HTTPURLResponse,
9099
newRequest request: URLRequest,
91100
completionHandler: @escaping (URLRequest?) -> Void) {
92-
marker?.set(responseSize: HTTPMarker.Size(response))
93-
marker?.finish(response: response, error: nil)
94-
marker = try? Instana.current?.monitors.http?.mark(request)
95-
completionHandler(request)
101+
markerQueue.sync {
102+
marker?.set(responseSize: HTTPMarker.Size(response))
103+
marker?.finish(response: response, error: nil)
104+
marker = try? Instana.current?.monitors.http?.mark(request)
105+
completionHandler(request)
106+
}
96107
}
97108
}
98109

0 commit comments

Comments
 (0)