Versi 1.2.0 pipeline pesanan dideploy pukul 02:00, dan pukul 06:10 tim analytics melaporkan keanehan: tabel hasil hanya berisi 4 baris untuk 10 pesanan. Dua pilihan langsung menghadang: putar balik ke versi lama yang dipercaya, atau perbaiki maju dengan hotfix. Rollback untuk pipeline data berbeda dari aplikasi web — kode bisa dikembalikan dalam hitungan detik, tetapi data yang sudah ditulis dan angka yang sudah terkirim ke konsumen tidak bisa ditarik kembali. Chapter ini membangun disiplin rollback untuk kedua lapisan tersebut, dari artefak berversi sampai kejujuran soal data yang tidak bisa di-rollback.

Artefak Berversi dan Pointer current #

Chapter Pipeline Versioning telah mencatat versi kode ke run_metadata dan menulis output ke tabel berversi. Untuk bisa memutar balik, langkah berikutnya adalah menyimpan artefak kode itu sendiri secara berversi: setiap rilis hasil build dari Chapter Continuous Integration dan Delivery menjadi satu direktori tak berubah di bawah versions/, sementara satu symlink current menunjuk versi yang aktif di production. Jadwal production dari Chapter Scheduling dan Production Runs hanya membaca path current, sehingga pergantian versi cukup memindahkan penunjuknya:

text
versions/
├── v1.0.0/
│   └── transform.py
└── v1.2.0/
    └── transform.py
current -> versions/v1.0.0   (symlink; dipindahkan saat deploy/rollback)

Rollback Drill: Deteksi, Putar Balik, Verifikasi #

Drill berikut dijalankan nyata, bukan skenario di atas kertas. Skrip ini membangun dua artefak, menjalankan baseline dengan v1.0.0, “mendeploy” v1.2.0 yang memuat bug dedup pelanggan, mendeteksi bedanya lewat perbandingan dengan baseline, lalu memutar balik symlink dan memverifikasi hasil:

python
"""Rollback drill: bangun artefak berversi, jalankan baseline v1.0.0, deploy
v1.2.0 lewat symlink current, deteksi bug data dengan membandingkan baseline,
lalu rollback symlink dan verifikasi hasil identik dengan baseline."""
import importlib.util
import json
import os
from pathlib import Path

MODUL_V1 = '''\
import json

def jalankan(baris_baris):
    hasil = []
    for baris in baris_baris:
        d = json.loads(baris)
        total = d["qty"] * d["harga"] - (d.get("diskon") or 0)
        hasil.append((d["pesanan_id"], d["pelanggan"], total))
    return hasil
'''

MODUL_V12 = '''\
import json

def dedup_pelanggan(hasil):
    terlihat, bersih = set(), []
    for pesanan_id, pelanggan, total in hasil:
        if pelanggan not in terlihat:
            terlihat.add(pelanggan)
            bersih.append((pesanan_id, pelanggan, total))
    return bersih

def jalankan(baris_baris):
    hasil = []
    for baris in baris_baris:
        d = json.loads(baris)
        total = d["qty"] * d["harga"] - (d.get("diskon") or 0)
        hasil.append((d["pesanan_id"], d["pelanggan"], total))
    return dedup_pelanggan(hasil)  # BUG: pesanan bukan event per pelanggan
'''

SUMBER = [
    '{"pesanan_id": 1, "pelanggan": "Budi", "qty": 2, "harga": 240000, "diskon": 20000}',
    '{"pesanan_id": 2, "pelanggan": "Siti", "qty": 1, "harga": 850000, "diskon": 0}',
    '{"pesanan_id": 3, "pelanggan": "Dewi", "qty": 1, "harga": 310000, "diskon": 0}',
    '{"pesanan_id": 4, "pelanggan": "Budi", "qty": 1, "harga": 210000, "diskon": 10000}',
    '{"pesanan_id": 5, "pelanggan": "Rina", "qty": 3, "harga": 120000, "diskon": 30000}',
    '{"pesanan_id": 6, "pelanggan": "Siti", "qty": 1, "harga": 240000, "diskon": 0}',
    '{"pesanan_id": 7, "pelanggan": "Dewi", "qty": 2, "harga": 180000, "diskon": 20000}',
    '{"pesanan_id": 8, "pelanggan": "Budi", "qty": 1, "harga": 310000, "diskon": 0}',
    '{"pesanan_id": 9, "pelanggan": "Rina", "qty": 2, "harga": 150000, "diskon": 0}',
    '{"pesanan_id": 10, "pelanggan": "Siti", "qty": 2, "harga": 120000, "diskon": 20000}',
]


def muat_versi_aktif():
    """Muat modul transform dari path yang ditunjuk symlink current."""
    berkas = Path("current") / "transform.py"
    spec = importlib.util.spec_from_file_location("transform", berkas)
    modul = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(modul)
    return modul


def pasang_versi(versi):
    """Arahkan symlink current ke versions/<versi> secara atomik."""
    tmp = Path("current.tmp")
    if tmp.is_symlink():
        tmp.unlink()
    os.symlink(f"versions/{versi}", tmp)
    os.replace(tmp, "current")


def jalankan(tag):
    """Jalankan pipeline dengan versi aktif, kembalikan ringkasan hasil."""
    modul = muat_versi_aktif()
    hasil = modul.jalankan(SUMBER)
    ringkas = {"versi": os.readlink("current"), "baris": len(hasil),
               "total": sum(r[2] for r in hasil)}
    print(f"[{tag}] current -> {ringkas['versi']}: {ringkas['baris']} baris, "
          f"total Rp{ringkas['total']:,}")
    return ringkas


