Live data from Hacker News

Idempotency is easy until the second request is different

blog.dochia.dev

151–160 of 244 posts

Re: Idempotency is easy until the second request is different

#151

Earlier quoted context omitted.

If you're a client using the same idempotency key for a materially different request you have a bug.

Yes, and if you are building a payment API you need to be robust to client bugs.

Rejecting the conflicting request is being robust to improperly reused idempotency keys. There's no other reasonable answer.

Re: Idempotency is easy until the second request is different

#152
post #135

Earlier quoted context omitted.

In the case in the article, the request is being rebuilt again by the client, and may be slightly different. Typically, the server doesn't have to care about any of that if it's just "did we get something for this ID?" and either it did and errors (could be a 4xx or a 5xx depending on what it now has), or it didn't, and processes the request.

So what you propose is first you create the request payload and POST it, which generates a request-id-bound URL (but it does nothing stateful yet) and then you actually request to perform it? Because otherwise I don't see any difference.

If you must use POST with a idempotency-key, then my suggestion would be to use it like you'd use a PUT: you generate a guaranteed unique URI on the client, according to a spec agreed upon with the server, along with some idempotency-key value (UUID or whatever per the recommendation of the RFC), and then POST the request to the generated URI. If you get back 200 or 201, great! If you get an error that indicates it might not have worked or you get nothing because of a partition, send the request again. If the server had already processed the first request, the second one should 400 or 409 or something, regardless of any differences between the first and second request. If there was some sort of partial processing, then some other permanent error for that particular ID or URI should convey that.

My original point, though, was that these semantics are well-understood for PUT, so just use PUT, or use POST (with the idempotency-key header) exactly as you would PUT.

Re: Idempotency is easy until the second request is different

#154
post #123

Earlier quoted context omitted.

This is just normal concurrent programming? If two requests come in for the same idempotency key/customer reference id, only one will succeed. Use standard database transaction isolation. So one will complete with 200, one will complete with 409. It doesn't matter which. That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a…

> If the original request hasn't gotten a response yet, why is it sending a retry? Um, because connections over the Internet aren't 100% always on? Because packets can get lost? Because computers sometimes have to reboot? You're assuming that the client will always receive whatever response your server finally sends, and that the client will wait indefinitely to receive a response. Neither of those things are true. S…

You're asking questions that broadly summarize as: "what if we had an idempotency key [which must work concurrently to be useful], but it didn't work concurrently?"

Idempotency keys are themselves the solution you're looking for. If they don't work concurrently, they aren't idempotency keys. Your response in races or duplicates doesn't inherently matter in that sense, pick whatever semantics make sense for your system.

Re: Idempotency is easy until the second request is different

#155
post #123

Earlier quoted context omitted.

> If the original request hasn't gotten a response yet, why is it sending a retry? Um, because connections over the Internet aren't 100% always on? Because packets can get lost? Because computers sometimes have to reboot? You're assuming that the client will always receive whatever response your server finally sends, and that the client will wait indefinitely to receive a response. Neither of those things are true. S…

I'm not assuming anything. Let me try to reframe this for you. The case of "client sends a retry with the same idempotency key" generalizes to "multiple requests come in for the same idempotency key". These can come in spread out over time (like a traditional loop), or they could come in at once. The solution is the same either way. The problem of "how do we deal with multiple conflicting requests coming in at once"…

> I'm not assuming anything.

Sure you are. You said:

"Retries will only receive 409 if the original request was successful. If the original request failed, the server performs the operation as normal on the second request. It doesn't replay failures."

I understand all that just fine; you don't need to keep trying to "reframe" it. But what you said that I just quoted above assumes, implicitly, that if you get a second request with the same idempotency key, the original request has either failed or succeeded--because you don't even address the case where neither of those things are true. I'm asking you to address that case.

If your answer is "that will never happen", I disagree, and I explained why in response to your question about why the client would send a retry if it hasn't received a response to the original request. You could answer, I guess, that you still think that would never happen--and I would still disagree. But at least that would be an answer. So far all you've done is "reframe" something that I already understand and wasn't asking about.

Re: Idempotency is easy until the second request is different

#156

Earlier quoted context omitted.

To be honest, I liked your original response about returning a 409 - it's not something I'd done before and I like how it keeps things simpler. But your follow up responses here are making me rethink. Now you have to have all these special cases where the original request is still in process. I think or assertion of "99% are simple POST operations" is bullshit. For the times where idempotency is hard and really matte…

> special cases where the original request is still in process This isn't a special case, and it's the same problem if you want to replay the original response on conflict. If the original request isn't complete, what are you going to replay ?

> If the original request isn't complete, what are you going to replay?

Who says you have to replay? If you get a second request with the same idempotency key, and the original request is still in process, why not just send the client a response that says so?

Re: Idempotency is easy until the second request is different

#157

This is all way too much. If you see a duplicate idempotency key, skip the replay and always return 409. This becomes a client problem. Clients already need to help enforce idempotent contracts; "check for conflict response" is not an onerous imposition. I've built multiple ecommerce APIs with this approach and they work great. No heroic measures required. You can often satisfy this contract with a unique constraint;…

[deleted]

Re: Idempotency is easy until the second request is different

#158
post #113

Earlier quoted context omitted.

> That's usually solved with traditional database transactions. That doesn't answer my question. What response do you return to the client in the case I described?

This is just normal concurrent programming? If two requests come in for the same idempotency key/customer reference id, only one will succeed. Use standard database transaction isolation. So one will complete with 200, one will complete with 409. It doesn't matter which. That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a…

> That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a retry?

Because it hasn't gotten a response yet. That's got to be far and away the most common reason any request gets retried in any context.

Re: Idempotency is easy until the second request is different

#159
post #150
post #142

Earlier quoted context omitted.

In my opinion, the idea of idempotency is to accept both requests, but only one is actioned (and the requester is non-the-wiser about which). Otherwise, you're just recreating database transactions - something that doesn't need to be named idempotency. And you haven't considered multiple servers in your scenario - what if two requests meant to be idempotent with each other arrived at different servers?

How would the requester get notified if it doesn’t know which request succeeded? Is it listening for events? And at the sake of repeating the above commenter, you solve the multiple server by serializing somewhere, because you ultimately need a lock on something. You can also perform the operation in both places and then reconcile the state later but that’s a lot more complex.

> How would the requester get notified if it doesn’t know which request succeeded?

By sending a third request and getting a response that reveals the state of the system.

Re: Idempotency is easy until the second request is different

#160
A queue with a pre-flight check can assist with this quite well. Requests are queued and executed ASAP and you use a checker function to verify whether future requests/jobs can run. Just check that idempotency key and if it's already in the queue/db, skip it (and if need be, log the double attempt for future forensics).
Post reply on HN