`loadRelated` can return stale children while blocks are queued for writing (`!= any` in `FindDerivedQuery`; unrelated queued writes not excluded)
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 50/100
Direção de pesquisa
Start with FindDerivedQuery in store/postgres/src/relational_queries.rs and Queue::get_derived/effective_ops in store/postgres/src/writable.rs. Run the deterministic cases in store/test-store/tests/postgres/writable.rs using pause_writer and allow_steps. Done means queued and post-flush results match for the two-removal, removal-plus-creation, parent-change, and control cases.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
Summary
With the pipelined writer (the default, GRAPH_STORE_WRITE_QUEUE > 0), store.loadRelated (EntityCache::load_related → WritableStore::get_derived → Queue::get_derived) can return children that a queued block has already removed, or has already moved to another parent. The answer depends on how far the background writer has progressed. Two indexers, or two runs of the same indexer, can therefore see different loadRelated results for the same block. When a mapping derives what it writes from that result, which is the usual reason to call loadRelated, entities and the PoI can diverge.
Queue::get_derived merges the queued changes with a database read taken at the block before the queue. The keys found in the queue are passed as excluded_keys, so the database does not return an older version of them. There are two independent problems with this exclusion.
A. != any($excluded) excludes nothing once two or more keys are listed
store/postgres/src/relational_queries.rs, FindDerivedQuery (master 6838f4e3c, line 2244):
out.push_identifier(&self.table.primary_key().name)?;
out.push_sql(" != any(");
self.excluded_keys.push_bind_param(&mut out)?;
out.push_sql(") and ");
id != any(array) is true when id differs from at least one element of the array. With one key it behaves like id <> key. With two or more keys, every row satisfies it, so nothing is excluded, and a child that a queued block removes comes back from the database. The intended predicate is id <> all(array). This was introduced by 4d2479c04 ("store: Avoid 'too many bind params' error in FindDerivedEntityQuery", April 2024), which replaced not in (...) with != any(...). The first release that contains it is v0.35.1.
B. A queued write that no longer points to the parent does not exclude its key
store/postgres/src/writable.rs, Queue::get_derived → effective_ops (master 6838f4e3c, lines 1340-1355):
EntityOp::Write { key, entity } if is_related(derived_query, entity) => Some((key.clone(), Some(entity.clone()))),
EntityOp::Write { .. } => None,
EntityOp::Remove { key } => Some((key.clone(), None)),
If a child changes its @derivedFrom field (moves from p0 to p1) in a queued block, the queued write is not related to p0. It yields None, so its key is not excluded, and the database still returns the old version under p0. A single queued key is enough. This form of the match dates from 12427fd5c (v0.31.0).
Reproduction
Four deterministic store tests, added in the accompanying PR to store/test-store/tests/postgres/writable.rs (using the existing pause_writer helper):
- Write block 1 to the database: parents
p0andp1; childrenwandxunderp0. - Pause the writer (
pause_writer, which usesallow_steps). - Queue an empty block 2. It absorbs a writer that may already be past its pause point; the test asserts that the database head is still ≤ 2.
- Queue block 3, call
get_derivedfor the children ofp0while block 3 is queued, then flush and call it again.
| case | queued block 3 | expected | returned while queued, before the fix |
|---|---|---|---|
| one removal (control) | remove x |
[w] |
[w] |
| two removals (A) | remove w, remove x |
[] |
[w, x] |
| removal + creation (A) | remove x; insert y under p0 |
[w, y] |
[w, x, y] |
| parent change (B) | x.parent: p0 → p1 |
[w] |
[w, x] |
After flush every case returns the expected value. The failures do not depend on timing: the queued block is held, and the tests fail deterministically.
We also built an end-to-end runner fixture. Children are moved and removed in some blocks, and loadRelated results are written to an immutable entity in later blocks. With the writer holding a batch, the recorded results and the PoI differ from a run with a synchronous writer. With the natural batching behaviour, the result also varied between runs.
Possibly related
#5866 ("Load Related requests not deterministic") reported a PoI discrepancy where the number of entities returned from the database and the queue varied between runs, with the root cause unidentified; it was closed for inactivity. Both defects above produce exactly that symptom, so they may be its root cause. The pattern that triggers case A (loading children with loadRelated, then removing several of them in the same handler) is common in production mappings.
A PR with both fixes and the tests above follows.
- Linguagem predominante
- Rust
- Estrelas
- 3.2k
- Forks
- 1.1k
- Métricas de merge de PRs
- Nenhum PR com merge em 30d
Preparar o ambiente
Inicia o contêiner de desenvolvimento do projeto no navegador, com a sua própria conta do GitHub.
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de graphprotocol/graph-node
-
current: include emits an all-null bucket for dimensionless aggregations, nulling the whole responseAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
graphprotocol/graph-node#6719 ·
-
RUSTSEC-2026-0194: Quadratic run time when checking a start tag for duplicate attribute namesTalvez já em andamento @szupzj18 assumiu há 57 dias. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
graphprotocol/graph-node#6673 ·
-
RUSTSEC-2026-0185: Remote memory exhaustion in quinn-proto from unbounded out-of-order stream reassemblyTalvez já em andamento @abisheik687 assumiu há 99 dias. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
graphprotocol/graph-node#6650 · 1 comentário ·
-
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 48/100
graphprotocol/graph-node#6722 ·
-
Dificuldade 3/5 1-2 dias Facilidade para iniciantes 68/100
graphprotocol/graph-node#6721 ·
Todas as issues de graphprotocol/graph-node
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
pact-foundation/pact-cli#154 ·
Mantenedores costumam responder em até 3 dias
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
antithesishq/bombadil#361 ·
Mantenedores costumam responder em até 1 dia
-
test(executor_l0): assert execute() TaskOutcome, not only bus events / 断言 execute() 返回的 TaskOutcomeAbertatype:debt
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 62/100
skaiy/wild_agentos#425 ·
Mantenedores costumam responder em até 1 dia
-
Default-import note suggests `import * as process` for velt:process, which does not name the builtinAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
Mantenedores costumam responder em até 1 dia
-
bug ticket
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
cratestack/cratestack#1154 ·
Mantenedores costumam responder em até 1 dia