fix(api): retry commands that clash with a concurrent write
Services run commands through a CommandRunner that runs one again (up to 5 times, waiting a little longer each time) when its aggregate changed underneath, e.g. a saga writing to the same ticket. Each command loads its aggregate fresh and stores its events in one transaction, so a ConcurrencyError means nothing was stored and running it again is safe. People no longer get a 409 for it, and the e2e suite stops flaking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,8 @@ Las reglas no son una convención: las comprueba `@nx/enforce-module-boundaries`
|
||||
- `domain` solo puede importar `@nestjs/common` (por `@Injectable()` de las factories) y `@nestjs/cqrs` (por `AggregateRoot`).
|
||||
Nada de TypeORM, Mongoose, Redis, GraphQL, Angular, NgRx ni Apollo.
|
||||
- `application` no puede importar adaptadores (TypeORM, Mongoose, Redis, Apollo…).
|
||||
- Los buses CQRS (`CommandBus`, `QueryBus`) solo los usan los `*.service.ts` de `application`; todo lo demás pasa por
|
||||
- Los buses CQRS solo los usan los `*.service.ts` de `application` (los comandos, a través del `CommandRunner`, que
|
||||
los reintenta si chocan; las consultas, con el `QueryBus`); todo lo demás pasa por
|
||||
un servicio: resolvers, controllers, temporizadores (recordatorios, papelera; por eso viven en `presenters`), el
|
||||
handler de eventos de las automatizaciones y el directorio de usuarios del tracker (`IdentityService`). La única
|
||||
excepción es el `EventsBridge`, que ejecuta los comandos que devuelven las sagas.
|
||||
@@ -109,7 +110,10 @@ sequenceDiagram
|
||||
- **Guardar**: el `EventStorePublisher` sustituye al publisher del `EventBus`, así que `commit()` escribe en la tabla
|
||||
`events`. `commitAndWait()` espera a que la escritura termine (el `commit()` de Nest no devuelve la promesa).
|
||||
- **Concurrencia optimista**: `UNIQUE(stream_id, position)`. Si dos peticiones modifican el mismo agregado a la vez, la
|
||||
segunda falla con `ConcurrencyError` (código GraphQL `CONFLICT`).
|
||||
segunda falla con `ConcurrencyError`. El `CommandRunner` (`shared/application`) la repite sobre el estado nuevo hasta
|
||||
5 veces, con una pequeña espera creciente: cada comando carga su agregado de cero y guarda sus eventos en una sola
|
||||
transacción, así que un choque significa que no se guardó nada. Solo si sigue chocando llega al cliente (código
|
||||
GraphQL `CONFLICT`, HTTP `409`). Las sagas tienen su propio reintento en el `EventsBridge`.
|
||||
- **Lookups**: lo que no se puede comprobar dentro de un agregado vive en tablas aparte: claves de proyecto únicas
|
||||
(`project_keys`), numeración de tickets (`issue_sequences`) y nombres de release únicos (`release_names`).
|
||||
- **Ids**: todas las tablas tienen `id uuid` (`gen_random_uuid()`); lo que hace única una fila de lookup es una
|
||||
|
||||
Reference in New Issue
Block a user