[cloud_firestore] iOS: transaction.get() after the native timeout aborts the app ("A transaction object cannot be used after its update callback has been invoked")
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 52/100
調査の方向性
Start with FLTTransactionStreamHandler.m around lines 62–115 and 141, then trace transaction storage and lookup in FLTFirebaseFirestorePlugin.m around lines 667–682 and 791–796. Check how the iOS transaction handler and tests cover timeout, retries, and operations arriving after an attempt ends. Done means late reads fail cleanly, timed-out attempts do not commit stale commands, and a regression test verifies the behavior.
索引モデルが issue の本文から書いたものです。
説明
Is there an existing issue for this?
- I have searched the existing issues.
Which plugins are affected?
Database
Which platforms are affected?
iOS
Description
A Dart transaction.get() can reach the native FIRTransaction after the transaction's update block has already returned. Firestore then fails an internal assertion and the process aborts (SIGABRT). It's an uncatchable native crash, not a Dart exception:
NSInternalInconsistencyException - FIRESTORE INTERNAL ASSERTION FAILED: A transaction object cannot be used after its update callback has been invoked. (expected !committed_)
Expected: a get that arrives after the native attempt has ended fails with a catchable FirebaseException (or the transaction fails cleanly), like the existing missing-transaction error.
Actual: the app is killed.
Where it happens (cloud_firestore 6.10.0, iOS sources)
FLTTransactionStreamHandler.m:62: the native update block posts the attempt to Dart, then waits on a semaphore for at mosttimeout(Dart default 30 s).FLTTransactionStreamHandler.m:65-115: on timeout it sends Dart adeadline-exceedederror event, then carries on. It appliesself.commandsand returnsnilwithout setting*pError, so the SDK goes on to commit.Transaction::Commitsetscommitted_ = truebefore the commit round trip.FLTFirebaseFirestorePlugin.m:791stores the attempt'sFIRTransactionin_transactions[transactionId]when the block starts.FLTFirebaseFirestorePlugin.m:796removes it only inended(), which runs from the completion block (FLTTransactionStreamHandler.m:141) after the commit finishes.FLTFirebaseFirestorePlugin.m:667-682(transactionGetApp) looks the transaction up by id and calls-[FIRTransaction getDocument:error:]with no check that the update block is still running.
So any Dart get that lands between the update block returning and the completion block (the commit round trip) calls Transaction::Lookup → EnsureCommitNotCalled() → assertion → abort. The Dart side has already been told the transaction failed (deadline-exceeded), but the user's handler is an ordinary async closure: it keeps running and issues its get.
How we hit it in production
A TestFlight build, iPhone, iOS 27.0.1. The app was suspended by iOS while a short read-modify-write transaction (one get, one set) was in flight. On resume (the user tapped a notification), the native wait's deadline had long passed, so the block timed out, returned and committed, just as the Dart handler resumed and sent its get. The crash was in the foreground, in the same second as the resume.
Any transaction can hit this whenever the Dart handler runs longer than timeout: app suspension, a slow first get on a poor connection, or a busy UI isolate.
Related
- The same bug in React Native Firebase: invertase/react-native-firebase#9298, fixed by PR #9303. Their bridge has the same structure (a semaphore wait with a timeout, and one transaction state per id). Their fix marks the attempt as returned after the wait, and rejects later operations from that attempt with a catchable error instead of letting them reach the SDK.
- Older FlutterFire reports of this assertion, all closed without a root cause: #1216, #1969, #6927.
Suggested fix
- Record per attempt that the update block has returned (for example a flag set, under the same lock as
_transactions, right after the semaphore wait). IntransactionGetApp, return aFlutterError(for exampleabortedordeadline-exceeded) instead of callinggetDocument:once it's set. - On timeout, fail the attempt (set
*pError, for exampleFIRFirestoreErrorCodeDeadlineExceededorAborted) instead of returningnil. Returningnillets the SDK commit a transaction that Dart has already been told failed. - Also:
resultTypeandcommandsare never reset between attempts. If a retried attempt times out, the block applies the previous attempt's commands (computed from the previous attempt's reads) to the new transaction.
Reproducing the issue
Timing-dependent: the get has to land inside the commit round trip after the native wait times out. This sketch hasn't been run as a standalone app; the production crash above is the evidence.
final db = FirebaseFirestore.instance;
final ref = db.doc('repro/doc');
for (var i = 0; i < 50; i++) {
try {
await db.runTransaction((tx) async {
await tx.get(ref); // a read, so the commit has a verify write
// Outlive the native wait (timeout below), then read again.
await Future<void>.delayed(Duration(milliseconds: 1000 + i * 5));
await tx.get(ref);
}, timeout: const Duration(seconds: 1));
} on FirebaseException catch (e) {
debugPrint('attempt $i: ${e.code}'); // deadline-exceeded is expected
}
}
Run it on a physical iOS device (a network link conditioner with added latency widens the window). Expected: each attempt fails with a catchable error. Actual: sooner or later the app aborts with the assertion above.
The production path: start a transaction, background the app so iOS suspends it before the handler's get, wait well past the timeout, then bring the app back.
Firebase Core version
4.15.0
Flutter Version
3.47.5
Relevant Log Output
Exception Type: EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6
NSInternalInconsistencyException - FIRESTORE INTERNAL ASSERTION FAILED: A transaction object cannot be used after its update callback has been invoked. (expected !committed_)
Crashed thread:
firebase::firestore::util::ObjcThrowHandler(...)
firebase::firestore::util::Throw(...)
firebase::firestore::util::internal::FailAssertion(...)
firebase::firestore::util::internal::FailAssertion(...)
firebase::firestore::core::Transaction::EnsureCommitNotCalled() + 104
firebase::firestore::core::Transaction::Lookup(...) + 56
-[FIRTransaction getDocument:completion:] + 264
-[FIRTransaction getDocument:error:] + 200
__78-[FLTFirebaseFirestorePlugin transactionGetApp:transactionId:path:completion:]_block_invoke + 172 (FLTFirebaseFirestorePlugin.m:682)
_dispatch_call_block_and_release
_dispatch_client_callout
_dispatch_queue_override_invoke
_dispatch_root_queue_drain
_dispatch_worker_thread2
_pthread_wqthread
No other thread was inside a transaction update block at the time of the crash: the block had already returned.
Flutter dependencies
Expand Flutter dependencies snippet
Replace this line with the contents of your `flutter pub deps -- --style=compact`.
Additional context and comments
Our workaround: don't start a transaction unless the app is in the foreground (AppLifecycleState.resumed), and run anything held back on resume. That avoids the suspension path, but not a slow handler in the foreground.
The macOS implementation appears to share these Darwin sources, so it's probably affected too (not tested).
- 主要言語
- Dart
- スター
- 9.3k
- フォーク
- 4.1k
- 平均マージ
- 1日 12時間
- マージ済み PR(30日)
- 79
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
firebase/flutterfire のほかの issue
-
[firebase_messaging]: Android ANR during background service destruction — main thread blocked in releaseWakeLock対応中かも @SelaseKay が 2 日前に担当しました。 オープンNeeds Attention platform: android plugin: messaging type: bug
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
firebase/flutterfire#18866 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
blocked: customer-response Needs Attention platform: ios plugin: messaging Stale type: bug
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
firebase/flutterfire#18776 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
-
[firebase_storage] Windows native crash in upload progress callback, sink ownership and thread dispatch再び着手できるかも このイシューのプルリクエストはマージされずにクローズされました。 オープンblocked: flutter platform: windows plugin: storage reproduced type: bug
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
firebase/flutterfire#18664 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
[core] Decouple platform_interface from Flutter for pure-Dart web再び着手できるかも @Lyokone が 38 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンNeeds Attention platform: all platform: web plugin: core type: enhancement
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
firebase/flutterfire#18645 · コメント 3 件 · リアクション 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
blocked: firebase-sdk platform: android plugin: messaging reproduced type: bug
難易度 3/5 1〜2日 初心者へのやさしさ 56/100
firebase/flutterfire#18644 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
firebase/flutterfire の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
fossasia/badgemagic-app#2016 ·
メンテナーはふだん 1 日以内に返信
-
bug P3 ux
難易度 2/5 1〜3時間 初心者へのやさしさ 79/100
QueryaHub/Querya-Desktop#1837 ·
メンテナーはふだん 1 日以内に返信
-
direct-code
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
aaronified/fluenough#849 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
PATILYASHH/XPENC#179 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信