GenericDaoBase.persist() leaves the caller's transaction unbalanced when an insert throws

Aperta
#13,905 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
java
Ambito
backend, database

Direzione di ricerca

Iniziate tracciando GenericDaoBase.persist() e il comportamento di annidamento e commit di TransactionLegacy in framework/db, quindi esaminate UsageManagerImpl.createHelperRecord() e createVolumeHelperEvent() come percorso di riproduzione. Confermate che un errore di inserimento non possa lasciare sbilanciata la transazione del chiamante né fare in modo che il suo commit successivo diventi silenziosamente un no-op, mentre i chiamanti che gestiscono EntityExistsException ricevono il comportamento di errore previsto.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug
problem

Split out of #13399 at @DaanHoogland's request.

GenericDaoBase.persist() does not own a transaction, it joins the caller's via TransactionLegacy.currentTxn() and calls txn.start(), which pushes a START_TXN nesting level. On the SQLException path it never reaches txn.commit(), and there is no finally, so that nesting level is leaked:

final TransactionLegacy txn = TransactionLegacy.currentTxn();   // the CALLER's transaction
try {
    txn.start();                    // pushes a START_TXN nesting level
    ...
    pstmt.executeUpdate();          // throws
    ...
    txn.commit();                   // never reached
} catch (final SQLException e) {
    logger.error("DB Exception on: " + pstmt, e);
    handleEntityExistsException(e); // throws EntityExistsException
    throw new CloudRuntimeException("Unable to persist on DB, due to: " + e.getLocalizedMessage());
}
// no finally, the pushed nesting level is never released

Consequence: the caller's own commit() then finds the transaction unbalanced and silently no-ops, logging only:

WARN [db.Transaction.Transaction] txn: Commit called when it is not a transaction:

(TransactionLegacy.commit()if (!_txn) { LOGGER.warn(...); return false; })

Everything in that transaction is discarded while the caller believes it committed.

Why it matters beyond one call site: callers that deliberately catch EntityExistsException in order to log-and-continue cannot actually continue, because the enclosing transaction is already unrecoverable. UsageManagerImpl.createHelperRecord() is one such caller, and in #13399 this is what converts a single constraint violation into permanent usage-aggregation failure rather than one skipped record.

versions

Observed on CloudStack 4.22.1.0 (EL9 packages), MySQL 8.x / InnoDB.

This is a code-level defect in framework/db rather than an environment-specific one; the code path is not version-specific and hypervisor/storage/network are not relevant.

The steps to reproduce the bug
  1. On 4.22.1.0 with the Usage Server enabled, deploy an instance. Its ROOT volume produces a VOLUME.CREATE usage event carrying vm_id.
  2. UsageManagerImpl.createVolumeHelperEvent() performs two persist() calls sharing (volume_id, created); the second violates usage_volume's unique key (see #13399).
  3. createHelperRecord() catches the resulting EntityExistsException and logs a warning, intending to continue.
  4. Observe txn: Commit called when it is not a transaction shortly afterwards, and that the processed flags set for that batch of events in cloud_usage.usage_event were never committed.

Any caller that hits a constraint violation inside a transaction it owns should show the same behaviour, #13399 is simply a case where it happens on every VM deployment.

What to do about it?

Release the nesting level in a finally, and/or mark the transaction rollback-only so callers receive a real failure instead of a silent no-op.

Either way this needs someone familiar with TransactionLegacy's nesting semantics, since GenericDaoBase backs every DAO in the codebase. I'm raising it rather than proposing a patch.

Lingua principale
Java
Stelle
3.1k
Fork
1.4k
Merge medio
7g 5h
PR unite (30g)
28

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di apache/cloudstack

Tutte le issue di apache/cloudstack

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.