メインコンテンツにスキップ

Scheduling through an API without publishing twice

What an idempotency key has to cover, why a schedule needs an instant and a zone, and how to design a retry that a platform outage cannot turn into a duplicate.

この記事はまだあなたの言語では書かれていません。英語版を表示しています。

公開業務調査デスクが執筆プラットフォーム文書デスクがレビュー

If you are scheduling posts from your own code, through any publishing API, the interesting failure is never the request that fails cleanly. It is the request whose response you never saw. Somewhere between your timeout and the platform, a post may or may not exist, and the next thing your program does decides whether the account gets one post or two.

This is a short account of what has to be true for that decision to be safe, whether the scheduling API you call is one you bought or one you wrote.

The key has to be yours, and it has to mean one intention

An idempotency key is a value you generate before the first attempt and reuse on every retry of the same intention. Two properties matter more than the format.

  • It is generated by the caller, before the call. A key minted by the server on receipt cannot protect the case where the request arrived and the response did not.
  • It identifies one intention, not one payload. If a scheduled time is corrected, that is the same intention and the same key. If a second post is written, that is a new intention and a new key, even when the text is identical.

The second property is where most designs go wrong. Hashing the request body looks tidy and then quietly refuses a legitimate second post that happens to repeat yesterday wording, or accepts a duplicate because a stray whitespace change made a new hash.

key = uuid()                       # once, before the first attempt
store(key, intention, status="pending")
response = POST /schedules  Idempotency-Key: key
# timeout, connection reset, process killed:
# retry the same call with the same key, never a new one
# the server returns the original outcome rather than acting again
The shape of a safe retry. The key is minted once, stored with the intention, and reused unchanged.

A scheduled time is an instant plus a zone, never a local string

A schedule that stores nine in the morning without a zone is not a time. It is a wish that resolves differently on the server, on the client, and again after a daylight saving change.

Send both: the exact instant, and the IANA zone the person meant. The instant makes the schedule unambiguous. The zone is what lets the system answer the question a person will eventually ask, which is whether a post moved because the clocks changed or because somebody edited it. A stored offset cannot answer that, because an offset is a consequence of a zone and a date, not a substitute for either.

The dangerous hours are the ones that repeat or vanish

On a spring transition a local time can be absent, and on an autumn transition it can occur twice. Any scheduler that treats local time as authoritative will either skip a post or publish it twice on exactly two days a year, and both will be reported as random.

The platform beneath you offers less than you think

It is tempting to assume the publishing platform will protect you. Read what is actually documented.

Documented behaviour that shapes a retry
What the platform documentsWhat your code must therefore own
LinkedIn states that post deletions are idempotent, returning 204 for a post already deletedCleanup can be repeated safely. Creation has no such promise, so publish once is your guarantee to build, not theirs.
LinkedIn documents a PUBLISH_FAILED state and states that an edit is required to reattempt publishingA retry loop that resends the same call forever will never clear that state. Detect it and hand it to a person.
Instagram separates container creation from publishing, and suggests checking a container status roughly once a minute for up to five minutesPublishing is a small state machine, not a call. Persist which step you reached so a restart resumes rather than restarts.
Google documents refresh tokens expiring on several conditions, including six months of disuse and revocation by the userAuthentication can fail in the middle of a retry sequence. Distinguish a credential failure from a publishing failure, because only one of them should ever be retried.

Write the record before the call, not after it

A record written after a successful call cannot describe the only case that matters. Write the attempt first, with the key, the intention and the target account, then update it with the outcome. If the process dies mid flight, the record is what tells the next run that something might already exist.

Make that record immutable once written. An audit trail that can be edited to look tidy is not an audit trail, and the first time somebody asks whether a post went out twice, the tidy version is worth nothing.

  • The idempotency key, so a retry is recognisable as a retry.
  • The exact content sent, so a read back against the platform can be conclusive.
  • The scheduled instant and the zone, so a moved post can be explained.
  • The platform identifier once there is one, because that is the only proof the post exists.
  • The final state, including unknown, which is a real state and must be storable.

Test the five failures that actually happen

  1. The worker dies after the platform accepted the post but before the response was recorded.
  2. The platform times out, and the post turns out to exist.
  3. A webhook or callback is delivered twice for the same publication.
  4. The credential is revoked between scheduling and publication.
  5. A daylight saving transition moves the local time the person chose.

If those five are not in the test suite, they are in production instead. Each one is cheap to simulate and expensive to meet for the first time on a client account.

出典