learn-web3-with-phoebe / Practitioner session 6 of 10
Learn Web3 with Phoebe · Practitioner session 6 of 10

Reorgs hit your warehouse

Larkfield's loader appends every block as it arrives. Bench B runs 200 blocks with a 3 block replacement every 50, and the loader ends holding 511 rows, 5.75 percent of the warehouse, that describe history the chain no longer has. Nothing errored, nothing was flagged, and every check the team owns stayed green, because the loader is correct code doing exactly what append-only means. Tonight measures the damage and finds the one input the whole problem turns on, which is not in your data and is usually not written down anywhere.

🔧 Practitioner track Data engineers who own the loader Anyone who has signed off a row count 45 min
0-3 · Welcome 3-18 · Why nothing errored 18-30 · The size, and the missing input 30-45 · Build-along and Q&A
Part 0

A correct loader, a green dashboard, and 511 wrong rows

The five sessions so far have been about a number being wrong for reasons you can see once somebody points at them. Tonight is different in kind: the rows are wrong and there is no observation available inside the warehouse that would tell you so. The loader did not fail, the pipeline did not degrade, and the failure is not in anybody's code. It is in the assumption that an upstream only ever adds.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. The damage measured on Bench B: 511 orphaned rows at 5.75 percent of the warehouse, stable at 5.65 to 5.75 percent across four runs. A mechanical account of why four standard checks all stay green, including the one that moves in the reassuring direction. The full lag table, 0 through 12, with the dangerous middle named: a lag of 1 leaves 3.99 percent wrong and feels like a precaution. And the parameter the entire table is conditional on, which is not a column in your data and which most teams have never written down.

LiveWithdrawn is not late, and your toolkit only knows late4 min▶

Every warehouse has machinery for records that turn up after the period they belong to: idempotent loads, merge keys, restatement windows. That craft is general, it is well developed, and it belongs to learn-data-engineering-with-phoebe rather than here. What matters tonight is that it is machinery for a different event.

Those patterns handle data that arrives late. This is data that is withdrawn. A late row is additive: something that was missing appears, a total goes up, and nothing that was already correct stops being correct. A withdrawn row is subtractive: something that was counted is gone, a total goes down, and a figure that was right last week can no longer be reproduced. Reaching for a merge key here is not a partial fix, it is a fix for the other case.

Real world

Larkfield's engineer asked the reasonable question first: is this not just late-arriving data with extra steps? The team spent two days trying to express it as a restatement window before somebody noticed that every pattern they reached for produced a row, and the thing they needed was to remove one.

Self-studyThe words used tonight4 min▶
  • Replacement. The upstream withdrawing a run of recent blocks and continuing from an earlier point. Why it happens belongs to learn-blockchain-with-phoebe; what it does to a table is tonight.
  • Depth. How many blocks back a replacement reaches. Bench B fixes it at 3. It is the parameter the whole of tonight turns on.
  • Orphaned row. A row already loaded whose block has since been replaced. Still in the table, still queried, wrong, and counted by nobody.
  • Finality lag. How far behind the newest block a loader stays before writing a block's rows. Bench B sweeps it from 0 to 12.
  • Held rows. Rows that exist and are correct but have not been written yet because the lag has not elapsed. The freshness side of the ledger.
  • Append only. A loader that writes and never removes. Correct behaviour, and the thing that turns a replacement into a permanent error.
  • Late arriving. A record for an earlier period turning up now. Additive, familiar, and handled by patterns that already exist.
  • Withdrawn. A record that was loaded and is now gone from the source. Subtractive, and the case tonight is about.
Part 1 · covers the silence

Four checks, all green, none of them wrong to be

15 min live

Follow five blocks through a loader that appends and nothing else. Three of them are replaced, the replacements are appended too, and the warehouse now holds eight blocks of rows where the chain has five. Then walk the four places a normal pipeline would notice something and watch each of them be satisfied, correctly, by a table that is 3 blocks of rows wrong.

Same loader, two moments. Between them the chain took three blocks back. WHAT THE LOADER DID BLOCKS ARRIVING, NEWEST ON THE RIGHT 46 47 48 49 50 WAREHOUSE Five blocks in, five blocks of rows written, every one of them describing history the chain currently has. WHAT THE CHAIN DID NEXT THE LAST THREE BLOCKS ARE REPLACED 46 47 48 new 49 new 50 new WAREHOUSE Three blocks of rows describing history the chain no longer has, sitting beside the rows that replaced them. FOUR PLACES A PIPELINE WOULD NORMALLY NOTICE, AND WHY EACH ONE IS SATISFIED ROW COUNT 8 blocks of rows sit where 5 did. Volume is up, and alerts watch drops. SCHEMA Every new row is well formed. Nothing is null and nothing is malformed. UNIQUENESS A replaced block writes new rows, not duplicate keys. No constraint trips. FRESHNESS The newest block is newer than before. The service level is green and greener. The loader is correct. It did exactly what append only means, on every block it was given. There is no bug to find and no test that would have caught this, because every check above asks whether the rows that arrived are good. All of them are. The question nobody asked is whether the rows already written are still true.
🔍 Click to zoom - the row count moving up is the cruellest of the four. The one signal that changed moved in the direction everybody reads as healthy.
LiveWhy no check fires, one at a time4 min▶

