62 Pontos de Interceptação no JDK: Chaos Engineering In-Process com um Agente Java
Engenharia category.testing 26 de junho de 2026

62 Pontos de Interceptação no JDK: Chaos Engineering In-Process com um Agente Java

Um agente Java que instrumenta 62 pontos de chamada do JDK usando ByteBuddy. Isolamento de sessão por teste, auto-configuração Spring Boot 3/4, e sobrecarga JIT próxima de zero. Chaos engineering executado inline na sua suíte de testes JUnit e bloqueando o build em caso de falha.

E
Engineering Team
Senior Solutions Architects
18 min de leitura

A Camada que o Toxiproxy Não Alcança

Toxiproxy é excelente. tc netem é excelente. As bibliotecas de chaos com LD_PRELOAD são excelentes. Todas operam na fronteira de rede ou do sistema operacional — interceptando conexões TCP, entrega de pacotes e chamadas de sistema.

Mas o que nenhuma delas consegue ver é: o que acontece dentro do seu HikariPool quando ele tenta obter uma conexão e encontra um timeout. O que acontece dentro do seu ScheduledExecutorService quando está saturado. O que acontece dentro da implementação SSL do JDK quando encontra uma retransmissão. O que acontece dentro do seu pool de threads quando as tarefas começam a se acumular mais rápido do que os workers conseguem processá-las.

São modos de falha internos à JVM. Eles não se manifestam como falhas de rede na camada TCP. Manifestam-se como exceções lançadas por classes do JDK — `SQLException`, `RejectedExecutionException`, `SSLHandshakeException`, `TimeoutException` — e como padrões de esgotamento de recursos que só se tornam visíveis quando você instrumenta os pontos de chamada corretos dentro da própria JVM.

É essa lacuna que o agente de chaos para JVM preenche. Ele não substitui a camada LD_PRELOAD — ele se compõe com ela. Juntos, oferecem injeção de falhas tanto na fronteira do SO quanto na fronteira interna da JVM: a superfície de falha completa de uma aplicação Java.

62 Pontos de Chamada no JDK: A Superfície Completa

O agente instrumenta 62 pontos de chamada específicos no JDK — não escolhidos aleatoriamente, mas os que importam para workloads de backend empresarial. A seleção foi orientada pela análise de incidentes em falhas de sistemas distribuídos:

**Threading & Executors** Thread.start(), ThreadPoolExecutor.execute(), ScheduledExecutorService.schedule(), CompletableFuture.runAsync/supplyAsync, operações BlockingQueue (put, offer, poll, take), submissões ao ForkJoinPool, ciclo de vida de threads virtuais

**Rede & I/O** Socket.connect/read/write, SocketChannel.connect/read/write, Selector.select/selectNow, operações DatagramChannel, ServerSocket.accept

**DNS & Resolução de Nomes** InetAddress.getByName/getAllByName — o caminho de resolução interno do JDK antes de chamar o resolvedor do SO

**JDBC & Acesso a Dados** DataSource.getConnection (obtenção de conexão do pool), Statement.execute/executeQuery, operações PreparedStatement — no nível em que Hikari, DBCP e c3p0 convergem

**HTTP Clients** HttpURLConnection, HttpClient (JDK 11+) — send, sendAsync

**SSL/TLS** SSLEngine.wrap/unwrap, SSLSocket.startHandshake

**Tempo** System.currentTimeMillis(), System.nanoTime() — com efeitos FIXED offset, DRIFT e FREEZE

**Internos da JVM** System.gc(), carregamento de classes, reflection, serialização/desserialização de objetos, ThreadLocal.get/set, lookups JNDI, operações JMX, carregamento de bibliotecas nativas, inflação ZIP

java
// 62 interception targets across the JDK surface
Sealed interface ChaosSelector permits
    ThreadSelector,       // Thread.start, pool operations
    ExecutorSelector,     // ThreadPoolExecutor, ForkJoinPool
    QueueSelector,        // BlockingQueue operations
    FutureSelector,       // CompletableFuture
    SocketSelector,       // Socket, ServerSocket
    NioSelector,          // SocketChannel, Selector
    JdbcSelector,         // DataSource.getConnection, Statement
    HttpSelector,         // HttpURLConnection, HttpClient
    DnsSelector,          // InetAddress resolution
    SslSelector,          // SSLEngine, SSLSocket
    ClockSelector,        // System.currentTimeMillis, nanoTime
    GcSelector,           // System.gc
    ClassloadingSelector, // ClassLoader.loadClass
    SerializationSelector,// ObjectInputStream, ObjectOutputStream
    ThreadLocalSelector,  // ThreadLocal.get, set
    VirtualThreadSelector;// VirtualThread lifecycle

ByteBuddy + Bootstrap Bridge: Como Funciona

