Pada lingkungan dengan jaringan labil, masalah utamanya bukan sekadar request timeout, tetapi bagaimana memastikan pekerjaan tetap tercatat, dapat diulang dengan aman, dan tidak merusak konsistensi data saat koneksi kembali normal. Desain worker offline-first biasanya menggabungkan dua komponen inti: local queue untuk menahan operasi yang belum bisa dikirim, dan cache lokal untuk menjaga aplikasi tetap responsif saat offline.
Artikel ini membahas pola praktis untuk merancang queue worker dan cache pada perangkat yang sering berpindah jaringan atau terputus, terinspirasi dari konteks Linux mobile/offline-first seperti perangkat yang menjalankan postmarketOS, tetapi fokus pembahasannya ada pada backend dan sistem terdistribusi. Tujuannya bukan mengejar exactly-once yang sulit dan mahal, melainkan membangun sistem yang tahan gagal, mudah diobservasi, dan cukup sederhana untuk dioperasikan.
Mengapa pendekatan offline-first berbeda dari queue biasa
Queue pada server yang selalu tersambung umumnya mengasumsikan broker stabil, jam sistem relatif konsisten, dan worker dapat mengakses jaringan kapan saja. Pada lingkungan offline-first, asumsi itu tidak berlaku. Perangkat bisa:
- berpindah dari Wi-Fi ke seluler lalu offline total,
- kehabisan baterai di tengah proses sinkronisasi,
- menjalankan ulang proses worker saat ada update sistem,
- menyimpan state lokal lebih lama daripada perkiraan awal.
Karena itu, desain yang aman biasanya memakai prinsip berikut:
- Tulis dulu ke penyimpanan lokal yang durabel, baru kirim ke jaringan.
- Anggap pengiriman bisa terjadi lebih dari sekali.
- Jangan jadikan cache sebagai sumber kebenaran utama untuk operasi mutasi.
- Pisahkan status lokal dan status sinkronisasi.
- Sediakan mekanisme rekonsiliasi saat reconnect, bukan hanya retry membabi buta.
Arsitektur alur worker offline-first
Alur dasar yang umum dipakai terlihat seperti ini:
- Aplikasi menerima aksi pengguna atau event lokal.
- Operasi ditulis ke local queue dalam storage durabel, misalnya SQLite.
- Data tampilan diperbarui ke cache lokal agar UI atau service tetap responsif.
- Worker lokal memantau queue dan mencoba mengirim job saat jaringan tersedia.
- Server memproses job menggunakan idempotency key.
- Saat berhasil, item queue ditandai selesai dan cache diperbarui bila perlu.
- Saat gagal, worker menghitung retry schedule dengan backoff dan menyimpan alasan gagal.
- Saat reconnect setelah lama offline, sistem menjalankan revalidation, cache invalidation, dan bila perlu conflict resolution.
Komponen minimal
- Persistent local queue: menyimpan job, payload, metadata retry, dan status sinkronisasi.
- Connectivity detector: sinyal bahwa jaringan tersedia, tetapi jangan menganggapnya sebagai jaminan request pasti berhasil.
- Delivery worker: menarik job yang siap dikirim.
- Idempotent server endpoint: mencegah efek ganda dari retry.
- Cache store: menyimpan snapshot data yang bisa dipakai saat offline.
- Reconciliation logic: membandingkan state lokal dan state server setelah reconnect.
Merancang local queue yang aman
Untuk perangkat atau node kecil dengan kebutuhan sederhana hingga menengah, SQLite sering cukup sebagai queue lokal karena durabel, ringan, dan tidak membutuhkan service tambahan. Kuncinya ada pada skema yang jelas dan transisi status yang disiplin.
Skema tabel queue yang praktis
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
topic TEXT NOT NULL,
payload TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
status TEXT NOT NULL, -- pending, leased, done, failed, dead
attempt_count INTEGER NOT NULL DEFAULT 0,
next_attempt_at TEXT NOT NULL,
last_error TEXT,
locked_by TEXT,
locked_until TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_jobs_ready
ON jobs(status, next_attempt_at);
CREATE UNIQUE INDEX idx_jobs_idempotency
ON jobs(idempotency_key);Beberapa catatan desain:
- id adalah identitas lokal job.
- idempotency_key dipakai saat mengirim ke server agar retry tetap aman.
- status leased berguna untuk mencegah dua worker lokal memproses item yang sama bersamaan.
- locked_until mencegah lease hilang permanen bila proses mati mendadak.
Pola lease lebih aman daripada delete langsung
Kesalahan umum adalah mengambil satu job lalu langsung menghapusnya dari queue sebelum server mengonfirmasi sukses. Jika proses mati setelah job dihapus tetapi sebelum respons sukses dicatat, job hilang. Pola yang lebih aman:
- Pilih job yang siap diproses.
- Ubah status menjadi leased dengan batas waktu lease.
- Kirim ke server.
- Jika sukses, tandai done.
- Jika gagal, kembalikan ke pending dengan next_attempt_at baru.
Dengan begitu, crash di tengah proses tidak langsung menghilangkan pekerjaan.
Contoh pseudocode worker
loop:
if !networkLikelyAvailable():
sleep(short_interval)
continue
begin transaction
job = select one pending job
where next_attempt_at <= now()
order by created_at asc
if no job:
commit
sleep(short_interval)
continue
mark job as leased
locked_by = worker_id
locked_until = now() + lease_duration
updated_at = now()
commit
result = sendToServer(job.payload, job.idempotency_key)
begin transaction
if result.success:
mark job done
else if result.retryable:
increment attempt_count
set status = pending
set next_attempt_at = now() + backoff(attempt_count)
set last_error = result.error
else:
mark job dead
set last_error = result.error
commitYang penting di sini bukan sintaksnya, tetapi dua hal: claim job secara atomik dan simpan hasil proses kembali ke storage durabel.
Retry dengan backoff: jangan kirim ulang secara agresif
Pada jaringan labil, retry yang terlalu cepat justru memperparah masalah: baterai boros, radio jaringan aktif terus, server kewalahan, dan antrean lokal makin menumpuk. Karena itu gunakan exponential backoff dengan jitter.
Prinsip backoff yang masuk akal
- Kegagalan sementara seperti timeout, DNS gagal, atau HTTP 502/503/504 biasanya retryable.
- Kegagalan permanen seperti payload invalid atau autentikasi salah biasanya tidak boleh di-retry tanpa perubahan state.
- Tambahkan jitter agar banyak worker tidak menembak ulang bersamaan saat jaringan pulih.
- Tetapkan batas maksimum percobaan atau kirim job ke status dead untuk inspeksi manual.
Contoh fungsi backoff
function backoffSeconds(attempt) {
const base = 2;
const cap = 300; // 5 menit sebagai batas atas contoh
const exp = Math.min(cap, Math.pow(base, attempt));
const jitter = Math.random() * Math.min(30, exp * 0.2);
return exp + jitter;
}Nilai pastinya harus disesuaikan dengan beban, karakter jaringan, dan SLA. Yang lebih penting adalah pola umumnya: interval retry membesar, dibatasi, dan tidak seragam.
Catatan: jangan hanya bergantung pada sinyal "online" dari sistem operasi. Koneksi bisa terdeteksi aktif, tetapi DNS, TLS handshake, atau akses ke endpoint inti tetap gagal. Worker tetap perlu menangani error jaringan nyata di lapisan aplikasi.
Idempotency key: fondasi utama at-least-once
Pada sistem offline-first, at-least-once delivery adalah model yang paling realistis: job mungkin terkirim lebih dari sekali, tetapi seharusnya efek bisnisnya tidak terduplikasi. Di sinilah idempotency key sangat penting.
Cara kerjanya
Setiap operasi mutasi diberi kunci unik yang stabil untuk satu niat bisnis. Misalnya, "buat pesanan X" atau "upload hasil sinkronisasi sensor Y batch Z". Saat request datang ulang dengan idempotency key yang sama, server harus mengembalikan hasil yang konsisten atau menolak duplikasi tanpa mengeksekusi efek samping lagi.
Kapan idempotency key harus dibuat
- Buat saat job ditulis ke local queue, bukan saat akan dikirim.
- Simpan bersama payload.
- Pastikan key tetap sama di seluruh retry.
Contoh bentuk request
POST /sync/events
Idempotency-Key: 8f5f1d95-1e8b-4b51-9f53-2c6bb7d2e4b1
Content-Type: application/json
{
"device_id": "edge-17",
"event_id": "evt-00123",
"occurred_at": "2026-07-20T10:15:00Z",
"payload": {
"status": "completed"
}
}Di sisi server
Server biasanya menyimpan catatan berdasarkan idempotency key:
- apakah key pernah diproses,
- hasil respons sebelumnya,
- status apakah masih diproses atau sudah selesai.
Implementasi detail bisa memakai tabel database dengan unique constraint pada idempotency key. Jika dua request identik datang hampir bersamaan, constraint database lebih andal daripada logika cek-terus-insert yang rawan race condition.
Kesalahan umum
- Menggunakan timestamp sebagai idempotency key tanpa identitas bisnis yang stabil.
- Membuat key baru setiap retry.
- Menganggap idempotensi cukup ditangani di client, padahal validasi akhirnya harus dilakukan di server.
Distributed locking vs local file lock
Pertanyaan yang sering muncul: apakah worker perlu distributed lock seperti Redis lock, atau cukup file lock lokal? Jawabannya tergantung cakupan kontensi.
Kapan local file lock cukup
Jika satu perangkat atau satu host hanya boleh menjalankan satu instance worker untuk queue lokal yang sama, maka file lock lokal biasanya cukup. Contohnya memakai file lock untuk memastikan hanya ada satu proses yang menarik job dari SQLite di perangkat tersebut.
Kelebihan:
- sederhana,
- tanpa dependency jaringan tambahan,
- tetap bekerja saat offline penuh.
Kekurangan:
- hanya berlaku pada satu host,
- tidak membantu jika beberapa node berbagi antrean yang sama.
Kapan distributed locking dibutuhkan
Jika banyak worker pada node berbeda memproses sumber kerja bersama, Anda butuh koordinasi antar node. Di sini distributed lock bisa dipakai, tetapi jangan jadikan lock sebagai satu-satunya mekanisme keselamatan. Lock bisa kedaluwarsa, jaringan bisa terbelah, dan jam antarmesin bisa tidak sinkron sempurna.
Pada banyak kasus, desain yang lebih kokoh adalah:
- gunakan lease di level data pada database atau broker,
- andalkan unique constraint dan idempotensi,
- perlakukan lock hanya sebagai optimasi pengurang kontensi, bukan penjamin kebenaran mutlak.
Panduan memilih
- Single device / single host → local file lock atau lease di SQLite cukup.
- Multi-process satu host → file lock + transaksi database lokal.
- Multi-host shared queue → lease di database/broker, optional distributed lock bila memang perlu.
Desain cache untuk offline-first dan invalidasi saat reconnect
Cache pada desain offline-first bukan hanya untuk mempercepat baca, tetapi juga agar aplikasi tetap usable saat backend tidak dapat dijangkau. Namun cache lokal harus dianggap sebagai snapshot yang mungkin usang.
Pola cache yang umum
- Read-through cache: baca dari cache dulu, lalu fallback ke jaringan.
- Write-behind dengan queue: mutasi dicatat lokal dulu lalu disinkronkan belakangan.
- Stale-while-revalidate: tampilkan data cache, lalu segarkan di belakang saat jaringan tersedia.
Apa yang harus disimpan bersama cache
- versi atau etag bila ada,
- waktu terakhir sinkronisasi,
- asal data apakah lokal belum tersinkron atau dari server,
- dependency key untuk invalidasi terarah.
Cache invalidation saat reconnect
Saat perangkat kembali online, jangan langsung mengosongkan seluruh cache. Itu membuat lonjakan request dan pengalaman buruk. Lebih aman memakai langkah berikut:
- Kirim job tertunda yang bersifat mutasi.
- Ambil perubahan terbaru dari server berdasarkan token sinkronisasi, versi, atau rentang waktu yang aman.
- Invalidasi hanya key cache yang terdampak oleh mutasi lokal atau perubahan server.
- Lakukan revalidation untuk entitas prioritas tinggi terlebih dahulu.
Jika server mendukung ETag atau last modified, manfaatkan itu untuk mengurangi transfer data penuh. Jika tidak, minimal simpan penanda versi aplikasi atau domain data agar Anda tahu kapan cache harus dianggap tidak valid.
Kesalahan umum pada cache offline-first
- Menyamakan cache dengan sumber kebenaran final.
- Tidak menandai data yang masih pending sync.
- Menghapus cache total saat reconnect sehingga seluruh state harus dimuat ulang.
- Tidak punya strategi migrasi cache ketika skema payload berubah.
Conflict resolution setelah reconnect
Begitu perangkat offline cukup lama, konflik hampir pasti terjadi. Misalnya data yang sama diubah di server dan di perangkat lokal. Menyelesaikan konflik bukan tugas queue semata, melainkan aturan domain bisnis.
Pendekatan yang umum
- Last write wins: sederhana, tetapi berisiko menimpa perubahan penting.
- Version check / optimistic concurrency: update hanya diterima jika versi cocok; jika tidak, client harus rekonsiliasi.
- Merge per field: cocok untuk data yang dapat digabung tanpa ambigu.
- Server authoritative + manual review: cocok untuk domain sensitif seperti keuangan atau inventaris.
Contoh alur konflik yang sehat
- Client menyimpan edit lokal bersama base version dari data saat terakhir dibaca.
- Saat sinkronisasi, server membandingkan base version dengan versi terbaru.
- Jika cocok, update diterima.
- Jika tidak, server mengembalikan status konflik beserta state terbaru.
- Client menandai item sebagai perlu rekonsiliasi, bukan terus me-retry payload lama.
Retry tanpa memahami konflik hanya menghasilkan antrean yang tidak pernah selesai.
At-least-once vs exactly-once: pilih yang realistis
Pada praktik sistem terdistribusi, exactly-once end-to-end jarang benar-benar tercapai tanpa batasan yang ketat dan biaya kompleksitas yang tinggi. Yang lebih realistis adalah kombinasi:
- at-least-once delivery di sisi transport atau worker,
- idempotent processing di sisi aplikasi,
- deduplication di database atau service penerima.
Trade-off utama
At-least-once
- Kelebihan: lebih sederhana, tahan terhadap crash, cocok untuk jaringan labil.
- Kekurangan: duplikasi harus ditangani dengan serius.
Exactly-once
- Kelebihan: model mental lebih nyaman di atas kertas.
- Kekurangan: sulit dibangun lintas jaringan, storage, dan side effect; sering bergantung pada asumsi sempit.
Jika job memicu side effect eksternal seperti email, pembayaran, atau webhook, anggap selalu ada kemungkinan proses ulang. Solusi terbaik biasanya ada pada desain bisnis yang idempotent dan dapat dideduplikasi.
Failure mode operasional yang sering terjadi
1. Job sukses di server, tetapi client mengira gagal
Ini klasik: request diterima dan diproses, tetapi respons hilang karena timeout. Jika client me-retry tanpa idempotency key, efek ganda bisa terjadi. Dengan idempotency key, retry aman karena server mengenali operasi yang sama.
2. Worker crash setelah lease, sebelum update status
Tanpa locked_until, job bisa macet selamanya di status leased. Karena itu lease harus punya kedaluwarsa dan ada proses yang melepaskan atau mengambil kembali lease usang.
3. Redis tersedia, tetapi jaringan ke API utama gagal
Jangan menganggap dependency yang sehat berarti seluruh sistem sehat. Observabilitas harus memisahkan status broker/cache dari status endpoint bisnis.
4. Reconnect memicu banjir sinkronisasi
Saat banyak perangkat kembali online bersamaan, semua worker bisa membanjiri API. Mitigasinya: backoff, jitter, pembatasan laju, dan prioritas antrean.
5. Cache usang menimpa data server
Terjadi jika mutasi lokal menggunakan snapshot lama tanpa version check. Solusinya: kirim versi dasar atau etag saat update, lalu tangani konflik secara eksplisit.
6. Jam sistem perangkat melenceng
Jika logika retry atau konflik terlalu bergantung pada timestamp lokal, hasilnya bisa aneh. Lebih aman memakai counter versi, lease berbasis durasi lokal yang konservatif, dan keputusan final di server.
Metrik observabilitas yang wajib dipantau
Worker offline-first sulit dioperasikan tanpa metrik yang tepat. Jangan hanya melihat error rate global.
Metrik queue
- queue depth: jumlah job pending, leased, dead.
- oldest pending age: umur job tertua yang belum tersinkron.
- attempt count distribution: apakah banyak job berulang kali gagal.
- lease timeout count: indikasi worker sering crash atau hang.
Metrik delivery
- success rate per endpoint atau topic.
- retry rate dan alasan retry.
- latency pengiriman dan waktu total sampai selesai sinkron.
- duplicate detection rate dari idempotency key di server.
Metrik cache dan sinkronisasi
- cache hit ratio untuk baca offline/lokal.
- staleness age data cache.
- reconciliation duration setelah reconnect.
- conflict count dan tipe konflik.
Metrik resource perangkat
- ukuran database lokal,
- pertumbuhan queue,
- penggunaan disk, CPU, dan memori worker,
- dampak retry terhadap konsumsi baterai atau radio jaringan bila relevan.
Untuk debugging, sertakan correlation ID yang menghubungkan log lokal, job queue, request ke server, dan respons server. Tanpa itu, investigasi duplikasi atau job hilang akan sangat sulit.
Kapan cukup dengan SQLite atau Redis, kapan perlu broker khusus
Cukup dengan SQLite jika
- queue bersifat lokal per perangkat atau per host,
- throughput relatif moderat,
- butuh durabilitas sederhana tanpa service tambahan,
- mode offline penuh harus tetap berjalan mandiri.
SQLite unggul untuk local queue karena dekat dengan aplikasi dan tahan restart. Ini sering pilihan terbaik untuk edge device, worker di perangkat mobile Linux, atau service pendamping yang berjalan mandiri.
Cukup dengan Redis jika
- Anda butuh antrean cepat di lingkungan yang mayoritas online,
- durabilitas tidak sepenuhnya lokal per perangkat,
- beberapa worker perlu berbagi state atau koordinasi ringan,
- Anda siap menerima trade-off persistence dan operasional Redis sesuai konfigurasi yang dipilih.
Redis cocok untuk banyak kasus backend, tetapi untuk offline-first murni di perangkat yang bisa benar-benar terputus, Redis bukan pengganti storage lokal durabel di perangkat itu sendiri.
Naik ke broker khusus jika
- butuh throughput lebih tinggi dan konsumen banyak,
- perlu fitur routing, fan-out, partitioning, atau retention yang lebih matang,
- perlu pemisahan jelas antara producer dan consumer pada banyak sistem,
- kegagalan dan observabilitas sudah terlalu kompleks untuk ditangani dengan queue buatan sendiri.
Namun broker khusus tidak otomatis menyelesaikan masalah offline-first. Anda tetap memerlukan local queue di sisi perangkat jika perangkat bisa terputus total dari broker.
Checklist keputusan praktis
- Apakah node bisa offline total? Jika ya, harus ada persistent local queue.
- Apakah job harus bertahan setelah reboot? Jika ya, gunakan storage durabel.
- Apakah ada banyak konsumen lintas host? Jika ya, pertimbangkan broker atau database shared dengan lease yang baik.
- Apakah duplikasi bisa diterima bila idempotent? Jika ya, at-least-once biasanya cukup.
- Apakah konflik data perlu rekonsiliasi domain? Jika ya, desain conflict resolution lebih penting daripada mengganti broker.
- Apakah beban operasional service tambahan sepadan? Jika tidak, mulai dari SQLite lalu evolusikan.
Praktik implementasi yang direkomendasikan
- Simpan job ke local queue sebelum mengubah state sinkronisasi apa pun di memori.
- Gunakan status yang eksplisit: pending, leased, done, dead.
- Pisahkan error retryable dan non-retryable.
- Bangun endpoint server yang idempotent by design.
- Tandai entitas cache yang masih punya perubahan lokal belum tersinkron.
- Saat reconnect, lakukan flush mutasi dulu, lalu revalidate cache.
- Sediakan dead-letter handling walau sederhana, misalnya tabel khusus atau dashboard admin.
- Uji skenario crash di tengah proses, bukan hanya jalur sukses.
Penutup
Desain worker offline-first untuk jaringan labil paling efektif jika dibangun di atas local queue yang durabel, retry dengan backoff dan jitter, idempotency key, serta cache yang sadar terhadap status sinkronisasi. Untuk banyak kasus edge device atau perangkat Linux mobile, kombinasi SQLite untuk queue lokal dan cache lokal yang bisa direvalidasi saat reconnect sudah cukup kuat sebelum Anda mempertimbangkan broker yang lebih berat.
Fokus utamanya bukan mengejar jaminan teoritis yang mahal, melainkan memastikan sistem tetap benar saat gagal: job tidak hilang, duplikasi tidak merusak data, konflik bisa ditangani, dan operator dapat melihat apa yang terjadi. Jika fondasi ini sudah ada, sistem Anda akan jauh lebih tahan terhadap kenyataan jaringan yang tidak stabil.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!