Skip to content

Commit

Permalink
Merge pull request #21 from SergeyKrupov/threadsafe_context
Browse files Browse the repository at this point in the history
Threadsafe context
  • Loading branch information
AndreyZarembo authored Nov 28, 2018
2 parents 1de718d + 3186349 commit dd20aae
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 49 deletions.
4 changes: 4 additions & 0 deletions EasyDi.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
8BEE13521F9A27C800A02331 /* EasyDi.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEE13501F9A27C800A02331 /* EasyDi.h */; settings = {ATTRIBUTES = (Public, ); }; };
8BEE13561F9A27EA00A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; };
8BEE13571F9A27F200A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; };
A5ABB84321A5522400C96320 /* Test_Threadsafety.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */; };
C3614B541F1C8B6800B1F4A1 /* Test_Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B451F1C8B5F00B1F4A1 /* Test_Context.swift */; };
C3614B551F1C8B6800B1F4A1 /* Test_CrossAssemblyInjections.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B461F1C8B5F00B1F4A1 /* Test_CrossAssemblyInjections.swift */; };
C3614B561F1C8B6800B1F4A1 /* Test_Injections.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B471F1C8B5F00B1F4A1 /* Test_Injections.swift */; };
Expand Down Expand Up @@ -54,6 +55,7 @@
8BEE13501F9A27C800A02331 /* EasyDi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EasyDi.h; sourceTree = "<group>"; };
8BEE13511F9A27C800A02331 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8BEE13551F9A27EA00A02331 /* EasyDi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyDi.swift; sourceTree = "<group>"; };
A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_Threadsafety.swift; sourceTree = "<group>"; };
C3614B3B1F1C8AE900B1F4A1 /* EasyDi-iOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "EasyDi-iOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
C3614B441F1C8B5F00B1F4A1 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; };
C3614B451F1C8B5F00B1F4A1 /* Test_Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Test_Context.swift; path = Tests/Test_Context.swift; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -137,6 +139,7 @@
D2B16C962123116500CF69E8 /* Test_ImplicitlyUnwrappedOptional.swift */,
C3614B4A1F1C8B5F00B1F4A1 /* Test_Scope.swift */,
C3614B4B1F1C8B5F00B1F4A1 /* Test_StructsInjection.swift */,
A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -366,6 +369,7 @@
C3614B541F1C8B6800B1F4A1 /* Test_Context.swift in Sources */,
C3614B561F1C8B6800B1F4A1 /* Test_Injections.swift in Sources */,
C3614B5A1F1C8B6800B1F4A1 /* Test_StructsInjection.swift in Sources */,
A5ABB84321A5522400C96320 /* Test_Threadsafety.swift in Sources */,
E6DCF5C61F2F62A600D9F8BC /* Test_CrossAssemblyInjections_SingletonCycle.swift in Sources */,
C3614B581F1C8B6800B1F4A1 /* Test_ProtocolBasedInjection.swift in Sources */,
);
Expand Down
106 changes: 57 additions & 49 deletions Sources/EasyDi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import Foundation

public typealias InjectableObject = Any

public protocol DIContextLocker {
func lock()
func unlock()
}

public struct DIContextEmptyLocker {
func lock() {}
func unlock() {}
}

extension NSRecursiveLock: DIContextLocker {
}

