Skip to content

Commit 9f47f17

Browse files
committed
EventPool implementations
- Fixed `EventDispatcher` to ensure it updates its `receivers` when removing an `EventReceivable` from the managed collection of buckets - Documentation improvements for `EventDispatching` - First version of the `EventPool` implementation produced - First version of the `EventPoolLowestLoadBalancer` implementation produced - First version of the `EventPoolRoundRobinBalancer` implementation produced - First version of the `EventPoolBalancer` implementation produced - First version of the `EventPoolBalancing` protocol defined - First version of the `EventPoolPooling` protocol defined - First version of the `EventPoolScaler` implementation produced - First version of the `EventPoolScaling` protocol defined - First version of the `EventPoolStaticScaler` implementation produced - `EventThread`'s `init``func` marked `required` - `EventThreadable` protocol now defines `init` `func` - `BasicEventPoolTests` unit tests added and executing properly
1 parent a585c61 commit 9f47f17

File tree

14 files changed

+463
-13
lines changed

14 files changed

+463
-13
lines changed

Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ open class EventDispatcher: EventHandler, EventDispatching {
6060
bucket!.removeAll { receiverContainer in
6161
receiverContainer.receiver != nil && ObjectIdentifier(receiverContainer.receiver!) == ObjectIdentifier(receiver)
6262
}
63+
64+
receivers[eventTypeName] = bucket // Update the Bucket for this Event Type
6365
}
6466
}
6567

Sources/EventDrivenSwift/EventDispatcher/EventDispatching.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,28 @@ public protocol EventDispatching: EventHandling {
1818
Registers the given `receiver` for the given `Eventable` Type
1919
- Author: Simon J. Stuart
2020
- Version: 1.0.0
21+
- Parameters:
22+
- receiver: An Object conforming to `EventReceiving`
23+
- forEventType: An `Eventable` Type Reference
2124
*/
2225
func addReceiver(_ receiver: any EventReceiving, forEventType: Eventable.Type)
2326

2427
/**
2528
Unregisters the given `receiver` from the given `Eventable` Type
2629
- Author: Simon J. Stuart
2730
- Version: 1.0.0
31+
- Parameters:
32+
- receiver: An Object conforming to `EventReceiving`
33+
- forEventType: An `Eventable` Type Reference
2834
*/
2935
func removeReceiver(_ receiver: any EventReceiving, forEventType: Eventable.Type)
3036

3137
/**
32-
Unregisters the given `receiver` from all `Eventable` Types
38+
Unregisters the given `receiver` from **all** `Eventable` Types
3339
- Author: Simon J. Stuart
3440
- Version: 1.0.0
41+
- Parameters:
42+
- receiver: An Object conforming to `EventReceiving`
3543
*/
3644
func removeReceiver(_ receiver: any EventReceiving)
3745
}

Sources/EventDrivenSwift/EventPool/EventPool.swift

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,160 @@
77
//
88

99
import Foundation
10+
import ThreadSafeSwift
1011

