Sabtu pagi, kotak masuk dukungan proyek pengingat jadwal sampinganku kedatangan tujuh email dalam waktu dua jam, semuanya dengan judul yang mirip: "kenapa notifikasi masuk jam 1 malam?". Aplikasinya sederhana—orang mengatur satu jam dalam sehari untuk dapat pengingat lewat push notification, biasanya buat minum obat, olahraga, atau sekadar cek jurnal harian. Tidak ada yang mengatur jam 1 pagi. Yang mereka atur kebanyakan jam 7 atau 8 pagi. Aku cek dashboard cron: semua job jalan tepat waktu, tidak ada error, tidak ada job yang gagal. Tapi jam yang tercatat sebagai "waktu kirim" di log memang benar-benar berbeda tujuh jam dari yang diminta pengguna.
Migrasi yang Kukira Cuma Soal Pindah Alamat
Tiga hari sebelumnya aku memindahkan proyek ini dari VPS lama yang kusewa manual ke platform container terkelola, alasannya cuma supaya tidak perlu lagi urus patch OS dan sertifikat sendiri. Kode aplikasinya tidak kusentuh sama sekali—cukup build image Docker dari Dockerfile yang sama, deploy, selesai. Karena tidak ada perubahan kode, aku juga tidak menjalankan pengujian menyeluruh terhadap fitur notifikasi; fokusku waktu itu cuma memastikan endpoint API dan koneksi database jalan normal setelah pindah.
Yang tidak kusadari: VPS lama kuset zona waktunya ke Asia/Jakarta sejak awal setup, kebiasaan lama supaya log server gampang dibaca tanpa perlu konversi mental. Image dasar yang dipakai platform baru default-nya UTC, dan tidak ada satu baris pun di Dockerfile atau konfigurasi deployment yang secara eksplisit mengatur TZ. Server baru menyala dengan jam sistem tujuh jam lebih awal dari WIB, dan tidak ada peringatan apa pun soal itu—dari sudut pandang platform, semuanya normal, wajar, memang begitu defaultnya.
Kenapa Staging Tidak Pernah Menangkap Ini
Yang bikin ini lolos bukan cuma karena aku malas—staging environment-ku sendiri kebetulan masih jalan di VPS lama yang belum kumatikan, jadi setiap kali aku uji fitur reminder di staging sebelum migrasi, hasilnya selalu benar karena server itu juga berzona waktu Jakarta. Migrasi production jadi satu-satunya tempat asumsi itu benar-benar diuji, dan gagal diam-diam karena tidak ada error yang muncul di log mana pun—job cron tetap jalan sesuai jadwalnya sendiri, cuma jadwal itu sendiri yang sudah salah acuan.
Satu Baris Kode yang Mengasumsikan Server Selalu di Jakarta
Akar masalahnya ternyata satu fungsi kecil yang dipakai untuk mencocokkan jadwal user dengan waktu berjalan job cron. Saat pengguna mengatur jam kirim lewat input <input type="time"> di form pengaturan, nilainya disimpan sebagai angka jam sederhana, misalnya 8 untuk jam 8 pagi—tanpa keterangan zona waktu sama sekali, karena selama ini semua orang yang pakai aplikasi ini memang di WIB dan servernya juga di WIB, jadi angka itu "kebetulan" selalu benar tanpa perlu dikonversi. Job cron yang jalan tiap jam lalu membandingkan angka itu langsung dengan new Date().getHours()—jam lokal server, bukan UTC yang eksplisit dikonversi ke WIB. Selama server memang berzona Jakarta, dua angka itu identik dan semuanya terasa berfungsi normal, padahal sebenarnya cuma menumpang pada kebetulan konfigurasi infrastruktur yang tidak pernah dituliskan sebagai syarat di mana pun dalam kode.
Begitu server pindah ke UTC, getHours() mulai mengembalikan jam UTC murni. Pengguna yang mengatur jam 8 pagi WIB sebenarnya berarti jam 1 dini hari UTC, jadi notifikasinya baru terkirim jam 8 pagi UTC—yang setara jam 3 sore WIB untuk sebagian, dan untuk pengguna lain yang mengatur jam malam justru jatuh persis di jam 1 dini hari WIB. Tidak ada pola yang langsung kelihatan dari keluhan yang masuk karena tiap pengguna kena offset yang sama tapi hasil akhirnya beda-beda tergantung jam berapa yang mereka atur—itu yang bikin aku sempat berpikir ini bug acak, bukan pergeseran tujuh jam yang konsisten.
Perbaikan yang Akhirnya Kupasang
Perbaikannya bukan sekadar menyetel TZ=Asia/Jakarta di konfigurasi deployment baru, meski itu juga kulakukan sebagai penambal cepat. Aku ubah cara penyimpanan jam kirim: sekarang disimpan sebagai waktu UTC yang sudah dikonversi eksplisit saat pengguna menyimpannya lewat form, pakai library zona waktu yang eksplisit alih-alih mengandalkan Date bawaan JavaScript yang selalu bergantung pada zona waktu sistem yang menjalankannya. Job cron sekarang membandingkan waktu UTC eksplisit lawan UTC eksplisit, tidak pernah lagi bersentuhan dengan jam lokal server siapa pun. Aku juga menambahkan satu langkah di pipeline deployment yang menjalankan test suite dengan TZ=UTC dipaksa secara eksplisit, supaya lolos-tidaknya fitur tidak pernah lagi diam-diam bergantung pada zona waktu mesin yang kebetulan menjalankannya.
Takeaway
Kode yang "bekerja" karena server kebetulan berzona waktu tertentu bukan kode yang benar—itu kode yang menumpang pada konfigurasi infrastruktur yang tidak tertulis di mana pun sebagai syarat, dan bisa runtuh diam-diam begitu infrastrukturnya berubah tanpa ada satu error pun yang berbunyi. Setiap kali aplikasi menyimpan atau membandingkan waktu, aku sekarang selalu bertanya: apakah ini tetap benar kalau server yang menjalankannya besok pindah ke benua lain? Kalau jawabannya "asal servernya di Jakarta, aman", itu tandanya asumsi zona waktu perlu dibuat eksplisit di kode, bukan dibiarkan menempel diam-diam di mesin yang menjalankannya.
---
Sumber gambar: Ales Nesetril di Unsplash Sumber gambar: imgix di Unsplash Sumber gambar: Chris Ried di Unsplash
The conversation 0
No comments yet. Start the conversation.