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()
anderror()
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()
anderror()
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.