Pular para o conteúdo principal
Página não listada
Esta página não está listada. Mecanismos de busca não irão indexá-la, e somente usuários que possuam o link direto poderão acessá-la

Retry e Resiliência

Toda EXECUTABLE_TASK executada pelo motor pode falhar — uma exceção lançada dentro de um TaskHandler, uma falha transitória de rede em uma integração externa, um bug. O Kikwiflow trata esse caminho como um cidadão de primeira classe do modelo de execução, não como uma exceção não tratada: toda falha passa por io.kikwiflow.execution.FailureHandler, que decide entre reagendar uma nova tentativa ou abrir um incidente.

O ciclo de vida de uma falha

// Sobrecarga de conveniência; delega para a versão de 4 argumentos com listas vazias.
public void handleFailure(ExecutableTask task, Exception exception) { /* ... */ }

public void handleFailure(ExecutableTask task, Exception exception,
List<OutboxEventEntity> criticalEvents, String tenantId) {
RetryPolicyEvaluator.RetryEvaluationResult evaluation = policyEvaluator.evaluate(task, exception, task.retryPolicy());
Throwable rootCause = exception.getCause() != null ? exception.getCause() : exception;
boolean isUnhandledBusinessError = rootCause instanceof ProcessErrorException;

if (!evaluation.shouldCreateIncident() && !isUnhandledBusinessError) {
// reagenda: status PENDING, retries = retriesLeft, executions++, dueDate = próxima tentativa, error = mensagem
// criticalEventRecorder.recordRetryScheduled(...) -> anexa um RETRY_SCHEDULED à lista de eventos
} else {
// esgotou tentativas (ou é erro fatal/de negócio não tratado): status ERROR + cria Incident
// criticalEventRecorder.recordIncidentCreated(...) -> anexa um INCIDENT_CREATED à lista de eventos
}
// tarefas atualizadas + incidentes + eventos críticos vão num único UnitOfWork -> repository.commitWork(uow)
}

Os parâmetros criticalEvents/tenantId existem para que os eventos de outbox já construídos durante a execução que falhou (ex.: o FLOW_NODE_FINISHED(ERROR) do próprio nó) e os que a falha gera agora (RETRY_SCHEDULED ou INCIDENT_CREATED, via CriticalEventRecorder) sejam persistidos na mesma transação que atualiza a ExecutableTask e cria o Incident — nada de evento perdido só porque o nó terminou em exceção. Esses eventos só são de fato gravados se o outbox estiver habilitado (ver Eventos e Observabilidade); ExecutableTask não carrega tenantId, então quem chama (que já tem o ProcessInstance) o repassa para INCIDENT_CREATED/RETRY_SCHEDULED não ficarem sem tenant.

Toda a decisão de quando tentar de novo é delegada à interface RetryPolicyEvaluator — uma SPI substituível, assim como TaskHandler e AnswerProvider:

public interface RetryPolicyEvaluator {
RetryEvaluationResult evaluate(ExecutableTask task, Exception exception, RetryPolicy policy);

record RetryEvaluationResult(long retriesLeft, Instant nextDueDate, boolean shouldCreateIncident) {}
}

A implementação padrão, DefaultRetryPolicyEvaluator, é registrada automaticamente pelo kikwi-spring-boot-autoconfigure com @ConditionalOnMissingBean — ou seja, você pode fornecer seu próprio bean RetryPolicyEvaluator para, por exemplo, integrar com um sistema de alertas antes de decidir se cria um incidente, sem tocar em código do motor.

RetryPolicy: a política declarada no nó

public record RetryPolicy(
RetryStrategy strategy,
int maxRetries,
String initialInterval,
Double multiplier,
String maxInterval,
List<String> intervals
) {}

public enum RetryStrategy {
LINEAR,
EXPONENTIAL_BACKOFF
}

RetryPolicy é opcional em ExecutableTaskDefinition. Se ausente, o motor recorre ao fallback global (kikwiflow.retry.default-retry-interval, um intervalo fixo aplicado a toda tentativa, sem crescimento).

EXPONENTIAL_BACKOFF

private Instant calculateExponential(RetryPolicy policy, int attemptIndex) {
Duration initial = Duration.parse(policy.initialInterval());
double multiplier = policy.multiplier() != null ? policy.multiplier() : 2.0;
long calculatedMillis = (long) (initial.toMillis() * Math.pow(multiplier, attemptIndex));
Duration calculatedDuration = Duration.ofMillis(calculatedMillis);
if (policy.maxInterval() != null) {
Duration max = Duration.parse(policy.maxInterval());
if (calculatedDuration.compareTo(max) > 0) calculatedDuration = max;
}
return Instant.now().plus(calculatedDuration);
}
{
"retryPolicy": {
"strategy": "EXPONENTIAL_BACKOFF",
"maxRetries": 5,
"initialInterval": "PT10S",
"multiplier": 2.0,
"maxInterval": "PT10M"
}
}

Com essa política: 10s, 20s, 40s, 80s, 160s (limitado a 600s pelo maxInterval) — o cálculo usa attemptIndex = task.executions() (o número de execuções já realizadas), então o crescimento é determinístico e não depende de estado externo.

LINEAR

private Instant calculateLinear(RetryPolicy policy, int attemptIndex) {
if (policy.intervals() == null || policy.intervals().isEmpty()) {
return Instant.now().plus(Duration.ofMinutes(1)); // fallback de 1 minuto
}
int index = Math.min(attemptIndex, policy.intervals().size() - 1);
return Instant.now().plus(Duration.parse(policy.intervals().get(index)));
}
{
"retryPolicy": {
"strategy": "LINEAR",
"maxRetries": 3,
"intervals": ["PT5S", "PT30S", "PT2M"]
}
}

