starpc 0.52.1 → 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.
Files changed (91) hide show
  1. package/README.md +37 -8
  2. package/dist/srpc/client-rpc.d.ts +1 -0
  3. package/dist/srpc/client-rpc.js +9 -0
  4. package/dist/srpc/client-rpc.test.d.ts +1 -0
  5. package/dist/srpc/client-rpc.test.js +50 -0
  6. package/dist/srpc/common-rpc.d.ts +5 -0
  7. package/dist/srpc/common-rpc.js +37 -8
  8. package/dist/srpc/errors.d.ts +2 -0
  9. package/dist/srpc/errors.js +14 -0
  10. package/dist/srpc/index.d.ts +1 -1
  11. package/dist/srpc/index.js +1 -1
  12. package/echo/Cargo.toml +8 -0
  13. package/echo/echo_e2e_test.cpp +542 -985
  14. package/echo/echo_srpc.pb.cpp +3 -5
  15. package/echo/echo_srpc.pb.go +6 -9
  16. package/echo/echo_srpc.pb.hpp +23 -3
  17. package/echo/echo_srpc.pb.rs +61 -26
  18. package/echo/echo_srpc.py +4 -1
  19. package/echo/half_close_test.rs +178 -0
  20. package/echo/integration_server.rs +14 -21
  21. package/echo/main.rs +28 -48
  22. package/go.mod +1 -1
  23. package/go.sum +2 -18
  24. package/integration/cross-language/cpp-client.cpp +446 -402
  25. package/integration/cross-language/cpp-pipe-server.cpp +139 -0
  26. package/integration/cross-language/cpp-pipe_test.go +114 -0
  27. package/integration/cross-language/cpp-server.cpp +222 -186
  28. package/mock/mock_srpc.pb.cpp +1 -1
  29. package/mock/mock_srpc.pb.hpp +1 -1
  30. package/mock/mock_srpc.pb.rs +10 -9
  31. package/package.json +1 -1
  32. package/srpc/CMakeLists.txt +3 -58
  33. package/srpc/README-cpp.md +69 -0
  34. package/srpc/accept.go +10 -4
  35. package/srpc/accept_test.go +25 -0
  36. package/srpc/build.rs +129 -34
  37. package/srpc/client-half-close_test.go +152 -0
  38. package/srpc/client-invoker.go +32 -42
  39. package/srpc/client-invoker_test.go +53 -0
  40. package/srpc/client-rpc.cpp +65 -80
  41. package/srpc/client-rpc.go +32 -33
  42. package/srpc/client-rpc.hpp +52 -38
  43. package/srpc/client-rpc.test.ts +67 -0
  44. package/srpc/client-rpc.ts +10 -0
  45. package/srpc/client-rpc_test.go +76 -0
  46. package/srpc/client.cpp +99 -87
  47. package/srpc/client.hpp +46 -35
  48. package/srpc/client.rs +78 -38
  49. package/srpc/client_half_close_test.cc +331 -0
  50. package/srpc/client_invoker.rs +75 -0
  51. package/srpc/common-rpc.cpp +186 -146
  52. package/srpc/common-rpc.go +5 -4
  53. package/srpc/common-rpc.hpp +119 -89
  54. package/srpc/common-rpc.ts +43 -8
  55. package/srpc/common-rpc_test.go +23 -0
  56. package/srpc/error.rs +19 -2
  57. package/srpc/errors.hpp +80 -50
  58. package/srpc/errors.ts +16 -0
  59. package/srpc/handler.hpp +9 -7
  60. package/srpc/handler.rs +7 -45
  61. package/srpc/index.ts +2 -0
  62. package/srpc/invoker.hpp +82 -55
  63. package/srpc/invoker.rs +14 -52
  64. package/srpc/lib.rs +2 -0
  65. package/srpc/message.hpp +17 -12
  66. package/srpc/msg-stream.go +1 -0
  67. package/srpc/msg-stream.hpp +108 -61
  68. package/srpc/mux.cpp +84 -84
  69. package/srpc/mux.hpp +48 -38
  70. package/srpc/mux.rs +4 -4
  71. package/srpc/net.go +12 -3
  72. package/srpc/packet.cpp +60 -58
  73. package/srpc/packet.hpp +35 -20
  74. package/srpc/packet.rs +8 -27
  75. package/srpc/rpc.rs +332 -115
  76. package/srpc/rpc_lifecycle_test.cc +333 -0
  77. package/srpc/server-cancel_test.go +39 -0
  78. package/srpc/server-rpc.cpp +83 -73
  79. package/srpc/server-rpc.go +21 -19
  80. package/srpc/server-rpc.hpp +56 -37
  81. package/srpc/server.rs +76 -131
  82. package/srpc/starpc.hpp +4 -2
  83. package/srpc/stream.go +4 -0
  84. package/srpc/stream.hpp +95 -34
  85. package/srpc/stream.rs +7 -2
  86. package/srpc/testing.rs +11 -66
  87. package/srpc/transport.rs +21 -71
  88. package/srpc/websocket.rs +106 -57
  89. package/srpc/writer.hpp +46 -32
  90. package/srpc/yamux.rs +82 -26
  91. package/srpc/tests/integration_test.rs +0 -495
