starpc 0.49.9 → 0.49.10

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/go.mod CHANGED
@@ -5,7 +5,7 @@ go 1.25.0
5
5
  require (
6
6
  github.com/aperturerobotics/common v0.33.0 // latest
7
7
  github.com/aperturerobotics/protobuf-go-lite v0.13.0 // latest
8
- github.com/aperturerobotics/util v1.34.3 // latest
8
+ github.com/aperturerobotics/util v1.34.5-0.20260516103104-cbfc6d6a0589 // latest
9
9
  )
10
10
 
11
11
  require (
@@ -29,5 +29,5 @@ require (
29
29
  github.com/tetratelabs/wazero v1.11.0 // indirect
30
30
  github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
31
31
  golang.org/x/mod v0.35.0 // indirect
32
- golang.org/x/sys v0.43.0 // indirect
32
+ golang.org/x/sys v0.44.0 // indirect
33
33
  )
package/go.sum CHANGED
@@ -16,8 +16,10 @@ github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4 h1:4Dy3B
16
16
  github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4/go.mod h1:tMgO7y6SJo/d9ZcvrpNqIQtdYT9de+QmYaHOZ4KnhOg=
17
17
  github.com/aperturerobotics/protobuf-go-lite v0.13.0 h1:jEvCJhHaJEikDY/va2AUnS0DOb/0n82aISLAqxSh4Sk=
18
18
  github.com/aperturerobotics/protobuf-go-lite v0.13.0/go.mod h1:lGH3s5ArCTXKI4wJdlNpaybUtwSjfAG0vdWjxOfMcF8=
19
- github.com/aperturerobotics/util v1.34.3 h1:9lFYJovGlAHYX66aWKVzfoVzYox13P054d/Dy8T/GRg=
20
- github.com/aperturerobotics/util v1.34.3/go.mod h1:xtE2hwpgKGaW0TLx01+9xLsG+rYvhygQ+JCuQeAbvME=
19
+ github.com/aperturerobotics/util v1.34.5-0.20260515183346-68f9eac1d69f h1:xISFLs00h441uZcMVxhZbLIZsMRcjOM5Yont18i7WjA=
20
+ github.com/aperturerobotics/util v1.34.5-0.20260515183346-68f9eac1d69f/go.mod h1:mDe7WnncVuV7yjeeVSsagyfrw4xfncu7d+f0+d70niY=
21
+ github.com/aperturerobotics/util v1.34.5-0.20260516103104-cbfc6d6a0589 h1:8B9O13He1sz8Spr2pc+RL3hBzAMveLgUCXT7BpAfvEY=
22
+ github.com/aperturerobotics/util v1.34.5-0.20260516103104-cbfc6d6a0589/go.mod h1:mDe7WnncVuV7yjeeVSsagyfrw4xfncu7d+f0+d70niY=
21
23
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
24
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
25
  github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -40,8 +42,8 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
40
42
  github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
41
43
  golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
42
44
  golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
43
- golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
44
- golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
45
+ golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
46
+ golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
45
47
  google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
46
48
  google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
47
49
  gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.49.9",
3
+ "version": "0.49.10",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -146,15 +146,22 @@ func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err er
146
146
 
147
147
  // HandleStreamClose handles the incoming stream closing w/ optional error.
148
148
  func (c *commonRPC) HandleStreamClose(closeErr error) {
149
+ var writer PacketWriter
149
150
  c.bcast.HoldLock(func(broadcast func(), getWaitCh func() <-chan struct{}) {
151
+ if c.dataClosed {
152
+ return
153
+ }
150
154
  if closeErr != nil && c.remoteErr == nil {
151
155
  c.remoteErr = closeErr
152
156
  }
153
157
  c.dataClosed = true
154
158
  c.ctxCancel()
155
- _ = c.writer.Close()
159
+ writer = c.writer
156
160
  broadcast()
157
161
  })
162
+ if writer != nil {
163
+ _ = writer.Close()
164
+ }
158
165
  }
159
166
 
160
167
  // HandleCallCancel handles the call cancel packet.
@@ -210,6 +217,9 @@ func (c *commonRPC) WriteCallCancel() error {
210
217
 
211
218
  // closeLocked releases resources held by the RPC.
212
219
  func (c *commonRPC) closeLocked(broadcast func()) {
220
+ if c.dataClosed {
221
+ return
222
+ }
213
223
  c.dataClosed = true
214
224
  c.localCompleted.Store(true)
215
225
  if c.remoteErr == nil {
@@ -0,0 +1,65 @@
1
+ package srpc
2
+
3
+ import (
4
+ "context"
5
+ "io"
6
+ "sync/atomic"
7
+ "testing"
8
+ )
9
+
10
+ type closeCountingPacketWriter struct {
11
+ closed atomic.Int32
12
+ }
13
+
14
+ type closeCallbackPacketWriter struct {
15
+ closeFn func()
16
+ }
17
+
18
+ func (w *closeCountingPacketWriter) WritePacket(*Packet) error {
19
+ return nil
20
+ }
21
+
22
+ func (w *closeCountingPacketWriter) Close() error {
23
+ w.closed.Add(1)
24
+ return nil
25
+ }
26
+
27
+ func (w *closeCallbackPacketWriter) WritePacket(*Packet) error {
28
+ return nil
29
+ }
30
+
31
+ func (w *closeCallbackPacketWriter) Close() error {
32
+ if w.closeFn != nil {
33
+ w.closeFn()
34
+ }
35
+ return nil
36
+ }
37
+
38
+ func TestCommonRPCHandleStreamCloseIdempotent(t *testing.T) {
39
+ writer := &closeCountingPacketWriter{}
40
+ rpc := NewServerRPC(context.Background(), InvokerFunc(nil), writer)
41
+
42
+ rpc.HandleStreamClose(io.EOF)
43
+ rpc.HandleStreamClose(context.Canceled)
44
+
45
+ if got := writer.closed.Load(); got != 1 {
46
+ t.Fatalf("expected writer closed once, got %d", got)
47
+ }
48
+ }
49
+
50
+ func TestCommonRPCHandleStreamCloseClosesWriterOutsideBroadcastLock(t *testing.T) {
51
+ var rpc *ServerRPC
52
+ writerClosedOutsideLock := false
53
+ writer := &closeCallbackPacketWriter{
54
+ closeFn: func() {
55
+ writerClosedOutsideLock = rpc.bcast.TryHoldLock(func(func(), func() <-chan struct{}) {})
56
+ },
57
+ }
58
+ rpc = NewServerRPC(context.Background(), InvokerFunc(nil), writer)
59
+
60
+ rpc.HandleStreamClose(io.EOF)
61
+
62
+ if !writerClosedOutsideLock {
63
+ t.Fatal("expected writer close outside broadcast lock")
64
+ }
65
+ }
package/srpc/packet-rw.go CHANGED
@@ -10,8 +10,31 @@ import (
10
10
  "github.com/pkg/errors"
11
11
  )
12
12
 
13
- // maxMessageSize is the max message size in bytes
14
- const maxMessageSize = 10_000_000
13
+ const (
14
+ // maxMessageSize is the max message size in bytes.
15
+ maxMessageSize = 10_000_000
16
+ // readBufferSize is the packet read scratch buffer size.
17
+ readBufferSize = 2048
18
+ // pooledWriteBufferMaxSize is the largest outbound frame buffer to pool.
19
+ pooledWriteBufferMaxSize = 64 * 1024
20
+ )
21
+
22
+ var (
23
+ readBufferPool = sync.Pool{
24
+ New: func() any {
25
+ return new([readBufferSize]byte)
26
+ },
27
+ }
28
+ writeBufferPool = sync.Pool{
29
+ New: func() any {
30
+ return new(writeBuffer)
31
+ },
32
+ }
33
+ )
34
+
35
+ type writeBuffer struct {
36
+ data []byte
37
+ }
15
38
 
16
39
  // PacketReadWriter reads and writes packets from a io.ReadWriter.
17
40
  // Uses a LittleEndian uint32 length prefix.
@@ -46,7 +69,9 @@ func (r *PacketReadWriter) WritePacket(p *Packet) error {
46
69
  return errors.Errorf("message size %v greater than maximum %v", msgSize, maxMessageSize)
47
70
  }
48
71
 
49
- data := make([]byte, 4+msgSize)
72
+ writeBuf := getWriteBuffer(4 + msgSize)
73
+ defer putWriteBuffer(writeBuf)
74
+ data := writeBuf.data
50
75
  binary.LittleEndian.PutUint32(data, uint32(msgSize)) //nolint:gosec
51
76
 
52
77
  _, err := p.MarshalToSizedBufferVT(data[4:])
@@ -56,10 +81,13 @@ func (r *PacketReadWriter) WritePacket(p *Packet) error {
56
81
 
57
82
  var written, n int
58
83
  for written < len(data) {
59
- n, err = r.rw.Write(data)
84
+ n, err = r.rw.Write(data[written:])
60
85
  if err != nil {
61
86
  return err
62
87
  }
88
+ if n == 0 {
89
+ return io.ErrShortWrite
90
+ }
63
91
  written += n
64
92
  }
65
93
 
@@ -81,7 +109,9 @@ func (r *PacketReadWriter) ReadPump(cb PacketDataHandler, closed CloseHandler) {
81
109
  // Does not handle closing the stream, use ReadPump instead.
82
110
  func (r *PacketReadWriter) ReadToHandler(cb PacketDataHandler) error {
83
111
  var currLen uint32
84
- buf := make([]byte, 2048)
112
+ bufPtr := readBufferPool.Get().(*[readBufferSize]byte)
113
+ defer readBufferPool.Put(bufPtr)
114
+ buf := bufPtr[:]
85
115
  isOpen := true
86
116
 
87
117
  for isOpen {
@@ -150,5 +180,25 @@ func (r *PacketReadWriter) readLengthPrefix(b []byte) uint32 {
150
180
  return binary.LittleEndian.Uint32(b)
151
181
  }
152
182
 
183
+ func getWriteBuffer(size int) *writeBuffer {
184
+ if size > pooledWriteBufferMaxSize {
185
+ return &writeBuffer{data: make([]byte, size)}
186
+ }
187
+ buf := writeBufferPool.Get().(*writeBuffer)
188
+ if cap(buf.data) < size {
189
+ buf.data = make([]byte, size)
190
+ }
191
+ buf.data = buf.data[:size]
192
+ return buf
193
+ }
194
+
195
+ func putWriteBuffer(buf *writeBuffer) {
196
+ if cap(buf.data) <= pooledWriteBufferMaxSize {
197
+ clear(buf.data)
198
+ buf.data = buf.data[:0]
199
+ writeBufferPool.Put(buf)
200
+ }
201
+ }
202
+
153
203
  // _ is a type assertion
154
204
  var _ PacketWriter = (*PacketReadWriter)(nil)
@@ -0,0 +1,68 @@
1
+ package srpc
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/binary"
6
+ "io"
7
+ "testing"
8
+ )
9
+
10
+ type chunkedReadWriteCloser struct {
11
+ bytes.Buffer
12
+ maxWrite int
13
+ }
14
+
15
+ func (c *chunkedReadWriteCloser) Read([]byte) (int, error) {
16
+ return 0, io.EOF
17
+ }
18
+
19
+ func (c *chunkedReadWriteCloser) Write(p []byte) (int, error) {
20
+ if len(p) > c.maxWrite {
21
+ p = p[:c.maxWrite]
22
+ }
23
+ return c.Buffer.Write(p)
24
+ }
25
+
26
+ func (c *chunkedReadWriteCloser) Close() error {
27
+ return nil
28
+ }
29
+
30
+ func TestPacketReadWriterWritePacketHandlesShortWrites(t *testing.T) {
31
+ pkt := NewCallDataPacket([]byte("packet payload"), false, true, nil)
32
+ size := pkt.SizeVT()
33
+ want := make([]byte, 4+size)
34
+ binary.LittleEndian.PutUint32(want, uint32(size)) //nolint:gosec
35
+ if _, err := pkt.MarshalToSizedBufferVT(want[4:]); err != nil {
36
+ t.Fatal(err)
37
+ }
38
+
39
+ rwc := &chunkedReadWriteCloser{maxWrite: 3}
40
+ if err := NewPacketReadWriter(rwc).WritePacket(pkt); err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ if got := rwc.Bytes(); !bytes.Equal(got, want) {
44
+ t.Fatalf("written packet mismatch:\ngot %x\nwant %x", got, want)
45
+ }
46
+ }
47
+
48
+ func TestPacketUnmarshalCopiesByteFields(t *testing.T) {
49
+ want := []byte("stable data")
50
+ srcPkt := NewCallDataPacket(want, false, true, nil)
51
+ data, err := srcPkt.MarshalVT()
52
+ if err != nil {
53
+ t.Fatal(err)
54
+ }
55
+
56
+ var pkt Packet
57
+ if err := pkt.UnmarshalVT(data); err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ for i := range data {
61
+ data[i] = 0xff
62
+ }
63
+
64
+ got := pkt.GetCallData().GetData()
65
+ if !bytes.Equal(got, want) {
66
+ t.Fatalf("unmarshal retained source bytes: got %q want %q", got, want)
67
+ }
68
+ }