Micro-services for micro-controllers #869
Replies: 2 comments 2 replies
-
I wouldn't say that they are likely to be updated independently, it is more that they could potentially be updated independently. In all the lib code, it would all be in one go. In our application it also is not possible for a service and its interface to be updated independently. It can happen, but it is not likely. I know this is a small issue, but it is important to notice that is more the exceptional behaviour to have them that independent and not the normal behaviour....
I know it is probably a matter of coding style, but I found it to generate more confusion in our services to separate out the interface instead of just having the Client class be the interface. It somehow felt convoluted to have the Definition class implement the Service interface, so we removed that and then the interface class itself was unnecessary.
All that repetition annoys the eyes 🙂
|
Beta Was this translation helpful? Give feedback.
-
Trying to understand the services framework, I run into several problems. Going to Service Resources things get more complicated. In the example I miss the place where the constant Finally I tried the notification example. but I am afraid I don't really grasp how this is supposed to work. All this arises because of my attempt to create a simple MQTT service that works both ways. Any clarification would be welcome |
Beta Was this translation helpful? Give feedback.
-
Edit: The content of this post has been added to the Toit documentation: https://docs.toit.io/language/sdk/services.
We have built out the underpinnings that will allow us to decompose the functionality of an embedded device into a set of micro-services that are loosely-coupled and fine-grained.
The services can be defined by code running in one container and used from other containers, so it is a natural way of separating out complex drivers (like the ones for cellular modems), so they can run in the own address spaces.
To define a service, we start with an interface. Notice that you can find all the snippets in this document, including this first interface one, in an example in the repository:
The
RandomService.SELECTOR
constant is what will bind a client of the service and the service implementation together. We typically just generate random UUIDs, because all we need is some notion of identity. Thedd9e5fd1-a5e9-464e-b2ef-92bf15ea02ca
constant was generated via https://www.uuidgenerator.net/.The
RandomService.SELECTOR.major
andRandomService.SELECTOR.minor
values represent the current version of the interface. The version is used during service discovery, because it is possible that a client will be trying to access a newer or older implementation of the service due to the fact that clients and implementations are decoupled and are likely to be updated independently of each other.The
next
method is the only method in the interface. We manually assign an index calledNEXT_INDEX
to the method for serialization purposes, but you could imagine generating the interface definition from something like a protocol buffer service and have the indices automatically assigned. If you change the index of any existing method you should bump the major version, but if you only add new methods with previously unused indices, you can get away with just bumping the minor version.Service clients
Once we have the service interface ready, we should make it easy to use the service. This is where a service client comes in. It is a helper class that implements the service interface through an RPC mechanism.
The helper class can be derived from the interface and for
RandomService
it should look like this:Currently, there is no tooling available to automate the generating of the client helper classes, but it certainly might be worth looking into.
The simplest part of the helper is the implementation of the
next
method. It just callsinvoke_
with the method index and the singlelimit
argument. If the method had taken more arguments, we would have wrapped them in a list using[]
, becauseinvoke_
always take exactly two arguments.The constructor takes the
selector
as an argument, but defaults toRandomService.SELECTOR
. The common way to use it is to construct the client when you need it:or to have it in a lazy-initialized constant:
The
--if_absent
block is invoked when we cannot find the requested service. You can provide a timeout if you're willing towait a bit for the service to appear:
Service providers
Now we are ready to provide an implementation of the
RandomService
interface. To do this, we introduce a service provider:The provider has its own name and versioning for debugging purposes (test/[email protected]), but the important part is that it registers that it provides an implementation of the
RandomService
interface through the call toprovides
from the constructor. The implementation of the interface methods are done through thehandle
method that looks at the method index and calls the right implementation method, so it isn't strictly necessary that this implements theRandomService
interface. Again, this code could and probably should be generated through tooling. Thepid
andclient
arguments are useful to identify the caller, which we typically rely on if we're need to keep track of service resources owned by clients.If you want to run the service provider in a separate process, you can combine the service client and the service provider and
spawn
a separate process for the provider:Serialization of arguments
The arguments provided to service method calls are serialized to a flat sequence of bytes using a simple, but efficient serialization mechanism built into the Toit virtual machine. It handles
null
, numbers, booleans, strings, lists, maps, and byte arrays. So if you want to send an instance of a specific class to the other side, you have to adapt the service client code to convert the instance to one of those more primitive types. Similarly, the service provider will be handed the converted type.Service resources and proxies
Sometimes it is useful for a service to let clients refer to resources allocated on their behalf. The resource lives with the service provider and looks something like this:
The
on_closed
method is automatically called when the client closes the resource or if the client happens to go away. Instances ofServiceResource
are automatically serializable, so it is possible to return them from thehandle
method in the service provider and theServiceResource
constructor takes care of registering them correctly, so they can be found later on future client method calls.Now, imagine we added new
create_die
androll_die
methods to our service interface like this:The service provider's
handle
method could then be extended to handle this, but do note that at this point it might make sense to no longer markRandomServiceProvider
as implementingRandomService
, because we're dealing with the calls directly fromhandle
:The resource is automatically converted to an
int
when returned, so now all we need is a way to call methods on theresource. On the client side, we will instantiate a resource proxy for the resource and let that be the facade other client
code uses:
and we will return instances of that from the
RandomServiceClient.create_die
calls:Now we can extend our proxy class with a new
roll
method using theclient_
andhandle_
helpers from theServiceResourceProxy
class:The client class needs to implement the
roll_die
method:That takes care of the client side of things. Now we need to make sure the service provider forwards the
roll
calls to the right resource through itshandle
method:and finally we get to implement the
roll
method onDieResource
:With all that machinery in place, we can now use create resources through our client and call methods on them:
Resource notifications
While most interactions with resources follow a simple request-response pattern, it can be useful to be able to asynchronously notify users of a resource of certain events. The
ServiceResource
andServiceResourceProxy
classes have built-in support for this through notifications. A notification is any kind of serializable object sent from the resource to the proxy. The resources that take part in this must be marked notifiable at construction time:Even though you probably wouldn't expect dice to ping you periodically, we can now experiment with the behavior by adding periodic notifications like this:
The notifications will show up on the proxy side through calls to
on_notified_
:You'll need to wait a bit in
main
for the notifications to start showing up:Example: Network by proxy
We have used the service framework to allow providing a full network implementation from a separate container. The core of this is the
NetworkService
interface and the associatedNetworkServiceClient
:https://github.com/toitlang/toit/blob/master/lib/system/api/network.toit
We use them to build proxying sockets like
SocketResourceProxy_
that forward reads and writes to the network service:You can find all the helpers in the main repository:
https://github.com/toitlang/toit/blob/master/lib/system/base/network.toit
All in all, this allows a cellular driver to provide a network to all other apps that a blissfully unaware that their data flows through a good old-fashioned sequence of AT commands.
Beta Was this translation helpful? Give feedback.
All reactions