Kegagalan rilis pada sistem terdistribusi menuntut mekanisme pemulihan yang cepat, deterministik, dan bebas efek samping tak terduga. Pendekatan branching kondisional konvensional (seperti if-else atau case bersarang yang panjang) sering kali menyulitkan pengujian skenario rollback bertingkat. Artikel ini membahas penerapan konsep data-directed programming (SICP 2.4.3) di Haskell untuk mengorkestrasi status deployment dan aksi rollback otomatis via tabel dispatch terpusat.

Problem Desain: Monolithic Case Statements vs Data-Directed Dispatch

Saat pipeline rilis membesar, penanganan kegagalan deployment umumnya terjebak pada antipattern verifikasi status berulang:

-- Antipattern: Penanganan rilis procedural yang rentan regresi
handleFailure :: ReleaseStep -> StepStatus -> IO ()
handleFailure step status = case step of
  DatabaseMigration -> case status of
    Failed -> rollbackMigrations
    _      -> pure ()
  TrafficSwitch -> case status of
    Degraded -> drainNewTraffic >> revertDNS
    _        -> pure ()

Pendekatan ini melanggar prinsip open-closed. Menambah status rilis atau strategi recovery baru memaksa modifikasi langsung pada percabangan inti. Solusinya adalah memisahkan deskripsi data operasi dari implementasi eksekusi melalui tabel dispatch eksplisit.

Model State Deployment dan Registry Typeclass

Langkah pertama adalah memodelkan fase rilis dan status kesehatan sistem sebagai tipe data aljabar murni (ADT). Hubungan antara tipe status dan penangan rollback dimodelkan via registry fungsi di dalam Data.Map atau dispatch dictionary.

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}

module Release.Orchestrator where

import qualified Data.Map.Strict as Map
import Data.Text (Text)

data DeploymentPhase
  = SchemaMigration
  | CanaryRouting
  | BaselineTraffic
  deriving (Eq, Ord, Show)

data MetricStatus
  = MetricHealthy
  | MetricDegraded Double -- error rate percentage
  | MetricFatal Text
  deriving (Eq, Show)

type RollbackAction = IO (Either Text ())
type DispatchTable = Map.Map DeploymentPhase (MetricStatus -> RollbackAction)

Konstruksi Dispatch Table untuk Data-Directed Rollback

Dalam data-directed programming, tabel 2 dimensi (Fase × Kondisi) menentukan prosedur yang dipanggil tanpa mengandalkan kontrol alur imperative. Berikut perakitan tabel routing rollback:

rollbackRegistry :: DispatchTable
rollbackRegistry = Map.fromList
  [ (SchemaMigration, handleMigrationRollback)
  , (CanaryRouting, handleCanaryRollback)
  , (BaselineTraffic, handleFullTrafficRollback)
  ]

handleCanaryRollback :: MetricStatus -> RollbackAction
handleCanaryRollback status = case status of
  MetricHealthy -> pure (Right ())
  MetricDegraded rate -> do
    putStrLn $ "Degradasi terdeteksi: " ++ show rate ++ "%. Mengurangi bobot trafik ke 0%."
    -- Ponytail: Panggil runtime routing API di sini
    pure (Right ())
  MetricFatal err -> do
    putStrLn $ "Fatal error: " ++ show err ++ ". Isolasi instans dan fallback ke versi stabil."
    pure (Right ())

handleMigrationRollback :: MetricStatus -> RollbackAction
handleMigrationRollback status = case status of
  MetricFatal _ -> do
    putStrLn "Menjalankan migrasi database down-step deterministik."
    pure (Right ())
  _ -> pure (Right ())

handleFullTrafficRollback :: MetricStatus -> RollbackAction
handleFullTrafficRollback _ = pure (Right ())

Eksekusi Teardown Deterministik Saat Incident Triage

Ketika sistem observabilitas mendeteksi anomali pada interval health check, engine rilis mengeksekusi aksi yang terdaftar pada registry. Jika fase tidak terdaftar dalam tabel, proses triage harus jatuh ke strategi teardown fallback yang aman.

executeRollback
  :: DispatchTable
  -> DeploymentPhase
  -> MetricStatus
  -> IO (Either Text ())
executeRollback registry phase status =
  case Map.lookup phase registry of
    Nothing -> do
      putStrLn "PERINGATAN: Aksi tidak terdaftar. Menjalankan fallback hard-stop deterministik."
      executeHardStop phase
    Just actionRunner -> actionRunner status

executeHardStop :: DeploymentPhase -> IO (Either Text ())
executeHardStop phase = do
  putStrLn $ "Mematikan alur kerja secara paksa pada fase: " ++ show phase
  pure (Left "Aborted via generic safety net")

Trade-off dan Pertimbangan Praktis

  • Runtime vs Compile-time Safety: Pendekatan registry berbasis Data.Map memvalidasi dispatch pada saat runtime (lookup mengembalikan Maybe). Jika exhaustive check mutlak diperlukan pada saat kompilasi, gunakan pendekatan representasi typeclass berparameter ganda (Multi-Param Typeclasses) atau GADTs murni.
  • State Persistence: Tabel di memori tidak tahan terhadap restart node orkestrator. Simpan fase terakhir dan status audit ke storage persisten (seperti PostgreSQL atau etcd) sebelum mengeksekusi RollbackAction.
  • Idempotensi: Operasi rollback di dalam tabel dispatch harus bersifat idempoten untuk mengantisipasi eksekusi berulang saat network timeout.