Luotettavan, aikavyöhyketietoisen push-ilmoitusjärjestelmän rakentaminen
Terveyden ja hyvinvoinnin sovelluksen piti lähettää henkilökohtaisia päivittäisiä muistutuksia – ateriamuistutuksia, mielialatarkistuksia, nesteytyskehotuksia, unimerkkejä ja mukautettuja muistutuksia – tuhansille käyttäjille ympäri maailmaa. Haasteena oli: jokaisen ilmoituksen piti saapua oikeaan paikalliseen aikaan, tarkalleen kerran, eikä koskaan toimimattomaan laitteeseen. Suunnittelimme ja rakensimme hajautetun putkilinjan, joka tekee tästä totta.
Haaste
- Aikavyöhyketarkkuus mittakaavassa. "Kello 9 aamiaismuistutus" tarkoittaa eri asiaa jokaiselle käyttäjälle. Lähettäminen palvelimen ajassa lähettäisi ilmoituksen jollekin Sydneyssä kello 3 yöllä. Jokaisen ilmoituksen piti mukautua käyttäjän paikalliseen aikaan.
- Ei roskapostia, ei päällekkäisyyttä. Cronejen ajoitukset menevät väistämättä päällekkäin ja ajetaan uudelleen. Ilman tiukkoja takeita yksittäinen käyttäjä voisi saada saman "aika lounaalle 🥗" -muistutuksen kaksi tai kolme kertaa – mikä johtaa nopeasti sovelluksen poistamiseen.
- Laitteet ovat epäluotettavia. Käyttäjät poistavat sovelluksia, kumoavat käyttöoikeuksia ja vaihtavat push-tunnuksia jatkuvasti. Ilmoitusten lähettäminen sokeasti vanhentuneisiin tunnuksiin tuhlaa resursseja ja vääristää toimitusmittareita.
- Tarkka ajoitus ilman raakaa voimaa käyttävää ajastinta. Satojen ilmoitusten toimittaminen tarkkoina minuutteina – ilman, että cron pommittaa tietokantaa 60 sekunnin välein – vaati älykkäämmän mekanismin kuin naiivi kysely.
Ratkaisumme
Rakensimme kolmivaiheisen putkilinjan, joka erottaa selkeästi mitä lähetetään, milloin lähetetään ja varsinaisen lähetyksen – jotta jokainen vaihe voi epäonnistua ja palautua itsenäisesti. Tietokanta on totuuden lähde, viestijono hoitaa tarkan ajoituksen, ja yksi työntekijäkerros keskustelee push-palveluntarjoajan kanssa.

Arkkitehtuuri
- Expo-notifications on React Native -asiakasohjelma, jossa on natiivikanavat, yksilölliset äänet ja syvät linkit, ja joka käyttää yhtä tunnusmuotoa ja toimitus-APIa sekä iOS- että Android-laitteille.
- NestJS-taustaohjelma, jossa expo-server-sdk on yhtenäinen push-abstraktio FCM:n ja APNs:n yläpuolella.
- MongoDB totuuden lähteenä – NotificationMessage-, NotificationToken- ja NotificationCounter-kokoelmat.
- ActiveMQ (STOMP) viivejonot, yksi per kategoria (ateria, mieliala, aktiviteetti, turvallisuus, muistutukset), tarkkaa aikataulutettua toimitusta varten.
- Creator crons, jotka luovat aikavyöhykkeen mukaan ratkaistuja ilmoitustietueita käyttäjää kohden.
- Kuluttajatyöntekijät, jotka tilaavat jokaiseen jonoon ja suorittavat lopullisen validoinnin ennen lähettämistä.
- AWS ECS Fargate, joka ajaa croneja ja kuluttajia; ActiveMQ omalla EC2-instanssilla.
Avainominaisuudet
- Aikavyöhyketietoinen ajoitus. Käyttäen date-fns-tz, jokaisen käyttäjän paikallinen lähetysaika lasketaan, muunnetaan takaisin UTC-aikaan tallennusta varten ja rajataan UTC-päivämääräikkunoilla, jotta taataan yksi muistutus päivässä.
- Tietokannan valvomakin toistettavuus. Odottavien viestien osittainen yksilöivä indeksi tekee kaksoiskappaleiden luomisesta mahdotonta – vaikka cron ajaisi kahdesti:
| // Yksilöllinen vain, kun viesti on vielä PENDING-tilassa eikä sitä ole poistettu schema.index( { userId: 1, notificationTokenId: 1, category: 1, label: 1, scheduledAt: 1 }, { unique: true, partialFilterExpression: { status: 'pending', isDeleted: false } } ); |
3. Tarkka toimitus viivejonoilla. Minuuttikohtaisen cronin sijaan ajastin asettaa viestit jonoon heti, mutta lykkää toimitusta tarkkaan eräpäiväminuuttiin käyttäen ActiveMQ:n `scheduled-delay` -otsikkoa:
| client.send(`/queue/${queueName}`, { persistent: 'true' 'AMQ_SCHEDULED_DELAY': String(delayMs), // toimitettu tarkalleen eräpäivänä }, JSON.stringify(message)); |
4. Yksi aktiivinen tunnus laitetta kohden. Osittainen yksilöllinen indeksi takaa tasan yhden aktiivisen tunnuksen per laite; uudet kirjautumiset poistavat siististi vanhan tunnuksen, ja eksponentiaalisella viivästetyllä uudelleenyrityksellä selvitään samanaikaisista kirjautumisista.
5. Vastaanottovahvistuksen tarkistus + automaattinen puhdistus. Lähetyksen jälkeen kysymme Expo-kuittauksia. DeviceNotRegistered-vastaus deaktivoi välittömästi kuolleen tunnuksen, joten emme koskaan tuhlaa lähetystä siihen uudelleen.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // lopeta lähettäminen toimimattomiin laitteisiin } |
6. Vikaantumiskatto. Jokaisella laitteella on uudelleenyrityslaskuri; kolmen peräkkäisen vian jälkeen tunnus poistetaan automaattisesti käytöstä – ei äärettömiä silmukoita, ei haamutunnuksia.
7. Käyttäjän tarkoituksen kunnioittaminen lähetyshetkellä. Ilmoitusasetus tarkistetaan uudelleen kuluttajan toimesta toimituksen yhteydessä, ei vain ajoituksen yhteydessä – joten käyttäjä, joka peruu tilauksen tuntia ennen muistutusta, ei koskaan saa sitä. Viestit palautuvat rehellisiin päätetiloihin: success, failed tai is_missed.
Tulokset
- Jokainen käyttäjä saa muistutukset oikeaan paikalliseen aikaan, maailmanlaajuisesti – ei yhtään työajan ulkopuolista ilmoitusta.
- Kaksoiskappaleilmoitukset eliminoitiin kokonaan tietokantatason toistettavuudella.
- Kuolleet ja vanhentuneet laitetunnukset havaitaan ja poistetaan automaattisesti, pitäen toimituksen puhtaana.
- Luotettavuus paranee ja tietokantakuormitus vähenee tarkan, minuuttitarkan toimituksen ansiosta ilman raakaa voimaa käyttävää ajastinta.
Teknologiapino
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

