About idempotency

From reading the docs/wiki I gather that EventId together with an ExpectedVersion other than Any is the vehicle to achieve idempotency.

  1. Is it safe to assume that in most cases the EventId is actually the CausationId, i.e. the unique identifier of the message or action that caused the appending of events to happen? From what I’ve read I’m inclined to say yes.

  2. If multiple events need to be appended to the stream, do I need to use a transaction (as in the use of StartTransaction) to make sure that all or nothing is committed and not suffer the “Some of the events were previously committed” remark, or is AppendToStream(Async) already taking care of treating the whole as a transaction behind the scene? From what I’ve read I’d say AppendToStream alone is enough.

  3. If multiple events need to be appended to the stream, can they all carry the same EventId? From what I’ve read I’m inclined to say yes.

Help me validate my assumptions,

Yves.

  1. Is it safe to assume that in most cases the EventId is actually the CausationId, i.e. the unique identifier of the message or action that caused the appending of events to happen? From what I’ve read I’m inclined to say yes.

I am not sure I follow the question here.

  1. If multiple events need to be appended to the stream, do I need to use a transaction (as in the use of StartTransaction) to make sure that all or nothing is committed and not suffer the “Some of the events were previously committed” remark, or is AppendToStream(Async) already taking care of treating the whole as a transaction behind the scene? From what I’ve read I’d say AppendToStream alone is enough.

AppendToStreamAsync will handle as a batch if multiple events you do not need a transaction for this.

  1. If multiple events need to be appended to the stream, can they all carry the same EventId? From what I’ve read I’m inclined to say yes.

Why would they all have the same eventid? I have not tried this edge case but my guess would be that it might cause some interesting emergent behaviours.

Cheers,

Greg

An additional question: How do you determine “the events were previously committed”? Things need to be identical at the binary level? Does this include the metadata?

We check the ids. Ids are supposed to be unique to an event.

The general use of idempotency is as follows. There are a slew of problems that can happen between the time a client sends something and the time that it gets back a message saying that its been persisted (network drops, power outages …). The role of the client is to simply retry an operation if they receive anything but a success or a failure from us. When we say “retry” we mean retry the exact same message. All cases of retrying the exact same message are completely handled.

btw: I will probably add a check if ids in message are not unique to give an error if its not already there.

Cheers,

Greg

On 1: Well, I need a stable Guid because if I generate a new one each time I append those events how am I to get idempotency? Or am I missing something here?
On 3: Well, same problem as above really. No stable Guid, no idempotency, right? Unless for some reason EventId is not part of checking “events have been committed before”.

Oh, I see. Hm … let me run a use case by you:

  1. I receive a command (ActivateBottlingMachine) with a causation id of “ed0577b2-d317-404c-99c0-49c7d734cd31”

  2. Some code gets executed and two events get produced (ActivatedBottlingMachine, BottleCountResetOnMachine)

  3. I assign a guid to each event: ActivatedBottlingMachine - “06d1cda8-6b85-434d-87fe-7370633770f8”, BottleCountResetOnMachine - “672e2162-288d-457e-8a62-9e5207955db9”

  4. I append it to the stream “bottlingmachine/BT-EHR-1240” using the AppendToStream/Async method

  5. Meanwhile, before the event store gets back to me to ack success, my process dies.

  6. My process comes back up and retries the ActivateBottlingMachine command with causation id of “ed0577b2-d317-404c-99c0-49c7d734cd31”

  7. Some code gets executed and two events get produced (ActivatedBottlingMachine, BottleCountResetOnMachine)

  8. I assign a guid to each event: ActivatedBottlingMachine - “8ef2457a-96cf-4f0b-869f-b348835b2edf”, BottleCountResetOnMachine - “dd1ae558-c90b-48b0-90ad-f3e5751b9440”

  9. I append it to the stream “bottlingmachine/BT-EHR-1240” using the AppendToStream/Async method

  10. After the event store acks success, I now have 4 events in there instead of 2.

Now, I’m not saying there aren’t any natural idempotency cases (there are a lot). But I think you see how retrying the same command is causing new identifiers to be assigned to the events, hence my thinking of using the causation id (and if it weren’t a guid I’d use causation id + an event ordinal). If I want to make this stable, I’d somehow have to make the event id generation deterministic (e.g. allocating guids in advance for a given command) in the “event” (LOL) my process dies (however unlikely this may be).

Maybe my approach and thinking is naive, so do enlighten me.

Regards,

Yves.

How do you rhyme that with “It is possible to reuse an EventId across streams whilst maintaining idempotence”?

The event store handles the idempotency of events not of your commands. A common trick if you want to provide idempotency all the way out to the commands is to put the future event ids in the commands (or a deterministic seed to generate ids from) think random.next() where you use the same seed.

Another option is to make persistent the events you want to save locally as part of writing to the store. That way if you go down you will still have them to retry when you come up (eg at least once messaging). In practice though this is not generally an acceptable solution.

Cheers,

Greg

I should add another common way to provide idempotency on commands is to put them in the eventstore as well :slight_smile:

But that’d be cross stream persistence (command stream, affected stream)? Or would you put them in the stream the command affects?

if I were doing it that way I would use it as a command queue for processing i would be reacting to the commands

That’s what I figured from your previous comment. The emphasis is on event idempotency, not command idempotency. I figure I’m not the only one that made the wrong assumption.

Any particular reason you chose Guid instead of String for EventId? Because CausationId+Ordinal or other deterministic event id generation would make this scenario way easier to support. “Guid” makes you bend in odd ways in this case.

mostly fixed size.

Enlightening thread.

@yves - I’m reading the situation you are trying to counteract as being: a network partition after the event store has received the events but before your process receives the ack and your process dies.

Assuming this is very rare, can you detect this situation and delegate resolution to the client/user?

  • Refresh the view to the latest.

  • Evaluate whether the command makes sense anymore; is it still legal given the current view.

  • If you can’t determine automatically, inform the user of the situation and give them the choice to resubmit.

I suppose a fully automated scenario you’re still in the same boat.

You’re assuming human interaction :wink: Command was received & accepted, now have to deal with.

Yeah. Considered m2m by the time I reached my last sentence. :-]

“But that’d be cross stream persistence…”

Is this not possible or not advisable?

If you stored the command, with it’s fixed id, transactionally with the events wouldn’t this solve the problem? On concurrency error, you’d know the events were stored, assuming that transactions in GES work this way.

They don’t, by design, AFAIK. None of the options appeal to me.

The idempotency in EventStore is geared more to ensuring that if EventStore crashes, retries from ClientAPI are handled correctly and transparently for you, as retry happens inside ClientAPI reusing events (and their EventId) that you originally provided.

But if you need to use this mechanism to ensure idempotency works also in case YOUR code crashes – why not use some kind of hash (MD5?) on content of event to generate EventId – that will ensure unique event IDs and will be deterministic.

Thanks Andrii, that is a really interesting idea. Since both MD5 and a Guid are 16 bytes that seems very plausable. Combined with the causation id I think that’s “unique enough”.

Strictly speaking EventIDs are not required to be completely unique, though it’s good to ensure that events in the SAME STREAM have unique IDs. Also, if you are using explicit expected versions, EventID will be checked only in sutiation where idempotency CAN occur.

See https://github.com/EventStore/EventStore/blob/master/src/EventStore/EventStore.Core/Services/Storage/ReaderIndex/IndexWriter.cs#L186 for complete details.