Skip to content

Commit

Permalink
fix: Concurrent dependency access (#950)
Browse files Browse the repository at this point in the history
* Synchronize Dependency Access

* Added unit tests for dependency registration and resolution

* Formatting

* Remove working files

* Add dependency to Package.swift

* Update DependencyInjection.swift

Remove whitespace

* Removed second sync. Added test for multiple queues
  • Loading branch information
NQuinn27 authored Aug 5, 2024
1 parent a7cc0c5 commit 415d472
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 26 deletions.
12 changes: 10 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ let package = Package(
targets: ["PrimerSDK"]
)
],
dependencies: [
.package(url: "https://github.com/primer-io/primer-sdk-3ds-ios", from: "2.3.1")
],
targets: [
.target(
name: "PrimerSDK",
dependencies: [
.product(name: "Primer3DS", package: "primer-sdk-3ds-ios")
],
path: "Sources/PrimerSDK",
resources: [
.process("Resources"),
Expand All @@ -26,12 +32,14 @@ let package = Package(
.testTarget(
name: "Tests",
dependencies: [
.product(name: "Primer3DS", package: "primer-sdk-3ds-ios"),
.byName(name: "PrimerSDK")
],
path: "Tests/",
sources: [
"Primer/",
"Utilities/"
"3DS/",
"Utilities/",
"Primer/"
]
)
],
Expand Down
39 changes: 39 additions & 0 deletions Package.vanilla.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// swift-tools-version:5.3

import PackageDescription

let package = Package(
name: "PrimerSDK",
defaultLocalization: "en",
platforms: [
.iOS("13.1")
],
products: [
.library(
name: "PrimerSDK",
targets: ["PrimerSDK"]
)
],
targets: [
.target(
name: "PrimerSDK",
path: "Sources/PrimerSDK",
resources: [
.process("Resources"),
.copy("Classes/Third Party/PromiseKit/LICENSE")
]
),
.testTarget(
name: "Tests",
dependencies: [
.byName(name: "PrimerSDK")
],
path: "Tests/",
sources: [
"Primer/",
"Utilities/"
]
)
],
swiftLanguageVersions: [.v5]
)
59 changes: 35 additions & 24 deletions Sources/PrimerSDK/Classes/Core/Primer/DependencyInjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Created by Carl Eriksson on 10/02/2021.
//

import Foundation

@propertyWrapper
struct Dependency<T> {
var wrappedValue: T
Expand All @@ -20,6 +22,8 @@ private let _DependencyContainer = DependencyContainer()

final internal class DependencyContainer {

private static let queue: DispatchQueue = DispatchQueue(label: "primer.dependencycontainer")

private var dependencies = [String: AnyObject]()

static var shared: DependencyContainer {
Expand All @@ -36,36 +40,43 @@ final internal class DependencyContainer {

private func register<T>(_ dependency: T) {
let key = String(describing: T.self)
dependencies[key] = dependency as AnyObject
Self.queue.async(flags: .barrier) {
self.dependencies[key] = dependency as AnyObject
}
}

private func resolve<T>() -> T {
let key = String(describing: T.self)
let dependency = dependencies[key] as? T

if dependency == nil {
if key == String(describing: AppStateProtocol.self) {
let appState: AppStateProtocol = AppState()
DependencyContainer.register(appState)
return self.resolve()

} else if key == String(describing: PrimerSettingsProtocol.self) {
let primerSettings: PrimerSettingsProtocol = PrimerSettings()
DependencyContainer.register(primerSettings)
return self.resolve()

} else if key == String(describing: PrimerThemeProtocol.self) {
let primerTheme: PrimerThemeProtocol = PrimerTheme()
DependencyContainer.register(primerTheme)
return self.resolve()

return Self.queue.sync(flags: .barrier) {
if let dependency = self.dependencies[key] as? T {
return dependency
}
}

precondition(
dependency != nil,
"No dependency found for \(key)! must register a dependency before resolve."
)
let dependency: T? = self.createDependency(for: key)
if let dependency = dependency {
self.dependencies[key] = dependency as AnyObject
}

return dependency!
precondition(
dependency != nil,
"No dependency found for \(key)! must register a dependency before resolve."
)

return dependency!
}
}

private func createDependency<T>(for key: String) -> T? {
switch key {
case String(describing: AppStateProtocol.self):
return AppState() as? T
case String(describing: PrimerSettingsProtocol.self):
return PrimerSettings() as? T
case String(describing: PrimerThemeProtocol.self):
return PrimerTheme() as? T
default:
return nil
}
}
}
147 changes: 147 additions & 0 deletions Tests/Primer/DependencyContainerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//
// DependencyContainerTests.swift
//
//
// Created by Niall Quinn on 23/07/24.
//

import XCTest
@testable import PrimerSDK

class DependencyContainerTests: XCTestCase {

override func setUp() {
super.setUp()
}

func test_concurrentRegistrationAndResolution() {
let expectation = self.expectation(description: "Concurrent operations completed")
let operationsCount = 1000
let queue = DispatchQueue(label: "testQueue", attributes: .concurrent)

let appState = MockAppState()

for i in 0..<operationsCount {
queue.async {
if i % 2 == 0 {
DependencyContainer.register(appState)
} else {
_ = DependencyContainer.resolve() as AppStateProtocol
}
}
}

queue.async(flags: .barrier) {
expectation.fulfill()
}

waitForExpectations(timeout: 20, handler: nil)
}

func test_concurrentRegistrationOfSameType() {
let expectation = self.expectation(description: "Concurrent registrations completed")
let operationsCount = 1000
let queue = DispatchQueue(label: "testQueue", attributes: .concurrent)

let appState = MockAppState()

for i in 0..<operationsCount {
queue.async {
DependencyContainer.register(appState)
}
}

queue.async(flags: .barrier) {
expectation.fulfill()
}

waitForExpectations(timeout: 5, handler: nil)

let resolvedState: AppStateProtocol = DependencyContainer.resolve()

XCTAssertNotNil(resolvedState)
}

func test_concurrentResolutionOfNonExistentDependency() {
let expectation = self.expectation(description: "Concurrent resolutions completed")
let operationsCount = 1000
let queue = DispatchQueue(label: "testQueue", attributes: .concurrent)

for _ in 0..<operationsCount {
queue.async {
XCTAssertNil(DependencyContainer.resolve() as Int?)
}
}

queue.async(flags: .barrier) {
expectation.fulfill()
}

waitForExpectations(timeout: 5, handler: nil)
}

func test_concurrentRegistrationAndResolutionOfMultipleTypes() {
let expectation = self.expectation(description: "Concurrent operations completed")
let operationsCount = 1000
let queue = DispatchQueue(label: "testQueue", attributes: .concurrent)

let appState = MockAppState()
let settings = MockPrimerSettings()

for i in 0..<operationsCount {
queue.async {
switch i % 2 {
case 0:
DependencyContainer.register(appState)
case 1:
DependencyContainer.register(settings)
default:
break
}
}
}

for _ in 0..<operationsCount {
queue.async {
_ = DependencyContainer.resolve() as AppStateProtocol
_ = DependencyContainer.resolve() as PrimerSettingsProtocol
}
}

queue.async(flags: .barrier) {
expectation.fulfill()
}

waitForExpectations(timeout: 5, handler: nil)

// Verify that the last value of each type was registered
XCTAssertNotNil(DependencyContainer.resolve() as AppStateProtocol)
XCTAssertNotNil(DependencyContainer.resolve() as PrimerSettingsProtocol)
}

func test_concurrentQueues() {
let expectation = XCTestExpectation(description: "Concurrent access")
let iterationCount = 1000
let concurrentQueues = 4

for _ in 0..<concurrentQueues {
DispatchQueue.global().async {
for i in 0..<iterationCount {
// Register a unique dependency
let dependency = MockPrimerSettings()
DependencyContainer.register(dependency)

// Immediately resolve the dependency
let resolved: MockPrimerSettings = DependencyContainer.resolve()

// Verify that the resolved dependency matches the registered one
XCTAssertEqual(dependency.paymentHandling, resolved.paymentHandling)
}

expectation.fulfill()
}
}

wait(for: [expectation], timeout: 10.0)
}
}

0 comments on commit 415d472

Please sign in to comment.