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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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.SELECTORconstant 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-92bf15ea02caconstant was generated via https://www.uuidgenerator.net/.The
RandomService.SELECTOR.majorandRandomService.SELECTOR.minorvalues 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
nextmethod is the only method in the interface. We manually assign an index calledNEXT_INDEXto 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
RandomServiceit 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
nextmethod. It just callsinvoke_with the method index and the singlelimitargument. 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
selectoras 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_absentblock 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
RandomServiceinterface. 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
RandomServiceinterface through the call toprovidesfrom the constructor. The implementation of the interface methods are done through thehandlemethod that looks at the method index and calls the right implementation method, so it isn't strictly necessary that this implements theRandomServiceinterface. Again, this code could and probably should be generated through tooling. Thepidandclientarguments 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
spawna 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_closedmethod is automatically called when the client closes the resource or if the client happens to go away. Instances ofServiceResourceare automatically serializable, so it is possible to return them from thehandlemethod in the service provider and theServiceResourceconstructor takes care of registering them correctly, so they can be found later on future client method calls.Now, imagine we added new
create_dieandroll_diemethods to our service interface like this:The service provider's
handlemethod could then be extended to handle this, but do note that at this point it might make sense to no longer markRandomServiceProvideras implementingRandomService, because we're dealing with the calls directly fromhandle:The resource is automatically converted to an
intwhen 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_diecalls:Now we can extend our proxy class with a new
rollmethod using theclient_andhandle_helpers from theServiceResourceProxyclass:The client class needs to implement the
roll_diemethod:That takes care of the client side of things. Now we need to make sure the service provider forwards the
rollcalls to the right resource through itshandlemethod:and finally we get to implement the
rollmethod 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
ServiceResourceandServiceResourceProxyclasses 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
mainfor 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
NetworkServiceinterface 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