Files
Flash5/flash-extensions/flash-ext-data-core/docs/README.md
T
Zakaria El OrcheandClaude Opus 5 0194470c1f feat(data): actually invoke TxSynchronization.beforeCommit, and document the contract
beforeCommit(boolean) has been part of the TxSynchronization API from the
start and was invoked by nothing, in either manager — anyone implementing it
got silence, the same failure class as the dropped afterCommit callbacks in
the previous commit. Left out of that one because fixing it is a design
decision rather than a restored behaviour; this is that decision, written
down.

It now runs immediately before the real commit, with the transaction still
active and its session/connection still bound, which is the whole reason to
have a hook on this side of the commit: it can still write through the same
resource and land in the same atomic unit. Skipped when the transaction is
already rollback-only, since there is no commit to precede.

Throwing from it vetoes the commit: the transaction rolls back, the surviving
callbacks hear ROLLED_BACK, and the exception propagates. Without that, a hook
running before the commit would be strictly less useful than one running
after. A commit that fails on its own now takes the same path instead of
completing silently with no callback at all, and a rollback that also fails is
attached as a suppressed exception rather than replacing the one that explains
the failure.

Documented on the interface itself and in flash-ext-data-core/docs/README.md:
which hook sits on which side of the commit, what each may still touch, what
throwing does, and the per-transaction scoping rule from the previous commit.

Tests: 6 more (3 per manager) — runs inside the transaction with the resource
still bound, receives the read-only flag, skipped on a rollback-only
transaction, and vetoes the commit when it throws. 37 across the two managers
now, all green, reactor verify passes the 80% Jacoco gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:33:05 +00:00

125 lines
5.2 KiB
Markdown

# flash-ext-data-core
Core comune per il layer dati di Flash.
## Scopo
Questo modulo definisce il contratto transazionale condiviso tra le implementazioni backend.
Non parla con Hibernate o JDBC direttamente: espone solo astrazioni e un runtime minimale.
## Componenti
- `TxDefinition`: metadata immutabile della transazione.
- `TxStatus`: stato runtime restituito dal manager.
- `TxManager`: contratto per `begin`, `commit`, `rollback`.
- `Tx`: orchestration runtime e stack transazionale per thread.
- `ResourceRegistry`: storage thread-local di risorse e synchronizations.
- `Repository<T, ID>`: base repository auto-transazionale.
- `Spec<T>`: predicato componibile.
- `Query<T>`: oggetto query con spec, sort e paging.
- `SpecBuilder<T>`: DSL fluente per costruire spec tipizzate.
- `RepositorySupport<T, ID>`: helper interno condiviso.
- `TransactionPropagation`: semantica di propagazione.
- `TransactionIsolation`: livello di isolamento.
- `TxSynchronization`: hook lifecycle (vedi sotto).
## Modello di esecuzione
Il flusso è:
1. `Tx.call(definition, work)` chiama `TxManager.begin(definition)`.
2. Il `TxManager` crea un `TxStatus` backend-specific.
3. Lo status viene pushato nello stack thread-local.
4. Il lavoro usa `Tx.resource(Class)` per ottenere la risorsa corrente.
5. A fine lavoro `Tx` decide tra `commit` e `rollback`.
6. Lo stack viene poppato e il thread-local viene pulito se vuoto.
## Propagation supportata
- `REQUIRED`: usa la tx attiva oppure ne apre una nuova.
- `REQUIRES_NEW`: sospende la tx corrente e apre una nuova tx.
- `SUPPORTS`: se esiste una tx attiva si aggancia, altrimenti esegue senza tx.
- `NOT_SUPPORTED`: sospende la tx corrente ed esegue senza tx.
- `MANDATORY`: richiede una tx attiva.
## Synchronization (`TxSynchronization`)
Hook sul ciclo di vita di **una** transazione, registrati con `Data.afterCommit(...)` (o
direttamente con `ResourceRegistry.addSynchronization(...)`).
Ogni callback appartiene esattamente alla transazione più interna attiva al momento della
registrazione, e scatta una volta sola quando *quella* transazione completa:
- una tx interna **joined** (`REQUIRED`) non è una transazione a sé, quindi i callback registrati
al suo interno aspettano il commit più esterno;
- una tx `REQUIRES_NEW` lo è, quindi completarla fa scattare solo i propri callback e lascia in
sospeso quelli della transazione esterna sospesa.
### Posizione rispetto al commit
| hook | quando | risorsa |
| --- | --- | --- |
| `beforeCommit(readOnly)` | subito **prima** del commit reale | sessione/connection ancora **bound**, tx ancora attiva |
| `afterCommit()` / `afterRollback()` | dopo il completamento | risorsa già **sganciata** |
| `afterCompletion(outcome)` | dopo i due precedenti | risorsa già sganciata |
`beforeCommit` è l'unico hook che può ancora scrivere sulla stessa risorsa e finire nella stessa
unità atomica: flush di un buffer, riga di audit, valore derivato. Non viene eseguito se la
transazione è già `rollback-only`, perché non c'è nessun commit da precedere.
I callback post-completamento girano invece a risorsa sganciata: uno che apre una propria
transazione ne ottiene una **nuova** invece di agganciarsi a quella appena conclusa. È questo che
li rende il posto giusto per invalidare una cache, accodare un messaggio o notificare qualcosa
fuori dal database.
### Fallimenti
Lanciare da `beforeCommit` **veta il commit**: la transazione va in rollback, scattano
`afterRollback`/`afterCompletion(ROLLED_BACK)` e l'eccezione arriva al chiamante. È il motivo per
cui l'hook gira prima del commit e non dopo — può ancora rifiutare.
Gli hook post-completamento non hanno questo potere: la transazione è già chiusa quando girano,
quindi un'eccezione si propaga ma non cambia nulla di ciò che è stato committato, e blocca i
callback in coda dietro di lei.
## Uso di `Repository`
`Repository` è la base comune per le repository concrete.
Ogni operazione pubblica usa internamente una tx `REQUIRED` o `REQUIRED` read-only.
Le sottoclassi implementano i metodi `doXxx(...)` del nuovo modello:
- `doFind(Query<T>)`
- `doFindOne(Spec<T>)`
- `doFindPage(Query<T>)`
- `doDeleteAll(Spec<T>)`
- `doUpdateAll(Spec<T>, T)`
I vecchi overload di `findAll(...)` e `findPage(...)` sono stati ridotti a una combinazione di `Query<T>` e `Spec<T>`.
```java
public abstract class Repository<T, ID> {
protected Repository(Tx tx) { ... }
protected final <R> R tx(Tx.TxCallable<R> work) { ... }
}
```
## Composizione con Flash
`DataExtension` registra:
- `Tx` nel `FlashContext`
- `TxManager` nel `FlashContext`
- un annotation processor per `@Transactional`
Questo rende il layer dati componibile con il sistema di extension di Flash senza stato globale.
## Note implementative
- Lo stack transazionale è thread-local e viene ripulito quando torna vuoto.
- Le risorse backend sono sospese e ripristinate per `REQUIRES_NEW` e `NOT_SUPPORTED`.
- `TxSynchronization` è il punto di aggancio per hook di commit/rollback/completion.
- Le synchronization sono in una lista thread-local; ogni transazione nuova registra quante ne
esistevano già alla sua apertura e fa scattare solo la propria coda, così una `REQUIRES_NEW`
non trascina con sé quelle della transazione sospesa.