State drift pada sinkronisasi status presence remote worker terjadi ketika event webhook diterima di luar urutan (out-of-order delivery) atau terduplikasi akibat mekanisme automatic retry dari publisher. Jika pekerja memperbarui status dari ONLINE ke BUSY lalu OFFLINE, latensi jaringan atau retry dapat menyebabkan event BUSY tiba paling akhir. Tanpa penanganan khusus, sistem lokal menetapkan status pekerja sebagai BUSY tanpa batas waktu.

Mengandalkan wall-clock timestamp (seperti Date.now() atau ISO string) tidak menyelesaikan masalah akibat clock skew antar-server dan delay antrean webhook. Solusi deterministik membutuhkan tiga pilar: verifikasi signature kriptografis, deduplikasi berbasis idempotency key, dan kontrol mutasi berbasis logical versioning.

Kontrak Payload Webhook

Publisher harus menyertakan identifier unik event dan nomor urut versi logis (monotonik naik) per entitas user. Hindari payload yang hanya membawa delta status tanpa nomor sekuens.

{
  "event_id": "evt_01HTZ89PXYZQ8A2B3C4D5E6F7G",
  "user_id": "usr_987654321",
  "presence": "AWAY",
  "sequence_number": 1042,
  "timestamp": 1711928400
}
  • event_id: UUIDv7 atau ULID unik per pengiriman pesan, digunakan untuk deduplikasi instan.
  • sequence_number: Integer monotonik naik (logical clock) yang diinkrementasi publisher setiap kali status user berubah.
  • presence: Nilai status valid (ONLINE, BUSY, AWAY, OFFLINE).

Verifikasi Autentikasi HMAC-SHA256

Sebelum memproses payload, pastikan request berasal dari publisher resmi dan tidak dimanipulasi di tengah jalan. Gunakan constant-time comparison untuk mencegah timing attack.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhookSignature(rawBody: string, signatureHeader: string, secret: string): boolean {
  if (!signatureHeader || !rawBody) return false;

  const computedSignature = createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const signatureBuffer = Buffer.from(signatureHeader, 'hex');
  const computedBuffer = Buffer.from(computedSignature, 'hex');

  if (signatureBuffer.length !== computedBuffer.length) {
    return false;
  }

  return timingSafeEqual(signatureBuffer, computedBuffer);
}

Penyimpanan Idempotency dan Logical Versioning

Gunakan database relasional seperti PostgreSQL dengan mekanisme conditional upsert untuk menjamin atomisitas status, atau simpan idempotency record dengan TTL di Redis.

Skema PostgreSQL

