Membina Sistem Pemberitahuan Tolak yang Cakna Zon Waktu dan Andal
Sebuah aplikasi kesihatan & kesejahteraan perlu menghantar peringatan harian yang diperibadikan — peringatan waktu makan, pemeriksaan suasana hati, peringatan hidrasi, isyarat tidur, dan peringatan tersuai — kepada ribuan pengguna di seluruh dunia. Kekangannya: setiap pemberitahuan perlu tiba pada waktu tempatan yang betul, tepat sekali, dan tidak pernah kepada peranti yang tidak aktif. Kami mereka bentuk dan membina saluran paip teragih yang merealisasikan perkara itu.
Cabaran
- Ketepatan zon waktu pada skala besar. "Peringatan sarapan jam 9 pagi" bermaksud sesuatu yang berbeza bagi setiap pengguna. Menghantar mengikut waktu pelayan akan menghantar notifikasi kepada seseorang di Sydney pada jam 3 pagi. Setiap pemberitahuan perlu diselesaikan mengikut waktu tempatan pengguna.
- Tiada spam, tiada penduaan. Penjadualan cron tidak dapat dielakkan akan bertindih dan berjalan semula. Tanpa jaminan yang ketat, seorang pengguna boleh menerima peringatan "masa makan tengah hari 🥗" yang sama dua atau tiga kali — jalan pantas ke nyahpasang.
- Peranti tidak boleh dipercayai. Pengguna menyahpasang aplikasi, membatalkan kebenaran, dan menukar token push secara berterusan. Menghantar pemberitahuan secara buta kepada token yang usang membazirkan sumber dan merosakkan metrik penghantaran.
- Masa yang tepat tanpa penjadual 'brute-force'. Menghantar ratusan pemberitahuan pada minit yang tepat — tanpa cron membebankan pangkalan data setiap 60 saat — memerlukan mekanisme yang lebih pintar daripada tinjauan naif.
Penyelesaian Kami
Kami membina saluran paip tiga peringkat yang mengasingkan dengan jelas apa yang perlu dihantar, bila perlu dihantar, dan benar-benar menghantarnya — supaya setiap peringkat boleh gagal dan pulih secara bebas. Pangkalan data adalah sumber kebenaran, baris gilir mesej mengendalikan masa yang tepat, dan lapisan pekerja tunggal berkomunikasi dengan penyedia push.

Seni Bina
- Expo-notifications ialah klien React Native dengan saluran asli, bunyi unik, dan deep links yang menggunakan format token tunggal dan API penghantaran untuk kedua-dua iOS dan Android.
- Backend NestJS dengan expo-server-sdk sebagai abstraksi push terpadu mengatasi FCM dan APNs.
- MongoDB sebagai sumber kebenaran — koleksi NotificationMessage, NotificationToken, dan NotificationCounter.
- Baris gilir tunda ActiveMQ (STOMP), satu untuk setiap kategori (makanan, suasana hati, aktiviti, keselamatan, peringatan), untuk penghantaran terjadual yang tepat.
- cron Pencipta yang menjana rekod pemberitahuan yang diselesaikan mengikut zon waktu bagi setiap pengguna.
- Pekerja Pengguna yang melanggan setiap baris gilir dan melakukan pengesahan akhir sebelum menghantar.
- AWS ECS Fargate menjalankan cron dan pengguna; ActiveMQ pada instans EC2 khusus.
Ciri-ciri Utama
- Penjadualan cakna zon waktu. Menggunakan date-fns-tz, waktu penghantaran tempatan setiap pengguna dikira, ditukar kembali ke UTC untuk penyimpanan, dan dibatasi oleh tetingkap tarikh UTC untuk menjamin satu peringatan setiap hari.
- Idempotensi dikuatkuasakan pangkalan data. Indeks unik separa pada mesej yang menunggu menjadikan penciptaan pendua mustahil — walaupun cron berjalan dua kali:
| // Unique only while the message is still PENDING and not deleted schema.index( { userId: 1, notificationTokenId: 1, category: 1, label: 1, scheduledAt: 1 }, { unique: true, partialFilterExpression: { status: 'pending', isDeleted: false } } ); |
3. Penghantaran tepat melalui baris gilir tunda. Daripada cron setiap minit, penjadual menempatkan mesej dalam baris gilir sekarang tetapi menangguhkan penghantaran ke minit tepat yang sepatutnya menggunakan pengepala `scheduled-delay` ActiveMQ:
| client.send(`/queue/${queueName}`, { persistent: 'true', 'AMQ_SCHEDULED_DELAY': String(delayMs), // delivered exactly when due }, JSON.stringify(message)); |
4. Satu token aktif tunggal setiap peranti. Indeks unik separa menjamin tepat satu token aktif setiap peranti; log masuk baharu akan menamatkan token lama dengan bersih, dengan percubaan semula `exponential-backoff` untuk bertahan daripada log masuk serentak.
5. Semakan resit + pembersihan automatik. Selepas menghantar, kami meninjau resit Expo. Respons DeviceNotRegistered serta-merta menyahaktifkan token yang tidak aktif supaya kami tidak lagi membazir penghantaran padanya.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // stop sending to dead devices } |
6. Had kegagalan. Setiap peranti mempunyai kaunter percubaan semula; selepas 3 kegagalan berturut-turut, token secara automatik ditamatkan — tiada gelung tak terhingga, tiada token zombie.
7. Menghormati niat pengguna pada waktu penghantaran. Keutamaan pemberitahuan disemak semula oleh pengguna pada waktu penghantaran, bukan hanya pada penjadualan — jadi pengguna yang memilih keluar sejam sebelum peringatan tidak akan pernah menerimanya. Mesej diselesaikan kepada status akhir yang jujur: success, failed, atau is_missed.
Hasil
- Setiap pengguna menerima peringatan pada waktu tempatan yang betul, di seluruh dunia — sifar pemberitahuan luar waktu.
- Pemberitahuan pendua dihapuskan sepenuhnya melalui idempotensi peringkat pangkalan data.
- Token peranti yang tidak aktif dan usang dikesan dan ditamatkan secara automatik, menjaga penghantaran sentiasa bersih.
- Kebolehpercayaan ditingkatkan dan beban pangkalan data dikurangkan dengan penghantaran yang tepat, minit-akurat tanpa penjadual 'brute-force'.
Timbunan Teknologi
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

