starpc 0.49.17 → 0.49.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.49.17",
3
+ "version": "0.49.18",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -34,6 +34,10 @@ type commonRPC struct {
34
34
  // localCompleting is set while the local handler is publishing its terminal
35
35
  // packet and closing the writer.
36
36
  localCompleting bool
37
+ // localActive is set while the local handler goroutine may still be inside
38
+ // user code. Resource owners use Wait as a lifetime barrier, so cancellation
39
+ // must not make Wait return while a handler can still touch mux-owned state.
40
+ localActive bool
37
41
  // localDone is set after the local handler has completed normally.
38
42
  localDone bool
39
43
  // dataQueue contains incoming data packets.
@@ -73,6 +77,21 @@ func (c *commonRPC) Wait(ctx context.Context) error {
73
77
  locked := c.bcast.Lock()
74
78
  err = c.remoteErr
75
79
  rpcCanceled = c.ctx.Err() != nil
80
+ // A canceled stream tells the handler to stop, but it is not proof that
81
+ // the handler has returned. Keep waiting while localActive is true so a
82
+ // caller that releases resources after Wait cannot race in-flight user
83
+ // code still running on the canceled Stream context.
84
+ if c.localActive {
85
+ waitCh = locked.WaitCh()
86
+ locked.Unlock()
87
+
88
+ select {
89
+ case <-ctx.Done():
90
+ return context.Canceled
91
+ case <-waitCh:
92
+ continue
93
+ }
94
+ }
76
95
  localDone = c.localDone
77
96
  if err == nil && !rpcCanceled && !localDone {
78
97
  waitCh = locked.WaitCh()
@@ -276,6 +295,7 @@ func (c *commonRPC) finishLocalCompletion() {
276
295
  }
277
296
  locked = c.bcast.Lock()
278
297
  c.localCompleting = false
298
+ c.localActive = false
279
299
  c.localDone = true
280
300
  locked.Broadcast()
281
301
  locked.Unlock()
@@ -232,6 +232,58 @@ func TestServerRPCRemoteCloseAfterLocalCompletionDoesNotCancelStreamContext(t *t
232
232
  }
233
233
  }
234
234
 
235
+ func TestServerRPCWaitDoesNotReturnUntilCanceledInvokeExits(t *testing.T) {
236
+ writer := newPacketRecordingWriter()
237
+ invoked := make(chan struct{})
238
+ ctxCanceled := make(chan struct{})
239
+ releaseInvoke := make(chan struct{})
240
+ rpc := NewServerRPC(context.Background(), InvokerFunc(func(serviceID, methodID string, strm Stream) (bool, error) {
241
+ close(invoked)
242
+ <-strm.Context().Done()
243
+ close(ctxCanceled)
244
+ <-releaseInvoke
245
+ return true, nil
246
+ }), writer)
247
+
248
+ if err := rpc.HandleCallStart(NewCallStartPacket("service", "method", nil, false).GetCallStart()); err != nil {
249
+ t.Fatalf("handle call start: %v", err)
250
+ }
251
+
252
+ select {
253
+ case <-invoked:
254
+ case <-time.After(time.Second):
255
+ t.Fatal("invoker did not start")
256
+ }
257
+
258
+ done := make(chan error, 1)
259
+ go func() {
260
+ done <- rpc.Wait(context.Background())
261
+ }()
262
+
263
+ rpc.cancelContext()
264
+ select {
265
+ case <-ctxCanceled:
266
+ case <-time.After(time.Second):
267
+ t.Fatal("invoke context was not canceled")
268
+ }
269
+
270
+ select {
271
+ case err := <-done:
272
+ t.Fatalf("wait returned before canceled invoke exited: %v", err)
273
+ default:
274
+ }
275
+
276
+ close(releaseInvoke)
277
+ select {
278
+ case err := <-done:
279
+ if err != nil {
280
+ t.Fatalf("wait after invoke exit: %v", err)
281
+ }
282
+ case <-time.After(time.Second):
283
+ t.Fatal("wait did not return after invoke exited")
284
+ }
285
+ }
286
+
235
287
  func TestServerRPCRemoteCloseDuringLocalCompletionDoesNotCancelStreamContext(t *testing.T) {
236
288
  writer := newBlockingPacketWriter()
237
289
  streamCtxCh := make(chan context.Context, 1)
@@ -0,0 +1,7 @@
1
+ //go:build !goscript
2
+
3
+ package srpc
4
+
5
+ func startServerRPCInvoke(fn func()) {
6
+ go fn()
7
+ }
@@ -0,0 +1,12 @@
1
+ //go:build goscript
2
+
3
+ package srpc
4
+
5
+ import "time"
6
+
7
+ func startServerRPCInvoke(fn func()) {
8
+ if fn == nil {
9
+ return
10
+ }
11
+ time.AfterFunc(0, fn)
12
+ }
@@ -0,0 +1,27 @@
1
+ //go:build goscript
2
+
3
+ package srpc
4
+
5
+ import (
6
+ "testing"
7
+ "time"
8
+ )
9
+
10
+ func TestStartServerRPCInvokeDefersGoscriptWork(t *testing.T) {
11
+ ran := make(chan struct{}, 1)
12
+ startServerRPCInvoke(func() {
13
+ ran <- struct{}{}
14
+ })
15
+
16
+ select {
17
+ case <-ran:
18
+ t.Fatal("server rpc invoke ran inline")
19
+ default:
20
+ }
21
+
22
+ select {
23
+ case <-ran:
24
+ case <-time.After(time.Second):
25
+ t.Fatal("server rpc invoke did not run")
26
+ }
27
+ }
@@ -80,9 +80,16 @@ func (r *ServerRPC) HandleCallStart(pkt *CallStart) error {
80
80
  r.dataQueue = append(r.dataQueue, data)
81
81
  }
82
82
 
83
+ // Wait is used as a resource lifetime barrier by rpcstream components.
84
+ // Mark the method active before scheduling it so cancellation cannot make
85
+ // Wait return and release a mux while invokeRPC is still running user code.
86
+ r.localActive = true
87
+
83
88
  // invoke the rpc
84
89
  locked.Broadcast()
85
- go r.invokeRPC(service, method)
90
+ startServerRPCInvoke(func() {
91
+ r.invokeRPC(service, method)
92
+ })
86
93
  locked.Unlock()
87
94
 
88
95
  return err