CREATE TABLE user_presence (
    user_id VARCHAR(64) PRIMARY KEY,
    status VARCHAR(32) NOT NULL,
    last_sequence BIGINT NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE processed_webhook_events (
    event_id VARCHAR(64) PRIMARY KEY,
    user_id VARCHAR(64) NOT NULL,
    processed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

Conditional State Update Query

Mutasi status hanya dieksekusi jika sequence_number yang masuk lebih besar daripada last_sequence yang tersimpan di database.

INSERT INTO user_presence (user_id, status, last_sequence, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (user_id) DO UPDATE
SET status = EXCLUDED.status,
    last_sequence = EXCLUDED.last_sequence,
    updated_at = NOW()
WHERE EXCLUDED.last_sequence > user_presence.last_sequence;

Jika query di atas mengembalikan rowCount === 0 pada baris yang sudah ada, event tersebut terbukti basi (stale) dan harus diabaikan.

Penanganan HTTP Response Status Code

Pola penanganan status code menentukan apakah publisher akan terus mengirimkan retry atau menghentikannya:

  • 200 OK: Kembalikan saat event berhasil diproses atau ketika event terdeteksi sebagai duplikat/stale. Mengirim 4xx/5xx untuk event lama akan memicu retry berulang dari publisher dan memperburuk traffic spike. Berikan metadata respons JSON seperti {"status": "ignored", "reason": "stale_event"}.
  • 401 Unauthorized: Kembalikan jika verifikasi signature HMAC gagal.
  • 409 Conflict: Gunakan secara spesifik jika terjadi lock contention internal (misalnya concurrent update pada transaction queue lokal) di mana consumer sengaja meminta publisher melakukan exponential backoff retry. Jangan gunakan 409 untuk event yang urutannya basi secara permanen.
  • 500 Internal Server Error: Kembalikan hanya saat dependensi kritis down (misalnya koneksi PostgreSQL terputus) agar publisher menjadwalkan redelivery.

Implementasi Consumer dan Self-Check Test

Kode mandiri berikut mendemonstrasikan logika verifikasi signature, penolakan duplicate, dan mitigasi event out-of-order menggunakan Node.js standar tanpa dependensi eksternal.

import assert from 'node:assert';
import { createHmac, timingSafeEqual } from 'node:crypto';

interface PresenceEvent {
  event_id: string;
  user_id: string;
  presence: 'ONLINE' | 'BUSY' | 'AWAY' | 'OFFLINE';
  sequence_number: number;
  timestamp: number;
}

class PresenceSyncConsumer {
  private processedEvents = new Set<string>();
  // ponytail: in-memory store; ganti dengan PostgreSQL/Redis connection pool di production.
  private userPresenceStore = new Map<string, { status: string; lastSequence: number }>();
  private webhookSecret: string;

  constructor(secret: string) {
    this.webhookSecret = secret;
  }

  public handleWebhook(rawBody: string, signature: string): { code: number; body: object } {
    const isValid = this.verifySignature(rawBody, signature);
    if (!isValid) {
      return { code: 401, body: { error: 'Invalid signature' } };
    }

    const event: PresenceEvent = JSON.parse(rawBody);

    // 1. Cek duplikasi event_id
    if (this.processedEvents.has(event.event_id)) {
      return { code: 200, body: { status: 'deduplicated', event_id: event.event_id } };
    }

    const current = this.userPresenceStore.get(event.user_id);

    // 2. Evaluasi logical sequence number (Deteksi Out-of-Order)
    if (current && event.sequence_number <= current.lastSequence) {
      // Tandai event sudah diterima agar tidak diproses ulang jika di-retry
      this.processedEvents.add(event.event_id);
      return { code: 200, body: { status: 'dropped_stale', current_seq: current.lastSequence } };
    }

    // 3. Mutasi status
    this.userPresenceStore.set(event.user_id, {
      status: event.presence,
      lastSequence: event.sequence_number,
    });
    this.processedEvents.add(event.event_id);

    return { code: 200, body: { status: 'applied', sequence: event.sequence_number } };
  }

  public getStatus(userId: string) {
    return this.userPresenceStore.get(userId);
  }

  private verifySignature(rawBody: string, signatureHeader: string): boolean {
    const computed = createHmac('sha256', this.webhookSecret).update(rawBody).digest('hex');
    const sigBuf = Buffer.from(signatureHeader, 'hex');
    const compBuf = Buffer.from(computed, 'hex');
    if (sigBuf.length !== compBuf.length) return false;
    return timingSafeEqual(sigBuf, compBuf);
  }
}

// --- RUNNABLE SELF-CHECK ---
const SECRET = 'test-secret-key-12345';
const consumer = new PresenceSyncConsumer(SECRET);

function sign(payload: string): string {
  return createHmac('sha256', SECRET).update(payload).digest('hex');
}

// Event 1: Normal arrival (Seq 101, ONLINE)
const payload1 = JSON.stringify({
  event_id: 'evt_1',
  user_id: 'usr_A',
  presence: 'ONLINE',
  sequence_number: 101,
  timestamp: 1000,
});
const res1 = consumer.handleWebhook(payload1, sign(payload1));
assert.strictEqual(res1.code, 200);
assert.strictEqual(consumer.getStatus('usr_A')?.status, 'ONLINE');

// Event 2: Newer arrival (Seq 103, OFFLINE)
const payload3 = JSON.stringify({
  event_id: 'evt_3',
  user_id: 'usr_A',
  presence: 'OFFLINE',
  sequence_number: 103,
  timestamp: 3000,
});
const res2 = consumer.handleWebhook(payload3, sign(payload3));
assert.strictEqual(res2.code, 200);
assert.strictEqual(consumer.getStatus('usr_A')?.status, 'OFFLINE');

// Event 3: Out-of-order delivery arrival (Seq 102, BUSY arrives late)
const payload2 = JSON.stringify({
  event_id: 'evt_2',
  user_id: 'usr_A',
  presence: 'BUSY',
  sequence_number: 102,
  timestamp: 2000,
});
const res3 = consumer.handleWebhook(payload2, sign(payload2));
assert.strictEqual(res3.code, 200);
assert.strictEqual(consumer.getStatus('usr_A')?.status, 'OFFLINE', 'State drift terdeteksi: status tertimpa event lama!');

// Event 4: Duplicate delivery retry (evt_1 again)
const res4 = consumer.handleWebhook(payload1, sign(payload1));
assert.strictEqual(res4.code, 200);
assert.strictEqual(res4.body['status'], 'deduplicated');

console.log('Semua assert lolos deterministik.');

[code] → skipped: database pool & worker queue, add when throughput > 500 req/sec.

Ringkasan Logika State Sync

  1. Tolak akses invalid: Gunakan HMAC constant-time comparison sebelum parsing JSON.
  2. Deduplikasi instan: Periksa keberadaan event_id untuk mencegah pemrosesan ganda akibat network retry.
  3. Tolak event basi: Periksa sequence_number <= last_sequence lokal. Jika basi, abaikan mutasi database namun tetap return HTTP 200.
  4. Gunakan conditional lock/update: Di tingkat SQL, gunakan klausul WHERE sequence_number > current_sequence untuk mengamankan konkurensi antar-thread consumer.