בניית מערכת התראות דחיפה אמינה, המודעת לאזורי זמן
אפליקציית בריאות ואיכות חיים נדרשה לשלוח תזכורות יומיות מותאמות אישית – תזכורות לארוחות, בדיקות מצב רוח, הנחיות לשתייה, רמזים לשינה ותזכורות מותאמות אישית – לאלפי משתמשים ברחבי העולם. המלכוד: כל התראה הייתה צריכה להגיע בזמן המקומי הנכון, בדיוק פעם אחת, ולעולם לא למכשיר לא פעיל. תכננו ובנינו את צינור הנתונים המבוזר שמגשים זאת.
האתגר
- נכונות אזורי זמן בקנה מידה רחב. "תזכורת לארוחת בוקר ב-9 בבוקר" משמעותה שונה עבור כל משתמש. שליחה בזמן השרת הייתה שולחת התראה למישהו בסידני ב-3 לפנות בוקר. כל התראה הייתה צריכה להישלח במועד המקומי של המשתמש.
- ללא ספאם, ללא כפילויות. תזמוני crons נחפפים ומופעלים מחדש באופן בלתי נמנע. ללא הבטחות קפדניות, משתמש יחיד עלול לקבל את אותה תזכורת "זמן לארוחת צהריים 🥗" פעמיים או שלוש – דרך מהירה להסרת התקנה.
- מכשירים אינם אמינים. משתמשים מסירים אפליקציות, מבטלים הרשאות ומסובבים (משנים) אסימוני Push באופן קבוע. שליחת התראות עיוורת לאסימונים מיושנים מבזבזת משאבים ומשחיתה מדדי מסירה.
- תזמון מדויק ללא מתזמן בכוח גס. מסירת מאות התראות בדקות מדויקות – ללא cron שמפציץ את מסד הנתונים כל 60 שניות – דרשה מנגנון חכם יותר מ-polling נאיבי.
הפתרון שלנו
בנינו צינור נתונים תלת-שלבי המפריד בצורה ברורה בין מה לשלוח, מתי לשלוח זאת ושליחה בפועל – כך שכל שלב יכול להיכשל ולהתאושש באופן עצמאי. מסד הנתונים הוא מקור האמת, תור הודעות מטפל בתזמון מדויק, ושכבת עובד יחידה מתקשרת עם ספק ה-Push.

ארכיטקטורה
- Expo-notifications הוא לקוח React Native עם ערוצים מקוריים, צלילים ייחודיים וקישורים עמוקים, המשתמש בפורמט אסימון יחיד וב-API למסירה עבור iOS ו-Android כאחד.
- Backend ב-NestJS עם expo-server-sdk כהפשטת ה-Push המאוחדת מעל FCM ו-APNs.
- MongoDB כמקור האמת – אוספי NotificationMessage, NotificationToken ו-NotificationCounter.
- תורי השהיה של ActiveMQ (STOMP), אחד לכל קטגוריה (ארוחה, מצב רוח, פעילות, בטיחות, תזכורות), למסירה מתוזמנת מדויקת.
- Crons יוצרים המפיקים רשומות התראות מותאמות אזור זמן עבור כל משתמש.
- Workerים צורכים הנרשמים לכל תור ומבצעים אימות סופי לפני השליחה.
- AWS ECS Fargate מפעיל את ה-crons וה-consumers; ActiveMQ על מופע EC2 ייעודי.
תכונות עיקריות
- תזמון מודע לאזורי זמן. באמצעות date-fns-tz, זמן השליחה המקומי של כל משתמש מחושב, מומר חזרה ל-UTC לאחסון, ומוגבל על ידי חלונות תאריך UTC כדי להבטיח תזכורת אחת ליום.
- אידמפוטנטיות נאכפת במסד הנתונים. אינדקס חלקי ייחודי על הודעות ממתינות הופך יצירת כפילויות לבלתי אפשרית – גם כאשר cron מופעל פעמיים:
| // 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. מסירה מדויקת באמצעות תורי השהיה. במקום cron שמפעיל פעם בדקה, המתזמן מכניס הודעות לתור כעת אך דוחה את המסירה לדקה המדויקת באמצעות כותרת ה-scheduled-delay של ActiveMQ:
| client.send(`/queue/${queueName}`, { persistent: 'true' 'AMQ_SCHEDULED_DELAY': String(delayMs), // delivered exactly when due }, JSON.stringify(message)); |
4. אסימון פעיל יחיד למכשיר. אינדקס חלקי ייחודי מבטיח בדיוק אסימון פעיל אחד למכשיר; כניסות חדשות מנטרלות באופן נקי את האסימון הישן, עם ניסיונות חוזרים (retries) של exponential-backoff כדי לשרוד כניסות בו-זמניות.
5. בדיקת קבלות + ניקוי אוטומטי. לאחר השליחה, אנו מבצעים polling לקבלות של Expo. תגובת DeviceNotRegistered מנטרלת מיד את האסימון הלא פעיל כך שלעולם לא נבזבז שליחה עליו שוב.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // stop sending to dead devices } |
6. תקרת כשלונות. לכל מכשיר יש מונה ניסיונות חוזרים (retry counter); לאחר 3 כשלונות רצופים, האסימון מנוטרל אוטומטית – ללא לולאות אינסופיות, ללא אסימוני זומבי.
7. כיבוד כוונת המשתמש בזמן השליחה. העדפת ההתראות נבדקת מחדש על ידי ה-consumer בעת המסירה, לא רק בעת התזמון – כך שמשתמש שמבטל הסכמה שעה לפני תזכורת לעולם לא יקבל אותה. ההודעות נפתרות למצבים סופיים כנים: success, failed, או is_missed.
תוצאות
- כל משתמש מקבל תזכורות בזמן המקומי הנכון, בכל העולם – אפס התראות בשעות לא מתאימות.
- התראות כפולות בוטלו לחלוטין באמצעות אידמפוטנטיות ברמת מסד הנתונים.
- אסימוני מכשירים מתים ומיושנים מזוהים ומנוטרלים אוטומטית, ושומרים על מסירה נקייה.
- האמינות גדלה ועומס מסד הנתונים מופחת בזכות מסירה מדויקת עד לדקה, ללא מתזמן בכוח גס.
מחסנית טכנולוגית
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