# 1) bangun artefak berversi + data sumber
for versi, isi in (("v1.0.0", MODUL_V1), ("v1.2.0", MODUL_V12)):
    dir_versi = Path("versions") / versi
    dir_versi.mkdir(parents=True, exist_ok=True)
    (dir_versi / "transform.py").write_text(isi)
Path("sumber.jsonl").write_text("\n".join(SUMBER) + "\n")

# 2) baseline dengan v1.0.0 (kode yang dipercaya)
pasang_versi("v1.0.0")
baseline = jalankan("baseline")
Path("baseline.json").write_text(json.dumps(baseline))
print(f"  baseline disimpan ke baseline.json")

# 3) deploy v1.2.0, bandingkan dengan baseline
pasang_versi("v1.2.0")
kandidat = jalankan("deploy")
if (kandidat["baris"], kandidat["total"]) != (baseline["baris"], baseline["total"]):
    print(f"  BUG DATA TERDETEKSI: {kandidat['baris']} baris != "
          f"{baseline['baris']} baris baseline -> rollback artefak")

# 4) rollback: kembalikan current ke v1.0.0, jalankan ulang, verifikasi
pasang_versi("v1.0.0")
setelah = jalankan("rollback")
if (setelah["baris"], setelah["total"]) == (baseline["baris"], baseline["total"]):
    print("  VERIFIKASI LULUS: hasil identik dengan baseline v1.0.0")

Output yang Dihasilkan:

text
[baseline] current -> versions/v1.0.0: 10 baris, total Rp3,560,000
  baseline disimpan ke baseline.json
[deploy] current -> versions/v1.2.0: 4 baris, total Rp1,950,000
  BUG DATA TERDETEKSI: 4 baris != 10 baris baseline -> rollback artefak
[rollback] current -> versions/v1.0.0: 10 baris, total Rp3,560,000
  VERIFIKASI LULUS: hasil identik dengan baseline v1.0.0

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-39 (dua modul): artefak v1.0.0 dan v1.2.0 berupa berkas terpisah yang isinya tidak pernah diubah setelah dibuat — prinsip direktori rilis tak berubah dari Google SRE Book: Release Engineering .
  2. Baris 55-61 (muat_versi_aktif): pipeline membaca current/transform.py; karena current adalah symlink, modul yang dimuat selalu mengikuti versi yang sedang aktif.
  3. Baris 64-70 (pasang_versi): symlink baru dibuat dengan nama sementara lalu dipindahkan dengan os.replace — operasi rename bersifat atomik, sehingga tidak pernah ada momen current tidak menunjuk ke mana-mana.
  4. Baris 92-95 (baseline): ringkasan hasil versi tepercaya disimpan sebelum versi baru dipasang; inilah pembanding untuk deteksi.
  5. Baris 98-102 (deteksi): perbandingan jumlah baris dan total nilai cukup untuk kasus ini; pada pipeline nyata, baseline biasanya berisi checksum per kolom.

Rollback Artefak vs Rollback Data #

Drill di atas memutar balik artefak dalam hitungan detik — tetapi perhatikan apa yang tidak ikut terpulihkan. Bila v1.2.0 sudah berjalan semalaman dan laporan_finance sudah mengekspor angka salah, memindahkan symlink tidak menarik kembali berkas yang terkirim. Dua jenis rollback ini berbeda radikal:

AspekRollback artefakRollback data
Yang diputar balikkode yang dijalankantabel/berkas hasil yang salah
Kecepatandetik (pindahkan pointer)menit sampai jam (proses ulang partisi)
Yang tidak bisa dibatalkan—angka yang sudah dikirim ke konsumen eksternal
Penanganan sisa dampakjalankan ulang jobproses ulang partisi + notice koreksi dari Chapter Recovery dan Reprocessing

Keputusan: Rollback atau Forward Fix #

Rollback bukan satu-satunya jawaban. Bila perbaikan kecil dan sudah teruji, forward fix plus reprocessing bisa lebih cepat daripada memutar balik lalu menunggu rilis ulang. Alurnya:

Cara terbaik menghindari keputusan sulit itu adalah menemukan bug sebelum sampai ke production. Canary run menjalankan versi baru hanya pada sebagian kecil partisi, lalu membandingkan hasilnya dengan versi lama pada partisi yang sama; shadow run menjalankan versi baru secara penuh tetapi tidak menulis output produksi. Keduanya membangun pada tabel paralel v1/v2 dari Chapter Pipeline Versioning — pola dual-write yang membeli waktu verifikasi tanpa mempertaruhkan konsumen.

Runbook Rollback #

Runbook yang layak dipakai saat insiden menjawab lima pertanyaan sebelum kejadian, bukan saat telepon berdering pukul tiga pagi. Jawaban diuji lewat drill berkala — sekali per kuartal cukup — karena durasi rollback yang diukur saat latihan jauh lebih bisa dipercaya daripada estimasi di dokumen:

  • Siapa yang berwenang memutuskan rollback: incident commander sesuai Chapter Incident Response.
  • Versi mana target rollback: daftar versi pada direktori versions/ beserta status kesehatannya.
  • Bagaimana verifikasi pemulihan: perbandingan hasil terhadap baseline tersimpan, otomatis setelah rollback.
  • Kapan beralih ke forward fix: bila perbaikan kecil sudah teruji dan lebih cepat daripada rilis ulang.
  • Siapa yang mengomunikasikan status: daftar konsumen data dari blast radius reprocessing.

Insiden scratch worker yang penuh pada timeline November adalah pengingat bahwa sumber kegagalan berikutnya bisa jadi bukan kode, melainkan infrastruktur yang kehabisan ruang. Chapter Capacity Planning membahas cara memperkirakan titik jebol tersebut sebelum kalender menunjukkannya.