Dagli Incidenti in Produzione alle Annotazioni JUnit 5: Un Framework di Chaos Testing a Tre Livelli
Ingegneria category.testing 26 giugno 2026

Dagli Incidenti in Produzione alle Annotazioni JUnit 5: Un Framework di Chaos Testing a Tre Livelli

448 primitive syscall L1. 92 compositi di fault L2. 64 scenari di incidenti in produzione codificati come singole annotazioni. Un framework di estensione JUnit 5 che trasforma i post-mortem in gate CI per Spring Boot, Quarkus e Micronaut.

E
Engineering Team
Senior Solutions Architects
20 min di lettura

Un'Annotazione. Il Tuo Ultimo Incidente in Produzione. Riprodotto.

Da qualche parte nella cronologia Slack del tuo team c'è un post-mortem. Ha una sezione chiamata "Causa Radice" che descrive come un rolling update di Kubernetes ha creato una finestra di 30 secondi in cui iptables non aveva ancora propagato la rimozione del nuovo pod, e ECONNRESET veniva restituito sulle connessioni attive al vecchio pod. Ha una sezione "Mitigazione" che dice che hai configurato retry-with-backoff. Ha una sezione "Azioni" con una casella di spunta accanto a "Aggiungi test di chaos per questo scenario."

Questa casella non è mai stata spuntata.

Non perché al team non importi. Perché scrivere un test di chaos per quello scenario specifico richiede di capire come il lag di iptables si traduce in tassi di ECONNRESET, quale chiamata libc viene intercettata, qual è la probabilità giusta e come iniettare il fault in un container Docker che gira in un ambiente CI.

È tanta conoscenza prerequisita per una casella di spunta.

`@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)` è un import e un'annotazione. Questa è la casella che viene spuntata.

java
// The post-mortem's action item, as a test:
@Test
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)
void service_survives_rolling_update_rst_storm() {
    // 30% of RECV calls return ECONNRESET
    // This is exactly what a Kubernetes rolling update looks like
    assertThat(callService("/api/health")).isEqualTo(200);
}

La Gerarchia di Annotazioni a Tre Livelli

Il framework organizza gli scenari di chaos in tre livelli, ognuno dei quali affronta un diverso livello di competenza e caso d'uso:

**L1 — Primitive Grezze (448 annotazioni):** Controllo diretto a livello di syscall. Un'annotazione corrisponde a una regola di fault: un errno, un tipo di operazione, una probabilità. Questo è il livello per gli ingegneri che sanno esattamente quale chiamata kernel vogliono mettere in fault e a quale tasso. Massima espressività, nessuna astrazione.

**L2 — Compositi con Nome (92 annotazioni):** Famiglie di fault documentate, ognuna delle quali compone 2–6 regole L1 in un pattern con nome. Questo è il livello per gli scenari di guasto comuni che hanno una forma ben compresa ma non è necessario ricondurli a un incidente specifico. I valori predefiniti sono calibrati dall'analisi di guasti del mondo reale.

**L3 — Scenari di Incidenti (64 annotazioni):** Incidenti reali in produzione codificati come composizioni multi-dominio. Ogni annotazione L3 porta un rating di Severità, un riferimento all'incidente originale o alla fonte di settore, e una classe Composer che sa come scomporre lo scenario nel giusto mix di regole L1 su più domini. Questo è il livello per "riprodurre ciò che è successo nel post-mortem."

I livelli non sono esclusivi — puoi mescolare annotazioni L1, L2 e L3 sulla stessa classe di test o metodo, e si accumulano correttamente. Le annotazioni con scope di metodo sovrascrivono quelle con scope di classe per la durata del metodo di test, poi ripristinano le regole con scope di classe quando il metodo termina.

L1: Primitive Syscall Grezze — 448 Annotazioni

Il livello L1 ti dà il controllo diretto sulle singole regole di fault a livello syscall. Ogni annotazione segue lo stesso pattern: operazione target + errno o effetto + probabilità + pattern di host o percorso opzionale.

