Writing services

Services are the main construction blocks of Snorky. In this section you will learn how services work and how they are created.

The Snorky service protocol

Every message send through Snorky must be directed to a service, identified by a service name, which shall be an string.

The message itself must be a JSON entity. That includes strings, numbers, arrays, objects and the null value. The JSON web site contains the full specification of the language, describing each data type in detail.

Often in this documentation, JSON arrays will be referred as lists and JSON objects will be referred as dictionaries, matching the Python data types those primitive types are transformed into.

The following is an example of a message sent to an echo service, as it could be sent through WebSocket:

{"service":"echo","message":"Hello"}

The Service definition

A service class must inherit from Service or one of their descendants. It must provide an implementation for the method Service.process_message_from().

The following example service sends back to the client each message it receives:

from snorky.services.base import Service

class EchoService(Service):
    def process_message_from(self, client, msg):
        self.send_message_to(client, msg)
class snorky.services.base.Service(name)[source]

Subclass this class and redefine process_message_from() in order to create a new service.

process_message_from(client, msg)[source]

Called when a message is received.

msg contains the message as a JSON decoded entity. msg and all their descedants are always hashable.

send_message_to(client, msg)[source]

Sends a message to a client through the current service.

Services should use this method instead of calling directly to client.send() in order to add the service header.

client_connected(client)[source]

Called each time a client connects to Snorky through a channel which is connected to the same snorky.ServiceRegistry than this service.

Exceptionally, this method is not called when a client connects from a short-lived channel like snorky.request_handlers.http.BackendHTTPHandler.

client_disconnected(client)[source]

Called each time a client disconnects from Snorky through a channel which is connected to the same snorky.ServiceRegistry than this service.

Exceptionally, this method is not called when a client connects from a short-lived channel like snorky.request_handlers.http.BackendHTTPHandler.

RPC services

Although you could write services inheriting directly from Service and using its simple methods, they often fall short.

Most services often work, at least partially, in a request-response fashion, ocasionally sending notifications to the client that are not part of the response.

Snorky leverages this pattern through the subclass RPCService. Currently, all instanciable Snorky services are RPC services, and chances are yours will be too.

class snorky.services.base.RPCService(name)[source]

Subclass this class to make RPC services.

RPC services expose a more convenient interface than bare Snorky services.

Commands

Each RPC service has a series of commands which are defined as methods with the rpc_command() decorator.

Commands accept a set of parameters which is specified in the signature of the method. They may have default values.

The return value of a command is sent automatically to the requester client. Every entity that can be serialized as JSON is a valid return value.

If the method does not return anything, null is sent as response. This is usually done in order to signal that the request has been processed successfully but there is nothing interesting to send in return.

The following service calculates sums and logarithms in response to client requests:

import math
from snorky.services.base import RPCService, rpc_command

class CalculatorService(RPCService):
    @rpc_command
    def sum(self, req, number1, number2):
        return number1 + number2

    @rpc_command
    def log(self, req, number, base=2.718):
        return math.log(number, base)

Note

The names of RPC commands and their parameters are usually written in camelCase instead of snake_case because they are exposed in Javascript with the same name.

Exceptions

Sometimes you want to signal an error condition. In this cases, instead of returning, raise an instance of RPCError. For example, raise RPCError("Not authorized").

Snorky already signals some error conditions by default:

  • If a client requests a non existing command, Unknown command is raised.
  • If the request params don’t fit the ones specified in the method, i.e. nonexistent parameters are used or required parameters are ommited, Invalid params is raised.
  • If an exception different from RPCError is raised while the command is being handled, Internal error is raised.

Asynchronous commands

Sometimes the processing of a command has to be temporarily suspended until a certain event occurs.

For example, the command may need to perform a HTTP request. It’s undesirable for the command to block the entire server, as that would kill performance. Instead, asynchronous requests shall be used.

Such RPC commands must use the decorator rpc_asynchronous(), in addition to rpc_command().

Asynchronous RPC commands do not send a response when the method call returns nothing. Instead, is expected that the request will be replied eventually as a response to another event.

The req parameter in RPC commands contains a Request object with methods to send either a successful reply or signal an error to the client.

The Request class

class snorky.services.base.Request(service, client, msg)[source]

Represents a request against an RPC service and provides methods to resolve it.

reply(data)[source]

Sends a successful response.

Each request can be resolved one time. Calling this method twice or calling both reply() and error() will trigger a server error.

error(msg)[source]

Sends an error response.

Each request can be resolved one time. Calling this method twice or calling both reply() and error() will trigger a server error.

client

The client which initiated this requests.

The requester client. It complies with the interface defined in snorky.client.Client.

command

The requested command.

The requested command name.

params

The specified parameters as a dictionary.

The params supplied by the client, as a dictionary.

resolved

Whether the request has been resolved either with success or failure.

True if the request has been resolved either with a successful reply or with an error.

Sending notifications

At any moment you can send an arbitrary message to any client of your service. These messages should be JSON objects and should contain a type attribute which must be neither response or error, since these types are used by RPC calls.

Messages which are neither of type response or error are called notifications.

The following example shows a simple PubSub service in which any client can publish a message to every client subscribed (including itself):

from snorky.services.base import RPCService, rpc_command

class MinimalPubSubService(RPCService):
    def __init__(self, name):
        # Call parent constructor
        RPCService.__init__(self, name)

        self.clients = set()

    @rpc_command
    def subscribe(self, req):
        if req.client not in self.clients:
            self.clients.add(req.client)

    @rpc_command
    def unsubscribe(self, req):
        if req.client in self.clients:
            self.clients.remove(req.client)

    def client_disconnected(self, client):
        # Never forget to remove the client from the set after disconnection!
        if client in self.clients:
            self.clients.remove(client)

    @rpc_command
    def publish(self, req, message):
        for client in self.clients:
            self.send_message_to(client, {
                "type": "publication",
                "message": message,
            })

Conclusion

This chapter has explained how to built services with Snorky.

Although Snorky comes with a few services, often you will need to extend them or create small specific services for your application. Nevertheless, Snorky utilities should not make this task difficult.

The next chapter will explain how to connect to Snorky from a web application and how service connectors are created in Javascript.