Un bus che decide in base al contenuto
Hai un servizio Ordini che pubblica OrdineEffettuato. Vuoi che gli ordini sopra 500 euro dei clienti premium
arrivino a un team dedicato, che qualunque evento di errore finisca in una coda di analisi e che nessun ordine di prova
raggiunga la produzione. Se il publisher decide da solo a chi mandare cosa, ogni nuova regola è una modifica al publisher.
Amazon EventBridge sposta la decisione sul bus: il publisher consegna l'evento, e le regole
(ognuna con un event pattern e uno o più target) scelgono i destinatari guardando il contenuto del JSON.
Qui nessun account AWS: eseguiamo il meccanismo in piccolo, un matcher di pattern e un bus con regole, archivio e replay, e poi guardiamo che cosa dice la documentazione ufficiale sul servizio vero, consultata il 2026-11-12. Una precisazione che il resto dell'articolo rispetta: oggi la guida di EventBridge descrive due tipi di bus, il Custom Event Bus, consigliato per le applicazioni nuove, che dichiara ordinamento della consegna, replay e conservazione degli eventi, e il Custom Event Bus - Classic, quello con regole e target scritti da te, che resta disponibile.
Il modello di questo articolo (regole, pattern, target, archivio) è quello della guida del bus Classic. Se hai già letto il confronto con SQS e SNS sai dove il bus si colloca; per la teoria di fondo vedi i fondamenti degli eventi.
Il matcher: che cosa significa «combacia»
Un pattern ha la stessa struttura dell'evento che vuole selezionare, e un evento combacia o non combacia: non ci sono punteggi intermedi. Le regole da ricordare sono tre. Un pattern è una conferma campo per campo: tutti i campi nominati devono combaciare (AND), mentre i campi dell'evento non nominati sono ignorati.
Un array di valori è un OR: ["PREMIUM", "VIP"]
vuol dire «uno di questi». Un oggetto al posto del valore è un operatore. Il programma implementa solo un sottoinsieme degli
operatori della guida (il resto, come $or, wildcard o filtri su indirizzi IP, non c'è) e lo fa per mostrare la
logica, non per riprodurre il motore reale.
// Sottoinsieme degli operatori di filtro descritti nella documentazione di EventBridge:
// valori esatti (array = OR), prefix, suffix, anything-but, numeric, exists.
type Json = null | boolean | number | string | Json[] | { [k: string]: Json };
type Obj = { [k: string]: Json };
function operatore(op: Obj, v: Json | undefined): boolean {
if ('exists' in op) return (v !== undefined) === op.exists;
if (v === undefined) return false;
if ('prefix' in op) return typeof v === 'string' && v.startsWith(op.prefix as string);
if ('suffix' in op) return typeof v === 'string' && v.endsWith(op.suffix as string);
if ('anything-but' in op) {
const vietati = ([] as Json[]).concat(op['anything-but']);
return !vietati.includes(v);
}
if ('numeric' in op) {
if (typeof v !== 'number') return false;
const c = op.numeric as (string | number)[];
for (let i = 0; i < c.length; i += 2) {
const n = c[i + 1] as number;
const ok = { '<': v < n, '<=': v <= n, '>': v > n, '>=': v >= n, '=': v === n }[c[i] as string];
if (!ok) return false;
}
return true;
}
throw new Error('operatore non supportato: ' + Object.keys(op).join(','));
}
// Tutti i campi del pattern devono combaciare (AND); i campi dell'evento non nominati dal pattern sono ignorati.
export function combacia(pattern: Obj, evento: Obj): boolean {
return Object.entries(pattern).every(([campo, atteso]) => {
const v = evento[campo];
if (Array.isArray(atteso)) {
// L'array e' un OR di valori esatti o di operatori.
return atteso.some((a) => (a !== null && typeof a === 'object' && !Array.isArray(a)) ? operatore(a, v) : a === v);
}
if (atteso !== null && typeof atteso === 'object') {
return v !== null && typeof v === 'object' && !Array.isArray(v) && combacia(atteso as Obj, v as Obj);
}
throw new Error(`il pattern "${campo}" deve essere un array o un oggetto`);
});
}
La funzione combacia percorre il pattern. Se il valore atteso è un array, some prova ogni alternativa:
un valore secco si confronta con ===, un oggetto passa a operatore. Se è un oggetto annidato, per
esempio detail, scende ricorsivamente.
L'exists è l'unico operatore che accetta un campo assente;
per tutti gli altri un campo mancante significa nessuna corrispondenza, ed è la ragione per cui un pattern su
detail.tipoCliente non seleziona gli ordini di chi quel campo non lo manda. I numeri si confrontano come numeri:
{ numeric: ['>', 500] } non accetta la stringa "750". Il modulo viene esercitato dal programma successivo.
Un bus con regole, archivio e replay
Il bus del programma fa le tre cose che il servizio descrive: put riceve l'evento, gli assegna id e orario e lo
consegna ai target delle regole che combaciano; ogni evento finisce anche in un archivio; replay rilegge
l'archivio in un intervallo e consegna solo alle regole indicate. Le tre regole sono quelle dell'esempio iniziale:
ordini VIP sopra 500 euro, errori di qualunque servizio dell'app, ordini fuori da test e staging. Le cinque chiamate a
put sono scelte per colpire i casi limite: un ordine che passa una regola e non l'altra, uno che le passa tutte e
uno che viene da un'applicazione estranea.
import { combacia } from './pattern.ts';
type Obj = { [k: string]: any };
type Regola = { nome: string; pattern: Obj; bersagli: Map<string, (e: Obj) => void> };
class Bus {
regole: Regola[] = [];
archivio: Obj[] = [];
private n = 0;
regola(nome: string, pattern: Obj, bersaglio: (e: Obj) => void): void {
this.regole.push({ nome, pattern, bersagli: new Map([[nome + '-target', bersaglio]]) });
}
// Come PutEvents: l'evento riceve id e time dal bus; ogni regola che combacia consegna ai suoi bersagli.
put(source: string, detailType: string, detail: Obj): string[] {
const evento = { version: '0', id: `evt-${++this.n}`, source, 'detail-type': detailType, time: `2026-11-12T10:0${this.n}:00Z`, detail };
this.archivio.push(evento);
return this.consegna(evento, this.regole);
}
private consegna(evento: Obj, regole: Regola[]): string[] {
const hit = regole.filter((r) => combacia(r.pattern, evento));
hit.forEach((r) => r.bersagli.forEach((f) => f(evento)));
return hit.map((r) => r.nome);
}
// Replay: rilegge l'archivio nell'intervallo e consegna SOLO alle regole indicate, sullo stesso bus.
replay(dal: string, al: string, soloRegole: string[]): number {
const target = this.regole.filter((r) => soloRegole.includes(r.nome));
let n = 0;
for (const e of this.archivio.filter((x) => x.time >= dal && x.time <= al)) n += this.consegna(e, target).length;
return n;
}
}
const bus = new Bus();
const visti: Record<string, string[]> = { vip: [], errori: [], tutto: [] };
bus.regola('ordini-vip', { source: ['com.mioapp.ordini'], 'detail-type': ['OrdineEffettuato'],
detail: { totale: [{ numeric: ['>', 500] }], tipoCliente: ['PREMIUM', 'VIP'] } }, (e) => visti.vip.push(e.id));
bus.regola('errori', { source: [{ prefix: 'com.mioapp.' }], 'detail-type': [{ suffix: 'Failed' }] }, (e) => visti.errori.push(e.id));
bus.regola('no-test', { source: ['com.mioapp.ordini'], detail: { ambiente: [{ 'anything-but': ['test', 'staging'] }] } }, (e) => visti.tutto.push(e.id));
const ordine = (totale: number, tipoCliente: string, ambiente: string) => ({ totale, tipoCliente, ambiente });
console.log('1', bus.put('com.mioapp.ordini', 'OrdineEffettuato', ordine(750, 'VIP', 'prod')));
console.log('2', bus.put('com.mioapp.ordini', 'OrdineEffettuato', ordine(750, 'STANDARD', 'prod')));
console.log('3', bus.put('com.mioapp.ordini', 'OrdineEffettuato', ordine(900, 'PREMIUM', 'test')));
console.log('4', bus.put('com.mioapp.pagamenti', 'PagamentoFailed', { ordineId: 'o-9' }));
console.log('5', bus.put('com.altraapp.ordini', 'OrdineEffettuato', ordine(900, 'VIP', 'prod')));
console.log('consegne:', JSON.stringify(visti));
// Un nuovo consumatore nasce dopo: lo si alimenta con la storia, senza ripetere gli effetti degli altri.
const nuovi: string[] = [];
bus.regola('analisi-ordini', { 'detail-type': ['OrdineEffettuato'] }, (e) => nuovi.push(e.id));
console.log('replay consegne:', bus.replay('2026-11-12T10:01:00Z', '2026-11-12T10:03:59Z', ['analisi-ordini']), JSON.stringify(nuovi));
console.log('consegne dopo il replay:', JSON.stringify(visti));
// Output:
// 1 [ 'ordini-vip', 'no-test' ]
// 2 [ 'no-test' ]
// 3 [ 'ordini-vip' ]
// 4 [ 'errori' ]
// 5 []
// consegne: {"vip":["evt-1","evt-3"],"errori":["evt-4"],"tutto":["evt-1","evt-2"]}
// replay consegne: 3 ["evt-1","evt-2","evt-3"]
// consegne dopo il replay: {"vip":["evt-1","evt-3"],"errori":["evt-4"],"tutto":["evt-1","evt-2"]}
L'evento 1 (750 euro, cliente VIP, produzione) attiva ordini-vip e no-test. L'evento 2 (cliente
standard) attiva solo no-test, perché il pattern VIP richiede tipoCliente in due valori. L'evento 3 è
il contrario: ordine premium da 900 euro ma ambiente test, quindi la regola VIP lo prende e no-test lo
esclude con anything-but. L'evento 4 non ha nulla a che fare con gli ordini e viene preso solo da
errori, grazie al suffisso Failed.
L'evento 5 non attiva nessuna regola: il
source è com.altraapp.ordini, e un evento che nessuna regola seleziona non viene consegnato a
nessuno. Il punto sul replay è nell'ultima riga: il nuovo consumatore analisi-ordini, nato dopo, riceve i tre
ordini archiviati nell'intervallo, mentre le consegne delle altre regole sono identiche a prima. Senza la restrizione sulle
regole il replay avrebbe scritto di nuovo ai consumatori già serviti.
Che cosa dice il servizio vero
Il programma è un modello; ecco i comportamenti del servizio che non ho potuto provare e che conviene conoscere, tutti dalla documentazione ufficiale (le pagine sono nelle fonti):
- La pubblicazione con
PutEventspuò riuscire a metà: la risposta contiene un esito per ogni entry e un contatoreFailedEntryCount, quindi il chiamante deve controllarlo e rimandare le entry fallite. - Pubblicare su un bus che non esiste restituisce comunque risposta positiva: la documentazione dice che l'evento non trova regole e viene scartato, senza essere conteggiato tra i fallimenti.
- La dimensione totale di una richiesta
PutEventsdeve restare sotto 1 MB (1.048.576 byte); per payload più grandi la guida consiglia di salvare l'oggetto in S3 e di mettere nell'evento solo l'URL. - Se la consegna a un target fallisce per un errore ritentabile, EventBridge riprova per default per 24 ore e fino a 185 volte con attesa esponenziale e jitter; dopo di che l'evento viene scartato, a meno di una coda DLQ configurata sul target.
- Una regola può avere fino a cinque target nella quota indicata dalla guida per ogni regione supportata.
Sono cinque fatti che il programma sopra non può modellare perché dipendono dalla rete e dal servizio, e ognuno cambia il codice del producer o del consumer. Il primo e il secondo sono errori che passano in silenzio se non controlli la risposta; il quarto è quello che nel progetto di un consumatore pesa di più, perché dice che lo stesso evento può arrivare più volte (si veda l'idempotenza dei consumer) e che senza una DLQ i fallimenti definitivi spariscono (si veda la dead letter queue).
Schema registry, archivio e replay: cosa fanno davvero
Lo schema registry di EventBridge scopre gli schemi dagli eventi che transitano sul bus e permette di scaricare code binding per i linguaggi più usati. La guida lo descrive come un modo di accelerare lo sviluppo, non come un validatore: non rifiuta eventi che non rispettano lo schema. Se vuoi un controllo in pubblicazione, lo scrivi tu nel producer; un sistema che dichiari «errori di schema rilevati alla pubblicazione» descrive una funzione diversa da quella documentata. La stessa guida avverte che i valori di alcune proprietà usate per costruire gli schemi possono essere mascherati con asterischi perché contengono dati sensibili.
Sull'archivio la guida dice che EventBridge rimuove gli eventi solo quando superano il periodo di conservazione o quando elimini l'archivio. Sul replay precisa che puoi rigiocare gli eventi solo verso lo stesso bus che li ha emessi e puoi limitare la consegna a certe regole, come fa il programma. Il replay è lo strumento per ricostruire un modello di lettura o testare un consumatore nuovo; non è una macchina del tempo: gli eventi rigiocati ripassano da regole e target di oggi, quindi il consumatore deve tollerare di vedere eventi vecchi e deve essere idempotente.
Quando un bus per contenuto non è la scelta
La documentazione avverte di un errore da non sottovalutare: è possibile scrivere regole che producono un ciclo infinito, per esempio una regola che reagisce a una modifica di permessi e la cui azione modifica di nuovo i permessi. Le regole per contenuto sono comode proprio perché sono implicite, ma questo le rende difficili da ricostruire: per sapere chi riceve un evento devi guardare tutte le regole del bus.
Se il tuo caso è «un messaggio, un consumatore, ritentativi» una coda basta; se serve un fan-out semplice, un topic basta. Il bus ripaga quando i destinatari sono molti, cambiano spesso e devono decidere in base ai dati. Se ti serve invece l'ordinamento della consegna, la guida attuale lo associa al Custom Event Bus e non al modello Classic usato qui; verifica quale dei due stai usando prima di fartelo promettere.
Che cosa sai fare adesso
Sai scrivere un pattern per contenuto e prevedere quali eventi seleziona, sai perché un evento senza regole sparisce senza
errore, e sai usare il replay con filtro per alimentare un consumatore nuovo senza disturbare gli altri. Sai anche cosa
controllare sul servizio vero: l'esito per entry di PutEvents, i ritentativi con scarto finale, la DLQ e la
differenza tra scoperta di schemi e validazione. Il passo successivo è il confronto con
SQS e SNS. Per l'esperienza d'uso quando un consumatore resta indietro rispetto al bus, vedi la consistenza eventuale.
Fonti
- Amazon EventBridge User Guide, What is Amazon EventBridge? - i due tipi di bus (Custom Event Bus e Classic), consultata il 2026-11-12
- EventBridge, Event patterns - un pattern ha la stessa struttura dell'evento ed è un match totale, e il rischio di cicli infiniti nelle regole, consultata il 2026-11-12
- EventBridge, Content filtering in event patterns - operatori prefix, suffix, anything-but, numeric, exists, consultata il 2026-11-12
- EventBridge, Sending events with PutEvents - esito per entry, FailedEntryCount, bus inesistente, limite di 1 MB, consultata il 2026-11-12
- EventBridge, How EventBridge retries delivering events - 24 ore e 185 tentativi per default, scarto dopo l'ultimo, DLQ, consultata il 2026-11-12
- EventBridge, Archiving events - rimozione solo a fine conservazione, consultata il 2026-11-12; Replaying archived events - replay solo verso lo stesso bus, con regole selezionabili, consultata il 2026-11-12
- EventBridge, Schema registry - scoperta degli schemi, code binding, mascheramento dei valori, consultata il 2026-11-12
Il matcher e il bus (pattern.ts, bus.ts) sono stati eseguiti con Node.js v24.11.0; le uscite sono quelle reali. Sono un modello dei concetti, non il servizio: nessuna chiamata ad AWS è stata fatta, e il matcher implementa solo un sottoinsieme degli operatori. Le quote variano per regione e nel tempo: ricontrolla la pagina delle quote prima di dimensionare.

Commenti
Caricamento commenti...
Accedi per commentare