Lesson 06 Idempotency & Retries
Theme:

Homework 06 — Idempotency in practice

Lesson 06 · Data Engineering with Python · 3 tasks

These tasks extend the idempotency patterns from the lab. You will refactor the daily-rollup DAG from Lesson 05, add a failure-notification task with correct retry semantics, and analyse a set of hypothetical task bodies to identify idempotency violations and their fixes.

Task 1 — Make the daily rollup idempotent

In Lesson 05's homework you added dags/gharchive_daily_rollup.py with a _rollup function that wrote the output directly via out.to_parquet(final_path). This is not atomic — a mid-write crash leaves a partial file at the final path.

Refactor _rollup to use the staging-then-rename pattern from _helpers.py:

Self-check: trigger a manual run and kill the scheduler mid-write (same technique as Lab 06 Step 5). Inspect the daily_rollups/ directory — you should see a *.staging.* file but no final .parquet. After clearing and re-running, the staging file is gone and the final file is correct. Re-running the same day a second time produces the same file with no duplicates.

Task 2 — Tune retry budgets with a failure-notification task

Add a task notify_slack that runs after transform but only when transform fails. Use BashOperator with the command echo "would notify slack: the hour {{ ds }} transform failed". Set trigger_rule="one_failed" and retries=0 on the operator.

Then deliberately fail transform by temporarily adding raise RuntimeError("injected failure") at the top of _transform. Trigger a manual run.

Self-check: in the Grid view, transform is red (failed) and notify_slack is green (success). The task log for notify_slack shows exactly one echo line. Remove the injected error before moving on.

Task 3 — Identify the non-idempotent operation

Below are four hypothetical task bodies. For each one, state whether it is idempotent. For the non-idempotent ones, name the specific fix.

(a)

def task(**ctx):
    with open("counter.txt", "a") as f:
        f.write("1\n")

(b)

def task(**ctx):
    db.execute(
        "INSERT INTO events (hour, count) VALUES (%s, %s)",
        (hour, n),
    )

(c)

from uuid import uuid4

def task(**ctx):
    requests.post(
        "https://api.example.com/orders",
        json={"id": uuid4().hex, "amount": 100},
    )

(d)

def task(**ctx):
    df.to_parquet(f"out/{hour}.parquet")  # pandas default: overwrite

Self-check: you can articulate — in one sentence each — which operations are idempotent, which are not, and the minimal change that would make the non-idempotent ones safe.

View solutions →