Skip to content

Latest commit

 

History

History
229 lines (172 loc) · 7.1 KB

tutorial-clock.md

File metadata and controls

229 lines (172 loc) · 7.1 KB

How to create a new service

This guide will show you how to create and test a new service.

In this tutorial, we will implements a timestamp service whose role is to allow clients to produce synchronized timestamps. It is based on the clock service example.

Define the service interface

The public interface of a service is described by an IDL file. Create a file clock.qi.idl with the content:

    package clock

    interface Timestamp
        fn nanoseconds() -> int64
    end

This IDL file will be processed to generate the serializing code.

Generate the server stub

The IDL file is used to generate the necessary boiler code to implement the service. Use qiloop to generate the server stub:

    qiloop stub --idl clock.qi.idl --output clock_stub_gen.go

Automate the stub generation

In order to easily update the clock_stub_gen.go file, create a file called generate.go with the following content:

    //go:generate qiloop stub --idl clock.qi.idl --output clock_stub_gen.go
    package clock

Then execute the command:

    go generate

Implement the service

The file clock_stub_gen.go defines an interface called TimestampImplementor which describes the methods to be implemented in order to create the timestamp service:

    type TimestampImplementor interface {
            Activate(activation bus.Activation, helper TimestampSignalHelper) error
            OnTerminate()
            Nanoseconds() (int64, error)
    }

The Activate method is called just before the service registration. In contains runtime information useful for the service. For example the activation parameter contains a session to connect other services.

The OnTerminate method is called just before the service terminates.

Finally, the Nanoseconds method is the implementation of the timestamp function. Let's start with creating something which computes timestamps:

    // Timestamper creates monotonic timestamps.
    type Timestamper time.Time

    // Nanoseconds returns the timestamp.
    func (t Timestamper) Nanoseconds() (int64, error) {
            return time.Since(time.Time(t)).Nanoseconds(), nil
    }

Using this Timestamper type, let's implements the TimestampImplementor interface:

    // timestampService implements TimestampImplementor.
    type timestampService struct {
            Timestamper // inherits the Nanoseconds method.
    }

    // Activate is called once the service is online. It provides the
    // implementation important runtime informations.
    func (t timestampService) Activate(activation bus.Activation,
            helper TimestampSignalHelper) error {
            return nil
    }

    // OnTerminate is called when the service is termninated.
    func (t timestampService) OnTerminate() {
    }

The file clock_stub_gen defines a constructor method for Timestamp object called TimestampObject:

    func TimestampObject(impl TimestampImplementor) bus.Actor

It returns a bus.Actor type which can be passed to a bus.Server. Let's wrap this function to create a timestamp object based on our implementation of TimestampImplementor:

    // NewTimestampObject creates a timestamp object which can be
    // registered to a bus.Server.
    func NewTimestampObject() bus.Actor {
            return TimestampObject(timestampService{
                    Timestamper(time.Now()),
            })
    }

That's it. The service implementation is completed. Let's use it in a main() function to start the service.

Create a program

In order to use the timestamp service, we need a program which uses NewTimestampObject and registers it. The app package contains an helper function for this.

package main

import (
        "flag"
        "log"
        "os"
        "os/signal"

        "github.com/lugu/qiloop/app"
        "github.com/lugu/qiloop/examples/clock"
)

func main() {
        flag.Parse()

        server, err := app.ServerFromFlag("Timestamp", clock.NewTimestampObject())
        if err != nil {
                log.Fatal(err)
        }
        defer server.Terminate()

        log.Print("Timestamp service running...")

        interrupt := make(chan os.Signal, 1)
        signal.Notify(interrupt, os.Interrupt)

        // wait until the server fails or is interrupted.
        select {
        case err = <-server.WaitTerminate():
                if err != nil {
                        log.Fatal(err)
                }
        case <-interrupt:
                log.Print("interrupt, quitting.")
        }
}

In order to test it, we need a running instance of QiMessaging. We can create one with the qiloop server command:

    $ qiloop server
    2019/07/15 22:57:09 Listening at tcp://localhost:9559

Now we can start the timestamp service with:

    $ go run ./examples/clock/cmd/service/main.go
2019/07/15 23:00:20 Timestamp service running...

Let double check if the timestamp service is registered to the service directory using qiloop info:

$ qiloop info
[
    {
	"Name": "ServiceDirectory",
	"ServiceId": 1,
	"MachineId": "e9b7594a1f209b898e7a3caea5e3199a407cf5bb08d090419e4fffdeddcf167f",
	"ProcessId": 17179,
	"Endpoints": [
	    "tcp://localhost:9559"
	],
	"SessionId": ""
    },
    {
	"Name": "Timestamp",
	"ServiceId": 2,
	"MachineId": "e9b7594a1f209b898e7a3caea5e3199a407cf5bb08d090419e4fffdeddcf167f",
	"ProcessId": 18596,
	"Endpoints": [
	    "unix:///tmp/qiloop-271149288"
	],
	"SessionId": ""
    }
]

Mission completed: a fonctionnal timestamp service! But wait, for a timestamp to be precise it needs to be locally generated. Let's use use this service to synchronize a Timestamper so we can have precise and synchronized timestamps.

    // SynchronizedTimestamper returns a locally generated timestamp
    // source synchronized is the remote Timestamp service.
    func SynchronizedTimestamper(session bus.Session) (Timestamper, error) {

            ref := time.Now()

            constructor := Services(session)
            timestampProxy, err := constructor.Timestamp()
            if err != nil {
                    return Timestamper(ref),
                            fmt.Errorf("reference timestamp: %s", err)
            }

            delta1 := time.Since(ref)
            ts, err := timestampProxy.Nanoseconds()
            delta2 := time.Since(ref)

            if err != nil {
                    return Timestamper(ref),
                            fmt.Errorf("reference timestamp: %s", err)
            }

            offset := ((delta1 + delta2) / 2) - time.Duration(ts)
            return Timestamper(ref.Add(offset)), nil
    }

That's it. We have seen how to implement a QiMessaging service and acces it.

The complete code can be found here.