Background execution pada platform mobile memiliki batasan ketat. Android (WorkManager/Headless JS) dan iOS (BGTaskScheduler) rata-rata hanya memberikan jendela eksekusi (execution window) sekitar 30 detik. Sistem antrean standar berbasis First-In, First-Out (FIFO) kerap mengalami kegagalan struktural pada kondisi ini: tugas non-kritis berukuran besar mengunci antrean, menahan tugas kritis, hingga akhirnya sistem operasi menghentikan proses secara paksa via SIGKILL.

1. Anatomi Masalah: FIFO Queue Starvation dan Limitasi OS

Saat aplikasi React Native berpindah ke background, ekosistem JavaScript runtime berjalan dengan alokasi daya dan CPU terbatas. Terdapat tiga kegagalan struktural utama pada implementasi FIFO antrean tunggal:

  • Head-of-Line Blocking: Jika antrean diawali oleh operasi sinkronisasi media (bulk task), tugas penting seperti pengiriman mutasi transaksi atau pembaruan token otentikasi akan tertahan di belakang antrean.
  • Budget Window Depletion: Sistem operasi membatasi waktu eksekusi background sekitar 25-30 detik. Task non-kritis yang lambat menghabiskan alokasi waktu ini, menyebabkan seluruh sisa antrean tidak dieksekusi.
  • SIGKILL Tanpa Penanganan: Jika thread runtime tidak merespons panggilan penyelesaian dari OS sebelum waktu habis, OS akan membunuh proses secara paksa. Hal ini menyebabkan status antrean corrupt atau berada dalam status menggantung (orphaned state).

2. Desain Multi-Tier Priority Queue dengan Persistent Storage

Untuk menghindari starvation, pekerjaan harus dikelompokkan ke dalam tiga tier prioritas yang jelas:

  1. Tier 1 (Critical): Validasi pembayaran, otentikasi token, dan mutasi data tunggal hasil aksi langsung pengguna. Harus dieksekusi pertama dengan timeout ketat (2-3 detik per task).
  2. Tier 2 (Sync): Sinkronisasi data incremental dua arah dan pembaruan entitas lokal. Dieksekusi jika budget waktu masih memadai.
  3. Tier 3 (Bulk): Log telemetri, analitik, dan prefetching data statis. Hanya dieksekusi jika tier 1 dan 2 kosong serta waktu eksekusi tersisa aman.

Status antrean wajib disimpan secara persisten di storage sinkron berkecepatan tinggi seperti MMKV atau SQLite (misalnya via OP-SQLite), bukan AsyncStorage berbasis bridge asynchronous biasa yang rawan mengalami race condition saat proses dibunuh.

3. Implementasi: Adaptive Worker Runner dengan Time-Slicing

Pola implementasi berikut menggunakan alokasi budget waktu adaptif dan AbortController untuk membatalkan koneksi jaringan secara bersih sebelum batas waktu OS terlampaui.

// types.ts
export type JobPriority = 'critical' | 'sync' | 'bulk';

export interface Job<T = unknown> {
  id: string;
  priority: JobPriority;
  payload: T;
  retries: number;
  maxRetries: number;
}

export type JobHandler<T = unknown> = (payload: T, signal: AbortSignal) => Promise<void>;

Berikut adalah implementasi runner dengan time-budgeting:

// QueueRunner.ts
export class QueueRunner {
  private handlers = new Map<string, JobHandler>();
  private safetyBufferMs = 5000; // Sisa waktu aman sebelum OS trigger kill

  constructor(
    private storage: {
      peekNext: () => Job | null;
      dequeue: (id: string) => void;
      requeue: (job: Job) => void;
      markFailed: (job: Job, error: Error) => void;
    }
  ) {}

  registerHandler(type: string, handler: JobHandler) {
    this.handlers.set(type, handler);
  }

  async runQueue(osBudgetMs: number = 30000): Promise<void> {
    const deadline = Date.now() + osBudgetMs - this.safetyBufferMs;

    while (Date.now() < deadline) {
      const job = this.storage.peekNext();
      if (!job) break; // Antrean kosong

      const remainingTime = deadline - Date.now();
      const abortController = new AbortController();
      const timeoutId = setTimeout(() => abortController.abort(), remainingTime);

      try {
        const handler = this.handlers.get(job.priority);
        if (!handler) {
          throw new Error(`Handler tidak ditemukan untuk prioritas: ${job.priority}`);
        }

        await handler(job.payload, abortController.signal);
        clearTimeout(timeoutId);
        this.storage.dequeue(job.id);
      } catch (error: any) {
        clearTimeout(timeoutId);

        if (abortController.signal.aborted || error.name === 'AbortError') {
          // Batalkan dan simpan kembali job tanpa menaikkan retries count
          this.storage.requeue(job);
          break; // Hentikan loop; alokasi waktu habis
        }

        if (job.retries + 1 >= job.maxRetries) {
          this.storage.markFailed(job, error);
          this.storage.dequeue(job.id);
        } else {
          job.retries += 1;
          this.storage.requeue(job);
        }
      }
    }
  }
}

4. Mekanisme Graceful Shutdown

Kunci mencegah inkonsistensi status pada background sync adalah pemanfaatan AbortSignal pada setiap network request dan penanganan fallback status:

  • Atomic Re-serialization: Jangan menandai job sebagai failed jika pembatalan dipicu oleh signal.aborted. Kembalikan job ke antrean lokal dengan prioritas semula agar dapat dilanjutkan pada siklus sync berikutnya.
  • Idempotency Token: Setiap request mutasi yang dikirim worker wajib menyertakan Idempotency-Key berbasis job.id. Hal ini mengamankan backend dari duplikasi transaksi jika request berhasil di server namun worker keburu terhenti sebelum sempat memperbarui storage lokal.

Catatan: Selalu berikan safetyBufferMs minimal 5 detik dari batas maksimum background window OS. Gunakan sisa waktu ini khusus untuk menyelesaikan penulisan disk atau I/O lokal sebelum memanggil callback selesai ke platform native.

5. Verifikasi dan Pengujian Antrean

Pengujian background queue membutuhkan simulasi dua skenario utama: prioritas eksekusi dan pemotongan alokasi waktu.

// QueueRunner.test.ts
import { QueueRunner } from './QueueRunner';

describe('Priority Queue & Time Budget Verification', () => {
  it('harus memprioritaskan Critical job di atas Bulk job', async () => {
    const executed: string[] = [];
    const mockStorage = {
      jobs: [
        { id: '1', priority: 'bulk', payload: 'B1', retries: 0, maxRetries: 3 },
        { id: '2', priority: 'critical', payload: 'C1', retries: 0, maxRetries: 3 },
      ],
      peekNext() {
        // Return job critical terlebih dahulu terlepas urutan array
        return this.jobs.sort((a, b) => (a.priority === 'critical' ? -1 : 1))[0] || null;
      },
      dequeue(id: string) {
        this.jobs = this.jobs.filter((j) => j.id !== id);
      },
      requeue: jest.fn(),
      markFailed: jest.fn(),
    };

    const runner = new QueueRunner(mockStorage as any);
    runner.registerHandler('critical', async (p) => { executed.push(p as string); });
    runner.registerHandler('bulk', async (p) => { executed.push(p as string); });

    await runner.runQueue(10000);
    expect(executed[0]).toBe('C1');
  });
});

Melalui implementasi priority scheduler yang dipadukan dengan adaptive time-slicing dan abort signal, aplikasi React Native dapat mencegah penalti proses dari OS, menjaga integritas data lokal, dan memastikan transaksi pengguna selalu diproses tepat waktu.