A Retried File Should Not Become New Training Data
Why Finding a File Was Not the Same as Finishing It
BOTIE
Databricks announced Auto Loader in public preview in February 2020 to handle incremental discovery and loading of files from cloud storage. It is a file source used with Spark streaming, with checkpointed progress so a restarted query can continue. A checkpoint is the durable recovery record for that query; it has to work with the destination’s commit behaviour.
That distinction is what we will build. A discovery says there is a file to consider. A completed-file record says its effect belongs in the committed output. Keeping the output rows and progress together prevents a lost acknowledgement from becoming a second copy of the observations.
The Job Recovered. Why Did the Dataset Grow Again?
Botie’s loader finishes a sensor file and the connection fails before the interface sees its success. The scheduler retries the job. Appending the same file again makes the training dataset larger, even though no new observations arrived.
We need a durable answer to which files have already contributed. We also need that answer to agree with the output: recording progress before the rows are safely written can lose data instead of duplicating it.
This local state transition assumes immutable file identities and returns accepted rows with their progress record. A real loader needs durable sink commits, and duplicate business events need their own identities.
Step 1 of 1
A repeated discovery is not a new file
The durable checkpoint already contains f0.
| Discovery | File identity | Records |
|---|---|---|
| Already committed | f0 | r0 |
| New | f1 | r1 |
| Repeated notification | f1 | r1 |
Already committed
- File identity
- f0
- Records
- r0
New
- File identity
- f1
- Records
- r1
Repeated notification
- File identity
- f1
- Records
- r1
Identify the unit that was processed
File acceptance changes the output and durable progress together in the returned teaching state.
Make a completed file stay completed
A scheduler retries after losing the acknowledgement. Botie must not learn from a second copy of the same file.
BOTIE
New output state and committed file identities
{
"rows": [
{
"id": "r0"
},
{
"id": "r1"
}
],
"checkpoint": [
"f0",
"f1"
]
}Follow the operations
1. Current discovered immutable files
[ { "file": "f0", "rows": [ { "id": "r0" } ] }, { "file": "f1", "rows": [ { "id": "r1" } ] }, { "file": "f1", "rows": [ { "id": "r1" } ] } ]2. Earlier committed file identities
- 0f0
3. Keep files absent from the checkpoint
[ { "file": "f1", "rows": [ { "id": "r1" } ] } ]4. Earlier committed output rows
[ { "id": "r0" } ]5. Commit accepted rows and file progress
{ "rows": [ { "id": "r0" }, { "id": "r1" } ], "checkpoint": [ "f0", "f1" ] }6. Result
{ "rows": [ { "id": "r0" }, { "id": "r1" } ], "checkpoint": [ "f0", "f1" ] }
This local state transition assumes immutable file identities and returns accepted rows with their progress record. A real loader needs durable sink commits, and duplicate business events need their own identities.
1. Identify the unit that was processed
Imagine a short list of files waiting to be loaded. Each file has an identity and a list of sensor records. The checkpoint keeps the identities whose rows have already reached the committed output. That is the information a restarted job needs before deciding what remains to do.
Our Python representation mirrors those pieces: a list of file dictionaries, a rows list inside each file, and a checkpoint list. A set is useful for asking whether an identity is already present, while the returned list preserves a deliberate order.
In this example, one identity always refers to the same contents. If a producer overwrites a path with new data, we need a version or a different replacement policy before the same rule is safe.
Two different files can still contain the same business event. Detecting that requires an event identity as well as file progress.
Ask whether a file identity has completed
A set supports membership checks without attaching a numerical meaning to an identifier.
completed = {"f0", "f2"}
print("f0" not in completed)
print("f1" not in completed)False
TrueThis checks only committed identities. The full ingestion procedure also has to suppress repeated new discoveries and return progress with the output.
2. Do not confuse discovered with committed
A notification tells us that a file may be available. It does not prove the file has contributed to the output. Select discoveries absent from committed progress, and suppress identical repeated discoveries in the current batch.
If one batch presents different contents under the same immutable file identity, stop rather than arbitrarily taking the first. The conflict is evidence that the identity contract is broken. The checkpoint alone cannot compare a new file with historical contents that were never retained in this model.
The accepted-file preview lets us review what the next commit will include.
3. Keep the output and the progress consistent
There are two ways to leave a misleading recovery record. We could write the rows and fail before recording that their file completed. A retry would append them again. Or we could record completion before the rows are safely stored. A retry would skip a file whose output is missing.
Our commit component returns both changes as one new state, leaving the earlier state untouched. That is the relationship we want from an atomic update: the rows and their progress agree about what completed.
A real destination, often called the sink, needs a durable transaction or an operation identity that makes the same write have one effect when repeated. That repeat-safe property is called idempotency. Returning two Python fields together helps us inspect the rule; real storage must enforce it through failures.
earlier checkpoint = [f0]
new accepted file identities = [f1]
next checkpoint = [f0, f1]
retry with this checkpoint: accepted file identities = []On a successful replay, the committed dataset should be the same even when the scheduler has attempted the job more than once.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which file identities should contribute new rows?
Use the information below.
Checkpoint ["f0"]. Discoveries are f0:[r0], f1:[r1], f1:[r1]. Files are immutable.4. Let Auto Loader keep its progress on an approved sink
The reference uses a fixed schema and a persistent checkpoint path, then writes to a Delta table. availableNow asks the query to process the available input and finish; awaitTermination waits for that query. Keep the checkpoint associated with this logical query.
Choose source, checkpoint and output locations deliberately. Deleting a checkpoint is a state change with replay consequences. It is not a harmless way to clear an error. Changing the target or reusing a checkpoint for another query requires a reviewed recovery plan.
Lakeflow can manage checkpointing for its own pipelines. This standalone reference makes the path visible because checkpoint identity is what we are learning.
Run in your own environment
Read immutable JSON files incrementally
This optional reference runs in your own Databricks environment. The browser exercise below uses ordinary Python and does not contact Databricks.
Use your own Databricks workspace with both existing Unity Catalog volumes: lab.training.landing holds the approved source JSON files, and lab.training.checkpoints holds this query’s checkpoint directory. Use Unity Catalog-compatible compute. The job needs USE CATALOG on lab and USE SCHEMA on lab.training, READ VOLUME on lab.training.landing, and READ VOLUME plus WRITE VOLUME on lab.training.checkpoints. Creating the target requires CREATE TABLE on lab.training; appending to an existing target requires MODIFY on lab.training.raw_readings. This reference writes data and consumes compute.
Use a dedicated persistent checkpoint for this query. Do not copy the example paths over an existing production query.
source_path = "/Volumes/lab/training/landing/sensors"
checkpoint_path = "/Volumes/lab/training/checkpoints/sensor_ingest"
query = (spark.readStream.format("cloudFiles")
.option("cloudFiles.format", "json")
.schema("pump STRING, temperature DOUBLE")
.load(source_path)
.writeStream
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.toTable("lab.training.raw_readings"))
query.awaitTermination()Inspect the output and checkpoint after completion, then rerun against the same immutable files. Row-level business duplicates still require a separate rule.
Official interface documentation. Documentation checked .
5. Keep unrelated side effects outside the claimed guarantee
Sending an email, charging a customer or calling a tool during a streaming batch is another effect. The table transaction cannot automatically make an arbitrary remote API call exactly once. Give such effects their own durable operation identities and recovery procedure.
Even a valid file checkpoint does not decide how to handle late events, corrected records or schema drift. Those require their own declared contracts. We are adding one reliable boundary to the pipeline, not declaring every later operation safe.
The next lesson gives corrected entity records an explicit source sequence so arrival order cannot choose the current truth.
6. Try both a replay and a genuinely new file
A useful test runs the accepted result back through the same discoveries. Nothing should be appended. Another test supplies a new file identity and checks that its rows do arrive. Include an empty file: it can still be a completed input even though it contributes no rows.
Keep old input structures unchanged in these tests. Otherwise a successful assertion can depend on the function having quietly altered the checkpoint or removed rows from the discovery fixture.
Try an empty file as well as a replay, and check the checkpoint alongside the output rows.
Check yourself
Questions on Parts 1 to 6
Question 1 of 4
What can happen on retry?
Use the information below.
A completed-file flag is saved before its output rows become durable. The process fails.Try it with Botie
Build with Botie
Write and run Python