starpc 0.51.0 → 0.52.1
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/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/echo/echo_srpc.pb.d.ts +44 -1
- package/dist/echo/server.d.ts +9 -8
- package/dist/echo/server.js +6 -6
- 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/mock/mock_srpc.pb.d.ts +14 -1
- 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/channel.js +6 -3
- package/dist/srpc/channel.test.js +20 -1
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/handler.d.ts +12 -3
- package/dist/srpc/index.d.ts +2 -0
- package/dist/srpc/index.js +1 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- 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-context.d.ts +11 -0
- package/dist/srpc/server-context.js +28 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +78 -6
- 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/srpc/watchdog.test.js +1 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.ts +74 -0
- package/echo/echo_srpc.py +306 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/server.ts +24 -5
- package/go.mod +2 -2
- package/go.sum +14 -0
- 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.ts +19 -1
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +20 -6
- package/srpc/__init__.py +0 -0
- package/srpc/channel.test.ts +21 -1
- package/srpc/channel.ts +7 -3
- package/srpc/client.ts +20 -4
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/handler.ts +54 -4
- package/srpc/index.ts +7 -0
- package/srpc/invoker.ts +23 -6
- 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.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-context.ts +55 -0
- package/srpc/server-rpc.ts +4 -1
- package/srpc/server.test.ts +100 -5
- package/srpc/server.ts +22 -6
- 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/watchdog.test.ts +1 -0
|
@@ -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: ...
|
package/echo/server.ts
CHANGED
|
@@ -3,14 +3,15 @@ import { Message } from '@aptre/protobuf-es-lite'
|
|
|
3
3
|
import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty'
|
|
4
4
|
import { EchoMsg } from './echo.pb.js'
|
|
5
5
|
import { Server } from '../srpc/server.js'
|
|
6
|
+
import type { ServerContext } from '../srpc/server-context.js'
|
|
6
7
|
import { messagePushable, writeToPushable } from '../srpc/pushable.js'
|
|
7
8
|
import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js'
|
|
8
9
|
import { MessageStream } from '../srpc/message.js'
|
|
9
10
|
import { handleRpcStream, RpcStreamHandler } from '../rpcstream/rpcstream.js'
|
|
10
|
-
import {
|
|
11
|
+
import type { EchoerHandler } from './echo_srpc.pb.js'
|
|
11
12
|
|
|
12
13
|
// EchoServer implements the Echoer server.
|
|
13
|
-
export class EchoerServer implements
|
|
14
|
+
export class EchoerServer implements EchoerHandler {
|
|
14
15
|
// proxyServer is the server used for RpcStream requests.
|
|
15
16
|
private proxyServer?: Server
|
|
16
17
|
|
|
@@ -18,11 +19,19 @@ export class EchoerServer implements Echoer {
|
|
|
18
19
|
this.proxyServer = proxyServer
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
public async Echo(
|
|
22
|
+
public async Echo(
|
|
23
|
+
request: EchoMsg,
|
|
24
|
+
_abortSignal: AbortSignal,
|
|
25
|
+
_context: ServerContext,
|
|
26
|
+
): Promise<Message<EchoMsg>> {
|
|
22
27
|
return request
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
public async *EchoServerStream(
|
|
30
|
+
public async *EchoServerStream(
|
|
31
|
+
request: EchoMsg,
|
|
32
|
+
_abortSignal: AbortSignal,
|
|
33
|
+
_context: ServerContext,
|
|
34
|
+
): MessageStream<EchoMsg> {
|
|
26
35
|
for (let i = 0; i < 5; i++) {
|
|
27
36
|
yield request
|
|
28
37
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
@@ -31,6 +40,8 @@ export class EchoerServer implements Echoer {
|
|
|
31
40
|
|
|
32
41
|
public async EchoClientStream(
|
|
33
42
|
request: MessageStream<EchoMsg>,
|
|
43
|
+
_abortSignal: AbortSignal,
|
|
44
|
+
_context: ServerContext,
|
|
34
45
|
): Promise<Message<EchoMsg>> {
|
|
35
46
|
// return the first message sent by the client.
|
|
36
47
|
const message = await first(request)
|
|
@@ -42,6 +53,8 @@ export class EchoerServer implements Echoer {
|
|
|
42
53
|
|
|
43
54
|
public EchoBidiStream(
|
|
44
55
|
request: MessageStream<EchoMsg>,
|
|
56
|
+
_abortSignal: AbortSignal,
|
|
57
|
+
_context: ServerContext,
|
|
45
58
|
): MessageStream<EchoMsg> {
|
|
46
59
|
// build result observable
|
|
47
60
|
const result = messagePushable<EchoMsg>()
|
|
@@ -52,6 +65,8 @@ export class EchoerServer implements Echoer {
|
|
|
52
65
|
|
|
53
66
|
public RpcStream(
|
|
54
67
|
request: MessageStream<RpcStreamPacket>,
|
|
68
|
+
_abortSignal: AbortSignal,
|
|
69
|
+
_context: ServerContext,
|
|
55
70
|
): MessageStream<RpcStreamPacket> {
|
|
56
71
|
return handleRpcStream(
|
|
57
72
|
request[Symbol.asyncIterator](),
|
|
@@ -64,7 +79,11 @@ export class EchoerServer implements Echoer {
|
|
|
64
79
|
)
|
|
65
80
|
}
|
|
66
81
|
|
|
67
|
-
public async DoNothing(
|
|
82
|
+
public async DoNothing(
|
|
83
|
+
_request: Empty,
|
|
84
|
+
_abortSignal: AbortSignal,
|
|
85
|
+
_context: ServerContext,
|
|
86
|
+
): Promise<Empty> {
|
|
68
87
|
return {}
|
|
69
88
|
}
|
|
70
89
|
}
|
package/go.mod
CHANGED
|
@@ -3,7 +3,7 @@ module github.com/aperturerobotics/starpc
|
|
|
3
3
|
go 1.25.0
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
-
github.com/aperturerobotics/common v0.35.0 // latest
|
|
6
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd // latest
|
|
7
7
|
github.com/aperturerobotics/protobuf-go-lite v0.16.0 // latest
|
|
8
8
|
github.com/aperturerobotics/util v1.34.9 // latest
|
|
9
9
|
)
|
|
@@ -12,7 +12,7 @@ require (
|
|
|
12
12
|
github.com/aperturerobotics/abseil-cpp v0.0.0-20260131110040-4bb56e2f9017 // aperture-2
|
|
13
13
|
github.com/aperturerobotics/cli v1.1.0 // indirect
|
|
14
14
|
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b // indirect
|
|
15
|
-
github.com/aperturerobotics/go-protoc-wasi v0.0.0-
|
|
15
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f // indirect
|
|
16
16
|
github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1 // master
|
|
17
17
|
github.com/aperturerobotics/json-iterator-lite v1.1.0 // indirect
|
|
18
18
|
github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4 // wasi
|
package/go.sum
CHANGED
|
@@ -6,10 +6,24 @@ github.com/aperturerobotics/common v0.34.4 h1:RQTOm6LxEnucXkED/kruhKKAOL6tbaPzMg
|
|
|
6
6
|
github.com/aperturerobotics/common v0.34.4/go.mod h1:xnb1VBRs3x2o1PaKg0OlP7SqDaC9Hmrmt9xH85rOS+A=
|
|
7
7
|
github.com/aperturerobotics/common v0.35.0 h1:VU4OPMXJyqT37lTOtNEMa3QLW5tR8YIBAKbjgxSzuZs=
|
|
8
8
|
github.com/aperturerobotics/common v0.35.0/go.mod h1:xnb1VBRs3x2o1PaKg0OlP7SqDaC9Hmrmt9xH85rOS+A=
|
|
9
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808094909-49d3232b4f83 h1:QkFoyeJ0QBw21a+1YR0kB+JaBSY2mHFiIGiFwWUJ/wQ=
|
|
10
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808094909-49d3232b4f83/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
11
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808095531-8f6fd4367389 h1:J/vc+AKFh8hIVJJ2LrAJVwEgIYvFeQ5XANg3dK3OnYk=
|
|
12
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808095531-8f6fd4367389/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
13
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808100945-fe2b561bec93 h1:Bc7ThfLt5ekHZmOH9vGW0PiiuVUl1R+vneFH/G0j5D4=
|
|
14
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808100945-fe2b561bec93/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
15
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808101203-caeb6303ba93 h1:JoERBAX9HuOzopNLZQCT851YnYz9h9nYSpEe6XjcP0o=
|
|
16
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808101203-caeb6303ba93/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
17
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808102028-33a08259dd38 h1:c3IXUqJHt1EAlCyD0d0IrI7YYf4txwICtoh+ylQV6tU=
|
|
18
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808102028-33a08259dd38/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
19
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd h1:BTfB1V2X+u3/56negSjrx+AWg+Uk84rFFryvpGTj7f4=
|
|
20
|
+
github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
|
|
9
21
|
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b h1:MMk+AbWPCMfqc2Lm2bJl8lCCsuvlM2+C0CP10fpE3bc=
|
|
10
22
|
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b/go.mod h1:OBb/beWmr/pDIZAUfi86j/4tBh2v5ctTxKMqSnh9c/4=
|
|
11
23
|
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260712054757-d8078c296c17 h1:WT1YmZjJf8fUF1zKbeNNKXFA+t8Wtbr8AVMAmdKcrUU=
|
|
12
24
|
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260712054757-d8078c296c17/go.mod h1:vEq8i7EKb32+KXGtIEZjjhNns+BdsL2dUMw4uhy3578=
|
|
25
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f h1:UszdpmKBfQd0LdOZjypeRGrHz2tcPpmKpw6SV3DYyN0=
|
|
26
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f/go.mod h1:vEq8i7EKb32+KXGtIEZjjhNns+BdsL2dUMw4uhy3578=
|
|
13
27
|
github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1 h1:q2qkdNOrnhAFf6B7TBeUoAYstYWxPvozN+3vJi2jgp0=
|
|
14
28
|
github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1/go.mod h1:9KnSGuqxSXbdB/Oi0I6vvfPLkclfJwMGAQaDDGVgGow=
|
|
15
29
|
github.com/aperturerobotics/json-iterator-lite v1.1.0 h1:ZLLqHhHTKYlmCAP873ras2e/yVU/THtwC+ji3tXLMMg=
|
|
@@ -6,19 +6,23 @@ import (
|
|
|
6
6
|
"io"
|
|
7
7
|
"net"
|
|
8
8
|
"os"
|
|
9
|
+
"strings"
|
|
9
10
|
|
|
10
11
|
"github.com/aperturerobotics/starpc/echo"
|
|
12
|
+
"github.com/aperturerobotics/starpc/rpcstream"
|
|
11
13
|
"github.com/aperturerobotics/starpc/srpc"
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
const bodyTxt = "hello world via starpc cross-language e2e test"
|
|
15
17
|
|
|
16
18
|
func main() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
nested := len(os.Args) >= 3 && os.Args[1] == "--nested"
|
|
20
|
+
nestedRelease := len(os.Args) >= 3 && os.Args[1] == "--nested-release"
|
|
21
|
+
if len(os.Args) != 2 && !nested && !nestedRelease {
|
|
22
|
+
fmt.Fprintf(os.Stderr, "usage: go-client [--nested] <addr>\n")
|
|
19
23
|
os.Exit(1)
|
|
20
24
|
}
|
|
21
|
-
addr := os.Args[1]
|
|
25
|
+
addr := os.Args[len(os.Args)-1]
|
|
22
26
|
openStream := func(ctx context.Context, msgHandler srpc.PacketDataHandler, closeHandler srpc.CloseHandler) (srpc.PacketWriter, error) {
|
|
23
27
|
conn, err := net.Dial("tcp", addr) //nolint:gosec
|
|
24
28
|
if err != nil {
|
|
@@ -48,6 +52,12 @@ func main() {
|
|
|
48
52
|
fmt.Fprintf(os.Stderr, "bidi stream test failed: %v\n", err)
|
|
49
53
|
os.Exit(1)
|
|
50
54
|
}
|
|
55
|
+
if nested || nestedRelease {
|
|
56
|
+
if err := testNested(ctx, echoClient, nestedRelease); err != nil {
|
|
57
|
+
fmt.Fprintf(os.Stderr, "nested lifecycle test failed: %v\n", err)
|
|
58
|
+
os.Exit(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
51
61
|
fmt.Println("All tests passed.")
|
|
52
62
|
}
|
|
53
63
|
|
|
@@ -143,3 +153,69 @@ func testBidiStream(ctx context.Context, client echo.SRPCEchoerClient) error {
|
|
|
143
153
|
fmt.Println(" PASSED")
|
|
144
154
|
return nil
|
|
145
155
|
}
|
|
156
|
+
|
|
157
|
+
func testNested(ctx context.Context, parent echo.SRPCEchoerClient, release bool) error {
|
|
158
|
+
proxy := rpcstream.NewRpcStreamClient(parent.RpcStream, "test", true)
|
|
159
|
+
if err := testUnary(ctx, echo.NewSRPCEchoerClient(proxy)); err != nil {
|
|
160
|
+
return fmt.Errorf("nested unary: %w", err)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if release {
|
|
164
|
+
missing := rpcstream.NewRpcStreamClient(parent.RpcStream, "missing", true)
|
|
165
|
+
var missingOut echo.EchoMsg
|
|
166
|
+
if err := missing.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{}, &missingOut); err == nil || !strings.Contains(err.Error(), "unknown component: missing") {
|
|
167
|
+
return fmt.Errorf("unknown component returned wrong result: %v", err)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stream, err := proxy.NewStream(ctx, "echo.Echoer", "EchoBidiStream", nil)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return fmt.Errorf("nested bidi: %w", err)
|
|
174
|
+
}
|
|
175
|
+
if err := stream.MsgSend(&echo.EchoMsg{Body: "nested later data"}); err != nil {
|
|
176
|
+
return fmt.Errorf("nested later data: %w", err)
|
|
177
|
+
}
|
|
178
|
+
if err := stream.Close(); err != nil {
|
|
179
|
+
return fmt.Errorf("nested cancel: %w", err)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
var terminal echo.EchoMsg
|
|
183
|
+
if err := proxy.ExecCall(ctx, "missing.Service", "Missing", &echo.EchoMsg{}, &terminal); err == nil || !strings.Contains(err.Error(), "missing.Service") {
|
|
184
|
+
return fmt.Errorf("unknown nested method returned wrong result: %v", err)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if release {
|
|
188
|
+
var terminalError echo.EchoMsg
|
|
189
|
+
if err := proxy.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{Body: "__nested_error__"}, &terminalError); err == nil || !strings.Contains(err.Error(), "nested terminal error") {
|
|
190
|
+
return fmt.Errorf("terminal nested error returned wrong result: %v", err)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if release {
|
|
195
|
+
releaseClient := rpcstream.NewRpcStreamClient(parent.RpcStream, "release", true)
|
|
196
|
+
var released echo.EchoMsg
|
|
197
|
+
err := releaseClient.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{Body: "__nested_release__"}, &released)
|
|
198
|
+
if err == nil || (!strings.Contains(err.Error(), "stream closed before the remote reported completion") && !strings.Contains(err.Error(), "EOF") && !strings.Contains(err.Error(), "canceled")) {
|
|
199
|
+
return fmt.Errorf("release during active call returned wrong result: %v", err)
|
|
200
|
+
}
|
|
201
|
+
status, err := parent.Echo(ctx, &echo.EchoMsg{Body: "__nested_release_status__"})
|
|
202
|
+
if err != nil {
|
|
203
|
+
return fmt.Errorf("release completion status failed: %w", err)
|
|
204
|
+
}
|
|
205
|
+
if status.GetBody() != "released" {
|
|
206
|
+
return fmt.Errorf("release completion returned %q", status.GetBody())
|
|
207
|
+
}
|
|
208
|
+
if err := releaseClient.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{}, &released); err == nil || !strings.Contains(err.Error(), "unknown component: release") {
|
|
209
|
+
return fmt.Errorf("released component returned wrong result: %v", err)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
direct, err := rpcstream.OpenRpcStream(ctx, parent.RpcStream, "test", true)
|
|
214
|
+
if err != nil {
|
|
215
|
+
return fmt.Errorf("nested direct open: %w", err)
|
|
216
|
+
}
|
|
217
|
+
if err := direct.Close(); err != nil {
|
|
218
|
+
return fmt.Errorf("nested abrupt close: %w", err)
|
|
219
|
+
}
|
|
220
|
+
return nil
|
|
221
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# The checkout supplies generated message packages outside the installed wheel.
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from google.protobuf import empty_pb2
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
12
|
+
|
|
13
|
+
from echo import echo_pb2
|
|
14
|
+
from echo.echo_srpc import EchoerClient
|
|
15
|
+
from starpc.call import CallCancelledError, RemoteCallError
|
|
16
|
+
from starpc.client import Client
|
|
17
|
+
from starpc.stream import open_tcp_stream
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def main() -> None:
|
|
21
|
+
args = sys.argv[1:]
|
|
22
|
+
nested = args[:1] in (["--nested"], ["--nested-release"])
|
|
23
|
+
strict_nested = args[:1] == ["--nested-release"]
|
|
24
|
+
if nested:
|
|
25
|
+
args = args[1:]
|
|
26
|
+
if len(args) != 1 or ":" not in args[0]:
|
|
27
|
+
raise ValueError("usage: python-client.py [--nested] host:port")
|
|
28
|
+
host, port_text = args[0].rsplit(":", 1)
|
|
29
|
+
port = int(port_text)
|
|
30
|
+
raw_client = Client(lambda: open_tcp_stream(host, port))
|
|
31
|
+
client = EchoerClient(raw_client)
|
|
32
|
+
body = "hello world via python"
|
|
33
|
+
response = await client.echo(echo_pb2.EchoMsg(body=body))
|
|
34
|
+
if response.body != body:
|
|
35
|
+
raise RuntimeError(f"unexpected unary response: {response.body!r}")
|
|
36
|
+
empty_response = await client.echo(echo_pb2.EchoMsg())
|
|
37
|
+
if empty_response.body:
|
|
38
|
+
raise RuntimeError(f"unexpected empty unary response: {empty_response.body!r}")
|
|
39
|
+
received = [
|
|
40
|
+
message
|
|
41
|
+
async for message in client.echo_server_stream(echo_pb2.EchoMsg(body=body))
|
|
42
|
+
]
|
|
43
|
+
if len(received) != 5 or any(message.body != body for message in received):
|
|
44
|
+
raise RuntimeError("unexpected server-stream response")
|
|
45
|
+
client_stream = await client.echo_client_stream(_one_request(body))
|
|
46
|
+
if client_stream.body != body:
|
|
47
|
+
raise RuntimeError("unexpected client-stream response")
|
|
48
|
+
bidi = client.echo_bidi_stream(_one_request(body))
|
|
49
|
+
initial = await bidi.__anext__()
|
|
50
|
+
if initial.body != "hello from server":
|
|
51
|
+
raise RuntimeError("unexpected bidi initial response")
|
|
52
|
+
echoed = await bidi.__anext__()
|
|
53
|
+
if echoed.body != body:
|
|
54
|
+
raise RuntimeError("unexpected bidi echo")
|
|
55
|
+
try:
|
|
56
|
+
await bidi.__anext__()
|
|
57
|
+
except StopAsyncIteration:
|
|
58
|
+
pass
|
|
59
|
+
else:
|
|
60
|
+
raise RuntimeError("bidi stream did not terminate")
|
|
61
|
+
empty = await client.do_nothing(empty_pb2.Empty())
|
|
62
|
+
if empty.ByteSize() != 0:
|
|
63
|
+
raise RuntimeError("unexpected DoNothing response")
|
|
64
|
+
|
|
65
|
+
unknown = await raw_client.open_call("missing.Service", "Missing")
|
|
66
|
+
try:
|
|
67
|
+
await unknown.finish()
|
|
68
|
+
try:
|
|
69
|
+
await unknown.receive()
|
|
70
|
+
except RemoteCallError:
|
|
71
|
+
pass
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError("unknown method did not return a remote error")
|
|
74
|
+
finally:
|
|
75
|
+
await unknown.aclose()
|
|
76
|
+
|
|
77
|
+
cancelled = await raw_client.open_call("echo.Echoer", "EchoBidiStream")
|
|
78
|
+
await cancelled.cancel()
|
|
79
|
+
try:
|
|
80
|
+
await cancelled.wait_closed()
|
|
81
|
+
except CallCancelledError:
|
|
82
|
+
pass
|
|
83
|
+
await cancelled.aclose()
|
|
84
|
+
if nested:
|
|
85
|
+
await run_nested_test(client, strict_nested)
|
|
86
|
+
print("All tests passed.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def run_nested_test(client: EchoerClient, strict: bool = False) -> None:
|
|
90
|
+
from starpc.rpcstream import (
|
|
91
|
+
RpcStreamRemoteError,
|
|
92
|
+
build_rpc_stream_open_stream,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
nested_client = Client(build_rpc_stream_open_stream("test", client.rpc_stream))
|
|
96
|
+
nested_echo = EchoerClient(nested_client)
|
|
97
|
+
body = "hello world via nested python"
|
|
98
|
+
response = await nested_echo.echo(echo_pb2.EchoMsg(body=body))
|
|
99
|
+
if response.body != body:
|
|
100
|
+
raise RuntimeError(f"unexpected nested unary response: {response.body!r}")
|
|
101
|
+
|
|
102
|
+
if strict:
|
|
103
|
+
try:
|
|
104
|
+
await nested_echo.echo(echo_pb2.EchoMsg(body="__nested_error__"))
|
|
105
|
+
except RemoteCallError:
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError("terminal nested error unexpectedly succeeded")
|
|
109
|
+
|
|
110
|
+
if strict:
|
|
111
|
+
missing = Client(build_rpc_stream_open_stream("missing", client.rpc_stream))
|
|
112
|
+
try:
|
|
113
|
+
await missing.open_call("echo.Echoer", "Echo")
|
|
114
|
+
except RpcStreamRemoteError:
|
|
115
|
+
pass
|
|
116
|
+
else:
|
|
117
|
+
raise RuntimeError("unknown component unexpectedly succeeded")
|
|
118
|
+
|
|
119
|
+
terminal = await nested_client.open_call("missing.Service", "Missing")
|
|
120
|
+
try:
|
|
121
|
+
await terminal.receive()
|
|
122
|
+
except RemoteCallError:
|
|
123
|
+
pass
|
|
124
|
+
else:
|
|
125
|
+
raise RuntimeError("unknown nested method unexpectedly succeeded")
|
|
126
|
+
finally:
|
|
127
|
+
await terminal.aclose()
|
|
128
|
+
|
|
129
|
+
cancelled = await nested_client.open_call("echo.Echoer", "EchoBidiStream")
|
|
130
|
+
await cancelled.send(
|
|
131
|
+
echo_pb2.EchoMsg(body="nested later data").SerializeToString(deterministic=True)
|
|
132
|
+
)
|
|
133
|
+
await cancelled.cancel()
|
|
134
|
+
try:
|
|
135
|
+
await cancelled.wait_closed()
|
|
136
|
+
except CallCancelledError:
|
|
137
|
+
pass
|
|
138
|
+
await cancelled.aclose()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _one_request(body: str) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
142
|
+
yield echo_pb2.EchoMsg(body=body)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# The checkout supplies generated message packages outside the installed wheel.
|
|
4
|
+
import asyncio
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from google.protobuf import empty_pb2
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
13
|
+
|
|
14
|
+
from echo import echo_pb2
|
|
15
|
+
from echo.echo_srpc import register_echoer
|
|
16
|
+
from rpcstream import rpcstream_pb2
|
|
17
|
+
from starpc.rpcstream import ComponentRegistry, handle_rpc_stream
|
|
18
|
+
from starpc.server import Server, ServiceRegistry
|
|
19
|
+
from starpc.stream import TCPStreamServer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Implementation:
|
|
23
|
+
def __init__(self, components: ComponentRegistry) -> None:
|
|
24
|
+
self._components = components
|
|
25
|
+
self._release_task: asyncio.Task[None] | None = None
|
|
26
|
+
self._release_complete = asyncio.Event()
|
|
27
|
+
|
|
28
|
+
async def _release_component(self) -> None:
|
|
29
|
+
try:
|
|
30
|
+
await self._components.unregister("release")
|
|
31
|
+
finally:
|
|
32
|
+
self._release_complete.set()
|
|
33
|
+
|
|
34
|
+
async def wait_for_release(self) -> None:
|
|
35
|
+
task = self._release_task
|
|
36
|
+
if task is not None:
|
|
37
|
+
await task
|
|
38
|
+
|
|
39
|
+
async def echo(self, request: echo_pb2.EchoMsg) -> echo_pb2.EchoMsg:
|
|
40
|
+
if request.body == "__nested_error__":
|
|
41
|
+
raise RuntimeError("nested terminal error")
|
|
42
|
+
if request.body == "__nested_release__":
|
|
43
|
+
if self._release_task is None:
|
|
44
|
+
self._release_task = asyncio.create_task(self._release_component())
|
|
45
|
+
await asyncio.Future()
|
|
46
|
+
if request.body == "__nested_release_status__":
|
|
47
|
+
await self._release_complete.wait()
|
|
48
|
+
await self.wait_for_release()
|
|
49
|
+
return echo_pb2.EchoMsg(body="released")
|
|
50
|
+
return echo_pb2.EchoMsg(body=request.body)
|
|
51
|
+
|
|
52
|
+
async def echo_server_stream(
|
|
53
|
+
self, request: echo_pb2.EchoMsg
|
|
54
|
+
) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
55
|
+
for _ in range(5):
|
|
56
|
+
yield echo_pb2.EchoMsg(body=request.body)
|
|
57
|
+
await asyncio.sleep(0.2)
|
|
58
|
+
|
|
59
|
+
async def echo_client_stream(
|
|
60
|
+
self, requests: AsyncIterator[echo_pb2.EchoMsg]
|
|
61
|
+
) -> echo_pb2.EchoMsg:
|
|
62
|
+
async for request in requests:
|
|
63
|
+
return request
|
|
64
|
+
return echo_pb2.EchoMsg()
|
|
65
|
+
|
|
66
|
+
async def echo_bidi_stream(
|
|
67
|
+
self, requests: AsyncIterator[echo_pb2.EchoMsg]
|
|
68
|
+
) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
69
|
+
yield echo_pb2.EchoMsg(body="hello from server")
|
|
70
|
+
async for request in requests:
|
|
71
|
+
yield request
|
|
72
|
+
|
|
73
|
+
def rpc_stream(
|
|
74
|
+
self, requests: AsyncIterator[rpcstream_pb2.RpcStreamPacket]
|
|
75
|
+
) -> AsyncIterator[rpcstream_pb2.RpcStreamPacket]:
|
|
76
|
+
return handle_rpc_stream(requests, self._components)
|
|
77
|
+
|
|
78
|
+
async def do_nothing(self, request: empty_pb2.Empty) -> empty_pb2.Empty:
|
|
79
|
+
return request
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def main() -> None:
|
|
83
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
|
84
|
+
registry = ServiceRegistry()
|
|
85
|
+
components = ComponentRegistry()
|
|
86
|
+
await components.register("test", Server(registry))
|
|
87
|
+
await components.register("release", Server(registry))
|
|
88
|
+
implementation = Implementation(components)
|
|
89
|
+
register_echoer(registry, implementation)
|
|
90
|
+
server = TCPStreamServer()
|
|
91
|
+
await server.start("127.0.0.1", port)
|
|
92
|
+
host, bound_port = server.address
|
|
93
|
+
print(f"LISTENING {host}:{bound_port}", flush=True)
|
|
94
|
+
rpc_server = Server(registry)
|
|
95
|
+
tasks: set[asyncio.Task[None]] = set()
|
|
96
|
+
errors: list[BaseException] = []
|
|
97
|
+
stopping = asyncio.Event()
|
|
98
|
+
loop = asyncio.get_running_loop()
|
|
99
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
100
|
+
loop.add_signal_handler(sig, stopping.set)
|
|
101
|
+
|
|
102
|
+
def observe_task(task: asyncio.Task[None]) -> None:
|
|
103
|
+
tasks.discard(task)
|
|
104
|
+
if not task.cancelled() and (error := task.exception()) is not None:
|
|
105
|
+
errors.append(error)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
while not stopping.is_set():
|
|
109
|
+
accepting = asyncio.create_task(server.accept())
|
|
110
|
+
stopped = asyncio.create_task(stopping.wait())
|
|
111
|
+
done, _ = await asyncio.wait(
|
|
112
|
+
{accepting, stopped}, return_when=asyncio.FIRST_COMPLETED
|
|
113
|
+
)
|
|
114
|
+
if stopped in done:
|
|
115
|
+
accepting.cancel()
|
|
116
|
+
await asyncio.gather(accepting, return_exceptions=True)
|
|
117
|
+
break
|
|
118
|
+
stopped.cancel()
|
|
119
|
+
await asyncio.gather(stopped, return_exceptions=True)
|
|
120
|
+
stream = accepting.result()
|
|
121
|
+
task = asyncio.create_task(rpc_server.serve(stream))
|
|
122
|
+
tasks.add(task)
|
|
123
|
+
task.add_done_callback(observe_task)
|
|
124
|
+
finally:
|
|
125
|
+
await server.aclose()
|
|
126
|
+
for task in tasks:
|
|
127
|
+
task.cancel()
|
|
128
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
129
|
+
await implementation.wait_for_release()
|
|
130
|
+
await components.unregister("test")
|
|
131
|
+
await components.unregister("release")
|
|
132
|
+
if errors:
|
|
133
|
+
raise ExceptionGroup("cross-language server tasks failed", errors)
|
|
134
|
+
if components._components or components._retired:
|
|
135
|
+
raise RuntimeError("nested component cleanup is incomplete")
|
|
136
|
+
print("NESTED_CLEAN", flush=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
asyncio.run(main())
|