# Retries and idempotency

Source: <https://docs.gjold.com/notes/retries>

A request that moves money can fail after it has done its work: the capture went through, but the answer never reached you. Retry it so that it can't happen twice.

## Three outcomes

A request ends in one of three ways, and only one of them leaves you in doubt:

1. **An answer of 200.** It went through.
2. **An answer of 4xx.** It did nothing, and doing it again will not help, but for a `408`. See [Errors](https://docs.gjold.com/v1/errors).
3. **No answer, or a 5xx.** The connection broke, your client timed out, or we failed. It may have gone through, or not.

The third is the one to plan for. With no answer, or a `502`, `503`, or `504`, which come from in front of us, retry with the same `Idempotency-Key`, so that the request runs at most once, however many times you send it. A `500` is an answer of ours, which a retry with the same key only repeats: synchronize, and if the act is not there, decide afresh from the transaction as seq returns it, and send the request again with a new key.

In v1, most failures are a `500`: both those that did nothing, such as a stale index or a declined charge, and those where we or the acquirer failed part of the way. The message says which, but a `500` alone does not, so synchronize after each.

In v2, a request that did nothing because of the object's state is a `409`, to retry after reading the object again, and a charge's decline is a `402`, whose `transient` may succeed later. See [Errors](https://docs.gjold.com/v2/errors).

## Keys

Capture, refund, and charge take an `Idempotency-Key` in both versions, and so does void in v2. Make one for each such request, store it with what the request is for, and only then send it:

```
key := LoadOrCreateKey(order, "capture")  -- stored before the first try
for try := 1 to 6 do
    res := POST(url, body, "Idempotency-Key: " ‖ key)
    if res is 200 then return res
    if res is 4xx and not 408 then fail(res)
    if res is 500 then break              -- our answer: a retry repeats it
    Sleep(2^(try-1) seconds)              -- no answer, 408, 502, 503, or 504
end for
Synchronize()                             -- still in doubt: find out from seq
```

The key must outlive your process. If your server crashes after sending a capture, the retry after the restart must send the same key, or it is a second capture. A key that is made fresh on each try is no key at all.

Use a key for one request only. We don't compare a replay with the first request, so a key reused for another capture answers with the first capture's result, and the second capture never happens. Make keys random, with 128 bits or more, rather than from your order numbers, so that they can't meet another's. And keep a retry within 24 hours of the first try, as we forget a key after that. See [Idempotency](https://docs.gjold.com/v1/idempotency).

## The index

A capture or a refund also carries the transaction's `"index"`: the number of acts it had when you decided to capture. It catches what a key can't, two different requests made from the same state:

- Two of your servers, each seeing an uncaptured order, both capture it. Both send `"index": 0`, and only the first goes through.
- Someone captures in the dashboard while your code does.

A capture with a stale index fails, and changes nothing. Synchronize, look at the transaction again, and decide afresh: it may already be captured.

In v1, a stale index is `500 transaction database error: index mismatch`, and a missing `"index"` is taken as 0, which is right only for a transaction's first act, so always send it. In v2, it is `409 index mismatch`, and the `"index"` is required.

Keys and the index work together. The key makes a retry of one request safe, and the index makes two requests from the same state safe. A retry with the same key and the same index is a replay, and never fails for its index.

## Timeouts

Capture, refund, charge, and v2's void wait for the acquirer before they answer, and the acquirer can be slow:

| Request | Waits for | Up to |
|---|---|---|
| Capture, refund, v2's void | One call to the acquirer | 1 minute with Clearhaus, 10 minutes with Nets |
| Charge subscriber | One call, or two with `"autocapture"` | 2 minutes with Clearhaus, 20 minutes with Nets |
| Create link, renew subscriber, seq, and v2's delete subscriber | Our own databases | Seconds |
| v2's delete payment link | A payment in progress, if there is one | Seconds, or minutes while a payer is in 3-D Secure |

Your HTTP client's timeout is likely shorter, which does no harm with a key: when it times out, retry with the same key, and the retry waits for the first request to finish and answers with its result. Without a key, a timeout is how a charge happens twice.

Don't time out sooner than 30 seconds, as most calls to an acquirer take a few seconds, and a retry storm on a slow acquirer makes it slower.

## In doubt for good

If your retries run out, and you still have no answer, the request may have gone through. Don't send it again with a new key. Synchronize: the transaction's acts, or the charge in seq, say whether it happened. A charge that did not happen is not in seq, so wait a minute and look again before you conclude that.
