Supervisão

O supervisor gerencia ciclos de vida de serviços, tratando ordenação de inicialização, reinicializações automáticas e encerramento gracioso. Serviços com auto_start: true são iniciados quando a aplicação inicia.

Configuração de Ciclo de Vida

Serviços se registram com o supervisor usando um bloco lifecycle. Para processos, use process.service para encapsular uma definição de processo:

# Process definition (the code)
- name: worker_process
  kind: process.lua
  source: file://worker.lua
  method: main

# Supervised service (wraps the process with lifecycle management)
- name: worker
  kind: process.service
  process: app:worker_process
  host: app:processes
  lifecycle:
    auto_start: true
    startup: required
    start_timeout: 30s
    stop_timeout: 10s
    stable_threshold: 5s
    requires:
      - app:database
    restart:
      initial_delay: 2s
      max_delay: 60s
      max_attempts: 10

host precisa referenciar um host de processos configurado. A entrada em requires precisa resolver para outro serviço supervisionado ou, por meio da extração de dependências do registro, para um serviço supervisionado que seja proprietário do recurso referenciado.

Campo Padrão Descrição
auto_start false Inicia automaticamente quando supervisor inicia
startup required Política de inicialização de uma raiz automática: required bloqueia o boot em caso de falha; optional pode falhar e continuar tentando sem bloquear ramos independentes
start_timeout 10s Tempo máximo permitido para inicialização
stop_timeout 10s Tempo máximo para encerramento gracioso
stable_threshold 5s Tempo de execução antes do serviço ser considerado estável
requires [] Serviços que devem estar executando primeiro (alias legado: depends_on)
startup required required reporta um auto-start falho ou bloqueado como erro de transação; optional deixa o serviço continuar tentando em segundo plano sem falhar o lote

Resolução de Dependências

O supervisor resolve dependências de duas fontes:

  1. Dependências explícitas declaradas em requires (ou o legado depends_on)
  2. Dependências extraídas do registro de referências de entradas (ex: database: app:db na sua configuração)
graph LR
    A[HTTP Server] --> B[Router]
    B --> C[Handler Function]
    C --> D[Database]
    C --> E[Cache]

Dependências iniciam antes dos dependentes. Se o Serviço C depende de A e B, ambos A e B devem alcançar o estado Running antes de C iniciar.

Você não precisa repetir uma referência de infraestrutura em requires quando a extração de dependências do registro consegue rastrear essa referência até um serviço supervisionado. Use requires para dependências de ciclo de vida que ainda não estejam expressas por referências de entradas.

Política de Reinicialização

Quando um serviço falha, o supervisor tenta novamente de acordo com seu bloco restart:

lifecycle:
  restart:
    initial_delay: 1s      # First retry wait
    max_delay: 90s         # Accepted backoff cap; see current behavior below
    backoff_factor: 2.0    # Accepted multiplier; see current behavior below
    jitter: 0.1            # ±10% randomization
    max_attempts: 0        # 0 = infinite retries

No runtime v0.3.32a, o supervisor cria uma nova calculadora de backoff para cada tentativa e usa somente seu primeiro intervalo. Portanto, cada nova tentativa aguarda initial_delay com o jitter configurado — 0,9s a 1,1s para os valores acima. backoff_factor e max_delay são campos de configuração aceitos, mas não alteram esse cronograma no runtime fixado.

max_attempts conta a primeira inicialização que falhou. O valor 1 não permite nova tentativa, 10 permite no máximo nove inicializações posteriores e 0 permite tentativas ilimitadas.

Quando um serviço executa por mais tempo que stable_threshold, o contador de tentativas reseta. Isso previne que falhas transitórias escalem delays permanentemente.

Erros Terminais

Estes erros param tentativas de retry:

  • Cancelamento de contexto
  • Requisição de terminação explícita
  • Erros marcados como não-retentáveis

Contexto de Segurança

Serviços podem executar com uma identidade de segurança específica:

# Process definition
- name: admin_worker_process
  kind: process.lua
  source: file://admin_worker.lua
  method: main

# Supervised service with security context
- name: admin_worker
  kind: process.service
  process: app:admin_worker_process
  host: app:processes
  lifecycle:
    auto_start: true
    security:
      actor:
        id: "service:admin-worker"
        meta:
          role: admin
      groups:
        - app:admin_policies
      policies:
        - app:data_access

