← Release Manager: versões, prontidão e operação
09 / 10 · 60 MIN

Ensaios de prontidão e decisões de janela

Relaciona evidência, recursos e tempo de recuperação antes de confirmar uma janela de execução.

Definir o que falta demonstrar

Uma revisão de prontidão operacional liga as condições de entrada em serviço a observações concretas. Num serviço fictício de processamento de fundos, a lista inclui acesso APS, encaminhamento de um alerta e execução de uma tarefa de recuperação. Dois resultados positivos e uma tarefa não executada significam evidência incompleta. Não demonstram falha da recuperação nem prontidão por maioria. Regista critério, executor, identidade da versão e configuração, ambiente, momento, resultado observado e limitação. O responsável pela decisão precisa de perceber exatamente a lacuna, a sua consequência e quem pode resolvê-la. A orientação AWS sobre revisões de prontidão descreve uma prática adaptável ao contexto, não um formulário obrigatório do banco. O exercício também não permite transformar uma exceção aceite em prova de que a tarefa passou. Se houver decisão explícita de avançar com uma condição por cumprir, preserva separadamente essa condição e os limites da decisão.

Ensaiar com quem vai operar

Pede ao operador APS que execute a tarefa com a sua própria conta, o procedimento aplicável e os contactos disponíveis durante a janela. Observar o autor a executar com privilégios superiores não demonstra autonomia do operador. Um runbook deve identificar pré-condições, passos, resultado esperado, tratamento de erros e escalonamento. Quando o passo quatro diverge entre o documento e o estado do serviço, interrompe a passagem desse procedimento e esclarece a combinação correta antes de o considerar utilizável. Um ensaio R8/C3/E2 não cobre automaticamente R8/C4/E2: neste caso, C4 muda o destino do alerta, pelo que é necessário verificar encaminhamento e receção no contexto de destino. A evidência anterior continua útil para o que realmente exercitou. Não precisa de ser apagada, nem pode ser alargada a propriedades não observadas. Confirma também que as pessoas necessárias à recuperação continuam disponíveis depois da instalação; a sua presença apenas no início não cobre a decisão tardia.

Calcular a janela com as restrições reais

O plano didático prevê drenagem de oito minutos, aplicação de doze e middleware de dezoito em paralelo, smoke test de dez e validação de quinze. Como a junção espera ambos os ramos, a duração mínima é 8 + max(12,18) + 10 + 15 = 51 minutos. Se ambos os ramos exigirem o mesmo executor e este só puder realizar uma tarefa de cada vez, passam a ser sequenciais: 63 minutos. Para este exercício, o acordo exige ainda preservar 25 minutos de recuperação após a execução normal. São então necessários 88 minutos, mais 33 que a janela de 55. Conseguir um segundo executor habilitado reduz a necessidade para 76 minutos, continuando a faltar 21. Esta reserva adicional é uma restrição explícita do caso; não é uma regra universal nem um modelo de todos os caminhos de recuperação. Antes de confirmar, revê âmbito, sequência, recursos ou janela com os responsáveis. Abrir duas sessões não remove a restrição humana declarada.

Distinguir observação, exceção e decisão

O painel apresenta zero erros, mas a última amostra tem seis minutos e o critério fictício exige idade máxima de dois. O sinal está desatualizado: não prova saúde atual nem, isoladamente, indisponibilidade. Da mesma forma, uma autorização urgente para o incidente X até às 18:00 não cobre execução às 19:00 com uma funcionalidade Y acrescentada. Expõe lacunas de tempo e âmbito à autoridade definida. Se ocorrer um incidente, alinha a recuperação com a coordenação desse incidente; dois planos concorrentes podem alterar o estado um do outro. Guarda o código completo abaixo como release-readiness.py e executa python3 release-readiness.py. As catorze verificações exercitam apenas aritmética, dependências declaradas e lacunas em observações fornecidas. Altera uma duração ou observação e antecipa a asserção que falhará antes de executar. O programa não mede um serviço, não verifica permissões reais e não autoriza produção. Entrega uma nota com lacuna, evidência necessária, responsável e momento de decisão.

"""Original DR teaching fixtures. No network, deployment or production approval."""
import hashlib
import json
import platform
from pathlib import Path


def finish_times(tasks):
    """tasks: name -> (nonnegative minutes, already-declared predecessors)."""
    finished = {}
    for name, (minutes, predecessors) in tasks.items():
        if minutes < 0 or any(p not in finished for p in predecessors):
            raise ValueError("Durations must be nonnegative and predecessors declared first")
        finished[name] = max((finished[p] for p in predecessors), default=0) + minutes
    return finished


def requirement_gaps(required, observations):
    # A missing result is different from an observed failure.
    return {name: observations.get(name, "not-run") for name in required
            if observations.get(name) != "pass"}


def changed_context(observed, target):
    return sorted(k for k in observed.keys() | target.keys()
                  if k not in observed or k not in target or observed[k] != target[k])


