Typorithm · medium

Recovery lab: facts, CDC, checkpoint and batch audit

pythonetldata-engineeringindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Recovery lab: facts, CDC, checkpoint and batch audit

0%

Concept and contract

Build a real disk-backed pipeline that survives process interruption and safe replay inside one SQLite boundary.

The capstone combines source progress, versioned current state, delete tombstones and a batch audit in one owned SQLite database. Source positions are contiguous positive integers; per-entity versions decide authoritative state. These are different orderings: even a stale entity event can occupy a new source position that must be consumed safely.

A successful batch commits facts, checkpoint and audit together. An interruption before commit must leave all three unchanged. The guarantee is deliberately local to this database, not an exactly-once claim about external APIs, file publication or an entire distributed pipeline. The downloadable lab contains the complete program and independent recovery probes.

Mechanism and worked example

prepare creates the tables and initial zero checkpoint.

prepare creates the tables and initial zero checkpoint. apply_batch selects positions beyond that checkpoint, rejects gaps or duplicates, validates envelopes and applies version-guarded UPSERTs with retained tombstones. It rejects different payloads at one entity version. Only after data statements does it update progress and insert the batch audit within the same transaction.

The main fixture includes an update, another identity, a newer delete, a stale resurrection attempt and a final new identity. A controlled pre-commit fault leaves an empty sink and zero progress. Reopening and retrying commits five source events into the correct visible state; another replay leaves the same rows, checkpoint and audit count.

Complete worked example

python

import sqlite3

def connect(path):
    connection = sqlite3.connect(path, autocommit=True)
    connection.execute('PRAGMA synchronous=FULL')
    connection.autocommit = False
    return connection

def prepare(path):
    connection = connect(path)
    try:
        connection.executescript('CREATE TABLE IF NOT EXISTS fact(id TEXT PRIMARY KEY, version INTEGER, amount_cents INTEGER, deleted INTEGER); CREATE TABLE IF NOT EXISTS checkpoint(id INTEGER PRIMARY KEY CHECK(id=1),pos INTEGER); INSERT OR IGNORE INTO checkpoint VALUES(1,0); CREATE TABLE IF NOT EXISTS batches(start_pos INTEGER,end_pos INTEGER,source_rows INTEGER, UNIQUE(start_pos,end_pos));')
        connection.commit()
    finally:
        connection.close()

def read_state(path):
    connection = connect(path)
    try:
        return {'rows':[list(r) for r in connection.execute('SELECT id,version,amount_cents FROM fact WHERE deleted=0 ORDER BY id')],
                'checkpoint':connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0],
                'batches':connection.execute('SELECT count(*) FROM batches').fetchone()[0]}
    finally:
        connection.close()

def audit_source_rows(path):
    connection = connect(path)
    try:
        return connection.execute('SELECT coalesce(sum(source_rows),0) FROM batches').fetchone()[0]
    finally:
        connection.close()

def apply_batch(path, events, fault=None):
    connection = connect(path)
    try:
        old = connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0]
        if any(type(r['pos']) is not int or r['pos'] <= 0 for r in events):
            raise ValueError('positive integer positions required')
        selected = sorted((r for r in events if r['pos'] > old), key=lambda r:r['pos'])
        if not selected:
            return
        if [r['pos'] for r in selected] != list(range(old+1,selected[-1]['pos']+1)):
            raise ValueError('source gap or duplicate position')
        for r in selected:
            if r['op'] not in ['upsert','delete'] or not isinstance(r['id'],str) or not r['id'] or type(r['version']) is not int or r['version'] < 1:
                raise ValueError('invalid source envelope')
            if r['op'] == 'upsert' and type(r.get('amount_cents')) is not int:
                raise ValueError('integer cents required')
        with connection:
            for r in selected:
                payload = (r.get('amount_cents') if r['op']=='upsert' else None, int(r['op']=='delete'))
                previous = connection.execute('SELECT version,amount_cents,deleted FROM fact WHERE id=?',[r['id']]).fetchone()
                if previous and previous[0] == r['version'] and tuple(previous[1:]) != payload:
                    raise ValueError('conflicting source version')
                connection.execute('INSERT INTO fact VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,amount_cents=excluded.amount_cents,deleted=excluded.deleted WHERE excluded.version > fact.version',[r['id'],r['version'],*payload])
            if fault:
                fault()
            connection.execute('UPDATE checkpoint SET pos=? WHERE id=1',[selected[-1]['pos']])
            connection.execute('INSERT INTO batches VALUES(?,?,?)',[old,selected[-1]['pos'],len(selected)])
    finally:
        connection.close()