Instrumentar classes do JDK não é trivial. As classes do JDK são carregadas pelo bootstrap classloader — o classloader raiz que não possui pai. O código do agente reside no agent classloader, que o bootstrap classloader não consegue enxergar pelo nome. Fazer essa ponte corretamente exige uso cuidadoso de JVMTI e do Java Memory Model.

**Passo 1 — Premain ou agentmain.** O agente se conecta na inicialização (via `-javaagent:`) ou dinamicamente em tempo de teste (via JDK Attach API). Ao conectar dinamicamente, o Spring Boot test starter chama `VirtualMachine.attach(processId)` e injeta o jar do agente.

**Passo 2 — Injeção no bootstrap.** Uma classe `BootstrapDispatcher` é extraída do jar do agente e escrita em um JAR temporário. Esse JAR é adicionado ao bootstrap classpath via `Instrumentation.appendToBootstrapClassLoaderSearch()`. O bootstrap classloader agora consegue enxergar `BootstrapDispatcher`.

**Passo 3 — Wiring com MethodHandle.** Um array `MethodHandle[]` de 62 slots é construído, um handle por alvo de interceptação, apontando para a implementação no agent classloader. O array e um objeto `delegate` são publicados no `BootstrapDispatcher` como campos `volatile`. O JMM garante que qualquer thread que observe `delegate != null` também enxergue o array `handles` completamente inicializado.

**Passo 4 — Inlining de advice de bytecode.** O mecanismo `@Advice` do ByteBuddy copia o bytecode do advice diretamente no corpo do método JDK instrumentado. Não há dispatch virtual nem chamada de interface — o bytecode inserido é inline. Após a compilação JIT no aquecimento (~10.000 invocações), toda a cadeia de dispatch compila para código nativo.

java
// Hot path after JIT compilation — effectively just:
public static void connect(Socket socket, SocketAddress endpoint, int timeout) {
    Object delegate = BootstrapDispatcher.delegate;  // volatile read
    if (delegate != null) {                           // null check — fast path
        BootstrapDispatcher.handles[SOCKET_CONNECT]   // MethodHandle
            .invoke(delegate, socket, endpoint);       // → ChaosRuntime
    }
    originalConnect(socket, endpoint, timeout);       // real JDK call
}
// Zero-scenario overhead: one volatile read + one null check + one untaken branch
// Measured: ~60 ns per call in the uncontested case after warm-up

Isolamento de Sessão: Cada Teste Tem Seu Próprio Chaos

O problema de engenharia mais difícil em um agente de chaos no nível da JVM não é a instrumentação de bytecode — é o isolamento. Múltiplos testes JUnit rodam concorrentemente na mesma JVM. Se o teste A injeta falhas de conexão JDBC, o teste B não pode ser afetado por essas falhas.

A solução é `ChaosSession`, com suporte em `ThreadLocal<UUID>`.

Cada método de teste (ou classe de teste, dependendo do ciclo de vida) recebe um ID de sessão único. Os cenários de chaos podem ser registrados como com escopo na JVM (afeta todas as threads) ou com escopo na sessão (afeta apenas as threads que carregam o ID de sessão deste teste). O `ThreadLocal` fornece o vínculo.

Para tarefas submetidas a executors, o ID de sessão precisa se propagar da thread submetente para a thread worker. O agente instrumenta `ThreadPoolExecutor.execute()` para envolver o `Runnable` submetido com um decorator que captura o ID de sessão atual e o restaura na thread worker antes de executar a tarefa. Essa propagação acontece de forma transparente — o código da aplicação não sabe que está ocorrendo.

java
@ChaosTest  // @SpringBootTest composed annotation
class OrderServiceChaosTest {

    @Test
    void retryLogic_handles_transient_jdbc_failures(ChaosSession session) {
        var scenario = ChaosScenario.builder("transient-db")
            .selector(ChaosSelector.jdbc(OperationType.JDBC_CONNECTION_ACQUIRE))
            .effect(ChaosEffect.reject("chaos: simulated pool timeout"))
            .activationPolicy(ActivationPolicy.builder()
                .probability(1.0)
                .maxApplications(2)   // First 2 borrows fail, 3rd succeeds
                .build())
            .build();

        try (var handle = session.activate(scenario)) {
            try (var scope = session.bind()) {
                // This thread and any executor tasks it submits
                // will see the JDBC failures — nothing else will
                var result = orderService.placeOrder(testOrder);
                assertThat(result.status()).isEqualTo(OrderStatus.CONFIRMED);
            }
        }
        // Chaos cleaned up — next test is unaffected
    }
}

Spring Boot 3 e 4: Configuração Zero

Os starters de teste do Spring Boot existem porque o wiring deve ser invisível. Você não precisa entender ByteBuddy, o bootstrap classloader ou JVMTI para rodar seu primeiro teste de chaos. O starter cuida de:

