|
| 1 | +import Foundation |
| 2 | + |
| 3 | +/// A retention policy describing which snapshot iterations should be kept around on disk. |
| 4 | +/// |
| 5 | +/// Every write is made as a part of a top-level transaction that gets recorded atomically to disk as a snapshot iteration. These iterations can domtain edits to one or more datastores, and represent a complete view of all data at any one moment in time. Keeping iterations around allows you to rewind the datastores in a consistent and non-breaking way, though they take up disk space for all pages that are no longer current, ie. those containing deletions or older versions of records persisted to disk. |
| 6 | +/// |
| 7 | +/// A retention policy allows the disk persistence to automatically clean up these older iterations according to the policy you need for your app. The retention policy is only enforced when a write transaction completes, though the persistence may defer cleanup until later if write volumes are high. |
| 8 | +public struct SnapshotRetentionPolicy: Sendable { |
| 9 | + /// Internal predicate that tests if an iteration should be pruned. |
| 10 | + /// |
| 11 | + /// - Parameter iteration: The iteration to check. |
| 12 | + /// - Parameter distance: How far the iteration is from the current root. The current root is `0` away from itself, while the next oldest iteration has a distance of `1`. |
| 13 | + /// - Returns: `true` if the iteration, all its ancestors, and all it's other decedents should be pruned, `false` if the next iteration should be checked. |
| 14 | + typealias PrunePredicate = @Sendable (_ iteration: SnapshotIteration, _ distance: Int) -> Bool |
| 15 | + |
| 16 | + /// Internal marker indicating if the retention policy refers to the ``none`` policy. |
| 17 | + let isNone: Bool |
| 18 | + |
| 19 | + /// Internal marker indicating if the retention policy refers to the ``indefinite`` policy. |
| 20 | + let isIndefinite: Bool |
| 21 | + |
| 22 | + /// Internal predicate that tests if an iteration should be pruned. |
| 23 | + /// |
| 24 | + /// - Parameter iteration: The iteration to check. |
| 25 | + /// - Parameter distance: How far the iteration is from the current root. The current root is `0` away from itself, while the next oldest iteration has a distance of `1`. |
| 26 | + /// - Returns: `true` if the iteration, all its ancestors, and all it's other decedents should be pruned, `false` if the next iteration should be checked. |
| 27 | + let shouldPrune: PrunePredicate |
| 28 | + |
| 29 | + /// Internal initializer for creating a retention policy from flags and a predicate. |
| 30 | + /// - Parameters: |
| 31 | + /// - isNone: Wether this represents a ``none`` policy. |
| 32 | + /// - isIndefinite: Wether this represents an ``indefinite`` policy. |
| 33 | + /// - shouldPrune: The predicate to use when testing retention. |
| 34 | + init( |
| 35 | + isNone: Bool = false, |
| 36 | + isIndefinite: Bool = false, |
| 37 | + shouldPrune: @escaping PrunePredicate |
| 38 | + ) { |
| 39 | + self.isNone = isNone |
| 40 | + self.isIndefinite = isIndefinite |
| 41 | + self.shouldPrune = shouldPrune |
| 42 | + } |
| 43 | + |
| 44 | + /// A retention policy that only the most recent iteration should be kept around on disk, and all other iterations should be discarded. |
| 45 | + /// |
| 46 | + /// - Note: It will not be possible to rewind the datastore to a previous state using this policy, and other processes won't be able to read from a read-only datastore while the main one is writing to it. |
| 47 | + public static let none = SnapshotRetentionPolicy(isNone: true) { _, _ in true } |
| 48 | + |
| 49 | + /// A retention policy that includes all iterations. |
| 50 | + /// |
| 51 | + /// - Note: This policy may incur a large amount of disc usage, especially on datastores with many writes. |
| 52 | + public static let indefinite = SnapshotRetentionPolicy(isIndefinite: true) { _, _ in false } |
| 53 | + |
| 54 | + /// A retention policy that retains the specified number of transactions, including the most recent transaction. |
| 55 | + /// |
| 56 | + /// To retain only the most recent transaction, specify a count of `0`. To retain the last 10 transactions, in addition to the current one (leaving up to 11 on disk at once), specify a count of `10`. Specifying a negative number will assert at runtime if assertions are enabled. |
| 57 | + /// |
| 58 | + /// This is a useful way to ensure a minimum number of transactions will always be accessible on disk at once for other processes to read, though the exact number an app will need will depend on how often write transactions occur, and how much disk space each write transaction occupies. |
| 59 | + /// |
| 60 | + /// - Parameter count: The number of additional transactions to retain. |
| 61 | + /// - Returns: A policy retaining at most `count` additional transactions. |
| 62 | + public static func transactionCount(_ count: Int) -> Self { |
| 63 | + assert(count >= 0, "Transaction count must be larger or equal to 0") |
| 64 | + return SnapshotRetentionPolicy { _, distance in distance > count} |
| 65 | + } |
| 66 | + |
| 67 | + /// A retention policy that retains transactions younger than a specified duration. |
| 68 | + /// |
| 69 | + /// A retention cutoff is calculated right at the moment the last write transaction takes place, subtracting the specified `timeInterval` from this moment in time. Note that this policy is sensitive to time changes on the host, as previous transactions record their creation date in a runtime agnostic way that relies on an absolute date and time. |
| 70 | + /// |
| 71 | + /// - Note: This policy may be more stable than ``transactionCount(_:)``, but may incur a non-constant amount of additional disk space depending on write volume. |
| 72 | + /// - Parameter timeInterval: The time interval in seconds to indicate an acceptable retention window. |
| 73 | + /// - Returns: A policy retaining transactions as old as the specified `timeInterval`. |
| 74 | + public static func duration(_ timeInterval: TimeInterval) -> Self { |
| 75 | + SnapshotRetentionPolicy { iteration, _ in iteration.creationDate < Date(timeIntervalSinceNow: -timeInterval)} |
| 76 | + } |
| 77 | + |
| 78 | + /// A retention policy that retains transactions younger than a specified duration. |
| 79 | + /// |
| 80 | + /// A retention cutoff is calculated right at the moment the last write transaction takes place, subtracting the specified `duration` from this moment in time. Note that this policy is sensitive to time changes on the host, as previous transactions record their creation date in a runtime agnostic way that relies on an absolute date and time. |
| 81 | + /// |
| 82 | + /// - Note: This policy may be more stable than ``transactionCount(_:)``, but may incur a non-constant amount of additional disk space depending on write volume. |
| 83 | + /// - Parameter duration: The duration to indicate an acceptable retention window. |
| 84 | + /// - Returns: A policy retaining transactions as old as the specified `duration`. |
| 85 | + @_disfavoredOverload |
| 86 | + @available(macOS 13.0, *) |
| 87 | + public static func duration(_ duration: Duration) -> Self { |
| 88 | + .duration(TimeInterval(duration.components.seconds)) |
| 89 | + } |
| 90 | + |
| 91 | + /// A retention policy that retains transactions younger than a specified duration. |
| 92 | + /// |
| 93 | + /// A retention cutoff is calculated right at the moment the last write transaction takes place, subtracting the specified `duration` from this moment in time. Note that this policy is sensitive to time changes on the host, as previous transactions record their creation date in a runtime agnostic way that relies on an absolute date and time. |
| 94 | + /// |
| 95 | + /// - Note: This policy may be more stable than ``transactionCount(_:)``, but may incur a non-constant amount of additional disk space depending on write volume. |
| 96 | + /// - Parameter duration: The duration in seconds to indicate an acceptable retention window. |
| 97 | + /// - Returns: A policy retaining transactions as old as the specified `duration`. |
| 98 | + public static func duration(_ duration: RetentionDuration) -> Self { |
| 99 | + .duration(TimeInterval(duration.timeInterval)) |
| 100 | + } |
| 101 | + |
| 102 | + /// A retention policy ensuring both specified policies are enforced before pruning a snapshot. |
| 103 | + /// |
| 104 | + /// This policy is useful to indicate that at least the specified number of transactions should be kept around, for at least a specified amount of time: |
| 105 | + /// |
| 106 | + /// persistence.retentionPolicy = .both(.transactionCount(10), and: .duration(.days(2))) |
| 107 | + /// |
| 108 | + /// As a result, this policy errs on the side of keeping transactions around when compared with ``either(_:or:)``. |
| 109 | + /// |
| 110 | + /// - Parameters: |
| 111 | + /// - lhs: A policy to evaluate. |
| 112 | + /// - rhs: Another policy to evaluate. |
| 113 | + /// - Returns: A policy that ensures both `lhs` and `rhs` allow a transaction to be pruned before actually pruning it. |
| 114 | + public static func both(_ lhs: SnapshotRetentionPolicy, and rhs: SnapshotRetentionPolicy) -> Self { |
| 115 | + guard !lhs.isIndefinite, !rhs.isIndefinite else { return .indefinite } |
| 116 | + if lhs.isNone { return rhs } |
| 117 | + if rhs.isNone { return lhs } |
| 118 | + return SnapshotRetentionPolicy { lhs.shouldIterationBePruned(iteration: $0, distance: $1) && rhs.shouldIterationBePruned(iteration: $0, distance: $1)} |
| 119 | + } |
| 120 | + |
| 121 | + /// A retention policy ensuring either specified policies are enforced before pruning a snapshot. |
| 122 | + /// |
| 123 | + /// This policy is useful to indicate that at most the specified number of transactions should be kept around, for at most a specified amount of time: |
| 124 | + /// |
| 125 | + /// persistence.retentionPolicy = .either(.transactionCount(10), or: .duration(.days(2))) |
| 126 | + /// |
| 127 | + /// As a result, this policy errs on the side of removing transactions when compared with ``both(_:and:)``. |
| 128 | + /// |
| 129 | + /// - Parameters: |
| 130 | + /// - lhs: A policy to evaluate. |
| 131 | + /// - rhs: Another policy to evaluate. |
| 132 | + /// - Returns: A policy that ensures either `lhs` or `rhs` allow a transaction to be pruned before actually pruning it. |
| 133 | + public static func either(_ lhs: SnapshotRetentionPolicy, or rhs: SnapshotRetentionPolicy) -> Self { |
| 134 | + guard !lhs.isNone, !rhs.isNone else { return .none } |
| 135 | + if lhs.isIndefinite { return rhs } |
| 136 | + if rhs.isIndefinite { return lhs } |
| 137 | + return SnapshotRetentionPolicy { lhs.shouldIterationBePruned(iteration: $0, distance: $1) || rhs.shouldIterationBePruned(iteration: $0, distance: $1)} |
| 138 | + } |
| 139 | + |
| 140 | + /// Internal method to check if an iteration should be pruned and removed from disk. |
| 141 | + /// |
| 142 | + /// - Parameter iteration: The iteration to check. |
| 143 | + /// - Parameter distance: How far the iteration is from the current root. The current root is `0` away from itself, while the next oldest iteration has a distance of `1`. |
| 144 | + /// - Returns: `true` if the iteration, all its ancestors, and all it's other decedents should be pruned, `false` if the next iteration should be checked. |
| 145 | + func shouldIterationBePruned(iteration: SnapshotIteration, distance: Int) -> Bool { |
| 146 | + shouldPrune(iteration, distance) |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +/// The duration in time snapshot iterations should be retained for. |
| 151 | +public struct RetentionDuration: Hashable, Sendable { |
| 152 | + /// Internal representation of a retention duration. |
| 153 | + @usableFromInline |
| 154 | + var timeInterval: TimeInterval |
| 155 | + |
| 156 | + /// Internal initializer for creating a retention duration from a time interval. |
| 157 | + @usableFromInline |
| 158 | + init(timeInterval: TimeInterval) { |
| 159 | + self.timeInterval = timeInterval |
| 160 | + } |
| 161 | + |
| 162 | + /// A retention duration in seconds. |
| 163 | + @inlinable |
| 164 | + public static func seconds<Int: BinaryInteger>(_ seconds: Int) -> Self { |
| 165 | + RetentionDuration(timeInterval: TimeInterval(seconds)) |
| 166 | + } |
| 167 | + |
| 168 | + /// A retention duration in seconds. |
| 169 | + @inlinable |
| 170 | + public static func seconds<Float: BinaryFloatingPoint>(_ seconds: Float) -> Self { |
| 171 | + RetentionDuration(timeInterval: TimeInterval(seconds)) |
| 172 | + } |
| 173 | + |
| 174 | + /// A retention duration in minutes. |
| 175 | + /// |
| 176 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with minutes when leap seconds are applied. |
| 177 | + @inlinable |
| 178 | + public static func minutes<Int: BinaryInteger>(_ minutes: Int) -> Self { |
| 179 | + RetentionDuration(timeInterval: TimeInterval(minutes)*60) |
| 180 | + } |
| 181 | + |
| 182 | + /// A retention duration in minutes. |
| 183 | + /// |
| 184 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with minutes when leap seconds are applied. |
| 185 | + @inlinable |
| 186 | + public static func minutes<Float: BinaryFloatingPoint>(_ minutes: Float) -> Self { |
| 187 | + RetentionDuration(timeInterval: TimeInterval(minutes)*60) |
| 188 | + } |
| 189 | + |
| 190 | + /// A retention duration in hours. |
| 191 | + /// |
| 192 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with hours on a calendar across events like seasonal time changes dependent on timezone. |
| 193 | + @inlinable |
| 194 | + public static func hours<Int: BinaryInteger>(_ hours: Int) -> Self { |
| 195 | + RetentionDuration(timeInterval: TimeInterval(hours)*60*60) |
| 196 | + } |
| 197 | + |
| 198 | + /// A retention duration in hours. |
| 199 | + /// |
| 200 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with hours on a calendar across events like seasonal time changes dependent on timezone. |
| 201 | + @inlinable |
| 202 | + public static func hours<Float: BinaryFloatingPoint>(_ hours: Float) -> Self { |
| 203 | + RetentionDuration(timeInterval: TimeInterval(hours)*60*60) |
| 204 | + } |
| 205 | + |
| 206 | + /// A retention duration in 24 hour days. |
| 207 | + /// |
| 208 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days on a calendar across events like seasonal time changes dependent on timezone. |
| 209 | + @inlinable |
| 210 | + public static func days<Int: BinaryInteger>(_ days: Int) -> Self { |
| 211 | + RetentionDuration(timeInterval: TimeInterval(days)*60*60*24) |
| 212 | + } |
| 213 | + |
| 214 | + /// A retention duration in 24 hour days. |
| 215 | + /// |
| 216 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days on a calendar across events like seasonal time changes dependent on timezone. |
| 217 | + @inlinable |
| 218 | + public static func days<Float: BinaryFloatingPoint>(_ days: Float) -> Self { |
| 219 | + RetentionDuration(timeInterval: TimeInterval(days)*60*60*24) |
| 220 | + } |
| 221 | + |
| 222 | + /// A retention duration in weeks, defined as seven 24 hour days. |
| 223 | + /// |
| 224 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days on a calendar across events like seasonal time changes dependent on timezone. |
| 225 | + @inlinable |
| 226 | + public static func weeks<Int: BinaryInteger>(_ weeks: Int) -> Self { |
| 227 | + RetentionDuration(timeInterval: TimeInterval(weeks)*60*60*24*7) |
| 228 | + } |
| 229 | + |
| 230 | + /// A retention duration in weeks, defined as seven 24 hour days. |
| 231 | + /// |
| 232 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days on a calendar across events like seasonal time changes dependent on timezone. |
| 233 | + @inlinable |
| 234 | + public static func weeks<Float: BinaryFloatingPoint>(_ weeks: Float) -> Self { |
| 235 | + RetentionDuration(timeInterval: TimeInterval(weeks)*60*60*24*7) |
| 236 | + } |
| 237 | + |
| 238 | + /// A retention duration in months, defined as thirty 24 hour days. |
| 239 | + /// |
| 240 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days or even months on a calendar across events like seasonal time changes dependent on timezone, different length months, or leap days. |
| 241 | + @inlinable |
| 242 | + public static func months<Int: BinaryInteger>(_ months: Int) -> Self { |
| 243 | + RetentionDuration(timeInterval: TimeInterval(months)*60*60*24*30) |
| 244 | + } |
| 245 | + |
| 246 | + /// A retention duration in months, defined as thirty 24 hour days. |
| 247 | + /// |
| 248 | + /// - Warning: This duration does not take into account timezones or calendar dates, and strictly represents a duration of time. It therefore makes no guarantees to line up with days or even months on a calendar across events like seasonal time changes dependent on timezone, different length months, or leap days. |
| 249 | + @inlinable |
| 250 | + public static func months<Float: BinaryFloatingPoint>(_ months: Float) -> Self { |
| 251 | + RetentionDuration(timeInterval: TimeInterval(months)*60*60*24*30) |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +extension RetentionDuration: Comparable { |
| 256 | + @inlinable |
| 257 | + public static func < (lhs: Self, rhs: Self) -> Bool { |
| 258 | + lhs.timeInterval < rhs.timeInterval |
| 259 | + } |
| 260 | +} |
| 261 | + |
| 262 | +extension RetentionDuration: AdditiveArithmetic { |
| 263 | + public static let zero = RetentionDuration(timeInterval: 0) |
| 264 | + |
| 265 | + @inlinable |
| 266 | + public prefix static func + (rhs: Self) -> Self { |
| 267 | + rhs |
| 268 | + } |
| 269 | + |
| 270 | + @inlinable |
| 271 | + public prefix static func - (rhs: Self) -> Self { |
| 272 | + RetentionDuration(timeInterval: -rhs.timeInterval) |
| 273 | + } |
| 274 | + |
| 275 | + @inlinable |
| 276 | + public static func + (lhs: Self, rhs: Self) -> Self { |
| 277 | + RetentionDuration(timeInterval: lhs.timeInterval + rhs.timeInterval) |
| 278 | + } |
| 279 | + |
| 280 | + @inlinable |
| 281 | + public static func += (lhs: inout Self, rhs: Self) { |
| 282 | + lhs.timeInterval += rhs.timeInterval |
| 283 | + } |
| 284 | + |
| 285 | + @inlinable |
| 286 | + public static func - (lhs: Self, rhs: Self) -> Self { |
| 287 | + RetentionDuration(timeInterval: lhs.timeInterval - rhs.timeInterval) |
| 288 | + } |
| 289 | + |
| 290 | + @inlinable |
| 291 | + public static func -= (lhs: inout Self, rhs: Self) { |
| 292 | + lhs.timeInterval -= rhs.timeInterval |
| 293 | + } |
| 294 | +} |
0 commit comments