grpc_kit 0.3.9 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ac10170b0067b5b136488dc0fe600c398ba766036b5586b95ba73d078226678
4
- data.tar.gz: 6f62b35ab4cdeeaf4ad8a0b2d1f7865281fdea95525e32d91a9f72ecac982b61
3
+ metadata.gz: 4a609dafaf12831fc04a1ffbf84f38b1551a408aeeeeb0fc22ec59eae33ab607
4
+ data.tar.gz: b2623749a3c1ec772963e081fafb4ca249fd0afba6e682fe61ccec68dd8e1c6a
5
5
  SHA512:
6
- metadata.gz: ef362c8bc6bbe89a3793826dbccd7501b63112b7e8da64c81d24dd2d6657828c0fda0dc16cc8b924016f99319c38980a4ac3fa5711015a965d037eadff542d3a
7
- data.tar.gz: 614e6ac35969ff541457b3c63af01b56e78ed4dffe627a50113ecf9a75b93c97b091a76c4c23e956fa957945be95283a5721f6546e3a77696e1671bf72d881b0
6
+ metadata.gz: bbf6bad8922e0058979e7e1962aed08ff7625f24691ae55cd62fc5a9ae6c53a6a68b1938050d17320b972b861e37edf1435e48497d63186863ed8e452c736eaa
7
+ data.tar.gz: aabd06f81259f04e8ea051466d88b5160cc062422f070a1bb6cb4945db43747ae488cbbe6d2f4af0bd97dfcf159c6705d52ceaaa7a6b916f7fc31b2f1378abf0
@@ -0,0 +1,25 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ ruby:
10
+ name: ruby
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby_version:
15
+ - 2.5
16
+ - 2.6
17
+ - 2.7
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: '${{ matrix.ruby_version }}'
23
+ bundler-cache: true
24
+
25
+ - run: 'bundle exec rake'
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## v0.4.0 (2020-10-23)
4
+
5
+ - bug: Fix Ruby 2.7 keyword argument separation warnings ([#27](https://github.com/cookpad/grpc_kit/pull/27))
6
+ - bug: HTTP/2 Trailer (grpc-status) might not be sent due to race condition ([#30](https://github.com/cookpad/grpc_kit/pull/30))
7
+ - improve: Reduce number of select(2) calls by adding pipe(2) to wake blocking threads ([#28](https://github.com/cookpad/grpc_kit/pull/28))
8
+ - improve: Improved performance when receiving streaming messages by blocking queue. ([#31](https://github.com/cookpad/grpc_kit/pull/31))
9
+
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GrpcKit
2
2
 
3
- [![Build Status](https://travis-ci.org/cookpad/grpc_kit.svg?branch=master)](https://travis-ci.org/cookpad/grpc_kit)
3
+ [![Build Sttaus](https://github.com/cookpad/grpc_kit/workflows/ci/badge.svg)](https://github.com/cookpad/grpc_kit/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/grpc_kit.svg)](https://badge.fury.io/rb/grpc_kit)
5
5
 
6
6
  A kit for creating [gRPC](https://grpc.io/) server/client in Ruby.
@@ -10,10 +10,13 @@ module GrpcKit
10
10
 
11
11
  alias outgoing_metadata metadata
12
12
 
13
- def initialize(*)
13
+ def initialize(**)
14
14
  super
15
- @mutex = Mutex.new
15
+ @recv_mutex = Mutex.new
16
+
16
17
  @send = false
18
+ @send_cv = Thread::ConditionVariable.new
19
+ @send_mutex = Mutex.new
17
20
  end
18
21
 
19
22
  # @param data [Object] request message
@@ -23,29 +26,21 @@ module GrpcKit
23
26
  raise "Upstream returns an error status: #{@reason}"
24
27
  end
25
28
 
26
- @mutex.synchronize do
29
+ @send_mutex.synchronize do
27
30
  @stream.send_msg(data, metadata: outgoing_metadata)
31
+ @send = true
32
+ @send_cv.broadcast
28
33
  end
29
-
30
- @send = true
31
34
  end
32
35
 
33
- # This method not is expected to be call in the main thread where #send_msg is called
34
- #
36
+ # Receive a message from peer. This method is not thread safe, never call from multiple threads at once.
35
37
  # @return [Object] response object
38
+ # @raise [StopIteration]
36
39
  def recv
37
- sleep 0.1 until @send
38
-
39
- loop do
40
- msg = @mutex.synchronize do
41
- @stream.recv_msg(blocking: false)
42
- end
43
-
44
- unless msg == :wait_readable
45
- return msg
46
- end
47
- end
40
+ @send_mutex.synchronize { @send_cv.wait(@send_mutex) until @send } unless @send
48
41
 
42
+ msg = @stream.recv_msg(blocking: true)
43
+ return msg if msg
49
44
  raise StopIteration
50
45
  rescue GrpcKit::Errors::BadStatus => e
51
46
  @reason = e
@@ -53,14 +48,16 @@ module GrpcKit
53
48
  end
54
49
 
55
50
  def close_and_send
56
- @mutex.synchronize do
51
+ @send_mutex.synchronize do
57
52
  @stream.close_and_send
58
53
  end
59
54
  end
60
55
 
61
56
  # @yieldparam response [Object] each response object of bidi streaming RPC
62
57
  def each
63
- loop { yield(recv) }
58
+ @recv_mutex.synchronize do
59
+ loop { yield(recv) }
60
+ end
64
61
  end
65
62
  end
66
63
  end
@@ -16,6 +16,7 @@ module GrpcKit
16
16
  @stream.send_msg(data, last: true, metadata: outgoing_metadata)
17
17
  end
18
18
 
19
+ # This method is not thread safe, never call from multiple threads at once.
19
20
  # @return [Object] response object
20
21
  def recv
21
22
  @stream.recv_msg
@@ -11,7 +11,7 @@ module GrpcKit
11
11
  attr_reader :outgoing_initial_metadata, :outgoing_trailing_metadata
12
12
  alias incoming_metadata metadata
13
13
 
14
- def initialize(*)
14
+ def initialize(**)
15
15
  super
16
16
 
17
17
  @outgoing_initial_metadata = {}
@@ -30,6 +30,7 @@ module GrpcKit
30
30
  )
31
31
  end
32
32
 
33
+ # This method is not thread safe, never call from multiple threads at once.
33
34
  # @return [Object] response object
34
35
  def recv
35
36
  @stream.recv_msg(@codec, limit_size: @config.max_receive_message_size)
@@ -11,7 +11,7 @@ module GrpcKit
11
11
  attr_reader :outgoing_initial_metadata, :outgoing_trailing_metadata
12
12
  alias incoming_metadata metadata
13
13
 
14
- def initialize(*)
14
+ def initialize(**)
15
15
  super
16
16
 
17
17
  @outgoing_initial_metadata = {}
@@ -31,6 +31,7 @@ module GrpcKit
31
31
  )
32
32
  end
33
33
 
34
+ # This method is not thread safe, never call from multiple threads at once.
34
35
  # @return [Object] response object
35
36
  def recv
36
37
  @stream.recv_msg(@codec, limit_size: @config.max_receive_message_size)
@@ -9,7 +9,7 @@ module GrpcKit
9
9
  attr_reader :outgoing_initial_metadata, :outgoing_trailing_metadata
10
10
  alias incoming_metadata metadata
11
11
 
12
- def initialize(*)
12
+ def initialize(**)
13
13
  super
14
14
 
15
15
  @outgoing_initial_metadata = {}
@@ -9,7 +9,7 @@ module GrpcKit
9
9
  attr_reader :outgoing_initial_metadata, :outgoing_trailing_metadata
10
10
  alias incoming_metadata metadata
11
11
 
12
- def initialize(*)
12
+ def initialize(**)
13
13
  super
14
14
 
15
15
  @outgoing_initial_metadata = {}
@@ -30,32 +30,32 @@ module GrpcKit
30
30
  # @param rpc [GrpcKit::Rpcs::Client::RequestResponse]
31
31
  # @param request [Object]
32
32
  # @param opts [Hash]
33
- def request_response(rpc, request, opts = {})
33
+ def request_response(rpc, request, **opts)
34
34
  GrpcKit.logger.debug('Calling request_respose')
35
- do_request(rpc, request, opts)
35
+ do_request(rpc, request, **opts)
36
36
  end
37
37
 
38
38
  # @param rpc [GrpcKit::Rpcs::Client::ClientStreamer]
39
39
  # @param opts [Hash]
40
- def client_streamer(rpc, opts = {})
40
+ def client_streamer(rpc, **opts)
41
41
  GrpcKit.logger.debug('Calling client_streamer')
42
- do_request(rpc, nil, opts)
42
+ do_request(rpc, nil, **opts)
43
43
  end
44
44
 
45
45
  # @param rpc [GrpcKit::Rpcs::Client::ServerStreamer]
46
46
  # @param request [Object]
47
47
  # @param opts [Hash]
48
- def server_streamer(rpc, request, opts = {})
48
+ def server_streamer(rpc, request, **opts)
49
49
  GrpcKit.logger.debug('Calling server_streamer')
50
- do_request(rpc, request, opts)
50
+ do_request(rpc, request, **opts)
51
51
  end
52
52
 
53
53
  # @param rpc [GrpcKit::Rpcs::Client::ServerStreamer]
54
54
  # @param _requests [Object] it's for compatibility, no use
55
55
  # @param opts [Hash]
56
- def bidi_streamer(rpc, _requests, opts = {})
56
+ def bidi_streamer(rpc, _requests, **opts)
57
57
  GrpcKit.logger.debug('Calling bidi_streamer')
58
- do_request(rpc, nil, opts)
58
+ do_request(rpc, nil, **opts)
59
59
  end
60
60
 
61
61
  private
@@ -68,7 +68,7 @@ module GrpcKit
68
68
  end
69
69
 
70
70
  Class.new(GrpcKit::Client) do
71
- def initialize(*)
71
+ def initialize(*, **)
72
72
  @rpcs = {}
73
73
  super
74
74
  end
@@ -82,20 +82,20 @@ module GrpcKit
82
82
 
83
83
  rpc_descs_.each do |method_name, rpc_desc|
84
84
  if rpc_desc.request_response?
85
- define_method(method_name) do |request, opts = {}|
86
- request_response(@rpcs.fetch(method_name), request, opts)
85
+ define_method(method_name) do |request, **opts|
86
+ request_response(@rpcs.fetch(method_name), request, **opts)
87
87
  end
88
88
  elsif rpc_desc.client_streamer?
89
- define_method(method_name) do |opts = {}|
90
- client_streamer(@rpcs.fetch(method_name), opts)
89
+ define_method(method_name) do |**opts|
90
+ client_streamer(@rpcs.fetch(method_name), **opts)
91
91
  end
92
92
  elsif rpc_desc.server_streamer?
93
- define_method(method_name) do |request, opts = {}|
94
- server_streamer(@rpcs.fetch(method_name), request, opts)
93
+ define_method(method_name) do |request, **opts|
94
+ server_streamer(@rpcs.fetch(method_name), request, **opts)
95
95
  end
96
96
  elsif rpc_desc.bidi_streamer?
97
- define_method(method_name) do |requests, opts = {}, &blk|
98
- bidi_streamer(@rpcs.fetch(method_name), requests, opts, &blk)
97
+ define_method(method_name) do |requests, **opts, &blk|
98
+ bidi_streamer(@rpcs.fetch(method_name), requests, **opts, &blk)
99
99
  end
100
100
  else
101
101
  raise "unknown #{rpc_desc}"
@@ -18,7 +18,7 @@ module GrpcKit
18
18
 
19
19
  # @param io [GrpcKit::Session::IO]
20
20
  # @param opts [Hash]
21
- def initialize(io, opts = {})
21
+ def initialize(io, **opts)
22
22
  super() # initialize DS9::Session
23
23
 
24
24
  @io = io
@@ -27,6 +27,7 @@ module GrpcKit
27
27
  @draining = false
28
28
  @stop = false
29
29
  @no_write_data = false
30
+ @mutex = Mutex.new
30
31
  end
31
32
 
32
33
  # @param headers [Hash<String,String>]
@@ -47,7 +48,8 @@ module GrpcKit
47
48
  # @param stream_id [Integer]
48
49
  # @return [void]
49
50
  def start(stream_id)
50
- stream = @streams.fetch(stream_id)
51
+ stream = @streams[stream_id]
52
+ return unless stream # stream might have already close
51
53
 
52
54
  loop do
53
55
  if (!want_read? && !want_write?) || stream.close?
@@ -63,26 +65,28 @@ module GrpcKit
63
65
 
64
66
  # @return [void]
65
67
  def run_once
66
- return if @stop
67
-
68
- if @draining && @drain_time < Time.now
69
- raise 'trasport is closing'
70
- end
71
-
72
- if @no_write_data
73
- @io.wait_readable
68
+ @mutex.synchronize do
69
+ return if @stop
74
70
 
75
- if want_read?
76
- do_read
77
- end
78
- else
79
- rs, ws = @io.select
80
- if !rs.empty? && want_read?
81
- do_read
71
+ if @draining && @drain_time < Time.now
72
+ raise 'trasport is closing'
82
73
  end
83
74
 
84
- if !ws.empty? && want_write?
85
- send
75
+ if @no_write_data && !@streams.empty?
76
+ @io.wait_readable
77
+
78
+ if want_read?
79
+ do_read
80
+ end
81
+ else
82
+ rs, ws = @io.select
83
+ if !rs.empty? && want_read?
84
+ do_read
85
+ end
86
+
87
+ if !ws.empty? && want_write?
88
+ send
89
+ end
86
90
  end
87
91
  end
88
92
  end
@@ -155,8 +159,10 @@ module GrpcKit
155
159
  def on_stream_close(stream_id, error_code)
156
160
  GrpcKit.logger.debug("on_stream_close stream_id=#{stream_id}, error_code=#{error_code}")
157
161
  stream = @streams.delete(stream_id)
158
- return unless stream
159
-
162
+ unless stream
163
+ GrpcKit.logger.warn("on_stream_close stream_id=#{stream_id} not remain on ClientSession")
164
+ return
165
+ end
160
166
  stream.close
161
167
  end
162
168
 
@@ -3,8 +3,9 @@
3
3
  module GrpcKit
4
4
  module Session
5
5
  class ControlQueue
6
- def initialize
6
+ def initialize(waker: proc { })
7
7
  @event_stream = Queue.new
8
+ @waker = waker
8
9
  end
9
10
 
10
11
  # Be nonblocking
@@ -20,14 +21,17 @@ module GrpcKit
20
21
 
21
22
  def submit_response(id, headers)
22
23
  @event_stream.push([:submit_response, id, headers])
24
+ @waker.call(:submit_response)
23
25
  end
24
26
 
25
27
  def submit_headers(id, headers)
26
28
  @event_stream.push([:submit_headers, id, headers])
29
+ @waker.call(:submit_headers)
27
30
  end
28
31
 
29
32
  def resume_data(id)
30
33
  @event_stream.push([:resume_data, id])
34
+ @waker.call(:submit_response)
31
35
  end
32
36
  end
33
37
  end
@@ -7,9 +7,12 @@ module GrpcKit
7
7
  class IO
8
8
  def initialize(io)
9
9
  @io = io
10
+ @wake_o, @wake_i = ::IO.pipe
10
11
  end
11
12
 
12
13
  def close
14
+ @wake_i.close
15
+ @wake_o.close
13
16
  @io.close
14
17
  end
15
18
 
@@ -52,8 +55,19 @@ module GrpcKit
52
55
 
53
56
  # Blocking until io object is readable or writable
54
57
  # @return [void]
55
- def select(timeout = 1)
56
- ::IO.select([@io], [@io], [], timeout)
58
+ def select(timeout: 1, write: true)
59
+ rs, ws = ::IO.select([@io, @wake_o], write ? [@io] : [], [], timeout)
60
+ @wake_o.read(@wake_o.stat.size) if rs&.delete(@wake_o) && !@wake_o.closed?
61
+ [rs || [], ws || []]
62
+ end
63
+
64
+ # Wake thread blocked at #select method
65
+ # @param [Symbol] Indicate what event needed to invoke blocking thread. This argument is for debugging purpose.
66
+ def wake!(memo = nil)
67
+ @wake_i.write_nonblock(?\0, exception: false)
68
+ rescue Errno::EPIPE
69
+ rescue IOError
70
+ raise unless @wake_i.closed?
57
71
  end
58
72
 
59
73
  # @return [void]
@@ -3,35 +3,69 @@
3
3
  module GrpcKit
4
4
  module Session
5
5
  class RecvBuffer
6
+ class Closed < Exception; end
7
+
6
8
  def initialize
7
9
  @buffer = +''.b
8
10
  @end = false
9
- @mutex = Mutex.new
11
+ @queue = Queue.new
10
12
  end
11
13
 
12
14
  # @param data [String]
13
15
  # @return [void]
14
16
  def write(data)
15
- @mutex.synchronize { @buffer << data }
17
+ @queue << data
18
+ rescue ClosedQueueError
19
+ raise Closed, "[BUG] write to closed queue"
20
+ end
21
+
22
+ # @return [Boolean]
23
+ def empty?
24
+ @queue.empty?
25
+ end
26
+
27
+ # @return [Boolean]
28
+ def closed?
29
+ @queue.closed?
16
30
  end
17
31
 
32
+ # @return [void]
33
+ def close
34
+ @queue.close
35
+ end
36
+
37
+ # This method is not thread safe (as RecvBuffer is designed to be a multi-producer/single-consumer)
18
38
  # @param size [Integer,nil]
19
39
  # @param last [Boolean]
20
- # @return [String,nil]
21
- def read(size = nil, last: false)
22
- buf = @mutex.synchronize do
23
- return nil if @buffer.empty?
24
-
25
- if size.nil? || @buffer.bytesize < size
26
- buf = @buffer
27
- @buffer = ''.b
28
- buf
29
- else
30
- @buffer.freeze
31
- rbuf = @buffer.byteslice(0, size)
32
- @buffer = @buffer.byteslice(size, @buffer.bytesize)
33
- rbuf
34
- end
40
+ # @param blocking [Boolean]
41
+ # @return [String,Symbol,nil]
42
+ def read(size = nil, last: false, blocking:)
43
+ if @buffer.empty?
44
+ return nil if empty? && closed?
45
+ return :wait_readable if empty? && !blocking
46
+
47
+ # Consume existing data as much as possible to continue (important on clients where single-threaded)
48
+ loop do
49
+ begin
50
+ data = @queue.shift(!blocking)
51
+ @buffer << data if data
52
+ rescue ThreadError
53
+ break
54
+ end
55
+
56
+ break if empty?
57
+ end
58
+ end
59
+
60
+ buf = if size.nil? || @buffer.bytesize < size
61
+ rbuf = @buffer
62
+ @buffer = ''.b
63
+ rbuf
64
+ else
65
+ @buffer.freeze
66
+ rbuf = @buffer.byteslice(0, size)
67
+ @buffer = @buffer.byteslice(size, @buffer.bytesize)
68
+ rbuf
35
69
  end
36
70
 
37
71
  end_read if last
@@ -29,7 +29,7 @@ module GrpcKit
29
29
  @stop = false
30
30
  @inflights = []
31
31
  @drain_controller = GrpcKit::Session::DrainController.new
32
- @control_queue = GrpcKit::Session::ControlQueue.new
32
+ @control_queue = GrpcKit::Session::ControlQueue.new(waker: @io.method(:wake!))
33
33
  @dispatcher = dispatcher
34
34
  end
35
35
 
@@ -64,7 +64,7 @@ module GrpcKit
64
64
  @drain_controller.next(self)
65
65
  end
66
66
 
67
- rs, ws = @io.select
67
+ rs, ws = @io.select(timeout: 5, write: want_write?)
68
68
 
69
69
  if !rs.empty? && want_read?
70
70
  do_read
@@ -148,6 +148,8 @@ module GrpcKit
148
148
  data = @streams[stream_id].pending_send_data.read(length)
149
149
  if data.nil?
150
150
  submit_trailer(stream_id, stream.trailer_data)
151
+ @io.wake!(:submit_trailer)
152
+
151
153
  # trailer header
152
154
  false
153
155
  else
@@ -180,6 +182,7 @@ module GrpcKit
180
182
  stream.inflight = true
181
183
  @dispatcher.schedule([stream, @control_queue])
182
184
  end
185
+
183
186
  when DS9::Frames::Headers
184
187
  if frame.end_stream?
185
188
  stream = @streams[frame.stream_id]
@@ -13,7 +13,6 @@ module GrpcKit
13
13
 
14
14
  delegate %i[end_write?] => :@pending_send_data
15
15
  delegate %i[end_read?] => :@pending_recv_data
16
- delegate %i[close close_remote close_local close? close_remote? close_local?] => :@status
17
16
 
18
17
  attr_reader :headers, :pending_send_data, :pending_recv_data, :trailer_data, :status
19
18
  attr_accessor :inflight, :stream_id
@@ -59,9 +58,10 @@ module GrpcKit
59
58
  end
60
59
 
61
60
  # @param last [Boolean]
61
+ # @param blocking [Boolean]
62
62
  # @return [void]
63
- def read_recv_data(last: false)
64
- @pending_recv_data.read(last: last)
63
+ def read_recv_data(last: false, blocking:)
64
+ @pending_recv_data.read(last: last, blocking: blocking)
65
65
  end
66
66
 
67
67
  # @param name [String]
@@ -70,6 +70,18 @@ module GrpcKit
70
70
  def add_header(name, value)
71
71
  @headers.add(name, value)
72
72
  end
73
+
74
+ delegate %i[close_local close? close_remote? close_local?] => :@status
75
+
76
+ def close
77
+ status.close
78
+ pending_recv_data.close
79
+ end
80
+
81
+ def close_remote
82
+ status.close_remote
83
+ pending_recv_data.close
84
+ end
73
85
  end
74
86
  end
75
87
  end
@@ -47,6 +47,7 @@ module GrpcKit
47
47
  end
48
48
  end
49
49
 
50
+ # This method is not thread safe, never call from multiple threads at once.
50
51
  # @raise [StopIteration] when recving message finished
51
52
  # @param last [Boolean]
52
53
  # @param blocking [Boolean]
@@ -50,6 +50,7 @@ module GrpcKit
50
50
  end
51
51
  end
52
52
 
53
+ # This method is not thread safe, never call from multiple threads at once.
53
54
  # @raise [StopIteration] when recving message finished
54
55
  # @param codec [GrpcKit::Codec]
55
56
  # @param last [Boolean]
@@ -89,13 +90,18 @@ module GrpcKit
89
90
  t = build_trailers(status, msg, metadata)
90
91
  @transport.write_data(data, last: true) if data
91
92
 
92
- @transport.end_write
93
- if @started
93
+ if @started # Complete stream
94
94
  @transport.write_trailers(t)
95
- elsif data
95
+ @transport.end_write
96
+
97
+ elsif data # Complete stream with a data
96
98
  @transport.write_trailers(t)
97
- start_response
98
- else
99
+ @transport.end_write
100
+
101
+ start_response # will send queued data and trailer.
102
+
103
+ else # return status (likely non-200) and immediately complete stream.
104
+ @transport.end_write
99
105
  send_headers(trailers: t)
100
106
  end
101
107
  end
@@ -41,20 +41,31 @@ module GrpcKit
41
41
  end
42
42
 
43
43
  # @param last [Boolean]
44
- # @return [nil,String]
44
+ # @return [nil,Array<Boolean,Integer,String>] nil when closed, tuple of Length-Prefixed-Message
45
45
  def read_data(last: false)
46
- unpack(recv_data(last: last))
46
+ data_in_buffer = unpack(nil)
47
+ return data_in_buffer if data_in_buffer
48
+ loop do
49
+ data = recv_data(last: last)
50
+ return unpack(nil) unless data
51
+ message = unpack(data)
52
+ return message if message
53
+ end
47
54
  end
48
55
 
49
56
  # @param last [Boolean]
50
- # @return [nil,String]
57
+ # @return [nil,Array<Boolean,Integer,String>,Symbol] nil when closed, tuple of Length-Prefixed-Message, or :wait_readable
51
58
  def read_data_nonblock(last: false)
59
+ data_in_buffer = unpack(nil)
60
+ return data_in_buffer if data_in_buffer
61
+
52
62
  data = nonblock_recv_data(last: last)
53
63
  if data == :wait_readable
54
- unpack(nil) # nil is needed read buffered data
55
64
  :wait_readable
65
+ elsif data == nil
66
+ return unpack(nil)
56
67
  else
57
- unpack(data)
68
+ unpack(data) || :wait_readable
58
69
  end
59
70
  end
60
71
 
@@ -78,33 +89,26 @@ module GrpcKit
78
89
  end
79
90
 
80
91
  def nonblock_recv_data(last: false)
81
- data = @stream.read_recv_data(last: last)
82
- return data unless data.nil?
83
-
84
- if @stream.close_remote?
85
- return nil
86
- end
87
-
88
- @session.run_once
92
+ data = @stream.read_recv_data(last: last, blocking: false)
93
+ return data if data.is_a?(String)
94
+ return nil unless data
89
95
 
90
96
  :wait_readable
91
97
  end
92
98
 
93
99
  def recv_data(last: false)
94
100
  loop do
95
- data = @stream.read_recv_data(last: last)
96
- return data unless data.nil?
97
-
98
- if @stream.close_remote?
99
- # it do not receive data which we need, it may receive invalid grpc-status
100
- unless @stream.end_read?
101
- return nil
102
- end
103
-
101
+ # FIXME: GrpcKit::Client isn't threaded, this cannot be blocked to trigger ClientSession#run_once appropriately
102
+ # but run_once would block while no outbound requests. Could be problematic on BiDi calls.
103
+ data = @stream.read_recv_data(last: last, blocking: false)
104
+ case data
105
+ when :wait_readable
106
+ @session.run_once
107
+ when String
108
+ return data
109
+ when nil
104
110
  return nil
105
111
  end
106
-
107
- @session.run_once
108
112
  end
109
113
  end
110
114
 
@@ -24,8 +24,8 @@ module GrpcKit
24
24
  end
25
25
 
26
26
  class Unpacker
27
- # Compressed bytes(1 Byte) + length bytes(4 Bytes)
28
- METADATA_SIZE = 5
27
+ # Compressed-Flag (1 byte) + Message-Length (4 Bytes)
28
+ PREFIX_SIZE = 5
29
29
 
30
30
  def initialize
31
31
  @data = +''.b
@@ -44,14 +44,17 @@ module GrpcKit
44
44
 
45
45
  # @return [nil, Array<Boolean, Integer, String>]
46
46
  def read
47
- return nil if @data.empty?
47
+ return nil if @data.bytesize < PREFIX_SIZE
48
+
49
+ prefix = @data.byteslice(0, PREFIX_SIZE)
50
+ compressed, length = prefix.unpack('CN')
51
+
52
+ return nil if (@data.bytesize-PREFIX_SIZE) < length
48
53
 
49
54
  d = @data.freeze
50
- metadata = d.byteslice(0, METADATA_SIZE)
51
- c, size = metadata.unpack('CN')
52
- data = @data.byteslice(METADATA_SIZE, size)
53
- @data = @data.byteslice(METADATA_SIZE + size, @data.bytesize)
54
- [c != 0, size, data]
55
+ data = d.byteslice(PREFIX_SIZE, length)
56
+ @data = d.byteslice(PREFIX_SIZE + length, d.bytesize)
57
+ [compressed == 1, length, data]
55
58
  end
56
59
  end
57
60
  end
@@ -36,9 +36,16 @@ module GrpcKit
36
36
  end
37
37
 
38
38
  # @param last [Boolean]
39
- # @return [nil,String]
39
+ # @return [nil,Array<Boolean,Integer,String>] nil when closed, tuple of Length-Prefixed-Message
40
40
  def read_data(last: false)
41
- unpack(recv_data(last: last))
41
+ data_in_buffer = unpack(nil)
42
+ return data_in_buffer if data_in_buffer
43
+ loop do
44
+ data = recv_data(last: last)
45
+ return nil unless data
46
+ message = unpack(data)
47
+ return message if message
48
+ end
42
49
  end
43
50
 
44
51
  # @param trailer [Hash<String, String>]
@@ -61,17 +68,7 @@ module GrpcKit
61
68
  private
62
69
 
63
70
  def recv_data(last: false)
64
- loop do
65
- data = @stream.read_recv_data(last: last)
66
- return data if data
67
-
68
- if @stream.close_remote?
69
- # Call @stream.read_recv_data after checking @stream.close_remote?
70
- # because of the order of nghttp2 callbacks which calls a callback receiving data before a callback receiving END_STREAM flag
71
- data = @stream.read_recv_data(last: last)
72
- return data
73
- end
74
- end
71
+ @stream.read_recv_data(last: last, blocking: true)
75
72
  end
76
73
 
77
74
  def send_data
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrpcKit
4
- VERSION = '0.3.9'
4
+ VERSION = '0.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grpc_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuta Iwama
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-02 00:00:00.000000000 Z
11
+ date: 2020-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ds9
@@ -143,11 +143,12 @@ executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
+ - ".github/workflows/ci.yml"
146
147
  - ".gitignore"
147
148
  - ".rspec"
148
149
  - ".rubocop.yml"
149
150
  - ".ruby-version"
150
- - ".travis.yml"
151
+ - CHANGELOG.md
151
152
  - Gemfile
152
153
  - LICENSE.txt
153
154
  - README.md
@@ -237,7 +238,7 @@ homepage: https://github.com/ganmacs/grpc_kit
237
238
  licenses:
238
239
  - MIT
239
240
  metadata: {}
240
- post_install_message:
241
+ post_install_message:
241
242
  rdoc_options: []
242
243
  require_paths:
243
244
  - lib
@@ -253,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
254
  version: '0'
254
255
  requirements: []
255
256
  rubygems_version: 3.1.2
256
- signing_key:
257
+ signing_key:
257
258
  specification_version: 4
258
259
  summary: A kit for creating gRPC server/client
259
260
  test_files: []
@@ -1,14 +0,0 @@
1
- language: ruby
2
- cache:
3
- - bundler
4
- dist: trusty
5
- rvm:
6
- - ruby-head
7
- - 2.6.1
8
- - 2.5.3
9
- - 2.4.5
10
- before_install:
11
- - gem install bundler -v 1.17.0
12
- matrix:
13
- allow_failures:
14
- - rvm: ruby-head