DEV Community

Cover image for Your ERP extract is 31% wrong. Nothing errored.
Tim Collinson
Tim Collinson

Posted on Edited on

Your ERP extract is 31% wrong. Nothing errored.

Update, Oct 28: The day after this went up, two readers found a hole in the pipeline, and it’s patched. A sweep that ended early with a clean 200 would have been treated as complete, and the merge would have deleted everything it missed. Now every full read has to prove it’s complete before it can delete anything, and every merge writes down exactly what it deleted. Every number below still stands. The story is in “The night my pipeline deleted itself,” and the fix is tag post-02.1. Thanks, Karan Pulyala (@karanpulyala) and Alex Shev (@alexshev).

What you'll get out of this

In about 12 minutes, you'll walk away with:

  • Why the obvious way to pull data from an ERP gets it quietly wrong. Our first extractor got 30.8% of rows wrong and never threw an error.
  • The six defenses that took it to zero, and which mess each one fixes.
  • A serverless pattern for ERP extraction on AWS: Amazon EventBridge Scheduler, AWS Step Functions, AWS Lambda, raw pages in Amazon Simple Storage Service (Amazon S3), and curated Apache Iceberg tables in the AWS Glue Data Catalog, built with Amazon Athena MERGE. About $0 when idle.
  • The gotchas that cost us time: Lambda function URLs that rewrite your $, a Date header that isn't the server's, why we dropped Amazon S3 Tables, and the night our pipeline deleted half its own data and reported success.
  • An honest comparison of API polling, change data capture (CDC), and scheduled exports, so you can pick the right one for your plant.
  • A repo you can deploy and break on purpose.

Plant leaders: read the opening, the headline numbers, and the closing questions. Builders: jump to "The robust extractor."

Part 1: The problem

I cheated last time

Confession time. In the last post, I proved that Crestline's ERP was hiding a money-losing press brake. Grumpy PB-02, second shift. It made a nice story.

But I cheated. The ERP data in that post came out as one clean bulk export, perfectly consistent, taken at a single instant. Real ERPs don't hand you that. Real ERPs make you ask nicely, through an API, a page at a time, while the plant keeps running and the data keeps changing underneath you.

Why does that matter? Great question. Because every number in post 1 was only as good as the copy of the ERP it ran on. And nobody checks the copy.

So this time I built an ERP that fights back, wrote the extractor most of us write first, and measured how wrong it was.

It was 30.8% wrong. It never threw an error. Not one.

TL;DR

  • Crestline's fake ERP now changes while you read it, and has nine realistic messes: expiring tokens, throttling, drifting pages, a LastModified that lies, and more.
  • A typical incremental extractor got 5,762 of 18,707 rows wrong (30.8%) and kept 55 rows the ERP had deleted. Against a slow ERP, 93.3% wrong.
  • A robust extractor got zero wrong in every scenario. The key defense: a nightly full sweep, because some changes never move the timestamp.
  • On AWS it's serverless and costs about $0 when idle. The curated Iceberg tables match the ERP exactly, and post 1's margin query runs on them unchanged.
  • Repo: https://github.com/trcollinson/crestline-supply (branch tag post-02.1)

An ERP that changes while you read it

The fake ERP is a REST API in the style of mid-market vendors: an OAuth token, OData-style $filter, $top, $skip, and nextLink paging. Its data comes from the plant simulation, so there's always a known right answer.

The important part is time. The simulator emits the ERP's change log: orders entered, jobs released, and a nightly 22:00 labor batch. The API serves the ERP as of a replay clock. So the first page of your read and the fiftieth can come from slightly different versions of the ERP, the way they do against a live system.

Then I added nine messes, each one a switch in mess.yaml.

Things the API does to you:

  • Tokens that expire mid-extract (401)
  • Throttling (429 with Retry-After)
  • Random 500s
  • $skip paging that drifts when data changes between pages
  • Numbers served as "250.00000000"
  • Legacy /Date(1759420800000)/ dates

