Skip to content

Commit 8fc03cd

Browse files
crosbymichaelkatiewasnothere
authored andcommitted
add rotating allocator for UInt32 (#11)
This creates an allocator based on a FIFO that will allocate through the range before reusing previously released allocations. Signed-off-by: michael crosby <[email protected]>
1 parent 3ee87d9 commit 8fc03cd

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ let package = Package(
244244
.product(name: "Logging", package: "swift-log"),
245245
]
246246
),
247+
.testTarget(
248+
name: "ContainerizationExtrasTests",
249+
dependencies: [
250+
"ContainerizationExtras",
251+
"CShim",
252+
]
253+
),
247254
.target(
248255
name: "CShim"
249256
),

Sources/ContainerizationExtras/NetworkAddress+Allocator.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ extension UInt32 {
7373
indexToAddress: { lower + UInt32($0) }
7474
)
7575
}
76+
77+
/// Creates a rotating allocator for vsock ports, or any UInt32 values.
78+
public static func rotatingAllocator(lower: UInt32, size: UInt32) throws -> any AddressAllocator<UInt32> {
79+
guard 0xffff_ffff - lower + 1 >= size else {
80+
throw AllocatorError.rangeExceeded
81+
}
82+
83+
return RotatingAddressAllocator(
84+
size: size,
85+
addressToIndex: { address in
86+
guard address >= lower && address <= lower + UInt32(size) else {
87+
return nil
88+
}
89+
return Int(address - lower)
90+
},
91+
indexToAddress: { lower + UInt32($0) }
92+
)
93+
}
7694
}
7795

7896
extension Character {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors.
3+
// All rights reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//===----------------------------------------------------------------------===//
17+
18+
import Synchronization
19+
20+
package final class RotatingAddressAllocator: AddressAllocator {
21+
package typealias AddressType = UInt32
22+
23+
private struct State {
24+
var allocations: [AddressType]
25+
var enabled: Bool
26+
var allocationCount: Int
27+
let addressToIndex: AddressToIndexTransform<AddressType>
28+
let indexToAddress: IndexToAddressTransform<AddressType>
29+
30+
init(
31+
size: UInt32,
32+
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
33+
indexToAddress: @escaping IndexToAddressTransform<AddressType>
34+
) {
35+
self.allocations = [UInt32](0..<size)
36+
self.enabled = true
37+
self.allocationCount = 0
38+
self.addressToIndex = addressToIndex
39+
self.indexToAddress = indexToAddress
40+
}
41+
}
42+
43+
private let stateGuard: Mutex<State>
44+
45+
/// Create an allocator with specified size and index mappings.
46+
package init(
47+
size: UInt32,
48+
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
49+
indexToAddress: @escaping IndexToAddressTransform<AddressType>
50+
) {
51+
let state = State(
52+
size: size,
53+
addressToIndex: addressToIndex,
54+
indexToAddress: indexToAddress
55+
)
56+
self.stateGuard = Mutex(state)
57+
}
58+
59+
public func allocate() throws -> AddressType {
60+
try self.stateGuard.withLock { state in
61+
guard state.enabled else {
62+
throw AllocatorError.allocatorDisabled
63+
}
64+
65+
guard state.allocations.count > 0 else {
66+
throw AllocatorError.allocatorFull
67+
}
68+
69+
let value = state.allocations.removeFirst()
70+
71+
guard let address = state.indexToAddress(Int(value)) else {
72+
throw AllocatorError.invalidIndex(Int(value))
73+
}
74+
75+
state.allocationCount += 1
76+
return address
77+
}
78+
}
79+
80+
package func reserve(_ address: AddressType) throws {
81+
try self.stateGuard.withLock { state in
82+
guard state.enabled else {
83+
throw AllocatorError.allocatorDisabled
84+
}
85+
86+
guard let index = state.addressToIndex(address) else {
87+
throw AllocatorError.invalidAddress(address.description)
88+
}
89+
90+
let i = state.allocations.firstIndex(of: UInt32(index))
91+
guard let i else {
92+
throw AllocatorError.alreadyAllocated("\(address.description)")
93+
}
94+
95+
_ = state.allocations.remove(at: i)
96+
state.allocationCount += 1
97+
}
98+
}
99+
100+
package func release(_ address: AddressType) throws {
101+
try self.stateGuard.withLock { state in
102+
guard let index = (state.addressToIndex(address)) else {
103+
throw AllocatorError.invalidAddress(address.description)
104+
}
105+
let value = UInt32(index)
106+
107+
guard !state.allocations.contains(value) else {
108+
throw AllocatorError.notAllocated("\(address.description)")
109+
}
110+
111+
state.allocations.append(value)
112+
state.allocationCount -= 1
113+
}
114+
}
115+
116+
package func disableAllocator() -> Bool {
117+
self.stateGuard.withLock { state in
118+
guard state.allocationCount == 0 else {
119+
return false
120+
}
121+
state.enabled = false
122+
return true
123+
}
124+
}
125+
}

Tests/ContainerizationExtrasTests/TestNetworkAddress+Allocator.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,68 @@ final class TestAddressAllocators {
182182
let value = try allocator.allocate()
183183
#expect(value == address)
184184
}
185+
186+
@Test
187+
func testRotatingUInt32PortAllocator() throws {
188+
var allocations = Set<UInt32>()
189+
let lower = UInt32(5000)
190+
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
191+
allocations.insert(try allocator.allocate())
192+
allocations.insert(try allocator.allocate())
193+
allocations.insert(try allocator.allocate())
194+
do {
195+
_ = try allocator.allocate()
196+
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
197+
} catch {
198+
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
199+
}
200+
201+
let address = UInt32(5001)
202+
try allocator.release(address)
203+
let value = try allocator.allocate()
204+
#expect(value == address)
205+
}
206+
207+
@Test
208+
func testRotatingFIFOUInt32PortAllocator() throws {
209+
let lower = UInt32(5000)
210+
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
211+
let first = try allocator.allocate()
212+
#expect(first == 5000)
213+
let second = try allocator.allocate()
214+
#expect(second == 5001)
215+
216+
try allocator.release(first)
217+
let third = try allocator.allocate()
218+
// even after a release, it should continue to allocate in the range
219+
// before reusing an previous allocation on the stack.
220+
#expect(third == 5002)
221+
222+
// now the next allocation should be our first port
223+
let reused = try allocator.allocate()
224+
#expect(reused == first)
225+
226+
try allocator.release(third)
227+
let thirdReused = try allocator.allocate()
228+
#expect(thirdReused == third)
229+
}
230+
231+
@Test
232+
func testRotatingReservedUInt32PortAllocator() throws {
233+
let lower = UInt32(5000)
234+
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
235+
236+
try allocator.reserve(5001)
237+
let first = try allocator.allocate()
238+
#expect(first == 5000)
239+
// this should skip the reserved 5001
240+
let second = try allocator.allocate()
241+
#expect(second == 5002)
242+
243+
// no release our reserved
244+
try allocator.release(5001)
245+
246+
let third = try allocator.allocate()
247+
#expect(third == 5001)
248+
}
185249
}

0 commit comments

Comments
 (0)