starpc 0.52.0 → 0.52.2
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 +37 -8
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/client-rpc.d.ts +1 -0
- package/dist/srpc/client-rpc.js +9 -0
- package/dist/srpc/client-rpc.test.d.ts +1 -0
- package/dist/srpc/client-rpc.test.js +50 -0
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.d.ts +5 -0
- package/dist/srpc/common-rpc.js +37 -8
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/errors.d.ts +2 -0
- package/dist/srpc/errors.js +14 -0
- package/dist/srpc/index.d.ts +1 -1
- package/dist/srpc/index.js +1 -1
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +43 -0
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/Cargo.toml +8 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_e2e_test.cpp +542 -985
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.cpp +3 -5
- package/echo/echo_srpc.pb.go +6 -9
- package/echo/echo_srpc.pb.hpp +23 -3
- package/echo/echo_srpc.pb.rs +61 -26
- package/echo/echo_srpc.py +309 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/half_close_test.rs +178 -0
- package/echo/integration_server.rs +14 -21
- package/echo/main.rs +28 -48
- package/go.mod +3 -3
- package/go.sum +6 -8
- package/integration/cross-language/cpp-client.cpp +446 -402
- package/integration/cross-language/cpp-pipe-server.cpp +139 -0
- package/integration/cross-language/cpp-pipe_test.go +114 -0
- package/integration/cross-language/cpp-server.cpp +222 -186
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +10 -9
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +19 -5
- package/srpc/CMakeLists.txt +3 -58
- package/srpc/README-cpp.md +69 -0
- package/srpc/__init__.py +0 -0
- package/srpc/accept.go +10 -4
- package/srpc/accept_test.go +25 -0
- package/srpc/build.rs +129 -34
- package/srpc/client-half-close_test.go +152 -0
- package/srpc/client-invoker.go +32 -42
- package/srpc/client-invoker_test.go +53 -0
- package/srpc/client-rpc.cpp +65 -80
- package/srpc/client-rpc.go +32 -33
- package/srpc/client-rpc.hpp +52 -38
- package/srpc/client-rpc.test.ts +67 -0
- package/srpc/client-rpc.ts +10 -0
- package/srpc/client-rpc_test.go +76 -0
- package/srpc/client.cpp +99 -87
- package/srpc/client.hpp +46 -35
- package/srpc/client.rs +78 -38
- package/srpc/client.ts +20 -4
- package/srpc/client_half_close_test.cc +331 -0
- package/srpc/client_invoker.rs +75 -0
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.cpp +186 -146
- package/srpc/common-rpc.go +5 -4
- package/srpc/common-rpc.hpp +119 -89
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/common-rpc.ts +43 -8
- package/srpc/common-rpc_test.go +23 -0
- package/srpc/error.rs +19 -2
- package/srpc/errors.hpp +80 -50
- package/srpc/errors.ts +16 -0
- package/srpc/handler.hpp +9 -7
- package/srpc/handler.rs +7 -45
- package/srpc/index.ts +2 -0
- package/srpc/invoker.hpp +82 -55
- package/srpc/invoker.rs +14 -52
- package/srpc/lib.rs +2 -0
- package/srpc/message.hpp +17 -12
- package/srpc/msg-stream.go +1 -0
- package/srpc/msg-stream.hpp +108 -61
- package/srpc/mux.cpp +84 -84
- package/srpc/mux.hpp +48 -38
- package/srpc/mux.rs +4 -4
- package/srpc/net.go +12 -3
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.cpp +60 -58
- package/srpc/packet.hpp +35 -20
- package/srpc/packet.rs +8 -27
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpc.rs +332 -115
- package/srpc/rpc_lifecycle_test.cc +333 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-cancel_test.go +39 -0
- package/srpc/server-rpc.cpp +83 -73
- package/srpc/server-rpc.go +21 -19
- package/srpc/server-rpc.hpp +56 -37
- package/srpc/server.rs +76 -131
- package/srpc/server.test.ts +50 -0
- package/srpc/server.ts +22 -6
- package/srpc/starpc.hpp +4 -2
- package/srpc/stream.go +4 -0
- package/srpc/stream.hpp +95 -34
- package/srpc/stream.rs +7 -2
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
- package/srpc/testing.rs +11 -66
- package/srpc/transport.rs +21 -71
- package/srpc/websocket.rs +106 -57
- package/srpc/writer.hpp +46 -32
- package/srpc/yamux.rs +82 -26
- package/srpc/tests/integration_test.rs +0 -495
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from google.protobuf import empty_pb2 as _google_protobuf_empty_pb2
|
|
8
|
+
|
|
9
|
+
from echo import (
|
|
10
|
+
echo_pb2 as _github_com_aperturerobotics_starpc_echo_echo_pb2,
|
|
11
|
+
)
|
|
12
|
+
from rpcstream import (
|
|
13
|
+
rpcstream_pb2 as _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2,
|
|
14
|
+
)
|
|
15
|
+
from starpc.call import Call, CallProtocolError
|
|
16
|
+
from starpc.client import Client
|
|
17
|
+
from starpc.server import ServiceRegistry
|
|
18
|
+
from starpc.service import MethodDescriptor, ServiceDescriptor, bidirectional_bytes
|
|
19
|
+
|
|
20
|
+
ECHOER_SERVICE = ServiceDescriptor(
|
|
21
|
+
"echo.Echoer",
|
|
22
|
+
(
|
|
23
|
+
MethodDescriptor(
|
|
24
|
+
"Echo",
|
|
25
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
26
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
27
|
+
False,
|
|
28
|
+
False,
|
|
29
|
+
),
|
|
30
|
+
MethodDescriptor(
|
|
31
|
+
"EchoServerStream",
|
|
32
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
33
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
34
|
+
False,
|
|
35
|
+
True,
|
|
36
|
+
),
|
|
37
|
+
MethodDescriptor(
|
|
38
|
+
"EchoClientStream",
|
|
39
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
40
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
41
|
+
True,
|
|
42
|
+
False,
|
|
43
|
+
),
|
|
44
|
+
MethodDescriptor(
|
|
45
|
+
"EchoBidiStream",
|
|
46
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
47
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
|
|
48
|
+
True,
|
|
49
|
+
True,
|
|
50
|
+
),
|
|
51
|
+
MethodDescriptor(
|
|
52
|
+
"RpcStream",
|
|
53
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket,
|
|
54
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket,
|
|
55
|
+
True,
|
|
56
|
+
True,
|
|
57
|
+
),
|
|
58
|
+
MethodDescriptor(
|
|
59
|
+
"DoNothing",
|
|
60
|
+
_google_protobuf_empty_pb2.Empty,
|
|
61
|
+
_google_protobuf_empty_pb2.Empty,
|
|
62
|
+
False,
|
|
63
|
+
False,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EchoerClient:
|
|
70
|
+
def __init__(self, client: Client, service: str | None = None) -> None:
|
|
71
|
+
self._client = client
|
|
72
|
+
self._service = service or "echo.Echoer"
|
|
73
|
+
|
|
74
|
+
async def echo(
|
|
75
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
76
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg:
|
|
77
|
+
call = await self._client.open_call(
|
|
78
|
+
self._service, "Echo", request.SerializeToString(deterministic=True)
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
data = await call.receive()
|
|
82
|
+
if data is None:
|
|
83
|
+
raise CallProtocolError("missing unary response")
|
|
84
|
+
response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
85
|
+
response.ParseFromString(data)
|
|
86
|
+
if await call.receive() is not None:
|
|
87
|
+
raise CallProtocolError("extra unary response")
|
|
88
|
+
return response
|
|
89
|
+
finally:
|
|
90
|
+
await call.aclose()
|
|
91
|
+
|
|
92
|
+
async def echo_server_stream(
|
|
93
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
94
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]:
|
|
95
|
+
call = await self._client.open_call(
|
|
96
|
+
self._service,
|
|
97
|
+
"EchoServerStream",
|
|
98
|
+
request.SerializeToString(deterministic=True),
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
while True:
|
|
102
|
+
data = await call.receive()
|
|
103
|
+
if data is None:
|
|
104
|
+
return
|
|
105
|
+
response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
106
|
+
response.ParseFromString(data)
|
|
107
|
+
yield response
|
|
108
|
+
finally:
|
|
109
|
+
await call.aclose()
|
|
110
|
+
|
|
111
|
+
async def echo_client_stream(
|
|
112
|
+
self,
|
|
113
|
+
requests: AsyncIterable[
|
|
114
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
115
|
+
],
|
|
116
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg:
|
|
117
|
+
call = await self._client.open_call(self._service, "EchoClientStream")
|
|
118
|
+
try:
|
|
119
|
+
async for request in requests:
|
|
120
|
+
await call.send(request.SerializeToString(deterministic=True))
|
|
121
|
+
# A failed half-close means the call ended; receive reports its outcome.
|
|
122
|
+
with contextlib.suppress(OSError):
|
|
123
|
+
await call.finish()
|
|
124
|
+
data = await call.receive()
|
|
125
|
+
if data is None:
|
|
126
|
+
raise CallProtocolError("missing client-stream response")
|
|
127
|
+
response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
128
|
+
response.ParseFromString(data)
|
|
129
|
+
if await call.receive() is not None:
|
|
130
|
+
raise CallProtocolError("extra client-stream response")
|
|
131
|
+
return response
|
|
132
|
+
finally:
|
|
133
|
+
await call.aclose()
|
|
134
|
+
|
|
135
|
+
async def echo_bidi_stream(
|
|
136
|
+
self,
|
|
137
|
+
requests: AsyncIterable[
|
|
138
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
139
|
+
],
|
|
140
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]:
|
|
141
|
+
call = await self._client.open_call(self._service, "EchoBidiStream")
|
|
142
|
+
|
|
143
|
+
async def encoded() -> AsyncIterator[bytes]:
|
|
144
|
+
async for request in requests:
|
|
145
|
+
yield request.SerializeToString(deterministic=True)
|
|
146
|
+
|
|
147
|
+
async for data in bidirectional_bytes(call, encoded()):
|
|
148
|
+
response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
149
|
+
response.ParseFromString(data)
|
|
150
|
+
yield response
|
|
151
|
+
|
|
152
|
+
async def rpc_stream(
|
|
153
|
+
self,
|
|
154
|
+
requests: AsyncIterable[
|
|
155
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
156
|
+
],
|
|
157
|
+
) -> AsyncIterator[
|
|
158
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
159
|
+
]:
|
|
160
|
+
call = await self._client.open_call(self._service, "RpcStream")
|
|
161
|
+
|
|
162
|
+
async def encoded() -> AsyncIterator[bytes]:
|
|
163
|
+
async for request in requests:
|
|
164
|
+
yield request.SerializeToString(deterministic=True)
|
|
165
|
+
|
|
166
|
+
async for data in bidirectional_bytes(call, encoded()):
|
|
167
|
+
response = _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket()
|
|
168
|
+
response.ParseFromString(data)
|
|
169
|
+
yield response
|
|
170
|
+
|
|
171
|
+
async def do_nothing(
|
|
172
|
+
self, request: _google_protobuf_empty_pb2.Empty
|
|
173
|
+
) -> _google_protobuf_empty_pb2.Empty:
|
|
174
|
+
call = await self._client.open_call(
|
|
175
|
+
self._service, "DoNothing", request.SerializeToString(deterministic=True)
|
|
176
|
+
)
|
|
177
|
+
try:
|
|
178
|
+
data = await call.receive()
|
|
179
|
+
if data is None:
|
|
180
|
+
raise CallProtocolError("missing unary response")
|
|
181
|
+
response = _google_protobuf_empty_pb2.Empty()
|
|
182
|
+
response.ParseFromString(data)
|
|
183
|
+
if await call.receive() is not None:
|
|
184
|
+
raise CallProtocolError("extra unary response")
|
|
185
|
+
return response
|
|
186
|
+
finally:
|
|
187
|
+
await call.aclose()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class EchoerServer(Protocol):
|
|
191
|
+
async def echo(
|
|
192
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
193
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
194
|
+
def echo_server_stream(
|
|
195
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
196
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
197
|
+
async def echo_client_stream(
|
|
198
|
+
self,
|
|
199
|
+
requests: AsyncIterator[
|
|
200
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
201
|
+
],
|
|
202
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
203
|
+
def echo_bidi_stream(
|
|
204
|
+
self,
|
|
205
|
+
requests: AsyncIterator[
|
|
206
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
207
|
+
],
|
|
208
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
209
|
+
def rpc_stream(
|
|
210
|
+
self,
|
|
211
|
+
requests: AsyncIterator[
|
|
212
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
213
|
+
],
|
|
214
|
+
) -> AsyncIterator[
|
|
215
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
216
|
+
]: ...
|
|
217
|
+
async def do_nothing(
|
|
218
|
+
self, request: _google_protobuf_empty_pb2.Empty
|
|
219
|
+
) -> _google_protobuf_empty_pb2.Empty: ...
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def register_echoer(
|
|
223
|
+
registry: ServiceRegistry,
|
|
224
|
+
implementation: EchoerServer,
|
|
225
|
+
service: str = "echo.Echoer",
|
|
226
|
+
) -> None:
|
|
227
|
+
async def echo_handler(call: Call) -> None:
|
|
228
|
+
first = await call.receive()
|
|
229
|
+
if first is None:
|
|
230
|
+
raise CallProtocolError("missing initial request")
|
|
231
|
+
request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
232
|
+
request.ParseFromString(first)
|
|
233
|
+
response = await implementation.echo(request)
|
|
234
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
235
|
+
|
|
236
|
+
registry.register(service, "Echo", echo_handler)
|
|
237
|
+
|
|
238
|
+
async def echo_server_stream_handler(call: Call) -> None:
|
|
239
|
+
first = await call.receive()
|
|
240
|
+
if first is None:
|
|
241
|
+
raise CallProtocolError("missing initial request")
|
|
242
|
+
request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
243
|
+
request.ParseFromString(first)
|
|
244
|
+
async for response in implementation.echo_server_stream(request):
|
|
245
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
246
|
+
|
|
247
|
+
registry.register(service, "EchoServerStream", echo_server_stream_handler)
|
|
248
|
+
|
|
249
|
+
async def echo_client_stream_handler(call: Call) -> None:
|
|
250
|
+
async def requests() -> AsyncIterator[
|
|
251
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
252
|
+
]:
|
|
253
|
+
while True:
|
|
254
|
+
data = await call.receive()
|
|
255
|
+
if data is None:
|
|
256
|
+
return
|
|
257
|
+
request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
258
|
+
request.ParseFromString(data)
|
|
259
|
+
yield request
|
|
260
|
+
|
|
261
|
+
response = await implementation.echo_client_stream(requests())
|
|
262
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
263
|
+
|
|
264
|
+
registry.register(service, "EchoClientStream", echo_client_stream_handler)
|
|
265
|
+
|
|
266
|
+
async def echo_bidi_stream_handler(call: Call) -> None:
|
|
267
|
+
async def requests() -> AsyncIterator[
|
|
268
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
269
|
+
]:
|
|
270
|
+
while True:
|
|
271
|
+
data = await call.receive()
|
|
272
|
+
if data is None:
|
|
273
|
+
return
|
|
274
|
+
request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
|
|
275
|
+
request.ParseFromString(data)
|
|
276
|
+
yield request
|
|
277
|
+
|
|
278
|
+
async for response in implementation.echo_bidi_stream(requests()):
|
|
279
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
280
|
+
|
|
281
|
+
registry.register(service, "EchoBidiStream", echo_bidi_stream_handler)
|
|
282
|
+
|
|
283
|
+
async def rpc_stream_handler(call: Call) -> None:
|
|
284
|
+
async def requests() -> AsyncIterator[
|
|
285
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
286
|
+
]:
|
|
287
|
+
while True:
|
|
288
|
+
data = await call.receive()
|
|
289
|
+
if data is None:
|
|
290
|
+
return
|
|
291
|
+
request = _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket()
|
|
292
|
+
request.ParseFromString(data)
|
|
293
|
+
yield request
|
|
294
|
+
|
|
295
|
+
async for response in implementation.rpc_stream(requests()):
|
|
296
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
297
|
+
|
|
298
|
+
registry.register(service, "RpcStream", rpc_stream_handler)
|
|
299
|
+
|
|
300
|
+
async def do_nothing_handler(call: Call) -> None:
|
|
301
|
+
first = await call.receive()
|
|
302
|
+
if first is None:
|
|
303
|
+
raise CallProtocolError("missing initial request")
|
|
304
|
+
request = _google_protobuf_empty_pb2.Empty()
|
|
305
|
+
request.ParseFromString(first)
|
|
306
|
+
response = await implementation.do_nothing(request)
|
|
307
|
+
await call.send(response.SerializeToString(deterministic=True))
|
|
308
|
+
|
|
309
|
+
registry.register(service, "DoNothing", do_nothing_handler)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from google.protobuf import empty_pb2 as _google_protobuf_empty_pb2
|
|
5
|
+
|
|
6
|
+
from echo import (
|
|
7
|
+
echo_pb2 as _github_com_aperturerobotics_starpc_echo_echo_pb2,
|
|
8
|
+
)
|
|
9
|
+
from rpcstream import (
|
|
10
|
+
rpcstream_pb2 as _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2,
|
|
11
|
+
)
|
|
12
|
+
from starpc.client import Client
|
|
13
|
+
from starpc.server import ServiceRegistry
|
|
14
|
+
from starpc.service import ServiceDescriptor
|
|
15
|
+
|
|
16
|
+
ECHOER_SERVICE: ServiceDescriptor
|
|
17
|
+
|
|
18
|
+
class EchoerClient:
|
|
19
|
+
def __init__(self, client: Client, service: str | None = None) -> None: ...
|
|
20
|
+
async def echo(
|
|
21
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
22
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
23
|
+
def echo_server_stream(
|
|
24
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
25
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
26
|
+
async def echo_client_stream(
|
|
27
|
+
self,
|
|
28
|
+
requests: AsyncIterable[
|
|
29
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
30
|
+
],
|
|
31
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
32
|
+
def echo_bidi_stream(
|
|
33
|
+
self,
|
|
34
|
+
requests: AsyncIterable[
|
|
35
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
36
|
+
],
|
|
37
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
38
|
+
def rpc_stream(
|
|
39
|
+
self,
|
|
40
|
+
requests: AsyncIterable[
|
|
41
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
42
|
+
],
|
|
43
|
+
) -> AsyncIterator[
|
|
44
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
45
|
+
]: ...
|
|
46
|
+
async def do_nothing(
|
|
47
|
+
self, request: _google_protobuf_empty_pb2.Empty
|
|
48
|
+
) -> _google_protobuf_empty_pb2.Empty: ...
|
|
49
|
+
|
|
50
|
+
class EchoerServer(Protocol):
|
|
51
|
+
async def echo(
|
|
52
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
53
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
54
|
+
def echo_server_stream(
|
|
55
|
+
self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
56
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
57
|
+
async def echo_client_stream(
|
|
58
|
+
self,
|
|
59
|
+
requests: AsyncIterator[
|
|
60
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
61
|
+
],
|
|
62
|
+
) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
|
|
63
|
+
def echo_bidi_stream(
|
|
64
|
+
self,
|
|
65
|
+
requests: AsyncIterator[
|
|
66
|
+
_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
|
|
67
|
+
],
|
|
68
|
+
) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
|
|
69
|
+
def rpc_stream(
|
|
70
|
+
self,
|
|
71
|
+
requests: AsyncIterator[
|
|
72
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
73
|
+
],
|
|
74
|
+
) -> AsyncIterator[
|
|
75
|
+
_github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
|
|
76
|
+
]: ...
|
|
77
|
+
async def do_nothing(
|
|
78
|
+
self, request: _google_protobuf_empty_pb2.Empty
|
|
79
|
+
) -> _google_protobuf_empty_pb2.Empty: ...
|
|
80
|
+
|
|
81
|
+
def register_echoer(
|
|
82
|
+
registry: ServiceRegistry,
|
|
83
|
+
implementation: EchoerServer,
|
|
84
|
+
service: str = "echo.Echoer",
|
|
85
|
+
) -> None: ...
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//! Half-close tests for the generated Rust callers.
|
|
2
|
+
//!
|
|
3
|
+
//! The half-close is only a notice once the request is written: a caller that
|
|
4
|
+
//! half-closes after the remote ended the call must still read the buffered
|
|
5
|
+
//! responses and the call's outcome.
|
|
6
|
+
|
|
7
|
+
#[allow(dead_code)]
|
|
8
|
+
mod gen;
|
|
9
|
+
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
|
|
12
|
+
use async_trait::async_trait;
|
|
13
|
+
use futures::StreamExt as _;
|
|
14
|
+
use prost::Message;
|
|
15
|
+
use starpc::client::OpenStream;
|
|
16
|
+
use starpc::testing::{create_pipe_default, create_test_pair, SingleInMemoryOpener};
|
|
17
|
+
use starpc::{Client, Error, Mux, PacketCodec, Result, Server, SrpcClient, Stream, StreamExt};
|
|
18
|
+
use tokio::task::JoinHandle;
|
|
19
|
+
use tokio_util::codec::FramedRead;
|
|
20
|
+
|
|
21
|
+
use gen::{EchoMsg, EchoerClient, EchoerClientImpl, EchoerHandler, EchoerServer};
|
|
22
|
+
|
|
23
|
+
const BODY_TXT: &str = "hello world via starpc half-close test";
|
|
24
|
+
|
|
25
|
+
/// Answers each streaming call once and completes it.
|
|
26
|
+
struct OnceEchoServer;
|
|
27
|
+
|
|
28
|
+
#[async_trait]
|
|
29
|
+
impl EchoerServer for OnceEchoServer {
|
|
30
|
+
async fn echo(&self, _context: &starpc::Context, request: EchoMsg) -> Result<EchoMsg> {
|
|
31
|
+
Ok(request)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async fn echo_server_stream(&self, request: EchoMsg, stream: Box<dyn Stream>) -> Result<()> {
|
|
35
|
+
stream.msg_send(&request).await
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async fn echo_client_stream(&self, stream: &dyn Stream) -> Result<EchoMsg> {
|
|
39
|
+
// Reply to the first message without waiting for the half-close.
|
|
40
|
+
stream.msg_recv().await
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async fn echo_bidi_stream(&self, _stream: Box<dyn Stream>) -> Result<()> {
|
|
44
|
+
Err(Error::Unimplemented)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async fn rpc_stream(&self, _stream: Box<dyn Stream>) -> Result<()> {
|
|
48
|
+
Err(Error::Unimplemented)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async fn do_nothing(
|
|
52
|
+
&self,
|
|
53
|
+
_context: &starpc::Context,
|
|
54
|
+
request: gen::Empty,
|
|
55
|
+
) -> Result<gen::Empty> {
|
|
56
|
+
Ok(request)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Returns each new stream only after its transport has closed, so the
|
|
61
|
+
/// generated caller half-closes after the remote already ended the call.
|
|
62
|
+
struct EndedClient<T: OpenStream> {
|
|
63
|
+
client: SrpcClient<T>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[async_trait]
|
|
67
|
+
impl<T: OpenStream + 'static> Client for EndedClient<T> {
|
|
68
|
+
async fn exec_call<I, O>(&self, service: &str, method: &str, input: &I) -> Result<O>
|
|
69
|
+
where
|
|
70
|
+
I: Message + Send + Sync,
|
|
71
|
+
O: Message + Default,
|
|
72
|
+
{
|
|
73
|
+
self.client.exec_call(service, method, input).await
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async fn new_stream(
|
|
77
|
+
&self,
|
|
78
|
+
service: &str,
|
|
79
|
+
method: &str,
|
|
80
|
+
first_msg: Option<&[u8]>,
|
|
81
|
+
) -> Result<Box<dyn Stream>> {
|
|
82
|
+
let stream = self.client.new_stream(service, method, first_msg).await?;
|
|
83
|
+
// The transport close handler cancels the context after it settles
|
|
84
|
+
// the call's outcome and closes the writer.
|
|
85
|
+
stream.context().cancelled().await;
|
|
86
|
+
Ok(stream)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Returns one transport opener and the server task that its caller must join.
|
|
91
|
+
fn serve_once_echo() -> (SingleInMemoryOpener, JoinHandle<Result<()>>) {
|
|
92
|
+
let mux = Arc::new(Mux::new());
|
|
93
|
+
mux.register(Arc::new(EchoerHandler::new(OnceEchoServer)))
|
|
94
|
+
.unwrap();
|
|
95
|
+
let (opener, server_stream) = create_test_pair();
|
|
96
|
+
let server = Server::with_arc(mux);
|
|
97
|
+
let task = tokio::spawn(async move { server.handle_stream(server_stream).await });
|
|
98
|
+
(opener, task)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Returns a transport that closes after CallStart and the peer task to join after observation.
|
|
102
|
+
fn drop_after_request() -> (SingleInMemoryOpener, JoinHandle<()>) {
|
|
103
|
+
// Use the actual framing boundary to order peer closure after request admission.
|
|
104
|
+
let (client_stream, server_stream) = create_pipe_default();
|
|
105
|
+
let task = tokio::spawn(async move {
|
|
106
|
+
let mut framed = FramedRead::new(server_stream, PacketCodec::new());
|
|
107
|
+
let _ = framed.next().await;
|
|
108
|
+
});
|
|
109
|
+
(SingleInMemoryOpener::new(client_stream), task)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[tokio::test]
|
|
113
|
+
async fn server_stream_completed_before_close_send() {
|
|
114
|
+
// Complete the server stream before the generated caller half-closes.
|
|
115
|
+
let (opener, server) = serve_once_echo();
|
|
116
|
+
let client = EchoerClientImpl::new(EndedClient {
|
|
117
|
+
client: SrpcClient::new(opener),
|
|
118
|
+
});
|
|
119
|
+
let request = EchoMsg {
|
|
120
|
+
body: BODY_TXT.into(),
|
|
121
|
+
};
|
|
122
|
+
let stream = client
|
|
123
|
+
.echo_server_stream(&request)
|
|
124
|
+
.await
|
|
125
|
+
.expect("server stream discarded after remote completion");
|
|
126
|
+
|
|
127
|
+
// Read the buffered response, then the clean completion.
|
|
128
|
+
let msg = stream.recv().await.expect("buffered response lost");
|
|
129
|
+
assert_eq!(msg.body, BODY_TXT);
|
|
130
|
+
let end = stream.recv().await;
|
|
131
|
+
assert!(
|
|
132
|
+
matches!(end, Err(Error::StreamClosed)),
|
|
133
|
+
"expected StreamClosed after completion, got {end:?}"
|
|
134
|
+
);
|
|
135
|
+
server.await.unwrap().unwrap();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[tokio::test]
|
|
139
|
+
async fn client_stream_completed_before_close_send() {
|
|
140
|
+
let (opener, server) = serve_once_echo();
|
|
141
|
+
let client = EchoerClientImpl::new(SrpcClient::new(opener));
|
|
142
|
+
let stream = client.echo_client_stream().await.unwrap();
|
|
143
|
+
|
|
144
|
+
// Send the one request and wait for the call to end.
|
|
145
|
+
let request = EchoMsg {
|
|
146
|
+
body: BODY_TXT.into(),
|
|
147
|
+
};
|
|
148
|
+
stream.send(&request).await.unwrap();
|
|
149
|
+
stream.context().cancelled().await;
|
|
150
|
+
|
|
151
|
+
// The half-close fails, but the reply is still readable.
|
|
152
|
+
let msg = stream.close_and_recv().await.expect("buffered reply lost");
|
|
153
|
+
assert_eq!(msg.body, BODY_TXT);
|
|
154
|
+
server.await.unwrap().unwrap();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[tokio::test]
|
|
158
|
+
async fn server_stream_transport_failure_before_close_send() {
|
|
159
|
+
let (opener, server) = drop_after_request();
|
|
160
|
+
let client = EchoerClientImpl::new(EndedClient {
|
|
161
|
+
client: SrpcClient::new(opener),
|
|
162
|
+
});
|
|
163
|
+
let request = EchoMsg {
|
|
164
|
+
body: BODY_TXT.into(),
|
|
165
|
+
};
|
|
166
|
+
let stream = client
|
|
167
|
+
.echo_server_stream(&request)
|
|
168
|
+
.await
|
|
169
|
+
.expect("server stream discarded after transport failure");
|
|
170
|
+
|
|
171
|
+
// Recv reports that the call ended without a verdict.
|
|
172
|
+
let end = stream.recv().await;
|
|
173
|
+
assert!(
|
|
174
|
+
matches!(end, Err(Error::ClosedBeforeCompletion)),
|
|
175
|
+
"expected ClosedBeforeCompletion, got {end:?}"
|
|
176
|
+
);
|
|
177
|
+
server.await.unwrap();
|
|
178
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
//! Echo transport endpoint used by cross-language integration checks.
|
|
2
|
+
|
|
1
3
|
#[allow(dead_code)]
|
|
2
4
|
mod gen;
|
|
3
5
|
|
|
@@ -9,21 +11,16 @@ use tokio::net::TcpListener;
|
|
|
9
11
|
|
|
10
12
|
use gen::{EchoMsg, EchoerHandler, EchoerServer, Empty};
|
|
11
13
|
|
|
14
|
+
/// Implements each echo call shape through the supplied transport stream.
|
|
12
15
|
struct EchoServerImpl;
|
|
13
16
|
|
|
14
17
|
#[async_trait]
|
|
15
18
|
impl EchoerServer for EchoServerImpl {
|
|
16
|
-
async fn echo(&self, request: EchoMsg) -> Result<EchoMsg> {
|
|
17
|
-
Ok(EchoMsg {
|
|
18
|
-
body: request.body,
|
|
19
|
-
})
|
|
19
|
+
async fn echo(&self, _context: &starpc::Context, request: EchoMsg) -> Result<EchoMsg> {
|
|
20
|
+
Ok(EchoMsg { body: request.body })
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
async fn echo_server_stream(
|
|
23
|
-
&self,
|
|
24
|
-
request: EchoMsg,
|
|
25
|
-
stream: Box<dyn Stream>,
|
|
26
|
-
) -> Result<()> {
|
|
23
|
+
async fn echo_server_stream(&self, request: EchoMsg, stream: Box<dyn Stream>) -> Result<()> {
|
|
27
24
|
for _ in 0..5 {
|
|
28
25
|
let response = EchoMsg {
|
|
29
26
|
body: request.body.clone(),
|
|
@@ -34,10 +31,7 @@ impl EchoerServer for EchoServerImpl {
|
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
async fn echo_client_stream(&self, stream: &dyn Stream) -> Result<EchoMsg> {
|
|
37
|
-
|
|
38
|
-
Ok(msg) => Ok(msg),
|
|
39
|
-
Err(e) => Err(e),
|
|
40
|
-
}
|
|
34
|
+
stream.msg_recv().await
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
async fn echo_bidi_stream(&self, stream: Box<dyn Stream>) -> Result<()> {
|
|
@@ -63,11 +57,12 @@ impl EchoerServer for EchoServerImpl {
|
|
|
63
57
|
Err(Error::Unimplemented)
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
async fn do_nothing(&self, _request: Empty) -> Result<Empty> {
|
|
60
|
+
async fn do_nothing(&self, _context: &starpc::Context, _request: Empty) -> Result<Empty> {
|
|
67
61
|
Ok(Empty {})
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
64
|
|
|
65
|
+
/// Publishes a bound address and keeps accepted connections under the server owner.
|
|
71
66
|
#[tokio::main]
|
|
72
67
|
async fn main() -> Result<()> {
|
|
73
68
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
@@ -77,11 +72,9 @@ async fn main() -> Result<()> {
|
|
|
77
72
|
let mux = Arc::new(Mux::new());
|
|
78
73
|
mux.register(Arc::new(EchoerHandler::new(EchoServerImpl)))?;
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
}
|
|
75
|
+
// The server owns concurrent calls and their cancellation for this listener's lifetime.
|
|
76
|
+
let incoming = futures::stream::unfold(listener, |listener| async move {
|
|
77
|
+
Some((listener.accept().await.map(|(stream, _)| stream), listener))
|
|
78
|
+
});
|
|
79
|
+
Server::with_arc(mux).serve(Box::pin(incoming)).await
|
|
87
80
|
}
|