Documentation
tomodachi

Building a microservice

Building blocks for a service class and microservice entrypoint

  1. import tomodachi and create a class that extends tomodachi.Service – it can be called anything or just Service to keep it simple and clean.
  2. Add a name attribute to the class and give it a string value. Having a name attribute isn't required, but good practice and can be included in your log output.
  3. Define an awaitable function (using async def) in the service class – in this example we'll use it as an entrypoint to trigger code in the service by decorating it with one of the available invoker decorators. Note that a service class must have at least one decorated function available, for it to even be recognized as a service by tomodachi run.
  4. Decide on how to trigger the function – for example using HTTP, pub/sub or on a timed interval, then decorate your function with one of these trigger / subscription decorators, which also invokes what capabilities the service initially has.

Read more about how each of the built-in invoker decorators work and which keywords and parameters you can use to change their behaviour.

📘

Note: Publishing and subscribing to events and messages may require user credentials or hosting configuration to be able to access queues and topics.

For simplicity, let's do HTTP:

  • On each POST request to /sheep, the service will wait for up to one whole second (pretend that it's performing I/O – waiting for a response to a slow sheep counting database modification, for example) and then issue a 200 OK with some data.
  • It's also possible to query the amount of times the POST tasks has run by doing a GET request to the same url, /sheep.
  • By using @tomodachi.http an HTTP server backed by aiohttp will be started on service init. tomodachi will act as a middleware to route requests to the correct handlers, upgrade websocket connections and later also gracefully await active connections with still executing tasks, when the service is asked to stop – up until a configurable amount of time has passed. So called graceful termination.
import asyncio
import random

import tomodachi


class Service(tomodachi.Service):
    name = "sleepy-sheep-counter"

    _sheep_count = 0

    @tomodachi.http("POST", r"/sheep")
    async def add_to_sheep_count(self, request):
        await asyncio.sleep(random.random())
        self._sheep_count += 1
        return 200, str(self._sheep_count)

    @tomodachi.http("GET", r"/sheep")
    async def return_sheep_count(self, request):
        return 200, str(self._sheep_count)
# generic execution of services
app/ $ tomodachi run <service.py ...>

# example: if our service class exists in service/app.py
app/ $ tomodachi run service/app.py

Beside the currently existing built-in ways of interfacing with a service, it's also possible to build custom function decorators for the use-cases one may have.

To give a few possible examples / ideas of functionality that could be coded to call functions with data in similar ways:

  • Using Redis as a task queue with configurable keys to push or pop onto.
  • Subscribing to Kinesis or Kafka event streams and act on the data received.
  • An abstraction around otherwise complex functionality or to unify API design.
  • As an example to above sentence; GraphQL resolver functionality with built-in tracability and authentication management, with a unified API to application devs.