|
void Repo::StartTransaction(const Path& path, |
|
DoTransactionWithContext transaction_function, |
|
void* context, void (*delete_context)(void*), |
|
bool trigger_local_events, |
|
ReferenceCountedFutureImpl* api, |
|
SafeFutureHandle<DataSnapshot> handle) { |
|
// Make sure we're listening on this node. |
|
// Note: we can't do this asynchronously. To preserve event ordering, it has |
|
// to be done in this block. This is ok, this block is guaranteed to be our |
|
// own event loop |
|
DatabaseReferenceInternal* ref_impl = |
|
new DatabaseReferenceInternal(database_, path); |
|
DatabaseReference watch_ref(ref_impl); |
|
std::unique_ptr<NoopListener> listener = std::make_unique<NoopListener>(); |
|
NoopListener* listener_ptr = listener.get(); |
|
QuerySpec query_spec(path); |
|
AddEventCallback(std::make_unique<ValueEventRegistration>( |
|
database_, listener_ptr, query_spec)); |
|
|
|
TransactionDataPtr transaction_data = std::make_shared<TransactionData>( |
|
handle, api, query_spec.path, transaction_function, context, |
|
delete_context, trigger_local_events, std::move(listener)); |
|
|
|
// Run transaction initially. |
|
Variant current_state = GetLatestState(path); |
|
transaction_data->current_input_snapshot = current_state; |
|
MutableDataInternal* mutable_data_impl = |
|
new MutableDataInternal(database_, current_state); |
|
MutableData mutable_current(mutable_data_impl); |
|
|
|
TransactionResult result = transaction_function(&mutable_current, context); |
|
if (result != kTransactionResultSuccess) { |
|
// Abort the transaction. |
|
transaction_data->current_output_snapshot_raw = Variant::Null(); |
|
transaction_data->current_output_snapshot_resolved = Variant::Null(); |
|
transaction_data->status = TransactionData::kStatusNeedsAbort; |
|
transaction_data->ref_future->Complete(transaction_data->future_handle, |
|
kErrorWriteCanceled); |
|
// If there was an error, the listener must be removed to prevent calls to |
|
// it in case the listener is destroyed. |
|
RemoveEventCallback(listener_ptr, query_spec); |
|
} else { |
|
// Mark as run and add to our queue. |
|
transaction_data->status = TransactionData::kStatusRun; |
|
|
|
auto* queue_node = transaction_queue_tree_.GetOrMakeSubtree(path); |
|
if (!queue_node->value().has_value()) { |
|
queue_node->set_value(std::vector<TransactionDataPtr>()); |
|
} |
|
queue_node->value()->push_back(transaction_data); |
|
|
|
Variant server_values = GenerateServerValues(server_time_offset_); |
|
const Variant* new_node_unresolved = mutable_data_impl->GetNode(); |
|
Variant new_node_resolved = |
|
ResolveDeferredValueSnapshot(*new_node_unresolved, server_values); |
|
|
|
transaction_data->current_output_snapshot_raw = *new_node_unresolved; |
|
transaction_data->current_output_snapshot_resolved = new_node_resolved; |
|
transaction_data->current_write_id = GetNextWriteId(); |
|
|
|
std::vector<Event> events = server_sync_tree_->ApplyUserOverwrite( |
|
path, *new_node_unresolved, new_node_resolved, |
|
transaction_data->current_write_id, |
|
trigger_local_events ? kOverwriteVisible : kOverwriteInvisible, |
|
kDoNotPersist); |
|
|
|
PostEvents(events); |
|
|
|
SendAllReadyTransactions(); |
|
} |
|
} |
Environment
Summary
On desktop, if the
DoTransactioncallback aborts on the first invocation, the future completes withkErrorWriteCanceled(11) — seeRepo::StartTransaction:firebase-cpp-sdk/database/src/desktop/core/repo.cc
Lines 659 to 729 in 3d7ce2a
But if the abort happens on a rerun invocation (after a
datastaleresponse forces the transaction to run again), the abort reason is taken from a localerrorvariable that was initialized tokErrorNone:RerunTransactionQueue()initializesError error = kErrorNoneat line 1067. If the rerun callback returns abort, line 1095 assigns that unchanged zero value to the function-localabort_reason.abort_reasonand the current input intoFutureToComplete. No later assignment changes the reason: line 1129 references the queued value, and lines 1131–1134 complete the Future withCompleteWithResult(..., abort_reason, snapshot).firebase-cpp-sdk/database/src/desktop/core/repo.cc
Lines 1062 to 1138 in 3d7ce2a
The result: the Future completes with
error() == kErrorNoneand a snapshot — indistinguishable from a successful commit — for a transaction that was aborted and never committed.Expected
A rerun-invocation abort should complete with the same abort code as a first-invocation abort (or, better, with
kErrorTransactionAbortedByUserto match the mobile SDKs), never withkErrorNone.Impact
Consumers that branch on the future's error to decide "did my transaction commit?" will believe an aborted transaction succeeded whenever the abort happened after a
datastalererun — e.g. optimistic/fenced updates that abort once fresh data shows another writer won. In the FlutterFire Windows plugin this surfaces asTransactionResult(committed: true)for an aborted transaction.Related
Filed alongside a second desktop transaction-error report:
Repo::HandleTransactionResponsecollapses every non-datastaleserver error (includingpermission_denied) tokErrorUnknownErrorwith an empty message. Both were found while debugging the same Windows application.