def inspect_example(events, fail):
    path = 'capstone.db'
    prepare(path)
    def interrupted():
        raise RuntimeError('controlled pre-commit interruption')
    try:
        apply_batch(path,events,interrupted if fail else None)
    except RuntimeError:
        pass
    before = read_state(path)
    apply_batch(path,events)
    after = read_state(path)
    apply_batch(path,events)
    return {'before':before,'after':after,'replayUnchanged':after == read_state(path)}
Reasoning
prepare creates the tables and initial zero checkpoint. apply_batch selects positions beyond that checkpoint, rejects gaps or duplicates, validates envelopes and applies version-guarded UPSERTs with retained tombstones. It rejects different payloads at one entity version. Only after data statements does it update progress and insert the batch audit within the same transaction.
Download the ETL and Data Engineering computer lab ↗

Focused decision: use the complete example for context

python

with connection:
    write_current_state(connection, events)
    save_checkpoint(connection, position)
    save_batch_audit(connection, count)
Decision
Report the actual committed source-row audit after failure, restart and replay without advancing a checkpoint ahead of its data.
Download the ETL and Data Engineering computer lab ↗

Counterexamples and common mistakes

The mutation advances and commits the checkpoint before entering the data transaction.

The mutation advances and commits the checkpoint before entering the data transaction. When the deliberate fault rolls back the fact writes, the cursor already says the batch was consumed. A restart skips those missing facts, demonstrating why progress must not be treated as an unrelated operational annotation.

The verifier additionally starts a fresh interpreter and calls os._exit at the pre-commit fault point. Another interpreter opens the same file and verifies rollback before retrying. This is actual process-death evidence, stronger than a caught exception alone, but it is still not a test of power failure, storage hardware or every possible filesystem configuration.

Controlled bug: observe the rejected data contract

python

import sqlite3

def connect(path):
    connection = sqlite3.connect(path, autocommit=True)
    connection.execute('PRAGMA synchronous=FULL')
    connection.autocommit = False
    return connection

def prepare(path):
    connection = connect(path)
    try:
        connection.executescript('CREATE TABLE IF NOT EXISTS fact(id TEXT PRIMARY KEY, version INTEGER, amount_cents INTEGER, deleted INTEGER); CREATE TABLE IF NOT EXISTS checkpoint(id INTEGER PRIMARY KEY CHECK(id=1),pos INTEGER); INSERT OR IGNORE INTO checkpoint VALUES(1,0); CREATE TABLE IF NOT EXISTS batches(start_pos INTEGER,end_pos INTEGER,source_rows INTEGER, UNIQUE(start_pos,end_pos));')
        connection.commit()
    finally:
        connection.close()

def read_state(path):
    connection = connect(path)
    try:
        return {'rows':[list(r) for r in connection.execute('SELECT id,version,amount_cents FROM fact WHERE deleted=0 ORDER BY id')],
                'checkpoint':connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0],
                'batches':connection.execute('SELECT count(*) FROM batches').fetchone()[0]}
    finally:
        connection.close()

def audit_source_rows(path):
    connection = connect(path)
    try:
        return connection.execute('SELECT coalesce(sum(source_rows),0) FROM batches').fetchone()[0]
    finally:
        connection.close()