java
// Network — connect/recv/send
@ChaosConnectEconnrefused(probability = 0.10)
@ChaosConnectEtimedout(probability = 0.03)
@ChaosRecvEconnreset(probability = 0.05)
@ChaosSendEpipe(probability = 0.02)

// DNS — getaddrinfo
@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)
@ChaosDnsGetaddrinfoEaiFail(hostPattern = "legacy.svc", probability = 1.0)

// File I/O — writes and reads
@ChaosWriteTorn(pathPrefix = "/data/wal", probability = 0.05)
@ChaosReadEio(pathPrefix = "/etc/app", probability = 0.01)
@ChaosWriteEnospc(pathPrefix = "/data", probability = 0.001)

// Memory
@ChaosMmapEnomem(probability = 0.005)

// Timing — breaks JWT expiry checks, caching TTLs
@ChaosClock(effect = ClockEffect.OFFSET, offsetMs = 30_000)  // +30 second skew

// All annotations support:
//   probability  — 0.0 (never) to 1.0 (always)
//   id           — target a specific container by ID
//   onMissingEnv — ERROR (fail test) or ABORT (skip test) if chaos env unavailable

L2: Compositi di Fault con Nome — 92 Pattern Documentati

Il livello L2 codifica pattern di guasto documentati con valori predefiniti calibrati. Ogni composito rappresenta una forma di guasto che ha un nome nei post-mortem degli incidenti e un impatto noto e prevedibile sul comportamento dell'applicazione.

  • @CompositeChaosConnectionRefused — ECONNREFUSED su ogni connect(), simula un servizio downstream non raggiungibile
  • @CompositeChaosTransientDnsFailure — EAI_AGAIN sul 15% delle chiamate getaddrinfo(), simula un sovraccarico di CoreDNS
  • @CompositeChaosLowMemoryPressure — ENOMEM sullo 0,5% delle chiamate mmap(), simula una contesa di memoria
  • @CompositeChaosNetworkFlap — alternanza di ECONNRESET e successo, simula un link di rete instabile
  • @CompositeChaosSlowDisk — latenza di 50–200ms su write(), simula uno storage I/O saturo
  • @CompositeChaosClockDrift — skew del clock con segno ±5 secondi, simula una deriva NTP
  • @CompositeChaosConnectionTimeout — ETIMEDOUT sul 5% di connect() con latenza di 3s, simula un timeout del firewall
  • @CompositeChaosShortWrite — TORN sul 10% delle scritture, simula il filesystem che restituisce conteggi ridotti

L3: Incidenti Reali in Produzione — 64 Scenari Annotati

Il livello L3 è dove il chaos engineering diventa memoria istituzionale. Ogni annotazione codifica una modalità di guasto reale — documentata in report di incidenti in produzione, issue tracker di Kubernetes o note analisi di guasti in sistemi distribuiti.

Il pattern Composer gestisce la scomposizione: ogni annotazione L3 fa riferimento a una classe Composer che sa come tradurre una descrizione ad alto livello di un incidente nel giusto mix di regole L1 su più domini di fault.

java
// L3 Incident: Kubernetes Rolling Update RST Storm
// Source: Kubernetes GitHub Issue #56903, multiple production incidents
// Severity: CRITICAL
// What happens: During rolling updates, iptables rules update asynchronously.
// Old pods receive connection attempts for ~15-30s after replacement.
// Active connections to old pods get ECONNRESET when the pod terminates.
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)

// L3 Incident: Feign Retry Amplification Storm
// Source: Documented in multiple high-traffic Java service incidents
// Severity: CRITICAL
// What happens: Feign default retry (5x) × replicas (3) × backends (N)
// creates multiplicative amplification. A 50% failure rate becomes 9x load.
@IncidentChaosFeignRetryAmplification(toxicity = 0.5)

