learn-data-orchestration-with-phoebe / Session 4 of 6
Learn Data Orchestration with Phoebe · Session 4 of 6

Rerun-safe by construction

Session 3 bought the missing mornings back, and the bill arrived immediately: silently wrong rose from 4 to 6, because twice this month a writer crashed after appending half its rows and the retry appended all of them again. The retry did its job. The task was not safe to retry. Tonight we fix the task - so that running it twice, or ten times, lands the same table as running it once - and the fix costs zero code cleverness, just one writing discipline.

🟡 Medium Your retries are double-writing two nights a month 45 min
0-3 · Welcome 3-18 · Pure tasks, and the three determinism sins 18-40 · Build-along: the dip repaired at the source 40-45 · Q&A
Part 0

Where the artifact stands

Daybreak's build now has real dependencies and two retries with backoff: 28 mornings on time out of 30. But silently wrong sits at 6, up from the DAG rung's 4, and the trace knows why - two nights the crashed writer had already appended rows before the retry ran the whole write again. The lever that saved the schedule poisoned the data. This session ends with the same 28 on-time mornings, silently wrong back at 4, and double-writes at zero - without touching the retry dial.

🎫 Clear all three quiz questions to stamp your passport Live = presented in the 45 minutes Self-study = the full depth, after class

★ What you walk out with: the deterministic-plus-idempotent pair kept properly separate, the overwrite-the-partition pattern that makes any crash-and-retry converge, and a three-item checklist of determinism sins - now(), "latest data", and append - to hunt through your own tasks tomorrow.

Part 1 · covers the functional paradigm

Tasks as pure functions 8 min live

The clearest statement of tonight's idea comes from Maxime Beauchemin - who identifies himself in the article as the creator of Airflow - writing on functional data engineering: batch tasks should behave like pure functions over partitions of data.

The same 3:40am crash and retry, two write strategies Append on rerun Overwrite the partition attempt 1: appends 48 of 96 rows, dies retry: appends all 96 rows again tonight's partition: 144 rows total inflated by half, and every exit code was 0 attempt 1: writes a partial partition, dies retry: rewrites the whole partition tonight's partition: exactly 96 rows run it twice or ten times: the same table Append makes a rerun arithmetic. Overwrite makes it idempotent: twice equals once.
🔍 Click to zoom - the crash is identical on both sides; only the write strategy decides whether the retry heals or harms.
Live Two properties, kept properly separate 5 min ▶

Beauchemin's definition packs both into one sentence: "A pure task should be deterministic and idempotent, meaning that it will produce the same result every time it runs or re-runs". Those are two different promises, and a nightly task wants both.

  • Deterministic - same inputs, same output. Run the task against night 12's data next Tuesday and you get night 12's answer, not next Tuesday's.
  • Idempotent - running twice equals running once. The second execution lands the system in the same state as the first, so a retry is free.

The write side is where idempotency is won or lost, and the prescription is blunt: "A pure task should always fully overwrite a partition as its output". More generally: "Thinking of partitions as immutable blocks of data and systematically overwriting partitions is the way to make your tasks functional". Why care this much? Because "Having idempotent tasks is vital for the operability of pipelines" - session 3's retry dial, and session 5's backfill button, are both only safe when this property holds.

Real world

Daybreak's two dip nights, replayed: the aggregate crashes at 3:40am after appending half its rows. The retry - working exactly as configured - runs the task again and appends all of them. The morning total is inflated, the dashboard renders, nothing anywhere errored. Rewrite the task to overwrite the night's partition and the identical crash-plus-retry converges to the correct table, every time.

Self-study The deeper prize: reproducibility 3 min ▶

Rerun-safety is the operational win, but Beauchemin's argument goes one floor further down: "immutable data along with versioned logic are key to reproducibility". If partitions are immutable blocks and each one records which version of the logic wrote it, then any historical number can be regenerated and checked - the pipeline becomes auditable, not just restartable.

That matters beyond incidents. When finance asks why March 12 revenue changed between two pulls of the same report, an append-style pipeline can only shrug. A partition-overwriting one can answer: this partition, that logic version, rewritten on this date, for this reason. The same discipline that makes retries safe makes the numbers defensible.

Part 2 · covers the Airflow translation

Write like a transaction, not a diary 7 min live

The Airflow best-practices page turns the paradigm into rules you can grep for. Its framing device is the one every backend engineer already trusts: "You should treat tasks in Airflow equivalent to transactions in a database" - they should "never produce incomplete results".

Three determinism sins, three fixes The sin The fix now() inside the task same code tomorrow gives a different answer use the run's logical date the run is about a night, not a wall clock read the latest available data a rerun next week reads next week's rows read that night's input partition data_interval_start names the partition append rows with plain INSERT every rerun stacks duplicate rows overwrite the partition, or UPSERT reruns converge to one true table One disease behind all three: the answer depends on WHEN the task ran, not WHICH night it is about.
🔍 Click to zoom - each sin ties the task's output to the moment of execution; each fix ties it back to the data interval.
Live Partition by the data interval 4 min ▶