def apply_batch(path, events, fault=None):
    connection = connect(path)
    try:
        old = connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0]
        if any(type(r['pos']) is not int or r['pos'] <= 0 for r in events):
            raise ValueError('positive integer positions required')
        selected = sorted((r for r in events if r['pos'] > old), key=lambda r:r['pos'])
        if not selected:
            return
        if [r['pos'] for r in selected] != list(range(old+1,selected[-1]['pos']+1)):
            raise ValueError('source gap or duplicate position')
        for r in selected:
            if r['op'] not in ['upsert','delete'] or not isinstance(r['id'],str) or not r['id'] or type(r['version']) is not int or r['version'] < 1:
                raise ValueError('invalid source envelope')
            if r['op'] == 'upsert' and type(r.get('amount_cents')) is not int:
                raise ValueError('integer cents required')
        connection.execute('UPDATE checkpoint SET pos=? WHERE id=1',[selected[-1]['pos']])
        connection.commit()
        with connection:
            for r in selected:
                payload = (r.get('amount_cents') if r['op']=='upsert' else None, int(r['op']=='delete'))
                previous = connection.execute('SELECT version,amount_cents,deleted FROM fact WHERE id=?',[r['id']]).fetchone()
                if previous and previous[0] == r['version'] and tuple(previous[1:]) != payload:
                    raise ValueError('conflicting source version')
                connection.execute('INSERT INTO fact VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,amount_cents=excluded.amount_cents,deleted=excluded.deleted WHERE excluded.version > fact.version',[r['id'],r['version'],*payload])
            if fault:
                fault()
            connection.execute('UPDATE checkpoint SET pos=? WHERE id=1',[selected[-1]['pos']])
            connection.execute('INSERT INTO batches VALUES(?,?,?)',[old,selected[-1]['pos'],len(selected)])
    finally:
        connection.close()

def inspect_example(events, fail):
    path = 'capstone.db'
    prepare(path)
    def interrupted():
        raise RuntimeError('controlled pre-commit interruption')
    try:
        apply_batch(path,events,interrupted if fail else None)
    except RuntimeError:
        pass
    before = read_state(path)
    apply_batch(path,events)
    after = read_state(path)
    apply_batch(path,events)
    return {'before':before,'after':after,'replayUnchanged':after == read_state(path)}
Reasoning
The mutation advances and commits the checkpoint before entering the data transaction. When the deliberate fault rolls back the fact writes, the cursor already says the batch was consumed. A restart skips those missing facts, demonstrating why progress must not be treated as an unrelated operational annotation.
Download the ETL and Data Engineering computer lab ↗

Independent practice

Add auditSourceRows to the final report, measured by summing source_rows from committed audit entries.

Add auditSourceRows to the final report, measured by summing source_rows from committed audit entries. Keep checkpoint, visible facts and replay observations unchanged. The count must reflect committed source events, including stale versions that were consumed without replacing canonical state, rather than merely counting visible business rows.

Use audit_source_rows on the reopened database after recovery. The five-event fixture should report five although only two identities remain visible. Empty input reports zero, and an ordinary one-event delivery reports one. Preserve source-gap rejection and unchanged caller data; do not add an audit entry merely because an attempt started and later rolled back.

Step 1: Report the actual committed source-row audit after failure, restart and replay without advancing a checkpoint ahead of its data. Step 2: Edit the owned exercise file and run verify.py --practice etl-capstone. Preserve caller inputs and the independently declared failure cases.

Practice starter: solve before reading the solution

python

import sqlite3

def connect(path):
    connection = sqlite3.connect(path, autocommit=True)
    connection.execute('PRAGMA synchronous=FULL')
    connection.autocommit = False
    return connection

def prepare(path):
    connection = connect(path)
    try:
        connection.executescript('CREATE TABLE IF NOT EXISTS fact(id TEXT PRIMARY KEY, version INTEGER, amount_cents INTEGER, deleted INTEGER); CREATE TABLE IF NOT EXISTS checkpoint(id INTEGER PRIMARY KEY CHECK(id=1),pos INTEGER); INSERT OR IGNORE INTO checkpoint VALUES(1,0); CREATE TABLE IF NOT EXISTS batches(start_pos INTEGER,end_pos INTEGER,source_rows INTEGER, UNIQUE(start_pos,end_pos));')
        connection.commit()
    finally:
        connection.close()

def read_state(path):
    connection = connect(path)
    try:
        return {'rows':[list(r) for r in connection.execute('SELECT id,version,amount_cents FROM fact WHERE deleted=0 ORDER BY id')],
                'checkpoint':connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0],
                'batches':connection.execute('SELECT count(*) FROM batches').fetchone()[0]}
    finally:
        connection.close()

def audit_source_rows(path):
    connection = connect(path)
    try:
        return connection.execute('SELECT coalesce(sum(source_rows),0) FROM batches').fetchone()[0]
    finally:
        connection.close()

