Pular para conteúdo

0020 — GraalVM native-image (CE 25)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-20 · Decidido em: 2026-08-20

Contexto. O integrador roda como shadowJar na JVM em 6 deployments Coolify (um por cliente, mesmo binário). Compilar para GraalVM native-image derruba RAM e boot por instância — economia ×6 — e move o build spike para off-host. O Micronaut já é nativo por design; os bloqueadores eram duas libs sujas em native no grafo de alcance: Apache POI (export XLSX, arrasta XMLBeans + java.awt.Font) e firebase-admin (push FCM, gRPC/reflexão).

Decisão.

  • Export POI → FastExcel writer (org.dhatim:fastexcel:0.20.2, StAX, sem XMLBeans/AWT). TableExportService reescrito com Workbook/Worksheet.value/finish. POI removido por completo — inclusive de teste: o teste de caracterização reabre o XLSX com org.dhatim:fastexcel-reader (ReadableWorkbook), não POI. O runtimeOnly log4j-to-slf4j saiu junto (existia só porque o POI trazia log4j-api). Padrão da casa (norma 0023, mesmo do bi-transporte-xls).
  • Largura de coluna heurística. FastExcel writer não tem autoSizeColumn (o do POI usava java.awt.Font, morto em native). Substituído por ws.width(col, maxChars) com maxChars = maior contagem de char da coluna, teto 60. Mudança cosmética explícita (constituição §6/REGRA 3): as larguras não batem 1:1 com o autoSize antigo; o conteúdo tem paridade (provada no teste de caracterização). O autoSize antigo já era best-effort (try/catch ignore).
  • Perfil de build native. Bloco graalvmNative { binaries { named("main") } }: -PnativeQuick → -Ob (quick build, loop de diagnóstico) / sem a flag → -Os (otimiza tamanho, release); --gc=serial sempre (menor footprint de RAM). Reaproveita o toolchainDetection = false e o dockerfileNative.jdkVersion = "25" já presentes.
  • Reachability metadata só onde faltou. O nativeCompile passou de primeira, sem reflect-config manual: Micronaut + o GraalVM reachability-metadata repository cobrem firebase-admin (grpc-netty- shaded + protobuf entram na imagem) e o Sentry. O gap era de runtime, no boot: o logback instancia appenders por reflexão. reflect-config em src/main/resources/META-INF/native-image/br.com.xadm/integrador/ registrando ConsoleAppender, RollingFileAppender, TimeBasedRollingPolicy, PatternLayoutEncoder, SentryAppender. E resource-config incluindo firebase-admin-sdk.json — carregado via getResourceAsStream em runtime, não auto-detectado; sem ele o push desabilitaria silenciosamente (default seguro: incluir mesmo que em prod a credencial venha de disco).
  • FCM: metadata bastou, sem rewrite. O send() do FCM é HTTP v1 REST por baixo, não gRPC (o poço gRPC é do Firestore, que o app não usa). O fallback previsto — reescrever FcmPushService para FCM HTTP v1 cru — não foi acionado: compile inclui firebase-admin e o boot não quebra (init lazy).

Consequências / trade-offs.

  • Imagem native integrador:latest 251MB (vs integrador:pre JVM 624MB). Boot limpo em native (/health → {"versao":"1.3.5","status":"UP"}), smoke do export verde (GET /export/xlsx/municipio → 200, XLSX válido; tabela inexistente → 404).
  • dockerBuildNative exige --no-configuration-cache. Com o config-cache ligado (gradle.properties) a task dockerfileNative/generateResourcesConfigFile falha na serialização (bug Micronaut+Gradle). O nativeCompile não encadeia essa task e passa com o cache. O build do Coolify deve passar a flag (ou desligar o config-cache) no caminho native.
  • Rollout incremental por instância (as 6 são o mesmo binário): subir 1 piloto — a de menor tráfego — validar, depois as demais. Rollback por instância.
  • WARNINGs de grpc-netty-shaded no build (negotiators internos não encontrados) são benignos: os caminhos gRPC não são exercitados (FCM é HTTP v1).

Verificação. ./gradlew check verde (checkstyle + Testcontainers + piso JaCoCo). nativeCompile -PnativeQuick verde; binário boota limpo e passa smoke dos caminhos quentes; dockerBuildNative --no-configuration-cache gera a imagem. Envio FCM real em native não exercitado (dev tem push.enabled=false; exige credencial + FCM de prod) — fica para o smoke da instância piloto.

Fora de escopo: rewrite do FcmPushService para HTTP v1 (fallback não acionado); app central notificacao.xadm.biz (centralizaria email/push/telegram/wa; longo prazo); o e2e funcional (vive e roda em outro repo). Ver o runbook de build em dev/native-image.