At Selltag we built a simple chat service based on events, keeping It as simple as possible.
The message flow is simple. A message arrives, we emit an event, different listeners launch different tasks on background depending on the type of the message.
The service is an HTTP Rest API listening on the private network, except the websocket, It has to be listening through SSL to the Internet for the web clients to be able to connect. In the example I don't distinguish between these two cases, just an application listening to
We have different kind of events, all defined in the
MESSAGE_READ, etc. Those events are emitted in the Tornado handlers and are connected to different listeners with a concrete purpose. If the action requires calling an external service (Android Push, iOS push, analytics, email), the listener will send a background task to execute the action. We were using
Celery for that.
I made a repository with an example of how could that be, It doesn't have everything developed but you could get an idea of how would It work and how to extend It to your needs.
It's not rocket science, It's a good approach when you want to perform completely isolated tasks, talk to third-party services, or separate logic. It's important to notice this solution is not always a good idea because the flow of the application can be very easily messed up and difficult to maintain or follow.
Get to the code
We have an application file
app.py, the entrance of our chat. The
apps/chat/router defines our urls and the handlers of each of those urls.
We send a message through a websocket using
websocket.send from the WebSocket JS client, our
WebSocketHandler inside the handlers of the chat app will get It through the
on_message method, we prepare the message the way we want (ideally It would be a Message model and you could add the attributes you want to be sent there), and we emit an event using the blinker package.
from blinker import signal signal(Event.NEW_MESSAGE).send(data)
We have initialized the active listeners in the app.
def __init__(self, router): # ... self.listeners = [AndroidListener, IosListener, PersistListener] def start_listeners(self): for listener in self.listeners: self.started_listeners.append(listener(self.options)) # ...
In those listeners, we can connect with the
Event.NEW_MESSAGE event to get the message and do something with It. For example in the Android listener(
apps/chat/listeners.py) we'd have a connection like:
from blinker import signal class AndroidListener(): def __init__(self, options): # ... signal(Event.NEW_MESSAGE).connect(self.send) def send(self, message): # Send push notification logging.info('SENDING ANDROID PUSH')
That's basically how could we use events to perform different separate actions, in the example we have also an
Event.NEW_DEVICE. At Selltag we were using that event when a
X-DEVICE-ID HTTP header was present in the request.
If the header was present, we emitted the event and persisted the device id for this user to be able to send him/her push notifications using Android/iOS.
The easiest way of checking out the application is using Vagrant and Ansible, make sure you have them installed.
cd ansible ansible-galaxy install -r requirements.yml cd .. vagrant up
Once It's finished you can go to
localhost:8888 in your browser and send yourself a message. ;-)
It's important to notice I am using Redis as a backend, but if you'd want to use a different one,
MongoDB for example, you could implement the example abstract classes from
base_backend.py as I did. We should have added more abstract classes and methods to have a platform agnostic skeleton of the application, you can do It if you feel the urge. :-)
In a following article I will write about how we improved our microservices architecture using rpc and Nameko.