def apply_batch(path, events, fault=None):
    connection = connect(path)
    try:
        old = connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0]
        if any(type(r['pos']) is not int or r['pos'] <= 0 for r in events):
            raise ValueError('positive integer positions required')
        selected = sorted((r for r in events if r['pos'] > old), key=lambda r:r['pos'])
        if not selected:
            return
        if [r['pos'] for r in selected] != list(range(old+1,selected[-1]['pos']+1)):
            raise ValueError('source gap or duplicate position')
        for r in selected:
            if r['op'] not in ['upsert','delete'] or not isinstance(r['id'],str) or not r['id'] or type(r['version']) is not int or r['version'] < 1:
                raise ValueError('invalid source envelope')
            if r['op'] == 'upsert' and type(r.get('amount_cents')) is not int:
                raise ValueError('integer cents required')
        with connection:
            for r in selected:
                payload = (r.get('amount_cents') if r['op']=='upsert' else None, int(r['op']=='delete'))
                previous = connection.execute('SELECT version,amount_cents,deleted FROM fact WHERE id=?',[r['id']]).fetchone()
                if previous and previous[0] == r['version'] and tuple(previous[1:]) != payload:
                    raise ValueError('conflicting source version')
                connection.execute('INSERT INTO fact VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,amount_cents=excluded.amount_cents,deleted=excluded.deleted WHERE excluded.version > fact.version',[r['id'],r['version'],*payload])
            if fault:
                fault()
            connection.execute('UPDATE checkpoint SET pos=? WHERE id=1',[selected[-1]['pos']])
            connection.execute('INSERT INTO batches VALUES(?,?,?)',[old,selected[-1]['pos'],len(selected)])
    finally:
        connection.close()

def inspect_example(events, fail):
    path = 'capstone.db'
    prepare(path)
    def interrupted():
        raise RuntimeError('controlled pre-commit interruption')
    try:
        apply_batch(path,events,interrupted if fail else None)
    except RuntimeError:
        pass
    before = read_state(path)
    apply_batch(path,events)
    after = read_state(path)
    apply_batch(path,events)
    return {'before':before,'after':after,'replayUnchanged':after == read_state(path)}

# Independent task: Report the actual committed source-row audit after failure, restart and replay without advancing a checkpoint ahead of its data.
Reasoning
Report the actual committed source-row audit after failure, restart and replay without advancing a checkpoint ahead of its data.
Download the ETL and Data Engineering computer lab ↗
  • Hint 1: Add auditSourceRows to the final report, measured by summing source_rows from committed audit entries. Keep checkpoint, visible facts and replay observations unchanged. The count must reflect committed source events, including stale versions that were consumed without replacing canonical state, rather than merely counting visible business rows.
  • Hint 2: Use audit_source_rows on the reopened database after recovery. The five-event fixture should report five although only two identities remain visible. Empty input reports zero, and an ordinary one-event delivery reports one. Preserve source-gap rejection and unchanged caller data; do not add an audit entry merely because an attempt started and later rolled back.

Solution, tests and trade-offs

The solution reports committed source coverage alongside canonical state.

The solution reports committed source coverage alongside canonical state. Its independent acceptance cases and process-exit probe both verify that recovery yields one audit batch, checkpoint five and no stale resurrection. Replaying the already committed source produces no additional database effect, so the audit remains an account of committed work rather than attempts.

Database constraints and synchronous settings support this local design, but a production pipeline also needs source retention, corruption handling, schema migration, concurrency control and observability. The separate manifest lesson handles file visibility with a different boundary. Joining those systems requires a deliberate publication protocol or reconciliation instead of assuming one transaction covers them all.

Practice solution and acceptance checks

python

import sqlite3

def connect(path):
    connection = sqlite3.connect(path, autocommit=True)
    connection.execute('PRAGMA synchronous=FULL')
    connection.autocommit = False
    return connection

def prepare(path):
    connection = connect(path)
    try:
        connection.executescript('CREATE TABLE IF NOT EXISTS fact(id TEXT PRIMARY KEY, version INTEGER, amount_cents INTEGER, deleted INTEGER); CREATE TABLE IF NOT EXISTS checkpoint(id INTEGER PRIMARY KEY CHECK(id=1),pos INTEGER); INSERT OR IGNORE INTO checkpoint VALUES(1,0); CREATE TABLE IF NOT EXISTS batches(start_pos INTEGER,end_pos INTEGER,source_rows INTEGER, UNIQUE(start_pos,end_pos));')
        connection.commit()
    finally:
        connection.close()

