RTDB: updateChildren() creating a new child raises no local events at locations observed only by filtered queries — silent, and dependent on invisible cache state
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 48/100
Hướng nghiên cứu
Bắt đầu trong firebase-database/core/view/ViewProcessor.java, lần theo applyUserOverwrite qua WriteTreeCompleteChildSource.getCompleteChild và đường dẫn bộ nhớ đệm SyncTree/SyncPoint/View được mô tả trong issue. So sánh hành vi tương ứng của firebase-js-sdk ViewProcessor.ts và chạy hoặc bổ sung coverage cho reproduction orderByKey updateChildren đã lọc, control setValue của nó và control loadsAllData. Hoàn tất khi hành vi child_added cục bộ dự kiến đã được coverage và hành vi được chọn—lập chỉ mục theo key, thận trọng hoặc lỗi tường minh—nhất quán.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
When a user write creates a child implicitly — updateChildren() at the
child path, or setValue() below it — under a location observed only by
filtered queries (e.g. orderByKey().startAt(...)), no local child_added
is ever raised. The write is persisted, syncs to the server normally, and
listeners eventually receive it as server echo — so while online the loss
is invisible (~50 ms latency instead of ~1 ms), and offline the app appears
frozen despite writing correctly.
setValue() at the child raises normally in the identical state. And the
behavior flips if an unfiltered (loadsAllData) listener has ever completed a
sync at the location this session — or is seeded complete from persisted
tracked-query records of a previous session — making the observable local
semantics of an identical write depend on invisible cache lineage.
The same logic exists verbatim in firebase-js-sdk, and we reproduced the
behavior empirically on the iOS SDK (macOS) as well — it appears inherited
from the common ancestral implementation.
Root cause (line-anchored, main branch)
ViewProcessor.applyUserOverwrite (firebase-database
core/view/ViewProcessor.java):
- For
changePathdeeper than one level (childChangePathnon-empty — the
implicit-creation case), the new child must be synthesized:
Node childNode = source.getCompleteChild(childKey)(~line 377). WriteTreeCompleteChildSource.getCompleteChild(~704–721) can only
succeed via (a) the event cache containing the child — never true for a
new key on a filtered view, sinceCacheNode.isCompleteForChildis
(fullyInitialized && !filtered) || hasChild(key); (b) the
optCompleteServerCacheargument; or (c)
WriteTree.calcCompleteChild(~336–349) passing the same
isCompleteForChildtest against the filtered server cache — also false
for a new key.optCompleteServerCachecomes from
SyncTree.applyOperationHelper(~1044) →
SyncPoint.getCompleteServerCache(emptyPath)→
View.getCompleteServerCache(~99–110), which with an empty path returns
non-null only for aquery.loadsAllData()view with a fully
initialized server cache. A location with only bounded listeners can never
supply it.- With
getCompleteChild == null:newChild = EmptyNode.Empty()(~389,
comment "There is no complete child node available"), then
!oldChild.equals(newChild)(~392) compares Empty to Empty for the
brand-new key → treated as no change →newViewCache = oldViewCache
(~405). No event, no warning, nothing.
setValue() at the child takes the childChangePath.isEmpty() branch
(~373–375, newChild = changedSnap) and never consults the gate — which is
why it works.
firebase-js-sdk ViewProcessor.ts (viewProcessorApplyUserOverwrite)
contains the identical structure: getCompleteChild null →
ChildrenNode.EMPTY_NODE → equals skip.
Why this is a bug and not acceptable conservatism
- Silent contract violation. The documented behavior — local writes fire
events on local listeners immediately, before server acknowledgment — is
violated with no error, no log, no documented carve-out. - Semantics depend on invisible cache lineage. The same write at the
same location raises or doesn't depending on whether some other listener
(loadsAllData) ever completed a sync there this session, or left a
persisted tracked-query completeness record in a previous session. No
application developer can reason about this from the API surface. No
specification would choose "same write, same listener, different result
depending on how the cache got here." - The conservatism is provably unnecessary for key-indexed queries. For
orderByKeyranges, window membership is decidable from the key alone —
no old value is needed to know achild_addedis due. Applying the merge
overEmptyNodefor a key the filter accepts (or at minimum for
key-indexed filters) would be correct where the current code is silent.
(For value-indexed windows the old value genuinely matters, and a
conservative path is defensible — but it should not be silent.)
Reproduction (no network manipulation needed)
Persistence enabled. Fresh app data (important: no persisted loadsAllData
completeness records for the path).
- Attach
ref(L).orderByKey().startAt("x")with aChildEventListener;
never attach an unfiltered listener atLin the app's lifetime. - Let the initial sync complete (online).
goOffline()(offline only makes the loss observable; the local event
is equally absent online, masked by server echo).ref(L).child("x_new").updateChildren({"v": 1}).- Expected: local
child_addedforx_new. Actual: nothing until
reconnect (server echo). - Controls, same state:
ref(L).child("x_new2").setValue(1)→ fires;
attaching an unfiltered listener atL, letting it sync, then repeating
step 4 → fires (the cache-lineage flip).
Evidence from our app (verbose native logging)
Deaf (bounded-only location):
RepoOperation: update: /teams/.../events/2026-08/1788115390730_A3..._0
DataOperation: update: ... {us=222}
Persistence: Persisted user merge in 1ms
Persistence: Transaction completed. Elapsed: 3ms
(no "Updated tracked query keys", no EventRaiser output)
Identical state, setValue at the child:
Persistence: Persisted user merge in 0ms
Persistence: Updated tracked query keys (1 added, 0 removed) for tracked query id 4256
EventRaiser: Raising 1 event(s)
EventRaiser: Raising /teams/.../events/2026-08: CHILD_ADDED: { 1788121988664_FF..._0: 1 }
Environment / impact
- flutterfire firebase_database 12.4.1 and 12.0.4 (pre/post Pigeon —
identical; native SDK behavior), Android 14/15; reproduced on macOS via the
iOS SDK. - Impact: our offline-first sports-timing console wrote every live event with
updateChildrenunder boundedorderByKeylisteners; during venue Wi-Fi
outages (events still arriving over BLE) the operator display froze
entirely while data was written and synced correctly — discovered only
after a two-month-old release because server echo masks the loss whenever
online. - Workaround we shipped:
setValue()at the child. Also effective:
maintaining anyloadsAllDatalistener at the location (with its cost).
Ask
Either raise the event when the filter can decide membership without the old
value (key-indexed case at minimum), or fail loudly / document prominently
that updateChildren under exclusively-filtered listeners does not raise
local events.
- Ngôn ngữ chính
- Java
- Star
- 2.6k
- Fork
- 733
- Merge trung bình
- 2 ngày 15 giờ
- Pull request đã merge (30 ngày)
- 72
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của firebase/firebase-android-sdk
-
api: remoteconfig
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
firebase/firebase-android-sdk#8827 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Crashlytics plugin writes build ID lists in directory listing order, so APKs can differ between machinesCó thể đã có người làm @mrober đã nhận 8 ngày trước. Đang mởapi: crashlytics
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
firebase/firebase-android-sdk#8789 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
api: auth
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
firebase/firebase-android-sdk#8505 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 15/100
firebase/firebase-android-sdk#8835 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
firebase/firebase-android-sdk#8832 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của firebase/firebase-android-sdk
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
NationalSecurityAgency/ghidra#10220 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 80/100
Maintainer thường phản hồi trong vòng 1 ngày
-
RIS export splits an organization name containing “and” into multiple authorsCó thể đã có người làm @Gautam-aman đã nhận hôm nay. Đang mởstatus: needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
JabRef/jabref#17821 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
FailureContext.java rounds down thresholds in lastedAtLeast()Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày