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:
- 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. - Delta Queries (Change Tracking): Jika webhook terlewat (misalnya karena downtime receiver), gunakan API delta sync berkala (seperti
/drive/root/deltapada 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
TOMBSTONEDagar 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!