@@ -0,0 +1,152 @@
1
+ package srpc_test
2
+
3
+ import (
4
+ "context"
5
+ "io"
6
+ "net"
7
+ "testing"
8
+
9
+ "github.com/aperturerobotics/starpc/echo"
10
+ "github.com/aperturerobotics/starpc/srpc"
11
+ "github.com/pkg/errors"
12
+ )
13
+
14
+ // endedClient returns each new stream only after its transport has closed, so
15
+ // the generated caller half-closes after the remote already ended the call.
16
+ type endedClient struct {
17
+ srpc.Client
18
+ // closed is closed after the transport close callback of the one stream
19
+ // this client may open returns.
20
+ closed <-chan struct{}
21
+ }
22
+
23
+ // NewStream opens the stream and waits for its transport to close.
24
+ func (c *endedClient) NewStream(
25
+ ctx context.Context,
26
+ service, method string,
27
+ firstMsg srpc.Message,
28
+ ) (srpc.Stream, error) {
29
+ strm, err := c.Client.NewStream(ctx, service, method, firstMsg)
30
+ if err != nil {
31
+ return nil, err
32
+ }
33
+ <-c.closed
34
+ return strm, nil
35
+ }
36
+
37
+ // onceEchoServer answers a server stream with one message and returns.
38
+ type onceEchoServer struct {
39
+ *echo.EchoServer
40
+ }
41
+
42
+ // EchoServerStream sends the request back once and completes the call.
43
+ func (onceEchoServer) EchoServerStream(msg *echo.EchoMsg, strm echo.SRPCEchoer_EchoServerStreamStream) error {
44
+ return strm.Send(msg)
45
+ }
46
+
47
+ // newEchoPipe serves impl over an in-memory pipe per stream.
48
+ func newEchoPipe(t *testing.T, impl echo.SRPCEchoerServer) srpc.OpenStreamFunc {
49
+ t.Helper()
50
+ mux := srpc.NewMux()
51
+ if err := echo.SRPCRegisterEchoer(mux, impl); err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ return srpc.NewServerPipe(srpc.NewServer(mux))
55
+ }
56
+
57
+ // notifyClose wraps openStream for a single stream and returns a channel
58
+ // closed after that stream's transport close callback returns.
59
+ func notifyClose(openStream srpc.OpenStreamFunc) (srpc.OpenStreamFunc, <-chan struct{}) {
60
+ closed := make(chan struct{})
61
+ return func(
62
+ ctx context.Context,
63
+ msgHandler srpc.PacketDataHandler,
64
+ closeHandler srpc.CloseHandler,
65
+ ) (srpc.PacketWriter, error) {
66
+ return openStream(ctx, msgHandler, func(closeErr error) {
67
+ closeHandler(closeErr)
68
+ close(closed)
69
+ })
70
+ }, closed
71
+ }
72
+
73
+ func TestServerStreamCompletedBeforeCloseSend(t *testing.T) {
74
+ // Complete the server stream before the generated caller half-closes.
75
+ openStream, closed := notifyClose(newEchoPipe(t, onceEchoServer{echo.NewEchoServer(nil)}))
76
+ client := echo.NewSRPCEchoerClient(&endedClient{Client: srpc.NewClient(openStream), closed: closed})
77
+ strm, err := client.EchoServerStream(t.Context(), &echo.EchoMsg{Body: bodyTxt})
78
+ if err != nil {
79
+ t.Fatalf("server stream discarded after remote completion: %v", err)
80
+ }
81
+ t.Cleanup(func() { _ = strm.Close() })
82
+
83
+ // Read the buffered response, then the clean completion.
84
+ msg, err := strm.Recv()
85
+ if err != nil {
86
+ t.Fatalf("buffered response lost: %v", err)
87
+ }
88
+ if msg.GetBody() != bodyTxt {
89
+ t.Fatalf("expected %q got %q", bodyTxt, msg.GetBody())
90
+ }
91
+ if _, err := strm.Recv(); err != io.EOF {
92
+ t.Fatalf("expected io.EOF after completion, got %v", err)
93
+ }
94
+ }
95
+
96
+ func TestClientStreamCompletedBeforeCloseSend(t *testing.T) {
97
+ // EchoClientStream replies to the first message without waiting for the
98
+ // half-close, so the call ends while the client may still send.
99
+ openStream, closed := notifyClose(newEchoPipe(t, echo.NewEchoServer(nil)))
100
+ client := echo.NewSRPCEchoerClient(srpc.NewClient(openStream))
101
+ strm, err := client.EchoClientStream(t.Context())
102
+ if err != nil {
103
+ t.Fatal(err)
104
+ }
105
+ t.Cleanup(func() { _ = strm.Close() })
106
+
107
+ // Send the one request and wait for the call to end.
108
+ if err := strm.Send(&echo.EchoMsg{Body: bodyTxt}); err != nil {
109
+ t.Fatal(err)
110
+ }
111
+ <-closed
112
+
113
+ // The half-close fails, but the reply is still readable.
114
+ msg, err := strm.CloseAndRecv()
115
+ if err != nil {
116
+ t.Fatalf("buffered reply lost: %v", err)
117
+ }
118
+ if msg.GetBody() != bodyTxt {
119
+ t.Fatalf("expected %q got %q", bodyTxt, msg.GetBody())
120
+ }
121
+ }
122
+
123
+ func TestServerStreamTransportFailureBeforeCloseSend(t *testing.T) {
124
+ // Accept CallStart, then drop the transport without a response.
125
+ openStream, closed := notifyClose(func(
126
+ ctx context.Context,
127
+ msgHandler srpc.PacketDataHandler,
128
+ closeHandler srpc.CloseHandler,
129
+ ) (srpc.PacketWriter, error) {
130
+ srvPipe, clientPipe := net.Pipe()
131
+ srvPrw := srpc.NewPacketReadWriter(srvPipe)
132
+ go func() {
133
+ _ = srvPrw.ReadToHandler(func([]byte) error {
134
+ return srvPrw.Close()
135
+ })
136
+ }()
137
+ clientPrw := srpc.NewPacketReadWriter(clientPipe)
138
+ go clientPrw.ReadPump(msgHandler, closeHandler)
139
+ return clientPrw, nil
140
+ })
141
+ client := echo.NewSRPCEchoerClient(&endedClient{Client: srpc.NewClient(openStream), closed: closed})
142
+ strm, err := client.EchoServerStream(t.Context(), &echo.EchoMsg{Body: bodyTxt})
143
+ if err != nil {
144
+ t.Fatalf("server stream discarded after transport failure: %v", err)
145
+ }
146
+ t.Cleanup(func() { _ = strm.Close() })
147
+
148
+ // Recv reports that the call ended without a verdict.
149
+ if _, err := strm.Recv(); !errors.Is(err, srpc.ErrClosedBeforeCompletion) {
150
+ t.Fatalf("expected ErrClosedBeforeCompletion, got %v", err)
151
+ }
152
+ }
@@ -17,65 +17,55 @@ func NewClientInvoker(client Client) *ClientInvoker {
17
17
  }