Data quality checks are almost all statements about rows that arrive. Is this row well formed, is the key unique, is the volume in range, is the newest record recent enough. Every one of those is answered by inspecting the incoming batch, and every one of them is answered correctly here, because the incoming batch is fine. The replacement blocks are real blocks and their rows are real rows.

The rows that are wrong are the ones already in the table, and nothing in a normal pipeline goes back to ask whether a row it wrote last week is still supported by the source. That question has no natural home: it is not a schema property, it is not a batch property, and it cannot be answered without re-reading history you have already processed.

The freshness check deserves a moment on its own. After a replacement the newest block is newer than it was, the loader is up to date, and the service level indicator improves. The single observable that moved moved in the reassuring direction, so a team watching a dashboard sees the situation get better at the moment it got worse.

The loader, and the branch that does not exist in it // A plain append-only load. There is nothing wrong with this code. function onBlock(block) { warehouse.insert(block.height, block.rows); // append, never update } // What a replacement looks like from in here: nothing. The loader is handed // the new blocks and appends them, exactly as it appends every other block. // // There is no error path because there is no error. Rows arrived and were // written. The rows written earlier were never re-examined, because no // append-only loader has a reason to re-examine anything. // // Note what is missing and cannot be added without a second read of history: // if (blockWasReplaced(height)) { ... } // nothing supplies this predicate
LiveWhere the wrong rows sit, which is not evenly spread3 min▶

A replacement takes back the most recent blocks, so every orphaned row came from the tip of the chain at the moment the replacement landed. The damage is not scattered uniformly across 200 blocks of history. It is concentrated in short runs immediately before each event, four times over in this run.

That concentration matters for who feels it. Long range aggregates dilute it across a lot of correct history. Anything recency weighted does not: a last-24-hours panel, a daily delta, an alerting rule on the newest window, a cohort defined by when somebody first appeared. Those read disproportionately from exactly the blocks a replacement is most likely to take back.

So the same 5.75 percent produces very different consequences depending on which report you look at, and a team that measures the error only as a share of the whole table will understate what it does to the numbers people actually watch.

Part 2 · covers the size and the missing input

511 rows, 5.75 percent, and a table with a hidden parameter

12 min live

Run the undefended loader and read the figure. Then run it again on other draws and watch it stay put, which is what makes it a property of the arrangement rather than an accident of one run. Then look at the lag table, which is the standard first response, and notice two things about it: the middle of it is worse than it looks, and the whole of it is conditional on a number that does not appear anywhere in your warehouse.

Live Bench B running in this page, with no lag and no defence of any kind. 200 blocks, a replacement every 50.

...

...
Live5.75 percent, and it is not a draw4 min▶

At the bench setting the undefended loader ends with 511 orphaned rows, 5.75 percent of the warehouse. Change the run and the share lands between 5.65 and 5.75 percent across four of them. That narrow band is the useful part: this is not a bad day, it is what this arrangement produces, and a team that saw the figure once and hoped it was unlucky will see it again next month.

Now move the depth selector and watch the figure move with it. That is the sentence Part 2 exists for. The share of your warehouse that is wrong is a function of how far back the upstream reaches, and the bench is showing you a family of answers rather than one.

One row of the table is worth pointing at while it is on screen: errors raised, zero. Not zero because the bench is being kind. Zero because there is no code path in an append-only loader that could raise one, which Part 1 walked through line by line.

Wrong rows against finality lag, with the middle of it named THE DANGEROUS MIDDLE 700 350 0 511 5.75% 346 3.99% 172 2.03% 0 0 0 0 0 1 2 3 4 6 12 finality lag, in blocks ROWS HELD BACK, NOT REPORTED YET 0 44 84 125 168 249 497 The bars reach their floor at lag 3. The row underneath them never stops climbing. Lags 3, 4, 6 and 12 all leave 0 wrong rows, and the only thing they do differently is hold 125, 168, 249 and 497 rows back.
🔍 Click to zoom - lags 1 and 2 are the only configurations on this chart that are both wrong and expensive, which is why the shaded region has a name.
LiveThe dangerous middle, and why it is worse than nothing4 min▶