O contexto de segurança define:

Campo Descrição
actor.id String de identidade para este serviço
actor.meta Metadados chave-valor (role, permissões, etc.)
groups Grupos de políticas a aplicar
policies Políticas individuais a aplicar

Código executando no serviço herda este contexto de segurança. O módulo security pode então verificar permissões:

local security = require("security")

if security.can("delete", "users") then
    -- allowed
end
Quando nenhum contexto de segurança está configurado, o serviço executa sem um ator. No modo estrito (padrão), verificações de segurança falham. Configure um contexto de segurança para serviços que precisam de autorização.

Reregistro e Substituição

Uma mudança no registry pode reregistrar um ID que já tem um controller em execução. Se o registro carrega a mesma instância de serviço, nada é perturbado. Se carrega uma instância diferente — o manager reconstruiu o serviço porque sua configuração mudou — o supervisor aposenta o controller existente e adota o substituto.

A aposentadoria abrange mais que o serviço isolado. Um dependente em execução capturou a instância substituída, então não pode continuar rodando contra um serviço que está sendo trocado por baixo dele; o fecho de aposentadoria é o serviço substituído mais todo serviço em execução que depende dele, parados em ordem de dependência (dependentes primeiro). Serviços já parados não são parados uma segunda vez — um manager que para sua própria instância antes de reregistrar não recebe um Stop redundante.

A transferência é transacional:

  1. O plano é computado sem tocar em nada, então uma falha de planejamento deixa o conjunto em execução intacto.
  2. O lote de paradas é executado. Se qualquer parada falhar, a transferência é rejeitada: os serviços que o lote já parou são reerguidos e o erro é reportado. Um serviço que não pôde ser reerguido é nomeado nesse erro. O supervisor termina possuindo o mesmo conjunto em execução que tinha antes do commit, nunca um parcialmente aposentado.
  3. Somente depois de o lote ter sucesso os controllers aposentados são descartados e cancelados, liberando as instâncias de serviço substituídas.
  4. O substituto é criado e iniciado através do mesmo sequenciador ciente de dependências de qualquer outro início, e os dependentes que foram parados para a transferência voltam a subir contra a instância adotada.

Um serviço que estava em execução antes da substituição é reiniciado depois dela mesmo quando o novo registro define auto_start: false — substituir um serviço ativo é uma atualização, não uma parada implícita. Reiniciar um dependente parado é regido por sua própria política de reinicialização e não bloqueia o commit.

Estados de Serviço

stateDiagram-v2
    [*] --> Unknown
    Unknown --> Starting
    Starting --> Running
    Running --> Stopping
    Stopping --> Stopped
    Stopping --> Failed : timeout/cancel
    Stopped --> [*]

    Running --> Failed
    Starting --> Failed
    Failed --> Starting : retry
    Running --> Exited
    Starting --> Exited
    Exited --> [*]

O supervisor transiciona serviços através destes estados:

Estado Descrição
Unknown Registrado mas não iniciado
Starting Inicialização em progresso
Running Operando normalmente
Stopping Encerramento gracioso em progresso
Stopped Terminado de forma limpa
Exited Terminado por requisição explícita ou por um erro terminal/não recuperável
Failed Erro ocorreu, pode tentar novamente

Ordem de Inicialização e Encerramento

Inicialização: Dependências primeiro, depois dependentes. Serviços no mesmo nível de dependência podem iniciar em paralelo.

Encerramento: Dependentes primeiro, depois dependências. Isso garante que serviços dependentes terminem antes de suas dependências pararem.

Inicialização: database → cache → handler → http_server
Encerramento:  http_server → handler → cache → database

Em SIGINT ou SIGTERM, o runtime inicia um encerramento gracioso e a sequência inteira roda sob um único orçamento, shutdown.timeout na configuração do runtime (padrão 30s). Esse orçamento é um prazo novo que não herda o contexto interrompido, então um Ctrl-C não corta o encerramento dos componentes; o stop_timeout por serviço continua limitando cada parada individual dentro dele. Um segundo sinal pula a sequência e sai imediatamente.

# .wippy.yaml
shutdown:
  timeout: 60s

Veja Também