RadrootsAdmissionCleanupTests.swift (16019B)
1 import Darwin 2 import Foundation 3 @testable import RadrootsKit 4 import Testing 5 6 @Test func nativeAdmissionCleanupRetriesOnlyBoundedCreationAbsence() throws { 7 let f = try AdmissionCleanupFixture() 8 defer { f.remove() } 9 var attempts = 0 10 let path = f.path("creation") 11 let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path, injectedOpenError: { attempt in 12 attempts += 1 13 return attempt < 2 ? ENOENT : nil 14 })) 15 #expect(attempts == 3) 16 #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: path) == nil) 17 Darwin.close(descriptor) 18 attempts = 0 19 #expect(throws: RadrootsAppleFileError.transientFailure) { 20 _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("exhausted"), injectedOpenError: { _ in 21 attempts += 1; return ENOENT 22 }) 23 } 24 #expect(attempts == 4 && !FileManager.default.fileExists(atPath: f.path("exhausted").path)) 25 attempts = 0 26 #expect(throws: RadrootsAppleFileError.permanentFailure) { 27 _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("denied"), injectedOpenError: { _ in 28 attempts += 1; return EACCES 29 }) 30 } 31 #expect(attempts == 1) 32 #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("missing"), create: false) == nil) 33 #expect(!FileManager.default.fileExists(atPath: f.path("missing").path)) 34 } 35 36 @Test func nativeAdmissionCleanupCreationRetryRejectsChangedParent() throws { 37 let f = try AdmissionCleanupFixture() 38 defer { f.remove() } 39 var attempts = 0 40 #expect(throws: RadrootsAppleFileError.permanentFailure) { 41 _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("changed"), injectedOpenError: { _ in 42 attempts += 1 43 try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("moved")) 44 try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) 45 return ENOENT 46 }) 47 } 48 #expect(attempts == 1) 49 #expect(!FileManager.default.fileExists(atPath: f.path("changed").path)) 50 } 51 52 @Test func nativeAdmissionCleanupConcurrentOwnersNeverSplitOneReservation() async throws { 53 let f = try AdmissionCleanupFixture() 54 defer { f.remove() } 55 let request = try f.request("contended") 56 let probe = AdmissionCleanupProbe() 57 let collector = RadrootsAppleBackgroundTransferStore(roots: f.roots) 58 try await withThrowingTaskGroup(of: Void.self) { group in 59 for _ in 0 ..< 8 { 60 group.addTask { 61 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 62 for _ in 0 ..< 20 { 63 do { 64 _ = try await store.withAdmission(for: request.identifier) { 65 await probe.enter() 66 await Task.yield() 67 await probe.leave() 68 return RadrootsBackgroundTransferHandle(request: request) 69 } 70 } catch RadrootsBackgroundTransferError.invalidRequest { 71 // Another owner has this exact reservation. 72 } catch RadrootsBackgroundTransferError.unavailable { 73 // Bounded coordination contention remains transient. 74 } 75 } 76 } 77 } 78 group.addTask { 79 for _ in 0 ..< 40 { 80 do { _ = try await collector.collectInactiveAdmissions(limit: 7) } 81 catch RadrootsBackgroundTransferError.unavailable {} 82 await Task.yield() 83 } 84 } 85 try await group.waitForAll() 86 } 87 #expect(await probe.maximum == 1) 88 #expect(await probe.active == 0) 89 #expect(await probe.completed > 0) 90 #expect(try await !collector.admissionIsActive(for: request.identifier)) 91 } 92 93 @Test func nativeAdmissionCleanupPreservesNativeReceiptAndUploadLease() async throws { 94 let f = try AdmissionCleanupFixture() 95 defer { f.remove() } 96 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 97 let request = try f.request("unverified") 98 let receipt = try RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification, 99 response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: "application/json", body: Data("{}".utf8)), 100 executionID: UUID()) 101 try await store.saveSnapshot(receipt) 102 let bytes = Data("OS-consumed immutable upload bytes".utf8) 103 let lease = try RadrootsAppleFileAccess(roots: f.roots).leaseUploadBytes(bytes, identifier: UUID().uuidString.lowercased()) 104 try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) 105 try Data().write(to: f.path(request.identifier.rawValue)) 106 #expect(try await store.collectInactiveAdmissions().removedFiles == 1) 107 #expect(try await store.loadSnapshots() == [receipt]) 108 #expect(try Data(contentsOf: lease.fileURL) == bytes) 109 } 110 111 @Test func nativeAdmissionCleanupRetainsHeldReservationAndAllowsIndependentIDs() async throws { 112 let f = try AdmissionCleanupFixture() 113 defer { f.remove() } 114 let first = RadrootsAppleBackgroundTransferStore(roots: f.roots) 115 let second = RadrootsAppleBackgroundTransferStore(roots: f.roots) 116 let request = try f.request("held") 117 let gate = AdmissionCleanupGate() 118 let pending = Task { 119 try await first.withAdmission(for: request.identifier) { 120 await gate.hold() 121 return RadrootsBackgroundTransferHandle(request: request) 122 } 123 } 124 await gate.waitUntilEntered() 125 let path = f.path(request.identifier.rawValue) 126 #expect(FileManager.default.fileExists(atPath: path.path)) 127 #expect(try await second.collectInactiveAdmissions().removedFiles == 0) 128 #expect(try await second.admissionIsActive(for: request.identifier)) 129 await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { 130 _ = try await second.withAdmission(for: request.identifier) { RadrootsBackgroundTransferHandle(request: request) } 131 } 132 let other = try f.request("independent") 133 _ = try await second.withAdmission(for: other.identifier) { RadrootsBackgroundTransferHandle(request: other) } 134 #expect(!FileManager.default.fileExists(atPath: f.path(other.identifier.rawValue).path)) 135 #expect(FileManager.default.fileExists(atPath: path.path)) 136 await gate.release() 137 #expect(try await pending.value.identifier == request.identifier) 138 #expect(!FileManager.default.fileExists(atPath: path.path)) 139 } 140 141 @Test func nativeAdmissionCleanupContinuesBeyondOneThousandEntriesAndRetainsUnknowns() async throws { 142 let f = try AdmissionCleanupFixture() 143 defer { f.remove() } 144 try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) 145 for index in 0 ..< 1001 { 146 try Data().write(to: f.path("orphan-\(index)")) 147 } 148 let nonempty = f.path("nonempty") 149 let outside = f.base.appendingPathComponent("outside") 150 try Data("protected".utf8).write(to: nonempty) 151 try Data("external".utf8).write(to: outside) 152 try FileManager.default.createSymbolicLink(at: f.path("symlink"), withDestinationURL: outside) 153 try FileManager.default.createDirectory(at: f.path("directory"), withIntermediateDirectories: false) 154 let unknown = f.directory.appendingPathComponent("unknown.lock") 155 try Data().write(to: unknown) 156 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 157 var removed = 0 158 var calls = 0 159 while true { 160 let result = try await store.collectInactiveAdmissions(limit: 37) 161 #expect(result.scannedEntries <= 37 && result.removedFiles <= result.scannedEntries) 162 removed += result.removedFiles 163 calls += 1 164 if result.reachedEnd { 165 break 166 } 167 try #require(calls < 100) 168 } 169 #expect(calls > 27 && removed == 1001) 170 #expect(try Data(contentsOf: nonempty) == Data("protected".utf8)) 171 #expect(try Data(contentsOf: outside) == Data("external".utf8)) 172 #expect(FileManager.default.fileExists(atPath: unknown.path)) 173 #expect(try await store.collectInactiveAdmissions().removedFiles == 0) 174 } 175 176 @Test func nativeAdmissionCleanupRetainsInterruptedRetirementThenRecovers() async throws { 177 let f = try AdmissionCleanupFixture() 178 defer { f.remove() } 179 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 180 let request = try f.request("interrupted") 181 let gate = AdmissionCleanupGate() 182 let pending = Task { 183 try await store.withAdmission(for: request.identifier) { 184 await gate.hold() 185 return RadrootsBackgroundTransferHandle(request: request) 186 } 187 } 188 await gate.waitUntilEntered() 189 let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination)) 190 await gate.release() 191 #expect(try await pending.value.identifier == request.identifier) 192 #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) 193 Darwin.close(coordination) 194 #expect(try await store.collectInactiveAdmissions().removedFiles == 1) 195 #expect(try await store.collectInactiveAdmissions().removedFiles == 0) 196 } 197 198 @Test func nativeAdmissionCleanupCancellationAndOperationErrorRetireOnlyTheirReservation() async throws { 199 let f = try AdmissionCleanupFixture() 200 defer { f.remove() } 201 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 202 let request = try f.request("cancelled") 203 let gate = AdmissionCleanupGate() 204 let pending = Task { 205 try await store.withAdmission(for: request.identifier) { 206 await gate.hold() 207 try Task.checkCancellation() 208 return RadrootsBackgroundTransferHandle(request: request) 209 } 210 } 211 await gate.waitUntilEntered() 212 pending.cancel() 213 #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) 214 await gate.release() 215 await #expect(throws: CancellationError.self) { _ = try await pending.value } 216 #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) 217 await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { 218 _ = try await store.withAdmission(for: request.identifier) { throw RadrootsBackgroundTransferError.transferFailure } 219 } 220 #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) 221 } 222 223 @Test func nativeAdmissionCleanupBoundsAndCoordinationContentionFailBeforeWork() async throws { 224 let f = try AdmissionCleanupFixture() 225 defer { f.remove() } 226 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 227 for limit in [0, -1, 65, Int.max] { 228 await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { 229 _ = try await store.collectInactiveAdmissions(limit: limit) 230 } 231 } 232 #expect(!FileManager.default.fileExists(atPath: f.directory.path)) 233 let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination)) 234 defer { Darwin.close(coordination) } 235 await #expect(throws: RadrootsBackgroundTransferError.unavailable) { 236 _ = try await store.collectInactiveAdmissions() 237 } 238 } 239 240 @Test func nativeAdmissionCleanupRejectsReplacedDirectoryAndResumesFreshPass() async throws { 241 let f = try AdmissionCleanupFixture() 242 defer { f.remove() } 243 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 244 #expect(try await !store.collectInactiveAdmissions(limit: 1).reachedEnd) 245 try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("retired")) 246 try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) 247 let replacement = f.path("replacement") 248 try Data().write(to: replacement) 249 await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { 250 _ = try await store.collectInactiveAdmissions() 251 } 252 #expect(FileManager.default.fileExists(atPath: replacement.path)) 253 #expect(try await store.collectInactiveAdmissions().removedFiles == 1) 254 } 255 256 @Test func nativeAdmissionCleanupDoesNotUnlinkReplacedLeafOrFollowRootSymlink() async throws { 257 let f = try AdmissionCleanupFixture() 258 defer { f.remove() } 259 let path = f.path("replaced") 260 let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path)) 261 defer { Darwin.close(descriptor) } 262 try FileManager.default.moveItem(at: path, to: f.base.appendingPathComponent("held-inode")) 263 try Data().write(to: path) 264 #expect(try !RadrootsAtomicFile.removeEmptyLockedFile(at: path, descriptor: descriptor)) 265 #expect(FileManager.default.fileExists(atPath: path.path)) 266 try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("original")) 267 try FileManager.default.createSymbolicLink(at: f.directory, withDestinationURL: f.base.appendingPathComponent("original")) 268 let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) 269 await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { _ = try await store.collectInactiveAdmissions() } 270 #expect(FileManager.default.fileExists(atPath: path.path)) 271 } 272 273 private actor AdmissionCleanupProbe { 274 var active = 0 275 var maximum = 0 276 var completed = 0 277 func enter() { 278 active += 1; maximum = max(maximum, active) 279 } 280 281 func leave() { 282 active -= 1; completed += 1 283 } 284 } 285 286 private actor AdmissionCleanupGate { 287 private var entered = false 288 private var waiters: [CheckedContinuation<Void, Never>] = [] 289 private var continuation: CheckedContinuation<Void, Never>? 290 291 func hold() async { 292 entered = true 293 for waiter in waiters { 294 waiter.resume() 295 } 296 waiters.removeAll() 297 await withCheckedContinuation { continuation = $0 } 298 } 299 300 func waitUntilEntered() async { 301 if !entered { 302 await withCheckedContinuation { waiters.append($0) } 303 } 304 } 305 306 func release() { 307 continuation?.resume(); continuation = nil 308 } 309 } 310 311 private struct AdmissionCleanupFixture: Sendable { 312 let base: URL 313 let roots: RadrootsAppleFileRoots 314 var directory: URL { 315 roots.dataRoot.appendingPathComponent("background_transfers/admissions", isDirectory: true) 316 } 317 318 var coordination: URL { 319 directory.appendingPathComponent(".coordination.lock") 320 } 321 322 init() throws { 323 let raw = FileManager.default.temporaryDirectory.appendingPathComponent("admission-cleanup-\(UUID().uuidString)") 324 try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true) 325 let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) }) 326 defer { Darwin.free(pointer) } 327 base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true) 328 roots = try RadrootsAppleFileRoots(appIdentifier: "org.radroots.tests", dataRoot: base.appendingPathComponent("data"), 329 cacheRoot: base.appendingPathComponent("cache"), temporaryRoot: base.appendingPathComponent("tmp")) 330 } 331 332 func path(_ identifier: String) -> URL { 333 directory.appendingPathComponent(RadrootsAppleFileDigest.sha256(Data(identifier.utf8)) + ".lock") 334 } 335 336 func request(_ id: String) throws -> RadrootsBackgroundTransferRequest { 337 try RadrootsBackgroundTransferRequest(identifier: RadrootsBackgroundTransferIdentifier(id), 338 remoteURL: #require(URL(string: "https://example.org/upload")), method: .put, 339 operation: .upload(source: .file(RadrootsFileReference(scope: .cache, relativePath: "body"))), 340 responsePolicy: .boundedJSON()) 341 } 342 343 func remove() { 344 try? FileManager.default.removeItem(at: base) 345 } 346 }