Skip to content

Commit 630ca57

Browse files
committed
Add blkio resource flags
1 parent c1a6d97 commit 630ca57

8 files changed

Lines changed: 215 additions & 7 deletions

File tree

Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import PackageDescription
2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
2525
let builderShimVersion = "0.12.0"
26-
let scVersion = "0.32.1"
26+
let scVersion = "feat/chaos-1380-blkio-runtime"
2727

2828
let package = Package(
2929
name: "container",
@@ -47,7 +47,7 @@ let package = Package(
4747
.library(name: "TerminalProgress", targets: ["TerminalProgress"]),
4848
],
4949
dependencies: [
50-
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
50+
.package(url: "https://github.com/full-chaos/containerization.git", branch: scVersion),
5151
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
5252
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"),
5353
.package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"),

Sources/ContainerResource/Container/ContainerConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ public struct ContainerConfiguration: Sendable, Codable {
145145
public var cpus: Int = 4
146146
/// Memory in bytes allocated.
147147
public var memoryInBytes: UInt64 = 1024.mib()
148+
/// Block I/O resource limits.
149+
public var blockIO: LinuxBlockIO?
148150
/// Storage quota/size in bytes.
149151
public var storage: UInt64?
150152

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,24 @@ public struct Flags {
101101
public struct Resource: ParsableArguments {
102102
public init() {}
103103

104-
public init(cpus: Int64?, memory: String?) {
104+
public init(
105+
cpus: Int64?,
106+
memory: String?,
107+
blkioWeight: UInt16? = nil,
108+
blkioWeightDevice: [String] = [],
109+
deviceReadBps: [String] = [],
110+
deviceWriteBps: [String] = [],
111+
deviceReadIops: [String] = [],
112+
deviceWriteIops: [String] = []
113+
) {
105114
self.cpus = cpus
106115
self.memory = memory
116+
self.blkioWeight = blkioWeight
117+
self.blkioWeightDevice = blkioWeightDevice
118+
self.deviceReadBps = deviceReadBps
119+
self.deviceWriteBps = deviceWriteBps
120+
self.deviceReadIops = deviceReadIops
121+
self.deviceWriteIops = deviceWriteIops
107122
}
108123

109124
@Option(name: .shortAndLong, help: "Number of CPUs to allocate to the container")
@@ -114,6 +129,39 @@ public struct Flags {
114129
help: "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
115130
)
116131
public var memory: String?
132+
133+
@Option(name: .customLong("blkio-weight"), help: "Block I/O weight, from 10 to 1000")
134+
public var blkioWeight: UInt16?
135+
136+
@Option(
137+
name: .customLong("blkio-weight-device"),
138+
help: .init("Block I/O weight for a device (format: <path>:<weight>)", valueName: "device-weight")
139+
)
140+
public var blkioWeightDevice: [String] = []
141+
142+
@Option(
143+
name: .customLong("device-read-bps"),
144+
help: .init("Throttle read rate from a device in bytes per second (format: <path>:<rate>)", valueName: "device-rate")
145+
)
146+
public var deviceReadBps: [String] = []
147+
148+
@Option(
149+
name: .customLong("device-write-bps"),
150+
help: .init("Throttle write rate to a device in bytes per second (format: <path>:<rate>)", valueName: "device-rate")
151+
)
152+
public var deviceWriteBps: [String] = []
153+
154+
@Option(
155+
name: .customLong("device-read-iops"),
156+
help: .init("Throttle read rate from a device in IO operations per second (format: <path>:<rate>)", valueName: "device-rate")
157+
)
158+
public var deviceReadIops: [String] = []
159+
160+
@Option(
161+
name: .customLong("device-write-iops"),
162+
help: .init("Throttle write rate to a device in IO operations per second (format: <path>:<rate>)", valueName: "device-rate")
163+
)
164+
public var deviceWriteIops: [String] = []
117165
}
118166

119167
public struct DNS: ParsableArguments {

Sources/Services/ContainerAPIService/Client/Parser.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ContainerizationError
2121
import ContainerizationExtras
2222
import ContainerizationOCI
2323
import ContainerizationOS
24+
import Darwin
2425
import Foundation
2526

2627
/// A parsed volume specification from user input
@@ -90,6 +91,12 @@ public struct Parser {
9091
public static func resources(
9192
cpus: Int64?,
9293
memory: String?,
94+
blkioWeight: UInt16? = nil,
95+
blkioWeightDevice: [String] = [],
96+
deviceReadBps: [String] = [],
97+
deviceWriteBps: [String] = [],
98+
deviceReadIops: [String] = [],
99+
deviceWriteIops: [String] = [],
93100
defaultCPUs: Int,
94101
defaultMemory: MemorySize,
95102
) throws -> ContainerConfiguration.Resources {
@@ -105,9 +112,116 @@ public struct Parser {
105112
resource.memoryInBytes = try Parser.memoryStringAsMiB(memory).mib()
106113
}
107114

115+
resource.blockIO = try Parser.blockIO(
116+
weight: blkioWeight,
117+
weightDevice: blkioWeightDevice,
118+
deviceReadBps: deviceReadBps,
119+
deviceWriteBps: deviceWriteBps,
120+
deviceReadIops: deviceReadIops,
121+
deviceWriteIops: deviceWriteIops
122+
)
123+
108124
return resource
109125
}
110126

127+
public static func blockIO(
128+
weight: UInt16?,
129+
weightDevice: [String],
130+
deviceReadBps: [String],
131+
deviceWriteBps: [String],
132+
deviceReadIops: [String],
133+
deviceWriteIops: [String]
134+
) throws -> LinuxBlockIO? {
135+
let hasBlockIO = weight != nil
136+
|| !weightDevice.isEmpty
137+
|| !deviceReadBps.isEmpty
138+
|| !deviceWriteBps.isEmpty
139+
|| !deviceReadIops.isEmpty
140+
|| !deviceWriteIops.isEmpty
141+
guard hasBlockIO else { return nil }
142+
143+
if let weight {
144+
try validateBlockIOWeight(weight)
145+
}
146+
147+
return try LinuxBlockIO(
148+
weight: weight,
149+
leafWeight: nil,
150+
weightDevice: weightDevice.map {
151+
let parsed = try parseBlockIODeviceSpec($0)
152+
let weight = try parseUInt16(parsed.value, name: "--blkio-weight-device weight")
153+
try validateBlockIOWeight(weight)
154+
return LinuxWeightDevice(major: parsed.device.major, minor: parsed.device.minor, weight: weight, leafWeight: nil)
155+
},
156+
throttleReadBpsDevice: deviceReadBps.map {
157+
let parsed = try parseBlockIODeviceSpec($0)
158+
return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value))
159+
},
160+
throttleWriteBpsDevice: deviceWriteBps.map {
161+
let parsed = try parseBlockIODeviceSpec($0)
162+
return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value))
163+
},
164+
throttleReadIOPSDevice: deviceReadIops.map {
165+
let parsed = try parseBlockIODeviceSpec($0)
166+
return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-read-iops rate"))
167+
},
168+
throttleWriteIOPSDevice: deviceWriteIops.map {
169+
let parsed = try parseBlockIODeviceSpec($0)
170+
return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-write-iops rate"))
171+
}
172+
)
173+
}
174+
175+
private static func parseBlockIODeviceSpec(_ value: String) throws -> (device: LinuxBlockIODevice, value: String) {
176+
let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
177+
guard parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else {
178+
throw ContainerizationError(.invalidArgument, message: "block I/O device spec must be '<path>:<value>'")
179+
}
180+
181+
return try (blockIODevice(path: String(parts[0])), String(parts[1]))
182+
}
183+
184+
private static func blockIODevice(path: String) throws -> LinuxBlockIODevice {
185+
var info = stat()
186+
guard stat(path, &info) == 0 else {
187+
throw ContainerizationError(.notFound, message: "block I/O device path not found: \(path)")
188+
}
189+
190+
let rawDevice = UInt32(bitPattern: info.st_rdev)
191+
let major = Int64((rawDevice >> 24) & 0xff)
192+
let minor = Int64(rawDevice & 0x00ff_ffff)
193+
return LinuxBlockIODevice(major: major, minor: minor)
194+
}
195+
196+
private static func parseByteRate(_ value: String) throws -> UInt64 {
197+
let measurement = try Measurement.parse(parsing: value)
198+
let bytes = measurement.converted(to: .bytes).value
199+
guard bytes >= 0, bytes.rounded(.down) == bytes else {
200+
throw ContainerizationError(.invalidArgument, message: "block I/O byte rate must be a non-negative whole number of bytes")
201+
}
202+
return UInt64(bytes)
203+
}
204+
205+
private static func parseUInt16(_ value: String, name: String) throws -> UInt16 {
206+
guard let parsed = UInt16(value) else {
207+
throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 16-bit integer")
208+
}
209+
return parsed
210+
}
211+
212+
private static func parseUInt64(_ value: String, name: String) throws -> UInt64 {
213+
guard let parsed = UInt64(value) else {
214+
throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 64-bit integer")
215+
}
216+
return parsed
217+
}
218+
219+
private static func validateBlockIOWeight(_ value: UInt16) throws {
220+
guard (10...1000).contains(value) else {
221+
throw ContainerizationError(.invalidArgument, message: "block I/O weight must be between 10 and 1000")
222+
}
223+
}
224+
111225
public static func allEnv(imageEnvs: [String], envFiles: [String], envs: [String]) throws -> [String] {
112226
var combined: [String] = []
113227
combined.append(contentsOf: Parser.env(envList: imageEnvs))

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ public struct Utility {
160160
config.resources = try Parser.resources(
161161
cpus: resource.cpus,
162162
memory: resource.memory,
163+
blkioWeight: resource.blkioWeight,
164+
blkioWeightDevice: resource.blkioWeightDevice,
165+
deviceReadBps: resource.deviceReadBps,
166+
deviceWriteBps: resource.deviceWriteBps,
167+
deviceReadIops: resource.deviceReadIops,
168+
deviceWriteIops: resource.deviceWriteIops,
163169
defaultCPUs: containerSystemConfig.container.cpus,
164170
defaultMemory: containerSystemConfig.container.memory
165171
)

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,7 @@ public actor SandboxService {
882882
) throws {
883883
czConfig.cpus = config.resources.cpus
884884
czConfig.memoryInBytes = config.resources.memoryInBytes
885+
czConfig.blockIO = config.resources.blockIO
885886
czConfig.sysctl = config.sysctls.reduce(into: [String: String]()) {
886887
$0[$1.key] = $1.value
887888
}

Tests/ContainerAPIClientTests/ParserTest.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,43 @@ struct ParserTest {
12011201
#expect(result.memoryInBytes == 256.mib())
12021202
}
12031203

1204+
@Test func testResourcesBlockIOFlags() throws {
1205+
let result = try Parser.resources(
1206+
cpus: nil,
1207+
memory: nil,
1208+
blkioWeight: 500,
1209+
blkioWeightDevice: ["/dev/null:700"],
1210+
deviceReadBps: ["/dev/null:1mb"],
1211+
deviceWriteBps: ["/dev/null:2mb"],
1212+
deviceReadIops: ["/dev/null:1000"],
1213+
deviceWriteIops: ["/dev/null:2000"],
1214+
defaultCPUs: 8,
1215+
defaultMemory: MemorySize("2g")
1216+
)
1217+
1218+
let blockIO = try #require(result.blockIO)
1219+
#expect(blockIO.weight == 500)
1220+
#expect(blockIO.weightDevice.first?.weight == 700)
1221+
#expect(blockIO.throttleReadBpsDevice.first?.rate == 1.mib())
1222+
#expect(blockIO.throttleWriteBpsDevice.first?.rate == 2.mib())
1223+
#expect(blockIO.throttleReadIOPSDevice.first?.rate == 1000)
1224+
#expect(blockIO.throttleWriteIOPSDevice.first?.rate == 2000)
1225+
}
1226+
1227+
@Test func testResourcesRejectsInvalidBlockIOWeight() throws {
1228+
#expect {
1229+
_ = try Parser.resources(
1230+
cpus: nil,
1231+
memory: nil,
1232+
blkioWeight: 1,
1233+
defaultCPUs: 8,
1234+
defaultMemory: MemorySize("2g")
1235+
)
1236+
} throws: { _ in
1237+
true
1238+
}
1239+
}
1240+
12041241
@Test func testResourcesBuildPropertyLookup() async throws {
12051242
let content = """
12061243
[build]

0 commit comments

Comments
 (0)