// // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation /// Executes "sync completion" blocks in order. /// /// The `addSyncCompletion` method may run blocks enqueued from separate /// threads in an arbitrary order. This type runs them in order. /// /// For example, if transactions t1 and t2 enqueue blocks b1 and b2, it's /// possible for t1 to execute before t2 but b2 to execute before b1. This /// behavior may be undesired, and this type fixes it. struct CompletionSerializer { /// Identifies `pendingBlocks`. Protected by `tx`'s exclusivity. private var blockCounter = 0 /// Ordered blocks that must be executed. private let pendingBlocks = AtomicValue<[(blockNumber: Int, block: () -> Void)]>([], lock: .init()) mutating func addOrderedSyncCompletion(tx: DBWriteTransaction, block: @escaping () -> Void) { self.blockCounter += 1 let blockNumber = self.blockCounter // Blocks enter this Array in the order they should be executed. self.pendingBlocks.update { $0.append((blockNumber, block)) } // However, addSyncCompletion blocks are executed in an arbitrary order. tx.addSyncCompletion { [pendingBlocks] in pendingBlocks.update { // Normally, two syncCompletion blocks will execute in order and will each // invoke a single pendingBlock. However, if those syncCompletion blocks // execute in the opposite order, the second one (now running first) will // execute two pendingBlocks, and the first one (now running second) won't // execute any pendingBlocks. Note that the pendingBlocks execute in the // correct order in both cases, and note also that they execute no later // than when their own syncCompletion block executes. while let pendingBlock = $0.first, pendingBlock.blockNumber <= blockNumber { pendingBlock.block() $0 = Array($0.dropFirst()) } } } } }