Skip to content

Commit cc9c484

Browse files
committed
Firestore: Add TransactionOptions to control how many times a transaction will retry commits before failing.
1 parent 5f7e636 commit cc9c484

10 files changed

+275
-20
lines changed

docs/readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ Support
155155

156156
Release Notes
157157
-------------
158+
### 9.2.0
159+
- Changes
160+
- Firestore: Added `TransactionOptions` to control how many times a
161+
transaction will retry commits before failing
162+
([#318](https://github.com/firebase/firebase-unity-sdk/pull/318)).
163+
158164
### 9.1.0
159165
- Changes
160166
- General: Added a missing namespace to the Google.MiniJson.dll.

firestore/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ set(firebase_firestore_src
7878
src/Source.cs
7979
src/Timestamp.cs
8080
src/Transaction.cs
81+
src/TransactionOptions.cs
8182
src/TransactionManager.cs
8283
src/UnknownPropertyHandling.cs
8384
src/ValueDeserializer.cs

firestore/src/FirebaseFirestore.cs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,7 @@ public WriteBatch StartBatch() {
248248
/// be invoked on the main thread.</param>
249249
/// <returns>A task which completes when the transaction has committed.</returns>
250250
public Task RunTransactionAsync(Func<Transaction, Task> callback) {
251-
Preconditions.CheckNotNull(callback, nameof(callback));
252-
// Just pass through to the overload where the callback returns a Task<T>.
253-
return RunTransactionAsync(transaction =>
254-
Util.MapResult<object>(callback(transaction), null));
251+
return RunTransactionAsync(new TransactionOptions(), callback);
255252
}
256253

257254
/// <summary>
@@ -276,8 +273,65 @@ public Task RunTransactionAsync(Func<Transaction, Task> callback) {
276273
/// <returns>A task which completes when the transaction has committed. The result of the task
277274
/// then contains the result of the callback.</returns>
278275
public Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
276+
return RunTransactionAsync(new TransactionOptions(), callback);
277+
}
278+
279+
/// <summary>
280+
/// Runs a transaction asynchronously, with an asynchronous callback that returns a value.
281+
/// The specified callback is executed for a newly-created transaction.
282+
/// </summary>
283+
/// <remarks>
284+
/// <para><c>RunTransactionAsync</c> executes the given callback on the main thread and then
285+
/// attempts to commit the changes applied within the transaction. If any document read within
286+
/// the transaction has changed, the <paramref name="callback"/> will be retried. If it fails to
287+
/// commit after the maximum number of attempts specified in the given <c>TransactionOptions</c>
288+
/// object, the transaction will fail.</para>
289+
///
290+
/// <para>The maximum number of writes allowed in a single transaction is 500, but note that
291+
/// each usage of <see cref="FieldValue.ServerTimestamp"/>, <c>FieldValue.ArrayUnion</c>,
292+
/// <c>FieldValue.ArrayRemove</c>, or <c>FieldValue.Increment</c> inside a transaction counts as
293+
/// an additional write.</para>
294+
/// </remarks>
295+
///
296+
/// <typeparam name="T">The result type of the callback.</typeparam>
297+
/// <param name="options">The transaction options for controlling execution. Must not be
298+
/// <c>null</c>.</param>
299+
/// <param name="callback">The callback to execute. Must not be <c>null</c>. The callback will
300+
/// be invoked on the main thread.</param>
301+
/// <returns>A task which completes when the transaction has committed. The result of the task
302+
/// then contains the result of the callback.</returns>
303+
public Task<T> RunTransactionAsync<T>(TransactionOptions options, Func<Transaction, Task<T>> callback) {
304+
Preconditions.CheckNotNull(options, nameof(options));
305+
Preconditions.CheckNotNull(callback, nameof(callback));
306+
return WithFirestoreProxy(proxy => _transactionManager.RunTransactionAsync(options, callback));
307+
}
308+
309+
/// <summary>
310+
/// Runs a transaction asynchronously, with an asynchronous callback that doesn't return a
311+
/// value. The specified callback is executed for a newly-created transaction.
312+
/// </summary>
313+
/// <remarks>
314+
/// <para><c>RunTransactionAsync</c> executes the given callback on the main thread and then
315+
/// attempts to commit the changes applied within the transaction. If any document read within
316+
/// the transaction has changed, the <paramref name="callback"/> will be retried. If it fails to
317+
/// commit after the maximum number of attempts specified in the given <c>TransactionOptions</c>
318+
/// object, the transaction will fail.</para>
319+
///
320+
/// <para>The maximum number of writes allowed in a single transaction is 500, but note that
321+
/// each usage of <see cref="FieldValue.ServerTimestamp"/>, <c>FieldValue.ArrayUnion</c>,
322+
/// <c>FieldValue.ArrayRemove</c>, or <c>FieldValue.Increment</c> inside a transaction counts as
323+
/// an additional write.</para>
324+
/// </remarks>
325+
/// <param name="options">The transaction options for controlling execution. Must not be
326+
/// <c>null</c>.</param>
327+
/// <param name="callback">The callback to execute. Must not be <c>null</c>. The callback will
328+
/// be invoked on the main thread.</param>
329+
/// <returns>A task which completes when the transaction has committed.</returns>
330+
public Task RunTransactionAsync(TransactionOptions options, Func<Transaction, Task> callback) {
331+
Preconditions.CheckNotNull(options, nameof(options));
279332
Preconditions.CheckNotNull(callback, nameof(callback));
280-
return WithFirestoreProxy(proxy => _transactionManager.RunTransactionAsync(callback));
333+
// Just pass through to the overload where the callback returns a Task<T>.
334+
return RunTransactionAsync(options, transaction => Util.MapResult<object>(callback(transaction), null));
281335
}
282336

283337
private static SnapshotsInSyncCallbackMap snapshotsInSyncCallbacks = new SnapshotsInSyncCallbackMap();

firestore/src/TransactionManager.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ public void Dispose() {
6767
/// <summary>
6868
/// Runs a transaction.
6969
/// </summary>
70+
/// <param name="options">The transaction options to use.</param>.
7071
/// <param name="callback">The callback to run.</param>.
7172
/// <returns>A task that completes when the transaction has completed.</returns>
72-
internal Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
73+
internal Task<T> RunTransactionAsync<T>(TransactionOptions options,
74+
Func<Transaction, Task<T>> callback) {
7375
// Store the result of the most recent invocation of the user-supplied callback.
7476
bool callbackWrapperInvoked = false;
7577
Task<T> lastCallbackTask = null;
@@ -118,7 +120,7 @@ internal Task<T> RunTransactionAsync<T>(Func<Transaction, Task<T>> callback) {
118120
}
119121
};
120122

121-
return _transactionManagerProxy.RunTransactionAsync(callbackId, ExecuteCallback)
123+
return _transactionManagerProxy.RunTransactionAsync(callbackId, options.Proxy, ExecuteCallback)
122124
.ContinueWith<T>(overallCallback);
123125
}
124126

firestore/src/TransactionOptions.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Threading;
17+
18+
namespace Firebase.Firestore {
19+
20+
/// <summary>
21+
/// Options to customize transaction behavior for
22+
/// <see cref="FirebaseFirestore.RunTransactionAsync"/>.
23+
/// </summary>
24+
public sealed class TransactionOptions {
25+
26+
// The lock that must be held during all accesses to _proxy.
27+
private readonly ReaderWriterLock _proxyLock = new ReaderWriterLock();
28+
29+
// The underlying C++ TransactionOptions object.
30+
private TransactionOptionsProxy _proxy = new TransactionOptionsProxy();
31+
32+
internal TransactionOptionsProxy Proxy {
33+
get {
34+
_proxyLock.AcquireReaderLock(Int32.MaxValue);
35+
try {
36+
return new TransactionOptionsProxy(_proxy);
37+
} finally {
38+
_proxyLock.ReleaseReaderLock();
39+
}
40+
}
41+
}
42+
43+
/// <summary>
44+
/// Creates the default <c>TransactionOptions</c>.
45+
/// </summary>
46+
public TransactionOptions() {
47+
}
48+
49+
/// <summary>
50+
/// The maximum number of attempts to commit, after which the transaction fails.
51+
/// </summary>
52+
///
53+
/// <remarks>
54+
/// The default value is 5, and must be greater than zero.
55+
/// </remarks>
56+
public Int32 MaxAttempts {
57+
get {
58+
_proxyLock.AcquireReaderLock(Int32.MaxValue);
59+
try {
60+
return _proxy.max_attempts();
61+
} finally {
62+
_proxyLock.ReleaseReaderLock();
63+
}
64+
}
65+
set {
66+
_proxyLock.AcquireWriterLock(Int32.MaxValue);
67+
try {
68+
_proxy.set_max_attempts(value);
69+
} finally {
70+
_proxyLock.ReleaseWriterLock();
71+
}
72+
}
73+
}
74+
75+
/// <inheritdoc />
76+
public override string ToString() {
77+
return nameof(TransactionOptions) + "{" + nameof(MaxAttempts) + "=" + MaxAttempts + "}";
78+
}
79+
80+
}
81+
82+
}

firestore/src/swig/firestore.i

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@ SWIG_CREATE_PROXY(firebase::firestore::LoadBundleTaskProgress);
411411
%rename("%s") firebase::firestore::LoadBundleTaskProgress::state;
412412
%include "firestore/src/include/firebase/firestore/load_bundle_task_progress.h"
413413
414+
// Generate a C# wrapper for TransactionOptions.
415+
SWIG_CREATE_PROXY(firebase::firestore::TransactionOptions);
416+
%rename("%s") firebase::firestore::TransactionOptions::TransactionOptions;
417+
%rename("%s") firebase::firestore::TransactionOptions::max_attempts;
418+
%rename("%s") firebase::firestore::TransactionOptions::set_max_attempts;
419+
%include "firestore/src/include/firebase/firestore/transaction_options.h"
420+
414421
// Generate a C# wrapper for Firestore. Comes last because it refers to multiple
415422
// other classes (e.g. `CollectionReference`).
416423
SWIG_CREATE_PROXY(firebase::firestore::Firestore);

firestore/src/swig/transaction_manager.cc

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,25 @@ class TransactionManagerInternal
177177
}
178178

179179
Future<void> RunTransaction(int32_t callback_id,
180+
TransactionOptions options,
180181
TransactionCallbackFn callback_fn) {
181182
std::lock_guard<std::mutex> lock(mutex_);
182183
if (is_disposed_) {
183184
return {};
184185
}
185186

186187
auto shared_this = shared_from_this();
187-
return firestore_->RunTransaction([shared_this, callback_id, callback_fn](
188-
Transaction& transaction,
189-
std::string& error_message) {
190-
if (shared_this->ExecuteCallback(callback_id, callback_fn, transaction)) {
191-
return Error::kErrorOk;
192-
} else {
193-
// Return a non-retryable error code.
194-
return Error::kErrorInvalidArgument;
188+
return firestore_->RunTransaction(
189+
options,
190+
[shared_this, callback_id, callback_fn](Transaction& transaction, std::string& error_message) {
191+
if (shared_this->ExecuteCallback(callback_id, callback_fn, transaction)) {
192+
return Error::kErrorOk;
193+
} else {
194+
// Return a non-retryable error code.
195+
return Error::kErrorInvalidArgument;
196+
}
195197
}
196-
});
198+
);
197199
}
198200

199201
private:
@@ -271,14 +273,15 @@ void TransactionManager::Dispose() {
271273
}
272274

273275
Future<void> TransactionManager::RunTransaction(
274-
int32_t callback_id, TransactionCallbackFn callback_fn) {
276+
int32_t callback_id, TransactionOptions options,
277+
TransactionCallbackFn callback_fn) {
275278
// Make a local copy of `internal_` since it could be reset asynchronously
276279
// by a call to `Dispose()`.
277280
std::shared_ptr<TransactionManagerInternal> internal_local = internal_;
278281
if (!internal_local) {
279282
return {};
280283
}
281-
return internal_local->RunTransaction(callback_id, callback_fn);
284+
return internal_local->RunTransaction(callback_id, options, callback_fn);
282285
}
283286

284287
void TransactionCallback::OnCompletion(bool callback_successful) {

firestore/src/swig/transaction_manager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ class TransactionManager {
198198
// If `Dispose()` has been invoked, or the `Firestore` instance has been
199199
// destroyed, then this method will immediately return an invalid `Future`.
200200
Future<void> RunTransaction(int32_t callback_id,
201+
TransactionOptions options,
201202
TransactionCallbackFn callback);
202203

203204
private:

firestore/testapp/Assets/Firebase/Sample/Firestore/InvalidArgumentsTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ public static InvalidArgumentsTestCase[] TestCases {
171171
name = "FirebaseFirestore_RunTransactionAsync_WithTypeParameter_NullCallback",
172172
action = FirebaseFirestore_RunTransactionAsync_WithTypeParameter_NullCallback
173173
},
174+
new InvalidArgumentsTestCase {
175+
name = "FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback",
176+
action = FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback
177+
},
178+
new InvalidArgumentsTestCase {
179+
name = "FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback",
180+
action = FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback
181+
},
174182
new InvalidArgumentsTestCase { name = "FirebaseFirestoreSettings_Host_Null",
175183
action = FirebaseFirestoreSettings_Host_Null },
176184
new InvalidArgumentsTestCase { name = "FirebaseFirestoreSettings_Host_EmptyString",
@@ -693,6 +701,36 @@ private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_Null
693701
() => handler.db.RunTransactionAsync<object>(null));
694702
}
695703

704+
private static void FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullCallback(
705+
UIHandlerAutomated handler) {
706+
var options = new TransactionOptions();
707+
handler.AssertException(typeof(ArgumentNullException),
708+
() => handler.db.RunTransactionAsync(options, null));
709+
}
710+
711+
private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullCallback(
712+
UIHandlerAutomated handler) {
713+
var options = new TransactionOptions();
714+
handler.AssertException(typeof(ArgumentNullException),
715+
() => handler.db.RunTransactionAsync<object>(options, null));
716+
}
717+
718+
private static void FirebaseFirestore_RunTransactionAsync_WithoutTypeParameter_WithOptions_NullOptions(
719+
UIHandlerAutomated handler) {
720+
DocumentReference doc = handler.TestDocument();
721+
handler.AssertException(typeof(ArgumentNullException),
722+
() => handler.db.RunTransactionAsync(null, tx => tx.GetSnapshotAsync(doc)));
723+
}
724+
725+
private static void FirebaseFirestore_RunTransactionAsync_WithTypeParameter_WithOptions_NullOptions(
726+
UIHandlerAutomated handler) {
727+
DocumentReference doc = handler.TestDocument();
728+
handler.AssertException(typeof(ArgumentNullException),
729+
() => handler.db.RunTransactionAsync<object>(null, tx => tx.GetSnapshotAsync(doc)
730+
.ContinueWith(snapshot => new object()))
731+
);
732+
}
733+
696734
private static void FirebaseFirestoreSettings_Host_Null(UIHandlerAutomated handler) {
697735
FirebaseFirestoreSettings settings = handler.db.Settings;
698736
handler.AssertException(typeof(ArgumentNullException), () => settings.Host = null);

0 commit comments

Comments
 (0)