Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .hunspell/ignore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,8 @@ accepted_words:
- committata
- ErrorPageComponent
- anomalyCommandBuffer
- lastSeen
- ctx

# --- Parole Italiane mancanti / Neologismi ---
- aggregatori
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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ì],
Expand Down Expand Up @@ -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",
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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],
Expand All @@ -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`],
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -45,4 +52,4 @@ changelog:
- Francesco Marcon
verifier: Valerio Solito
description: >
Prima stesura del documento
Prima stesura del documento
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. ],
),
)
Expand All @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ], [ ],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`)*
Expand Down Expand Up @@ -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],
)
Expand Down
Loading