25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

idempotence.md 7.5 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. # Idempotence
  2. Imdempotence (which you may read a formal definition of on [Wikipedia](https://en.wikipedia.org/wiki/Idempotence), when we are talking about messaging, is when a message redelivery can be handled without ending up in an unintended state.
  3. ## Delivery guarantees[^1]
  4. [^1]: The chapter refers to the [Delivery guarantees](https://github.com/rebus-org/Rebus/wiki/Delivery-guarantees) of rebus, which I think is described very good.
  5. Before we talk about idempotency, let's talk about the delivery of messages on the consumer side.
  6. Since CAP is not a used MS DTC or other type of 2PC distributed transaction mechanism, there is a problem that at least the message is strictly delivered once. Specifically, in a message-based system, there are three possibilities:
  7. * Exactly Once(*)
  8. * At Most Once
  9. * At Least Once
  10. Exactly once has a (*) next to it, because in the general case, it is simply not possible.
  11. ### At Most Once
  12. The At Most Once delivery guarantee covers the case when you are guaranteed to receive all messages either once, or maybe not at all.
  13. This type of delivery guarantee can arise from your messaging system and your code performing its actions in the following order:
  14. ```
  15. 1. Remove message from queue
  16. 2. Start work transaction
  17. 3. Handle message (your code)
  18. 4. Success?
  19. Yes:
  20. 1. Commit work transaction
  21. No:
  22. 1. Roll back work transaction
  23. 2. Put message back into the queue
  24. ```
  25. In the sunshine scenario, this is all well and good – your messages will be received, and work transactions will be committed, and you will be happy.
  26. However, the sun does not always shine, and stuff tends to fail – especially if you do enough stuff. Consider e.g. what would happen if anything fails after having performed step (1), and then – when you try to execute step (4)/(2) (i.e. put the message back into the queue) – the network was temporarily unavailable, or the message broker restarted, or the host machine decided to reboot because it had installed an update.
  27. This can be OK if it's what you want, but most things in CAP revolve around the concept of DURABLE messages, i.e. messages whose contents is just as important as the data in your database.
  28. ### At Least Once
  29. This delivery guarantee covers the case when you are guaranteed to receive all messages either once, or maybe more times if something has failed.
  30. It requires a slight change to the order we are executing our steps in, and it requires that the message queue system supports transactions, either in the form of the traditional begin-commit-rollback protocol (MSMQ does this), or in the form of a receive-ack-nack protocol (RabbitMQ, Azure Service Bus, etc. do this).
  31. Check this out – if we do this:
  32. ```
  33. 1. Grab lease on message in queue
  34. 2. Start work transaction
  35. 3. Handle message (your code)
  36. 4. Success?
  37. Yes:
  38. 1. Commit work transaction
  39. 2. Delete message from queue
  40. No:
  41. 1. Roll back work transaction
  42. 2. Release lease on message
  43. ```
  44. and the "lease" we grabbed on the message in step (1) is associated with an appropriate timeout, then we are guaranteed that no matter how wrong things go, we will only actually remove the message from the queue (i.e. execute step (4)/(2)) if we have successfully committed our "work transaction".
  45. ### What is a "work transaction"?
  46. It depends on what you're doing 😄 maybe it's a transaction in a relational database (which traditionally have pretty good support in this regard), maybe it's a transaction in a document database that happens to support transaction (like RavenDB or Postgres), or maybe it's a conceptual transaction in the form of whichever work you happen to carry out as a consequence of handling a message, e.g. update a bunch of documents in MongoDB, move some files around in the file system, or mutate some obscure in-mem data structure.
  47. The fact that the "work transaction" is just a conceptual thing is what makes it impossible to support the aforementioned Exactly Once delivery guarantee – it's just not generally possible to commit or roll back a "work transaction" and a "queue transaction" (which is what we could call the protocol carried out with the message queue systems) atomically and consistently.
  48. ## Idempotence at CAP
  49. In the CAP, the delivery guarantees we use is **At Least Once**.
  50. Since we have a temporary storage medium (database table), we may be able to do At Most Once, but in order to strictly guarantee that the message will not be lost, we do not provide related functions or configurations.
  51. ### Why are we not providing(achieving) idempotency ?
  52. 1. The message was successfully written, but the execution of the Consumer method failed.
  53. There are a lot of reasons why the Consumer method fails. I don't know if the specific scene is blindly retrying or not retrying is an incorrect choice.
  54. For example, if the consumer is debiting service, if the execution of the debit is successful, but fails to write the debit log, the CAP will judge that the consumer failed to execute and try again. If the client does not guarantee idempotency, the framework will retry it, which will inevitably lead to serious consequences for multiple debits.
  55. 2. The implementation of the Consumer method succeeded, but received the same message.
  56. The scenario is also possible here. If the Consumer has been successfully executed at the beginning, but for some reason, such as the Broker recovery, and received the same message, the CAP will consider this a new after receiving the Broker message. The message will be executed again by the Consumer. Because it is a new message, the CAP cannot be idempotent at this time.
  57. 3. The current data storage mode can not be idempotent.
  58. Since the table of the CAP message is deleted after 1 hour for the successfully consumed message, if the historical message cannot be idempotent. Historically, if the broker has maintained or manually processed some messages for some reason.
  59. 4. Industry practices.
  60. Many event-driven frameworks require users to ensure idempotent operations, such as ENode, RocketMQ, etc...
  61. From an implementation point of view, CAP can do some less stringent idempotence, but strict idempotent cannot.
  62. ### Naturally idempotent message processing
  63. Generally, the best way to deal with message redeliveries is to make the processing of each message naturally idempotent.
  64. Natural idempotence arises when the processing of a message consists of calling an idempotent method on a domain object, like
  65. ```
  66. obj.MarkAsDeleted();
  67. ```
  68. or
  69. ```
  70. obj.UpdatePeriod(message.NewPeriod);
  71. ```
  72. You can use the `INSERT ON DUPLICATE KEY UPDATE` provided by the database to easily done.
  73. ### Explicitly handling redeliveries
  74. Another way of making message processing idempotent, is to simply track IDs of processed messages explicitly, and then make your code handle a redelivery.
  75. Assuming that you are keeping track of message IDs by using an `IMessageTracker` that uses the same transactional data store as the rest of your work, your code might look somewhat like this:
  76. ```c#
  77. readonly IMessageTracker _messageTracker;
  78. public SomeMessageHandler(IMessageTracker messageTracker)
  79. {
  80. _messageTracker = messageTracker;
  81. }
  82. [CapSubscribe]
  83. public async Task Handle(SomeMessage message)
  84. {
  85. if (await _messageTracker.HasProcessed(message.Id))
  86. {
  87. return;
  88. }
  89. // do the work here
  90. // ...
  91. // remember that this message has been processed
  92. await _messageTracker.MarkAsProcessed(messageId);
  93. }
  94. ```
  95. As for the implementation of `IMessageTracker`, you can use a storage message Id such as Redis or a database and the corresponding processing state.