Tantangan Migrasi: Dari Single-Threaded Actor ke Centralized Storage

Stateful edge actor seperti Cloudflare Durable Objects menjamin model eksekusi single-threaded dengan konsistensi kuat per ID actor secara lokal. Ketika beban kerja dipindahkan ke database terpusat (seperti PostgreSQL dan Redis), model konkurensi berubah drastis. Database relasional membutuhkan isolasi transaksi eksplisit, penanganan koneksi terdistribusi, serta penguncian yang ketat untuk mencegah anomali state.

Risiko terbesar dalam migrasi ini mencakup modifikasi paralel tak terkendali, kegagalan mutasi parsial akibat network partition, dan split-brain saat traffic dialihkan bertahap antara edge runtime dan backend terpusat. Untuk menjamin integritas data, strategi uji regresi harus memvalidasi transisi state secara matematis melalui idempotency, pengujian race condition, dan pipeline continuous integration (CI) otomatis.

Pencegahan Race Condition dan Anomali Split-Brain

Saat fase canary rollout atau dual-write, instance edge actor dan backend PostgreSQL berpotensi mengeksekusi operasi pada entitas yang sama secara bersamaan. Tanpa proteksi konkurensi, anomali lost update atau mutasi out-of-order pasti terjadi.

1. Optimistic Concurrency Control (OCC) dengan Versioning

Gunakan nomor versi inkremental monotonik pada setiap entitas. Jika transaksi mencoba memperbarui state dengan versi yang tidak sesuai dengan database snapshot, operasi dibatalkan seketika.

-- Skema tabel untuk mencegah overwrite konkuren
CREATE TABLE account_states (
    id UUID PRIMARY KEY,
    balance BIGINT NOT NULL,
    version INT NOT NULL DEFAULT 1,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Eksekusi mutasi dengan verifikasi versi
UPDATE account_states
SET balance = balance + 500,
    version = version + 1,
    updated_at = NOW()
WHERE id = 'a3b8c2d1-0000-0000-0000-000000000000' 
  AND version = 3;

Jika jumlah baris terpengaruh bernilai 0, aplikasi mendeteksi adanya race condition dan melempar error konflik untuk dievaluasi ulang oleh client.

2. Distributed Locking dengan Redis Redlock

Untuk proses multi-tahap yang membutuhkan isolasi di luar batas transaksi PostgreSQL tunggal, terapkan mutual exclusion menggunakan Redis sebelum eksekusi dimulai:

// Pseudocode penguncian state pada migrasi gateway
async function mutateWithLock(key, ttlMs, mutationFn) {
  const token = crypto.randomUUID();
  const acquired = await redis.set(`lock:${key}`, token, 'NX', 'PX', ttlMs);
  
  if (!acquired) {
    throw new Error('CONCURRENT_MUTATION_DETECTED');
  }
  
  try {
    return await mutationFn();
  } finally {
    // Hapus lock hanya jika token identik (menggunakan skrip Lua)
    const releaseScript = `
      if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
      else
        return 0
      end
    `;
    await redis.eval(releaseScript, 1, `lock:${key}`, token);
  }
}

Pengujian Idempotency: Verifikasi Mutasi Jaringan Tak Terduga

Retry otomatis akibat transient network failure pada edge gateway berisiko menduplikasi mutasi balance atau order creation. Setiap endpoint stateful wajib menerapkan Idempotency-Key yang dicatat atomic di database terpusat.

Skenario uji idempotency mensimulasikan pemanggilan request paralel dengan payload identik menggunakan key yang sama:

import assert from 'node:assert';
import test from 'node:test';

test('Uji Konsistensi Idempotency pada Mutasi Saldo', async () => {
  const idempotencyKey = 'req_idemp_' + crypto.randomUUID();
  const payload = { accountId: 'acc-101', amount: 1000 };

  // Eksekusi dua request identik secara simultan
  const [resA, resB] = await Promise.all([
    sendTransferRequest(idempotencyKey, payload),
    sendTransferRequest(idempotencyKey, payload)
  ]);

  // Hanya satu operasi yang boleh bernilai 201 (Created), lainnya 200/409 idempotent cache
  const statuses = [resA.status, resB.status].sort();
  assert.deepStrictEqual(statuses, [200, 201]);
  assert.strictEqual(resA.body.finalBalance, resB.body.finalBalance);
  
  // Validasi ke database: saldo hanya bertambah satu kali
  const actualDbBalance = await getBalanceFromPg('acc-101');
  assert.strictEqual(actualDbBalance, 1000);
});

Otomatisasi Regression Test Suite di Pipeline CI

Uji konkurensi tidak boleh bergantung pada shared staging environment yang non-deterministik. Jalankan instance PostgreSQL dan Redis menggunakan GitHub Actions Services agar pengujian deterministik dan terisolasi.

name: Edge Migration Regression Suite

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  consistency-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: migration_test
          POSTGRES_PASSWORD: secret
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-timeout 2s
          --health-retries 5
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 2s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run db:migrate
        env:
          DATABASE_URL: postgres://postgres:secret@localhost:5432/migration_test
      - name: Run Chaos & Concurrency Regression Tests
        run: npm run test:regression
        env:
          DATABASE_URL: postgres://postgres:secret@localhost:5432/migration_test
          REDIS_URL: redis://localhost:6379
          CHAOS_CONCURRENCY_LEVEL: 50

Checklist Migrasi Stateful Production

  • Terapkan Write Fencing: Saat cutover, nonaktifkan instance edge actor menggunakan tombstone record sebelum membuka akses tulis ke backend terpusat.
  • Validasi Shadow Traffic: Jalankan mode shadow-write di mana edge actor tetap menjadi primary, sementara backend PostgreSQL menerima traffic sekunder untuk verifikasi kesesuaian output.
  • Monitor Drift State: Pasang background reconciliation cron job untuk membandingkan checksum hash state antara storage Durable Objects dan PostgreSQL selama masa transisi.