Skip to content

Commit 5f5b44b

Browse files
committed
Message streaming, ETag support etc.
The old request/response setup worked great, but had its limitations. The new model types are request, message (zero or more) and a receipt. The receipt can immplement some optional interfaces that gets/sets * ETag * ELastModified * ESize These may autimatically be set by the library if not provided by user (the `Etag` bassed on all message bodies if there's a hash function configured). The primary use case for the above would be client side caching. This also removes the slow Gob codec.
1 parent 2c5a478 commit 5f5b44b

16 files changed

Lines changed: 963 additions & 455 deletions

File tree

.github/workflows/test.yml

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
11
on:
22
push:
3-
branches: [ main ]
3+
branches: [main]
44
pull_request:
55
name: Test
66
jobs:
77
test:
88
strategy:
99
matrix:
10-
go-version: [1.18.x]
10+
go-version: [1.21.x, 1.22.x]
1111
platform: [ubuntu-latest, macos-latest, windows-latest]
1212
runs-on: ${{ matrix.platform }}
1313
steps:
14-
- name: Install Go
15-
uses: actions/setup-go@v3
16-
with:
17-
go-version: ${{ matrix.go-version }}
18-
- name: Install staticcheck
19-
run: go install honnef.co/go/tools/cmd/staticcheck@latest
20-
shell: bash
21-
- name: Install golint
22-
run: go install golang.org/x/lint/golint@latest
23-
shell: bash
24-
- name: Update PATH
25-
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
26-
shell: bash
27-
- name: Checkout code
28-
uses: actions/checkout@v1
29-
- name: Fmt
30-
if: matrix.platform != 'windows-latest' # :(
31-
run: "diff <(gofmt -d .) <(printf '')"
32-
shell: bash
33-
- name: Vet
34-
run: go vet ./...
35-
- name: Staticcheck
36-
run: staticcheck ./...
37-
- name: Lint
38-
run: golint ./...
39-
- name: Test
40-
run: go test -race ./...
14+
- name: Install Go
15+
uses: actions/setup-go@v5
16+
with:
17+
go-version: ${{ matrix.go-version }}
18+
- name: Install staticcheck
19+
run: go install honnef.co/go/tools/cmd/staticcheck@latest
20+
shell: bash
21+
- name: Update PATH
22+
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
23+
shell: bash
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
- name: Fmt
27+
if: matrix.platform != 'windows-latest' # :(
28+
run: "diff <(gofmt -d .) <(printf '')"
29+
shell: bash
30+
- name: Vet
31+
run: go vet ./...
32+
- name: Staticcheck
33+
run: staticcheck ./...
34+
- name: Test
35+
run: go test -race ./...

README.md

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,55 @@ This library implements a simple, custom [RPC protocol](https://en.wikipedia.org
77
A strongly typed client may look like this:
88

99
```go
10+
// Define the request, message and receipt types for the RPC call.
1011
client, err := execrpc.StartClient(
11-
execrpc.ClientOptions[model.ExampleRequest, model.ExampleResponse]{
12+
execrpc.ClientOptions[model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
1213
ClientRawOptions: execrpc.ClientRawOptions{
1314
Version: 1,
1415
Cmd: "go",
15-
Dir: "./examples/servers/typed"
16+
Dir: "./examples/servers/typed",
1617
Args: []string{"run", "."},
18+
Env: env,
19+
Timeout: 30 * time.Second,
1720
},
18-
Codec: codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{},
21+
Codec: codec,
1922
},
2023
)
2124

22-
result, _ := client.Execute(model.ExampleRequest{Text: "world"})
25+
if err != nil {
26+
logg.Fatal(err)
27+
}
28+
29+
30+
// Consume standalone messages (e.g. log messages) in its own goroutine.
31+
go func() {
32+
for msg := range client.MessagesRaw() {
33+
fmt.Println("got message", string(msg.Body))
34+
}
35+
}()
36+
37+
// Execute the request.
38+
result := client.Execute(model.ExampleRequest{Text: "world"})
2339

24-
fmt.Println(result.Hello)
40+
// Check for errors.
41+
if err; result.Err(); err != nil {
42+
logg.Fatal(err)
43+
}
44+
45+
// Consume the messages.
46+
for m := range result.Messages() {
47+
fmt.Println(m)
48+
}
2549

26-
//...
50+
// Wait for the receipt.
51+
receipt := result.Receipt()
52+
53+
// Check again for errors.
54+
if err; result.Err(); err != nil {
55+
logg.Fatal(err)
56+
}
2757

28-
client.Close()
58+
fmt.Println(receipt.Text)
2959

3060
```
3161

@@ -35,41 +65,75 @@ And the server side of the above:
3565

3666
```go
3767
func main() {
38-
server, _ := execrpc.NewServer(
39-
execrpc.ServerOptions[model.ExampleRequest, model.ExampleResponse]{
40-
Call: func(d execrpc.Dispatcher, req model.ExampleRequest) model.ExampleResponse {
41-
return model.ExampleResponse{
42-
Hello: "Hello " + req.Text + "!",
43-
}
68+
getHasher := func() hash.Hash {
69+
return fnv.New64a()
70+
}
71+
72+
server, err := execrpc.NewServer(
73+
execrpc.ServerOptions[model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
74+
// Optional function to get a hasher for the ETag.
75+
GetHasher: getHasher,
76+
77+
// Allows you to delay message delivery, and drop
78+
// them after reading the receipt (e.g. the ETag matches the ETag seen by client).
79+
DelayDelivery: false,
80+
81+
// Handle the incoming call.
82+
Handle: func(c *execrpc.Call[model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]) {
83+
// Raw messages are passed directly to the client,
84+
// typically used for log messages.
85+
c.SendRaw(
86+
execrpc.Message{
87+
Header: execrpc.Header{
88+
Version: 32,
89+
Status: 150,
90+
},
91+
Body: []byte("a log message"),
92+
},
93+
)
94+
95+
// Enqueue one or more messages.
96+
c.Enqueue(
97+
model.ExampleMessage{
98+
Hello: "Hello 1!",
99+
},
100+
model.ExampleMessage{
101+
Hello: "Hello 2!",
102+
},
103+
)
104+
105+
c.Enqueue(
106+
model.ExampleMessage{
107+
Hello: "Hello 3!",
108+
},
109+
)
110+
111+
// Wait for the framework generated receipt.
112+
receipt := <-c.Receipt()
113+
114+
// ETag provided by the framework.
115+
// A hash of all message bodies.
116+
fmt.Println("Receipt:", receipt.ETag)
117+
118+
// Modify if needed.
119+
receipt.Size = uint32(123)
120+
121+
// Close the message stream.
122+
c.Close(false, receipt)
44123
},
45124
},
46125
)
126+
if err != nil {
127+
log.Fatal(err)
128+
}
129+
130+
// Start the server. This will block.
47131
if err := server.Start(); err != nil {
48-
// ... handle error
132+
log.Fatal(err)
49133
}
50-
_ = server.Wait()
51134
}
52135
```
53136

54-
Of the included codecs, JSON seems to win by a small margin (but only tested with small requests/responses):
55-
56-
```bsh
57-
name time/op
58-
Client/JSON-10 4.89µs ± 0%
59-
Client/TOML-10 5.51µs ± 0%
60-
Client/Gob-10 17.0µs ± 0%
61-
62-
name alloc/op
63-
Client/JSON-10 922B ± 0%
64-
Client/TOML-10 1.67kB ± 0%
65-
Client/Gob-10 9.22kB ± 0%
66-
67-
name allocs/op
68-
Client/JSON-10 19.0 ± 0%
69-
Client/TOML-10 28.0 ± 0%
70-
Client/Gob-10 227 ± 0%
71-
```
72-
73137
## Status Codes
74138

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

0 commit comments

Comments
 (0)