No cronjobs. No extra processes in your OS.
Just simple sending scheduled notifications.
Where I used this: Binge-o-Philia
For what: To send a reminder (notifications) of upcoming booked-movie.
TLDR; I maintain a table at my back-end server to store scheduled notifications. Then, I set-up a ping service. The pings act as a trigger at my back-end server to poll the DB for all scheduled notifications which lie within the following ping interval and send all of them.
notification-server
My first plan was to have a custom 'notification server' which would also act as a separate datastore for my booked-movie-reminders. I wrote the code for the notification server, which uses django-background-tasks at its core.
This is VERY simple to use, but it requires an extra (long-running) process, which again, was sort of having cron jobs. Also, having a separate (and dedicated) server also requires proper checks and a few other overheads at both the main back-end server and notification-server in case of revoked notifications (cancellation of booked movies).
ping-service
I started looking for options/alternatives if there is a solution already implemented (hopefully in a better way) and the concept of ping-services caught my attention.
What they basically do is to just hit a particular endpoint at regular (short) intervals, and some can even store the logs, responses, and even provide some insights from that data.
I tried a few and finally settled for Uptime-Doctor which was perfect for me, as it provides free service for a few endpoints and I had a need for only 3.
The Back-End
Since I wasn't going to have a separate server for storing the notifications' data, I needed a table for that in my back-end and made a simple model:
class MovieRequest(models.Model):
creator = models.ForeignKey(User, ...)
recipient = models.ForeignKey(User, ...)
movie = models.ForeignKey(Movie, ...)
date = models.DateField(null=False, blank=False)
time = models.TimeField(null=False, blank=False)
accepted = models.BooleanField(default=False)
rejected = models.BooleanField(default=False)
class BookedMovieNotification(models.Model):
movie_request = models.ForeignKey(MovieRequest, related_name='notifications',
on_delete=models.CASCADE, null=False, blank=False)
time = models.DateTimeField()
Then I reserved an endpoint for receiving pings, polling the DB and sending notifications.
from datetime import timedelta
from django.utils import timezone
from rest_framework.views import APIView
from .models import BookedMovieNotification
def send_booked_movie_reminder(notification: BookedMovieNotification) -> None:
...
class BookedMovieReminderAPIView(APIView):
permission_classes = ()
# Use some Auth to identify requests coming from ping-service
def get(self, request, *args, **kwargs):
poll_max_time = timezone.now() + timedelta(minutes=PING_INTERVAL_MINUTES)
notifications = BookedMovieNotification.objects.filter(time__lte=poll_max_time)
for notification in notifications:
send_booked_movie_reminder(notification)
return Response({})
Push Notifications
The app is made using React-Native and Expo and uses Expo's own Push-Notification features. The push-notifications from the python-based back-end was pretty straightforward as specified in exponent-server-sdk-python.
Conclusion
This approach feels like having a third-party cron job with easier set-up :/
So this may or may not be a good option depending on your use-case and securtity/dependency concerns. As I had to test the feature in minimal time, I went for it and worked out pretty well :)