Skip to content

Commit 963ed96

Browse files
authored
Merge pull request #245 from giginet/shared-restored-cache-to-other-storages
Add automatic cache sharing for restored frameworks
2 parents d051f60 + 90f7035 commit 963ed96

File tree

4 files changed

+322
-12
lines changed

4 files changed

+322
-12
lines changed

Sources/ScipioKit/Producer/Cache/CacheSystem.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public struct SwiftPMCacheKey: CacheKey {
4848
}
4949

5050
struct CacheSystem: Sendable {
51-
static let defaultParalellNumber = 8
51+
static let defaultParallelNumber = 8
5252
private let outputDirectory: URL
5353
private let fileSystem: any FileSystem
5454

@@ -92,7 +92,7 @@ struct CacheSystem: Sendable {
9292
}
9393

9494
private func cacheFrameworks(_ targets: Set<CacheTarget>, to storage: some CacheStorage) async {
95-
let chunked = targets.chunks(ofCount: storage.parallelNumber ?? CacheSystem.defaultParalellNumber)
95+
let chunked = targets.chunks(ofCount: storage.parallelNumber ?? CacheSystem.defaultParallelNumber)
9696

9797
let storageName = storage.displayName
9898
for chunk in chunked {

Sources/ScipioKit/Producer/Cache/LocalDiskCacheStorage.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import ScipioStorage
33

4-
struct LocalDiskCacheStorage: CacheStorage {
4+
struct LocalDiskCacheStorage: CacheStorage, Equatable {
55
private let fileSystem: any FileSystem
66

77
var parallelNumber: Int? { nil }
@@ -19,6 +19,11 @@ struct LocalDiskCacheStorage: CacheStorage {
1919
self.fileSystem = fileSystem
2020
}
2121

22+
// MARK: - Equatable
23+
static func == (lhs: LocalDiskCacheStorage, rhs: LocalDiskCacheStorage) -> Bool {
24+
lhs.baseURL == rhs.baseURL
25+
}
26+
2227
private func buildBaseDirectoryPath() throws -> URL {
2328
let cacheDir: URL
2429
if let baseURL {

Sources/ScipioKit/Producer/FrameworkProducer.swift

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,25 @@ struct FrameworkProducer {
100100
// no-op
101101
targetGraph.remove(valid)
102102
} else {
103-
let restored = await restoreAllAvailableCachesIfNeeded(
103+
let restoredSetsToSourceStorage = await restoreAllAvailableCachesIfNeeded(
104104
availableTargets: targets.subtracting(valid),
105105
to: storagesWithConsumer,
106106
cacheSystem: cacheSystem
107107
)
108-
let skipTargets = valid.union(restored)
108+
109+
let allRestoredTargets = restoredSetsToSourceStorage.keys.reduce(into: Set<CacheSystem.CacheTarget>()) { result, targetSet in
110+
result.formUnion(targetSet)
111+
}
112+
113+
if !allRestoredTargets.isEmpty {
114+
await shareRestoredCachesToProducers(
115+
allRestoredTargets,
116+
restoredSetsToSourceStorage: restoredSetsToSourceStorage,
117+
cacheSystem: cacheSystem
118+
)
119+
}
120+
121+
let skipTargets = valid.union(allRestoredTargets)
109122
targetGraph.remove(skipTargets)
110123
}
111124
dependencyGraphToBuild = targetGraph
@@ -137,7 +150,7 @@ struct FrameworkProducer {
137150
availableTargets: Set<CacheSystem.CacheTarget>,
138151
cacheSystem: CacheSystem
139152
) async -> Set<CacheSystem.CacheTarget> {
140-
let chunked = availableTargets.chunks(ofCount: CacheSystem.defaultParalellNumber)
153+
let chunked = availableTargets.chunks(ofCount: CacheSystem.defaultParallelNumber)
141154

142155
var validFrameworks: Set<CacheSystem.CacheTarget> = []
143156
for chunk in chunked {
@@ -147,7 +160,7 @@ struct FrameworkProducer {
147160
do {
148161
let product = target.buildProduct
149162
let frameworkName = product.frameworkName
150-
let outputPath = outputDir.appendingPathComponent(frameworkName)
163+
let outputPath = outputDir.appending(component: frameworkName)
151164
let exists = fileSystem.exists(outputPath)
152165
guard exists else { return nil }
153166

@@ -184,9 +197,9 @@ struct FrameworkProducer {
184197
availableTargets: Set<CacheSystem.CacheTarget>,
185198
to storages: [any CacheStorage],
186199
cacheSystem: CacheSystem
187-
) async -> Set<CacheSystem.CacheTarget> {
200+
) async -> [Set<CacheSystem.CacheTarget>: any CacheStorage] {
188201
var remainingTargets = availableTargets
189-
var restored: Set<CacheSystem.CacheTarget> = []
202+
var restoredSetsToSourceStorage: [Set<CacheSystem.CacheTarget>: any CacheStorage] = [:]
190203

191204
for index in storages.indices {
192205
let storage = storages[index]
@@ -209,7 +222,11 @@ struct FrameworkProducer {
209222
from: storage,
210223
cacheSystem: cacheSystem
211224
)
212-
restored.formUnion(restoredPerStorage)
225+
226+
// Record which storage restored which set of targets
227+
if !restoredPerStorage.isEmpty {
228+
restoredSetsToSourceStorage[restoredPerStorage] = storage
229+
}
213230

214231
logger.info(
215232
"⏸️ Restoration finished with cache storage: \(logSuffix)",
@@ -224,15 +241,15 @@ struct FrameworkProducer {
224241
}
225242

226243
logger.info("⏹️ Restoration finished", metadata: .color(.green))
227-
return restored
244+
return restoredSetsToSourceStorage
228245
}
229246

230247
private func restoreCaches(
231248
for targets: Set<CacheSystem.CacheTarget>,
232249
from cacheStorage: any CacheStorage,
233250
cacheSystem: CacheSystem
234251
) async -> Set<CacheSystem.CacheTarget> {
235-
let chunked = targets.chunks(ofCount: cacheStorage.parallelNumber ?? CacheSystem.defaultParalellNumber)
252+
let chunked = targets.chunks(ofCount: cacheStorage.parallelNumber ?? CacheSystem.defaultParallelNumber)
236253

237254
var restored: Set<CacheSystem.CacheTarget> = []
238255
for chunk in chunked {
@@ -369,6 +386,89 @@ struct FrameworkProducer {
369386
logger.warning("⚠️ Could not create VersionFile. This framework will not be cached.", metadata: .color(.yellow))
370387
}
371388
}
389+
390+
private func shareRestoredCachesToProducers(
391+
_ allRestoredTargets: Set<CacheSystem.CacheTarget>,
392+
restoredSetsToSourceStorage: [Set<CacheSystem.CacheTarget>: any CacheStorage],
393+
cacheSystem: CacheSystem
394+
) async {
395+
let storagesWithProducer = cachePolicies.storages(for: .producer)
396+
guard !storagesWithProducer.isEmpty else { return }
397+
398+
logger.info(
399+
"🔄 Sharing \(allRestoredTargets.count) restored framework(s) to other cache storages",
400+
metadata: .color(.blue)
401+
)
402+
403+
for storage in storagesWithProducer {
404+
// Filter targets to exclude those that were restored from this storage
405+
var targetsToShare = allRestoredTargets
406+
407+
// Remove targets that were restored from this storage
408+
for (restoredSet, sourceStorage) in restoredSetsToSourceStorage where areStoragesEqual(sourceStorage, storage) {
409+
targetsToShare.subtract(restoredSet)
410+
}
411+
412+
if !targetsToShare.isEmpty {
413+
await shareCachesToStorage(targetsToShare, to: storage, cacheSystem: cacheSystem)
414+
}
415+
}
416+
417+
logger.info("⏹️ Sharing to other cache storages finished", metadata: .color(.green))
418+
}
419+
420+
private func areStoragesEqual(_ lhs: any CacheStorage, _ rhs: any CacheStorage) -> Bool {
421+
// ProjectCacheStorage instances are always considered the same
422+
if lhs is ProjectCacheStorage && rhs is ProjectCacheStorage {
423+
return true
424+
}
425+
426+
// For LocalDiskCacheStorage (value type), use Equatable comparison
427+
if let lhsLocal = lhs as? LocalDiskCacheStorage,
428+
let rhsLocal = rhs as? LocalDiskCacheStorage {
429+
return lhsLocal == rhsLocal
430+
}
431+
432+
// For reference types (actors, classes), use ObjectIdentifier comparison
433+
return ObjectIdentifier(lhs as AnyObject) == ObjectIdentifier(rhs as AnyObject)
434+
}
435+
436+
private func shareCachesToStorage(
437+
_ targets: Set<CacheSystem.CacheTarget>,
438+
to storage: any CacheStorage,
439+
cacheSystem: CacheSystem
440+
) async {
441+
let chunked = targets.chunks(ofCount: storage.parallelNumber ?? CacheSystem.defaultParallelNumber)
442+
443+
for chunk in chunked {
444+
await withTaskGroup(of: Void.self) { group in
445+
for target in chunk {
446+
group.addTask {
447+
do {
448+
let cacheKey = try await cacheSystem.calculateCacheKey(of: target)
449+
let hasCache = try await storage.existsValidCache(for: cacheKey)
450+
guard !hasCache else { return }
451+
452+
let frameworkName = target.buildProduct.frameworkName
453+
let frameworkPath = outputDir.appending(component: frameworkName)
454+
455+
logger.info(
456+
"🔄 Share \(frameworkName) to cache storage: \(storage.displayName)",
457+
metadata: .color(.blue)
458+
)
459+
try await storage.cacheFramework(frameworkPath, for: cacheKey)
460+
} catch {
461+
logger.warning(
462+
"⚠️ Failed to share cache to \(storage.displayName): \(error.localizedDescription)",
463+
metadata: .color(.yellow)
464+
)
465+
}
466+
}
467+
}
468+
await group.waitForAll()
469+
}
470+
}
471+
}
372472
}
373473

374474
extension [Runner.Options.CachePolicy] {

0 commit comments

Comments
 (0)