1112
/**
1213
Concrete Implementation for an `EventPool`.
1314
- Author: Simon J. Stuart
14-
- Version: 3.1.0
15+
- Version: 4.0.0
1516
- Parameters:
1617
- TEventThread: The `EventThreadable`-conforming Type to be managed by this `EventPool`
1718
- Note: Event Pools own and manage all instances of the given `TEventThread` type
1819
*/
1920
open class EventPool<TEventThread: EventThreadable>: EventHandler, EventPooling {
21+
@ThreadSafeSemaphore public var balancer: EventPoolBalancing
22+
@ThreadSafeSemaphore public var scaler: EventPoolScaling
23+
@ThreadSafeSemaphore public var capacity: UInt8
24+
25+
private var eventThreads = [TEventThread]()
26+
27+
struct ThreadContainer {
28+
weak var thread: (any EventThreadable)?
29+
}
30+
@ThreadSafeSemaphore private var pools = [String:[ThreadContainer]]()
2031

2132
public func addReceiver(_ receiver: EventReceiving, forEventType: Eventable.Type) {
22-
33+
if let eventThread = receiver as? EventThreadable { /// We must cast the `receiver` to `EventThreadable` safely
34+
let eventTypeName = String(reflecting: forEventType)
35+
36+
// We need to add the Thread into the Pool for this Event Type
37+
_pools.withLock { pools in
38+
var bucket = pools[eventTypeName]
39+
let newBucket = bucket == nil
40+
if newBucket { bucket = [ThreadContainer]() } /// If there's no Bucket for this Event Type, create one
41+
42+
/// If it's NOT a New Bucket, and the Bucket already contains this Receiver...
43+
if !newBucket && bucket!.contains(where: { threadContainer in
44+
threadContainer.thread != nil && ObjectIdentifier(threadContainer.thread!) == ObjectIdentifier(eventThread)
45+
}) {
46+
return // ... just Return!
47+
}
48+
49+
/// If we reach here, the Receiver is not already in the Bucket, so let's add it!
50+
bucket!.append(ThreadContainer(thread: eventThread))
51+
52+
if bucket!.count == 1 { EventCentral.shared.addReceiver(self, forEventType: forEventType) } /// If this is the *first* registered Thread for this Event Type, we need to register with Central Dispatch
53+
54+
pools[eventTypeName] = bucket!
55+
}
56+
}
2357
}
2458

2559
public func removeReceiver(_ receiver: EventReceiving, forEventType: Eventable.Type) {
26-
60+
if let eventThread = receiver as? EventThreadable { /// We must cast the `receiver` to `EventThreadable` safely
61+
let eventTypeName = String(reflecting: forEventType)
62+
63+
_pools.withLock { pools in
64+
var bucket = pools[eventTypeName]
65+
if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for hte Event Type
66+
67+
/// Remove any Receivers from this Event-Type Bucket for the given `receiver` instance.
68+
bucket!.removeAll { threadContainer in
69+
threadContainer.thread != nil && ObjectIdentifier(threadContainer.thread!) == ObjectIdentifier(eventThread)
70+
}
71+
72+
if bucket!.count == 0 { EventCentral.shared.removeReceiver(self, forEventType: forEventType) } /// If there are none left in the Bucket, unregister this `EventPool` from Central Dispatch
73+
74+
pools[eventTypeName] = bucket // Update the Bucket for this Event Type
75+
}
76+
}
2777
}
2878

2979
public func removeReceiver(_ receiver: EventReceiving) {
80+
if let eventThread = receiver as? EventThreadable { /// We must cast the `receiver` to `EventThreadable` safely
81+
82+
_pools.withLock { pools in
83+
for (eventTypeName, bucket) in pools { /// Iterate every Event Type
84+
var newBucket = bucket // Copy the Bucket
85+
newBucket.removeAll { threadContainer in /// Remove any occurences of the given Receiver from the Bucket
86+
threadContainer.thread != nil && ObjectIdentifier(threadContainer.thread!) == ObjectIdentifier(eventThread)
87+
}
88+
89+
if bucket.count == 0 { EventCentral.shared.removeReceiver(self) } /// If there are none left in the Bucket, unregister this `EventPool` from Central Dispatch
90+
91+
pools[eventTypeName] = newBucket /// Update the Bucket for this Event Type
92+
}
93+
}
94+
}
95+
}
96+
97+
internal func scalePool() {
98+
let scalingResult = scaler.calculateScaling(currentCapacity: capacity, eventThreads: eventThreads, eventsPending: eventCount)
99+
if !scalingResult.modifyCapacity { return } // If there's no scaling to perform, let's return
100+
//TODO: Implement Scaling + Culling here
101+
}
102+
103+
override internal func processEvent(_ event: any Eventable, dispatchMethod: EventDispatchMethod, priority: EventPriority) {
104+
let eventTypeName = String(reflecting: type(of: event))
105+
106+
var snapPools = [String:[ThreadContainer]]()
107+
108+
_pools.withLock { pools in
109+
// We should take this opportunity to remove any nil receivers
110+
pools[eventTypeName]?.removeAll(where: { threadContainer in
111+
threadContainer.thread == nil
112+
})
113+
snapPools = pools
114+
}
115+
116+
let bucket = snapPools[eventTypeName]
117+
if bucket == nil { return } /// No Receivers, so nothing more to do!
118+
119+
/// Now we need to determine the appropriate `EventThread` to receive this `Eventable`
120+
var bucketThreads = [EventThreadable]()
121+
for threadContainer in bucket! {
122+
if threadContainer.thread == nil { continue } //Can't consider this Thread if it doesn't exist!
123+
bucketThreads.append(threadContainer.thread!)
124+
}
125+
let targetThread = balancer.chooseEventThread(eventThreads: bucketThreads)
126+
127+
if targetThread != nil {
128+
switch dispatchMethod {
129+
case .stack:
130+
targetThread!.stackEvent(event, priority: priority)
131+
case .queue:
132+
targetThread!.queueEvent(event, priority: priority)
133+
}
134+
}
135+
136+
scalePool()
137+
}
138+
139+
/**
140+
Create a new `EventPool`
141+
- Author: Simon J. Stuart
142+
- Version: 4.0.0
143+
- Parameters:
144+
- capacity: The number of Threads to spawn
145+
- balancer: The Load Balancer to use (directs `Eventable` instances to the most appropriate `EventThread` at any given time) - Default is `nil`, uses the `EventPoolRoundRobinBalancer` if `nil`
146+
- scaler: The Scaler to use (increases and/or decreases the number of `EventThread` instances managed by the `EventPool` in response to rules defined by the `scaler` - Default is `nil`, uses the `EventPoolStaticScaler` if `nil`
147+
*/
148+
public init(
149+
capacity: UInt8,
150+
balancer: EventPoolBalancing? = nil,
151+
scaler: EventPoolScaling? = nil
152+
) {
153+
self.capacity = capacity
154+
self.balancer = balancer != nil ? balancer! : EventPoolRoundRobinBalancer()
155+
self.scaler = scaler != nil ? scaler! : EventPoolStaticScaler(initialCapacity: capacity, minimumCapacity: capacity, maximumCapacity: capacity)
30156

157+
super.init()
158+
// Now we create all of our Event Threads
159+
var current = 0
160+
while current < capacity + 1 {
161+
let eventThread = TEventThread(eventPool: self)
162+
eventThreads.append(eventThread)
163+
current += 1
164+
}
31165
}
32166
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// EventPoolLowestLoadBalancer.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 15th August 2022
5+
//
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
Simply delegates each request to the first `EventThread` in the `ThreadPool` to have the lowest number of pending `Eventable`s to process
13+
- Author: Simon J. Stuart
14+
- Version: 4.0.0
15+
*/
16+
public class EventPoolLowestLoadBalancer: EventPoolBalancer {
17+
override public func chooseEventThread(eventThreads: [EventThreadable]) -> EventThreadable? {
18+
if eventThreads.count == 0 { return nil } // If there are no Event Threads, we can't possibly return one!
19+
20+
let sorted = eventThreads.sorted { lhs, rhs in
21+
lhs.eventCount > rhs.eventCount
22+
}
23+
24+
return sorted.first
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// EventPoolRoundRobinBalancer.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 13th August 2022
5+
//
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
Simply delegates each request to the next `EventThread` in the `ThreadPool`, cycling back around to the first when passing the last.
13+
- Author: Simon J. Stuart
14+
- Version: 4.0.0
15+
*/
16+
public class EventPoolRoundRobinBalancer: EventPoolBalancer {
17+
private var lastThreadIndex: Int? = nil
18+
19+
override public func chooseEventThread(eventThreads: [EventThreadable]) -> EventThreadable? {
20+
if eventThreads.count == 0 { return nil } // If there are no Event Threads, we can't possibly return one!
21+
22+
lastThreadIndex = lastThreadIndex == nil ? 0 : lastThreadIndex! > eventThreads.count ? 0 : lastThreadIndex! + 1 // Determine the Thread Index to use
23+
24+
return eventThreads[lastThreadIndex!] // Return the selected Thread
25+
}
26+
}
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
//
2-
// File.swift
3-
//
2+
// EventPoolBalancer.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 13th August 2022
45
//
5-
// Created by Simon Stuart on 13/08/2022.
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
67
//
78

89
import Foundation
10+
11+
/**
12+
Abstract Base Class for all Event Pool Balancers.
13+
- Author: Simon J. Stuart
14+
- Version: 4.0.0
15+
- Note: Event Pool Balancers perform calculations to determine which `EventThread` should receive any inbound `Eventable`
16+
*/
17+
open class EventPoolBalancer: EventPoolBalancing {
18+
public func chooseEventThread(eventThreads: [EventThreadable]) -> EventThreadable? {
19+
preconditionFailure("chooseEventThread must be overriden!")
20+
}
21+
}
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
//
2-
// File.swift
3-
//
2+
// EventPoolBalancing.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 13th August 2022
45
//
5-
// Created by Simon Stuart on 13/08/2022.
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
67
//
78

89
import Foundation
10+
11+
/**
12+
Protocol describing anything that Balances `EventPooling`
13+
- Author: Simon J. Stuart
14+
- Version: 4.0.0
15+
- Note: Event Pool Balancers perform calculations to determine which `EventThread` should receive any inbound `Eventable`
16+
*/
17+
public protocol EventPoolBalancing {
18+
/**
19+
Invoked by `EventPool` to determine which `EventThreadable` should be used to process an inbound `Eventable`
20+
- Author: Simon J. Stuart
21+
- Version: 4.0.0
22+
- Parameters:
23+
- eventThreads: Reference to all of the `EventThreadable`s currently in the `EventPool`
24+
- Returns: The `EventThreadable` selected to process an inbound `Eventable`
25+
*/
26+
func chooseEventThread(eventThreads: [EventThreadable]) -> EventThreadable?
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// EventPoolScaler.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 13th August 2022
5+
//
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
7+
//
8+
9+
import Foundation
10+
import ThreadSafeSwift
11+
12+
/**
13+
Abstract Base Class for all Event Pool Scalers.
14+
- Author: Simon J. Stuart
15+
- Version: 4.0.0
16+
- Note: Event Pool Scalers perform calculations to increase or reduce the number of `EventThread`s in response to load and performance across the `EventPool`.
17+
*/
18+
open class EventPoolScaler: EventPoolScaling {
19+
@ThreadSafeSemaphore public var initialCapacity: UInt8
20+
@ThreadSafeSemaphore public var minimumCapacity: UInt8
21+
@ThreadSafeSemaphore public var maximumCapacity: UInt8
22+
23+
init(
24+
initialCapacity: UInt8,
25+
minimumCapacity: UInt8,
26+
maximumCapacity: UInt8
27+
) {
28+
self.initialCapacity = initialCapacity
29+
self.minimumCapacity = minimumCapacity
30+
self.maximumCapacity = maximumCapacity
31+
}
32+
33+
public func calculateScaling(currentCapacity: UInt8, eventThreads: [EventThreadable], eventsPending: Int) -> EventPoolScalingResult {
34+
return EventPoolScalingResult(modifyCapacity: false, newCapacity: currentCapacity, cullOrKeepThreads: [Bool]())
35+
}
36+
}

0 commit comments

Comments
 (0)