This library implements a simple, custom RPC protocol via os/exex and stdin and stdout. Both server and client comes in a raw ([]byte
) and strongly typed variant (using Go generics).
A strongly typed client may look like this:
// Define the request, message and receipt types for the RPC call.
client, err := execrpc.StartClient(
client, err := execrpc.StartClient(
execrpc.ClientOptions[model.ExampleConfig, model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
ClientRawOptions: execrpc.ClientRawOptions{
Version: 1,
Cmd: "go",
Dir: "./examples/servers/typed",
Args: []string{"run", "."},
Env: env,
Timeout: 30 * time.Second,
},
Config: model.ExampleConfig{},
Codec: codec,
},
)
if err != nil {
log.Fatal(err)
}
// Consume standalone messages (e.g. log messages) in its own goroutine.
go func() {
for msg := range client.MessagesRaw() {
fmt.Println("got message", string(msg.Body))
}
}()
// Execute the request.
result := client.Execute(model.ExampleRequest{Text: "world"})
// Check for errors.
if err := result.Err(); err != nil {
log.Fatal(err)
}
// Consume the messages.
for m := range result.Messages() {
fmt.Println(m)
}
// Wait for the receipt.
receipt := <-result.Receipt()
// Check again for errors.
if err := result.Err(); err != nil {
log.Fatal(err)
}
fmt.Println(receipt.Text)
// Close the client.
if err := client.Close(); err != nil {
log.Fatal(err)
}
To get the best performance you should keep the client open as long as its needed – and store it as a shared object; it's safe and encouraged to call Execute
from multiple goroutines.
And the server side of the above:
func main() {
log.SetFlags(0)
log.SetPrefix("readme-example: ")
var clientConfig model.ExampleConfig
server, err := execrpc.NewServer(
execrpc.ServerOptions[model.ExampleConfig, model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
// Optional function to provide a hasher for the ETag.
GetHasher: func() hash.Hash {
return fnv.New64a()
},
// Allows you to delay message delivery, and drop
// them after reading the receipt (e.g. the ETag matches the ETag seen by client).
DelayDelivery: false,
// Optional function to initialize the server
// with the client configuration.
// This will be called once on server start.
Init: func(cfg model.ExampleConfig) error {
clientConfig = cfg
return clientConfig.Init()
},
// Handle the incoming call.
Handle: func(c *execrpc.Call[model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]) {
// Raw messages are passed directly to the client,
// typically used for log messages.
c.SendRaw(
execrpc.Message{
Header: execrpc.Header{
Version: 32,
Status: 150,
},
Body: []byte("log message"),
},
)
// Enqueue one or more messages.
c.Enqueue(
model.ExampleMessage{
Hello: "Hello 1!",
},
model.ExampleMessage{
Hello: "Hello 2!",
},
)
c.Enqueue(
model.ExampleMessage{
Hello: "Hello 3!",
},
)
// Wait for the framework generated receipt.
receipt := <-c.Receipt()
// ETag provided by the framework.
// A hash of all message bodies.
// fmt.Println("Receipt:", receipt.ETag)
// Modify if needed.
receipt.Size = uint32(123)
receipt.Text = "echoed: " + c.Request.Text
// Close the message stream and send the receipt.
// Pass true to drop any queued messages,
// this is only relevant if DelayDelivery is enabled.
c.Close(false, receipt)
},
},
)
if err != nil {
handleErr(err)
}
if err := server.Start(); err != nil {
handleErr(err)
}
}
func handleErr(err error) {
log.Fatalf("error: failed to start typed echo server: %s", err)
}
The server can generate an ETag for the messages. This is a hash of all message bodies.
To enable this:
- Provide a
GetHasher
function to the server options. - Have the
Receipt
implement the TagProvider interface.
Note that there are three different optional E-interfaces for the Receipt
:
- TagProvider for the ETag.
- SizeProvider for the size.
- LastModifiedProvider for the last modified timestamp.
A convenient struct that can be embedded in your Receipt
that implements all of these is the Identity.
The status codes in the header between 1 and 99 are reserved for the system. This will typically be used to catch decoding/encoding errors on the server.