starpc 0.49.14 → 0.49.16
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 -1
- package/dist/srpc/common-rpc.d.ts +8 -1
- package/dist/srpc/common-rpc.js +31 -4
- package/dist/srpc/common-rpc.test.d.ts +1 -0
- package/dist/srpc/common-rpc.test.js +178 -0
- package/dist/srpc/server.test.js +65 -0
- package/package.json +1 -1
- package/srpc/client-rpc.go +8 -4
- package/srpc/client.go +1 -1
- package/srpc/client.rs +4 -0
- package/srpc/common-rpc.go +74 -19
- package/srpc/common-rpc.test.ts +256 -0
- package/srpc/common-rpc.ts +60 -14
- package/srpc/common-rpc_test.go +280 -0
- package/srpc/lib.rs +8 -0
- package/srpc/rwc-conn.go +3 -1
- package/srpc/server-rpc.go +3 -3
- package/srpc/server.go +3 -1
- package/srpc/server.rs +40 -2
- package/srpc/server.test.ts +88 -0
- package/srpc/stream-pipe.go +4 -2
- package/srpc/websocket.rs +70 -0
- package/srpc/yamux.rs +198 -0
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ import { WebSocketConn } from 'starpc'
|
|
|
232
232
|
import { EchoerClient } from './echo/index.js'
|
|
233
233
|
|
|
234
234
|
const ws = new WebSocket('ws://localhost:8080/api')
|
|
235
|
-
const conn = new WebSocketConn(ws)
|
|
235
|
+
const conn = new WebSocketConn(ws, 'outbound')
|
|
236
236
|
const client = conn.buildClient()
|
|
237
237
|
const echoer = new EchoerClient(client)
|
|
238
238
|
|
|
@@ -10,12 +10,14 @@ export declare class CommonRPC {
|
|
|
10
10
|
protected service?: string;
|
|
11
11
|
protected method?: string;
|
|
12
12
|
private closed?;
|
|
13
|
+
private readonly writeDrainAbort;
|
|
13
14
|
constructor();
|
|
14
15
|
get isClosed(): boolean | Error;
|
|
15
16
|
writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
|
|
17
|
+
private writeCallDataPacket;
|
|
16
18
|
writeCallCancel(): Promise<void>;
|
|
17
19
|
writeCallDataFromSource(dataSource: AsyncIterable<Uint8Array>): Promise<void>;
|
|
18
|
-
protected writePacket(packet: Packet): Promise<void>;
|
|
20
|
+
protected writePacket(packet: Packet, options?: WritePacketOptions): Promise<void>;
|
|
19
21
|
handleMessage(message: Uint8Array): Promise<void>;
|
|
20
22
|
handlePacket(packet: Packet): Promise<void>;
|
|
21
23
|
handleCallStart(packet: Partial<CallStart>): Promise<void>;
|
|
@@ -25,3 +27,8 @@ export declare class CommonRPC {
|
|
|
25
27
|
close(err?: Error): Promise<void>;
|
|
26
28
|
private _createSink;
|
|
27
29
|
}
|
|
30
|
+
interface WritePacketOptions {
|
|
31
|
+
allowClosed?: boolean;
|
|
32
|
+
waitForDrain?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
package/dist/srpc/common-rpc.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
2
|
import { Packet } from './rpcproto.pb.js';
|
|
3
3
|
import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js';
|
|
4
|
+
const maxBufferedOutgoingPackets = 1;
|
|
4
5
|
// CommonRPC is common logic between server and client RPCs.
|
|
5
6
|
export class CommonRPC {
|
|
6
7
|
// sink is the data sink for incoming messages.
|
|
@@ -23,6 +24,8 @@ export class CommonRPC {
|
|
|
23
24
|
method;
|
|
24
25
|
// closed indicates this rpc has been closed already.
|
|
25
26
|
closed;
|
|
27
|
+
// writeDrainAbort wakes writers waiting for outbound stream drain on close.
|
|
28
|
+
writeDrainAbort = new AbortController();
|
|
26
29
|
constructor() {
|
|
27
30
|
this.sink = this._createSink();
|
|
28
31
|
this.source = this._source;
|
|
@@ -34,6 +37,10 @@ export class CommonRPC {
|
|
|
34
37
|
}
|
|
35
38
|
// writeCallData writes the call data packet.
|
|
36
39
|
async writeCallData(data, complete, error) {
|
|
40
|
+
await this.writeCallDataPacket(data, complete, error);
|
|
41
|
+
}
|
|
42
|
+
// writeCallDataPacket writes a call-data packet with optional drain control.
|
|
43
|
+
async writeCallDataPacket(data, complete, error, writeOptions) {
|
|
37
44
|
const callData = {
|
|
38
45
|
data: data || new Uint8Array(0),
|
|
39
46
|
dataIsZero: !!data && data.length === 0,
|
|
@@ -45,7 +52,7 @@ export class CommonRPC {
|
|
|
45
52
|
case: 'callData',
|
|
46
53
|
value: callData,
|
|
47
54
|
},
|
|
48
|
-
});
|
|
55
|
+
}, writeOptions);
|
|
49
56
|
}
|
|
50
57
|
// writeCallCancel writes the call cancel packet.
|
|
51
58
|
async writeCallCancel() {
|
|
@@ -54,7 +61,7 @@ export class CommonRPC {
|
|
|
54
61
|
case: 'callCancel',
|
|
55
62
|
value: true,
|
|
56
63
|
},
|
|
57
|
-
});
|
|
64
|
+
}, { waitForDrain: false });
|
|
58
65
|
}
|
|
59
66
|
// writeCallDataFromSource writes all call data from the iterable.
|
|
60
67
|
async writeCallDataFromSource(dataSource) {
|
|
@@ -69,8 +76,24 @@ export class CommonRPC {
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
// writePacket writes a packet to the stream.
|
|
72
|
-
async writePacket(packet) {
|
|
79
|
+
async writePacket(packet, options) {
|
|
80
|
+
if (this.closed && !options?.allowClosed) {
|
|
81
|
+
throw new Error(ERR_RPC_ABORT);
|
|
82
|
+
}
|
|
73
83
|
this._source.push(packet);
|
|
84
|
+
if (options?.waitForDrain === false ||
|
|
85
|
+
this._source.readableLength <= maxBufferedOutgoingPackets) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await this._source.onEmpty({ signal: this.writeDrainAbort.signal });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (this.closed) {
|
|
93
|
+
throw new Error(ERR_RPC_ABORT, { cause: err });
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
74
97
|
}
|
|
75
98
|
// handleMessage handles an incoming encoded Packet.
|
|
76
99
|
//
|
|
@@ -149,8 +172,12 @@ export class CommonRPC {
|
|
|
149
172
|
this.closed = err ?? true;
|
|
150
173
|
// note: this does nothing if _source is already ended.
|
|
151
174
|
if (err && err.message) {
|
|
152
|
-
await this.
|
|
175
|
+
await this.writeCallDataPacket(undefined, true, err.message, {
|
|
176
|
+
allowClosed: true,
|
|
177
|
+
waitForDrain: false,
|
|
178
|
+
});
|
|
153
179
|
}
|
|
180
|
+
this.writeDrainAbort.abort();
|
|
154
181
|
this._source.end();
|
|
155
182
|
this._rpcDataSource.end(err);
|
|
156
183
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Client } from './client.js';
|
|
3
|
+
import { CommonRPC } from './common-rpc.js';
|
|
4
|
+
import { Packet } from './rpcproto.pb.js';
|
|
5
|
+
describe('CommonRPC', () => {
|
|
6
|
+
it('backpressures call-data sources until outbound packets drain', async () => {
|
|
7
|
+
const rpc = new CommonRPC();
|
|
8
|
+
const yielded = { count: 0 };
|
|
9
|
+
const secondYield = deferred();
|
|
10
|
+
const thirdYield = deferred();
|
|
11
|
+
const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
|
|
12
|
+
[1, secondYield],
|
|
13
|
+
[2, thirdYield],
|
|
14
|
+
])));
|
|
15
|
+
await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
|
|
16
|
+
expect(yielded.count).toBe(2);
|
|
17
|
+
await expectPending(thirdYield.promise, 'third call-data chunk');
|
|
18
|
+
expect(yielded.count).toBe(2);
|
|
19
|
+
const received = new Array();
|
|
20
|
+
const readComplete = (async () => {
|
|
21
|
+
for await (const packet of rpc.source) {
|
|
22
|
+
const body = packet.body;
|
|
23
|
+
if (body?.case !== 'callData') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (body.value.complete) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!body.value.data) {
|
|
30
|
+
throw new Error('call data packet missing data');
|
|
31
|
+
}
|
|
32
|
+
received.push(body.value.data[0]);
|
|
33
|
+
}
|
|
34
|
+
throw new Error('outbound call data ended before completion');
|
|
35
|
+
})();
|
|
36
|
+
await promiseWithTimeout(Promise.all([writeDone, readComplete]), 'backpressured call data drain');
|
|
37
|
+
await rpc.close();
|
|
38
|
+
expect(yielded.count).toBe(32);
|
|
39
|
+
expect(received).toHaveLength(32);
|
|
40
|
+
expect(received[0]).toBe(0);
|
|
41
|
+
expect(received[31]).toBe(31);
|
|
42
|
+
});
|
|
43
|
+
it('wakes call-data writers when the rpc closes while waiting for drain', async () => {
|
|
44
|
+
const rpc = new CommonRPC();
|
|
45
|
+
const yielded = { count: 0 };
|
|
46
|
+
const secondYield = deferred();
|
|
47
|
+
const thirdYield = deferred();
|
|
48
|
+
const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
|
|
49
|
+
[1, secondYield],
|
|
50
|
+
[2, thirdYield],
|
|
51
|
+
])));
|
|
52
|
+
await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
|
|
53
|
+
expect(yielded.count).toBe(2);
|
|
54
|
+
await expectPending(thirdYield.promise, 'third call-data chunk');
|
|
55
|
+
expect(yielded.count).toBe(2);
|
|
56
|
+
await rpc.close();
|
|
57
|
+
await expect(promiseWithTimeout(writeDone, 'call data writer close')).resolves.toBeUndefined();
|
|
58
|
+
expect(yielded.count).toBeLessThan(32);
|
|
59
|
+
});
|
|
60
|
+
it('wakes call-data writers when an error closes the rpc while waiting for drain', async () => {
|
|
61
|
+
const rpc = new CommonRPC();
|
|
62
|
+
const yielded = { count: 0 };
|
|
63
|
+
const secondYield = deferred();
|
|
64
|
+
const thirdYield = deferred();
|
|
65
|
+
const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
|
|
66
|
+
[1, secondYield],
|
|
67
|
+
[2, thirdYield],
|
|
68
|
+
])));
|
|
69
|
+
await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
|
|
70
|
+
expect(yielded.count).toBe(2);
|
|
71
|
+
await expectPending(thirdYield.promise, 'third call-data chunk');
|
|
72
|
+
expect(yielded.count).toBe(2);
|
|
73
|
+
await rpc.close(new Error('boom'));
|
|
74
|
+
await expect(promiseWithTimeout(writeDone, 'call data writer error close')).resolves.toBeUndefined();
|
|
75
|
+
expect(yielded.count).toBeLessThan(32);
|
|
76
|
+
});
|
|
77
|
+
it('does not backpressure call-cancel packets behind queued call data', async () => {
|
|
78
|
+
const rpc = new CommonRPC();
|
|
79
|
+
const yielded = { count: 0 };
|
|
80
|
+
const secondYield = deferred();
|
|
81
|
+
const thirdYield = deferred();
|
|
82
|
+
const writeDone = rpc.writeCallDataFromSource(chunkSource(yielded, new Map([
|
|
83
|
+
[1, secondYield],
|
|
84
|
+
[2, thirdYield],
|
|
85
|
+
])));
|
|
86
|
+
await promiseWithTimeout(secondYield.promise, 'second call-data chunk');
|
|
87
|
+
expect(yielded.count).toBe(2);
|
|
88
|
+
await expectPending(thirdYield.promise, 'third call-data chunk');
|
|
89
|
+
expect(yielded.count).toBe(2);
|
|
90
|
+
const cancelDone = rpc.writeCallCancel();
|
|
91
|
+
await rpc.close(new Error('abort'));
|
|
92
|
+
await expect(promiseWithTimeout(cancelDone, 'call cancel write')).resolves.toBeUndefined();
|
|
93
|
+
await expect(promiseWithTimeout(writeDone, 'blocked call data close')).resolves.toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
it('backpressures client-stream sources through the Client request path', async () => {
|
|
96
|
+
const yielded = { count: 0 };
|
|
97
|
+
const firstYield = deferred();
|
|
98
|
+
const secondYield = deferred();
|
|
99
|
+
const sinkConsumed = { count: 0 };
|
|
100
|
+
const sinkGate = deferred();
|
|
101
|
+
const responseGate = deferred();
|
|
102
|
+
const response = new Uint8Array([7]);
|
|
103
|
+
const client = new Client(async () => ({
|
|
104
|
+
source: (async function* () {
|
|
105
|
+
await responseGate.promise;
|
|
106
|
+
yield Packet.toBinary({
|
|
107
|
+
body: {
|
|
108
|
+
case: 'callData',
|
|
109
|
+
value: {
|
|
110
|
+
data: response,
|
|
111
|
+
dataIsZero: false,
|
|
112
|
+
complete: false,
|
|
113
|
+
error: '',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
})(),
|
|
118
|
+
sink: async (source) => {
|
|
119
|
+
await sinkGate.promise;
|
|
120
|
+
for await (const _packet of source) {
|
|
121
|
+
sinkConsumed.count++;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
const request = client.clientStreamingRequest('test.Service', 'Upload', chunkSource(yielded, new Map([
|
|
126
|
+
[0, firstYield],
|
|
127
|
+
[1, secondYield],
|
|
128
|
+
])));
|
|
129
|
+
await promiseWithTimeout(firstYield.promise, 'first upload chunk');
|
|
130
|
+
expect(yielded.count).toBe(1);
|
|
131
|
+
await expectPending(secondYield.promise, 'second upload chunk');
|
|
132
|
+
expect(yielded.count).toBe(1);
|
|
133
|
+
expect(sinkConsumed.count).toBe(0);
|
|
134
|
+
await expectPending(request, 'client-stream request');
|
|
135
|
+
sinkGate.resolve();
|
|
136
|
+
responseGate.resolve();
|
|
137
|
+
await expect(promiseWithTimeout(request, 'client-stream response')).resolves.toEqual(response);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
async function* chunkSource(yielded, yieldMarks) {
|
|
141
|
+
for (const i of Array.from({ length: 32 }, (_, index) => index)) {
|
|
142
|
+
yielded.count++;
|
|
143
|
+
yieldMarks?.get(i)?.resolve();
|
|
144
|
+
yield new Uint8Array([i]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function promiseWithTimeout(promise, label) {
|
|
148
|
+
return Promise.race([
|
|
149
|
+
promise,
|
|
150
|
+
new Promise((_, reject) => {
|
|
151
|
+
setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), 500);
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
async function expectPending(promise, label) {
|
|
156
|
+
await expect(Promise.race([
|
|
157
|
+
promise.then(() => 'settled'),
|
|
158
|
+
new Promise((resolve) => {
|
|
159
|
+
setTimeout(() => resolve('pending'), 50);
|
|
160
|
+
}),
|
|
161
|
+
]), `${label} should not be requested while outbound packets are blocked`).resolves.toBe('pending');
|
|
162
|
+
}
|
|
163
|
+
function deferred() {
|
|
164
|
+
const callbacks = {};
|
|
165
|
+
const promise = new Promise((resolve, reject) => {
|
|
166
|
+
callbacks.resolve = resolve;
|
|
167
|
+
callbacks.reject = reject;
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
promise,
|
|
171
|
+
resolve() {
|
|
172
|
+
callbacks.resolve?.();
|
|
173
|
+
},
|
|
174
|
+
reject(reason) {
|
|
175
|
+
callbacks.reject?.(reason);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/srpc/server.test.js
CHANGED
|
@@ -94,6 +94,71 @@ describe('srpc server', () => {
|
|
|
94
94
|
}
|
|
95
95
|
expect([...body.value.data]).toEqual([...response]);
|
|
96
96
|
});
|
|
97
|
+
it('keeps StreamConn server-streaming responses open after Go-style request close', async () => {
|
|
98
|
+
const mux = createMux();
|
|
99
|
+
const response = new TextEncoder().encode('streamconn init');
|
|
100
|
+
mux.registerLookupMethod(async (service, method) => {
|
|
101
|
+
if (service !== 'test.ResourceService' || method !== 'ResourceClient') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return async (_dataSource, dataSink) => {
|
|
105
|
+
await dataSink((async function* () {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
107
|
+
yield response;
|
|
108
|
+
})());
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const server = new Server(mux.lookupMethod);
|
|
112
|
+
const clientConn = new StreamConn();
|
|
113
|
+
const serverConn = new StreamConn(server, { direction: 'inbound' });
|
|
114
|
+
const { port1: clientPort, port2: serverPort } = new MessageChannel();
|
|
115
|
+
const clientChannelStream = new ChannelStream('client', clientPort);
|
|
116
|
+
const serverChannelStream = new ChannelStream('server', serverPort);
|
|
117
|
+
pipe(clientChannelStream, clientConn, combineUint8ArrayListTransform(), clientChannelStream)
|
|
118
|
+
.catch((err) => clientConn.close(err))
|
|
119
|
+
.then(() => clientConn.close());
|
|
120
|
+
pipe(serverChannelStream, serverConn, combineUint8ArrayListTransform(), serverChannelStream)
|
|
121
|
+
.catch((err) => serverConn.close(err))
|
|
122
|
+
.then(() => serverConn.close());
|
|
123
|
+
try {
|
|
124
|
+
const stream = await clientConn.openStream();
|
|
125
|
+
const firstResponse = (async () => {
|
|
126
|
+
for await (const packetData of stream.source) {
|
|
127
|
+
const packet = Packet.fromBinary(packetData);
|
|
128
|
+
if (packet.body?.case === 'callData') {
|
|
129
|
+
return packet;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new Error('server response stream ended before call data');
|
|
133
|
+
})();
|
|
134
|
+
await stream.sink((async function* () {
|
|
135
|
+
yield Packet.toBinary({
|
|
136
|
+
body: {
|
|
137
|
+
case: 'callStart',
|
|
138
|
+
value: {
|
|
139
|
+
rpcService: 'test.ResourceService',
|
|
140
|
+
rpcMethod: 'ResourceClient',
|
|
141
|
+
data: new Uint8Array(0),
|
|
142
|
+
dataIsZero: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
})());
|
|
147
|
+
const packet = await promiseWithTimeout(firstResponse, 'StreamConn server-streaming response');
|
|
148
|
+
const body = packet.body;
|
|
149
|
+
expect(body?.case).toBe('callData');
|
|
150
|
+
if (body?.case !== 'callData' || !body.value?.data) {
|
|
151
|
+
throw new Error('expected callData packet');
|
|
152
|
+
}
|
|
153
|
+
expect([...body.value.data]).toEqual([...response]);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
clientConn.close();
|
|
157
|
+
serverConn.close();
|
|
158
|
+
clientChannelStream.close();
|
|
159
|
+
serverChannelStream.close();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
97
162
|
it('removes abort listeners after a request completes', async () => {
|
|
98
163
|
const controller = new AbortController();
|
|
99
164
|
const removeEventListener = vi.spyOn(controller.signal, 'removeEventListener');
|
package/package.json
CHANGED
package/srpc/client-rpc.go
CHANGED
|
@@ -31,7 +31,7 @@ func (r *ClientRPC) Start(writer PacketWriter, writeFirstMsg bool, firstMsg []by
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
if err := r.ctx.Err(); err != nil {
|
|
34
|
-
r.
|
|
34
|
+
r.cancelContext()
|
|
35
35
|
_ = writer.Close()
|
|
36
36
|
return context.Canceled
|
|
37
37
|
}
|
|
@@ -48,7 +48,7 @@ func (r *ClientRPC) Start(writer PacketWriter, writeFirstMsg bool, firstMsg []by
|
|
|
48
48
|
pkt := NewCallStartPacket(r.service, r.method, firstMsg, firstMsgEmpty)
|
|
49
49
|
err = writer.WritePacket(pkt)
|
|
50
50
|
if err != nil {
|
|
51
|
-
r.
|
|
51
|
+
r.cancelContext()
|
|
52
52
|
_ = writer.Close()
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -74,7 +74,7 @@ func (r *ClientRPC) HandleStreamClose(closeErr error) {
|
|
|
74
74
|
r.remoteErr = closeErr
|
|
75
75
|
}
|
|
76
76
|
r.dataClosed = true
|
|
77
|
-
r.
|
|
77
|
+
r.cancelContext()
|
|
78
78
|
locked.Broadcast()
|
|
79
79
|
locked.Unlock()
|
|
80
80
|
}
|
|
@@ -109,10 +109,14 @@ func (r *ClientRPC) HandleCallStart(pkt *CallStart) error {
|
|
|
109
109
|
// Close releases any resources held by the ClientRPC.
|
|
110
110
|
func (r *ClientRPC) Close() {
|
|
111
111
|
locked := r.bcast.Lock()
|
|
112
|
+
var writer PacketWriter
|
|
112
113
|
// call did not start yet if writer is nil.
|
|
113
114
|
if r.writer != nil {
|
|
114
115
|
_ = r.WriteCallCancel()
|
|
115
|
-
r.closeLocked(&locked)
|
|
116
|
+
writer = r.closeLocked(&locked)
|
|
116
117
|
}
|
|
117
118
|
locked.Unlock()
|
|
119
|
+
if writer != nil {
|
|
120
|
+
_ = writer.Close()
|
|
121
|
+
}
|
|
118
122
|
}
|
package/srpc/client.go
CHANGED
package/srpc/client.rs
CHANGED
|
@@ -262,6 +262,10 @@ pub mod transport {
|
|
|
262
262
|
|
|
263
263
|
/// Re-export TransportPacketWriter for direct use.
|
|
264
264
|
pub use crate::transport::TransportPacketWriter;
|
|
265
|
+
|
|
266
|
+
/// Re-export YamuxStreamOpener for multiplexed transports.
|
|
267
|
+
#[cfg(feature = "yamux")]
|
|
268
|
+
pub use crate::yamux::YamuxStreamOpener;
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
#[cfg(test)]
|
package/srpc/common-rpc.go
CHANGED
|
@@ -5,16 +5,19 @@ import (
|
|
|
5
5
|
"io"
|
|
6
6
|
"sync/atomic"
|
|
7
7
|
|
|
8
|
+
"github.com/aperturerobotics/starpc/internal/contextutil"
|
|
8
9
|
"github.com/aperturerobotics/util/broadcast"
|
|
9
10
|
"github.com/pkg/errors"
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
// commonRPC contains common logic between server/client rpc.
|
|
13
14
|
type commonRPC struct {
|
|
14
|
-
// ctx is the context, canceled when the
|
|
15
|
+
// ctx is the RPC context, canceled when the RPC is canceled.
|
|
15
16
|
ctx context.Context
|
|
16
|
-
// ctxCancel
|
|
17
|
+
// ctxCancel cancels ctx.
|
|
17
18
|
ctxCancel context.CancelFunc
|
|
19
|
+
// ctxCanceled tracks whether ctxCancel has already been called.
|
|
20
|
+
ctxCanceled atomic.Bool
|
|
18
21
|
// service is the rpc service
|
|
19
22
|
service string
|
|
20
23
|
// method is the rpc method
|
|
@@ -26,6 +29,13 @@ type commonRPC struct {
|
|
|
26
29
|
bcast broadcast.Broadcast
|
|
27
30
|
// writer is the writer to write messages to
|
|
28
31
|
writer PacketWriter
|
|
32
|
+
// writerClosed is set after writer has been closed locally.
|
|
33
|
+
writerClosed bool
|
|
34
|
+
// localCompleting is set while the local handler is publishing its terminal
|
|
35
|
+
// packet and closing the writer.
|
|
36
|
+
localCompleting bool
|
|
37
|
+
// localDone is set after the local handler has completed normally.
|
|
38
|
+
localDone bool
|
|
29
39
|
// dataQueue contains incoming data packets.
|
|
30
40
|
// note: packets may be len() == 0
|
|
31
41
|
dataQueue [][]byte
|
|
@@ -38,7 +48,14 @@ type commonRPC struct {
|
|
|
38
48
|
|
|
39
49
|
// initCommonRPC initializes the commonRPC.
|
|
40
50
|
func initCommonRPC(ctx context.Context, rpc *commonRPC) {
|
|
41
|
-
rpc.ctx, rpc.ctxCancel =
|
|
51
|
+
rpc.ctx, rpc.ctxCancel = contextutil.WithCancel(ctx)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func (c *commonRPC) cancelContext() {
|
|
55
|
+
if c.ctxCanceled.Swap(true) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
c.ctxCancel()
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
// Context is canceled when the rpc has finished.
|
|
@@ -51,10 +68,13 @@ func (c *commonRPC) Wait(ctx context.Context) error {
|
|
|
51
68
|
for {
|
|
52
69
|
var err error
|
|
53
70
|
var waitCh <-chan struct{}
|
|
54
|
-
var
|
|
71
|
+
var rpcCanceled bool
|
|
72
|
+
var localDone bool
|
|
55
73
|
locked := c.bcast.Lock()
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
err = c.remoteErr
|
|
75
|
+
rpcCanceled = c.ctx.Err() != nil
|
|
76
|
+
localDone = c.localDone
|
|
77
|
+
if err == nil && !rpcCanceled && !localDone {
|
|
58
78
|
waitCh = locked.WaitCh()
|
|
59
79
|
}
|
|
60
80
|
locked.Unlock()
|
|
@@ -62,15 +82,16 @@ func (c *commonRPC) Wait(ctx context.Context) error {
|
|
|
62
82
|
if err != nil {
|
|
63
83
|
return err
|
|
64
84
|
}
|
|
65
|
-
if
|
|
66
|
-
|
|
85
|
+
if localDone {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
if rpcCanceled {
|
|
67
89
|
return context.Canceled
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
select {
|
|
71
93
|
case <-ctx.Done():
|
|
72
94
|
return context.Canceled
|
|
73
|
-
case <-rpcCtx.Done():
|
|
74
95
|
case <-waitCh:
|
|
75
96
|
}
|
|
76
97
|
}
|
|
@@ -86,8 +107,11 @@ func (c *commonRPC) ReadOne() ([]byte, error) {
|
|
|
86
107
|
locked := c.bcast.Lock()
|
|
87
108
|
if ctxDone && !c.dataClosed {
|
|
88
109
|
// context must have been canceled locally
|
|
89
|
-
c.closeLocked(&locked)
|
|
110
|
+
writer := c.closeLocked(&locked)
|
|
90
111
|
locked.Unlock()
|
|
112
|
+
if writer != nil {
|
|
113
|
+
_ = writer.Close()
|
|
114
|
+
}
|
|
91
115
|
return nil, context.Canceled
|
|
92
116
|
}
|
|
93
117
|
|
|
@@ -144,16 +168,19 @@ func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err er
|
|
|
144
168
|
func (c *commonRPC) HandleStreamClose(closeErr error) {
|
|
145
169
|
var writer PacketWriter
|
|
146
170
|
locked := c.bcast.Lock()
|
|
147
|
-
if c.dataClosed {
|
|
171
|
+
if c.dataClosed && c.writerClosed {
|
|
148
172
|
locked.Unlock()
|
|
149
173
|
return
|
|
150
174
|
}
|
|
175
|
+
normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
|
|
151
176
|
if closeErr != nil && c.remoteErr == nil {
|
|
152
177
|
c.remoteErr = closeErr
|
|
153
178
|
}
|
|
154
179
|
c.dataClosed = true
|
|
155
|
-
|
|
156
|
-
|
|
180
|
+
if !normalRemoteCloseAfterLocalComplete {
|
|
181
|
+
c.cancelContext()
|
|
182
|
+
writer = c.closeWriterLocked()
|
|
183
|
+
}
|
|
157
184
|
locked.Broadcast()
|
|
158
185
|
locked.Unlock()
|
|
159
186
|
if writer != nil {
|
|
@@ -212,16 +239,44 @@ func (c *commonRPC) WriteCallCancel() error {
|
|
|
212
239
|
}
|
|
213
240
|
|
|
214
241
|
// closeLocked releases resources held by the RPC.
|
|
215
|
-
func (c *commonRPC) closeLocked(locked *broadcast.Locked) {
|
|
216
|
-
if c.dataClosed {
|
|
217
|
-
return
|
|
218
|
-
}
|
|
242
|
+
func (c *commonRPC) closeLocked(locked *broadcast.Locked) PacketWriter {
|
|
219
243
|
c.dataClosed = true
|
|
220
244
|
c.localCompleted.Store(true)
|
|
221
245
|
if c.remoteErr == nil {
|
|
222
246
|
c.remoteErr = context.Canceled
|
|
223
247
|
}
|
|
224
|
-
|
|
248
|
+
writer := c.closeWriterLocked()
|
|
225
249
|
locked.Broadcast()
|
|
226
|
-
c.
|
|
250
|
+
c.cancelContext()
|
|
251
|
+
return writer
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func (c *commonRPC) closeWriterLocked() PacketWriter {
|
|
255
|
+
if c.writerClosed || c.writer == nil {
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
c.writerClosed = true
|
|
259
|
+
return c.writer
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func (c *commonRPC) beginLocalCompletion() {
|
|
263
|
+
locked := c.bcast.Lock()
|
|
264
|
+
c.localCompleted.Store(true)
|
|
265
|
+
c.localCompleting = true
|
|
266
|
+
locked.Unlock()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func (c *commonRPC) finishLocalCompletion() {
|
|
270
|
+
locked := c.bcast.Lock()
|
|
271
|
+
c.localCompleted.Store(true)
|
|
272
|
+
writer := c.closeWriterLocked()
|
|
273
|
+
locked.Unlock()
|
|
274
|
+
if writer != nil {
|
|
275
|
+
_ = writer.Close()
|
|
276
|
+
}
|
|
277
|
+
locked = c.bcast.Lock()
|
|
278
|
+
c.localCompleting = false
|
|
279
|
+
c.localDone = true
|
|
280
|
+
locked.Broadcast()
|
|
281
|
+
locked.Unlock()
|
|
227
282
|
}
|