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.
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.
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
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
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.
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.