// L3 Incident: Spring @Transactional(REQUIRES_NEW) Deadlock
// Source: Spring Framework known issue pattern
// Severity: SEVERE
// What happens: REQUIRES_NEW suspends outer transaction and borrows new
// connection. Under load, pool exhausts — outer tx holds one connection,
// REQUIRES_NEW waits for another, deadlock.
@IncidentChaosSpringTransactionalPoolDeadlock

// L3 Incident: Kubernetes DNS ndots=5 Storm
// Source: CoreDNS performance analysis, ndots search path behavior
// Severity: SEVERE
// What happens: With ndots:5 (K8s default), each hostname generates 5+
// DNS queries (search path expansion). Under CoreDNS load, EAI_AGAIN
// spikes. Services that don't handle DNS transient failures cascade.
@IncidentChaosK8sDnsNdots5Storm

// L3 Incident: JVM Code Cache Full
// Source: JVM JIT compilation documentation, production profiling
// Severity: SEVERE
// What happens: When code cache fills, JIT stops compiling new methods.
// Throughput drops 10–50x as hot paths deoptimize to interpreted mode.
@IncidentChaosJvmCodeCacheFull

// L3 Incident: Redis Sentinel Failover Storm
// Source: Redis Sentinel documentation, observed failover cascades
// Severity: MEDIUM
// What happens: Sentinel failover triggers connection flap on all clients.
// Applications that don't handle LOADING errors loop reconnecting,
// overwhelming the new master.
@IncidentChaosRedisNetworkFlap

Docker + LD_PRELOAD: Chaos su Qualsiasi Immagine di Produzione

Per i test che eseguono il codice dell'applicazione all'interno di container Docker (pattern Testcontainers), il framework inietta automaticamente le librerie C99 LD_PRELOAD nel container prima che si avvii.

Il meccanismo di iniezione utilizza il tar stream dell'API Docker — non shell exec, non volumi, non init container:

1. L'estensione JUnit rileva il sistema operativo base dell'immagine del container (Alpine → musl, Debian/RHEL/Ubuntu → glibc). 2. Seleziona il binario pre-compilato corretto (glibc/musl × amd64/arm64 — quattro varianti, incluse nel JAR). 3. Copia il file `.so` nel container tramite `DockerClient.copyArchiveToContainerCmd()` prima di `container.start()`. 4. Imposta `LD_PRELOAD=/chaos/libchaos-net.so` (e altri come dichiarati) nell'ambiente del container.

Ciò significa che il chaos testing funziona su immagini `distroless`, immagini basate su `scratch`, UBI minimal, Alpine — qualsiasi immagine che utilizzi un linker dinamico ELF standard. Nessuna modifica al Dockerfile, nessun sidecar, nessuna infrastruttura aggiuntiva.

java
// @SyscallLevelChaos declares which libraries to inject
// The framework handles the rest: image detection, binary selection, injection

@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos({LibchaosLib.NET, LibchaosLib.DNS})  // Inject both
class InventoryServiceChaosTest {

    @Container @AppContainer
    static GenericContainer<?> inventory =
        new GenericContainer<>("inventory-service:latest")  // Any image works
            .withExposedPorts(8080);

    @Container
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");

    @Test
    @CompositeChaosTransientDnsFailure  // L2: 15% EAI_AGAIN on DNS
    void inventory_lookup_resilient_to_dns_hiccups(String inventoryUrl) {
        // libchaos-dns.so is active inside the inventory-service container
        // DNS calls from the service will see 15% EAI_AGAIN failures
        var response = given().get(inventoryUrl + "/api/product/123");
        assertThat(response.statusCode()).isEqualTo(200);
    }
}

Integrazioni con i Framework: Spring Boot, Quarkus, Micronaut

Ogni grande framework Java ottiene un modulo di integrazione dedicato che gestisce sia l'orchestrazione lato test che il piano di controllo chaos opzionale per il runtime.