- Definir programaticamente `jdk.attach.allowAttachSelf=true` antes de a JVM processar esse flag (usando um inicializador estático na classe de auto-configuração do starter) - Detectar se o agente já está conectado (idempotente — seguro na execução paralela de classes de teste) - Conectar o agente via `VirtualMachine.attach("0")` (self-attach) se ainda não estiver presente - Registrar `ChaosControlPlane` e `ChaosSession` como beans Spring no ApplicationContext de teste - Fornecer resolução de parâmetros JUnit 5 para argumentos de construtor `ChaosSession` e `ChaosControlPlane`

O starter de runtime (para deploy fora de testes) adicionalmente expõe `/actuator/chaos` como um endpoint Actuator protegido para gerenciamento de cenários em produção.

xml
<!-- Spring Boot 3 — add to pom.xml testImplementation -->
<dependency>
    <groupId>com.macstab.chaos</groupId>
    <artifactId>chaos-agent-spring-boot3-test-starter</artifactId>
    <version>${chaos-agent.version}</version>
    <scope>test</scope>
</dependency>

<!-- That's it. No -javaagent flag. No JVM args. No beans to define. -->

<!-- Spring Boot 4 -->
<dependency>
    <groupId>com.macstab.chaos</groupId>
    <artifactId>chaos-agent-spring-boot4-test-starter</artifactId>
    <version>${chaos-agent.version}</version>
    <scope>test</scope>
</dependency>

O Pipeline de Ativação com Oito Portões

Cada avaliação de cenário de chaos passa por oito portões em sequência. Todos os portões precisam ser aprovados para que o efeito seja aplicado. Os portões são rápidos — avaliados na thread chamante, sem I/O e com bloqueio mínimo.

**Portão 1 — Verificação de início.** O cenário foi ativado? (Leitura barata de flag.)

**Portão 2 — Correspondência de ID de sessão.** Se com escopo de sessão, a `ThreadLocal` da thread atual carrega o UUID correto? (Leitura de thread-local, sem bloqueio.)

**Portão 3 — Correspondência de selector.** Esse ponto de chamada corresponde ao selector do cenário? (Comparação de enum, O(1).)

**Portão 4 — Janela de ativação.** O horário atual está dentro da janela de início/fim configurada do cenário? (Duas comparações de long.)

**Portão 5 — Contagem de aquecimento.** O cenário correspondeu pelo menos N vezes antes de começar a ser aplicado? (Leitura de AtomicLong.)

**Portão 6 — Limite de taxa.** O token bucket de janela deslizante tem capacidade? (Bloco sincronizado; caminho sem contenção ~5 ns.)

**Portão 7 — Probabilidade.** Sorteio aleatório contra a probabilidade configurada. (SplittableRandom com seed composto por ID do cenário + contagem de correspondências — determinístico para reprodução de falhas.)

**Portão 8 — Máximo de aplicações.** Loop CAS sobre AtomicLong para impor um limite rígido. (Loop compareAndSet; correto sob contenção — diferente de um simples incrementAndGet que pode ultrapassar o limite.)

Todos os oito portões aprovados → aplica o efeito. Qualquer portão reprovado → passa diretamente para o método JDK real sem alterações.

Estressores em Background: Além das Falhas no Caminho de Requisição

Além dos efeitos inline no caminho de requisição (delay, rejeição, injeção de exceção), o agente suporta estressores em background — threads vinculadas ao ciclo de vida que aplicam continuamente pressão de recursos independentemente do tráfego.

Os estressores simulam os modos de falha de degradação lenta que não são disparados por operações específicas, mas se acumulam ao longo do tempo:

**Estressores de memória:** Pressão de heap (reter blocos byte[]), pressão de GC (churn de alocação), pressão de Metaspace (definições sintéticas de classes), pressão de buffer direto (ByteBuffer off-heap), inundação de string intern

**Estressores de JVM:** Pressão no code cache (inundação de classes geradas pelo ByteBuddy para thrashing do JIT), backlog do finalizador (inundação da fila phantom-reference), inundação da fila de referência, tempestades de safepoint (retransformação periódica para disparar pausas stop-the-world)

**Estressores de thread:** Thread leak (threads permanentemente em espera consumindo memória de stack), ThreadLocal leak (entradas em threads reutilizáveis que se acumulam), injeção de deadlock (deadlock real de monitor JVM entre N threads — verificado com ThreadMXBean), contenção de monitor (threads em background competindo por um lock compartilhado), threads keep-alive (impedem o encerramento da JVM)

