0021. Worker daemon multi-workspace: un'istanza serve N workspace via N poller config-driven
- Stato: Accettata
- Data: 2026-07-16
- Decisori: Team BI/ETL
Contesto
Fino a ADR-0019, etl worker è monoworkspace: loadEtlWorkerConfig legge un unico NOEVA_WORKSPACE_ID dall'.env e etl worker avvia un solo EtlProcessWorker, che rivendica i processi con header x-noeva-workspace fisso. ADR-0019 assumeva esplicitamente il worker "deployato come istanza indipendente per workspace".
Nota importante sul modello di autenticazione: l'NOEVA_API_KEY è una sola e vale per tutti i workspace del cliente; il workspace target si seleziona esclusivamente con l'header x-noeva-workspace. Non esiste una API key per workspace — un errore assunto nelle prime bozze di questo ADR e corretto qui.
Ma un cliente Noeva può avere più workspace pur volendo una sola istanza ETL. Con il modello monoworkspace servirebbero N istanze/deploy separati, ciascuno con il proprio .env — costo operativo e infrastrutturale che cresce linearmente col numero di workspace del cliente, senza motivo tecnico.
Decisione
Una singola istanza etl worker può servire N workspace avviando un poller indipendente per workspace (approccio config-driven, zero modifiche a noeva-server-api):
- Nuovo loader
loadEtlWorkerConfigs(env): EtlWorkerConfig[](src/worker/config.ts). SeNOEVA_WORKSPACE_IDSè impostata (lista di workspace separati da virgola), produce unaEtlWorkerConfigper workspace; altrimenti fa fallback al singololoadEtlWorkerConfigda envNOEVA_*(comportamento monoworkspace invariato). - Credenziali condivise da tutti i workspace:
NOEVA_API_KEYeNOEVA_API_BASE_URL(piùETL_WORKER_TYPE/ETL_POLL_INTERVAL_MS) sono comuni; per workspace varia solo ilworkspaceId(l'id inviato nell'headerx-noeva-workspace). Nessun secret per workspace. I connettori sorgente/destinazione (PG/MSSQL/S3/SFTP…) restano su env globale condiviso. startWorkers(src/worker/run-workers.ts) crea unEtlProcessWorkerper config e ritorna un handle di shutdown combinato (stopAll, best-effort viaPromise.allSettled). La CLI (src/cli.ts) lo usa inetl worker, registrando un solo handler SIGTERM/SIGINT per tutti i poller.- Ogni poller riusa immutato il claim server-side esistente: rivendica con la stessa API key e il proprio header
x-noeva-workspace. Il contrattoprocesses/etl/*non cambia. - L'identità del workspace viaggia nel
RunContext(RunContext.workspaceIdopzionale), propagata dal worker viaDispatchOptionsfino acreateRunContext, così uno stage che parlerà con Noeva (es. un futuro sink versonoeva-server-api) legge il workspace dalla run, non daprocess.env.
Alternative considerate
- Claim server-side multi-workspace (claim che ritorna job cross-workspace col
workspace_idnel payload, un solo poller) — scartata per questo passo: richiede un cambio di contratto latonoeva-server-api(cross-repo), a fronte di un beneficio marginale rispetto agli N poller. Resta un'evoluzione possibile. - File JSON montato (
ETL_WORKSPACES_CONFIG) — scartata: era giustificata da un modello (poi rivelatosi errato) con una API key per workspace, che avrebbe richiesto un artefatto con più secret. Con un'unica API key condivisa per workspace varia solo l'id: una lista in env (NOEVA_WORKSPACE_IDS) è più semplice e non introduce un file di secret. - Config per-workspace anche dei connettori sorgente/destinazione — rimandata: oggi le sorgenti sono condivise per il cliente. Se servirà l'override per-workspace delle credenziali sorgente, si sposterà la config connettori da env globale al
RunContext(seam già predisposto) in un ADR dedicato.
Conseguenze
- Retrocompatibile: senza
NOEVA_WORKSPACE_IDS,etl workerresta monoworkspace, identico a prima (stesso codice diloadEtlWorkerConfig). - Nessun nuovo secret:
NOEVA_WORKSPACE_IDScontiene solo id di workspace (non sensibili), l'unica credenziale restaNOEVA_API_KEYcome nel caso singolo. Il loader valida fail-fast (lista non vuota, id non duplicati, API key/base URL presenti). - Concorrenza: N poller = fino a N pipeline concorrenti (prima 1). È un vantaggio di isolamento (un job lento su un workspace non blocca gli altri); ogni pipeline apre/chiude i propri pool connettori in
run(), quindi non c'è stato condiviso tra run. docs/pipelines/*e i runbook che assumevano "un'istanza = un workspace" vanno letti alla luce di questo ADR: l'istanza può ora enumerare più workspace.