**Spring Boot 3 e 4:** Lo starter di test fornisce `@ChaosTest` come annotazione composta, registra `ChaosControlPlane` e `ChaosSession` come bean Spring, e fornisce la risoluzione dei parametri JUnit 5 per entrambi. Si compone con `@SpringBootTest`, `@DataJpaTest`, `@WebMvcTest` e qualsiasi altra annotazione di slice di test Spring.

Lo starter di runtime espone `/actuator/chaos` come endpoint Actuator protetto per la gestione live degli scenari in ambienti non di produzione.

**Quarkus:** L'estensione Quarkus fornisce `@QuarkusChaosTest` come annotazione di test Quarkus composta con injection CDI di `ChaosControlPlane`. Si integra con `@QuarkusTest`, `@QuarkusIntegrationTest` e test di immagini native.

**Micronaut:** L'integrazione Micronaut fornisce `@MicronautChaosTest` con bean `ChaosControlPlane` e `ChaosSession` compatibili con `@Inject`. Funziona con `@MicronautTest` sia in modalità di test JVM che nativa.

  • Spring Boot 3/4: annotazione composta @ChaosTest, ChaosControlPlane come bean Spring, endpoint /actuator/chaos
  • Quarkus: @QuarkusChaosTest, injection CDI, funziona con @QuarkusIntegrationTest e native
  • Micronaut: @MicronautChaosTest, bean compatibili con @Inject, supporto per test JVM e native
  • JUnit 5 puro: @ExtendWith(ChaosTestingExtension.class) con ChaosControlPlane manuale — nessun framework richiesto
  • Compatibile con Gradle e Maven: tutti i moduli pubblicati su Maven Central con coordinate standard

Risorse e Vincoli: Annotazione @Resources

Oltre all'iniezione di fault, il framework supporta il test dei vincoli di risorse tramite `@Resources`. Questo dichiara i limiti di CPU e memoria che l'estensione JUnit applica al container Docker prima che si avvii — senza modificare la definizione del container nel codice di test.

Ciò consente una classe di test spesso saltata: "il mio servizio si comporta correttamente quando è vincolato nelle risorse?" Questo è uno scenario realistico in Kubernetes, dove i pod vengono eseguiti con limiti di CPU e memoria, e dove il raggiungimento di tali limiti provoca throttling e kill OOM.

java
// Test your service under the same resource constraints as production
@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos(LibchaosLib.NET)
@Resources(cpuQuota = 0.5, memoryMb = 256)  // Same limits as production K8s pod
class ResourceConstrainedChaosTest {

    @Container @AppContainer
    static GenericContainer<?> app =
        new GenericContainer<>("my-service:latest")
            .withExposedPorts(8080);

    @Test
    @IncidentChaosJvmCodeCacheFull  // Code cache fills → JIT stops
    void service_degrades_gracefully_under_resource_pressure(String appUrl) {
        // CPU-throttled + code cache pressure = realistic production stress test
        var response = given().get(appUrl + "/api/health");
        // Service should return 200 or 503 — not hang forever
        assertThat(response.statusCode()).isIn(200, 503);
        assertThat(response.time()).isLessThan(5000);  // Under 5s even degraded
    }
}

L'Architettura a Plugin: Un'Estensione, Tutti i Container

Le prime versioni del framework avevano estensioni JUnit separate per ogni tipo di container: una per Redis, una per PostgreSQL, una per Kafka e così via. Ogni estensione aveva 200+ righe di codice del ciclo di vita quasi identico. Estendersi a un nuovo tipo di container significava copiare e modificare.

L'architettura attuale utilizza un SPI `ChaosPlugin` scoperto tramite `ServiceLoader.load(ChaosPlugin.class)`. Ogni tipo di container registra un'implementazione. L'unica `ChaosTestingExtension` le orchestra tutte.

Il contratto SPI è minimale: scoprire i container nella classe di test, fornire le informazioni di connessione, gestire l'applicazione delle risorse e gestire la configurazione del chaos pre-avvio. L'estensione universale gestisce tutto il resto: elaborazione delle annotazioni, gestione del ciclo di vita, isolamento della sessione, risoluzione dei parametri.

