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) }
Generate ETag
The server can generate an ETag for the messages. This is a hash of all message bodies.
To enable this:
- Provide a
GetHasherfunction to the server options. - Have the
Receiptimplement 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.
Status Codes
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.