Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 20 additions & 15 deletions Sources/InstanaAgent/Configuration/InstanaConfiguraton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import Foundation
case none
}

struct InstanaConfiguration: Equatable {
// Use a reference type to avoid a copy when having concurrency
class InstanaConfiguration {
enum SuspendReporting {
/// Reporting is suspended while the device battery is low.
case lowBattery
Expand Down Expand Up @@ -63,7 +64,7 @@ struct InstanaConfiguration: Equatable {
var reporterSendLowBatteryDebounce: Instana.Types.Seconds
var maxRetries: Int
var gzipReport: Bool
var maxBeaconsPerRequest: Int
var maxBeaconsPerRequest: Int = 0
var maxQueueSize: Int
var preQueueUsageTime: TimeInterval
var reporterRateLimits: [ReporterRateLimitConfig]
Expand All @@ -73,19 +74,23 @@ struct InstanaConfiguration: Equatable {
.default(key: "", reportingURL: URL(string: "https://www.instana.com")!, httpCaptureConfig: .none)
}

required init(reportingURL: URL, key: String, httpCaptureConfig: HTTPCaptureConfig) {
self.reportingURL = reportingURL
self.key = key
self.httpCaptureConfig = httpCaptureConfig
suspendReporting = SuspendReporting.defaults
monitorTypes = MonitorTypes.current
reporterSendDebounce = Defaults.reporterSendDebounce
reporterSendLowBatteryDebounce = Defaults.reporterSendLowBatteryDebounce
maxRetries = Defaults.maxRetries
gzipReport = Defaults.gzipReport
maxBeaconsPerRequest = Defaults.maxBeaconsPerRequest
maxQueueSize = Defaults.maxQueueSize
preQueueUsageTime = Defaults.preQueueUsageTime
reporterRateLimits = Defaults.reporterRateLimits
}

static func `default`(key: String, reportingURL: URL, httpCaptureConfig: HTTPCaptureConfig = .automatic) -> InstanaConfiguration {
self.init(reportingURL: reportingURL,
key: key,
httpCaptureConfig: httpCaptureConfig,
suspendReporting: SuspendReporting.defaults,
monitorTypes: MonitorTypes.current,
reporterSendDebounce: Defaults.reporterSendDebounce,
reporterSendLowBatteryDebounce: Defaults.reporterSendLowBatteryDebounce,
maxRetries: Defaults.maxRetries,
gzipReport: Defaults.gzipReport,
maxBeaconsPerRequest: Defaults.maxBeaconsPerRequest,
maxQueueSize: Defaults.maxQueueSize,
preQueueUsageTime: Defaults.preQueueUsageTime,
reporterRateLimits: Defaults.reporterRateLimits)
self.init(reportingURL: reportingURL, key: key, httpCaptureConfig: httpCaptureConfig)
}
}
5 changes: 3 additions & 2 deletions Sources/InstanaAgent/Configuration/InstanaProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Foundation
typealias MetaData = [String: String]
typealias HTTPHeader = [String: String]

struct InstanaProperties: Equatable {
// Make this a reference type to avoid a copy for concurrency
class InstanaProperties {
struct User: Equatable {
static let userValueMaxLength = 128

Expand Down Expand Up @@ -51,7 +52,7 @@ struct InstanaProperties: Equatable {
self.view = Self.validate(view: view)
}

mutating func appendMetaData(_ key: String, _ value: String) {
func appendMetaData(_ key: String, _ value: String) {
let key = MetaData.validate(key: key)
let value = MetaData.validate(value: value)
if metaData.count < MetaData.Max.numberOfMetaEntries {
Expand Down
4 changes: 2 additions & 2 deletions Sources/InstanaAgent/Instana.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ import Foundation
/// Default: No HTTP header fields are captured. Keywords must be provided explicitly
///
/// - Parameters:
/// - regex: An array of NSRegularExpression objects to match the key of HTTP request/response headers that you want to capture.
/// - regex: An array of NSRegularExpression objects to match the key of HTTP request/response headers that you want to capture.
@objc
public static func setCaptureHeaders(matching regex: [NSRegularExpression]) {
Instana.current?.monitors.http?.filter.headerFieldsRegEx = regex
Instana.current?.monitors.http?.filter.headerFieldsRegEx = AtomicArray(regex)
}
}
4 changes: 2 additions & 2 deletions Sources/InstanaAgent/Monitors/HTTP/HTTPMonitorFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

class HTTPMonitorFilter {
private let redactionHandler: RedactionHandler
@Atomic var headerFieldsRegEx = [NSRegularExpression]()
var headerFieldsRegEx = AtomicArray<NSRegularExpression>()

init(redactionHandler: RedactionHandler = .default) {
self.redactionHandler = redactionHandler
Expand All @@ -13,7 +13,7 @@ class HTTPMonitorFilter {
}

func setRedaction(regex: [NSRegularExpression]) {
redactionHandler.regex = Set(regex)
redactionHandler.regex = AtomicSet(regex)
}

func filterHeaderFields(_ header: HTTPHeader?) -> HTTPHeader? {
Expand Down
6 changes: 3 additions & 3 deletions Sources/InstanaAgent/Monitors/HTTP/IgnoreURLHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import Foundation

struct IgnoreURLHandler {
/// Monitor ignores URLs that match the given regular expressions
@Atomic static var regex = Set<NSRegularExpression>()
static var regex = AtomicSet<NSRegularExpression>()

/// Monitor ignores the exact URLs given in this collection
@Atomic static var exactURLs = Set<URL>()
static var exactURLs = AtomicSet<URL>()

/// All sessions will be ignored from HTTP monitoring
@Atomic static var urlSessions = Set<URLSession>()
static var urlSessions = AtomicSet<URLSession>()

static func ignore(session: URLSession) {
urlSessions.insert(session)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ extension URLSessionConfiguration {
}
}

@Atomic static var all = [URLSessionConfiguration]()
static var all = AtomicArray<URLSessionConfiguration>()

static func removeAllInstanaURLProtocol() {
all.forEach { $0.protocolClasses?.removeAll(where: { (protocolClass) -> Bool in
Expand Down
4 changes: 2 additions & 2 deletions Sources/InstanaAgent/Monitors/HTTP/RedactionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class RedactionHandler {
return RedactionHandler(regex: regex)
}

@Atomic var regex = Set<NSRegularExpression>()
var regex = AtomicSet<NSRegularExpression>()

init(regex: [NSRegularExpression]) {
self.regex = Set(regex)
self.regex = AtomicSet(regex)
}

func redact(url: URL) -> URL {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class InstanaApplicationStateHandler {
}

typealias StateUpdater = (State) -> Void
private var stateUpdateHandler = [StateUpdater]()
private var stateUpdateHandler = AtomicArray<StateUpdater>()

init() {
_ = NotificationCenter.default.addObserver(forName: Application.didBecomeActiveNotification, object: nil, queue: nil) { _ in
Expand Down
151 changes: 145 additions & 6 deletions Sources/InstanaAgent/Utils/PropertyWrapper/Atomic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import Foundation

/// Don't use this propertyWrapper for value types, esp. Collections (Array, Dictionary, Set)
/// This can have unexpected results: see https://www.donnywals.com/why-your-atomic-property-wrapper-doesnt-work-for-collection-types/
@propertyWrapper
struct Atomic<Value> {
private var value: Value
Expand All @@ -14,19 +16,156 @@ struct Atomic<Value> {
}

var wrappedValue: Value {
get { return load() }
set { store(newValue: newValue) }
get { lock.atomic { value } }
set { lock.atomic { value = newValue } }
}
}

extension NSLock {
func atomic<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer {
unlock()
}
return try closure()
}
}

// Inspired by donnywals https://www.donnywals.com/why-your-atomic-property-wrapper-doesnt-work-for-collection-types/
// We must make this a reference type to avoid getting a copy for each different thread (expected value type behavior)
class AtomicDictionary<Key: Hashable, Value>: CustomDebugStringConvertible {
private var dict = [Key: Value]()
private let lock = NSLock()

subscript(key: Key) -> Value? {
get { lock.atomic { dict[key] } }
set { lock.atomic { dict[key] = newValue } }
}

var debugDescription: String {
lock.atomic { dict.debugDescription }
}
}

extension AtomicDictionary where Value: Equatable {
static func == <Key: Hashable, Value: Equatable>(lhs: AtomicDictionary<Key, Value>, rhs: AtomicDictionary<Key, Value>) -> Bool {
lhs.dict == rhs.dict
}
}

class AtomicArray<T>: CustomDebugStringConvertible, Sequence, Collection {
private var array: [T]
private let lock = NSLock()

var startIndex: Int {
lock.atomic { array.startIndex }
}

var endIndex: Int {
lock.atomic { array.endIndex }
}

init(_ array: [T] = []) {
self.array = array
}

subscript(index: Int) -> T {
get { lock.atomic { array[index] } }
set { lock.atomic { array[index] = newValue } }
}

func append(_ newElement: T) {
lock.atomic { array.append(newElement) }
}

var debugDescription: String {
lock.atomic { array.debugDescription }
}

func removeAll() {
lock.atomic { array.removeAll() }
}

func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows {
try lock.atomic {
try array.removeAll(where: shouldBeRemoved)
}
}

private func load() -> Value {
func index(after index: Int) -> Int {
lock.atomic {
array.index(after: index)
}
}

func index(before index: Int) -> Int {
lock.atomic {
array.index(before: index)
}
}

func makeIterator() -> IndexingIterator<[T]> {
lock.lock()
defer { lock.unlock() }
return value
return array.makeIterator()
}
}

extension AtomicArray where T: Equatable {
func contains(_ element: T) -> Bool {
lock.atomic { array.contains(element) }
}

static func == <T: Equatable>(lhs: AtomicArray<T>, rhs: AtomicArray<T>) -> Bool {
lhs.array == rhs.array
}
}

class AtomicSet<T: Hashable>: CustomDebugStringConvertible, Sequence {
private var set: Set<T>
private let lock = NSLock()

init(_ set: [T] = []) {
self.set = Set(set)
}

func contains(_ member: T) -> Bool {
lock.atomic { set.contains(member) }
}

private mutating func store(newValue: Value) {
@discardableResult func insert(_ newMember: T) -> (inserted: Bool, memberAfterInsert: T) {
lock.atomic { set.insert(newMember) }
}

var debugDescription: String {
lock.atomic { set.debugDescription }
}

func makeIterator() -> Set<T>.Iterator {
lock.lock()
defer { lock.unlock() }
value = newValue
return set.makeIterator()
}

func formUnion<S>(_ other: S) where T == S.Element, S: Sequence {
lock.atomic { set.formUnion(other) }
}

func removeAll() {
lock.atomic { set.removeAll() }
}

var count: Int {
lock.atomic { set.count }
}

var first: T? {
lock.atomic { set.first }
}
}

extension AtomicSet: Equatable {
static func == <T>(lhs: AtomicSet<T>, rhs: AtomicSet<T>) -> Bool {
lhs.set == rhs.set
}
}
4 changes: 2 additions & 2 deletions Tests/InstanaAgentTests/Beacons/ReporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ extension ReporterTests {
extension ReporterTests {

func createMockSession(_ delay: Instana.Types.Seconds = 0.0, preQueueUsageTime: Instana.Types.Seconds = 0.0, suspend: Set<InstanaConfiguration.SuspendReporting> = [], gzip: Bool = false) -> InstanaSession {
var config = InstanaConfiguration.mock(gzipReport: gzip)
let config = InstanaConfiguration.mock(gzipReport: gzip)
config.reporterSendDebounce = delay
config.reporterSendLowBatteryDebounce = delay
config.preQueueUsageTime = preQueueUsageTime
Expand Down Expand Up @@ -1208,7 +1208,7 @@ extension ReporterTests {
}

func mockBeaconSubmission(_ loadResult: Swift.Result<Int, Error>, resultCallback: @escaping (BeaconResult) -> Void) {
var config = InstanaConfiguration.mock
let config = InstanaConfiguration.mock
config.reporterSendDebounce = 0.0
config.reporterSendLowBatteryDebounce = 0.0
let reporter = Reporter(.mock(configuration: config),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class InstanaPropertyHandlerTests: InstanaTestCase {
let max = MetaData.Max.numberOfMetaEntries
let keys = (0..<max).map { "key \($0)" }
let values = (0..<max).map { "value \($0)" }
var properties = InstanaProperties()
let properties = InstanaProperties()

// When
(0..<max).forEach {index in
Expand All @@ -42,7 +42,7 @@ class InstanaPropertyHandlerTests: InstanaTestCase {
let max = MetaData.Max.numberOfMetaEntries
let keys = (0...max).map { "key \($0)" }
let values = (0...max).map { "value \($0)" }
var properties = InstanaProperties()
let properties = InstanaProperties()

// When
(0..<max).forEach {index in
Expand Down Expand Up @@ -88,7 +88,7 @@ class InstanaPropertiesTests: XCTestCase {

func test_appendMetaData() {
// Given
var properties = InstanaProperties()
let properties = InstanaProperties()

// When
properties.appendMetaData("Key", "Value")
Expand All @@ -103,7 +103,7 @@ class InstanaPropertiesTests: XCTestCase {
let key = (0...MetaData.Max.lengthMetaKey).map {_ in "K" }.joined()
let value = (0...MetaData.Max.lengthMetaValue).map {_ in "V" }.joined()

var properties = InstanaProperties()
let properties = InstanaProperties()

// When
properties.appendMetaData(key, value)
Expand All @@ -120,7 +120,7 @@ class InstanaPropertiesTests: XCTestCase {
// Given
let maxLength = InstanaProperties.viewMaxLength
let view = (0..<maxLength).map {_ in "V" }.joined()
var properties = InstanaProperties()
let properties = InstanaProperties()

// When
properties.view = view
Expand Down Expand Up @@ -148,7 +148,7 @@ class InstanaPropertiesTests: XCTestCase {
// Given
let maxLength = InstanaProperties.viewMaxLength
let view = (0...maxLength).map {_ in "V" }.joined()
var properties = InstanaProperties()
let properties = InstanaProperties()

// When
properties.view = view
Expand Down
Loading