Questo elimina ~8.000 righe di duplicazione e rende il framework estensibile a nuovi tipi di container senza modificare il core.

java
// Registering a custom container type
public class MyServiceChaosPlugin implements ChaosPlugin {

    @Override
    public boolean supportsContainer(GenericContainer<?> container) {
        return container.getDockerImageName().startsWith("my-service:");
    }

    @Override
    public ConnectionInfo extractConnectionInfo(GenericContainer<?> container,
                                               Annotation annotation) {
        return ConnectionInfo.of(
            "http://" + container.getHost() + ":" + container.getMappedPort(8080)
        );
    }

    @Override
    public Class<? extends Annotation> containerAnnotation() {
        return AppContainer.class;  // @AppContainer marks this container
    }
}
// META-INF/services/com.macstab.chaos.core.spi.ChaosPlugin:
// com.example.chaos.MyServiceChaosPlugin

43 Moduli, 11 Domini, Un Unico Modello Mentale

Il framework è organizzato su 43 moduli Gradle che coprono 11 domini di fault. Ecco il quadro completo:

**Infrastruttura core:** chaos-core (estensione JUnit 5, SPI, ciclo di vita), chaos-spi (contratto plugin), chaos-api (tipi di selettore ed effetto)

**Pack di test per dominio di fault:** testpacks-connection (TCP/UDP), testpacks-dns (risoluzione DNS), testpacks-memory (malloc/mmap), testpacks-process (fork/exec/segnali), testpacks-time (clock, skew temporale), testpacks-filesystem (I/O su file)

**Integrazione JVM:** chaos-java (trasporto bytecode JVM, cablaggio lato container)

**Pack di incidenti L3:** testpacks-l3-kubernetes (rolling update, DNS storm), testpacks-l3-feign (amplificazione dei retry), testpacks-l3-spring (deadlock transazionale, starvation OSIV), testpacks-l3-redis (failover storm), testpacks-l3-kafka (failover del broker), testpacks-l3-grpc (GOAWAY storm)

**Integrazioni con i framework:** java-spring-boot3, java-spring-boot3-test, java-spring-boot4, java-spring-boot4-test, java-quarkus, java-micronaut

  • 448 annotazioni L1: primitive syscall grezze su 6 domini di fault a livello OS
  • 92 compositi L2: pattern di fault con nome e valori predefiniti calibrati, derivati dalla produzione
  • 64 incidenti L3: post-mortem reali come singole annotazioni con rating di Severità
  • 43 moduli Gradle: versioning indipendente, nessuna dipendenza transitiva obbligatoria
  • 4 varianti binarie: glibc-amd64, glibc-arm64, musl-amd64, musl-arm64 — incluse nel JAR
  • Spring Boot 3/4, Quarkus, Micronaut: integrazione completa con i framework pronta all'uso

Key Takeaways

Il framework esiste perché i post-mortem degli incidenti hanno azioni che non vengono mai implementate. Non perché gli ingegneri siano pigri, ma perché il divario tra "dovremmo testare questa modalità di guasto" e "ecco un test che testa questa modalità di guasto" era troppo ampio.

448 annotazioni L1 per il controllo chirurgico. 92 compositi L2 per famiglie di guasti documentate. 64 scenari di incidenti L3 che ti permettono di trasformare un post-mortem su Slack in un test CI che fallisce in cinque minuti.

Il livello chaos non sostituisce i test unitari o i test di integrazione. Completa il quadro. La Fase 4 è quella in cui la tua pipeline CI impara a riprodurre gli incidenti in produzione prima che si ripetano.

#chaos-engineering #java #junit5 #spring-boot #quarkus #micronaut #testcontainers #resilience #ci-cd #annotations
E

Engineering Team

Senior Solutions Architects

Costruiamo sistemi distribuiti da prima che i 'microservizi' fossero una cosa. Le nostre cicatrici raccontano storie.