starpc 0.49.18 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
- package/dist/echo/echo.pb.d.ts +1 -1
- package/dist/echo/echo.pb.js +3 -3
- package/dist/integration/cross-language/ts-client.js +84 -11
- package/dist/integration/cross-language/ts-server.js +100 -4
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/receipt.test.d.ts +1 -0
- package/dist/rpcstream/receipt.test.js +41 -0
- package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
- package/dist/rpcstream/rpcstream.pb.js +14 -8
- package/dist/srpc/call-receipt.d.ts +17 -0
- package/dist/srpc/call-receipt.js +106 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +375 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -3
- package/dist/srpc/common-rpc.js +87 -10
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +3 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +45 -1
- package/dist/srpc/rpcproto.pb.js +60 -7
- package/dist/srpc/server-invocation.d.ts +17 -0
- package/dist/srpc/server-invocation.js +37 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/echo/echo.pb.go +1 -1
- package/echo/echo.pb.ts +6 -5
- package/echo/echo_srpc.pb.cpp +1 -1
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/echo_srpc.pb.hpp +1 -1
- package/echo/echo_srpc.pb.rs +1 -1
- package/go.mod +13 -12
- package/go.sum +24 -24
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
- package/integration/cross-language/go-server/main.go +169 -4
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +115 -7
- package/mock/index.ts +2 -2
- package/mock/mock.pb.go +1 -1
- package/mock/mock.pb.ts +6 -5
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +1 -1
- package/package.json +19 -26
- package/srpc/accept.go +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +441 -0
- package/srpc/call-receipt.ts +131 -0
- package/srpc/call-receipt_test.go +536 -0
- package/srpc/client-rpc.go +1 -8
- package/srpc/client.ts +29 -2
- package/srpc/common-rpc.go +114 -29
- package/srpc/common-rpc.ts +104 -13
- package/srpc/handler.ts +2 -0
- package/srpc/index.ts +4 -0
- package/srpc/invoker.ts +10 -5
- package/srpc/msg-stream.go +8 -0
- package/srpc/muxed-conn.go +9 -4
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.cc +15 -4
- package/srpc/rpcproto.pb.go +97 -1
- package/srpc/rpcproto.pb.h +59 -0
- package/srpc/rpcproto.pb.rs +45 -0
- package/srpc/rpcproto.pb.ts +90 -27
- package/srpc/rpcproto.proto +16 -0
- package/srpc/schema-ownership_test.go +115 -0
- package/srpc/server-invocation.go +40 -0
- package/srpc/server-invocation.ts +76 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream-yamux.go +1 -1
- package/srpc/stream.go +20 -0
- package/srpc/websocket.go +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"go/ast"
|
|
5
|
+
"go/parser"
|
|
6
|
+
"go/token"
|
|
7
|
+
"os"
|
|
8
|
+
"strconv"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// forbiddenFixtureCalls names the pipeline constructors that rebuild a second
|
|
13
|
+
// srpc server inside a fixture. The receipt post-mortem removed a hand-built
|
|
14
|
+
// ReadPump pipeline; the fixture must drive one srpc.Server through one
|
|
15
|
+
// Server.HandleStream and observe framed packets by decorating net.Conn.
|
|
16
|
+
var forbiddenFixtureCalls = []string{"NewServerRPC", "NewPacketReadWriter", "ReadPump"}
|
|
17
|
+
|
|
18
|
+
// fixtureOwnerViolations returns the ways a cross-language Go fixture source
|
|
19
|
+
// diverges from the single production server owner.
|
|
20
|
+
func fixtureOwnerViolations(src string) []string {
|
|
21
|
+
fset := token.NewFileSet()
|
|
22
|
+
file, err := parser.ParseFile(fset, "fixture.go", src, 0)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return []string{"parse error: " + err.Error()}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
seen := map[string]bool{}
|
|
28
|
+
handleStreamCalls := 0
|
|
29
|
+
newServerCalls := 0
|
|
30
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
31
|
+
call, ok := n.(*ast.CallExpr)
|
|
32
|
+
if !ok {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
36
|
+
if !ok {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
switch sel.Sel.Name {
|
|
40
|
+
case "HandleStream":
|
|
41
|
+
handleStreamCalls++
|
|
42
|
+
case "NewServer":
|
|
43
|
+
newServerCalls++
|
|
44
|
+
case "NewServerRPC", "NewPacketReadWriter", "ReadPump":
|
|
45
|
+
seen[sel.Sel.Name] = true
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
var violations []string
|
|
51
|
+
for _, name := range forbiddenFixtureCalls {
|
|
52
|
+
if seen[name] {
|
|
53
|
+
violations = append(violations, "reconstructs server pipeline via "+name)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if newServerCalls != 1 {
|
|
57
|
+
violations = append(violations, "expected exactly one srpc.NewServer, found "+strconv.Itoa(newServerCalls))
|
|
58
|
+
}
|
|
59
|
+
if handleStreamCalls != 1 {
|
|
60
|
+
violations = append(violations, "expected exactly one Server.HandleStream entrypoint, found "+strconv.Itoa(handleStreamCalls))
|
|
61
|
+
}
|
|
62
|
+
if !decoratesNetConn(file) {
|
|
63
|
+
violations = append(violations, "no fixture type decorates net.Conn")
|
|
64
|
+
}
|
|
65
|
+
return violations
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// decoratesNetConn reports whether the source embeds net.Conn in a struct, the
|
|
69
|
+
// only sanctioned way for fixture instrumentation to observe framed packets.
|
|
70
|
+
func decoratesNetConn(file *ast.File) bool {
|
|
71
|
+
found := false
|
|
72
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
73
|
+
st, ok := n.(*ast.StructType)
|
|
74
|
+
if !ok {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
for _, field := range st.Fields.List {
|
|
78
|
+
if len(field.Names) != 0 {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
sel, ok := field.Type.(*ast.SelectorExpr)
|
|
82
|
+
if !ok {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
pkg, ok := sel.X.(*ast.Ident)
|
|
86
|
+
if ok && pkg.Name == "net" && sel.Sel.Name == "Conn" {
|
|
87
|
+
found = true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
})
|
|
92
|
+
return found
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// TestFixtureUsesSingleServerOwner proves the shipped fixture drives one
|
|
96
|
+
// srpc.Server through one Server.HandleStream and decorates net.Conn.
|
|
97
|
+
func TestFixtureUsesSingleServerOwner(t *testing.T) {
|
|
98
|
+
src, err := os.ReadFile("main.go")
|
|
99
|
+
if err != nil {
|
|
100
|
+
t.Fatalf("read main.go: %v", err)
|
|
101
|
+
}
|
|
102
|
+
if violations := fixtureOwnerViolations(string(src)); len(violations) != 0 {
|
|
103
|
+
t.Fatalf("fixture diverged from single server owner: %v", violations)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// TestFixtureGuardDetectsSecondPipeline proves the guard fires when a fixture
|
|
108
|
+
// rebuilds a second RPC server pipeline.
|
|
109
|
+
func TestFixtureGuardDetectsSecondPipeline(t *testing.T) {
|
|
110
|
+
bad := `package main
|
|
111
|
+
|
|
112
|
+
import (
|
|
113
|
+
"net"
|
|
114
|
+
|
|
115
|
+
"github.com/aperturerobotics/starpc/srpc"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
func run(conn net.Conn) {
|
|
119
|
+
prw := srpc.NewPacketReadWriter(conn)
|
|
120
|
+
rpc := srpc.NewServerRPC(nil, nil)
|
|
121
|
+
_ = rpc.ReadPump(prw)
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
if violations := fixtureOwnerViolations(bad); len(violations) == 0 {
|
|
125
|
+
t.Fatal("guard failed to flag a second server pipeline")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -2,10 +2,13 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"encoding/binary"
|
|
5
6
|
"fmt"
|
|
6
7
|
"net"
|
|
7
8
|
"os"
|
|
8
9
|
"os/signal"
|
|
10
|
+
"sync"
|
|
11
|
+
"sync/atomic"
|
|
9
12
|
|
|
10
13
|
"github.com/aperturerobotics/starpc/echo"
|
|
11
14
|
"github.com/aperturerobotics/starpc/srpc"
|
|
@@ -15,14 +18,74 @@ func main() {
|
|
|
15
18
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
16
19
|
defer cancel()
|
|
17
20
|
|
|
21
|
+
receiptMode := len(os.Args) > 1 && os.Args[1] == "receipt"
|
|
22
|
+
receiptCase := ""
|
|
23
|
+
if receiptMode {
|
|
24
|
+
if len(os.Args) < 3 {
|
|
25
|
+
fmt.Fprintln(os.Stderr, "usage: go-server receipt <case>")
|
|
26
|
+
os.Exit(1)
|
|
27
|
+
}
|
|
28
|
+
receiptCase = os.Args[2]
|
|
29
|
+
switch receiptCase {
|
|
30
|
+
case "commit", "abort", "loss", "bare-close":
|
|
31
|
+
default:
|
|
32
|
+
fmt.Fprintf(os.Stderr, "unknown receipt case: %s\n", receiptCase)
|
|
33
|
+
os.Exit(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
mux := srpc.NewMux()
|
|
19
37
|
echoServer := echo.NewEchoServer(mux)
|
|
20
38
|
if err := echo.SRPCRegisterEchoer(mux, echoServer); err != nil {
|
|
21
39
|
fmt.Fprintf(os.Stderr, "register error: %v\n", err)
|
|
22
40
|
os.Exit(1)
|
|
23
41
|
}
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
var receiptDone <-chan struct{}
|
|
43
|
+
var finishReceipt func()
|
|
44
|
+
var receiptCommitted atomic.Bool
|
|
45
|
+
if receiptMode {
|
|
46
|
+
done := make(chan struct{})
|
|
47
|
+
var doneOnce sync.Once
|
|
48
|
+
receiptDone = done
|
|
49
|
+
finishReceipt = func() {
|
|
50
|
+
doneOnce.Do(func() {
|
|
51
|
+
close(done)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var invoker srpc.Invoker = mux
|
|
56
|
+
if receiptMode {
|
|
57
|
+
invoker = srpc.InvokerFunc(func(
|
|
58
|
+
serviceID, methodID string,
|
|
59
|
+
strm srpc.Stream,
|
|
60
|
+
) (bool, error) {
|
|
61
|
+
handled, err := mux.InvokeMethod(serviceID, methodID, strm)
|
|
62
|
+
if err != nil || !handled {
|
|
63
|
+
return handled, err
|
|
64
|
+
}
|
|
65
|
+
invocation, ok := srpc.GetServerInvocation(strm.Context())
|
|
66
|
+
if !ok {
|
|
67
|
+
return true, context.Canceled
|
|
68
|
+
}
|
|
69
|
+
kind, waitErr := invocation.WaitTerminal(context.Background())
|
|
70
|
+
markerErr := emitReceiptEvent(
|
|
71
|
+
fmt.Sprintf("SERVER_RECEIPT_TERMINAL %s", terminalName(kind)),
|
|
72
|
+
)
|
|
73
|
+
if waitErr != nil {
|
|
74
|
+
return true, waitErr
|
|
75
|
+
}
|
|
76
|
+
if markerErr != nil {
|
|
77
|
+
return true, markerErr
|
|
78
|
+
}
|
|
79
|
+
if kind == srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
80
|
+
receiptCommitted.Store(true)
|
|
81
|
+
}
|
|
82
|
+
if kind != srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
83
|
+
finishReceipt()
|
|
84
|
+
}
|
|
85
|
+
return true, nil
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
server := srpc.NewServer(invoker)
|
|
26
89
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
27
90
|
if err != nil {
|
|
28
91
|
fmt.Fprintf(os.Stderr, "listen error: %v\n", err)
|
|
@@ -30,11 +93,18 @@ func main() {
|
|
|
30
93
|
}
|
|
31
94
|
defer ln.Close()
|
|
32
95
|
|
|
96
|
+
if receiptMode {
|
|
97
|
+
go func() {
|
|
98
|
+
<-receiptDone
|
|
99
|
+
_ = ln.Close()
|
|
100
|
+
}()
|
|
101
|
+
}
|
|
102
|
+
|
|
33
103
|
fmt.Printf("LISTENING %s\n", ln.Addr().String())
|
|
34
104
|
|
|
35
105
|
go func() {
|
|
36
106
|
<-ctx.Done()
|
|
37
|
-
ln.Close()
|
|
107
|
+
_ = ln.Close()
|
|
38
108
|
}()
|
|
39
109
|
|
|
40
110
|
for {
|
|
@@ -42,6 +112,101 @@ func main() {
|
|
|
42
112
|
if err != nil {
|
|
43
113
|
return
|
|
44
114
|
}
|
|
45
|
-
|
|
115
|
+
var stream net.Conn
|
|
116
|
+
stream = conn
|
|
117
|
+
if receiptMode {
|
|
118
|
+
stream = &receiptConn{
|
|
119
|
+
Conn: conn,
|
|
120
|
+
finishReceipt: finishReceipt,
|
|
121
|
+
committed: &receiptCommitted,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
go server.HandleStream(ctx, stream)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type receiptConn struct {
|
|
129
|
+
net.Conn
|
|
130
|
+
finishReceipt func()
|
|
131
|
+
committed *atomic.Bool
|
|
132
|
+
inspect []byte
|
|
133
|
+
ackPending bool
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func (c *receiptConn) Write(p []byte) (int, error) {
|
|
137
|
+
if !c.ackPending {
|
|
138
|
+
if err := c.observeReceiptPackets(p); err != nil {
|
|
139
|
+
return 0, err
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
n, err := c.Conn.Write(p)
|
|
143
|
+
if err != nil {
|
|
144
|
+
return n, err
|
|
145
|
+
}
|
|
146
|
+
if c.ackPending && n == len(p) {
|
|
147
|
+
c.finishReceipt()
|
|
148
|
+
c.ackPending = false
|
|
149
|
+
}
|
|
150
|
+
return n, nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func (c *receiptConn) observeReceiptPackets(p []byte) error {
|
|
154
|
+
c.inspect = append(c.inspect, p...)
|
|
155
|
+
for len(c.inspect) >= 4 {
|
|
156
|
+
size := int(binary.LittleEndian.Uint32(c.inspect[:4]))
|
|
157
|
+
if len(c.inspect) < 4+size {
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
pkt := &srpc.Packet{}
|
|
161
|
+
if err := pkt.UnmarshalVT(c.inspect[4 : 4+size]); err != nil {
|
|
162
|
+
return err
|
|
163
|
+
}
|
|
164
|
+
c.inspect = c.inspect[4+size:]
|
|
165
|
+
data := pkt.GetCallData()
|
|
166
|
+
if !c.committed.Load() {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if data == nil || !data.GetComplete() || data.GetError() != "" {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if err := emitReceiptEvent("SERVER_RECEIPT_ACK_WRITE committed"); err != nil {
|
|
173
|
+
return err
|
|
174
|
+
}
|
|
175
|
+
c.ackPending = true
|
|
176
|
+
}
|
|
177
|
+
return nil
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func terminalName(kind srpc.TerminalKind) string {
|
|
181
|
+
switch kind {
|
|
182
|
+
case srpc.TerminalKind_TERMINAL_KIND_COMMITTED:
|
|
183
|
+
return "committed"
|
|
184
|
+
case srpc.TerminalKind_TERMINAL_KIND_CANCELED:
|
|
185
|
+
return "canceled"
|
|
186
|
+
case srpc.TerminalKind_TERMINAL_KIND_TRANSPORT_LOST:
|
|
187
|
+
return "transportLost"
|
|
188
|
+
case srpc.TerminalKind_TERMINAL_KIND_CLOSED:
|
|
189
|
+
return "closed"
|
|
190
|
+
case srpc.TerminalKind_TERMINAL_KIND_ABANDONED:
|
|
191
|
+
return "abandoned"
|
|
192
|
+
default:
|
|
193
|
+
return "unknown"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func emitReceiptEvent(line string) error {
|
|
198
|
+
fmt.Println(line)
|
|
199
|
+
fifo := os.Getenv("RECEIPT_EVENT_FIFO")
|
|
200
|
+
if fifo == "" {
|
|
201
|
+
return nil
|
|
202
|
+
}
|
|
203
|
+
file, err := os.OpenFile(fifo, os.O_WRONLY, 0) //nolint:gosec // FIFO path is created by the local integration runner.
|
|
204
|
+
if err != nil {
|
|
205
|
+
return err
|
|
206
|
+
}
|
|
207
|
+
if _, err := file.WriteString(line + "\n"); err != nil {
|
|
208
|
+
_ = file.Close()
|
|
209
|
+
return err
|
|
46
210
|
}
|
|
211
|
+
return file.Close()
|
|
47
212
|
}
|
|
@@ -20,6 +20,8 @@ PASSED=0
|
|
|
20
20
|
FAILED=0
|
|
21
21
|
ERRORS=""
|
|
22
22
|
|
|
23
|
+
SERVER_LOG=""
|
|
24
|
+
|
|
23
25
|
# should_run checks if a test name matches the active filters.
|
|
24
26
|
# Returns 0 (true) if the test should run, 1 (false) otherwise.
|
|
25
27
|
should_run() {
|
|
@@ -77,27 +79,26 @@ RUST_CLIENT="$REPO_DIR/target/release/integration-client"
|
|
|
77
79
|
CPP_SERVER="$REPO_DIR/build/cpp-integration-server"
|
|
78
80
|
CPP_CLIENT="$REPO_DIR/build/cpp-integration-client"
|
|
79
81
|
|
|
80
|
-
# Start a server and capture its address.
|
|
81
|
-
# Sets SERVER_PID and SERVER_ADDR.
|
|
82
82
|
start_server() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"$@" > "$addr_file" 2>&1 &
|
|
83
|
+
SERVER_LOG=$(mktemp)
|
|
84
|
+
"$@" > "$SERVER_LOG" 2>&1 &
|
|
86
85
|
SERVER_PID=$!
|
|
87
86
|
# Wait for LISTENING output (up to 3 seconds).
|
|
88
87
|
local waited=0
|
|
89
88
|
while [ $waited -lt 30 ]; do
|
|
90
|
-
if grep -q 'LISTENING' "$
|
|
89
|
+
if grep -q 'LISTENING' "$SERVER_LOG" 2>/dev/null; then
|
|
91
90
|
break
|
|
92
91
|
fi
|
|
93
92
|
sleep 0.1
|
|
94
93
|
waited=$((waited + 1))
|
|
95
94
|
done
|
|
96
|
-
SERVER_ADDR=$(grep 'LISTENING' "$
|
|
97
|
-
rm -f "$addr_file"
|
|
95
|
+
SERVER_ADDR=$(grep 'LISTENING' "$SERVER_LOG" 2>/dev/null | awk '{print $2}')
|
|
98
96
|
if [ -z "$SERVER_ADDR" ]; then
|
|
99
97
|
echo "FAILED: server did not start"
|
|
100
98
|
kill $SERVER_PID 2>/dev/null || true
|
|
99
|
+
wait $SERVER_PID 2>/dev/null || true
|
|
100
|
+
rm -f "$SERVER_LOG"
|
|
101
|
+
SERVER_LOG=""
|
|
101
102
|
return 1
|
|
102
103
|
fi
|
|
103
104
|
return 0
|
|
@@ -110,6 +111,27 @@ stop_server() {
|
|
|
110
111
|
|
|
111
112
|
# run_pair <test_name> <server_args...> -- <client_args...>
|
|
112
113
|
# The client receives $SERVER_ADDR as its last argument.
|
|
114
|
+
# check_receipt_events verifies terminal markers after the receipt server
|
|
115
|
+
# naturally completes, so receipt synchronization is event/process-backed.
|
|
116
|
+
check_receipt_events() {
|
|
117
|
+
local expected="$1"
|
|
118
|
+
local event_log="$2"
|
|
119
|
+
awk -v expected="$expected" '
|
|
120
|
+
$0 == "SERVER_RECEIPT_TERMINAL " expected { terminal = NR; next }
|
|
121
|
+
$0 == "SERVER_RECEIPT_ACK_WRITE " expected { ack_write = NR; next }
|
|
122
|
+
$0 == "CLIENT_RECEIPT_RESOLVED " expected { client = NR; next }
|
|
123
|
+
END {
|
|
124
|
+
if (!terminal || !client) {
|
|
125
|
+
exit 1
|
|
126
|
+
}
|
|
127
|
+
if (expected == "committed" &&
|
|
128
|
+
(!ack_write || terminal >= ack_write || ack_write >= client)) {
|
|
129
|
+
exit 1
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
' "$event_log"
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
run_pair() {
|
|
114
136
|
local test_name="$1"
|
|
115
137
|
shift
|
|
@@ -134,30 +156,104 @@ run_pair() {
|
|
|
134
156
|
fi
|
|
135
157
|
done
|
|
136
158
|
|
|
159
|
+
local expected_terminal=""
|
|
160
|
+
case "$test_name" in
|
|
161
|
+
*"receipt commit") expected_terminal="committed" ;;
|
|
162
|
+
*"receipt abort") expected_terminal="canceled" ;;
|
|
163
|
+
*"receipt loss") expected_terminal="transportLost" ;;
|
|
164
|
+
*"receipt bare-close") expected_terminal="closed" ;;
|
|
165
|
+
esac
|
|
166
|
+
|
|
137
167
|
echo -n " ${test_name}... "
|
|
138
168
|
|
|
169
|
+
local event_fifo=""
|
|
170
|
+
local event_log=""
|
|
171
|
+
local event_reader=""
|
|
172
|
+
if [ -n "$expected_terminal" ]; then
|
|
173
|
+
event_fifo=$(mktemp)
|
|
174
|
+
rm -f "$event_fifo"
|
|
175
|
+
mkfifo "$event_fifo"
|
|
176
|
+
event_log=$(mktemp)
|
|
177
|
+
exec 3<>"$event_fifo"
|
|
178
|
+
(
|
|
179
|
+
exec 3>&-
|
|
180
|
+
cat "$event_fifo" > "$event_log"
|
|
181
|
+
) &
|
|
182
|
+
event_reader=$!
|
|
183
|
+
export RECEIPT_EVENT_FIFO="$event_fifo"
|
|
184
|
+
else
|
|
185
|
+
unset RECEIPT_EVENT_FIFO
|
|
186
|
+
fi
|
|
187
|
+
|
|
139
188
|
if ! start_server "${srv_args[@]}"; then
|
|
140
189
|
echo "FAILED (server start)"
|
|
141
190
|
FAILED=$((FAILED + 1))
|
|
142
191
|
ERRORS="${ERRORS}\n ${test_name} (server start failed)"
|
|
192
|
+
if [ -n "$event_reader" ]; then
|
|
193
|
+
exec 3>&-
|
|
194
|
+
kill "$event_reader" 2>/dev/null || true
|
|
195
|
+
wait "$event_reader" 2>/dev/null || true
|
|
196
|
+
rm -f "$event_fifo" "$event_log"
|
|
197
|
+
fi
|
|
198
|
+
unset RECEIPT_EVENT_FIFO
|
|
143
199
|
return
|
|
144
200
|
fi
|
|
145
201
|
|
|
146
202
|
local client_out
|
|
147
203
|
client_out=$(mktemp)
|
|
204
|
+
local client_ok=false
|
|
205
|
+
local server_ok=true
|
|
148
206
|
if "${cli_args[@]}" "$SERVER_ADDR" > "$client_out" 2>&1; then
|
|
207
|
+
client_ok=true
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
if [ -n "$event_reader" ]; then
|
|
211
|
+
if $client_ok; then
|
|
212
|
+
if ! wait "$SERVER_PID"; then
|
|
213
|
+
server_ok=false
|
|
214
|
+
fi
|
|
215
|
+
else
|
|
216
|
+
stop_server
|
|
217
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
218
|
+
server_ok=false
|
|
219
|
+
fi
|
|
220
|
+
exec 3>&-
|
|
221
|
+
wait "$event_reader" 2>/dev/null || true
|
|
222
|
+
else
|
|
223
|
+
stop_server
|
|
224
|
+
fi
|
|
225
|
+
unset RECEIPT_EVENT_FIFO
|
|
226
|
+
|
|
227
|
+
local terminal_ok=true
|
|
228
|
+
if [ -n "$expected_terminal" ] &&
|
|
229
|
+
! check_receipt_events "$expected_terminal" "$event_log"; then
|
|
230
|
+
terminal_ok=false
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
if $client_ok && $server_ok && $terminal_ok; then
|
|
149
234
|
echo "PASSED"
|
|
235
|
+
if [ -n "$expected_terminal" ]; then
|
|
236
|
+
echo " receipt events:"
|
|
237
|
+
sed 's/^/ /' "$event_log"
|
|
238
|
+
fi
|
|
150
239
|
PASSED=$((PASSED + 1))
|
|
151
240
|
else
|
|
152
241
|
echo "FAILED"
|
|
153
242
|
FAILED=$((FAILED + 1))
|
|
154
243
|
ERRORS="${ERRORS}\n ${test_name}"
|
|
155
|
-
|
|
156
|
-
|
|
244
|
+
if ! $client_ok; then
|
|
245
|
+
echo " client output:"
|
|
246
|
+
sed 's/^/ /' "$client_out"
|
|
247
|
+
fi
|
|
248
|
+
if ! $terminal_ok; then
|
|
249
|
+
echo " receipt events:"
|
|
250
|
+
sed 's/^/ /' "$event_log"
|
|
251
|
+
echo " server output:"
|
|
252
|
+
sed 's/^/ /' "$SERVER_LOG"
|
|
253
|
+
fi
|
|
157
254
|
fi
|
|
158
|
-
rm -f "$client_out"
|
|
159
|
-
|
|
160
|
-
stop_server
|
|
255
|
+
rm -f "$client_out" "$SERVER_LOG" "$event_fifo" "$event_log"
|
|
256
|
+
SERVER_LOG=""
|
|
161
257
|
}
|
|
162
258
|
|
|
163
259
|
echo ""
|
|
@@ -168,6 +264,10 @@ echo ""
|
|
|
168
264
|
run_pair "go-server + go-client" "$GO_SERVER" -- "$GO_CLIENT"
|
|
169
265
|
run_pair "go-server + rust-client" "$GO_SERVER" -- "$RUST_CLIENT"
|
|
170
266
|
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT"
|
|
267
|
+
run_pair "go-server + ts-client receipt commit" "$GO_SERVER" receipt commit -- node "$TS_CLIENT" receipt commit
|
|
268
|
+
run_pair "go-server + ts-client receipt abort" "$GO_SERVER" receipt abort -- node "$TS_CLIENT" receipt abort
|
|
269
|
+
run_pair "go-server + ts-client receipt loss" "$GO_SERVER" receipt loss -- node "$TS_CLIENT" receipt loss
|
|
270
|
+
run_pair "go-server + ts-client receipt bare-close" "$GO_SERVER" receipt bare-close -- node "$TS_CLIENT" receipt bare-close
|
|
171
271
|
run_pair "go-server + cpp-client" "$GO_SERVER" -- "$CPP_CLIENT"
|
|
172
272
|
|
|
173
273
|
# Rust server combinations
|
|
@@ -178,6 +278,10 @@ run_pair "rust-server + cpp-client" "$RUST_SERVER" -- "$CPP_CLIENT"
|
|
|
178
278
|
|
|
179
279
|
# TypeScript server combinations
|
|
180
280
|
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT"
|
|
281
|
+
run_pair "ts-server + go-client receipt commit" node "$TS_SERVER" receipt commit -- "$GO_CLIENT" receipt commit
|
|
282
|
+
run_pair "ts-server + go-client receipt abort" node "$TS_SERVER" receipt abort -- "$GO_CLIENT" receipt abort
|
|
283
|
+
run_pair "ts-server + go-client receipt loss" node "$TS_SERVER" receipt loss -- "$GO_CLIENT" receipt loss
|
|
284
|
+
run_pair "ts-server + go-client receipt bare-close" node "$TS_SERVER" receipt bare-close -- "$GO_CLIENT" receipt bare-close
|
|
181
285
|
run_pair "ts-server + rust-client" node "$TS_SERVER" -- "$RUST_CLIENT"
|
|
182
286
|
run_pair "ts-server + ts-client" node "$TS_SERVER" -- node "$TS_CLIENT"
|
|
183
287
|
run_pair "ts-server + cpp-client" node "$TS_SERVER" -- "$CPP_CLIENT"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import net from 'net'
|
|
2
|
+
import { closeSync, openSync, writeSync } from 'node:fs'
|
|
2
3
|
import { pipe } from 'it-pipe'
|
|
3
4
|
import { pushable } from 'it-pushable'
|
|
4
5
|
import { Client } from '../../srpc/client.js'
|
|
@@ -7,9 +8,22 @@ import {
|
|
|
7
8
|
prependLengthPrefixTransform,
|
|
8
9
|
} from '../../srpc/packet.js'
|
|
9
10
|
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
|
|
10
|
-
import { runClientTest } from '../../echo/
|
|
11
|
+
import { EchoMsg, runClientTest } from '../../echo/index.js'
|
|
11
12
|
import type { OpenStreamFunc, PacketStream } from '../../srpc/stream.js'
|
|
12
13
|
import type { Source } from 'it-stream-types'
|
|
14
|
+
function emitReceiptEvent(line: string): void {
|
|
15
|
+
console.log(line)
|
|
16
|
+
const fifo = process.env.RECEIPT_EVENT_FIFO
|
|
17
|
+
if (!fifo) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const fd = openSync(fifo, 'w')
|
|
21
|
+
try {
|
|
22
|
+
writeSync(fd, `${line}\n`)
|
|
23
|
+
} finally {
|
|
24
|
+
closeSync(fd)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
|
|
14
28
|
// tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
|
|
15
29
|
function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
@@ -32,8 +46,7 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
|
32
46
|
source: socketSource(),
|
|
33
47
|
sink: async (source: Source<Uint8Array>): Promise<void> => {
|
|
34
48
|
for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
|
|
35
|
-
const data =
|
|
36
|
-
chunk instanceof Uint8Array ? chunk : (chunk as any).subarray()
|
|
49
|
+
const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
|
|
37
50
|
await new Promise<void>((resolve, reject) => {
|
|
38
51
|
socket.write(data, (err) => {
|
|
39
52
|
if (err) reject(err)
|
|
@@ -47,30 +60,98 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
async function main() {
|
|
50
|
-
const
|
|
63
|
+
const receiptMode = process.argv[2] === 'receipt'
|
|
64
|
+
const receiptCase = receiptMode ? process.argv[3] : undefined
|
|
65
|
+
const addr = receiptMode ? process.argv[4] : process.argv[2]
|
|
51
66
|
if (!addr) {
|
|
52
|
-
console.error('usage: ts-client <host:port>')
|
|
67
|
+
console.error('usage: ts-client [receipt <case>] <host:port>')
|
|
53
68
|
process.exit(1)
|
|
54
69
|
}
|
|
70
|
+
if (
|
|
71
|
+
receiptMode &&
|
|
72
|
+
!['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(`unknown receipt case: ${receiptCase}`)
|
|
75
|
+
}
|
|
55
76
|
|
|
56
77
|
const [host, portStr] = addr.split(':')
|
|
57
78
|
const port = parseInt(portStr, 10)
|
|
79
|
+
let activeSocket: net.Socket | undefined
|
|
58
80
|
|
|
59
81
|
const openStream: OpenStreamFunc = async (): Promise<PacketStream> => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
socket.on('error', reject)
|
|
82
|
+
const { promise, resolve, reject } = Promise.withResolvers<PacketStream>()
|
|
83
|
+
const socket = net.connect(port, host, () => {
|
|
84
|
+
activeSocket = socket
|
|
85
|
+
resolve(tcpSocketToPacketStream(socket))
|
|
65
86
|
})
|
|
87
|
+
socket.on('error', reject)
|
|
88
|
+
return promise
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
const client = new Client(openStream)
|
|
69
92
|
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
if (receiptMode) {
|
|
94
|
+
console.log(`Running held receipt test via TCP (${receiptCase})...`)
|
|
95
|
+
const request = EchoMsg.create({ body: 'held receipt' })
|
|
96
|
+
const held = await client.requestWithReceipt(
|
|
97
|
+
'echo.Echoer',
|
|
98
|
+
'Echo',
|
|
99
|
+
EchoMsg.toBinary(request),
|
|
100
|
+
)
|
|
101
|
+
const result = EchoMsg.fromBinary(held.response)
|
|
102
|
+
if (result.body !== request.body) {
|
|
103
|
+
throw new Error(`expected ${request.body}, got ${result.body}`)
|
|
104
|
+
}
|
|
105
|
+
switch (receiptCase) {
|
|
106
|
+
case 'commit':
|
|
107
|
+
await held.receipt.commit()
|
|
108
|
+
break
|
|
109
|
+
case 'abort':
|
|
110
|
+
await held.receipt.abort()
|
|
111
|
+
break
|
|
112
|
+
case 'loss':
|
|
113
|
+
activeSocket?.resetAndDestroy()
|
|
114
|
+
await expectReceiptFailure(held.receipt)
|
|
115
|
+
break
|
|
116
|
+
case 'bare-close':
|
|
117
|
+
activeSocket?.end()
|
|
118
|
+
await expectReceiptFailure(held.receipt)
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
emitReceiptEvent(
|
|
122
|
+
`CLIENT_RECEIPT_RESOLVED ${receiptTerminalName(receiptCase ?? '')}`,
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
console.log('Running client test via TCP...')
|
|
126
|
+
await runClientTest(client)
|
|
127
|
+
}
|
|
72
128
|
console.log('All tests passed.')
|
|
73
|
-
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function receiptTerminalName(receiptCase: string): string {
|
|
132
|
+
switch (receiptCase) {
|
|
133
|
+
case 'commit':
|
|
134
|
+
return 'committed'
|
|
135
|
+
case 'abort':
|
|
136
|
+
return 'canceled'
|
|
137
|
+
case 'loss':
|
|
138
|
+
return 'transportLost'
|
|
139
|
+
case 'bare-close':
|
|
140
|
+
return 'closed'
|
|
141
|
+
default:
|
|
142
|
+
return 'unknown'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function expectReceiptFailure(receipt: {
|
|
147
|
+
commit(): Promise<void>
|
|
148
|
+
}): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
await receipt.commit()
|
|
151
|
+
} catch {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
throw new Error('receipt commit unexpectedly succeeded')
|
|
74
155
|
}
|
|
75
156
|
|
|
76
157
|
process.on('unhandledRejection', (ev) => {
|