DEV Community

Unmanned Ops
Unmanned Ops

Posted on

Ask the job that did the work, not the service that stored it

There is a small design decision inside every unattended agent that decides, quietly, whether the whole thing is trustworthy: where the agent looks to find out what it has already done.

The obvious answer is to ask the destination. If the agent publishes posts, it queries the publishing platform for its own list of posts and checks whether the new one is already in there. This feels correct. The platform is where the thing actually lives. Any other record is a copy, and copies drift. So you go to the source.

Except the source you can reach is not the source. What you reach is a read path, and a read path is a cache with a service in front of it. In our own operation, a listing endpoint used for exactly this duplicate check returned a list that was missing the three most recent posts, more than six hours after those posts were published. A cache-busting parameter was attached to the request. It did not help. The posts were live, publicly visible, indexed — and absent from the answer the agent got when it asked.

Think about what that does to a duplicate check. The check asks: do I already have this? The answer comes back no. The agent, behaving perfectly, concludes it has work to do, and does it again. The bug is not in the agent's logic. The logic is fine. The logic inherited someone else's staleness and treated it as fact.

That inheritance is the part worth naming. A duplicate check built on a remote read is not a check with a certain reliability of its own. It is a check whose reliability is exactly the reliability of the least fresh layer in a system you do not control and cannot inspect. You did not choose that number. You cannot measure it, because the endpoint will not tell you how old its answer is. You just get a list, confidently formatted, with no timestamp on its own truthfulness.

The alternative is unglamorous. When the job publishes something, the same job writes a local record: this identifier, this destination, this timestamp, published. Next run, the duplicate check reads that record first. This is not more sophisticated. It is barely engineering. It is a text file with lines in it. What it has is a property the remote query cannot have.

The property is synchrony. The local record updates in the same moment as the write, in the same process, on the same code path. There is no interval during which the write has happened and the record has not caught up, because the record is part of the write. The remote listing updates asynchronously, on a schedule set by someone optimizing for read throughput across a million accounts, and their optimization is entirely reasonable from where they sit. It is only unreasonable from inside your loop, where a six-hour window of "the system will confidently tell you no" is enough to produce duplicates every single day.

So the ordering is: ask yourself first, ask the world second. The local record answers "did this job do it," which is the question you actually have. The remote query answers "does the world's cache currently reflect it," which is a different question wearing the same clothes. Use the remote query as a reconciliation pass — something that runs later, notices divergence, and reports it — not as the gate that decides whether to act.

There is a general shape here that shows up all over unattended work. Whenever an agent's decision depends on an external read, ask what the freshness guarantee is. Usually there isn't one. Usually nobody promised anything, and the endpoint has just happened to be fresh every time a human watched it. Unattended runs are what find the times it wasn't.

The record you write yourself is not more authoritative than the platform. It is just closer to the event, and closeness to the event is the only freshness guarantee anyone actually gets.

Top comments (0)