Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public actor DiskPersistence<AccessMode: _AccessMode>: Persistence {
var rollingPageCacheIndex = 0
var rollingPageCache: [Datastore.Page] = []

var transactionCounter = 0

/// Initialize a ``DiskPersistence`` with a read-write URL.
///
/// Use this initializer when creating a persistence from the main process that will access it, such as your app. To access the same persistence from another process, use ``init(readOnlyURL:)`` instead.
Expand Down Expand Up @@ -541,16 +543,26 @@ extension DiskPersistence {
// MARK: - Transactions

extension DiskPersistence {
func nextTransactionCounter() -> Int {
let transactionIndex = transactionCounter
transactionCounter += 1
return transactionIndex
}

public func _withTransaction<T: Sendable>(
actionName: String?,
options: UnsafeTransactionOptions,
transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T
) async throws -> T {
try await withoutActuallyEscaping(transaction) { escapingTransaction in
let currentCounter = nextTransactionCounter()
// print("[CDS] Starting transaction \(currentCounter) “\(actionName ?? "")” - \(options)")
let (transaction, task) = await Transaction.makeTransaction(
persistence: self,
transactionIndex: currentCounter,
lastTransaction: lastTransaction,
actionName: actionName, options: options
actionName: actionName,
options: options
) { interface, isDurable in
try await escapingTransaction(interface, isDurable)
}
Expand All @@ -560,11 +572,14 @@ extension DiskPersistence {
lastTransaction = transaction
}

return try await task.value
let result = try await task.value
// print("[CDS] Finished transaction \(currentCounter) “\(actionName ?? "")” - \(options)")
return result
}
}

func persist(
transactionIndex: Int,
actionName: String?,
roots: [DatastoreKey : Datastore.RootObject],
addedDatastoreRoots: Set<DatastoreRootReference>,
Expand All @@ -583,6 +598,8 @@ extension DiskPersistence {
/// If nothing changed, don't bother writing anything.
if !containsEdits { return }

// print("[CDS] Persisting \(transactionIndex) “\(actionName ?? "")” - \(roots.keys), added \(addedDatastoreRoots), removed \(removedDatastoreRoots)")

/// If we are read-only, make sure no edits have been made
guard let self = self as? DiskPersistence<ReadWrite>
else { throw DiskPersistenceError.cannotWrite }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ extension DiskPersistence {
weak var lastReadWriteChildTransaction: Transaction?

private(set) var task: Task<Void, Error>!
let options: UnsafeTransactionOptions
let transactionIndex: Int
let actionName: String?
let options: UnsafeTransactionOptions

var rootObjects: [DatastoreKey : Datastore.RootObject] = [:]

Expand All @@ -37,6 +38,7 @@ extension DiskPersistence {

private init(
persistence: DiskPersistence,
transactionIndex: Int,
parent: Transaction?,
actionName: String?,
options: UnsafeTransactionOptions
Expand All @@ -45,6 +47,7 @@ extension DiskPersistence {
self.parent = parent
self.actionName = actionName
self.options = options
self.transactionIndex = transactionIndex
}

private func attachTask<T>(
Expand Down Expand Up @@ -147,6 +150,7 @@ extension DiskPersistence {
assert(deletedPages.isEmpty, "Pages were deleted in a read-only transaction!")
return
}
// print("[CDS] Offloading persist to parent \(transactionIndex) - \(options)")
try await parent.apply(
rootObjects: rootObjects,
entryMutations: entryMutations,
Expand Down Expand Up @@ -176,6 +180,7 @@ extension DiskPersistence {
let removedDatastoreRoots = Set(deletedRootObjects.map(\.referenceID))

try await persistence.persist(
transactionIndex: transactionIndex,
actionName: actionName,
roots: rootObjects,
addedDatastoreRoots: addedDatastoreRoots,
Expand All @@ -197,18 +202,26 @@ extension DiskPersistence {

static func makeTransaction<T>(
persistence: DiskPersistence,
transactionIndex: Int,
lastTransaction: Transaction?,
actionName: String?,
options: UnsafeTransactionOptions,
@_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T
) async -> (Transaction, Task<T, Error>) {
if let parent = Self.unsafeCurrentTransaction {
let (child, task) = await parent.childTransaction(options: options, handler: handler)
// print("[CDS] Found parent, making child \(transactionIndex)")
let (child, task) = await parent.childTransaction(
transactionIndex: transactionIndex,
actionName: actionName,
options: options,
handler: handler
)
return (child, task)
}

let transaction = Transaction(
persistence: persistence,
transactionIndex: transactionIndex,
parent: nil,
actionName: actionName,
options: options
Expand All @@ -226,14 +239,17 @@ extension DiskPersistence {
}

func childTransaction<T>(
transactionIndex: Int,
actionName: String?,
options: UnsafeTransactionOptions,
@_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T
) async -> (Transaction, Task<T, Error>) {
assert(!self.options.contains(.readOnly) || options.contains(.readOnly), "A child transaction was declared read-write, even though its parent was read-only!")
let childTransaction = Transaction(
persistence: persistence,
transactionIndex: transactionIndex,
parent: self,
actionName: nil,
actionName: actionName,
options: options
)

Expand Down
22 changes: 20 additions & 2 deletions Sources/CodableDatastore/Persistence/TransactionOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// A set of options that the caller of a transaction can specify.
public struct TransactionOptions: OptionSet, Sendable {
public struct TransactionOptions: OptionSet, Sendable, CustomDebugStringConvertible {
public let rawValue: UInt64

public init(rawValue: UInt64) {
Expand All @@ -24,12 +24,20 @@ public struct TransactionOptions: OptionSet, Sendable {

/// The transaction is idempotent and does not modify any other kind of state, and can be retried when it encounters an inconsistency. This allows a transaction to concurrently operate with other writes, which may be necessary in a disptributed environment.
public static let idempotent = Self(rawValue: 1 << 2)

public var debugDescription: String {
var tokens: [String] = []
if contains(.readOnly) { tokens.append(".readOnly") }
if contains(.collateWrites) { tokens.append(".collateWrites") }
if contains(.idempotent) { tokens.append(".idempotent") }
return "TransactionOptions([\(tokens.joined(separator: ", "))])"
}
}

/// A set of options that the caller of a transaction can specify.
///
/// These options are generally unsafe to use improperly, and should generally not be used.
public struct UnsafeTransactionOptions: OptionSet, Sendable {
public struct UnsafeTransactionOptions: OptionSet, Sendable, CustomDebugStringConvertible {
public let rawValue: UInt64

public init(rawValue: UInt64) {
Expand All @@ -54,4 +62,14 @@ public struct UnsafeTransactionOptions: OptionSet, Sendable {

/// The transaction should persist to storage even if it is a child transaction. Note that this must be the _first_ non-readonly child transaction of a parent transaction to succeed.
public static let enforceDurability = Self(rawValue: 1 << 17)

public var debugDescription: String {
var tokens: [String] = []
if contains(.readOnly) { tokens.append(".readOnly") }
if contains(.collateWrites) { tokens.append(".collateWrites") }
if contains(.idempotent) { tokens.append(".idempotent") }
if contains(.skipObservations) { tokens.append(".skipObservations") }
if contains(.enforceDurability) { tokens.append(".enforceDurability") }
return "UnsafeTransactionOptions([\(tokens.joined(separator: ", "))])"
}
}
75 changes: 75 additions & 0 deletions Tests/CodableDatastoreTests/TransactionOptionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ final class TransactionOptionsTests: XCTestCase {
XCTAssertEqual(options.rawValue, expectedRawValue, file: file, line: line)
}

func assertTransactionOptions(
_ options: TransactionOptions,
haveDebugString expectedString: String,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertEqual(options.debugDescription, expectedString, file: file, line: line)
}

func testTransactionOptions() {
assertTransactionOptions(options: [], expectedRawValue: 0)

Expand All @@ -39,6 +48,21 @@ final class TransactionOptionsTests: XCTestCase {
assertTransactionOptions(options: TransactionOptions(rawValue: 10), expectedRawValue: 2)
assertTransactionOptions(options: TransactionOptions(rawValue: 11), expectedRawValue: 3)
}

func testDebugStrings() {
assertTransactionOptions([], haveDebugString: "TransactionOptions([])")

assertTransactionOptions(.readOnly, haveDebugString: "TransactionOptions([.readOnly])")
assertTransactionOptions(.idempotent, haveDebugString: "TransactionOptions([.idempotent])")
assertTransactionOptions(.collateWrites, haveDebugString: "TransactionOptions([.collateWrites])")

assertTransactionOptions([.idempotent, .readOnly], haveDebugString: "TransactionOptions([.readOnly, .idempotent])")
assertTransactionOptions([.readOnly, .idempotent], haveDebugString: "TransactionOptions([.readOnly, .idempotent])")
assertTransactionOptions([.readOnly, .collateWrites], haveDebugString: "TransactionOptions([.readOnly, .collateWrites])")
assertTransactionOptions([.idempotent, .collateWrites], haveDebugString: "TransactionOptions([.collateWrites, .idempotent])")

assertTransactionOptions([.readOnly, .idempotent, .collateWrites], haveDebugString: "TransactionOptions([.readOnly, .collateWrites, .idempotent])")
}
}

final class UnsafeTransactionOptionsTests: XCTestCase {
Expand All @@ -51,6 +75,15 @@ final class UnsafeTransactionOptionsTests: XCTestCase {
XCTAssertEqual(options.rawValue, expectedRawValue, file: file, line: line)
}

func assertUnsafeTransactionOptions(
_ options: UnsafeTransactionOptions,
haveDebugString expectedString: String,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertEqual(options.debugDescription, expectedString, file: file, line: line)
}

func testUnsafeTransactionOptions() {
assertUnsafeTransactionOptions(options: [], expectedRawValue: 0)

Expand Down Expand Up @@ -87,4 +120,46 @@ final class UnsafeTransactionOptionsTests: XCTestCase {

assertUnsafeTransactionOptions(options: UnsafeTransactionOptions(TransactionOptions([.readOnly, .collateWrites, .idempotent])), expectedRawValue: 7)
}

func testDebugStrings() {
assertUnsafeTransactionOptions([], haveDebugString: "UnsafeTransactionOptions([])")

assertUnsafeTransactionOptions(.readOnly, haveDebugString: "UnsafeTransactionOptions([.readOnly])")
assertUnsafeTransactionOptions(.idempotent, haveDebugString: "UnsafeTransactionOptions([.idempotent])")
assertUnsafeTransactionOptions(.collateWrites, haveDebugString: "UnsafeTransactionOptions([.collateWrites])")
assertUnsafeTransactionOptions(.skipObservations, haveDebugString: "UnsafeTransactionOptions([.skipObservations])")
assertUnsafeTransactionOptions(.enforceDurability, haveDebugString: "UnsafeTransactionOptions([.enforceDurability])")

assertUnsafeTransactionOptions([.idempotent, .readOnly], haveDebugString: "UnsafeTransactionOptions([.readOnly, .idempotent])")
assertUnsafeTransactionOptions([.readOnly, .idempotent], haveDebugString: "UnsafeTransactionOptions([.readOnly, .idempotent])")

assertUnsafeTransactionOptions([.readOnly, .collateWrites], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites])")
assertUnsafeTransactionOptions([.readOnly, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.readOnly, .skipObservations])")
assertUnsafeTransactionOptions([.readOnly, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .enforceDurability])")
assertUnsafeTransactionOptions([.collateWrites, .idempotent], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .idempotent])")
assertUnsafeTransactionOptions([.collateWrites, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .skipObservations])")
assertUnsafeTransactionOptions([.collateWrites, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .enforceDurability])")
assertUnsafeTransactionOptions([.idempotent, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.idempotent, .skipObservations])")
assertUnsafeTransactionOptions([.idempotent, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.idempotent, .enforceDurability])")
assertUnsafeTransactionOptions([.skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.skipObservations, .enforceDurability])")

assertUnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent])")
assertUnsafeTransactionOptions([.readOnly, .collateWrites, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .skipObservations])")
assertUnsafeTransactionOptions([.readOnly, .collateWrites, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .enforceDurability])")
assertUnsafeTransactionOptions([.readOnly, .idempotent, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.readOnly, .idempotent, .skipObservations])")
assertUnsafeTransactionOptions([.readOnly, .idempotent, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .idempotent, .enforceDurability])")
assertUnsafeTransactionOptions([.readOnly, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .skipObservations, .enforceDurability])")
assertUnsafeTransactionOptions([.collateWrites, .idempotent, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .idempotent, .skipObservations])")
assertUnsafeTransactionOptions([.collateWrites, .idempotent, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .idempotent, .enforceDurability])")
assertUnsafeTransactionOptions([.collateWrites, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .skipObservations, .enforceDurability])")
assertUnsafeTransactionOptions([.idempotent, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.idempotent, .skipObservations, .enforceDurability])")

assertUnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .skipObservations], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .skipObservations])")
assertUnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .enforceDurability])")
assertUnsafeTransactionOptions([.readOnly, .collateWrites, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .skipObservations, .enforceDurability])")
assertUnsafeTransactionOptions([.readOnly, .idempotent, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .idempotent, .skipObservations, .enforceDurability])")
assertUnsafeTransactionOptions([.collateWrites, .idempotent, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.collateWrites, .idempotent, .skipObservations, .enforceDurability])")

assertUnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .skipObservations, .enforceDurability], haveDebugString: "UnsafeTransactionOptions([.readOnly, .collateWrites, .idempotent, .skipObservations, .enforceDurability])")
}
}