plan/docs: the M2 gate is not reachable as written either
Priced the 13 aggregate failures one by one instead of counting them. Six
cannot turn green in M2 whatever is built: three need $listLocalSessions (M4)
*and* $addFields (M2.5), two need the expression engine, one needs a collation
(M8). So the gate reads '0 fail among the seven reachable', with the other six
named and attributed.
Note what the three db.aggregate() cases actually need. Implementing
{aggregate: 1} moves none of them, because each then fails on
$listLocalSessions instead. The review priced that line 'orthogonal, and
cheap'; it was cheap and worth nothing.
The seven that remain are $out and $merge, and they need a decision the review
had no authority to take. They write to a collection the pipeline is not
reading, and three things stand against that: aggregate is declared .read and
the dispatch contract says only .write commands may mutate; dispatch acquires
locks from a static table keyed on the command name, before the handler runs;
and Collection.lock's own comment states the invariant that decides it -- never
more than one collection lock at a time.
That is the same objection that kept $lookup out of M2.5's first cut, and the
review failed to apply it to the write stages in the same breath. Recorded as
the review's own error rather than quietly corrected. Two options set out in
§7, plus the durability question neither of them answers: mongod's $out
replaces the target atomically and this engine has no cross-collection
atomicity.
Nothing past Tier 0 is implemented until one is chosen.
This commit is contained in:
@@ -273,3 +273,71 @@ memory bound on `$group`/`$sort` joins Tier 2 rather than trailing it.
|
||||
- Whether the expression engine is shared with `update`'s pipeline-update form,
|
||||
which M3 will need. Probably yes; it changes where the code lives.
|
||||
- Index use inside a pipeline beyond the existing leading-`$match` pushdown.
|
||||
|
||||
---
|
||||
|
||||
## 7. Post-decision measurement: the gate is not reachable either
|
||||
|
||||
Written after Tier 0 landed, when the 13 failures were priced one by one
|
||||
instead of counted. Section 1 established that the *corpus* named in the plan
|
||||
was the wrong instrument; this establishes that the *target* set on the
|
||||
corpus we kept is unreachable too. Both were found by looking rather than by
|
||||
assuming, and this one only by opening each case.
|
||||
|
||||
| cases | what they need | milestone |
|
||||
|---|---|---|
|
||||
| 5 `aggregate-merge.json` | `$merge` with a plain `into` | M2, blocked below |
|
||||
| 2 `aggregate-out.json` | `$out` with a plain target | M2, blocked below |
|
||||
| 3 `db-aggregate*.json` | `$listLocalSessions` **and** `$addFields` | M4 + M2.5 |
|
||||
| 2 `aggregate-let.json` | `let`, `$$var`, `$expr` | M2.5 |
|
||||
| 1 `aggregate-collation.json` | a case-insensitive collation | M8 |
|
||||
|
||||
So **6 of 13 can never turn green in M2**, whatever is built, and the gate has
|
||||
to read *0 fail among the seven this milestone can reach*, with the other six
|
||||
named and attributed. Note what the three `db.aggregate()` cases really need:
|
||||
implementing `{aggregate: 1}` moves none of them, because each then fails on
|
||||
`$listLocalSessions` instead. The design review priced that line "orthogonal,
|
||||
and cheap"; it was cheap and it was worth nothing.
|
||||
|
||||
### And the seven need a decision this review has no authority to take
|
||||
|
||||
`$out` and `$merge` write to a collection the pipeline is not reading. Three
|
||||
things in the current design stand against that, and none of them is a detail:
|
||||
|
||||
- `aggregate` is declared `.kind = .read`, and the dispatch contract is
|
||||
explicit that "only `.write` commands may call engine mutation functions".
|
||||
- dispatch acquires locks from a **static table keyed on the command name**,
|
||||
before the handler runs, for the one collection named by the command.
|
||||
- `Collection.lock`'s own comment states the invariant that decides this:
|
||||
*never more than one collection lock at a time*. Taking the target's
|
||||
exclusive lock while holding the source's shared one breaks it directly.
|
||||
|
||||
This is the same shape as the objection that kept `$lookup` out of M2.5's first
|
||||
cut — "a stage that takes a second collection's lock while the pipeline holds
|
||||
one is a lock-order question that should not ride along" — and the review
|
||||
should have applied it to the write stages in the same breath. It did not,
|
||||
which is the review's own error and is recorded here rather than quietly
|
||||
corrected.
|
||||
|
||||
Two ways out, both real:
|
||||
|
||||
**(a) A pipeline that ends in a write stage is a write command.** Compute the
|
||||
effective lock shape from the pipeline rather than the name: catalog exclusive,
|
||||
no collection lock from dispatch, and the handler takes source-shared →
|
||||
release → target-exclusive in that order. Heavier than mongod, and it
|
||||
serializes such an aggregate against the whole catalog, but it keeps every
|
||||
existing invariant intact and is honest about what the command is.
|
||||
|
||||
**(b) The write stages run in the dispatch epilogue,** after every lock is
|
||||
released, next to the commit and the checkpoint that already live there. The
|
||||
handler stashes what to write. Cheaper and better-behaved under concurrency,
|
||||
but it amends the `.read`/`.write` contract, so the contract's comment has to
|
||||
change with it and say why.
|
||||
|
||||
Either way the durability question is open and belongs with whichever is
|
||||
chosen: mongod's `$out` replaces the target collection *atomically*, and this
|
||||
engine has no cross-collection atomicity. A pipeline that fails after writing
|
||||
half its output must not leave the target half-replaced.
|
||||
|
||||
**Nothing is implemented past Tier 0 until that is decided.**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user