Background process in Python/Bottle requests
Sometimes work takes significant time to be done, and creating a new thread isn't possible if you don't own the process context. This means, you'd likely be killed, let's learn how to spawn a process that can perform long-running tasks long after the actual HTTP response has been handled and process closed.
The setup
Imagine a chat application where a sender can write a message to the group. This would be pretty easy:
- Decode the request data
- Store the message
- Send out any notifications (push or even email)
- Update request view
Seems straight forward right? In this below example it's assumed integral components will be handled by bottle, gcm_clerk, apns3
libraries.
Our Handler
@route('/groupchat/send', method='POST')
def send_group_message(self):
# Do something with the user data
sender = data_store.get_user(authorisation_user())
chat = data_store.get_chat(request.body.conversation)
update_data(sender, chat , request.body.message)
# Send out push notifications
[push(user, 'message from %s' % sender.name) for member in chat.users]
# Generate the results for the user
return generate_view(conversation)
Our Notifier
def push(target, notification):
for device in target.devices:
if device.platform == 'APNS':
payload = Payload(alert= notification)
apns = APNs(cert_file='cert.pem', key_file='key.pem')
apns.gateway_server.send_notification(device.push_token, payload)
elif device.platform == 'GCM':
payload = {'gcm.notification.body': notification}
gcm_service = GCM(GCM_key)
gcm_service.send(PlainTextMessage(device.push_token, payload))
For the most part it is simple, however imagine if you're sending out those emails or push notifications. Not only could there be a large number of group members, each user could have a few different platforms each; not to mention that the tranmission for each channel could take from one to several seconds.
There's not much to sending a push notification, the libaries wrap most of it for us. The simple code below will successfully send push notifications, however there's the fact that it's going to be synchronous, and any exceptions from the request handling would need to be caught, lest it abruptly stop any subsequent outgoing notifications in the batch.
If we're doing this synchronously though, the result that the user gets back might be substantially delayed by the fact we're opening and closing possible connections to send push notifications or emails.
Asynchronous approach
Asynchronous? Sounds good. I can just spawn a new thread right? probably not
What's the hard part?
Depending on your host environment (e.g. Bottle/UWSGI) spawning a new thread will be effctive, up until the point your request handler returns the user data and the process is terminated. So, likely that the http response will be finished before all or any outbound connections have finished sending notifications.
So what now?
We can use a process. A process that is spawned will not be terminated when our request handler process is destroyed. You might have used the process library (or subprocess library) to spawn other appliations or terminal commands with python before, but actually the multiprocessing library contains some awesome functionality.
Remember to:
from multiprocessing import Process
We can create a new process within the context of python, not just command line.
Subclass Process
class PushProcess(Process):
def __init__(self, notification, recipients):
super(PushProcess, self).__init__()
self. notification, self.recipients = notification, recipients
def run(self):
for device in [dev for user in self.recipients for dev in user.devices]:
if device.platform == 'APNS':
payload = Payload(alert= self.notification)
apns = APNs(cert_file='cert.pem', key_file='key.pem')
apns.gateway_server.send_notification(device.push_token, payload)
elif device.platform == 'GCM' and alert is not None:
payload = {'gcm.notification.body': self. notification}
gcm_service = GCM(GCM_key)
gcm_service.send(PlainTextMessage(device.push_token, payload))
As you can see, we actually inherit from process, and can have our own initialiser, as well as pass any python objects into our self. It should be said that of course the items cross-process are NOT shared.
Our Updated Handler
@route('/groupchat/send', method='POST')
def send_group_message(self):
# Do something with the user data
sender = data_store.get_user(authorisation_user())
chat = data_store.get_chat(request.body.conversation)
update_data(sender, chat , request.body.message)
# Send out push notifications
PushProcess('message from %s' % sender.name, chat.users).start()
# Generate the results for the user
return generate_view(conversation)
While we could start a process for every user/device if we wished, starting new proceses does have a hard-limit and there will be more resources for each process created (more than just creating a thread) and so we shove all the work that needs to be done asynchronously after the request into one process.
As you can see, it's really not that difficult but the response time will be noticibly quicker and in general will lead to more resiliant request handling for the user, whilst these notifications (or any other task for instance) is handed off to the background.