Read the first three bars in order. 511, then 346, then 172. A lag of 1 removes about a third of the damage and a lag of 2 removes about two thirds, so both of them are doing something, and that is exactly the problem. The warehouse at lag 1 still holds 346 wrong rows, 3.99 percent of it, and the team now has a documented precaution in place.

A configuration that is wrong and looks unprotected keeps generating the discomfort that eventually makes somebody look at it again. A configuration that is wrong and looks protected does not. The concern has been spent, the ticket is closed, the design document says a finality lag is applied, and nothing on any dashboard will ever contradict that sentence. Partial protection here is not a safe middle position, it is the same error with the alarm removed.

The arithmetic underneath is not subtle. A replacement takes back the last 3 blocks. At a lag of 1 the loader has already written two of those three. At a lag of 2 it has written one. At a lag of 3 it has written none. The sequence 511, 346, 172, 0 is that sentence counted in rows.

The lag is a decision with two inputs, and only one of them is yours THE LAG YOU SET A number in your loader configuration. You choose it, and you can read it. + THE DEPTH YOU FACE How many blocks back a replacement reaches. Not a column in your data. ? = IS THE WAREHOUSE CORRECT? Answerable only when both of the boxes to the left are filled in. Every row of the lag table is conditional on a number nobody wrote down. Move the depth selector in the live box and the whole table moves with it. A lag of 3 is not a safe value, it is the answer to one question, and a team that cannot state the question has picked a number rather than built a defence. What the bench fixed, said plainly. Bench B uses a depth of 3, a replacement every 50 blocks, and 200 blocks in total, because a bench has to fix something. No figure on this page describes any real network, and the depth for whatever you are reading is a question to be answered outside the warehouse. Session 7 takes the two defences available and prices what each of them costs.
🔍 Click to zoom - the question mark is the whole diagram. Everything else on it is arithmetic once that box has a number in it.
LiveWhat tonight deliberately does not answer3 min▶

There are two mechanisms available against this and neither has been described tonight, on purpose. Holding rows back until the source has stopped changing its mind is one. Removing rows already written when the blocks they came from are replaced is the other. They are not two settings of the same thing, they fail in different circumstances, and they send their bills to different people.

Session 7 puts both of them against Bench B and prices them, including the configuration where you build both and the second one never fires. Tonight's contribution to that conversation is the size of the thing being defended against and the question that has to be answered before either defence can be specified.

The useful thing to carry out of the room is therefore not a recommendation. It is a measurement and a question: this much of my warehouse would be wrong, and I do not currently know the depth I am designing against.

Self-studyWhere serious people disagree5 min▶

One camp says 5.75 percent is not an emergency and treating it as one is how engineering time gets burned on the wrong thing. Their case is that plenty of production datasets carry errors of that order from sampling, from clock skew, from a source system's own corrections, and that nobody rebuilds a pipeline over it. They add a sharper point: the error self-limits, because a report run a day later reads mostly correct history, so the practical exposure is confined to the freshest window and can be managed by not making irreversible decisions on the freshest window.

The other camp says the percentage is beside the point, and that quoting it as though it were a tolerance is the actual mistake. Their case rests on three differences. The error is not random, so it does not average out of anything. It is unbounded from inside the warehouse, because the depth could be larger next month and nothing in the table would say so. And it is unmeasured in production: 5.75 percent is a number the bench can produce and your warehouse cannot, so calling it small is a claim about a quantity you have never observed.

Both positions are held by people who ship. Notice that they are not disagreeing about the mechanism or about the 511, and that the first camp's argument depends entirely on its last clause. If your organisation genuinely does not make irreversible decisions on the freshest window, the first position is defensible and cheap. If it does, and most do somewhere, then the exposure is concentrated exactly where the decisions are, and the first argument has been refuted by your own reporting calendar rather than by the second camp.

Part 3 · build-along

Find out whether your own upstream can take something back

15 min hands on

Six steps on a pipeline in your own estate, chain or no chain. The first three are answerable in ten minutes and most teams have never been asked any of them.

Ask whether your upstream can withdraw

Not restate, not correct, not arrive late. Withdraw: can a record you have already loaded cease to exist at the source with nothing sent to tell you? Write yes, no, or nobody knows. The third answer is the most common and it is a finding.

List the checks you run and mark what each one inspects

Every quality check on the table, with one word each: arriving or resident. Count them. If everything in the list says arriving, you have measured tonight's blind spot in your own pipeline in about five minutes.

Say what your volume alert would do

If your loader wrote a replacement batch alongside the batch it superseded, would any alert fire? Most volume rules trigger on a shortfall. Say out loud what yours does when the count goes up for the wrong reason.

Run the depth selector across its range

In the live box, hold the run fixed and move the depth from 1 to 6. Write down the share of the warehouse that is wrong at each end. That spread is what not knowing the depth costs you in ignorance, expressed as a range you cannot currently narrow.

