Skip to content

Commit

Permalink
Firestore TransactionOptions added, to specify maxAttempts (#318)
Browse files Browse the repository at this point in the history
  • Loading branch information
dconeybe authored Jul 7, 2022
1 parent e6a84ca commit 45781bc
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 20 deletions.
3 changes: 3 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ Release Notes
### 9.2.0
- Changes
- Crashlytics: Fix requiring user code to reference Crashlytics when using il2cpp.
- Firestore: Added `TransactionOptions` to control how many times a
transaction will retry commits before failing
([#318](https://github.com/firebase/firebase-unity-sdk/pull/318)).

### 9.1.0
- Changes
Expand Down
1 change: 1 addition & 0 deletions firestore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ set(firebase_firestore_src
src/Source.cs
src/Timestamp.cs
src/Transaction.cs
src/TransactionOptions.cs
src/TransactionManager.cs
src/UnknownPropertyHandling.cs
src/ValueDeserializer.cs
Expand Down
64 changes: 59 additions & 5 deletions firestore/src/FirebaseFirestore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,7 @@ public WriteBatch StartBatch() {
/// be invoked on the main thread.</param>
/// <returns>A task which completes when the transaction has committed.</returns>
public Task RunTransactionAsync(Func<Transaction, Task> callback) {
Preconditions.CheckNotNull(callback, nameof(callback));
// Just pass through to the overload where the callback returns a Task<T>.
return RunTransactionAsync(transaction =>
Util.MapResult<object>(callback(transaction), null));
return RunTransactionAsync(new TransactionOptions(), callback);
}

/// <summary>
Expand All @@ -276,8 +273,65 @@ public Task RunTransactionAsync(Func<Transaction, Task> callback) {
/// <returns>A task which completes when the transaction has committed. The result of the task
/// then contains the result of the callback.</returns>
public Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
return RunTransactionAsync(new TransactionOptions(), callback);
}

/// <summary>
/// Runs a transaction asynchronously, with an asynchronous callback that returns a value.
/// The specified callback is executed for a newly-created transaction.
/// </summary>
/// <remarks>
/// <para><c>RunTransactionAsync</c> executes the given callback on the main thread and then
/// attempts to commit the changes applied within the transaction. If any document read within
/// the transaction has changed, the <paramref name="callback"/> will be retried. If it fails to
/// commit after the maximum number of attempts specified in the given <c>TransactionOptions</c>
/// object, the transaction will fail.</para>
///
/// <para>The maximum number of writes allowed in a single transaction is 500, but note that
/// each usage of <see cref="FieldValue.ServerTimestamp"/>, <c>FieldValue.ArrayUnion</c>,
/// <c>FieldValue.ArrayRemove</c>, or <c>FieldValue.Increment</c> inside a transaction counts as
/// an additional write.</para>
/// </remarks>
///
/// <typeparam name="T">The result type of the callback.</typeparam>
/// <param name="options">The transaction options for controlling execution. Must not be
/// <c>null</c>.</param>
/// <param name="callback">The callback to execute. Must not be <c>null</c>. The callback will
/// be invoked on the main thread.</param>
/// <returns>A task which completes when the transaction has committed. The result of the task
/// then contains the result of the callback.</returns>
public Task<T> RunTransactionAsync<T>(TransactionOptions options, Func<Transaction, Task<T>> callback) {
Preconditions.CheckNotNull(options, nameof(options));
Preconditions.CheckNotNull(callback, nameof(callback));
return WithFirestoreProxy(proxy => _transactionManager.RunTransactionAsync(options, callback));
}

/// <summary>
/// Runs a transaction asynchronously, with an asynchronous callback that doesn't return a
/// value. The specified callback is executed for a newly-created transaction.
/// </summary>
/// <remarks>
/// <para><c>RunTransactionAsync</c> executes the given callback on the main thread and then
/// attempts to commit the changes applied within the transaction. If any document read within
/// the transaction has changed, the <paramref name="callback"/> will be retried. If it fails to
/// commit after the maximum number of attempts specified in the given <c>TransactionOptions</c>
/// object, the transaction will fail.</para>
///
/// <para>The maximum number of writes allowed in a single transaction is 500, but note that
/// each usage of <see cref="FieldValue.ServerTimestamp"/>, <c>FieldValue.ArrayUnion</c>,
/// <c>FieldValue.ArrayRemove</c>, or <c>FieldValue.Increment</c> inside a transaction counts as
/// an additional write.</para>
/// </remarks>
/// <param name="options">The transaction options for controlling execution. Must not be
/// <c>null</c>.</param>
/// <param name="callback">The callback to execute. Must not be <c>null</c>. The callback will
/// be invoked on the main thread.</param>
/// <returns>A task which completes when the transaction has committed.</returns>
public Task RunTransactionAsync(TransactionOptions options, Func<Transaction, Task> callback) {
Preconditions.CheckNotNull(options, nameof(options));
Preconditions.CheckNotNull(callback, nameof(callback));
return WithFirestoreProxy(proxy => _transactionManager.RunTransactionAsync(callback));
// Just pass through to the overload where the callback returns a Task<T>.
return RunTransactionAsync(options, transaction => Util.MapResult<object>(callback(transaction), null));
}

private static SnapshotsInSyncCallbackMap snapshotsInSyncCallbacks = new SnapshotsInSyncCallbackMap();
Expand Down
6 changes: 4 additions & 2 deletions firestore/src/TransactionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ public void Dispose() {
/// <summary>
/// Runs a transaction.
/// </summary>
/// <param name="options">The transaction options to use.</param>.
/// <param name="callback">The callback to run.</param>.
/// <returns>A task that completes when the transaction has completed.</returns>
internal Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
internal Task<T> RunTransactionAsync<T>(TransactionOptions options,
Func<Transaction, Task<T>> callback) {
// Store the result of the most recent invocation of the user-supplied callback.
bool callbackWrapperInvoked = false;
Task<T> lastCallbackTask = null;
Expand Down Expand Up @@ -118,7 +120,7 @@ internal Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
}
};

return _transactionManagerProxy.RunTransactionAsync(callbackId, ExecuteCallback)
return _transactionManagerProxy.RunTransactionAsync(callbackId, options.Proxy, ExecuteCallback)
.ContinueWith<T>(overallCallback);
}

Expand Down
82 changes: 82 additions & 0 deletions firestore/src/TransactionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Threading;

namespace Firebase.Firestore {

/// <summary>
/// Options to customize transaction behavior for
/// <see cref="FirebaseFirestore.RunTransactionAsync"/>.
/// </summary>
public sealed class TransactionOptions {

// The lock that must be held during all accesses to _proxy.
private readonly ReaderWriterLock _proxyLock = new ReaderWriterLock();

// The underlying C++ TransactionOptions object.
private TransactionOptionsProxy _proxy = new TransactionOptionsProxy();

internal TransactionOptionsProxy Proxy {
get {
_proxyLock.AcquireReaderLock(Int32.MaxValue);
try {
return new TransactionOptionsProxy(_proxy);
} finally {
_proxyLock.ReleaseReaderLock();
}
}
}

/// <summary>
/// Creates the default <c>TransactionOptions</c>.
/// </summary>
public TransactionOptions() {
}

/// <summary>
/// The maximum number of attempts to commit, after which the transaction fails.
/// </summary>
///
/// <remarks>
/// The default value is 5, and must be greater than zero.
/// </remarks>
public Int32 MaxAttempts {
get {
_proxyLock.AcquireReaderLock(Int32.MaxValue);
try {
return _proxy.max_attempts();
} finally {
_proxyLock.ReleaseReaderLock();
}
}
set {
_proxyLock.AcquireWriterLock(Int32.MaxValue);
try {
_proxy.set_max_attempts(value);
} finally {
_proxyLock.ReleaseWriterLock();
}
}
}

/// <inheritdoc />
public override string ToString() {
return nameof(TransactionOptions) + "{" + nameof(MaxAttempts) + "=" + MaxAttempts + "}";
}

}

}
7 changes: 7 additions & 0 deletions firestore/src/swig/firestore.i
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ SWIG_CREATE_PROXY(firebase::firestore::LoadBundleTaskProgress);
%rename("%s") firebase::firestore::LoadBundleTaskProgress::state;
%include "firestore/src/include/firebase/firestore/load_bundle_task_progress.h"
// Generate a C# wrapper for TransactionOptions.
SWIG_CREATE_PROXY(firebase::firestore::TransactionOptions);
%rename("%s") firebase::firestore::TransactionOptions::TransactionOptions;
%rename("%s") firebase::firestore::TransactionOptions::max_attempts;
%rename("%s") firebase::firestore::TransactionOptions::set_max_attempts;
%include "firestore/src/include/firebase/firestore/transaction_options.h"
// Generate a C# wrapper for Firestore. Comes last because it refers to multiple
// other classes (e.g. `CollectionReference`).
SWIG_CREATE_PROXY(firebase::firestore::Firestore);
Expand Down
25 changes: 14 additions & 11 deletions firestore/src/swig/transaction_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,23 +177,25 @@ class TransactionManagerInternal
}

Future<void> RunTransaction(int32_t callback_id,
TransactionOptions options,
TransactionCallbackFn callback_fn) {
std::lock_guard<std::mutex> lock(mutex_);
if (is_disposed_) {
return {};
}

auto shared_this = shared_from_this();
return firestore_->RunTransaction([shared_this, callback_id, callback_fn](
Transaction& transaction,
std::string& error_message) {
if (shared_this->ExecuteCallback(callback_id, callback_fn, transaction)) {
return Error::kErrorOk;
} else {
// Return a non-retryable error code.
return Error::kErrorInvalidArgument;
return firestore_->RunTransaction(
options,
[shared_this, callback_id, callback_fn](Transaction& transaction, std::string& error_message) {
if (shared_this->ExecuteCallback(callback_id, callback_fn, transaction)) {
return Error::kErrorOk;
} else {
// Return a non-retryable error code.
return Error::kErrorInvalidArgument;
}
}
});
);
}

private:
Expand Down Expand Up @@ -271,14 +273,15 @@ void TransactionManager::Dispose() {
}

Future<void> TransactionManager::RunTransaction(
int32_t callback_id, TransactionCallbackFn callback_fn) {
int32_t callback_id, TransactionOptions options,
TransactionCallbackFn callback_fn) {
// Make a local copy of `internal_` since it could be reset asynchronously
// by a call to `Dispose()`.
std::shared_ptr<TransactionManagerInternal> internal_local = internal_;
if (!internal_local) {
return {};
}
return internal_local->RunTransaction(callback_id, callback_fn);
return internal_local->RunTransaction(callback_id, options, callback_fn);
}

void TransactionCallback::OnCompletion(bool callback_successful) {
Expand Down
1 change: 1 addition & 0 deletions firestore/src/swig/transaction_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class TransactionManager {
// If `Dispose()` has been invoked, or the `Firestore` instance has been
// destroyed, then this method will immediately return an invalid `Future`.
Future<void> RunTransaction(int32_t callback_id,
TransactionOptions options,
TransactionCallbackFn callback);

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ public static InvalidArgumentsTestCase[] TestCases {
name = "FirebaseFirestore_RunTransactionAsync_WithTypeParameter_NullCallback",
action = FirebaseFirestore_RunTransactionAsync_WithTypeParameter_NullCallback
},
new InvalidArgumentsTestCase {
name = "FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback",
action = FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback
},
new InvalidArgumentsTestCase {
name = "FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback",
action = FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback
},
new InvalidArgumentsTestCase { name = "FirebaseFirestoreSettings_Host_Null",
action = FirebaseFirestoreSettings_Host_Null },
new InvalidArgumentsTestCase { name = "FirebaseFirestoreSettings_Host_EmptyString",
Expand Down Expand Up @@ -693,6 +701,36 @@ private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_Null
() => handler.db.RunTransactionAsync<object>(null));
}

private static void FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback(
UIHandlerAutomated handler) {
var options = new TransactionOptions();
handler.AssertException(typeof(ArgumentNullException),
() => handler.db.RunTransactionAsync(options, null));
}

private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback(
UIHandlerAutomated handler) {
var options = new TransactionOptions();
handler.AssertException(typeof(ArgumentNullException),
() => handler.db.RunTransactionAsync<object>(options, null));
}

private static void FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullOptions(
UIHandlerAutomated handler) {
DocumentReference doc = handler.TestDocument();
handler.AssertException(typeof(ArgumentNullException),
() => handler.db.RunTransactionAsync(null, tx => tx.GetSnapshotAsync(doc)));
}

private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullOptions(
UIHandlerAutomated handler) {
DocumentReference doc = handler.TestDocument();
handler.AssertException(typeof(ArgumentNullException),
() => handler.db.RunTransactionAsync<object>(null, tx => tx.GetSnapshotAsync(doc)
.ContinueWith(snapshot => new object()))
);
}

private static void FirebaseFirestoreSettings_Host_Null(UIHandlerAutomated handler) {
FirebaseFirestoreSettings settings = handler.db.Settings;
handler.AssertException(typeof(ArgumentNullException), () => settings.Host = null);
Expand Down
Loading

0 comments on commit 45781bc

Please sign in to comment.