diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 2c6561ffd6..801a924e9a 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -494,6 +494,7 @@ if (IOS) ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/snapshot_metadata.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/source.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/transaction.h + ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/transaction_options.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/write_batch.h ${FIREBASE_POD_DIR}/Pods/FirebaseFirestore/Firestore/core/include/firebase/firestore/firestore_errors.h ${FIREBASE_POD_DIR}/Pods/FirebaseFirestore/Firestore/core/include/firebase/firestore/firestore_version.h diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt index 59e5813ab3..84730a8b51 100644 --- a/firestore/CMakeLists.txt +++ b/firestore/CMakeLists.txt @@ -49,6 +49,7 @@ set(common_SRCS src/common/to_string.h src/common/type_mapping.h src/common/transaction.cc + src/common/transaction_options.cc src/common/util.cc src/common/util.h src/common/write_batch.cc @@ -127,6 +128,10 @@ set(android_SRCS src/android/timestamp_portable.cc src/android/transaction_android.cc src/android/transaction_android.h + src/android/transaction_options_android.cc + src/android/transaction_options_android.h + src/android/transaction_options_builder_android.cc + src/android/transaction_options_builder_android.h src/android/util_android.cc src/android/util_android.h src/android/wrapper.cc diff --git a/firestore/integration_test_internal/CMakeLists.txt b/firestore/integration_test_internal/CMakeLists.txt index 4e03212053..fd80ab2656 100644 --- a/firestore/integration_test_internal/CMakeLists.txt +++ b/firestore/integration_test_internal/CMakeLists.txt @@ -100,6 +100,7 @@ set(FIREBASE_INTEGRATION_TEST_PORTABLE_TEST_SRCS src/smoke_test.cc src/source_test.cc src/transaction_test.cc + src/transaction_options_test.cc src/type_test.cc src/validation_test.cc src/write_batch_test.cc @@ -116,6 +117,7 @@ set(FIREBASE_INTEGRATION_TEST_ANDROID_TEST_SRCS src/android/settings_android_test.cc src/android/snapshot_metadata_android_test.cc src/android/timestamp_android_test.cc + src/android/transaction_options_android_test.cc src/jni/declaration_test.cc src/jni/env_test.cc src/jni/object_test.cc diff --git a/firestore/integration_test_internal/integration_test.xcodeproj/project.pbxproj b/firestore/integration_test_internal/integration_test.xcodeproj/project.pbxproj index 75c12490f8..a2b4b16315 100644 --- a/firestore/integration_test_internal/integration_test.xcodeproj/project.pbxproj +++ b/firestore/integration_test_internal/integration_test.xcodeproj/project.pbxproj @@ -94,6 +94,8 @@ D6ED33BD2606CD890058CBF9 /* future_test_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = D6ED33B92606CD890058CBF9 /* future_test_util.cc */; }; D6ED33BE2606CD890058CBF9 /* integration_test_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = D6ED33BB2606CD890058CBF9 /* integration_test_util.cc */; }; D86FB1D3D4A592C451A29369 /* firebase_firestore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7F45B5446094E107E88721A /* firebase_firestore.framework */; }; + ED39362728657B740050FE2B /* transaction_options_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED39362628657B740050FE2B /* transaction_options_test.cc */; }; + ED39362828657B740050FE2B /* transaction_options_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED39362628657B740050FE2B /* transaction_options_test.cc */; }; EDEEC7632800CD0000EFBAAF /* leveldb_snappy_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDEEC7622800CD0000EFBAAF /* leveldb_snappy_test.cc */; }; EDEEC7642800CD0000EFBAAF /* leveldb_snappy_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDEEC7622800CD0000EFBAAF /* leveldb_snappy_test.cc */; }; /* End PBXBuildFile section */ @@ -171,6 +173,7 @@ D6ED33BB2606CD890058CBF9 /* integration_test_util.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = integration_test_util.cc; path = src/util/integration_test_util.cc; sourceTree = ""; }; D6ED33BC2606CD890058CBF9 /* future_test_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = future_test_util.h; path = src/util/future_test_util.h; sourceTree = ""; }; EAFAF9474EC412ADCC65F2CC /* firebase_firestore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = firebase_firestore.framework; path = Frameworks/firebase_firestore.framework; sourceTree = ""; }; + ED39362628657B740050FE2B /* transaction_options_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = transaction_options_test.cc; path = src/transaction_options_test.cc; sourceTree = ""; }; EDEEC7622800CD0000EFBAAF /* leveldb_snappy_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = leveldb_snappy_test.cc; path = src/leveldb_snappy_test.cc; sourceTree = ""; }; /* End PBXFileReference section */ @@ -262,6 +265,7 @@ 5292271D1C85FB5500C89379 /* src */ = { isa = PBXGroup; children = ( + ED39362628657B740050FE2B /* transaction_options_test.cc */, D60C58862819C902002E8A04 /* empty.swift */, EDEEC7622800CD0000EFBAAF /* leveldb_snappy_test.cc */, 12D513182684C8D100A83FAA /* bundle_builder.cc */, @@ -567,6 +571,7 @@ D6ED33BD2606CD890058CBF9 /* future_test_util.cc in Sources */, D6AAAD592606C22D0025C53B /* firestore_integration_test.cc in Sources */, D6C179EE22CB323300C2651A /* firebase_test_framework.cc in Sources */, + ED39362728657B740050FE2B /* transaction_options_test.cc in Sources */, D6AAAD4B2606C22D0025C53B /* array_transform_test.cc in Sources */, D6AAAD512606C22D0025C53B /* sanity_test.cc in Sources */, D6AAAD482606C22D0025C53B /* cursor_test.cc in Sources */, @@ -590,6 +595,7 @@ BC1D6843267B00EB005DC2DA /* numeric_transforms_test.cc in Sources */, BC1D6844267B00EB005DC2DA /* array_transform_test.cc in Sources */, BC1D6854267B00EB005DC2DA /* listener_registration_test.cc in Sources */, + ED39362828657B740050FE2B /* transaction_options_test.cc in Sources */, BC1D683F267B00EB005DC2DA /* document_snapshot_test.cc in Sources */, BC1D684E267B00EB005DC2DA /* includes_test.cc in Sources */, BC1D684C267B00EB005DC2DA /* document_change_test.cc in Sources */, diff --git a/firestore/integration_test_internal/src/android/transaction_options_android_test.cc b/firestore/integration_test_internal/src/android/transaction_options_android_test.cc new file mode 100644 index 0000000000..aa12fe54f7 --- /dev/null +++ b/firestore/integration_test_internal/src/android/transaction_options_android_test.cc @@ -0,0 +1,67 @@ +/* + * 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 + * + * http://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. + */ + +#include "firestore/src/android/transaction_options_android.h" +#include "firestore/src/android/transaction_options_builder_android.h" + +#include "firestore/src/jni/env.h" +#include "firestore_integration_test.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Env; +using jni::Local; + +using TransactionOptionsTestAndroid = FirestoreIntegrationTest; + +TEST_F(TransactionOptionsTestAndroid, DefaultTransactionOptions) { + Env env; + Local builder = + TransactionOptionsBuilderInternal::Create(env); + + Local options = builder.Build(env); + + EXPECT_EQ(options.GetMaxAttempts(env), 5); +} + +TEST_F(TransactionOptionsTestAndroid, SetMaxAttemptsReturnsSameInstance) { + Env env; + Local builder = + TransactionOptionsBuilderInternal::Create(env); + + Local retval = + builder.SetMaxAttempts(env, 42); + + EXPECT_TRUE(env.IsSameObject(builder, retval)); +} + +TEST_F(TransactionOptionsTestAndroid, SetMaxAttempts) { + Env env; + Local builder = + TransactionOptionsBuilderInternal::Create(env); + + builder.SetMaxAttempts(env, 42); + + EXPECT_EQ(builder.Build(env).GetMaxAttempts(env), 42); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/integration_test_internal/src/transaction_options_test.cc b/firestore/integration_test_internal/src/transaction_options_test.cc new file mode 100644 index 0000000000..07aa2a068e --- /dev/null +++ b/firestore/integration_test_internal/src/transaction_options_test.cc @@ -0,0 +1,167 @@ +/* + * 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 + * + * http://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. + */ + +#include +#include +#include + +#include "firebase/firestore.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace { + +TEST(TransactionOptionsTest, TypeTraits) { + static_assert(std::is_trivially_copyable::value, + "Update the public doxygen comments about TransactionOptions " + "being trivially copyable in the header file if it ever " + "changes to NOT be trivially copyable."); +} + +TEST(TransactionOptionsTest, DefaultConstructor) { + TransactionOptions options; + EXPECT_EQ(options.max_attempts(), 5); +} + +TEST(TransactionOptionsTest, CopyConstructor) { + TransactionOptions options; + options.set_max_attempts(99); + + TransactionOptions copied_options(options); + + EXPECT_EQ(options.max_attempts(), 99); + EXPECT_EQ(copied_options.max_attempts(), 99); +} + +TEST(TransactionOptionsTest, CopyAssignmentOperator) { + TransactionOptions options; + options.set_max_attempts(99); + TransactionOptions options_copy_dest; + options_copy_dest.set_max_attempts(333); + + options_copy_dest = options; + + EXPECT_EQ(options.max_attempts(), 99); + EXPECT_EQ(options_copy_dest.max_attempts(), 99); +} + +TEST(TransactionOptionsTest, MoveConstructor) { + TransactionOptions options; + options.set_max_attempts(99); + + TransactionOptions moved_options(std::move(options)); + + EXPECT_EQ(moved_options.max_attempts(), 99); +} + +TEST(TransactionOptionsTest, MoveAssignmentOperator) { + TransactionOptions options; + options.set_max_attempts(99); + TransactionOptions options_move_dest; + options_move_dest.set_max_attempts(333); + + options_move_dest = std::move(options); + + EXPECT_EQ(options_move_dest.max_attempts(), 99); +} + +TEST(TransactionOptionsTest, SetMaxAttemptsSetsValidValues) { + TransactionOptions options; + options.set_max_attempts(10); + EXPECT_EQ(options.max_attempts(), 10); + options.set_max_attempts(1); + EXPECT_EQ(options.max_attempts(), 1); + options.set_max_attempts(2); + EXPECT_EQ(options.max_attempts(), 2); + options.set_max_attempts(INT32_MAX); + EXPECT_EQ(options.max_attempts(), INT32_MAX); +} + +TEST(TransactionOptionsTest, SetMaxAttemptsThrowsOnInvalidValues) { + TransactionOptions options; + EXPECT_ANY_THROW(options.set_max_attempts(0)); + EXPECT_ANY_THROW(options.set_max_attempts(-1)); + EXPECT_ANY_THROW(options.set_max_attempts(INT32_MIN)); +} + +TEST(TransactionOptionsTest, ToString) { + TransactionOptions options; + options.set_max_attempts(42); + + const std::string to_string_result = options.ToString(); + + EXPECT_EQ(to_string_result, "TransactionOptions(max_attempts=42)"); +} + +TEST(TransactionOptionsTest, RightShiftOperatorToOutputStream) { + TransactionOptions options; + options.set_max_attempts(42); + const std::string expected_str = options.ToString(); + std::ostringstream ss; + + ss << options; + + EXPECT_EQ(ss.str(), expected_str); +} + +TEST(TransactionOptionsTest, EqualsOperator) { + TransactionOptions default_options1; + TransactionOptions default_options2; + TransactionOptions options1a; + options1a.set_max_attempts(1); + TransactionOptions options1b; + options1b.set_max_attempts(1); + TransactionOptions options2a; + options2a.set_max_attempts(99); + TransactionOptions options2b; + options2b.set_max_attempts(99); + + EXPECT_TRUE(default_options1 == default_options1); + EXPECT_TRUE(default_options1 == default_options2); + EXPECT_TRUE(options1a == options1b); + EXPECT_TRUE(options2a == options2b); + + EXPECT_FALSE(options1a == options2a); + EXPECT_FALSE(options1a == default_options1); + EXPECT_FALSE(options2a == default_options1); +} + +TEST(TransactionOptionsTest, NotEqualsOperator) { + TransactionOptions default_options1; + TransactionOptions default_options2; + TransactionOptions options1a; + options1a.set_max_attempts(1); + TransactionOptions options1b; + options1b.set_max_attempts(1); + TransactionOptions options2a; + options2a.set_max_attempts(99); + TransactionOptions options2b; + options2b.set_max_attempts(99); + + EXPECT_FALSE(default_options1 != default_options1); + EXPECT_FALSE(default_options1 != default_options2); + EXPECT_FALSE(options1a != options1b); + EXPECT_FALSE(options2a != options2b); + + EXPECT_TRUE(options1a != options2a); + EXPECT_TRUE(options1a != default_options1); + EXPECT_TRUE(options2a != default_options1); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/integration_test_internal/src/transaction_test.cc b/firestore/integration_test_internal/src/transaction_test.cc index a061eb333a..12c47c837b 100644 --- a/firestore/integration_test_internal/src/transaction_test.cc +++ b/firestore/integration_test_internal/src/transaction_test.cc @@ -14,6 +14,8 @@ * limitations under the License. */ +#include +#include #include #include @@ -728,5 +730,73 @@ TEST_F(TransactionTest, TestCancellationOnError) { EXPECT_FALSE(snapshot.exists()); } +TEST_F(TransactionTest, TestMaxAttempts) { + Firestore* firestore = TestFirestore(); + DocumentReference doc = firestore->Collection("TestMaxAttempts").Document(); + + TransactionOptions options; + options.set_max_attempts(3); + + // Keep track of the number of times that the update callback is invoked. + auto update_count = std::make_shared(); + update_count->store(0); + + Future run_transaction_future = firestore->RunTransaction( + options, + [update_count, &doc](Transaction& transaction, std::string&) -> Error { + SCOPED_TRACE("Update callback; update_count=" + + std::to_string(update_count->load())); + ++(*update_count); + + // Get the document via the transaction. + { + SCOPED_TRACE("transaction.Get()"); + Error error = Error::kErrorOk; + std::string error_message; + transaction.Get(doc, &error, &error_message); + if (error != kErrorOk) { + ADD_FAILURE() << "transaction.Get() failed: " << error_message + << " (error code " << error << ")"; + return Error::kErrorInternal; + } + } + + // Modify the document *outside* of the transaction. + { + SCOPED_TRACE("doc.Set() outside of transaction"); + auto set_future = + doc.Set({{"count", FieldValue::Integer(update_count->load())}}); + set_future.Await(10000L); + if (set_future.status() != FutureStatus::kFutureStatusComplete) { + ADD_FAILURE() << "Timeout waiting for doc.Set() to complete"; + return Error::kErrorInternal; + } else if (set_future.error() != Error::kErrorOk) { + ADD_FAILURE() << "doc.Set() failed: " << set_future.error_message() + << " (error code " << set_future.error() << ")"; + return Error::kErrorInternal; + } + } + + // Modify the document *using* of the transaction. + // This will fail because the document was modified since we retrieved + // it via the transaction, and will result in a retry. + transaction.Set( + doc, {{"count", FieldValue::String("this set should fail")}}); + + return Error::kErrorOk; + }); + + { + SCOPED_TRACE("Waiting for Future returned from RunTransaction()"); + Await(run_transaction_future); + ASSERT_EQ(run_transaction_future.status(), + FutureStatus::kFutureStatusComplete); + EXPECT_EQ(run_transaction_future.error(), Error::kErrorFailedPrecondition) + << "error message: " << run_transaction_future.error_message(); + } + + EXPECT_EQ(update_count->load(), 3); +} + } // namespace firestore } // namespace firebase diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index 765af93eac..62917accd8 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -52,8 +52,11 @@ #include "firestore/src/android/source_android.h" #include "firestore/src/android/timestamp_android.h" #include "firestore/src/android/transaction_android.h" +#include "firestore/src/android/transaction_options_android.h" +#include "firestore/src/android/transaction_options_builder_android.h" #include "firestore/src/android/wrapper.h" #include "firestore/src/android/write_batch_android.h" +#include "firestore/src/common/hard_assert_common.h" #include "firestore/src/common/make_unique.h" #include "firestore/src/include/firebase/firestore.h" #include "firestore/src/jni/array.h" @@ -120,7 +123,8 @@ Method kSetSettings( Method kBatch("batch", "()Lcom/google/firebase/firestore/WriteBatch;"); Method kRunTransaction( "runTransaction", - "(Lcom/google/firebase/firestore/Transaction$Function;)" + "(Lcom/google/firebase/firestore/TransactionOptions;" + "Lcom/google/firebase/firestore/Transaction$Function;)" "Lcom/google/android/gms/tasks/Task;"); Method kEnableNetwork("enableNetwork", "()Lcom/google/android/gms/tasks/Task;"); @@ -339,6 +343,8 @@ bool FirestoreInternal::Initialize(App* app) { Task::Initialize(loader); TimestampInternal::Initialize(loader); TransactionInternal::Initialize(loader); + TransactionOptionsBuilderInternal::Initialize(loader); + TransactionOptionsInternal::Initialize(loader); WriteBatchInternal::Initialize(loader); LoadBundleTaskInternal::Initialize(loader); LoadBundleTaskProgressInternal::Initialize(loader); @@ -455,12 +461,23 @@ WriteBatch FirestoreInternal::batch() const { } Future FirestoreInternal::RunTransaction( - std::function update) { + std::function update, + int32_t max_attempts) { + SIMPLE_HARD_ASSERT(max_attempts > 0); + auto* lambda_update = new LambdaTransactionFunction(Move(update)); Env env = GetEnv(); Local transaction_function = TransactionInternal::Create(env, this, lambda_update); - Local task = env.Call(obj_, kRunTransaction, transaction_function); + + Local options_builder = + TransactionOptionsBuilderInternal::Create(env); + options_builder.SetMaxAttempts(env, max_attempts); + Local options = options_builder.Build(env); + options_builder.clear(); + + Local task = + env.Call(obj_, kRunTransaction, options, transaction_function); if (!env.ok()) return {}; diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index 73a8b0a37c..5c5367a8f5 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -108,7 +108,8 @@ class FirestoreInternal { WriteBatch batch() const; Future RunTransaction( - std::function update); + std::function update, + int32_t max_attempts); // Disables network and gets anything from cache instead of server. Future DisableNetwork(); diff --git a/firestore/src/android/transaction_options_android.cc b/firestore/src/android/transaction_options_android.cc new file mode 100644 index 0000000000..942874f7d8 --- /dev/null +++ b/firestore/src/android/transaction_options_android.cc @@ -0,0 +1,46 @@ +/* + * 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 + * + * http://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. + */ + +#include "firestore/src/android/transaction_options_android.h" + +#include "firestore/src/jni/declaration.h" +#include "firestore/src/jni/env.h" +#include "firestore/src/jni/loader.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Env; +using jni::Loader; +using jni::Method; + +constexpr char kTransactionOptionsClass[] = + PROGUARD_KEEP_CLASS "com/google/firebase/firestore/TransactionOptions"; +Method kGetMaxAttempts("getMaxAttempts", "()I"); + +} // namespace + +void TransactionOptionsInternal::Initialize(Loader& loader) { + loader.LoadClass(kTransactionOptionsClass, kGetMaxAttempts); +} + +int32_t TransactionOptionsInternal::GetMaxAttempts(Env& env) const { + return env.Call(*this, kGetMaxAttempts); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/android/transaction_options_android.h b/firestore/src/android/transaction_options_android.h new file mode 100644 index 0000000000..21a864f244 --- /dev/null +++ b/firestore/src/android/transaction_options_android.h @@ -0,0 +1,38 @@ +/* + * 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 + * + * http://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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_ANDROID_H_ +#define FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_ANDROID_H_ + +#include "firestore/src/jni/jni_fwd.h" +#include "firestore/src/jni/object.h" + +namespace firebase { +namespace firestore { + +class TransactionOptionsInternal : public jni::Object { + public: + using jni::Object::Object; + + static void Initialize(jni::Loader& loader); + + int32_t GetMaxAttempts(jni::Env&) const; +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_ANDROID_H_ diff --git a/firestore/src/android/transaction_options_builder_android.cc b/firestore/src/android/transaction_options_builder_android.cc new file mode 100644 index 0000000000..51d6223419 --- /dev/null +++ b/firestore/src/android/transaction_options_builder_android.cc @@ -0,0 +1,67 @@ +/* + * 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 + * + * http://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. + */ + +#include "firestore/src/android/transaction_options_builder_android.h" + +#include "firestore/src/jni/declaration.h" +#include "firestore/src/jni/env.h" +#include "firestore/src/jni/loader.h" +#include "firestore/src/jni/ownership.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Constructor; +using jni::Env; +using jni::Loader; +using jni::Local; +using jni::Method; + +constexpr char kTransactionOptionsBuilderClass[] = PROGUARD_KEEP_CLASS + "com/google/firebase/firestore/TransactionOptions$Builder"; +Constructor kNewBuilder("()V"); +Method kSetMaxAttempts( + "setMaxAttempts", + "(I)Lcom/google/firebase/firestore/TransactionOptions$Builder;"); +Method kBuild( + "build", "()Lcom/google/firebase/firestore/TransactionOptions;"); + +} // namespace + +void TransactionOptionsBuilderInternal::Initialize(Loader& loader) { + loader.LoadClass(kTransactionOptionsBuilderClass, kNewBuilder, + kSetMaxAttempts, kBuild); +} + +Local +TransactionOptionsBuilderInternal::Create(Env& env) { + return env.New(kNewBuilder); +} + +Local +TransactionOptionsBuilderInternal::SetMaxAttempts(Env& env, + int32_t max_attempts) const { + return env.Call(*this, kSetMaxAttempts, max_attempts); +} + +Local TransactionOptionsBuilderInternal::Build( + Env& env) const { + return env.Call(*this, kBuild); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/android/transaction_options_builder_android.h b/firestore/src/android/transaction_options_builder_android.h new file mode 100644 index 0000000000..68ce1cfb1a --- /dev/null +++ b/firestore/src/android/transaction_options_builder_android.h @@ -0,0 +1,44 @@ +/* + * 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 + * + * http://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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_BUILDER_ANDROID_H_ +#define FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_BUILDER_ANDROID_H_ + +#include "firestore/src/android/transaction_options_android.h" +#include "firestore/src/jni/jni_fwd.h" +#include "firestore/src/jni/object.h" + +namespace firebase { +namespace firestore { + +class TransactionOptionsBuilderInternal : public jni::Object { + public: + using jni::Object::Object; + + static void Initialize(jni::Loader& loader); + + static jni::Local Create(jni::Env&); + + jni::Local SetMaxAttempts( + jni::Env&, int32_t max_attempts) const; + + jni::Local Build(jni::Env&) const; +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_ANDROID_TRANSACTION_OPTIONS_BUILDER_ANDROID_H_ diff --git a/firestore/src/common/firestore.cc b/firestore/src/common/firestore.cc index 6a2c2e4f1a..a7695a7b1a 100644 --- a/firestore/src/common/firestore.cc +++ b/firestore/src/common/firestore.cc @@ -309,13 +309,20 @@ WriteBatch Firestore::batch() const { Future Firestore::RunTransaction( std::function update) { + return RunTransaction({}, std::move(update)); +} + +Future Firestore::RunTransaction( + TransactionOptions options, + std::function update) { if (!update) { SimpleThrowInvalidArgument( "Transaction update callback cannot be an empty function."); } if (!internal_) return FailedFuture(); - return internal_->RunTransaction(firebase::Move(update)); + return internal_->RunTransaction(firebase::Move(update), + options.max_attempts()); } Future Firestore::DisableNetwork() { diff --git a/firestore/src/common/transaction_options.cc b/firestore/src/common/transaction_options.cc new file mode 100644 index 0000000000..87e6d7309e --- /dev/null +++ b/firestore/src/common/transaction_options.cc @@ -0,0 +1,50 @@ +/* + * 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 + * + * http://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. + */ + +#include "firestore/src/include/firebase/firestore/transaction_options.h" + +#include +#include +#include + +#include "firestore/src/common/exception_common.h" + +namespace firebase { +namespace firestore { + +void TransactionOptions::set_max_attempts(int32_t max_attempts) { + if (max_attempts <= 0) { + SimpleThrowInvalidArgument("invalid max_attempts: " + + std::to_string(max_attempts)); + } + max_attempts_ = max_attempts; +} + +std::string TransactionOptions::ToString() const { + return std::string("TransactionOptions(max_attempts=") + + std::to_string(max_attempts()) + ")"; +} + +std::ostream& operator<<(std::ostream& out, const TransactionOptions& options) { + return out << options.ToString(); +} + +bool operator==(const TransactionOptions& lhs, const TransactionOptions& rhs) { + return lhs.max_attempts() == rhs.max_attempts(); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/include/firebase/firestore.h b/firestore/src/include/firebase/firestore.h index 8ac64fda63..c35de11b4d 100644 --- a/firestore/src/include/firebase/firestore.h +++ b/firestore/src/include/firebase/firestore.h @@ -47,6 +47,7 @@ #include "firebase/firestore/source.h" #include "firebase/firestore/timestamp.h" #include "firebase/firestore/transaction.h" +#include "firebase/firestore/transaction_options.h" #include "firebase/firestore/write_batch.h" namespace firebase { @@ -253,6 +254,23 @@ class Firestore { virtual Future RunTransaction( std::function update); + /** + * Executes the given update and then attempts to commit the changes applied + * within the transaction. If any document read within the transaction has + * changed, the update function will be retried. If it fails to commit after + * the `max_attempts` specified in the given `TransactionOptions`, the + * transaction will fail. + * + * @param options The transaction options for controlling execution. + * @param update function or lambda to execute within the transaction context. + * The string reference parameter can be used to set the error message. + * + * @return A Future that will be resolved when the transaction finishes. + */ + virtual Future RunTransaction( + TransactionOptions options, + std::function update); + /** * Sets the log verbosity of all Firestore instances. * diff --git a/firestore/src/include/firebase/firestore/transaction_options.h b/firestore/src/include/firebase/firestore/transaction_options.h new file mode 100644 index 0000000000..9433578879 --- /dev/null +++ b/firestore/src/include/firebase/firestore/transaction_options.h @@ -0,0 +1,137 @@ +/* + * 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 + * + * http://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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_TRANSACTION_OPTIONS_H_ +#define FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_TRANSACTION_OPTIONS_H_ + +#include +#include +#include + +namespace firebase { +namespace firestore { + +/** + * Options to customize transaction behavior for `Firestore.runTransaction()`. + */ +class TransactionOptions final { + public: + /** + * @brief Creates the default `TransactionOptions`. + */ + TransactionOptions() = default; + + /** + * @brief Copy constructor. + * + * This performs a deep copy, creating an independent instance. + * + * @param[in] other `TransactionOptions` to copy from. + */ + TransactionOptions(const TransactionOptions& other) = default; + + /** + * @brief Move constructor. + * + * Moving is not any more efficient than copying for `TransactionOptions` + * because this class is trivially copyable; however, future additions to this + * class may make it not trivially copyable, at which point moving would be + * more efficient than copying. After being moved from, `TransactionOptions` + * is in a valid but unspecified state. + * + * @param[in] other `TransactionOptions` to move data from. + */ + TransactionOptions(TransactionOptions&& other) = default; + + /** + * @brief Copy assignment operator. + * + * This performs a deep copy, creating an independent instance. + * + * @param[in] other `TransactionOptions` to copy from. + * + * @return Reference to the destination `TransactionOptions`. + */ + TransactionOptions& operator=(const TransactionOptions& other) = default; + + /** + * @brief Move assignment operator. + * + * Moving is not any more efficient than copying for `TransactionOptions` + * because this class is trivially copyable; however, future additions to this + * class may make it not trivially copyable, at which point moving would be + * more efficient than copying. After being moved from, `TransactionOptions` + * is in a valid but unspecified state. + * + * @param[in] other `TransactionOptions` to move data from. + * + * @return Reference to the destination `TransactionOptions`. + */ + TransactionOptions& operator=(TransactionOptions&& other) = default; + + /** + * @brief Gets the maximum number of attempts to commit, after which the + * transaction fails. + * + * The default value is 5. + */ + int32_t max_attempts() const { return max_attempts_; } + + /** + * @brief Sets the maximum number of attempts to commit, after which the + * transaction fails. + * + * The default value is 5. + * + * @param[in] max_attempts The maximum number of attempts; must be greater + * than zero. + */ + void set_max_attempts(int32_t max_attempts); + + /** + * Returns a string representation of this `TransactionOptions` object for + * logging/debugging purposes. + * + * @note the exact string representation is unspecified and subject to + * change; don't rely on the format of the string. + */ + std::string ToString() const; + + /** + * Outputs the string representation of this `TransactionOptions` object to + * the given stream. + * + * @see `ToString()` for comments on the representation format. + */ + friend std::ostream& operator<<(std::ostream&, const TransactionOptions&); + + private: + int32_t max_attempts_ = 5; +}; + +/** Compares two `TransactionOptions` objects for equality. */ +bool operator==(const TransactionOptions&, const TransactionOptions&); + +/** Compares two `TransactionOptions` objects for inequality. */ +inline bool operator!=(const TransactionOptions& lhs, + const TransactionOptions& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_TRANSACTION_OPTIONS_H_ diff --git a/firestore/src/main/firestore_main.cc b/firestore/src/main/firestore_main.cc index c6d30c4d70..9174665b7c 100644 --- a/firestore/src/main/firestore_main.cc +++ b/firestore/src/main/firestore_main.cc @@ -198,7 +198,10 @@ WriteBatch FirestoreInternal::batch() const { } Future FirestoreInternal::RunTransaction( - std::function update) { + std::function update, + int32_t max_attempts) { + SIMPLE_HARD_ASSERT(max_attempts > 0); + auto executor = transaction_executor_; auto promise = promise_factory_.CreatePromise(AsyncApi::kRunTransaction); @@ -245,7 +248,8 @@ Future FirestoreInternal::RunTransaction( }; firestore_core_->RunTransaction(std::move(update_callback), - std::move(final_result_callback)); + std::move(final_result_callback), + max_attempts); return promise.future(); } diff --git a/firestore/src/main/firestore_main.h b/firestore/src/main/firestore_main.h index 46000276ec..68c02e2e49 100644 --- a/firestore/src/main/firestore_main.h +++ b/firestore/src/main/firestore_main.h @@ -85,7 +85,8 @@ class FirestoreInternal { WriteBatch batch() const; Future RunTransaction( - std::function update); + std::function update, + int32_t max_attempts); Future DisableNetwork(); diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 00b4055dcc..1a4d9c3c09 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -649,6 +649,9 @@ code. This fixes an issue with disk persistence on Linux. - Messaging (Android): Fixed #973. Make sure all the resources are closed in `RegistrationIntentService`. + - Firestore: Added `TransactionOptions` to control how many times a + transaction will retry commits before failing + ([#966](https://github.com/firebase/firebase-cpp-sdk/pull/966)). ### 9.1.0 - Changes