A lista intervals é indexada pelo número da tentativa; se houver mais tentativas do que intervalos declarados, o último intervalo da lista é reutilizado indefinidamente (Math.min(attemptIndex, size - 1)).

Exceções fatais: pulando retry inteiramente

kikwiflow:
retry:
fatal-exceptions:
- java.lang.NullPointerException
- java.lang.IllegalArgumentException

Qualquer exceção cujo nome de classe totalmente qualificado esteja nessa lista pula retry completamente e abre um incidente na primeira ocorrência, independentemente de maxRetries:

if (fatalExceptionsSet.contains(rootCause.getClass().getName())) {
return new RetryEvaluationResult(0L, Instant.now(), true);
}

:::tip Quando usar fatal-exceptions Reserve essa lista para classes de erro que nunca se resolvem sozinhas com uma nova tentativa — erros de programação (NullPointerException, IllegalArgumentException) ou violações de contrato de dados. Retentar automaticamente esse tipo de falha só atrasa a visibilidade do problema real; é preferível abrir o incidente imediatamente e notificar um humano. :::

Erros de negócio não tratados também abrem incidente

Se um TaskHandler lançar ProcessErrorException e nenhum BOUNDARY_ERROR_HANDLER casar com o errorCode (ver Tratamento de Erros de Negócio), o FailureHandler trata isso como isUnhandledBusinessError = true e força a criação de um incidente, mesmo que a política de retry ainda tivesse tentativas disponíveis — um erro de negócio explícito não é uma falha transitória, e insistir em reexecutar o mesmo TaskHandler não vai mudar o resultado.

Incidentes

public record Incident(
String id, String type, String message, String stackTrace,
String processDefinitionId, String processInstanceId, String executionId,
Instant createdAt, IncidentStatus status, String taskDefinitionId) {}

public enum IncidentStatus { OPEN, RESOLVED }

type é "FAILED_JOB" para esgotamento de tentativas normais, ou "UNHANDLED_BUSINESS_ERROR" para ProcessErrorException sem handler correspondente. stackTrace é capturado integralmente (FailureHandler.getStackTrace), então o operacional tem contexto completo sem precisar correlacionar logs.

Retentando um incidente manualmente

KikwiflowEngine.retryIncident(incidentId, identityContext) — exposto via PUT /incidents/{id}/retry no kikwi-management-rest — reativa a tarefa associada de forma atômica: a ExecutableTask volta a PENDING com dueDate = now e o incidente muda para RESOLVED, ambos no mesmo UnitOfWork transacional. Isso é o mecanismo de "hot-fix operacional": corrigir a causa raiz (deploy de um patch, correção de dado) e retentar sem precisar reiniciar o processo do zero.

public void retryIncident(String incidentId, IdentityContext identityContext) {
Incident incident = kikwiEngineRepository.findIncidentById(incidentId).orElseThrow(/* ... */);
if (incident.status() != IncidentStatus.OPEN) throw new IllegalStateException("Only OPEN incidents can be retried.");
long retriesToRestore = failedTask.retryPolicy() != null
? failedTask.retryPolicy().maxRetries()
: kikwiflowConfig.getDefaultMaxRetries();
// ExecutableTask.toBuilder().status(PENDING).retries(retriesToRestore).error(null).dueDate(now()).executorId(null).build()
// Incident com status RESOLVED
// ambos no mesmo UnitOfWork -> commitWork()
}

retryIncident reseta retries para RetryPolicy.maxRetries() do nó original (a mesma política já copiada em ExecutableTask.retryPolicy() no momento em que a tarefa foi criada), com fallback para KikwiflowConfig.defaultMaxRetries (valor fixo 3) quando o nó não declara retryPolicy — o mesmo fallback usado em ContinuationService ao calcular o orçamento inicial de retries de uma tarefa nova. Ou seja, retentar manualmente um incidente devolve à tarefa o mesmo orçamento de tentativas que ela teria se estivesse começando agora, respeitando a política declarada no processo em vez de um valor arbitrário.

:::note defaultMaxRetries ainda não tem chave kikwiflow.* KikwiflowConfig.defaultMaxRetries é hoje um valor de fábrica hard-coded (3) — ao contrário de default-retry-interval e fatal-exceptions, não há uma propriedade kikwiflow.retry.default-max-retries que o KikwiflowAutoConfiguration leia para sobrescrevê-lo. Para mudar esse teto, ou declare um retryPolicy explícito no nó, ou forneça seu próprio bean KikwiflowConfig. Mesma observação em Incidentes (API REST). :::

Ajuste cirúrgico com overrideTaskRetryContext

Para cenários operacionais mais finos — mudar o dueDate de uma tarefa travada, injetar uma RetryPolicy diferente da declarada no processo, sem esperar o próximo ciclo de falha — KikwiflowEngine.overrideTaskRetryContext(executableTaskId, customDueDate, newRetriesCount, customRetryPolicy, identityContext) permite sobrescrever qualquer combinação desses campos em uma única chamada transacional, sempre reiniciando a tarefa com status: PENDING e limpando error/executorId.

Tabela de referência rápida

ConfiguraçãoOndeEfeito
ExecutableTaskDefinition.retryPolicyJSON do processo, por nóSobrescreve o fallback global para aquele nó específico.
kikwiflow.retry.default-retry-intervalapplication.ymlIntervalo fixo usado quando o nó não declara retryPolicy.
kikwiflow.retry.fatal-exceptionsapplication.ymlLista de FQCNs que pulam retry e abrem incidente na 1ª falha.
kikwiflow.execution.lock-timeout-millisapplication.ymlVer Workers e Tarefas Externas — não é retry de negócio, é liberação de lock de um worker morto.