def read_state(path):
    connection = connect(path)
    try:
        return {'rows':[list(r) for r in connection.execute('SELECT id,version,amount_cents FROM fact WHERE deleted=0 ORDER BY id')],
                'checkpoint':connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0],
                'batches':connection.execute('SELECT count(*) FROM batches').fetchone()[0]}
    finally:
        connection.close()

def audit_source_rows(path):
    connection = connect(path)
    try:
        return connection.execute('SELECT coalesce(sum(source_rows),0) FROM batches').fetchone()[0]
    finally:
        connection.close()

def apply_batch(path, events, fault=None):
    connection = connect(path)
    try:
        old = connection.execute('SELECT pos FROM checkpoint WHERE id=1').fetchone()[0]
        if any(type(r['pos']) is not int or r['pos'] <= 0 for r in events):
            raise ValueError('positive integer positions required')
        selected = sorted((r for r in events if r['pos'] > old), key=lambda r:r['pos'])
        if not selected:
            return
        if [r['pos'] for r in selected] != list(range(old+1,selected[-1]['pos']+1)):
            raise ValueError('source gap or duplicate position')
        for r in selected:
            if r['op'] not in ['upsert','delete'] or not isinstance(r['id'],str) or not r['id'] or type(r['version']) is not int or r['version'] < 1:
                raise ValueError('invalid source envelope')
            if r['op'] == 'upsert' and type(r.get('amount_cents')) is not int:
                raise ValueError('integer cents required')
        with connection:
            for r in selected:
                payload = (r.get('amount_cents') if r['op']=='upsert' else None, int(r['op']=='delete'))
                previous = connection.execute('SELECT version,amount_cents,deleted FROM fact WHERE id=?',[r['id']]).fetchone()
                if previous and previous[0] == r['version'] and tuple(previous[1:]) != payload:
                    raise ValueError('conflicting source version')
                connection.execute('INSERT INTO fact VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,amount_cents=excluded.amount_cents,deleted=excluded.deleted WHERE excluded.version > fact.version',[r['id'],r['version'],*payload])
            if fault:
                fault()
            connection.execute('UPDATE checkpoint SET pos=? WHERE id=1',[selected[-1]['pos']])
            connection.execute('INSERT INTO batches VALUES(?,?,?)',[old,selected[-1]['pos'],len(selected)])
    finally:
        connection.close()

def inspect_example(events, fail):
    path = 'capstone.db'
    prepare(path)
    def interrupted():
        raise RuntimeError('controlled pre-commit interruption')
    try:
        apply_batch(path,events,interrupted if fail else None)
    except RuntimeError:
        pass
    before = read_state(path)
    apply_batch(path,events)
    after = read_state(path)
    apply_batch(path,events)
    return {'before':before,'after':after,'replayUnchanged':after == read_state(path), 'auditSourceRows':audit_source_rows(path)}
Reasoning
The solution reports committed source coverage alongside canonical state. Its independent acceptance cases and process-exit probe both verify that recovery yields one audit batch, checkpoint five and no stale resurrection. Replaying the already committed source produces no additional database effect, so the audit remains an account of committed work rather than attempts.
Download the ETL and Data Engineering computer lab ↗

Interview reasoning and next step

Walk through failure before writes, during facts, before checkpoint and after commit.

Walk through failure before writes, during facts, before checkpoint and after commit. For each point state what a fresh connection observes and whether restart replays the batch. Explain why source positions, entity versions, audit counts and visible row counts can legitimately differ while the pipeline remains correct.

For a stretch, design a bounded reconciler comparing raw source coverage, committed audit ranges and published manifest lineage. Keep repairs explicit and test them on owned fixtures. In the app, download this entire course manually to retain every lesson, example, prepared result, diagram and question offline; running the pipeline remains a computer-lab activity.

Optional stretch — after the core task works: Reconcile committed audit ranges with immutable raw lineage and a separately published manifest.

Computer practice: https://applydjinn.com/typorithm/etl-data-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt and run verify.py. Edit exercises/etl-capstone.py and run --practice etl-capstone. Each case owns its files and database; Parquet uses the pinned DuckDB engine. The recovery probe terminates a separate interpreter and reopens the SQLite file. The app retains static examples, prepared results and questions without an editor or code execution. Use the course Download button once to save every lesson and illustration; opening an unsaved course does not automatically download it.

Build a real disk-backed pipeline that survives process interruption and safe replay inside one SQLite boundary.