Session 2 established that a run is ABOUT a night - the logical date names the data interval, not the moment of execution. Idempotency is where that idea starts paying rent. The best-practices page spells out each rule: on writes, "Do not use INSERT during a task re-run, an INSERT statement might lead to duplicate rows in your database. Replace it with UPSERT". On time, now() "should never be used inside a task". On reads, "Never read the latest available data in a task" - instead, "Read and write in a specific partition. You can use data_interval_start as a partition."

Put together, Daybreak's aggregate becomes a pure function of one night: read the night's cleaned partition, compute, overwrite the night's aggregate partition. Same code, any night, any number of times. The retry stops being a gamble, and - quietly, for next session - so does rerunning a night from three weeks ago.

Self-study Where idempotency is genuinely hard 3 min ▶

Overwriting a partition works because the warehouse is yours to rewrite. Some task effects leave the building: sending an email, charging a card, calling a third-party API that creates something on the other side. Run those twice and the customer is emailed twice - no partition to overwrite. The working pattern is to make the WRITE idempotent even when the ACTION cannot be: record every intended side effect with a dedupe key (say, night plus customer plus message type) in a table you do own, and only perform the action for keys not already marked done. A retry then re-reads the ledger, skips what happened, and finishes what did not. It narrows the double-fire window rather than abolishing it - the honest claim is "at most once per key, verified against our ledger", and for a nightly batch that is usually exactly enough.

Build-along

The dip, repaired at the source ★ 22 min · everyone builds

Same thirty nights, same crashes, same retry dial. One new lever: tasks that overwrite their night's partition instead of appending to it.

Rebuild session 3's end state: DAG dependencies on, retries on. Confirm the board reads 28 mornings on time, 6 silently wrong, and the counter nobody likes: 2 nights double-wrote. Those two nights are the honest price of retrying tasks that append.

Flip "Idempotent tasks" and watch two numbers move together: double-wrote drops to 0, and silently wrong falls back to 4 of 30. Notice what you did NOT touch - the retry dial. The dip was never a retry problem; it was a write-strategy problem that retries exposed.

Kill a writer task - clean or aggregate - and read tonight's trace. The crash happens, the retry fires, and the line that used to warn about rows appended twice is gone: the rerun overwrote the night's partition and converged. Crash plus retry now equals a clean run.

Name what remains: 4 wrong mornings, days 10 through 13. That is the aggregate logic bug - correct scheduling, correct reruns, wrong code. No scheduler fixes it. Code bugs need a repair path for HISTORY, and that is exactly where session 5 picks up.

Why this lever is invisible in the wrong metric: idempotency did not add a single on-time morning - 28 before, 28 after. It made the mornings you already had TRUE. That is why it never shows up in uptime dashboards and always shows up in audits, and why teams that only track availability skip it until the first restated quarter.

Homework

Before session 5 20 min

Check yourself

Three questions

1. A task always produces the same output for the same inputs, but rerunning it appends a second copy of that output. What is it?

The two properties are separate promises. Deterministic is about the VALUE (same inputs, same output); idempotent is about the STATE (running twice equals running once). Daybreak's aggregate was perfectly deterministic on both dip nights - and the retry still doubled the rows.

2. Why does overwrite-the-partition beat append for a nightly task?

Performance and storage are side issues. The point is convergence: a full overwrite makes every rerun land the identical partition, so the second, third or tenth execution is harmless. Beauchemin's rule - a pure task should always fully overwrite a partition as its output - is a rerun-safety guarantee.

3. What is actually wrong with calling now() or reading "the latest data" inside a task?

Both sins tie the output to the moment of execution instead of the data interval. The Airflow guidance is to read and write a specific partition, using data_interval_start as the key - then night 12's run gives night 12's answer whether you execute it tonight or replay it in three weeks.

Answer all three to see your score.

Sources this session stands on

What was covered

✓Beauchemin · Functional Data Engineering (2018)pure tasks deterministic and idempotent, fully overwrite a partition, immutable partitions, reproducibility - retrieved via reader proxy
✓Apache Airflow docs (3.3.1) · Best practicestasks as database transactions, never incomplete results, UPSERT over INSERT on rerun, no now() in tasks, partition by data_interval_start
◐Apache Airflow docs (3.3.1) · DAG Runs data intervalrecap from session 2 - the logical date names the night the run is about

Session 4 cheat sheet

DeterministicSame inputs, same output. Night 12's run gives night 12's answer, whenever you execute it.
IdempotentRunning twice equals running once. The property that makes a retry free instead of a gamble.
Overwrite the partitionA pure task fully overwrites its output partition. Crash plus retry converges instead of inflating.
Tasks are transactionsNever produce incomplete results. On rerun paths, UPSERT or full overwrite - never plain INSERT.
The three sinsnow() in the task, reading latest data, appending. Fix: logical date, the interval's partition, overwrite.
What it boughtDouble-writes 2 to 0, silently wrong 6 back to 4, on-time unchanged. Truth, not uptime - audits notice.