Os estressores são iniciados quando um cenário é ativado e interrompidos quando ele é fechado. Eles se compõem com efeitos inline: você pode ter simultaneamente rejeições JDBC no caminho de requisição e um estressor de code cache ativo, testando se sua aplicação se recupera de falhas de conexão enquanto já está sob pressão de compilação JIT.

  • Pressão de heap: reter MB/s configuráveis em alocações byte[] para forçar pressão de GC
  • Pressão de Metaspace: definir classes sintéticas em um ClassLoader isolado para consumir permgen/metaspace
  • Pressão no code cache: gerar classes com ByteBuddy para saturar o compilador JIT
  • Tempestade de safepoint: forçar retransformação periódica para disparar pausas stop-the-world
  • Deadlock real: duas threads adquirem monitors em ordem oposta — detectado pelo ThreadMXBean
  • Thread leak: colocar threads em espera permanente para esgotar a memória de stack -Xss ao longo do tempo

Exemplo: Validando o Comportamento de SLA sob Esgotamento de Conexões JDBC

Um cenário de teste concreto que exercita o valor central do agente: provar que seu serviço responde corretamente a um pagamento com um alvo de SLA quando o pool de conexões do banco de dados está sob pressão.

java
@SpringBootTest
@ChaosTest
class PaymentSLAChaosTest {

    @Autowired PaymentService paymentService;

    @Test
    void payment_meets_sla_under_intermittent_jdbc_pressure(ChaosSession session) {
        // Activate 20% JDBC connection failure — simulates pool under load
        var jdbcChaos = ChaosScenario.builder("jdbc-pressure")
            .selector(ChaosSelector.jdbc(OperationType.JDBC_CONNECTION_ACQUIRE))
            .effect(ChaosEffect.delay(Duration.ofMillis(150)))  // Not rejection — delay
            .activationPolicy(ActivationPolicy.probability(0.20))
            .build();

        // Also activate a background heap stressor
        var heapStressor = ChaosScenario.builder("heap-pressure")
            .effect(ChaosEffect.stressor(StressorType.HEAP_PRESSURE)
                .retainMbPerSecond(50)
                .build())
            .build();

        try (var h1 = session.activate(jdbcChaos);
             var h2 = session.activate(heapStressor)) {

            try (var scope = session.bind()) {
                long start = System.nanoTime();
                var result = paymentService.process(testPayment);
                long elapsedMs = (System.nanoTime() - start) / 1_000_000;

                assertThat(result.status()).isEqualTo(PaymentStatus.COMPLETED);
                assertThat(elapsedMs).isLessThan(500);  // SLA: p99 < 500ms
            }
        }
    }
}

JMX e Observabilidade

O agente expõe um JMX MBean em `com.macstab.chaos.jvm:type=ChaosDiagnostics` que fornece um snapshot ao vivo de todos os cenários ativos — seu estado atual, contagens de correspondências e contagens de aplicações — sem exigir alterações de código ou configuração adicional.

Isso é útil durante a depuração de testes: se um teste de chaos está falhando inesperadamente, você pode conectar JConsole ou VisualVM à JVM de teste e inspecionar exatamente quais cenários estão ativos e quantas vezes eles foram disparados.

O barramento de observabilidade permite publicar eventos de chaos em sistemas externos de métricas. Se você está rodando testes de chaos em um ambiente de integração compartilhado e quer correlacionar eventos de chaos com dashboards do Grafana ou traces distribuídos, o barramento fornece o ponto de integração.

  • JMX MBean: lista de cenários ativos, contagens de correspondências, contagens de aplicações, estado atual por cenário
  • API de snapshot in-process: consultável a partir do código de teste para asserções sobre o comportamento do chaos
  • Debug dump: representação textual de todos os cenários e estado — útil na saída de falhas de teste
  • Barramento de observabilidade: publisher de eventos plugável para integração com métricas/tracing

Key Takeaways

O agente de chaos para JVM preenche a lacuna entre a injeção de falhas no nível de infraestrutura e os modos de falha internos reais da JVM. 62 pontos de chamada no JDK, isolamento de sessão por teste, auto-configuração Spring Boot 3/4, estressores em background para falhas de degradação lenta, e um caminho de dispatch otimizado pelo JIT que custa ~60 ns por chamada no caso sem cenários ativos.

O agente se compõe com a biblioteca LD_PRELOAD em C99 para cobertura completa de falhas em toda a pilha: falhas no nível de syscall abaixo da JVM, falhas no nível de bytecode dentro dela. Ambas são orquestradas pelo sistema de anotações do framework de testes Java — um único teste, ambas as camadas ativas, zero sobreposição de configuração.

Chaos engineering como portão de CI está pronto. A única questão é qual modo de falha seu circuit breaker ainda não está tratando.

#chaos-engineering #java #jvm #java-agent #bytebuddy #junit5 #spring-boot #bytecode #resilience #ci-cd
E

Engineering Team

Senior Solutions Architects

Construímos sistemas distribuídos desde antes de 'microsserviços' ser uma expressão comum. Nossas cicatrizes têm histórias para contar.