Things the ERP itself gets wrong:

  • LastModified doesn't move when labor posts against a job operation and changes its quantities
  • Duplicate orders, cancelled the next morning: the order line is soft-deleted and its job is hard-deleted
  • Jobs split into J and J-1 after release

If you've integrated with a mid-market ERP, you've probably met most of these. (If you've met all nine, I'm sorry. Let's get coffee.)

The extractor we all write first

Here's the naive extractor. Be honest, you've written this one. I have. Several times.

  1. Sign in once.
  2. Ask for everything with LastModified newer than the newest stamp you already have.
  3. Follow nextLink until it runs out.
  4. If a request fails, retry immediately, up to three times.

It's simple, it's fast, and it passes every test you'd think to write. Here's what it did over 90 days against an ERP at mess level 2:

Scenario Rows wrong (of 18,707) Share wrong
Clean ERP (control) 0 0%
Realistic ERP 5,762, plus 55 deleted rows kept 30.8%
Client polling hard (10 requests a second) 5,991, plus 51 deleted rows kept 32.0%
Slow ERP (15 minutes a page) 17,457 93.3%

The control row matters. On a clean ERP, the naive extractor is perfect, so every error below it is owed to a mess, not a bug in my code.

And almost all of the 30.8% is one mess. When the nightly labor batch posts against a job operation, it updates that operation's quantities but doesn't move LastModified. So the naive extractor never sees the update. Every operation that labor touched stays frozen at the values it had when the job was released.

Labor posts at 22:00 and changes a job operation's quantity, but its LastModified stays at 08:00. The next morning, the naive incremental read returns nothing and the copy stays at 0; the nightly sweep reads every row and fixes it.

One job operation, start to finish: labor posts at 22:00 and changes the quantities, but the stamp stays at 08:00. The next morning, the naive read asks for anything newer than 08:00 and gets nothing back. Only a read that looks at every row catches it.

Think about what that means for post 1. The margin analysis needed actual labor against every operation. A copy built this way would have shown standard numbers everywhere, and the PB-02 problem would have disappeared. Not because the plant fixed it. Because the extract hid it.

The slow-ERP number is a different failure. At 15 minutes a page, the token expires partway through, and the naive client never signs in again. It just stops, quietly, with whatever it had.

If you run the eval, you'll see a fourth scenario in between: an ERP that takes five minutes per request. The naive extractor lands at 31.1% wrong. Most of that is the same frozen operations, but on 23 nights the token expired partway through and it quietly skipped the job cost table, leaving about 50 jobs with stale actual costs. Those are exactly the numbers post 1's margin analysis runs on.

No errors. In any of it.

Part 2: The fix

The robust extractor

The robust extractor got zero rows wrong in every scenario: 0 missed, 0 lost, 0 stale, 0 deleted rows kept, 0 pulled twice. Six defenses got it there, and each one answers a specific mess. A seventh, from a reader, keeps the delete rule honest:

Defense Mess it fixes
Sign in again on 401 Tokens that expire mid-extract
Wait out 429s per Retry-After; back off with jitter on 5xx Throttling, random errors
Keyset paging: order by key, ask for keys past the last one seen $skip drift when data changes between pages
Watermarks with a one-hour overlap Changes stamped just before your last read
Read labor by sequence number, not timestamp Labor posted in a late nightly batch
A nightly full sweep of the four job tables Changes whose LastModified never moved, and hard deletes
Count before and after every full read (added in post-02.1) A read that stops early with no error, then deletes what it missed

The sweep is the one that matters most, and the one people skip. Incremental reads can only see changes that move the timestamp. When the ERP doesn't move it, no amount of clever filtering helps. You have to look at everything, compare, and correct. Over the 90 days, the sweeps corrected 5,878 operation versions whose stamps never moved.

It's also the only way to notice a hard delete. A row that vanished doesn't show up in "what changed since yesterday." It only shows up as missing in a full read.

One more rule: every page gets written to S3 exactly as the API served it, with its metadata (entity, page, request time, server time). Nothing gets cleaned on the way in. When you find a bug later, and you will, you can reprocess from the original.

On AWS: serverless, and about $0 when idle

Amazon EventBridge Scheduler starts AWS Step Functions hourly. Per entity, AWS Lambda extracts from the fake ERP to raw pages in Amazon S3, then an Amazon Athena MERGE curates them into Iceberg tables that match the ERP exactly and feed post 1's margin query.

Every hour, EventBridge Scheduler starts a Step Functions run. For each ERP entity, three at a time, a Lambda function extracts to S3 raw, then a curate step merges the new raw files into a typed Iceberg table. The fake ERP itself runs in Lambda behind a function URL, so nothing in this design runs when nothing is happening.

From raw pages to tables you can trust

Raw pages are the record of what the ERP said. They aren't something you want to query. So after each poll, a curate step runs one Athena MERGE per entity into a typed Iceberg table:

  • Newest version per key wins. Unchanged rows aren't rewritten, so the table's history shows only real changes.
  • A sweep is a complete picture. Any key it no longer contains was deleted from the ERP, so the merge deletes it too. Cancelled order lines keep their row with deleted = true, the way the ERP shows them.
  • Types are fixed on the way in. "250.00000000" becomes a number, /Date(ms)/ becomes a date, and every timestamp lands in UTC.
  • One commit per merge. Updates, inserts, and deletes land together, so nobody ever reads a half-applied change.

The proof is the copy, not the counts. At the end of the window, the curated tables match the ERP's final state exactly: 18,707 rows, 0 missing, 0 extra, 0 different.

Then the real test. Post 1's margin query, unchanged, now reads its ERP data from the curated tables through the same views. It matches the original in every cell, on all 1,252 finished jobs. PB-02 second shift still shows setups at 2.84 times standard, and an ERP margin of 20.5% against a rebuilt 14.8%.

(Sharp-eyed readers: post 1 said 20.4%. The tenth of a point comes from the job-split mess added for this post, which shrinks the parent job's material cost. Messy source data moves your numbers even when your pipeline is perfect. That's sort of the whole point.)

One honest caveat: a run reads each entity at a slightly different moment, so for a few minutes the tables can disagree with each other. The next run closes the gap. If you need a single consistent instant across every table, that's an argument for CDC or a snapshot export.

Part 3: Lessons and choices

What building it on AWS taught us

A Lambda function URL isn't a transparent pipe. It rejected a bare $ in the query string, which is a problem when your API is OData and every parameter starts with $top or $filter. The extractor sends %24top instead. It also replaces your server's Date header with its own clock. The watermarks depended on the ERP's clock, not Lambda's, so the API now sends an X-Server-Time header. Without that, every watermark would have been quietly wrong.

EventBridge Scheduler fires the first run the moment you create the schedule. Not at the next hour. Plan for a deploy that immediately starts working.

S3 Tables was the plan, and we dropped it. Athena ran MERGE against it fine, but CREATE VIEW over an S3 Tables table failed with "not yet supported for Cross-Account Glue DataCatalogs" (October 2026). It isn't a permissions gap: Athena's documentation lists CREATE VIEW as unsupported for S3 Tables. Every query in this project reads its sources through views, so the curated tables are plain Iceberg in the Glue catalog instead. The cost difference is a few cents a month.

Pick the merge window from S3 file names, not inside SQL. The curate step lists the new raw files and puts their exact dates into the MERGE. When we computed the window inside the SQL, Athena couldn't skip old partitions, so every run rescanned the entity's entire raw history, which grows about 2 MB an hour. Now a Jobs merge reads one sweep file: 0.2 MB.

Small tables need upkeep. Hourly merges leave lots of small files behind, so once a day each table gets OPTIMIZE and VACUUM. (Also: those two commands reject double-quoted table names. Ask me how I know.)

Retries are safe by design. Step Functions retries a failed merge, re-merging the same files changes nothing, and the "last merged" marker only moves after a merge succeeds. The marker lives on the poller's existing per-entity state record, so there's no extra state to manage.

The night my pipeline deleted itself

Remember the thesis? A wrong copy never throws an error. I'd love to tell you I learned that from other people's pipelines.

Storytime. To make the AWS run finish in days instead of months, the fake ERP replays its history on a fast clock. I left it running. It was running faster than I'd written down (240 times real time, not 24), and it had no end. So it ran straight past October, through 2027, and into 2028.

The raw table in Athena used partition projection with a date range of 2026 through 2027. New pages kept landing safely in S3, under dates Athena couldn't see. So the next nightly sweep looked empty to the merge. And a sweep is a complete picture, remember? Any key it doesn't contain has been deleted from the ERP. So the MERGE did exactly what I told it to do. It deleted 9,962 of 18,707 rows.

Then it reported success.

The same week, a Lambda function deployed before a fix made 69 of 95 hourly runs fail over three days. Nothing told anyone.

Before you laugh too hard: the replay clock only got there early. On a real ERP running on real time, that date range runs out on January 1, 2028. Same deletion. Same success message.

Here's what changed:

  • The date range now runs from 2020 to 2099. The merge always names its dates, so a wide range costs nothing.
  • A check runs before every MERGE. If Athena doesn't see exactly the rows the raw files hold, the run fails before anything changes.
  • A merge can't delete more than 5% of a table unless someone explicitly allows it, after checking.
  • An email alarm fires on any failed run, through an EventBridge rule and Amazon Simple Notification Service (Amazon SNS).
  • An AWS step isn't done until cdk diff shows nothing for every deployed stack, after the last commit. No more Lambdas older than the code.

Because the raw pages were never touched, the fix was a rebuild from raw alone: 18,707 rows, 0 wrong, and post 1's margin query unchanged. That's the other reason to keep raw.

The lesson isn't "add more guards." It's that "delete whatever the sweep doesn't see" is the most powerful rule in this pipeline. It's the rule that makes the copy exact, and it's the rule that emptied half of it. Rules that powerful need a check that fails loudly.

Update: the hole a reader found the next day

I published this post on a Monday. By Tuesday, Karan Pulyala (@karanpulyala) had found a hole in it.

Remember the most powerful rule in the pipeline? A sweep is a complete picture, so anything it doesn't contain gets deleted. Karan's question: who checks that the sweep is complete?

Nobody did. A dying token was already safe (the extractor throws the whole read away). But if the ERP sent back a page with no nextLink halfway through, or my paging code had a bug, the extractor would stop quietly and call the sweep complete. Then the merge would delete every row after the cut, and report success. Again.

Here's what post-02.1 adds:

  • Every full read counts the ERP's rows at the start and the end ($count=true). If the rows collected don't land in that range, the read is filed as short. A short read can update rows but can't delete any. The range gets ten rows of room for an ERP that's busy mid-read (Karan's second catch), which is far less than a 500-row page, so a lost page can't hide in it.
  • A read that ends on a full 500-row page asks one more time whether anything comes after its last key. Three real sweeps in the eval ended on exactly 500 rows. Without that extra question, all three would have failed.
  • Every merge that can delete writes down exactly what it deleted, diffed against the table's Iceberg snapshot from before the merge. A refused merge emails the count, the snapshot, and where the list lives.

Then Alex Shev (@alexshev) asked the right follow-up: have you actually seen it fire? So I planted a broken sweep in the raw bucket, one that skipped the extractor's count check entirely. The merge refused it before touching anything. It would have deleted 65 of 565 jobs. The table's snapshot didn't move, and the email named the count, the snapshot, and the file. An hour later the next real sweep took over and ran clean. Its audit listed the six rows, across four tables, of the one cancelled duplicate it was supposed to delete.

One honest caveat: none of my runs read while the ERP was mid-write, so the ten rows of room comes from reasoning and a unit test, not a measurement. I'll measure it in the next milestone.

This is the best part of writing in public. Two readers made this pipeline safer in a day.

API polling, CDC, or a scheduled export?

There are three common ways to get data out of an ERP. I built the first one. Here's how they compare, because the right answer depends on your plant.

API polling (built here) CDC from the database Scheduled export
Freshness Hourly, or whatever you schedule Near real time Daily, usually
Sees deletes Only with a full sweep Yes, from the log Only if you compare full files
Survives a lying LastModified Only with a sweep Yes, it reads the log, not stamps Yes, if each export is complete
Access you need API credentials Database-level access, often not allowed by the vendor or your hosting A job someone sets up in the ERP
What breaks it API changes, throttling, tokens Schema changes, vendor upgrades Someone changes the report
Running cost Near zero when idle An always-on replication instance, and often a read replica: tens to hundreds of dollars a month, depending on size Near zero
Effort to do well Medium (this post) High Low, but limited

For most mid-market plants, API polling with a nightly sweep is the right default. You usually can't get database access to a hosted ERP anyway, and "hourly and exactly right" beats "real time and expensive" for nearly every question finance and operations actually ask. CDC earns its cost when you truly need minutes, not hours, and you control the database.

Part 4: Your turn

Run it yourself

Everything deploys into your own AWS account and runs serverless, so it costs about nothing when it's idle.

You'll need: uv, Node.js with the AWS Cloud Development Kit (AWS CDK) CLI at version 2.1143 or later, the AWS Command Line Interface (AWS CLI), and an AWS profile with a region configured.

git clone --tag post-02.1 https://github.com/trcollinson/crestline-supply.git
cd crestline-supply
uv sync

# On your laptop: simulate 90 days, then score both extractors against the ERP
uv run crestline sim backfill --days 90 --end 2026-10-01 --seed 42 --out ./out
uv run python eval/extraction.py

# On AWS (set AWS_PROFILE first; cdk bootstrap is once per account and region)
(cd infra && cdk bootstrap && cdk deploy CrestlineFoundation)
uv run crestline sim backfill --days 90 --end 2026-10-01 --seed 42 --upload
uv run crestline erp-api secrets
(cd infra && cdk deploy CrestlineErpApi -c erpApiMess=../mess.yaml -c erpApiClockStart=2026-08-03T05:00:00Z -c erpApiClockSpeed=24)
(cd infra && cdk deploy CrestlineCurated)
# Optional: add -c [email protected] to get an email on any failed run
(cd infra && cdk deploy CrestlineErpIngest)   # the first poll starts right away, then hourly

# About two and a half days later, once the ERP's history has played out (one ERP day per hour):
uv run crestline athena setup
uv run python eval/curated.py --margin
uv run crestline query margin-gap --engine athena
Enter fullscreen mode Exit fullscreen mode

The experiment that makes the point: give the ERP honest timestamps. Add erp: lastmodified_skips: [] to mess.yaml, then rerun the backfill and the eval (the simulator reads that setting, so the eval alone isn't enough). The naive extractor drops to 0 rows wrong. But it still keeps the 55 deleted rows, because only a full read can see a delete. The eval's slow-ERP scenario shows the other failure: a token that dies mid-read, and 93.3% wrong.

Cost: a poll run is about a tenth of a cent in Step Functions plus a few seconds of Lambda. Billed, with every layer tagged, the whole pipeline comes to about $1 a month at hourly polling. Almost all of that is Athena (each query bills at least 10 MB, and every run now does a check plus a merge per entity) and S3 requests. Lambda and Step Functions fit inside the free tier.

Teardown: destroy the poller's stack first. It holds the hourly schedule, so polling stops as soon as it's gone. Then the rest, and the two secrets:

cd infra
cdk destroy CrestlineErpIngest
cdk destroy CrestlineErpApi CrestlineCurated CrestlineFoundation
cd ..
uv run crestline erp-api secrets --delete
Enter fullscreen mode Exit fullscreen mode

The buckets are emptied for you. As in post 1, the small Lambda functions that empty them leave their log groups behind (/aws/lambda/CrestlineFoundation-* and /aws/lambda/CrestlineCurated-*); delete those with aws logs delete-log-group.

What this means for your plant

Every report, dashboard, and AI agent you build on ERP data inherits the quality of the copy underneath it. And a bad copy doesn't announce itself. It just makes your numbers quietly look like the plan.

Three questions worth asking your team this week:

  1. When did we last compare our copy of the ERP to the ERP itself? Not the row counts. The rows.
  2. Does our extract trust LastModified? If so, find out which ERP operations don't update it. There's almost always at least one.
  3. Would we notice a hard delete? If your process only asks "what changed," the answer is no.

If you don't know the answers, you're in good company. Go find out. Do it this week.

The takeaway

If you remember one thing from this post: your data is only as right as your copy of it, and a wrong copy never throws an error.

  • The obvious extractor got 30.8% of rows wrong, almost all from one ERP habit: changes that don't move LastModified.
  • Six defenses took it to zero, and the one that mattered most was the least clever: look at everything, every night, and compare.
  • On AWS, the whole thing is serverless, costs about $0 when idle, and produces a copy we can prove is exact.

Post 1 found a money-losing press brake. This post makes sure the data you'd find it with is actually true.

What's next

So far, every number in this series has come from systems people type into: an ERP and an MES. Next up is the data nobody types. The machines.

In the next post, Crestline's shop floor starts talking: a modern laser speaking Sparkplug B, an old press brake with a dumb 16-bit counter, and a powder coat line that knows its oven temperatures but has no idea which parts are inside. Most discrete shops have no historian at all, so we'll build the one they don't have, on AWS, and see what it costs per machine.

P.S. If you run it and your naive extractor does better than mine, open an issue. I want to know what you did. (And if it does worse, I really want to know.)

Tim Collinson has spent nearly 30 years building software, much of it on AWS, across manufacturing, oil and gas, and enterprise IT. Reach him at [email protected].

Top comments (10)

Collapse
 
alexshev profile image
Alex Shev •

Silent correctness failures need observability that is about data shape, not only job success. Contract checks on row counts, null-rate shifts, key uniqueness, and reconciliation totals can surface a bad extract before it becomes a downstream report.

Collapse
 
trcollinson profile image
Tim Collinson •

Thanks Alex, I completely agree. "The job succeeded" was the least useful signal in this whole build. The run that deleted 9,962 rows reported success.

The pipeline now has two shape checks before every merge. Athena has to see exactly the rows the raw files hold, and no merge can delete more than 5% of a table without someone signing off. Null-rate shifts and reconciliation totals are the obvious next layer. A cost total that suddenly drops, or a column that suddenly goes empty, would have flagged the frozen operations long before anyone read a report. Adding those to the list!

Collapse
 
alexshev profile image
Alex Shev •

The deletion guard is a useful last boundary. I would also test it with a deliberately incomplete extract and confirm the merge is blocked before any destructive change, with the proposed delete count and source snapshot identified for the reviewer.

Thread Thread
 
trcollinson profile image
Tim Collinson •

Agreed, a guard nobody has seen fire is a hope, not a guard. So the patch includes a fire drill: a sweep deliberately cut short and planted in the raw bucket, skipping the extractor’s own count check. The merge has to refuse it before anything changes (same table snapshot before and after), and the alert has to name the delete count, the snapshot it would have touched, and the raw read that caused it, plus the command to accept it after checking. It’ll be in post-02.1 this week, and the reconciliation and null-rate checks you suggested first come in the next milestone.

Thread Thread
 
trcollinson profile image
Tim Collinson •

Done: github.com/trcollinson/crestline-s...

I ran the fire drill in a real account. I planted a sweep cut down to its first page in the raw bucket, skipping the extractor’s own count check, and let the merge run. It refused before anything changed: it would have deleted 65 of 565 jobs, the table’s snapshot was the same before and after, and the email named the count, the snapshot, the raw read that caused it, and the command to accept it after checking. An hour later the next real sweep superseded the planted one and ran clean, deleting only the one cancelled duplicate it was supposed to.

The reconciliation and null-rate checks come with the machine-data post. Thanks for pushing on this.

Collapse
 
karanpulyala profile image
Karan Pulyala •

The partition projection story is the best argument for "a sweep is a complete picture" needing its own proof that I've read. One more check that fits the OData shape of the API: ask for the count up front ($count=true on the first page gives you @odata.count) and again after the last page, then refuse to treat the sweep as complete unless the keys you collected land within that range. That catches a different failure from the Athena visibility check: a sweep that stopped early because a nextLink went missing or a token died on page 40, where every page that did land is perfectly visible. Since the curated tables are Iceberg, Athena can also query the previous snapshot with FOR VERSION AS OF, so the post-merge check can be an actual diff of keys before and after each MERGE rather than just a row count. That would have shown the 9,962 deletes as a list the moment they happened, and it's a nice complement to the 5% cap for small tables where 5% is only a handful of rows.

Collapse
 
trcollinson profile image
Tim Collinson •

Thanks Karan, this is a great comment. I didn't see the gap until you pointed it out.

The check I added only proves Athena sees everything that landed in S3. It says nothing about whether everything that should have landed actually did. A sweep that quietly stops on page 40 would pass it, and then the merge would happily delete everything it never saw. Asking the ERP for its count up front with $count=true and refusing to call the sweep complete unless the keys land in that range closes exactly that hole. Two different failures, two different checks.

And I love the time travel idea. Diffing keys against the previous snapshot with FOR VERSION AS OF turns "the row count changed" into "here are the 9,962 rows that just disappeared," which is a much better email to get at midnight. You're right that it covers the small tables too, where 5% is only a handful of rows.

Both are going into the next milestone. I'll credit you when they land!

Collapse
 
karanpulyala profile image
Karan Pulyala •

Glad it was useful, and the "here are the 9,962 rows that just disappeared" email is exactly the one you want at midnight. One small thing when you wire up the count check: if the ERP is still being written to during the sweep, the count can move by a few rows between the first and last page. Taking it at both ends and allowing for that small difference keeps a busy day from failing the check for the wrong reason. Looking forward to the next milestone.

Thread Thread
 
trcollinson profile image
Tim Collinson •

Good call, and it’s going in. The start/end range alone breaks on exactly the day you’d want it to work: rows inserted ahead of the cursor and deleted behind it can leave the count unchanged while the read collects a few more. So the check gets a few rows of slack, set from what the eval actually sees on a busy run, and kept well under a page so a dropped page can never hide in it. I’m also treating a full last page with no nextLink as short on its own, since that’s the most likely way a sweep ends early. Patch is coming as post-02.1 this week. I’ll link it here.

Thread Thread
 
trcollinson profile image
Tim Collinson •

Shipped: github.com/trcollinson/crestline-s...

Two things changed from what I said above. First, calling a full last page short on its own turned out to be wrong: three real sweeps in the eval ended on exactly 500 rows, and that rule would have failed all three. So a read that ends on a full page now asks one more time for anything after its last key before it’s allowed to call itself complete. Second, the slack is ten rows, far under a 500-row page, but it isn’t measured yet. None of my runs read while the ERP was mid-write, so it rests on reasoning and a unit test. Measuring it is on the list for the next milestone.

The snapshot diff is live too: every merge that can delete records the exact keys, and a refused merge sends the midnight email with the count, the snapshot, and where the keys are. Thank you, this made the pipeline better.