Skip to content

Runbook: task SQL, truncate e scambio di tabelle ​

Come scrivere, verificare e rieseguire un caricamento write-then-swap: si carica uno staging e lo si scambia con la tabella in uso solo a caricamento riuscito. I pezzi sono il nodo task con postgresSqlTask, mssqlSqlTask o mysqlSqlTask, e gli edge di controllo.

Chi lo esegue. Chi possiede la pipeline (l'owner nella sua scheda) e chi è di turno quando una run fallisce. Un task con destructive: true cancella o sostituisce dati: leggi «Chi autorizza» prima della prima esecuzione in produzione.

La forma ​

ts
graph("ordini-swap")
  // Pre-task: lo staging parte vuoto. Ripetibile, quindi retrySafe.
  .task(
    "svuota-staging",
    postgresSqlTask({
      pool,
      instanceId: "dwh",
      name: "svuota-staging",
      statements: ["TRUNCATE TABLE dwh.ordini_staging"],
      destructive: true,
      retrySafe: true,
    }),
  )
  .source("ordini", ordini, { after: ["svuota-staging"] })
  // … trasformazioni, controlli …
  .sink("controllati", postgresSink({ pool, name: "carica-staging", table: "dwh.ordini_staging", … }))
  // Post-task: parte solo se il sink è riuscito.
  .task("scambia", postgresSqlTask({ pool, name: "scambia", statements: SWAP, destructive: true }), {
    after: ["carica-staging"],
  })
  .build();

Tre regole, che il framework fa rispettare:

  • Statement statici. Si scrivono nel codice, non si compongono con valori della run. Un valore entra solo come parametro @nome dichiarato in params: arriva al database come parametro bind, e nel DAG compare il solo nome.
  • Una transazione per task. Tutti gli statement di un task sono atomici: o tutti o nessuno. Ogni voce di statements è un comando: niente ; in mezzo, e niente BEGIN, COMMIT o ROLLBACK, che romperebbero la transazione. Un blocco con più comandi va in una stored procedure.
  • destructive dichiarato. Compare nel DAG. Uno statement con TRUNCATE, DROP o DELETE e destructive: false fa fallire la costruzione del grafo; il contrario non vale, quindi uno scambio va dichiarato distruttivo anche se non contiene quelle parole.

Questi controlli sono una rete, non una garanzia. Guardano il testo dello statement, non ciò che lo statement chiama: una stored procedure, o SQL dinamico (EXEC sp_executesql @sql, EXECUTE format(…)), possono cancellare dati o chiudere una transazione senza che il framework lo veda, e un parametro passato a SQL dinamico torna a essere codice. Un task che li usa va dichiarato destructive e rivisto come tale.

Nel DAG i letterali stringa dello statement compaiono mascherati ('?') e i commenti sono tolti.

Lo scambio ​

Lo scambio a tre rinomina le due tabelle senza crearne né cancellarne: la versione precedente resta nello staging fino alla prossima run, che la svuota col pre-task. È il modo più semplice di tornare indietro (vedi «Tornare alla versione precedente»).

PostgreSQL ​

ts
const SWAP = [
  "ALTER TABLE dwh.ordini RENAME TO ordini_swap",
  "ALTER TABLE dwh.ordini_staging RENAME TO ordini",
  "ALTER TABLE dwh.ordini_swap RENAME TO ordini_staging",
];
  • ALTER TABLE … RENAME e TRUNCATE sono transazionali: chi legge dwh.ordini vede la tabella vecchia o quella nuova, mai uno stato intermedio.
  • Il rename prende un lock ACCESS EXCLUSIVE: aspetta le query in corso sulla tabella e blocca quelle nuove finché il task non fa commit. Su una tabella molto letta conviene un lock_timeout sulla connessione, così lo scambio fallisce invece di mettersi in coda.
  • Indici, vincoli e permessi seguono la tabella, non il nome. Le due tabelle devono quindi avere la stessa struttura e gli stessi GRANT. Una vista definita su dwh.ordini continua a puntare alla tabella vecchia, che dopo lo scambio si chiama ordini_staging: le viste vanno definite su un nome che non si scambia, o ricreate nello stesso task.

SQL Server ​

ts
const SWAP = [
  "EXEC sp_rename 'dbo.ordini', 'ordini_swap'",
  "EXEC sp_rename 'dbo.ordini_staging', 'ordini'",
  "EXEC sp_rename 'dbo.ordini_swap', 'ordini_staging'",
];
  • sp_rename e TRUNCATE TABLE sono transazionali. sp_rename prende un lock Sch-M sulla tabella: aspetta le query in corso.
  • In alternativa, con due schemi: ALTER SCHEMA staging_old TRANSFER dbo.ordini, poi ALTER SCHEMA dbo TRANSFER staging.ordini. Ha senso quando si scambiano più tabelle insieme.
  • sp_rename non aggiorna viste, stored procedure e funzioni che nominano la tabella: si risolvono per nome alla prossima esecuzione, quindi puntano alla tabella nuova. Una vista WITH SCHEMABINDING impedisce il rename.
  • TRUNCATE TABLE fallisce su una tabella referenziata da una chiave esterna: lo staging non deve averne in ingresso.

MySQL e MariaDB ​

ts
const SWAP = [
  "RENAME TABLE dwh.ordini TO dwh.ordini_swap, dwh.ordini_staging TO dwh.ordini, dwh.ordini_swap TO dwh.ordini_staging",
];
  • Il DDL non è transazionale. TRUNCATE, DROP, ALTER e RENAME TABLE fanno un commit implicito: un errore dopo di loro non li annulla, e un task che li mescola a un DML non ha l'atomicità che ha su Postgres.
  • Lo scambio atomico è uno statement solo: RENAME TABLE con più coppie rinomina tutto o niente, e chi legge vede la tabella vecchia o la nuova, mai una a metà. Per questo la lista ha una voce sola, non tre.
  • RENAME TABLE aspetta un lock sui metadati della tabella: una transazione lunga che la sta leggendo lo ritarda, e le query nuove si mettono in coda dietro di lui. Conviene non averne in corso, o un lock_wait_timeout più corto sulla sessione.
  • TRUNCATE TABLE fallisce su una tabella referenziata da una chiave esterna.
  • Indici, vincoli e permessi seguono la tabella, non il nome: le due tabelle devono avere la stessa struttura e gli stessi GRANT.

Quando lo scambio non parte ​

Il post-task parte solo se il sink dello staging è riuscito. Non parte, e nel report risulta skipped, quando:

Cosa è successoEsito della runStato dei dati
Il sink è fallito, o un altro nodo ha fatto fallire la runfailedtabella in uso intatta, staging parziale o vuoto
Un controllo a monte è fallito dentro un try-catch (input «troncato»)partialtabella in uso intatta, staging con i dati rifiutati
Il pre-task è fallitofailedtabella in uso intatta, staging com'era
La run è stata annullatafailedtabella in uso intatta

In tutti i casi la tabella in uso non è stata toccata.

Attenzione agli scarti. I record finiti in dead-letter a monte, o rifiutati uno per uno da un guard per-record, non fermano lo scambio: sono un percorso previsto, e la run è partial. Un assert prima del sink vede solo i record che gli arrivano, non quelli già scartati. Se la regola è «nessuno scarto, altrimenti niente scambio», togli deadLetter e guard per-record a monte del sink dello staging: il primo record non valido fa fallire la run.

Verifica dopo un fallimento fra caricamento e scambio ​

È il caso in cui lo staging è carico e lo scambio non è avvenuto, o non si sa.

  1. Leggi il report (etl runs show <runId>). Il nodo dello scambio ha uno di questi stati:

    • skipped: non è mai partito. La tabella in uso è quella di prima;
    • partito e success: false, con l'errore «la transazione è stata annullata»: lo scambio non è avvenuto. Vale anche per un annullamento a metà task;
    • partito e success: false, con l'errore «fallito durante il commit: l'esito è ignoto»: la connessione è caduta mentre il server confermava. Lo scambio può essere avvenuto: vai al passo 2. Questo errore non viene mai ritentato, nemmeno con retrySafe;
    • partito e success: true, con la run comunque failed: lo scambio è avvenuto, e a fallire è stato un nodo successivo (un ANALYZE, un altro ramo).
  2. Verifica sul database, senza fidarti del solo report. Il caso ambiguo è una connessione caduta durante il commit: il task riporta un errore, ma il server può avere confermato.

    sql
    -- PostgreSQL: quale tabella ha i dati di oggi?
    SELECT 'ordini' AS tabella, count(*), max(caricato_il) FROM dwh.ordini
    UNION ALL
    SELECT 'ordini_staging', count(*), max(caricato_il) FROM dwh.ordini_staging;

    Se esiste ancora una tabella ordini_swap la transazione non è stata annullata per intero: non deve succedere, fermati ed escala.

  3. Decidi: se la tabella in uso ha già i dati nuovi non c'è niente da fare. Altrimenti riesegui.

Rieseguire ​

Rieseguire l'intera run è sempre sicuro, a una condizione: lo scambio non deve essere già avvenuto (passo 2 sopra).

  • Il pre-task svuota lo staging, quindi un caricamento parziale della run fallita non si somma.
  • Il sink dello staging è idempotente.
  • Se lo scambio era già avvenuto, una riesecuzione completa svuota lo staging, che ora contiene la versione precedente, e ricarica: il risultato è corretto, ma si perde la versione precedente. Non è un danno, è la perdita del paracadute.

Non ritentare lo scambio in automatico. Uno scambio a tre eseguito due volte riporta le tabelle com'erano: per questo non si dichiara retrySafe, e retry sul nodo farebbe fallire la costruzione del grafo. Un TRUNCATE o un ANALYZE sono ripetibili e possono dichiararlo.

Tornare alla versione precedente ​

Finché la run successiva non ha svuotato lo staging, la versione precedente è lì. Per tornare indietro si esegue lo stesso scambio a tre, a mano e in transazione:

sql
BEGIN;
ALTER TABLE dwh.ordini RENAME TO ordini_swap;
ALTER TABLE dwh.ordini_staging RENAME TO ordini;
ALTER TABLE dwh.ordini_swap RENAME TO ordini_staging;
COMMIT;

Va fatto prima della run successiva, e la schedulazione va sospesa finché non si è capito perché i dati nuovi erano sbagliati.

Chi autorizza ​

Un task destructive è un'operazione distruttiva: chiede conferma esplicita e un runbook. In pratica:

  • Prima esecuzione in produzione di una pipeline con un task distruttivo: la autorizza l'owner della pipeline, per iscritto sul ticket, dopo una run completa in un ambiente non di produzione. La scheda della pipeline elenca i task distruttivi e le tabelle che toccano.
  • Un TRUNCATE sulla tabella in uso (non sullo staging) non è write-then-swap: fra il truncate e la fine del caricamento la tabella è vuota. Serve un'autorizzazione a parte, e di norma la risposta è usare lo scambio.
  • Una modifica agli statement di un task distruttivo passa dalla review come ogni modifica al codice: nel DAG e nella PR lo statement è visibile, e destructive: true lo segnala.
  • Un agente non esegue mai una pipeline contro un sistema di produzione senza una richiesta esplicita, e non lancia i test di integrazione dei task SQL fuori dai database locali: quei test si saltano da soli se l'host non è la macchina locale.

Escalation ​

  • Primo livello: l'owner della pipeline, dalla sua scheda.
  • Secondo livello: chi amministra il database, se resta una tabella *_swap o un lock che non si libera.

Noeva è un marchio registrato di 4D S.R.L.