Write the missing number as a question, not a guess

One line in your design document: the depth this pipeline is designed against is blank, and here is who would know. Resist filling it in from memory. A number in that slot with no source behind it is worse than a blank, because the blank gets asked about.

Name the report that reads the freshest window

One report, dashboard or alert in your estate that leans on the newest data. That is where a withdrawal would land first and hardest. Bring the name to session 7, because it is the thing the two defences are being chosen for.

The habit to keep. Sort your data quality checks into two piles: ones that inspect rows arriving and ones that inspect rows resident. Almost every team has a full pile and an empty pile, and almost nobody has noticed, because the empty pile has no failures in it. A pipeline whose entire quality suite asks about the incoming batch is silent by construction about every way the past can change, and silence from a check that was never written reads exactly like a pass.

Check yourself

Three questions

After a replacement, which of the four checks in Part 1 moves, and in which direction?

Every check inspects rows that arrive, and the rows that arrived are fine. The replacement writes new well formed rows with their own keys, so nothing trips, and the two observables that move both move upward. That is why the situation reads as healthy at the moment it got worse.

A team sets a finality lag of 1 against a depth of 3 and records it in the design document as a precaution. Why is that position worse than having set no lag at all?

A lag of 1 genuinely removes damage, dropping 511 to 346, which is precisely what makes it dangerous. The residual is invisible from inside the warehouse and the documented precaution ensures nobody goes looking for it.

Your team already handles late-arriving records well, with idempotent loads and merge keys. Why does none of that help here?

A merge key resolves a row that turns up after its period. It has nothing to say about a row that was written, counted and reported, and is now unsupported by the source. The first leaves every previously correct figure correct. The second does not.

Before session 7

Homework

Twenty minutes. One inventory and one number, both from a pipeline you actually run.

Finish the arriving-against-resident inventory

Every quality check on one table, sorted into the two piles from step 2, with the count of each. Bring the totals. If the resident pile is empty, write one sentence on what you would have to read twice in order to fill it.

Bring your current lag and where it came from

Whatever delay your loader applies today, in whatever unit, plus an honest note on whether it was a decision or an accident of scheduling. Session 7 prices both defences, and the first question it asks is what you are already paying.

Covered tonight

What this session covers

✓The damage, measured511 orphaned rows at 5.75 percent of the warehouse, from a loader with no defect in it.
✓Stable across runs5.65 to 5.75 percent over four runs, so a property of the arrangement rather than one bad draw.
✓Why four standard checks stay greenAll of them inspect rows arriving. The wrong rows are resident, and nothing re-examines those.
✓The reassuring signalRow count and freshness both improve after a replacement, which is the cruellest detail in the mechanism.
✓The full lag table511, 346, 172, 0, 0, 0, 0 against 0, 44, 84, 125, 168, 249 and 497 rows held back.
✓The dangerous middleA lag shallower than the depth buys partial protection and removes the pressure to look again.
✓The input nobody writes downEvery row of the table is conditional on the depth, which is not a column in your data.
✓Late against withdrawnAdditive against subtractive, and why the existing toolkit produces rows where one must be removed.
◐The two defences and their costsNamed tonight and left unpriced on purpose. Session 7 puts both against Bench B.
◐Establishing the depth you faceRaised tonight as the missing input. Session 7 is where it becomes a design input with a source.
○Idempotency and late-arriving craftNamed and pointed at learn-data-engineering-with-phoebe. The general patterns are theirs.
○Why branches get replaced at allThe prerequisite is learn-blockchain-with-phoebe and it is assumed rather than repeated.
○Named networks, and any real replacement depthEvery figure here comes from the generator in w3-live.js and describes nothing outside it.
○Anything to do with price, markets or holdingsOut of scope by design, and nothing here is advice of any kind.
Take this with you

Cheat sheet

The one sentence. 511 rows, 5.75 percent of the warehouse, describing history the chain no longer has, and nothing errored.
It is not a draw. 5.65 to 5.75 percent across four runs. This is what the arrangement produces, not a bad month.
Why it is silent. Every check inspects rows arriving. The wrong rows are resident, and nothing re-reads those.
The cruel detail. Row count and freshness both improve. The only signals that move move the reassuring way.
The lag table. 511, 346, 172, 0 at lags 0 to 3, then 0 at 4, 6 and 12, holding 125, 168, 249 and 497 rows.
The dangerous middle. Lag 1 leaves 3.99 percent wrong and a closed ticket. Partial protection removes the alarm, not the error.
The missing input. You cannot choose a lag without the depth, and the depth is not in your data.
The distinction. Late is additive and your toolkit expects it. Withdrawn is subtractive and nothing in it applies.