diff --git a/.hunspell/ignore.yaml b/.hunspell/ignore.yaml index f8862969..9be45915 100644 --- a/.hunspell/ignore.yaml +++ b/.hunspell/ignore.yaml @@ -876,6 +876,8 @@ accepted_words: - committata - ErrorPageComponent - anomalyCommandBuffer + - lastSeen + - ctx # --- Parole Italiane mancanti / Neologismi --- - aggregatori diff --git a/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.meta.yaml b/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.meta.yaml index 4f8fc8ec..27db9c0c 100644 --- a/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.meta.yaml +++ b/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.meta.yaml @@ -1,5 +1,12 @@ title: Specifica tecnica - Data-Consumer changelog: + - version: "1.1.0" + date: "2026-04-17" + authors: + - Francesco Marcon + verifier: Leonardo Preo + description: > + Aggiornamento contenuti a seguito del colloquio tecnico di PB del 17/04/2026 - version: "1.0.0" approver: Leonardo Preo baseline: PB diff --git a/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.typ b/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.typ index fe1d5dff..cab7fa59 100644 --- a/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.typ +++ b/docs/13-pb/docest/specifica_tecnica_data_consumer/specifica_tecnica_data_consumer.typ @@ -30,7 +30,7 @@ successive senza ulteriori branch. #table( - columns: (2fr, 2.2fr, 1.2fr, auto), + columns: (2fr, 2.3fr, 1fr, auto), [Campo], [Variabile d'ambiente], [Default], [Obbligatorio], [`NATSUrl`], [`NATS_URL`], [—], [Sì], [`NATSTlsCa`], [`NATS_TLS_CA`], [—], [Sì], @@ -239,16 +239,6 @@ ), ) - #st.port-interface( - name: "ClockProvider", - kind: "driven", - description: [Astrazione del clock di sistema. Consente l'iniezione di un clock deterministico nei test, eliminando - la dipendenza diretta da `time.Now()` nella logica di dominio e di servizio.], - methods: ( - ("Now", [Restituisce il timestamp corrente]), - ), - ) - #st.port-interface( name: "GatewayLifecycleProvider", kind: "driven", @@ -261,6 +251,16 @@ ), ) + #st.port-interface( + name: "ClockProvider", + kind: "driven", + description: [Astrazione del clock di sistema. Consente l'iniezione di un clock deterministico nei test, eliminando + la dipendenza diretta da `time.Now()` nella logica di dominio e di servizio.], + methods: ( + ("Now", [Restituisce il timestamp corrente]), + ), + ) + == Driving port Interfacce implementate dal dominio, invocate dagli adapter per immettere eventi nel sistema. @@ -478,7 +478,8 @@ Acquisisce write-lock. Cancella l'entry per `gatewayKey{tenantID, gatewayID}`. Aggiorna la metrica della dimensione della mappa. Nessun altro side effect. - *`Tick` — approccio a tre fasi* + *`Tick`* \ + Approccio a tre fasi: + *Grace period check:* se `clock.Now()` è prima di `startTime + gracePeriod`, ritorna immediatamente. + *Fase 1 — RLock snapshot:* acquisisce read-lock, copia tutte le entry in uno slice locale (value copy), rilascia read-lock. @@ -488,9 +489,11 @@ update); in caso di errore procede _fail-open_ (logga `slog.Warn` e continua) per evitare di mascherare offline reali quando il Management API non è raggiungibile; pubblica alert via `alertPublisher.Publish`; esegue dispatch `Offline` via il canale asincrono. - + *Fase 3 — WLock con re-validazione:* acquisisce write-lock; rilegge l'entry reale dalla mappa; se `lastSeen` è - avanzato rispetto allo snapshot (telemetria arrivata durante la Fase 2), annulla la transizione. Altrimenti imposta - `knownStatus = Offline`. + + *Fase 3 — WLock*: acquisisce write-lock; rilegge l'entry reale dalla mappa; imposta `knownStatus = Offline` + incondizionatamente. La re-validazione su lastSeen è stata rimossa: confrontare con lo snapshot lasciava + `knownStatus = Online` mentre il sistema esterno era già stato notificato Offline, causando la mancata rilevazione + dell'evento offline successivo. Se una telemetria è arrivata durante la Fase 2, il prossimo `HandleTelemetry` rileva + la transizione`Offline→Online` e gestisce il recovery *`Close`*\ Chiude `dispatchCh` (idempotente via `sync.Once`). Attende che `dispatchWorker` termini di drenare la coda e chiuda @@ -635,7 +638,7 @@ Dipende dall'interfaccia `alertConfigFetcher` (soddisfatta da `NATSRRClient`). #table( - columns: (1.5fr, 2fr, 2.5fr), + columns: (0.9fr, 2fr, 1.5fr), [Campo], [Tipo], [Note], [`snapshot`], [`atomic.Pointer[alertConfigSnapshot]`], [Sostituito atomicamente ad ogni refresh], [`rrClient`], [`alertConfigFetcher`], [Fetch configurazioni dal Management API], @@ -644,10 +647,10 @@ [`defaultTimeoutMs`], [`int64`], [Fallback in assenza di configurazione], [`refreshInterval`], [`time.Duration`], [Default: 2 minuti], [`maxRetries`], [`int`], [Default: 10; con backoff esponenziale], - [`initialBackoff`], [`time.Duration`], [Default: 1 s; configurabile via `ALERT_CONFIG_INITIAL_BACKOFF_MS`], + [`initialBackoff`], [`time.Duration`], [Default: 1 s; configurabile via \ `ALERT_CONFIG_INITIAL_BACKOFF_MS`], [`maxBackoff`], [`time.Duration`], - [Cap del backoff; configurabile via `ALERT_CONFIG_MAX_BACKOFF_MS` (default 30 s)], + [Cap del backoff; configurabile via \ `ALERT_CONFIG_MAX_BACKOFF_MS` (default 30 s)], [`wait`], [`func(ctx context.Context, d time.Duration) error`], @@ -683,6 +686,10 @@ [`fetchWithBackoff`], [`(ctx context.Context) error`], [Retry con backoff esponenziale; incrementa metrica ad ogni tentativo fallito], + + [`snapshotCounts`], + [`() (int, int)`], + [Ritorna `(len(byGateway), len(byTenant));` usato per logging dopo ogni refresh riuscito], ) *Snapshot interno `alertConfigSnapshot`* (immutabile dopo la costruzione): @@ -732,15 +739,18 @@ [`GetGatewayLifecycle`], [`(ctx context.Context, tenantID string, gatewayID string) (GatewayLifecycleState, error)`], - [Serializza `GatewayLifecycleRequest` in JSON, invia verso `internal.mgmt.gateway.get-status`; deserializza - `GatewayLifecycleResponse`; valida la risposta tramite `validateGatewayLifecycleResponse` (verifica che + [Serializza \ `GatewayLifecycleRequest` in JSON, invia verso `internal.mgmt.gateway.get-status`; deserializza + `GatewayLifecycleResponse`; valida la risposta tramite \ `validateGatewayLifecycleResponse` (verifica che `gateway_id` non sia vuoto, che corrisponda all'ID atteso, e che `state` sia uno dei valori ammessi); ritorna `LifecycleUnknown` + errore in caso di risposta non valida], ) Ogni metodo delega a `requestWithRetry`: 1 tentativo iniziale più fino a `maxRetries` retry aggiuntivi (totale `maxRetries + 1` tentativi), timeout per-tentativo derivato da `timeout`, backoff esponenziale dalla slice `backoff`, - con verifica della cancellazione del context tra un tentativo e l'altro. + con verifica della cancellazione del context tra un tentativo e l'altro. Su ogni tentativo fallito (eccetto l'ultimo) + emette `slog.Warn` con subject, numero tentativo, delay ed errore. All'esaurimento ritorna un errore aggregato che + concatena tutti gli errori individuali separati da `"; "` nella forma `"exhausted retries (N): err1; err2; ..."`. + === SystemClock @@ -826,6 +836,15 @@ + Dopo `WriteBatch`: ACK su successo; NAK con delay 5s per errori transitori; Term per errori permanenti di parsing (NATS non invia nuovamente). + *Interfaccia `natsJSSubscriber`:* + + #table( + columns: (1fr, 3fr), + [Metodo], [Firma], + [`Subscribe`], [`(subj string, cb nats.MsgHandler, opts ...nats.SubOpt) (*nats.Subscription, error)`], + ) + + #table( columns: (1.2fr, 1.8fr), [Campo], [Tipo], @@ -872,6 +891,10 @@ [`extractTenantID`], [`(subject string) (string, error)`], [Parsing del subject NATS], [`buildRow`], [`(tenantID string, envelope TelemetryEnvelope) TelemetryRow`], [Mapping envelope + tenantID → row], + + [`enqueuePending`], + [`(ctx context.Context, pending chan<- pendingMsg, pm pendingMsg)`], + [Se ctx è cancellato prima dell'enqueue: Term per `permanentError`, `NakWithDelay(5s)` per errori transienti], ) *Tipi interni:* `permanentError` — arricchisce un error per segnalare fallimenti non eseguibili nuovamente. diff --git a/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.meta.yaml b/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.meta.yaml index 4e763af8..f9395052 100644 --- a/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.meta.yaml +++ b/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.meta.yaml @@ -1,5 +1,12 @@ title: Specifica Tecnica - Simulator Backend & CLI changelog: + - version: "1.1.0" + date: "2026-04-17" + authors: + - Francesco Marcon + verifier: Leonardo Preo + description: > + Correzioni a seguito del colloquio tecnico di PB - version: "1.0.0" approver: Leonardo Preo baseline: PB @@ -45,4 +52,4 @@ changelog: - Francesco Marcon verifier: Valerio Solito description: > - Prima stesura del documento \ No newline at end of file + Prima stesura del documento diff --git a/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.typ b/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.typ index a29e131e..f15316c1 100644 --- a/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.typ +++ b/docs/13-pb/docest/specifica_tecnica_simulator_backend_cli/specifica_tecnica_simulator_backend_cli.typ @@ -235,13 +235,11 @@ manuale perché rispetta l'Open/Closed Principle: possiamo aggiungere nuovi tipi di sensori creando nuovi file, senza mai dover modificare il codice del motore di simulazione. - - *Gestione Ciclo di Vita (Observer Pattern):* Il coordinamento della terminazione dei gateway è affidato all'Observer - Pattern (DecommissionListener). Il problema era come notificare a più componenti (Worker, Database, Tickers) che un - gateway è stato rimosso dal cloud senza che il client NATS dovesse conoscere tutti i moduli del sistema. Un - approccio procedurale (chiamate dirette) avrebbe creato un accoppiamento stretto e fragile. L'Observer risolve - questo problema permettendo ai moduli di iscriversi all'evento di terminazione in modo indipendente. Questo - garantisce che il sistema sia facilmente estensibile: se in futuro si volesse aggiungere un nuovo modulo che - reagisce alla cancellazione, basterà registrarlo come Observer senza modificare il gestore dei messaggi. + - *Gestione Ciclo di Vita:* Il coordinamento della terminazione dei gateway è affidato all'`DecommissionListener`. Il + problema era come notificare a più componenti (Worker, Database, Tickers) che un gateway è stato rimosso dal cloud + senza che il client NATS dovesse conoscere tutti i moduli del sistema. Un approccio procedurale (chiamate dirette) + avrebbe creato un accoppiamento stretto e fragile. La realizzazione di interfacce dedicate risolve questo problema + permettendo ai moduli di iscriversi all'evento di terminazione in modo indipendente. - *Protezione del Materiale Crittografico (Value Object Pattern):* La chiave crittografica viene incapsulata in un’entità immutabile (`EncryptionKey`) che ne impedisce l'accesso diretto ai byte. Il problema era evitare la fuga @@ -360,7 +358,11 @@ columns: (1fr, 3fr), [ *Rotta* ], [ `POST /sim/gateways` ], [ *Descrizione* ], [ Crea un gateway locale, esegue l'onboarding crittografico con il Cloud e avvia il worker. ], - [ *Body Request* ], [ `{"factoryId": "...", "factoryKey": "...", "model": "...", "sendFrequencyMs": 5000}` ], + [ *Body Request* ], + [ + `{"factoryId": "...", "factoryKey": "...", "model": "...", "firmwareVersion": "...", "sendFrequencyMs": 5000}` + ], + [ *Response* ], [ `GatewayResponse` JSON. HTTP 201 Created. ], ), ) @@ -371,7 +373,11 @@ columns: (1fr, 3fr), [ *Rotta* ], [ `POST /sim/gateways/bulk` ], [ *Descrizione* ], [ Creazione massiva di N gateway. Utile per test di carico. ], - [ *Body Request* ], [ `{"baseFactoryIds": "sim-", "factoryKey": "...", "model": "..."}` ], + [ *Body Request* ], + [ + `{"factoryIds": ["sim-001", "sim-002", ...], "factoryKey": "...", "model": "...", "firmwareVersion": "...", "sendFrequencyMs": 5000}` + ], + [ *Response* ], [ Oggetto JSON contenente gli array `gateways` ed `errors`. HTTP 201 Created in caso di successo totale; HTTP 207 @@ -380,9 +386,8 @@ ), ) - _Nota: L'operazione di creazione massiva è limitata internamente a un massimo di 10 esecuzioni concorrenti - (`bulkCreateConcurrency = 10`) tramite semaforo a canale, per prevenire l'esaurimento dei socket e del rate-limiting - verso il Provisioning Service Cloud._ + _Nota: L'operazione di creazione massiva è limitata internamente a un massimo di 10 esecuzioni concorrenti tramite + semaforo a canale, per prevenire l'esaurimento delle risorse._ #figure( caption: [Endpoint GET /sim/gateways], @@ -673,8 +678,8 @@ ricevuto dal subject NATS corrisponda esattamente a quello salvato localmente per il gateway bersaglio; in caso di mismatch, l'evento viene ignorato loggando un warning (slog.Warn). 4. Il `GatewayRegistry`, che implementa tale interfaccia, riceve la chiamata. Acquisisce un lock di scrittura - (`RWMutex`), cerca il worker corrispondente, lo arresta inviando un segnale al `context.CancelFunc` e disconnette il - suo socket NATS. + (`RWMutex`), cerca il worker corrispondente, lo arresta inviando un segnale al `context.CancelFunc` e lo disconnette + da NATS. 5. I dati persistenti vengono eliminati dallo `Store` locale in SQLite in via definitiva. ==== Flusso di Buffering e Flush dei Comandi durante Anomalia @@ -1043,7 +1048,7 @@ table( columns: (1.2fr, 0.8fr, 2fr, 0.6fr, 1.2fr), [ *Campo* ], [ *Tipo* ], [ *Tag JSON* ], [ *Req.* ], [ *Note* ], - [ `FactoryIDs` ], [ `[]string` ], [ `factoryId` ], [ Sì ], [ ], + [ `FactoryIDs` ], [ `[]string` ], [ `factoryIds` ], [ Sì ], [ ], [ `FactoryKey` ], [ `string` ], [ `factoryKey` ], [ Sì ], [ ], [ `Model` ], [ `string` ], [ `model,omitempty` ], [ No ], [ ], [ `FirmwareVersion` ], [ `string` ], [ `firmwareVersion,omitempty` ], [ No ], [ ], @@ -1280,8 +1285,8 @@ [ `gateways bulk` ], [ - `--count` int default 1 (req.), `--factory-id` (req.), `--factory-key` (req.), `--model` (req.), `--firmware` - (req.), `--freq` int default 1000 (req.). + `--factory-id` (req., ripetibile — un'occorrenza per gateway), `--factory-key` (req.), `--model` (req.), + `--firmware` (req.), `--freq` int default 1000 (req.). ], [ HTTP 207 (parziale) non è un errore a livello comando: viene mostrato uno stato `Warning` con il conteggio dei @@ -1531,10 +1536,11 @@ [Il comando letto dal line-editor viene eseguito correttamente da Cobra], [`shell` — fallback a reader classico se line-editor non disponibile], - [Se il costruttore del line-editor fallisce, la shell passa automaticamente alla modalità `bufio.Scanner`], + [Se `MakeRaw` fallisce (modalità raw non disponibile), `runShellWithLineEditor` restituisce un errore e la shell + passa automaticamente alla modalità `bufio.Reader`], - [`shell` — copertura hook di default (`DefaultHooks`)], - [La funzione `DefaultHooks` restituisce i callback `BeforeCommand` e `AfterCommand` senza errori], + [`shell` — copertura delle funzioni iniettabili di default], + [Le funzioni iniettabili di default (`shellStdout`, `shellNewEditor`) restituiscono valori non nil senza errori], ) *Client HTTP e Costruzione Richieste (`internal/client`)* @@ -1674,7 +1680,7 @@ [Locale], [Se la slice dei sensori è vuota, la funzione di rendering non produce righe su stdout], - [Risoluzione UUID gateway (`GetGatewayUUID`)], + [Risoluzione UUID gateway (`gatewayUUID`)], [`httptest`], [Dato un identificativo numerico, il helper risolve e restituisce l'UUID corretto del gateway tramite GET], )