@@ -81,27 +81,34 @@ struct Prune: AsyncParsableCommand {
8181 }
8282
8383 static func pruneSpaceBudget( prunableStorages: [ PrunableStorage ] , spaceBudgetBytes: UInt64 ) throws {
84- let prunables : [ Prunable ] = try prunableStorages
85- . flatMap { try $0. prunables ( ) }
86- . sorted { try $0. accessDate ( ) > $1. accessDate ( ) }
87-
88- var spaceBudgetBytes = spaceBudgetBytes
89- var prunablesToDelete : [ Prunable ] = [ ]
90-
91- for prunable in prunables {
92- let prunableSizeBytes = UInt64 ( try prunable. allocatedSizeBytes ( ) )
93-
94- if prunableSizeBytes <= spaceBudgetBytes {
95- // Don't mark for deletion as
96- // there's a budget available
97- spaceBudgetBytes -= prunableSizeBytes
98- } else {
99- // Mark for deletion
100- prunablesToDelete. append ( prunable)
84+ while true {
85+ let prunables : [ Prunable ] = try prunableStorages
86+ . flatMap { try $0. prunables ( ) }
87+ . sorted { try $0. accessDate ( ) > $1. accessDate ( ) }
88+
89+ var remainingBudgetBytes = spaceBudgetBytes
90+ var prunableToDelete : Prunable ?
91+
92+ for prunable in prunables {
93+ let prunableSizeBytes = UInt64 ( try prunable. allocatedSizeBytes ( ) )
94+
95+ if prunableSizeBytes <= remainingBudgetBytes {
96+ // Don't mark for deletion as there is budget available
97+ remainingBudgetBytes -= prunableSizeBytes
98+ } else {
99+ prunableToDelete = prunable
100+ break
101+ }
102+ }
103+
104+ guard let prunableToDelete else {
105+ return
101106 }
102- }
103107
104- try prunablesToDelete. forEach { try $0. delete ( ) }
108+ // Deleting one cached stacked image can change which remaining image
109+ // owns shared immutable content. Rebuild before choosing another.
110+ try prunableToDelete. delete ( )
111+ }
105112 }
106113
107114 static func reclaimIfNeeded( _ requiredBytes: UInt64 , _ initiator: Prunable ? = nil ) throws {
@@ -145,46 +152,51 @@ struct Prune: AsyncParsableCommand {
145152 try Prune . reclaimIfPossible ( requiredBytes - volumeAvailableCapacityCalculated, initiator)
146153 }
147154
148- private static func reclaimIfPossible( _ reclaimBytes: UInt64 , _ initiator: Prunable ? = nil ) throws {
155+ static func reclaimIfPossible( _ reclaimBytes: UInt64 , _ initiator: Prunable ? = nil ) throws {
149156 let span = OTel . shared. tracer. spanBuilder ( spanName: " prune " ) . startSpan ( )
150157 defer { span. end ( ) }
151158
152159 let prunableStorages : [ PrunableStorage ] = [ try VMStorageOCI ( ) , try IPSWCache ( ) ]
153- let prunables : [ Prunable ] = try prunableStorages
154- . flatMap { try $0. prunables ( ) }
155- . sorted { try $0. accessDate ( ) < $1. accessDate ( ) }
160+ let prunables = {
161+ try prunableStorages
162+ . flatMap { try $0. prunables ( ) }
163+ . sorted { try $0. accessDate ( ) < $1. accessDate ( ) }
164+ }
156165
157166 // Does it even make sense to start?
158- let cacheUsedBytes = try prunables. map { try $0. allocatedSizeBytes ( ) } . reduce ( 0 , + )
159- if cacheUsedBytes < reclaimBytes {
167+ let initialPrunables = try prunables ( )
168+ let initialCacheUsedBytes = try initialPrunables. map { try $0. allocatedSizeBytes ( ) } . reduce ( 0 , + )
169+ guard let reclaimBytes = Int ( exactly: reclaimBytes) , initialCacheUsedBytes >= reclaimBytes else {
160170 return
161171 }
162172
163- var cacheReclaimedBytes : Int = 0
164-
165- var it = prunables. makeIterator ( )
173+ let targetCacheUsedBytes = initialCacheUsedBytes - reclaimBytes
174+ var currentCacheUsedBytes = initialCacheUsedBytes
175+ let initiatorPath = initiator. map {
176+ $0. url. resolvingSymlinksInPath ( ) . standardizedFileURL. path
177+ }
166178
167- while cacheReclaimedBytes <= reclaimBytes {
168- guard let prunable = it. next ( ) else {
179+ while currentCacheUsedBytes > targetCacheUsedBytes {
180+ // Deleting one cached stacked image can transfer ownership of shared
181+ // immutable content to another record without reclaiming those bytes.
182+ // Rebuild the candidates after every deletion so automatic pruning
183+ // measures the cache that remains rather than a stale ownership snapshot.
184+ guard let prunable = try prunables ( ) . first ( where: {
185+ $0. url. resolvingSymlinksInPath ( ) . standardizedFileURL. path != initiatorPath
186+ } ) else {
169187 break
170188 }
171189
172- if prunable. url == initiator? . url. resolvingSymlinksInPath ( ) {
173- // do not prune the initiator
174- continue
175- }
176-
177190 let allocatedSizeBytes = try prunable. allocatedSizeBytes ( )
178191
179192 OpenTelemetry . instance. contextProvider. activeSpan?
180193 . addEvent ( name: " Pruned \( allocatedSizeBytes) bytes for \( prunable. url. path) " )
181194
182- cacheReclaimedBytes += allocatedSizeBytes
183-
184195 try prunable. delete ( )
196+ currentCacheUsedBytes = try prunables ( ) . map { try $0. allocatedSizeBytes ( ) } . reduce ( 0 , + )
185197 }
186198
187199 OpenTelemetry . instance. contextProvider. activeSpan?
188- . addEvent ( name: " Reclaimed \( cacheReclaimedBytes ) bytes " )
200+ . addEvent ( name: " Reclaimed \( initialCacheUsedBytes - currentCacheUsedBytes ) bytes " )
189201 }
190202}
0 commit comments