Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/resilience implementation #1

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
36 changes: 36 additions & 0 deletions Package.resolved
@@ -1,6 +1,24 @@
{
"object": {
"pins": [
{
"package": "CircuitBreaker",
"repositoryURL": "https://github.com/Kitura/CircuitBreaker.git",
"state": {
"branch": null,
"revision": "bd4255762e48cc3748a448d197f1297a4ba705f7",
"version": "5.1.0"
}
},
{
"package": "LoggerAPI",
"repositoryURL": "https://github.com/Kitura/LoggerAPI.git",
"state": {
"branch": null,
"revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb",
"version": "1.9.200"
}
},
{
"package": "PromiseKit",
"repositoryURL": "https://github.com/mxcl/PromiseKit.git",
Expand All @@ -10,6 +28,15 @@
"version": "6.17.0"
}
},
{
"package": "SQLite.swift",
"repositoryURL": "https://github.com/stephencelis/SQLite.swift.git",
"state": {
"branch": null,
"revision": "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
"version": "0.14.1"
}
},
{
"package": "swift-atomics",
"repositoryURL": "https://github.com/apple/swift-atomics.git",
Expand All @@ -19,6 +46,15 @@
"version": "1.0.2"
}
},
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "32e8d724467f8fe623624570367e3d50c5638e46",
"version": "1.5.2"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Expand Up @@ -15,14 +15,16 @@ let package = Package(
targets: ["ABSmartly"])
],
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", .upToNextMajor(from: "0.14.1")),
.package(url: "https://github.com/Kitura/CircuitBreaker.git", .upToNextMajor(from: "5.0.3")),
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/mxcl/PromiseKit.git", .upToNextMajor(from: "6.8.4")),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", .upToNextMajor(from: "4.3.0"))
],
targets: [
.target(
name: "ABSmartly",
dependencies: [.product(name: "Atomics", package: "swift-atomics"), "PromiseKit", "SwiftyJSON"],
dependencies: [.product(name: "Atomics", package: "swift-atomics"), "PromiseKit", "SwiftyJSON", "CircuitBreaker", .product(name: "SQLite", package: "SQLite.swift")],
path: "Sources/ABSmartly"),
.testTarget(
name: "ABSmartlyTests",
Expand Down
14 changes: 12 additions & 2 deletions Sources/ABSmartly/ABSmartlyConfig.swift
Expand Up @@ -7,26 +7,36 @@ public class ABSmartlyConfig {
var contextEventLogger: ContextEventLogger?
var variableParser: VariableParser?
var client: Client?
var resilienceConfig: ResilienceConfig?

public init() {
}

public convenience init(client: Client) {
print("Hello, world!")
self.init(
contextDataProvider: nil, contextEventHandler: nil, contextEventLogger: nil, variableParser: nil,
scheduler: nil, client: client)
scheduler: nil, client: client, resilienceConfig: nil)
}

public convenience init(client: Client, resilienceConfig: ResilienceConfig) {
print("Hello, world!")
self.init(
contextDataProvider: nil, contextEventHandler: nil, contextEventLogger: nil, variableParser: nil,
scheduler: nil, client: client, resilienceConfig: resilienceConfig)
}

public init(
contextDataProvider: ContextDataProvider?, contextEventHandler: ContextEventHandler?,
contextEventLogger: ContextEventLogger?,
variableParser: VariableParser?, scheduler: Scheduler?, client: Client?
variableParser: VariableParser?, scheduler: Scheduler?, client: Client?, resilienceConfig: ResilienceConfig?
) {
self.scheduler = scheduler
self.contextDataProvider = contextDataProvider
self.contextEventHandler = contextEventHandler
self.contextEventLogger = contextEventLogger
self.variableParser = variableParser
self.client = client
self.resilienceConfig = resilienceConfig
}
}
30 changes: 21 additions & 9 deletions Sources/ABSmartly/ABSmartlySDK.swift
Expand Up @@ -15,17 +15,29 @@ public final class ABSmartlySDK {
scheduler = config.scheduler ?? DefaultScheduler()
client = config.client

if config.contextDataProvider == nil || config.contextEventHandler == nil {
if client == nil {
throw ABSmartlyError("Missing Client instance")
}

contextDataProvider = config.contextDataProvider ?? DefaultContextDataProvider(client: client!)
contextEventHandler = config.contextEventHandler ?? DefaultContextEventHandler(client: client!)
if config.resilienceConfig != nil {
contextEventHandler = ResilientContextEventHandler(
client: client!,
resilienceConfig: config.resilienceConfig!
)
contextDataProvider = ResilientContextDataProvider(
client: client!,
localCache: config.resilienceConfig!.localCache
)
} else {
contextDataProvider = config.contextDataProvider!
contextEventHandler = config.contextEventHandler!
if config.contextDataProvider == nil || config.contextEventHandler == nil {
if client == nil {
throw ABSmartlyError("Missing Client instance")
}

contextDataProvider = config.contextDataProvider ?? DefaultContextDataProvider(client: client!)
contextEventHandler = config.contextEventHandler ?? DefaultContextEventHandler(client: client!)
} else {
contextDataProvider = config.contextDataProvider!
contextEventHandler = config.contextEventHandler!
}
}

}

public func createContextWithData(config: ContextConfig, contextData: ContextData) -> Context {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ABSmartly/Attribute.swift
@@ -1,6 +1,6 @@
import Foundation

public struct Attribute: Encodable, Equatable {
public struct Attribute: Encodable, Equatable, Decodable {
public let name: String

public let value: JSON
Expand Down
116 changes: 116 additions & 0 deletions Sources/ABSmartly/CircuitBreakerHelper.swift
@@ -0,0 +1,116 @@
//
// Created by Hermes Waldemarin on 14/03/2023.
//

import CircuitBreaker
import Foundation
import PromiseKit

public class CircuitBreakerHelper {

private var circuitBreaker: CircuitBreaker<Promise<Void>, Resolver<BreakerError?>>!
private let scheduler: Scheduler = DefaultScheduler()
private let timeoutLock = NSLock()
private let flushLock = NSLock()
private var flushInExecution = false
private var timeout: ScheduledHandle?
private var backoffPeriodInMilliseconds: Int?
private var handler: ContextEventHandler?

public init(resilienceConfig: ResilienceConfig, handler: ContextEventHandler) {
self.backoffPeriodInMilliseconds = resilienceConfig.backoffPeriodInMilliseconds
self.circuitBreaker = CircuitBreaker(
name: "Circuit1",
timeout: resilienceConfig.timeoutInMilliseconds,
maxFailures: resilienceConfig.failureRateThreshold,
command: callFunction,
fallback: fallback)
self.handler = handler
}

public func decorate(promise: Promise<Void>, fallBackResolver: Resolver<BreakerError?>) -> Promise<Void> {
return Promise<Void> { seal in
circuitBreaker.run(commandArgs: promise, fallbackArgs: fallBackResolver)
seal.fulfill(())
}
}

func callFunction(invocation: Invocation<Promise<Void>, Resolver<BreakerError?>>) {
let promise = invocation.commandArgs
let fallBackResolver = invocation.fallbackArgs

promise.done { response in
fallBackResolver.fulfill(nil)
var state = self.circuitBreaker.breakerState
invocation.notifySuccess()
if (state == .halfopen) && !self.flushInExecution {
self.flushInExecution = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.timeoutLock.lock()
defer { self.timeoutLock.unlock() }
self.handler?.flushCache()
self.flushInExecution = false
}
}

}.catch { error in
var key = ""
var message = ""
if error is LocalizedError {
let localizedError = error as! LocalizedError
key = localizedError._domain
message = localizedError.errorDescription ?? ""
} else {
let nsError = error as! NSError
key = error._domain
nsError.code
message = error.localizedDescription
}

invocation.notifyFailure(
error: BreakerError(
key: key,
reason: message
)
)
}
}

private func fallback(err: BreakerError, fallBackPromise: Resolver<BreakerError?>) {
fallBackPromise.fulfill(err)
if err == .fastFail {
setTimeout()
} else {
}
}

private func clearTimeout() {
if timeout != nil {
timeoutLock.lock()
defer { timeoutLock.unlock() }

timeout?.cancel()
timeout = nil
}
}

private func setTimeout() {
if timeout == nil {
timeoutLock.lock()
defer { timeoutLock.unlock() }

if timeout == nil {
timeout = scheduler.schedule(
after: Double((backoffPeriodInMilliseconds! / 1000)),
execute: { [self] in
clearTimeout()
if circuitBreaker.breakerState != State.closed {
print("Resilience entering in half open state")
circuitBreaker.forceHalfOpen()
}

})
}
}
}
}
5 changes: 3 additions & 2 deletions Sources/ABSmartly/Context.swift
Expand Up @@ -137,6 +137,7 @@ public final class Context {
seal.fulfill(self)
} else if let ready = readyPromise {
_ = ready.done {
self.handler.flushCache()
seal.fulfill(self)
}
}
Expand Down Expand Up @@ -263,15 +264,15 @@ public final class Context {
}

public func getAttributes() -> [String: JSON] {
var result: [String:JSON] = [:]
var result: [String: JSON] = [:]

contextLock.lock()
defer { contextLock.unlock() }

for attribute in attributes {
result[attribute.name] = attribute.value
}
return result;
return result
}

public func setAttributes(_ attributes: [String: JSON]) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/ABSmartly/DefaultContextEventHandler.swift
Expand Up @@ -11,4 +11,8 @@ public class DefaultContextEventHandler: ContextEventHandler {
public func publish(event: PublishEvent) -> Promise<Void> {
return client.publish(event: event)
}

public func flushCache() {

}
}
2 changes: 1 addition & 1 deletion Sources/ABSmartly/Exposure.swift
@@ -1,6 +1,6 @@
import Foundation

public struct Exposure: Encodable, Equatable {
public struct Exposure: Encodable, Equatable, Decodable {
public let id: Int
public let name: String
public let unit: String?
Expand Down
2 changes: 1 addition & 1 deletion Sources/ABSmartly/GoalAchievement.swift
@@ -1,6 +1,6 @@
import Foundation

public struct GoalAchievement: Encodable, Equatable {
public struct GoalAchievement: Encodable, Equatable, Decodable {
public let name: String
public let achievedAt: Int64
public let properties: [String: JSON]?
Expand Down
25 changes: 25 additions & 0 deletions Sources/ABSmartly/MemoryCache.swift
@@ -0,0 +1,25 @@
//
// Created by Hermes Waldemarin on 09/03/2023.
//

import Foundation
import PromiseKit
import SQLite

public class MemoryCache: SqlliteCache {
public override init() {
}

public override func getConnection() -> Connection {
do {
if db == nil {
db = try! Connection(.inMemory)
setupDatabase()
}
} catch {
print(error)
}
return self.db!
}

}
1 change: 1 addition & 0 deletions Sources/ABSmartly/Protocols/ContextEventHandler.swift
Expand Up @@ -4,4 +4,5 @@ import PromiseKit
// sourcery: AutoMockable
public protocol ContextEventHandler {
func publish(event: PublishEvent) -> Promise<Void>
func flushCache()
}
13 changes: 13 additions & 0 deletions Sources/ABSmartly/Protocols/LocalCache.swift
@@ -0,0 +1,13 @@
//
// Created by Hermes Waldemarin on 09/03/2023.
//

import Foundation
import PromiseKit

public protocol LocalCache {
func writePublishEvent(event: PublishEvent)
func retrievePublishEvents() -> [PublishEvent]
func writeContextData(contextData: ContextData)
func getContextData() -> ContextData?
}