starpc 0.49.17 → 0.49.20
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 +84 -4
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- 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 +105 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +374 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -1
- package/dist/srpc/common-rpc.js +86 -6
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +2 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +1 -1
- package/dist/srpc/rpcproto.pb.js +7 -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/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/main.go +146 -3
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +94 -6
- 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 +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +438 -0
- package/srpc/call-receipt.ts +130 -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 +134 -29
- package/srpc/common-rpc.ts +98 -8
- package/srpc/common-rpc_test.go +52 -0
- 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 +8 -3
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/rpcproto.pb.go +1 -1
- package/srpc/rpcproto.pb.ts +28 -27
- package/srpc/server-invocation.go +58 -0
- package/srpc/server-invocation.ts +82 -0
- package/srpc/server-rpc-invoke.go +7 -0
- package/srpc/server-rpc-invoke_goscript.go +12 -0
- package/srpc/server-rpc-invoke_goscript_test.go +27 -0
- package/srpc/server-rpc.go +9 -2
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream.go +20 -0
package/srpc/common-rpc.go
CHANGED
|
@@ -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.
|
|
@@ -44,6 +48,14 @@ type commonRPC struct {
|
|
|
44
48
|
dataClosed bool
|
|
45
49
|
// remoteErr is an error set by the remote.
|
|
46
50
|
remoteErr error
|
|
51
|
+
// remoteCanceled distinguishes a received CallCancel from transport loss.
|
|
52
|
+
remoteCanceled bool
|
|
53
|
+
// remoteCompleted is set only by an explicit remote CallData completion.
|
|
54
|
+
remoteCompleted bool
|
|
55
|
+
// remoteTerminal is the first valid remote terminal.
|
|
56
|
+
remoteTerminal TerminalKind
|
|
57
|
+
// remoteTerminalSet records whether remoteTerminal is valid.
|
|
58
|
+
remoteTerminalSet bool
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
// initCommonRPC initializes the commonRPC.
|
|
@@ -73,6 +85,21 @@ func (c *commonRPC) Wait(ctx context.Context) error {
|
|
|
73
85
|
locked := c.bcast.Lock()
|
|
74
86
|
err = c.remoteErr
|
|
75
87
|
rpcCanceled = c.ctx.Err() != nil
|
|
88
|
+
// A canceled stream tells the handler to stop, but it is not proof that
|
|
89
|
+
// the handler has returned. Keep waiting while localActive is true so a
|
|
90
|
+
// caller that releases resources after Wait cannot race in-flight user
|
|
91
|
+
// code still running on the canceled Stream context.
|
|
92
|
+
if c.localActive {
|
|
93
|
+
waitCh = locked.WaitCh()
|
|
94
|
+
locked.Unlock()
|
|
95
|
+
|
|
96
|
+
select {
|
|
97
|
+
case <-ctx.Done():
|
|
98
|
+
return context.Canceled
|
|
99
|
+
case <-waitCh:
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
}
|
|
76
103
|
localDone = c.localDone
|
|
77
104
|
if err == nil && !rpcCanceled && !localDone {
|
|
78
105
|
waitCh = locked.WaitCh()
|
|
@@ -97,6 +124,41 @@ func (c *commonRPC) Wait(ctx context.Context) error {
|
|
|
97
124
|
}
|
|
98
125
|
}
|
|
99
126
|
|
|
127
|
+
// WaitTerminal waits for and classifies the remote terminal of a held unary
|
|
128
|
+
// invocation.
|
|
129
|
+
func (c *commonRPC) WaitTerminal(ownerCtx context.Context) (TerminalKind, error) {
|
|
130
|
+
var ownerDone bool
|
|
131
|
+
for {
|
|
132
|
+
locked := c.bcast.Lock()
|
|
133
|
+
if c.remoteTerminalSet {
|
|
134
|
+
terminal := c.remoteTerminal
|
|
135
|
+
locked.Unlock()
|
|
136
|
+
return terminal, nil
|
|
137
|
+
}
|
|
138
|
+
if ownerDone {
|
|
139
|
+
err := ownerCtx.Err()
|
|
140
|
+
locked.Unlock()
|
|
141
|
+
return TerminalAbandoned, err
|
|
142
|
+
}
|
|
143
|
+
waitCh := locked.WaitCh()
|
|
144
|
+
locked.Unlock()
|
|
145
|
+
|
|
146
|
+
select {
|
|
147
|
+
case <-ownerCtx.Done():
|
|
148
|
+
ownerDone = true
|
|
149
|
+
case <-waitCh:
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// receiptTerminalKind returns the first valid remote terminal.
|
|
155
|
+
func (c *commonRPC) receiptTerminalKind() (TerminalKind, bool) {
|
|
156
|
+
locked := c.bcast.Lock()
|
|
157
|
+
terminal, ok := c.remoteTerminal, c.remoteTerminalSet
|
|
158
|
+
locked.Unlock()
|
|
159
|
+
return terminal, ok
|
|
160
|
+
}
|
|
161
|
+
|
|
100
162
|
// ReadOne reads a single message and returns.
|
|
101
163
|
//
|
|
102
164
|
// returns io.EOF if the stream ended without a packet.
|
|
@@ -145,43 +207,26 @@ func (c *commonRPC) ReadOne() ([]byte, error) {
|
|
|
145
207
|
|
|
146
208
|
// WriteCallData writes a call data packet.
|
|
147
209
|
func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err error) error {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
210
|
+
if complete || err != nil {
|
|
211
|
+
if c.localCompleted.Swap(true) {
|
|
212
|
+
// Repeated completion is an idempotent no-op.
|
|
213
|
+
if complete && len(data) == 0 && !dataIsZero {
|
|
214
|
+
return nil
|
|
215
|
+
}
|
|
216
|
+
return ErrCompleted
|
|
153
217
|
}
|
|
154
|
-
|
|
218
|
+
} else if c.localCompleted.Load() {
|
|
155
219
|
return ErrCompleted
|
|
156
220
|
}
|
|
157
221
|
|
|
158
|
-
// Mark as completed if this call completes the RPC
|
|
159
|
-
if complete || err != nil {
|
|
160
|
-
c.localCompleted.Store(true)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
222
|
outPkt := NewCallDataPacket(data, len(data) == 0 && dataIsZero, complete, err)
|
|
164
223
|
return c.writer.WritePacket(outPkt)
|
|
165
224
|
}
|
|
166
225
|
|
|
167
226
|
// HandleStreamClose handles the incoming stream closing w/ optional error.
|
|
168
227
|
func (c *commonRPC) HandleStreamClose(closeErr error) {
|
|
169
|
-
var writer PacketWriter
|
|
170
228
|
locked := c.bcast.Lock()
|
|
171
|
-
|
|
172
|
-
locked.Unlock()
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
|
|
176
|
-
if closeErr != nil && c.remoteErr == nil {
|
|
177
|
-
c.remoteErr = closeErr
|
|
178
|
-
}
|
|
179
|
-
c.dataClosed = true
|
|
180
|
-
if !normalRemoteCloseAfterLocalComplete {
|
|
181
|
-
c.cancelContext()
|
|
182
|
-
writer = c.closeWriterLocked()
|
|
183
|
-
}
|
|
184
|
-
locked.Broadcast()
|
|
229
|
+
writer := c.handleStreamCloseLocked(&locked, closeErr)
|
|
185
230
|
locked.Unlock()
|
|
186
231
|
if writer != nil {
|
|
187
232
|
_ = writer.Close()
|
|
@@ -190,7 +235,15 @@ func (c *commonRPC) HandleStreamClose(closeErr error) {
|
|
|
190
235
|
|
|
191
236
|
// HandleCallCancel handles the call cancel packet.
|
|
192
237
|
func (c *commonRPC) HandleCallCancel() error {
|
|
193
|
-
c.
|
|
238
|
+
locked := c.bcast.Lock()
|
|
239
|
+
if (!c.dataClosed || !c.writerClosed) && !c.remoteTerminalSet {
|
|
240
|
+
c.remoteCanceled = true
|
|
241
|
+
}
|
|
242
|
+
writer := c.handleStreamCloseLocked(&locked, context.Canceled)
|
|
243
|
+
locked.Unlock()
|
|
244
|
+
if writer != nil {
|
|
245
|
+
_ = writer.Close()
|
|
246
|
+
}
|
|
194
247
|
return nil
|
|
195
248
|
}
|
|
196
249
|
|
|
@@ -212,14 +265,24 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
|
|
|
212
265
|
c.dataQueue = append(c.dataQueue, data)
|
|
213
266
|
}
|
|
214
267
|
|
|
268
|
+
pktErr := pkt.GetError()
|
|
215
269
|
complete := pkt.GetComplete()
|
|
216
|
-
if
|
|
270
|
+
if len(pktErr) != 0 {
|
|
217
271
|
complete = true
|
|
218
|
-
c.remoteErr
|
|
272
|
+
if c.remoteErr == nil {
|
|
273
|
+
c.remoteErr = errors.New(pktErr)
|
|
274
|
+
}
|
|
219
275
|
}
|
|
220
276
|
|
|
221
277
|
if complete {
|
|
222
278
|
c.dataClosed = true
|
|
279
|
+
if len(pktErr) == 0 {
|
|
280
|
+
if c.recordRemoteTerminalLocked(TerminalCommitted) {
|
|
281
|
+
c.remoteCompleted = true
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
c.recordRemoteTerminalLocked(TerminalLost)
|
|
285
|
+
}
|
|
223
286
|
}
|
|
224
287
|
|
|
225
288
|
locked.Broadcast()
|
|
@@ -228,6 +291,47 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
|
|
|
228
291
|
return err
|
|
229
292
|
}
|
|
230
293
|
|
|
294
|
+
func (c *commonRPC) recordRemoteTerminalLocked(kind TerminalKind) bool {
|
|
295
|
+
if c.remoteTerminalSet {
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
c.remoteTerminal = kind
|
|
299
|
+
c.remoteTerminalSet = true
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func (c *commonRPC) handleStreamCloseLocked(
|
|
304
|
+
locked *broadcast.Locked,
|
|
305
|
+
closeErr error,
|
|
306
|
+
) PacketWriter {
|
|
307
|
+
if c.dataClosed && c.writerClosed {
|
|
308
|
+
return nil
|
|
309
|
+
}
|
|
310
|
+
normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
|
|
311
|
+
if closeErr != nil && c.remoteErr == nil {
|
|
312
|
+
c.remoteErr = closeErr
|
|
313
|
+
}
|
|
314
|
+
if !normalRemoteCloseAfterLocalComplete && !c.remoteTerminalSet {
|
|
315
|
+
terminal := TerminalClosed
|
|
316
|
+
if closeErr != nil {
|
|
317
|
+
terminal = TerminalLost
|
|
318
|
+
if c.remoteCanceled {
|
|
319
|
+
terminal = TerminalCanceled
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
c.recordRemoteTerminalLocked(terminal)
|
|
323
|
+
}
|
|
324
|
+
c.dataClosed = true
|
|
325
|
+
if !normalRemoteCloseAfterLocalComplete {
|
|
326
|
+
c.cancelContext()
|
|
327
|
+
writer := c.closeWriterLocked()
|
|
328
|
+
locked.Broadcast()
|
|
329
|
+
return writer
|
|
330
|
+
}
|
|
331
|
+
locked.Broadcast()
|
|
332
|
+
return nil
|
|
333
|
+
}
|
|
334
|
+
|
|
231
335
|
// WriteCallCancel writes a call cancel packet.
|
|
232
336
|
func (c *commonRPC) WriteCallCancel() error {
|
|
233
337
|
// Use atomic swap to check and set completion atomically
|
|
@@ -276,6 +380,7 @@ func (c *commonRPC) finishLocalCompletion() {
|
|
|
276
380
|
}
|
|
277
381
|
locked = c.bcast.Lock()
|
|
278
382
|
c.localCompleting = false
|
|
383
|
+
c.localActive = false
|
|
279
384
|
c.localDone = true
|
|
280
385
|
locked.Broadcast()
|
|
281
386
|
locked.Unlock()
|
package/srpc/common-rpc.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="es2024.promise" />
|
|
1
2
|
import type { Sink, Source } from 'it-stream-types'
|
|
2
3
|
import { pushable, type Pushable } from 'it-pushable'
|
|
3
4
|
import { CompleteMessage } from '@aptre/protobuf-es-lite'
|
|
@@ -5,6 +6,7 @@ import { CompleteMessage } from '@aptre/protobuf-es-lite'
|
|
|
5
6
|
import type { CallData, CallStart } from './rpcproto.pb.js'
|
|
6
7
|
import { Packet } from './rpcproto.pb.js'
|
|
7
8
|
import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js'
|
|
9
|
+
import type { TerminalKind } from './server-invocation.js'
|
|
8
10
|
|
|
9
11
|
const maxBufferedOutgoingPackets = 1
|
|
10
12
|
|
|
@@ -34,10 +36,27 @@ export class CommonRPC {
|
|
|
34
36
|
|
|
35
37
|
// closed indicates this rpc has been closed already.
|
|
36
38
|
private closed?: true | Error
|
|
39
|
+
// remoteCompleted is set only by an explicit remote CallData completion.
|
|
40
|
+
private remoteCompleted = false
|
|
41
|
+
// remoteError records a remote error or transport failure.
|
|
42
|
+
private remoteError?: Error
|
|
43
|
+
// remoteSourceClosed records an incoming source ending without a packet error.
|
|
44
|
+
private remoteSourceClosed = false
|
|
45
|
+
// remoteTerminal is the first valid remote terminal.
|
|
46
|
+
private remoteTerminal?: TerminalKind
|
|
47
|
+
// invocationController cancels the server invocation on a remote terminal.
|
|
48
|
+
private readonly invocationController = new AbortController()
|
|
49
|
+
// terminalPromise resolves when a remote terminal is recorded.
|
|
50
|
+
private readonly terminalPromise: Promise<void>
|
|
51
|
+
private resolveTerminal!: () => void
|
|
52
|
+
|
|
37
53
|
// writeDrainAbort wakes writers waiting for outbound stream drain on close.
|
|
38
54
|
private readonly writeDrainAbort = new AbortController()
|
|
39
55
|
|
|
40
56
|
constructor() {
|
|
57
|
+
const { promise, resolve } = Promise.withResolvers<void>()
|
|
58
|
+
this.terminalPromise = promise
|
|
59
|
+
this.resolveTerminal = resolve
|
|
41
60
|
this.sink = this._createSink()
|
|
42
61
|
this.source = this._source
|
|
43
62
|
this.rpcDataSource = this._rpcDataSource
|
|
@@ -48,6 +67,57 @@ export class CommonRPC {
|
|
|
48
67
|
return this.closed ?? false
|
|
49
68
|
}
|
|
50
69
|
|
|
70
|
+
// invocationSignal is canceled when the RPC reaches a terminal.
|
|
71
|
+
protected get invocationSignal(): AbortSignal {
|
|
72
|
+
return this.invocationController.signal
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// waitTerminal waits for the remote terminal or external owner cancellation.
|
|
76
|
+
protected async waitTerminal(
|
|
77
|
+
ownerSignal: AbortSignal,
|
|
78
|
+
): Promise<TerminalKind> {
|
|
79
|
+
const { promise: ownerDone, resolve: resolveOwnerDone } =
|
|
80
|
+
Promise.withResolvers<void>()
|
|
81
|
+
const onAbort = () => resolveOwnerDone()
|
|
82
|
+
ownerSignal.addEventListener('abort', onAbort, { once: true })
|
|
83
|
+
let ownerAborted = ownerSignal.aborted
|
|
84
|
+
try {
|
|
85
|
+
for (;;) {
|
|
86
|
+
const terminal = this.getTerminalKind()
|
|
87
|
+
if (terminal !== undefined) {
|
|
88
|
+
if (
|
|
89
|
+
terminal === 'closed' &&
|
|
90
|
+
this.remoteSourceClosed &&
|
|
91
|
+
!this.closed
|
|
92
|
+
) {
|
|
93
|
+
await this.close()
|
|
94
|
+
}
|
|
95
|
+
return terminal
|
|
96
|
+
}
|
|
97
|
+
if (ownerAborted) {
|
|
98
|
+
return 'abandoned'
|
|
99
|
+
}
|
|
100
|
+
await Promise.race([this.terminalPromise, ownerDone])
|
|
101
|
+
ownerAborted = ownerSignal.aborted
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
ownerSignal.removeEventListener('abort', onAbort)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// getTerminalKind returns the observed remote terminal, if any.
|
|
109
|
+
public getTerminalKind(): TerminalKind | undefined {
|
|
110
|
+
return this.remoteTerminal
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private recordRemoteTerminal(kind: TerminalKind) {
|
|
114
|
+
if (this.remoteTerminal !== undefined) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
this.remoteTerminal = kind
|
|
118
|
+
this.resolveTerminal()
|
|
119
|
+
}
|
|
120
|
+
|
|
51
121
|
// writeCallData writes the call data packet.
|
|
52
122
|
public async writeCallData(
|
|
53
123
|
data?: Uint8Array,
|
|
@@ -82,7 +152,7 @@ export class CommonRPC {
|
|
|
82
152
|
}
|
|
83
153
|
|
|
84
154
|
// writeCallCancel writes the call cancel packet.
|
|
85
|
-
public async writeCallCancel() {
|
|
155
|
+
public async writeCallCancel(waitForDrain = false) {
|
|
86
156
|
await this.writePacket(
|
|
87
157
|
{
|
|
88
158
|
body: {
|
|
@@ -92,6 +162,9 @@ export class CommonRPC {
|
|
|
92
162
|
},
|
|
93
163
|
{ waitForDrain: false },
|
|
94
164
|
)
|
|
165
|
+
if (waitForDrain) {
|
|
166
|
+
await this._source.onEmpty({ signal: this.writeDrainAbort.signal })
|
|
167
|
+
}
|
|
95
168
|
}
|
|
96
169
|
|
|
97
170
|
// writeCallDataFromSource writes all call data from the iterable.
|
|
@@ -194,18 +267,28 @@ export class CommonRPC {
|
|
|
194
267
|
}
|
|
195
268
|
|
|
196
269
|
this.pushRpcData(packet.data, packet.dataIsZero)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
new RemoteRPCError(this.service, this.method, packet.error)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
270
|
+
const remoteError =
|
|
271
|
+
packet.error ?
|
|
272
|
+
new RemoteRPCError(this.service, this.method, packet.error)
|
|
273
|
+
: undefined
|
|
274
|
+
if (remoteError) {
|
|
275
|
+
this.remoteError ??= remoteError
|
|
276
|
+
this.invocationController.abort()
|
|
277
|
+
this.recordRemoteTerminal('transportLost')
|
|
278
|
+
}
|
|
279
|
+
if (packet.complete && !remoteError) {
|
|
280
|
+
this.remoteCompleted = true
|
|
281
|
+
this.recordRemoteTerminal('committed')
|
|
282
|
+
this._rpcDataSource.end(remoteError)
|
|
283
|
+
} else if (remoteError) {
|
|
284
|
+
this._rpcDataSource.end(remoteError)
|
|
203
285
|
}
|
|
204
286
|
}
|
|
205
287
|
|
|
206
288
|
// handleCallCancel handles a CallCancel packet.
|
|
207
289
|
public async handleCallCancel() {
|
|
208
|
-
this.
|
|
290
|
+
this.recordRemoteTerminal('canceled')
|
|
291
|
+
await this.close(new Error(ERR_RPC_ABORT))
|
|
209
292
|
}
|
|
210
293
|
|
|
211
294
|
// close closes the call, optionally with an error.
|
|
@@ -214,6 +297,11 @@ export class CommonRPC {
|
|
|
214
297
|
return
|
|
215
298
|
}
|
|
216
299
|
this.closed = err ?? true
|
|
300
|
+
if (!this.remoteError && err) {
|
|
301
|
+
this.remoteError = err
|
|
302
|
+
}
|
|
303
|
+
this.recordRemoteTerminal(err ? 'transportLost' : 'closed')
|
|
304
|
+
this.invocationController.abort()
|
|
217
305
|
// note: this does nothing if _source is already ended.
|
|
218
306
|
if (err && err.message) {
|
|
219
307
|
await this.writeCallDataPacket(undefined, true, err.message, {
|
|
@@ -241,6 +329,8 @@ export class CommonRPC {
|
|
|
241
329
|
await this.handlePacket(msg)
|
|
242
330
|
}
|
|
243
331
|
}
|
|
332
|
+
this.remoteSourceClosed = true
|
|
333
|
+
this.recordRemoteTerminal('closed')
|
|
244
334
|
} catch (err) {
|
|
245
335
|
this.close(err as Error)
|
|
246
336
|
}
|
package/srpc/common-rpc_test.go
CHANGED
|
@@ -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)
|
package/srpc/handler.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types'
|
|
2
2
|
import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js'
|
|
3
3
|
import { createInvokeFn } from './invoker.js'
|
|
4
|
+
import type { ServerInvocation } from './server-invocation.js'
|
|
4
5
|
|
|
5
6
|
// InvokeFn describes an SRPC call method invoke function.
|
|
6
7
|
export type InvokeFn = (
|
|
7
8
|
dataSource: Source<Uint8Array>,
|
|
8
9
|
dataSink: Sink<Source<Uint8Array>>,
|
|
10
|
+
invocation?: ServerInvocation,
|
|
9
11
|
) => Promise<void>
|
|
10
12
|
|
|
11
13
|
// Handler describes a SRPC call handler implementation.
|
package/srpc/index.ts
CHANGED
|
@@ -6,6 +6,10 @@ export {
|
|
|
6
6
|
castToError,
|
|
7
7
|
} from './errors.js'
|
|
8
8
|
export { Client } from './client.js'
|
|
9
|
+
export { CallReceipt } from './call-receipt.js'
|
|
10
|
+
export type { HeldCall, ReceiptRpc } from './call-receipt.js'
|
|
11
|
+
export { ServerInvocation } from './server-invocation.js'
|
|
12
|
+
export type { TerminalKind } from './server-invocation.js'
|
|
9
13
|
export { Server } from './server.js'
|
|
10
14
|
export { StreamConn } from './conn.js'
|
|
11
15
|
export type { StreamConnParams, StreamHandler } from './conn.js'
|
package/srpc/invoker.ts
CHANGED
|
@@ -11,12 +11,16 @@ import { writeToPushable } from './pushable.js'
|
|
|
11
11
|
import type { MessageType, Message } from '@aptre/protobuf-es-lite'
|
|
12
12
|
import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite'
|
|
13
13
|
|
|
14
|
+
import type { ServerInvocation } from './server-invocation.js'
|
|
14
15
|
// MethodProto is a function which matches one of the RPC signatures.
|
|
15
16
|
export type MethodProto<R extends Message<R>, O extends Message<O>> =
|
|
16
|
-
| ((request: R) => Promise<O>)
|
|
17
|
-
| ((request: R) => AsyncIterable<O>)
|
|
18
|
-
| ((request: AsyncIterable<R
|
|
19
|
-
| ((
|
|
17
|
+
| ((request: R, invocation?: ServerInvocation) => Promise<O>)
|
|
18
|
+
| ((request: R, invocation?: ServerInvocation) => AsyncIterable<O>)
|
|
19
|
+
| ((request: AsyncIterable<R>, invocation?: ServerInvocation) => Promise<O>)
|
|
20
|
+
| ((
|
|
21
|
+
request: AsyncIterable<R>,
|
|
22
|
+
invocation?: ServerInvocation,
|
|
23
|
+
) => AsyncIterable<O>)
|
|
20
24
|
|
|
21
25
|
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
22
26
|
export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
|
|
@@ -32,6 +36,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
|
|
|
32
36
|
return async (
|
|
33
37
|
dataSource: Source<Uint8Array>,
|
|
34
38
|
dataSink: Sink<Source<Uint8Array>>,
|
|
39
|
+
invocation?: ServerInvocation,
|
|
35
40
|
) => {
|
|
36
41
|
// responseSink is a Sink for response messages.
|
|
37
42
|
const responseSink = pushable<O>({
|
|
@@ -66,7 +71,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
|
|
|
66
71
|
|
|
67
72
|
// Call the implementation.
|
|
68
73
|
try {
|
|
69
|
-
const responseObj = methodProto(requestArg)
|
|
74
|
+
const responseObj = methodProto(requestArg, invocation)
|
|
70
75
|
if (!responseObj) {
|
|
71
76
|
throw new Error('return value was undefined')
|
|
72
77
|
}
|
package/srpc/msg-stream.go
CHANGED
|
@@ -86,5 +86,13 @@ func (r *MsgStream) Close() error {
|
|
|
86
86
|
return err
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
func (r *MsgStream) receiptTerminalKind() (TerminalKind, bool) {
|
|
90
|
+
receipt, ok := r.rw.(receiptTerminalStream)
|
|
91
|
+
if !ok {
|
|
92
|
+
return 0, false
|
|
93
|
+
}
|
|
94
|
+
return receipt.receiptTerminalKind()
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
// _ is a type assertion
|
|
90
98
|
var _ Stream = ((*MsgStream)(nil))
|
package/srpc/muxed-conn.go
CHANGED
|
@@ -9,8 +9,7 @@ import (
|
|
|
9
9
|
yamux "github.com/libp2p/go-yamux/v4"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
func NewYamuxConfig() *yamux.Config {
|
|
12
|
+
var yamuxConfigTemplate = func() yamux.Config {
|
|
14
13
|
config := yamux.DefaultConfig()
|
|
15
14
|
config.MaxStreamWindowSize = 16 * 1024 * 1024
|
|
16
15
|
config.LogOutput = io.Discard
|
|
@@ -18,7 +17,13 @@ func NewYamuxConfig() *yamux.Config {
|
|
|
18
17
|
config.MaxIncomingStreams = math.MaxUint32
|
|
19
18
|
config.AcceptBacklog = 512
|
|
20
19
|
config.EnableKeepAlive = false
|
|
21
|
-
return config
|
|
20
|
+
return *config
|
|
21
|
+
}()
|
|
22
|
+
|
|
23
|
+
// NewYamuxConfig returns a fresh copy of the default yamux configuration.
|
|
24
|
+
func NewYamuxConfig() *yamux.Config {
|
|
25
|
+
config := yamuxConfigTemplate
|
|
26
|
+
return &config
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
// NewMuxedConn constructs a new MuxedConn from a net.Conn.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package srpc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net"
|
|
5
|
+
"os"
|
|
6
|
+
"sync"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestNewMuxedConnConcurrentConfigIsolation(t *testing.T) {
|
|
11
|
+
first := NewYamuxConfig()
|
|
12
|
+
second := NewYamuxConfig()
|
|
13
|
+
if first == second {
|
|
14
|
+
t.Fatal("NewYamuxConfig returned a shared config")
|
|
15
|
+
}
|
|
16
|
+
first.AcceptBacklog++
|
|
17
|
+
if first.AcceptBacklog == second.AcceptBacklog {
|
|
18
|
+
t.Fatal("mutating one yamux config changed another")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// go-yamux's default config reads os.Stderr. Starpc discards yamux logs, so
|
|
22
|
+
// constructing a connection must not observe concurrent stderr replacement.
|
|
23
|
+
originalStderr := os.Stderr
|
|
24
|
+
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
25
|
+
if err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
defer devNull.Close()
|
|
29
|
+
|
|
30
|
+
stopReplacingStderr := make(chan struct{})
|
|
31
|
+
var replaceStderr sync.WaitGroup
|
|
32
|
+
replaceStderr.Go(func() {
|
|
33
|
+
for {
|
|
34
|
+
select {
|
|
35
|
+
case <-stopReplacingStderr:
|
|
36
|
+
return
|
|
37
|
+
default:
|
|
38
|
+
os.Stderr = devNull
|
|
39
|
+
os.Stderr = originalStderr
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const iterations = 100
|
|
45
|
+
start := make(chan struct{})
|
|
46
|
+
errs := make(chan error, 2)
|
|
47
|
+
var builders sync.WaitGroup
|
|
48
|
+
builders.Add(2)
|
|
49
|
+
for range 2 {
|
|
50
|
+
go func() {
|
|
51
|
+
defer builders.Done()
|
|
52
|
+
<-start
|
|
53
|
+
for range iterations {
|
|
54
|
+
conn, peer := net.Pipe()
|
|
55
|
+
muxed, err := NewMuxedConn(conn, false, nil)
|
|
56
|
+
if err == nil {
|
|
57
|
+
err = muxed.Close()
|
|
58
|
+
} else {
|
|
59
|
+
_ = conn.Close()
|
|
60
|
+
}
|
|
61
|
+
_ = peer.Close()
|
|
62
|
+
if err != nil {
|
|
63
|
+
errs <- err
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}()
|
|
68
|
+
}
|
|
69
|
+
close(start)
|
|
70
|
+
builders.Wait()
|
|
71
|
+
close(stopReplacingStderr)
|
|
72
|
+
replaceStderr.Wait()
|
|
73
|
+
os.Stderr = originalStderr
|
|
74
|
+
close(errs)
|
|
75
|
+
|
|
76
|
+
for err := range errs {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
}
|
package/srpc/rpcproto.pb.go
CHANGED