def sample_state(now_minute, sampled_minute, maximum_age):
    age = now_minute - sampled_minute
    if age < 0:
        return "clock-inconsistent"
    return "current" if age <= maximum_age else "stale"


def exception_gaps(now_minute, expires_minute, requested, covered):
    # This exercise makes expiry exclusive. Real policy must define its own boundary.
    return {"expired": now_minute >= expires_minute,
            "uncovered": sorted(set(requested) - set(covered))}


def run():
    checks = []

    def check(name, actual, expected):
        if actual != expected:
            raise AssertionError((name, actual, expected))
        checks.append({"name": name, "actual": actual, "passed": True})

    parallel = {
        "drain": (8, []), "app": (12, ["drain"]),
        "middleware": (18, ["drain"]),
        "smoke": (10, ["app", "middleware"]), "validate": (15, ["smoke"]),
    }
    serial = {**parallel, "middleware": (18, ["app"])}
    p, s = finish_times(parallel), finish_times(serial)
    check("parallel_dependency_timing", p,
          {"drain": 8, "app": 20, "middleware": 26, "smoke": 36, "validate": 51})
    check("exclusive_executor_timing", s,
          {"drain": 8, "app": 20, "middleware": 38, "smoke": 48, "validate": 63})
    window, reserve = 55, 25
    check("window_plus_explicit_recovery_reserve",
          {"parallelRequired": p["validate"] + reserve,
           "serialRequired": s["validate"] + reserve,
           "parallelGap": p["validate"] + reserve - window,
           "serialGap": s["validate"] + reserve - window},
          {"parallelRequired": 76, "serialRequired": 88, "parallelGap": 21, "serialGap": 33})
    required = ["aps-access", "alert-route", "recovery-task"]
    observations = {"aps-access": "pass", "alert-route": "pass"}
    check("missing_is_not_observed_failure", requirement_gaps(required, observations),
          {"recovery-task": "not-run"})
    check("failed_is_retained", requirement_gaps(required, {**observations, "recovery-task": "fail"}),
          {"recovery-task": "fail"})
    check("all_observed_is_only_model_coverage", requirement_gaps(required, {**observations, "recovery-task": "pass"}), {})
    observed = {"build": "R8", "config": "C3", "environment": "E2"}
    check("same_build_different_config", changed_context(observed, {**observed, "config": "C4"}), ["config"])
    check("absent_target_context_is_not_equivalence", changed_context(observed, {"build": "R8", "config": "C3"}), ["environment"])
    check("stale_zero_error_sample", sample_state(19 * 60, 19 * 60 - 6, 2), "stale")
    check("freshness_boundaries", [sample_state(100, 98, 2), sample_state(100, 101, 2)], ["current", "clock-inconsistent"])
    check("exception_time_and_scope_are_separate", exception_gaps(19 * 60, 18 * 60, ["incident-X", "feature-Y"], ["incident-X"]),
          {"expired": True, "uncovered": ["feature-Y"]})
    check("exception_expiry_boundary", [exception_gaps(t, 1080, ["incident-X"], ["incident-X"])["expired"] for t in [1079, 1080]], [False, True])
    milestones = {"apiAvailable": 20 * 60 + 10, "backlogComplete": 20 * 60 + 45, "reconciled": 21 * 60}
    check("distinct_recovery_milestones",
          [milestones["backlogComplete"] - milestones["apiAvailable"], milestones["reconciled"] - milestones["backlogComplete"], milestones["reconciled"] - milestones["apiAvailable"]], [35, 15, 50])
    residual = {"status": "accepted-open", "owner": "service-owner", "reviewDay": 10}
    check("accepted_is_not_closed_and_review_can_be_overdue", {"status": residual["status"], "daysOverdue": max(0, 12 - residual["reviewDay"])},
          {"status": "accepted-open", "daysOverdue": 2})
    return {"scope": "Synthetic arithmetic and evidence-gap fixtures only. No production gate, deployment, service health measurement or authorization decision. Durations and thresholds are fictional; the input observations are assumed, not collected.",
            "python": platform.python_version(), "scriptSha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
            "groups": len(checks), "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run(), indent=2, ensure_ascii=False))
NA PRÁTICA

Janela de 55 minutos, executor único e reserva adicional de 25: execução de 63, necessidade total de 88, lacuna de 33 minutos. Uma segunda pessoa reduz a lacuna para 21, sem eliminar a necessidade de decisão.

Armadilhas comuns

Confundir não executado com falhado; aceitar zero erros com amostras antigas; considerar dois ramos paralelos apesar de executor exclusivo; prolongar uma exceção sem nova decisão.

Tópicos relacionados: Capacidade e dependências · Prontidão operacional · Recuperação e escalonamento

Leva esta ideia contigo

Confirma a aplicabilidade da evidência e a viabilidade da sequência antes de pedir a decisão sobre a janela.

Criar conta

Referência: OPS07-BP02 Ensure a consistent review of operational readiness · Google SRE release and canary guidance; GitHub immutable releases and GitLab release evidence and deployment safety; DORA five-metric model; inspected 2026-10-01