Skip to content

Firestore TransactionOptions added, to specify maxAttempts #318

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 6 commits into from
Jul 7, 2022
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
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