18
18
 
19
19
  // InvokeMethod invokes the method by proxying to the remote via the client.
20
- // Returns false, nil if the client is nil.
20
+ // The remote's terminal result, including its error, is the result of the
21
+ // call. Returns false, nil if the client is nil.
21
22
  func (c *ClientInvoker) InvokeMethod(serviceID, methodID string, strm Stream) (bool, error) {
22
23
  if c.client == nil {
23
24
  return false, nil
24
25
  }
25
26
 
26
- ctx := strm.Context()
27
-
28
- // Open a stream to the remote
29
- remoteStrm, err := c.client.NewStream(ctx, serviceID, methodID, nil)
27
+ // Open a stream to the remote.
28
+ remoteStrm, err := c.client.NewStream(strm.Context(), serviceID, methodID, nil)
30
29
  if err != nil {
31
30
  return true, err
32
31
  }
33
32
  defer remoteStrm.Close()
34
33
 
35
- // Proxy data between the streams
36
- errCh := make(chan error, 2)
37
- go proxyStreamTo(strm, remoteStrm, errCh)
38
- go proxyStreamTo(remoteStrm, strm, errCh)
34
+ // Forward the caller's messages until it half-closes or fails.
35
+ go forwardRequests(strm, remoteStrm)
39
36
 
40
- // Wait for both directions to complete
41
- var outErr error
42
- for range 2 {
43
- if err := <-errCh; err != nil && outErr == nil && err != io.EOF {
44
- outErr = err
45
- }
37
+ // Forward the remote's messages; the server publishes the result to the
38
+ // caller after this returns.
39
+ return true, forwardMessages(remoteStrm, strm)
40
+ }
41
+
42
+ // forwardRequests copies the caller's messages to the remote, then
43
+ // half-closes the remote, or closes it when the caller fails.
44
+ func forwardRequests(caller, remote Stream) {
45
+ if err := forwardMessages(caller, remote); err != nil {
46
+ _ = remote.Close()
47
+ return
46
48
  }
47
- return true, outErr
49
+ _ = remote.CloseSend()
48
50
  }
49
51
 
50
- // proxyStreamTo copies messages from src to dst.
51
- func proxyStreamTo(src, dst Stream, errCh chan error) {
52
- rerr := func() error {
53
- pkt := NewRawMessage(nil, true)
54
- for {
55
- err := src.MsgRecv(pkt)
56
- if err != nil {
57
- return err
58
- }
59
- // Forward all messages including empty ones (valid for empty proto messages)
60
- err = dst.MsgSend(pkt)
61
- pkt.Clear()
62
- if err != nil {
63
- return err
52
+ // forwardMessages copies messages from src to dst until src ends. It returns
53
+ // nil when src half-closes.
54
+ func forwardMessages(src, dst Stream) error {
55
+ // Forward all messages including empty ones, which are valid empty protos.
56
+ pkt := NewRawMessage(nil, true)
57
+ for {
58
+ if err := src.MsgRecv(pkt); err != nil {
59
+ if err == io.EOF {
60
+ return nil
64
61
  }
62
+ return err
65
63
  }
66
- }()
67
-
68
- if rerr != nil && rerr != io.EOF {
69
- if errCh != nil {
70
- errCh <- rerr
64
+ err := dst.MsgSend(pkt)
65
+ pkt.Clear()
66
+ if err != nil {
67
+ return err
71
68
  }
72
- _ = dst.Close()
73
- return
74
- }
75
-
76
- rerr = dst.CloseSend()
77
- if errCh != nil {
78
- errCh <- rerr
79
69
  }
80
70
  }
81
71
 
@@ -0,0 +1,53 @@
1
+ package srpc_test
2
+
3
+ import (
4
+ "context"
5
+ "testing"
6
+
7
+ "github.com/aperturerobotics/starpc/echo"
8
+ "github.com/aperturerobotics/starpc/srpc"
9
+ "github.com/pkg/errors"
10
+ )
11
+
12
+ // failingEchoServer fails every unary echo.
13
+ type failingEchoServer struct {
14
+ *echo.EchoServer
15
+ }
16
+
17
+ // Echo returns errEchoFailed.
18
+ func (failingEchoServer) Echo(context.Context, *echo.EchoMsg) (*echo.EchoMsg, error) {
19
+ return nil, errEchoFailed
20
+ }
21
+
22
+ // errEchoFailed is the error failingEchoServer returns.
23
+ var errEchoFailed = errors.New("echo failed")
24
+
25
+ // TestClientInvokerForwardsResult checks that a proxied call reports the
26
+ // remote's reply and the remote handler's error.
27
+ func TestClientInvokerForwardsResult(t *testing.T) {
28
+ ctx := t.Context()
29
+ proxy := func(server echo.SRPCEchoerServer) echo.SRPCEchoerClient {
30
+ mux := srpc.NewMux()
31
+ if err := echo.SRPCRegisterEchoer(mux, server); err != nil {
32
+ t.Fatal(err)
33
+ }
34
+ remote := srpc.NewClient(srpc.NewServerPipe(srpc.NewServer(mux)))
35
+ invoker := srpc.NewClientInvoker(remote)
36
+ return echo.NewSRPCEchoerClient(srpc.NewClient(srpc.NewServerPipe(srpc.NewServer(invoker))))
37
+ }
38
+
39
+ // A reply passes through unchanged.
40
+ reply, err := proxy(echo.NewEchoServer(nil)).Echo(ctx, &echo.EchoMsg{Body: "hello"})
41
+ if err != nil {
42
+ t.Fatal(err)
43
+ }
44
+ if reply.GetBody() != "hello" {
45
+ t.Errorf("reply body = %q", reply.GetBody())
46
+ }
47
+
48
+ // A handler error reaches the caller instead of a cancellation.
49
+ _, err = proxy(failingEchoServer{}).Echo(ctx, &echo.EchoMsg{Body: "hello"})
50
+ if err == nil || err.Error() != errEchoFailed.Error() {
51
+ t.Errorf("proxied error = %v, want %v", err, errEchoFailed)
52
+ }
53
+ }
@@ -6,101 +6,86 @@
6
6
 
7
7
  namespace starpc {
8
8
 
9
- ClientRPC::ClientRPC(const std::string &service, const std::string &method) {
10
- Init();
11
- service_ = service;
12
- method_ = method;
9
+ ClientRPC::ClientRPC(const std::string &service, const std::string &method)
10
+ {
11
+ this->service = service;
12
+ this->method = method;
13
13
  }
14
14
 
15
15
  ClientRPC::~ClientRPC() = default;
16
16
 
17
17
  Error ClientRPC::Start(PacketWriter *writer, bool write_first_msg,
18
- const std::string &first_msg) {
19
- if (writer == nullptr) {
20
- return Error::NilWriter;
21
- }
22
-
23
- if (IsCanceled()) {
24
- Cancel();
25
- writer->Close();
26
- return Error::Canceled;
27
- }
28
-
29
- bool first_msg_empty = false;
30
- Error err = Error::OK;
31
-
32
- {
33
- std::lock_guard<std::mutex> lock(mtx_);
34
- writer_ = writer;
35
-
36
- if (write_first_msg) {
37
- first_msg_empty = first_msg.empty();
38
- }
39
-
40
- auto pkt =
41
- NewCallStartPacket(service_, method_, first_msg, first_msg_empty);
42
- err = writer->WritePacket(*pkt);
43
- if (err != Error::OK) {
44
- Cancel();
45
- writer->Close();
46
- }
47
- }
48
-
49
- cv_.notify_all();
50
- return err;
18
+ const std::string &first_msg)
19
+ {
20
+ if (writer == nullptr)
21
+ return Error::NilWriter;
22
+ if (service.empty())
23
+ return Error::EmptyServiceID;
24
+ if (method.empty())
25
+ return Error::EmptyMethodID;
26
+
27
+ std::unique_lock writing(write_mutex);
28
+ {
29
+ std::lock_guard lock(state_mutex);
30
+ if (this->writer != nullptr)
31
+ return Error::Completed;
32
+ this->writer = writer;
33
+ if (canceled || data_closed) {
34
+ writing.unlock();
35
+ /* The caller closes a rejected writer. */
36
+ return Error::Canceled;
37
+ }
38
+ }
39
+
40
+ const auto packet = NewCallStartPacket(
41
+ service, method, write_first_msg ? first_msg : "",
42
+ write_first_msg && first_msg.empty());
43
+ const auto written = writer->WritePacket(*packet);
44
+ writing.unlock();
45
+ if (written != Error::OK)
46
+ HandleStreamClose(written);
47
+ return written;
51
48
  }
52
49
 
53
- Error ClientRPC::HandlePacketData(const std::string &data) {
54
- srpc::Packet pkt;
55
- if (!pkt.ParseFromString(data)) {
56
- return Error::InvalidMessage;
57
- }
58
- return HandlePacket(pkt);
50
+ Error ClientRPC::HandlePacketData(const std::string &data)
51
+ {
52
+ srpc::Packet packet;
53
+ if (!packet.ParseFromString(data))
54
+ return Error::InvalidMessage;
55
+ return HandlePacket(packet);
59
56
  }
60
57
 
61
- void ClientRPC::HandleStreamClose(Error close_err) {
62
- std::lock_guard<std::mutex> lock(mtx_);
63
- if (close_err != Error::OK && remote_err_ == Error::OK) {
64
- remote_err_ = close_err;
65
- }
66
- data_closed_ = true;
67
- Cancel();
68
- cv_.notify_all();
58
+ void ClientRPC::HandleStreamClose(Error close_err)
59
+ {
60
+ CommonRPC::HandleStreamClose(close_err);
69
61
  }
70
62
 
71
- Error ClientRPC::HandlePacket(const srpc::Packet &msg) {
72
- Error err = ValidatePacket(msg);
73
- if (err != Error::OK) {
74
- return err;
75
- }
76
-
77
- switch (msg.body_case()) {
78
- case srpc::Packet::kCallStart:
79
- return HandleCallStart(msg.call_start());
80
- case srpc::Packet::kCallData:
81
- return HandleCallData(msg.call_data());
82
- case srpc::Packet::kCallCancel:
83
- if (msg.call_cancel()) {
84
- return HandleCallCancel();
85
- }
86
- return Error::OK;
87
- default:
88
- return Error::OK;
89
- }
63
+ Error ClientRPC::HandlePacket(const srpc::Packet &packet)
64
+ {
65
+ const auto err = ValidatePacket(packet);
66
+ if (err != Error::OK)
67
+ return err;
68
+
69
+ switch (packet.body_case()) {
70
+ case srpc::Packet::kCallStart:
71
+ return Error::UnrecognizedPacket;
72
+ case srpc::Packet::kCallData:
73
+ return HandleCallData(packet.call_data());
74
+ case srpc::Packet::kCallCancel:
75
+ return packet.call_cancel() ? HandleCallCancel() : Error::OK;
76
+ default:
77
+ return Error::UnrecognizedPacket;
78
+ }
90
79
  }
91
80
 
92
- Error ClientRPC::HandleCallStart(const srpc::CallStart &pkt) {
93
- // server-to-client calls not supported
94
- return Error::UnrecognizedPacket;
81
+ Error ClientRPC::HandleCallStart(const srpc::CallStart &)
82
+ {
83
+ return Error::UnrecognizedPacket;
95
84
  }
96
85
 
97
- void ClientRPC::Close() {
98
- std::lock_guard<std::mutex> lock(mtx_);
99
- // call did not start yet if writer is nil.
100
- if (writer_ != nullptr) {
101
- WriteCallCancelLocked();
102
- CloseLocked();
103
- }
86
+ void ClientRPC::Close()
87
+ {
88
+ Cancel();
104
89
  }
105
90
 
106
91
  } // namespace starpc
@@ -11,10 +11,8 @@ type ClientRPC struct {
11
11
  commonRPC
12
12
  }
13
13
 
14
- // NewClientRPC constructs a new ClientRPC session and writes CallStart.
15
- // the writer will be closed when the ClientRPC completes.
16
- // service and method must be specified.
17
- // must call Start after creating the RPC object.
14
+ // NewClientRPC constructs a call with the given service and method.
15
+ // Start attaches its transport; Close releases it.
18
16
  func NewClientRPC(ctx context.Context, service, method string) *ClientRPC {
19
17
  rpc := &ClientRPC{}
20
18
  initCommonRPC(ctx, &rpc.commonRPC)
@@ -23,39 +21,42 @@ func NewClientRPC(ctx context.Context, service, method string) *ClientRPC {
23
21
  return rpc
24
22
  }
25
23
 
26
- // Start sets the writer and writes the MsgSend message.
27
- // must only be called once!
24
+ // Start attaches one writer and sends CallStart without holding the state lock.
28
25
  func (r *ClientRPC) Start(writer PacketWriter, writeFirstMsg bool, firstMsg []byte) error {
26
+ // Validate the call before transferring the transport to it.
29
27
  if writer == nil {
30
28
  return ErrNilWriter
31
29
  }
32
-
33
- if err := r.ctx.Err(); err != nil {
34
- r.cancelContext()
35
- _ = writer.Close()
36
- return context.Canceled
30
+ if !writeFirstMsg {
31
+ firstMsg = nil
37
32
  }
38
-
39
- var firstMsgEmpty bool
40
- var err error
41
- locked := r.bcast.Lock()
42
- r.writer = writer
43
-
44
- if writeFirstMsg {
45
- firstMsgEmpty = len(firstMsg) == 0
33
+ pkt := NewCallStartPacket(r.service, r.method, firstMsg, writeFirstMsg && len(firstMsg) == 0)
34
+ if err := pkt.Validate(); err != nil {
35
+ return err
46
36
  }
47
37
 
48
- pkt := NewCallStartPacket(r.service, r.method, firstMsg, firstMsgEmpty)
49
- err = writer.WritePacket(pkt)
50
- if err != nil {
51
- r.cancelContext()
52
- _ = writer.Close()
38
+ // Publish the writer before transport callbacks can deliver a response.
39
+ locked := r.bcast.Lock()
40
+ if r.writer != nil {
41
+ locked.Unlock()
42
+ return ErrCompleted
53
43
  }
54
-
44
+ r.writer = writer
55
45
  locked.Broadcast()
56
46
  locked.Unlock()
57
47
 
58
- return err
48
+ // Cancellation closes the transport even when its first write is blocked.
49
+ stop := context.AfterFunc(r.ctx, r.Close)
50
+ defer stop()
51
+ if err := r.ctx.Err(); err != nil {
52
+ r.Close()
53
+ return err
54
+ }
55
+ if err := writer.WritePacket(pkt); err != nil {
56
+ r.Close()
57
+ return err
58
+ }
59
+ return nil
59
60
  }
60
61
 
61
62
  // HandlePacketData handles an incoming unparsed message packet.
@@ -95,20 +96,18 @@ func (r *ClientRPC) HandlePacket(msg *Packet) error {
95
96
 
96
97
  // HandleCallStart handles the call start packet.
97
98
  func (r *ClientRPC) HandleCallStart(pkt *CallStart) error {
98
- // server-to-client calls not supported
99
99
  return errors.Wrap(ErrUnrecognizedPacket, "call start packet unexpected")
100
100
  }
101
101
 
102
102
  // Close releases any resources held by the ClientRPC.
103
103
  func (r *ClientRPC) Close() {
104
+ // Settle the call before releasing a transport that can call back into it.
104
105
  locked := r.bcast.Lock()
105
- var writer PacketWriter
106
- // call did not start yet if writer is nil.
107
- if r.writer != nil {
108
- _ = r.WriteCallCancel()
109
- writer = r.closeLocked(&locked)
110
- }
106
+ writer := r.closeLocked(&locked)
111
107
  locked.Unlock()
108
+
109
+ // Closing the transport interrupts pending writes; sending cancellation first
110
+ // could itself block forever behind a peer that stopped reading.
112
111
  if writer != nil {
113
112
  _ = writer.Close()
114
113
  }