Akar Masalah: Asumsi URI Permanen dan Pola Data Expiry

Banyak sync engine backend dirancang dengan asumsi bahwa resource identifier pada upstream storage provider (seperti OneDrive, Google Drive, atau S3 bucket dengan retention rule) bersifat statis dan permanen. Ketika upstream memberlakukan kebijakan siklus hidup—misalnya retention policy kedaluwarsa, penghapusan dokumen otomatis, atau link sharing yang habis masa berlakunya—storage API tidak sekadar mengembalikan status missing, melainkan mengindikasikan bahwa data telah musnah secara permanen.

Masalah kritis muncul ketika engine memperlakukan seluruh kegagalan fetch sebagai network error umum atau 404 Not Found biasa. Worker queue yang mencoba melakukan fetch berulang kali terhadap resource yang sudah dihapus permanen akan mengalami deadlock throughput, membuang alokasi rate-limit API, dan meninggalkan broken references atau dangling foreign keys di database internal sistem.

Perbedaan Evaluasi: 404 Not Found vs. 410 Gone

Penanganan failure downstream harus dibedakan secara eksplisit pada layer HTTP client berdasarkan spesifikasi RFC 9110:

  • HTTP 404 Not Found (Transient/Ambiguous): Mengindikasikan resource saat ini tidak ditemukan pada target URI. Kegagalan ini bisa dipicu oleh replication lag antar-region penyimpanan cloud, race condition saat proses upload/move belum selesai, atau eventual consistency. Tindakan: Jadwalkan retry menggunakan exponential backoff dengan jitter dan batasan maksimum percobaan (misal: 3–5 kali).
  • HTTP 410 Gone (Terminal): Mengindikasikan resource sengaja dihapus dan kondisi ini bersifat permanen tanpa redirection forwarding address. Tindakan: Eksekusi status terminal seketika. Jangan masukkan kembali ke queue retry. Hentikan pemanggilan upstream dan picu alur cleanup/tombstoning lokal.

Catatan Arsitektur: Mencoba retry pada respons HTTP 410 adalah anti-pattern yang dapat menghabiskan kuota API rate limit tenant dan memblokir antrean pemrosesan pesan sinkronisasi lainnya.

Desain Kontrak API dan Skema Metadata Retention

Untuk mencegah ketergantungan pada pemanggilan upstream yang gagal, sync engine wajib melacak status retensi di database lokal. Skema penyimpanan lokal minimal harus mencatat masa berlaku data upstream secara proaktif.

Contoh skema DDL relasional untuk sinkronisasi file:

CREATE TABLE synced_files (
    id VARCHAR(64) PRIMARY KEY,
    upstream_item_id VARCHAR(255) NOT NULL UNIQUE,
    sync_status VARCHAR(32) NOT NULL DEFAULT 'SYNCED', -- SYNCED, PENDING_RETRY, TOMBSTONED
    expires_at TIMESTAMP WITH TIME ZONE NULL,
    tombstoned_at TIMESTAMP WITH TIME ZONE NULL,
    retry_count INT NOT NULL DEFAULT 0,
    last_sync_error TEXT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_synced_files_status_expiry ON synced_files (sync_status, expires_at);

Dengan menyimpan atribut expires_at yang diperoleh dari metadata response API upstream, engine dapat memfilter atau memperbarui status objek sebelum melakukan network request yang dipastikan akan gagal.

Deteksi Proaktif: Webhook Lifecycle dan Delta Queries

Mengandalkan kegagalan fetch on-demand bersifat reaktif dan tidak efisien. Sistem sinkronisasi skala produksi memerlukan dua mekanisme proaktif:

  1. Upstream Webhook Lifecycle Notifications: Provider seperti Microsoft Graph (OneDrive/SharePoint) mengirimkan event notifikasi ketika resource mendekati masa retensi atau dihapus. Worker yang menerima payload webhook harus langsung memvalidasi event dan menandai record target di database lokal sebagai TOMBSTONED.
  2. Delta Queries (Change Tracking): Jika webhook terlewat (misalnya karena downtime receiver), gunakan API delta sync berkala (seperti /drive/root/delta pada Microsoft Graph). Respons delta akan menyertakan objek facet "@removed": {"reason": "deleted"}. Objek ini memberikan instruksi definitif bahwa entitas lokal harus dihapus atau di-tombstone tanpa perlu memverifikasi URI individual via GET request.

Implementasi Sync Handler Idempoten

Kode di bawah ini mendemonstrasikan handler pemrosesan sinkronisasi dengan penanganan status HTTP 410 secara terminal, isolasi transient error 404, serta update status DB lokal secara idempoten:

import { DatabasePool } from './db';
import { HttpClient, HttpError } from './http';

interface SyncJob {
  fileId: string;
  upstreamItemId: string;
}

export async function handleFileSync(
  job: SyncJob,
  db: DatabasePool,
  http: HttpClient
): Promise<void> {
  try {
    // 1. Fetch metadata/konten dari upstream storage provider
    const upstreamData = await http.get(`/drive/items/${job.upstreamItemId}`);
    
    // 2. Pembaruan data sukses (Idempoten)
    await db.query(
      `UPDATE synced_files 
       SET sync_status = 'SYNCED',
           expires_at = $1,
           retry_count = 0,
           updated_at = NOW()
       WHERE id = $2`,
      [upstreamData.expiryDate || null, job.fileId]
    );
  } catch (error: unknown) {
    if (error instanceof HttpError) {
      // Skenario 1: 410 Gone (Data Expired / Permanently Deleted)
      if (error.statusCode === 410) {
        await markAsTombstoned(db, job.fileId, 'Resource deleted upstream (410 Gone)');
        return; // Job selesai secara terminal, jangan re-throw
      }

      // Skenario 2: 404 Not Found (Transient / Potensi propagation delay)
      if (error.statusCode === 404) {
        await handleTransientError(db, job.fileId, error.message);
        throw error; // Re-throw agar retry-queue mengeksekusi exponential backoff
      }
    }

    // Error umum lainnya (5xx, Network Timeout)
    throw error;
  }
}

async function markAsTombstoned(
  db: DatabasePool,
  fileId: string,
  reason: string
): Promise<void> {
  // Operasi update wajib idempoten
  await db.query(
    `UPDATE synced_files
     SET sync_status = 'TOMBSTONED',
         tombstoned_at = NOW(),
         last_sync_error = $1,
         updated_at = NOW()
     WHERE id = $2 AND sync_status != 'TOMBSTONED'`,
    [reason, fileId]
  );
  
  // Opsional: Hapus binary cache lokal bila tersimpan di disk lokal/secondary tier
  // await localDiskCache.evict(fileId);
}

async function handleTransientError(
  db: DatabasePool,
  fileId: string,
  errorMessage: string
): Promise<void> {
  await db.query(
    `UPDATE synced_files
     SET sync_status = 'PENDING_RETRY',
         retry_count = retry_count + 1,
         last_sync_error = $1,
         updated_at = NOW()
     WHERE id = $2`,
    [errorMessage, fileId]
  );
}

Strategi Penanganan Broken References

Menghapus row secara fisik (hard delete) saat menerima 410 Gone berisiko memicu cascading failure pada relasi database relasional lokal. Terapkan prinsip berikut:

  • Soft-Delete via Tombstone: Gunakan status TOMBSTONED agar relasi foreign key dari invoice, audit trail, atau modul transaksi tetap utuh, tetapi file tidak lagi disajikan ke user interface.
  • Fallback Asset Graceful: Ketika client meminta file yang telah berstatus TOMBSTONED, kembalikan asset placeholder atau payload JSON terstruktur seperti {"status": "expired", "code": "FILE_RETENTION_EXPIRED"} alih-alih melempar internal server error (500).
  • Purge Scheduler Mandiri: Jalankan cron job asinkron untuk membersihkan cache blob lokal atau metadata yang telah di-tombstone melebihi jangka waktu kepatuhan (misal: 30 hari pasca kedaluwarsa). Operasi ini memisahkan alur latensi sinkronisasi real-time dari housekeeping basis data.