/// This class is used to join assembly instances into separated shared group.
///
/// All assemblies with one context shares object graph stack.
Expand All @@ -23,27 +36,27 @@ public typealias InjectableObject = Any
/// ```
///
public final class DIContext {

fileprivate lazy var syncQueue:DispatchQueue = Dispatch.DispatchQueue(label: "EasyDi Context Sync Queue", qos: .userInteractive)


public static var defaultInstance = DIContext()
fileprivate var assemblies:[String:Assembly] = [:]
fileprivate var assemblies: [String: Assembly] = [:]

var objectGraphStorage: [String: InjectableObject] = [:]

var objectGraphStackDepth:Int = 0
var objectGraphStackDepth: Int = 0
let locker: DIContextLocker

/// All lazy singletons are stored here.
///
/// Dictionary key is **key** parameter from **define** method
var singletons:[String: InjectableObject] = [:]
var singletons: [String: InjectableObject] = [:]

/// Array of applyed substitutions
///
/// Dictionary key is **key** parameter from **define** method
internal var substitutions:[String: UntypedPatchClosure] = [:]
var substitutions: [String: UntypedPatchClosure] = [:]

public init() {}
public init(locker: DIContextLocker = NSRecursiveLock()) {
self.locker = locker
}

/// This method creates assembly instance based on it's return type.
///
Expand All @@ -53,7 +66,6 @@ public final class DIContext {
/// lazy var anotherAssembly: AnotherAssemblyClass = self.context.assembly()
/// ```
public func assembly<AssemblyType: Assembly>() -> AssemblyType {

let instance = self.instance(for: AssemblyType.self)
return castAssemblyInstance(instance, asType: AssemblyType.self)
}
Expand All @@ -68,21 +80,18 @@ public final class DIContext {
/// var assembly = context.instance(for AssemblyClass.self)
/// ```
public func instance(for assemblyType: Assembly.Type) -> Assembly {

var instance: Assembly? = nil
syncQueue.sync {
let assemblyClassName:String = String(reflecting: assemblyType)
if let existingInstance = self.assemblies[assemblyClassName] {
instance = existingInstance
}
else {
let newInstance = assemblyType.newInstance()
newInstance.context = self
self.assemblies[assemblyClassName] = newInstance
instance = newInstance
}
locker.lock(); defer { locker.unlock() }

let assemblyClassName = String(reflecting: assemblyType)
if let existingInstance = self.assemblies[assemblyClassName] {
return existingInstance
}
else {
let newInstance = assemblyType.newInstance()
newInstance.context = self
self.assemblies[assemblyClassName] = newInstance
return newInstance
}
return instance!
}
}

Expand Down Expand Up @@ -146,14 +155,14 @@ public enum Scope {
/// ```
open class Assembly: AssemblyInternal {

public internal(set) var context: DIContext!
public internal(set) weak var context: DIContext!

/// This method creates assembly for specified context or default context if no parameters provided
///
/// - parameter context: DIContext object which assembly should belong to
///
/// - returns: Assembly instance
public static func instance(from context: DIContext = DIContext.defaultInstance)->Self {
public static func instance(from context: DIContext = DIContext.defaultInstance) -> Self {

let instance = context.instance(for: self)
return castAssemblyInstance(instance, asType: self)
Expand All @@ -178,18 +187,19 @@ open class Assembly: AssemblyInternal {
public func addSubstitution<ObjectType: InjectableObject>(
for simpleDefinitionKey: String,
with substitutionClosure: @escaping SubstitutionClosure<ObjectType>) {
let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey
self.context.substitutions[definitionKey] = substitutionClosure

let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey
context.substitutions[definitionKey] = substitutionClosure
}

/// This method removes substitution from assembly
///
/// - parameter definitionKey: should exactly match method or property name of substituting dependency
///
public func removeSubstitution(for simpleDefinitionKey: String) {
let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey
self.context.substitutions[definitionKey] = nil

let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey
context.substitutions[definitionKey] = nil
}

/// The method defines return-only placeholder for object.
Expand Down Expand Up @@ -221,7 +231,7 @@ open class Assembly: AssemblyInternal {
public func definePlaceholder<ObjectType: InjectableObject>(key: String = #function) -> ObjectType {

let closure: DefinitionClosure<ObjectType>? = nil
return self.define(key: key, definitionClosure: closure)
return define(key: key, definitionClosure: closure)
}

/// This method defines injection into existing object without return
Expand Down Expand Up @@ -313,7 +323,7 @@ open class Assembly: AssemblyInternal {
init initClosure: @autoclosure @escaping () -> ObjectType,
inject injectClosure: ObjectInjectClosure<ObjectType>? = nil ) -> ResultType {

return self.define(key: key, definitionKey: definitionKey, scope: scope) { (definition:Definition<ObjectType>) in
return define(key: key, definitionKey: definitionKey, scope: scope) { (definition:Definition<ObjectType>) in
definition.initClosure = initClosure
definition.injectClosure = injectClosure
}
Expand All @@ -338,39 +348,38 @@ open class Assembly: AssemblyInternal {
definitionKey simpleDefinitionKey: String = #function,
scope: Scope = .objectGraph,
definitionClosure: DefinitionClosure<ObjectType>? = nil) -> ResultType {

// Objects are stored in context by key made of Assembly class name and name of var or method
let key: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleKey
let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey

var result:ObjectType

guard let context = self.context else {
fatalError("Assembly has no context to work in")
fatalError("Associated context doesn't exists anymore")
}

context.locker.lock(); defer { context.locker.unlock() }

// Objects are stored in context by key made of Assembly class name and name of var or method
let key: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleKey
let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey

var result: ObjectType

// First of all it checks if there's substitution for this var or method
if let substitutionClosure = self.context.substitutions[definitionKey] {
if let substitutionClosure = context.substitutions[definitionKey] {

let substitutionObject = substitutionClosure()
guard let object = substitutionObject as? ResultType else {
fatalError("Expected type: \(ResultType.self), received: \(type(of: substitutionObject))")
}
return object


// Next check for existing singletons
} else if scope == .lazySingleton, let singleton = self.context.singletons[key] {
} else if scope == .lazySingleton, let singleton = context.singletons[key] {

result = singleton as! ObjectType

// And trying to return object from graph
} else if let objectFromStack = context.objectGraphStorage[key],
scope != .prototype,
let unwrappedObject = objectFromStack as? ObjectType {

result = unwrappedObject

} else {

// Create Definition object to store injections and dependencies information
Expand Down Expand Up @@ -400,9 +409,8 @@ open class Assembly: AssemblyInternal {
}

// And save singletons
if self.context.singletons[key] == nil, scope == .lazySingleton {

self.context.singletons[key] = result
if context.singletons[key] == nil, scope == .lazySingleton {
context.singletons[key] = result
}

guard let finalResult = result as? ResultType else {
Expand Down
65 changes: 65 additions & 0 deletions Tests/Test_Threadsafety.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Test_Threadsafety.swift
// EasyDi-iOS-Tests
//
// Created by Sergey V. Krupov on 21.11.2018.
// Copyright © 2018 AndreyZarembo. All rights reserved.
//

import XCTest
import EasyDi

fileprivate protocol SomeProtocol {
}

fileprivate class SomeObject: SomeProtocol {
var values = Array<String>(repeating: "", count: 4000)
}

fileprivate class TestAssembly: Assembly {

var someObject: SomeProtocol {
return define(init: SomeObject()) {
for i in 0 ..< $0.values.count {
$0.values[i] = self.getSomeValue(at: i)
}
return $0
}
}

// Сделано для того, чтобы стабильно воспроизводить падение. Вряд ли в реальном приложении будет такой код.
private func getSomeValue(at index: Int) -> String {
return define(key: "getSomeValue_\(index)", init: "value-\(index)")
}
}

final class Test_Threadsafety: XCTestCase {

func test_ThreadSafety() {

let context = DIContext()
let assembly = TestAssembly.instance(from: context)

// Явно создаю 2 потока, т.к. не известно на скольких потоках будет работать concurrent dispatch queue

let queue1 = DispatchQueue(label: "Queue1")
let expectation1 = expectation(description: "Queue-1")
queue1.async {
for _ in 1 ..< 10 {
_ = assembly.someObject
}
expectation1.fulfill()
}

let queue2 = DispatchQueue(label: "Queue2")
let expectation2 = expectation(description: "Queue-2")
queue2.async {
for _ in 1 ..< 10 {
_ = assembly.someObject
}
expectation2.fulfill()
}

wait(for: [expectation1, expectation2], timeout: 10)
}
}

0 comments on commit dd20aae

Please sign in to comment.