| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| import Foundation |
| |
| public enum Batch { |
| public typealias BatchCompletionHandler = ErrorType? -> Void |
| public typealias Operation = BatchDatabase throws -> Void |
| |
| /** |
| Runs the given batch operation, managing retries and BatchDatabase's commit() and abort()s. |
| |
| This is run in a background thread and calls back on main. |
| |
| - Parameter retries: number of retries attempted before giving up. defaults to 3 |
| - Parameter db: database on which the batch operation is to be performed |
| - Parameter opts: batch configuration |
| - Parameter op: batch operation |
| - Parameter completionHandler: future result called when runInBatch finishes |
| */ |
| public static func runInBatch(retries: Int = 3, |
| db: Database, |
| opts: BatchOptions?, |
| op: Operation, |
| completionHandler: BatchCompletionHandler) { |
| dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) { |
| for _ in 0 ... retries { |
| // TODO(sadovsky): Commit() can fail for a number of reasons, e.g. RPC |
| // failure or ErrConcurrentTransaction. Depending on the cause of failure, |
| // it may be desirable to retry the Commit() and/or to call Abort(). |
| var err: ErrorType? = nil |
| do { |
| try attemptBatch(db, opts: opts, op: op) |
| err = nil |
| } catch SyncbaseError.ConcurrentBatch { |
| continue |
| } catch let e { |
| log.warning("Unable to complete batch operation: \(e)") |
| err = e |
| } |
| dispatch_async(Syncbase.queue) { |
| completionHandler(err) |
| } |
| return |
| } |
| // We never were able to do it without error |
| dispatch_async(Syncbase.queue) { |
| completionHandler(SyncbaseError.ConcurrentBatch) |
| } |
| } |
| } |
| |
| /** |
| Runs the given batch operation, managing retries and BatchDatabase's commit() and abort()s. |
| |
| This is run in a background thread and calls back on main. |
| |
| - Parameter retries: number of retries attempted before giving up. defaults to 3 |
| - Parameter db: database on which the batch operation is to be performed |
| - Parameter opts: batch configuration |
| - Parameter op: batch operation |
| - Parameter completionHandler: future result called when runInBatch finishes |
| */ |
| public static func runInBatchSync(retries: Int = 3, |
| db: Database, |
| opts: BatchOptions?, |
| op: Operation) throws { |
| for _ in 0 ... retries { |
| // TODO(sadovsky): Commit() can fail for a number of reasons, e.g. RPC |
| // failure or ErrConcurrentTransaction. Depending on the cause of failure, |
| // it may be desirable to retry the Commit() and/or to call Abort(). |
| do { |
| try attemptBatch(db, opts: opts, op: op) |
| return |
| } catch SyncbaseError.ConcurrentBatch { |
| continue |
| } catch let e { |
| log.warning("Unable to complete batch operation: \(e)") |
| throw e |
| } |
| } |
| // We never were able to do it without error. |
| throw SyncbaseError.ConcurrentBatch |
| } |
| |
| private static func attemptBatch(db: Database, opts: BatchOptions?, op: Operation) throws { |
| let batchDb = try db.beginBatch(opts) |
| // Use defer for abort to make sure it gets called in case op throws. |
| var commitCalled = false |
| defer { |
| if !commitCalled { |
| do { |
| try batchDb.abort() |
| } catch let e { |
| log.warning("Unable abort the non-comitted batch: \(e)") |
| } |
| } |
| } |
| // Attempt operation |
| try op(batchDb) |
| // A readonly batch should be Aborted; Commit would fail. |
| if opts?.readOnly ?? false { |
| return |
| } |
| // Commit is about to be called, do not call Abort. |
| commitCalled = true |
| do { |
| try batchDb.commit() |
| } catch SyncbaseError.UnknownBatch { |
| // Occurs if op called batchDb.abort() or batchDb.commit() -- ignore the unknown batch |
| // at this point. |
| } |
| } |
| } |
| |
| public struct BatchOptions { |
| /// Arbitrary string, typically used to describe the intent behind a batch. |
| /// Hints are surfaced to clients during conflict resolution. |
| /// TODO(sadovsky): Use "any" here? |
| public let hint: String? |
| |
| /// ReadOnly specifies whether the batch should allow writes. |
| /// If ReadOnly is set to true, Abort() should be used to release any resources |
| /// associated with this batch (though it is not strictly required), and |
| /// Commit() will always fail. |
| public let readOnly: Bool |
| |
| public init(hint: String? = nil, readOnly: Bool = false) { |
| self.hint = hint |
| self.readOnly = readOnly |
| } |
| } |
| |
| public class BatchDatabase: Database { |
| /// Commit persists the pending changes to the database. |
| /// If the batch is readonly, Commit() will fail with ErrReadOnlyBatch; Abort() |
| /// should be used instead. |
| public func commit() throws { |
| guard let cHandle = try batchHandle?.toCgoString() else { |
| throw SyncbaseError.UnknownBatch |
| } |
| try VError.maybeThrow { errPtr in |
| v23_syncbase_DbCommit( |
| try encodedDatabaseName.toCgoString(), |
| cHandle, |
| errPtr) |
| } |
| } |
| |
| /// Abort notifies the server that any pending changes can be discarded. |
| /// It is not strictly required, but it may allow the server to release locks |
| /// or other resources sooner than if it was not called. |
| public func abort() throws { |
| guard let cHandle = try batchHandle?.toCgoString() else { |
| throw SyncbaseError.UnknownBatch |
| } |
| try VError.maybeThrow { errPtr in |
| v23_syncbase_DbAbort( |
| try encodedDatabaseName.toCgoString(), |
| cHandle, |
| errPtr) |
| } |
| } |
| } |