Handling with Idempotency-Key on RDBMS
Mathematics: f(f(x)) == f(x)
Programming: "example".toUpperCase();
Http: GET method
Note: El estado no cambia.
Side effects ocurren solo una vez.
Beneficios: reintentos seguros, trata duplicados, ayuda integridad data
- Local State Mutations
- Foreign State Mutations
Note:
- Busco Idempotency-Key en Storage
- Si esta devuelvo Response
- Si no, actualizo estado y Storage con Response
Note: Hacer cambios locales sin perder de vista cambios remotos
Account Idempotent API
Note:
Acciones para cada Request
- Idempotency-Key
- Atomic Phases (RDBMS & ACID)
- Recovery Points
- Directed Acyclic Graph (DAG)
CREATE TABLE idempotency_keys (
id BIGSERIAL PRIMARY KEY,
idempotency_key TEXT NOT NULL,
-- parameters of the incoming request
request_method TEXT NOT NULL,
request_params TEXT NOT NULL,
request_path TEXT NOT NULL,
-- for finished requests, stored status code and body
response_code INT NULL,
response_body TEXT NULL,
recovery_point TEXT NOT NULL
);
public enum RecoveryPoint {
RECOVERY_POINT_STARTED("started"),
RECOVERY_POINT_ACCOUNT_CREATED("account_created"),
RECOVERY_POINT_DEPOSIT_CREATED("deposit_created"),
RECOVERY_POINT_FINISHED("finished");
...
}
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.SERIALIZABLE)
public void mutate(String key, Supplier<Action> function) {
...
try {
// executes a state mutation
Action action = function.get();
// updates request life cycle state
// with a new recovery point, response, etc.
action.execute(key);
} catch (Exception e) {
// handle exception
...
}
}
...
while (walk) {
IdempotencyKeys idempotencyKey = idempotencyKeyService.findByKey(key);
switch (idempotencyKey.getRecoveryPoint()) {
case RECOVERY_POINT_STARTED:
//here we perform a new mutation and set new state
stateMutationService.mutate(key, tx2);
break;
case RECOVERY_POINT_ACCOUNT_CREATED:
//here we perform a new mutation and set new state
stateMutationService.mutate(key, tx3);
break;
case RECOVERY_POINT_DEPOSIT_CREATED:
//here we perform a new mutation and set new state
stateMutationService.mutate(key, tx4);
break;
case RECOVERY_POINT_FINISHED:
walk = false;
break;
default:
throw new IllegalStateException(
String.format("Bug! Unhandled recovery point %s'", idempotencyKey.getRecoveryPoint()));
}
}
Successfully Request
Note:
Network Issue on Client - Account API
Note:
Network Issue on Account API - Mambu Fake API
Note:
Network Issue on Account API - Cbu Fake API
Note:
Note:
- Distributed transactions are hard
- Client and Services must do their homework!
- We like ACID ♡
Note:
Client tiene q implementar retry, timeout y compensaciones
Service tiene q ofrecer compensaciones e implementar idempotencia
SAGA vs Idempotencia
Redis vs RDBMS
- Implementing Stripe-like Idempotency
- Avoiding Double Payments in a Distributed Payments System
- Asynchronous background jobs with Job Drain Pattern