grpc_kit 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 429df1d2acc2083513f55a3b12dc612e0f27b2ae0a7fb13b198bc304294d6f75
4
- data.tar.gz: 482c39c5674efbf8a08c6deadb93eea903078460d2eba85532728d7ddeb58a80
3
+ metadata.gz: 4a609dafaf12831fc04a1ffbf84f38b1551a408aeeeeb0fc22ec59eae33ab607
4
+ data.tar.gz: b2623749a3c1ec772963e081fafb4ca249fd0afba6e682fe61ccec68dd8e1c6a
5
5
  SHA512:
6
- metadata.gz: 036c1f0bba4959a1a6a0aa0f060251bae8ea6624872f2a17b636ce1c5aa1fd5168a0f7ad12340ee2a71a7954088ca98d02b11455d6fb5c3adc523617d3bb5004
7
- data.tar.gz: 2b99be0aeedd437596ad8a94b25f25ddf06f45c2dbccad6da55e3bc27b339184c6dfe6d7c4ccca561f1f296f0369fa711042ce904e8bcf1b55c611e5da85ec34
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,10 +1,8 @@
1
1
  # GrpcKit
2
2
 
3
- [![Build Status](https://travis-ci.org/ganmacs/grpc_kit.svg?branch=master)](https://travis-ci.org/ganmacs/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
- __UNDER DEVELOPMENT__
7
-
8
6
  A kit for creating [gRPC](https://grpc.io/) server/client in Ruby.
9
7
 
10
8
  ## Installation
@@ -29,7 +27,7 @@ $ gem install grpc_kit
29
27
 
30
28
  ## Usage
31
29
 
32
- More Details in [examples directory](https://github.com/ganmacs/grpc_kit/tree/master/examples).
30
+ More Details in [examples directory](https://github.com/cookpad/grpc_kit/tree/master/examples).
33
31
 
34
32
  ##### Server
35
33
 
@@ -61,11 +59,11 @@ $ bundle install
61
59
 
62
60
  ## Projects using grpc_kit
63
61
 
64
- * [griffin](https://github.com/ganmacs/griffin) Multi process gRPC server in Ruby
62
+ * [griffin](https://github.com/cookpad/griffin) Multi process gRPC server in Ruby
65
63
 
66
64
  ## Contributing
67
65
 
68
- Bug reports and pull requests are welcome on GitHub at https://github.com/ganmacs/grpc_kit.
66
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cookpad/grpc_kit.
69
67
 
70
68
  ## License
71
69
 
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.add_dependency 'ds9', '>= 1.3.3'
26
- spec.add_dependency 'google-protobuf', '>= 3.6.1'
25
+ spec.add_dependency 'ds9', '>= 1.4.0'
26
+ spec.add_dependency 'google-protobuf', '>= 3.7.0'
27
27
  spec.add_dependency 'googleapis-common-protos-types', '>= 1.0.2'
28
28
 
29
29
  spec.add_development_dependency 'bundler'
@@ -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}"
@@ -10,7 +10,7 @@ module GrpcKit
10
10
  # @param shutdown_timeout [Integer] Number of seconds to wait for the server shutdown
11
11
  # @param min_pool_size [Integer] A mininum thread pool size
12
12
  # @param max_pool_size [Integer] A maximum thread pool size
13
- def initialize(interceptors: [], shutdown_timeout: 30, min_pool_size: nil, max_pool_size: nil)
13
+ def initialize(interceptors: [], shutdown_timeout: 30, min_pool_size: nil, max_pool_size: nil, settings: [])
14
14
  @interceptors = interceptors
15
15
  @shutdown_timeout = shutdown_timeout
16
16
  @min_pool_size = min_pool_size || GrpcKit::RpcDispatcher::DEFAULT_MIN
@@ -19,6 +19,7 @@ module GrpcKit
19
19
  @rpc_descs = {}
20
20
  @mutex = Mutex.new
21
21
  @stopping = false
22
+ @settings = settings
22
23
 
23
24
  GrpcKit.logger.debug("Launched grpc_kit(v#{GrpcKit::VERSION})")
24
25
  end
@@ -47,7 +48,7 @@ module GrpcKit
47
48
  raise 'Stopping server' if @stopping
48
49
 
49
50
  establish_session(conn) do |s|
50
- s.submit_settings([])
51
+ s.submit_settings(@settings)
51
52
  s.start
52
53
  end
53
54
  end
@@ -64,23 +65,23 @@ module GrpcKit
64
65
  end
65
66
 
66
67
  # This method is expected to be called in trap context
68
+ # @params timeout [Boolean] timeout error could be raised or not
67
69
  # @return [void]
68
- def graceful_shutdown
70
+ def graceful_shutdown(timeout: true)
69
71
  @stopping = true
70
72
 
71
73
  Thread.new do
72
74
  GrpcKit.logger.debug('graceful shutdown')
73
75
  @mutex.synchronize { @sessions.each(&:drain) }
74
76
 
75
- end_time = Time.now + @shutdown_timeout
76
- loop do
77
- if end_time < Time.now
78
- GrpcKit.logger.error("Graceful shutdown is timeout (#{@shutdown_timeout}sec). Perform shutdown forceibly")
79
- shutdown_sessions
80
- break
81
- elsif @sessions.empty?
82
- break
77
+ begin
78
+ sec = timeout ? @shutdown_timeout : 0
79
+ Timeout.timeout(sec) do
80
+ sleep 1 until @sessions.empty?
83
81
  end
82
+ rescue Timeout::Error => _
83
+ GrpcKit.logger.error("Graceful shutdown is timeout (#{@shutdown_timeout}sec). Perform shutdown forceibly")
84
+ shutdown_sessions
84
85
  end
85
86
  end
86
87
  end
@@ -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
@@ -8,12 +8,14 @@ module GrpcKit
8
8
  module Status
9
9
  NOT_START = 0
10
10
  STARTED = 1
11
- SENT_PING = 2
12
- RECV_PING_ACK = 3
13
- SENT_GOAWAY = 4
11
+ SENT_SHUTDOWN = 2
12
+ SENT_PING = 3
13
+ RECV_PING_ACK = 4
14
+ SENT_GOAWAY = 5
14
15
  end
15
16
 
16
- def initialize
17
+ def initialize(draining_time = 5)
18
+ @draining_time = draining_time
17
19
  @status = Status::NOT_START
18
20
  end
19
21
 
@@ -39,15 +41,22 @@ module GrpcKit
39
41
  # next_step
40
42
  when Status::STARTED
41
43
  session.submit_shutdown
44
+ next_step
45
+ when Status::SENT_SHUTDOWN
42
46
  session.submit_ping
47
+ @sent_time = Time.now.to_i
43
48
  next_step
44
49
  when Status::SENT_PING
45
50
  # skip until #recv_ping_ack is called (1RTT)
46
51
  when Status::RECV_PING_ACK
47
- session.submit_goaway(DS9::NO_ERROR, session.last_proc_stream_id)
52
+ if @sent_time && (Time.now.to_i - @sent_time) > @draining_time
53
+ return
54
+ end
55
+
56
+ session.submit_goaway(session.last_proc_stream_id, DS9::NO_ERROR)
48
57
  next_step
49
58
  when Status::SENT_GOAWAY
50
- session.shutdown
59
+ # session.shutdown
51
60
  end
52
61
  end
53
62
 
@@ -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
@@ -16,14 +16,20 @@ module GrpcKit
16
16
  # @param io [GrpcKit::Session::IO]
17
17
  # @param pool [GrpcKit::RcpDispatcher]
18
18
  def initialize(io, dispatcher)
19
- super() # initialize DS9::Session
19
+ opt = DS9::Option.new.tap do |o|
20
+ # https://github.com/nghttp2/nghttp2/issues/810
21
+ # grpc_kit doesn't need to retain closed stream.
22
+ # This would derease the memory usage.
23
+ o.set_no_closed_streams
24
+ end
25
+ super(option: opt) # initialize DS9::Session
20
26
 
21
27
  @io = io
22
28
  @streams = {}
23
29
  @stop = false
24
30
  @inflights = []
25
31
  @drain_controller = GrpcKit::Session::DrainController.new
26
- @control_queue = GrpcKit::Session::ControlQueue.new
32
+ @control_queue = GrpcKit::Session::ControlQueue.new(waker: @io.method(:wake!))
27
33
  @dispatcher = dispatcher
28
34
  end
29
35
 
@@ -54,7 +60,11 @@ module GrpcKit
54
60
  return false
55
61
  end
56
62
 
57
- rs, ws = @io.select
63
+ if @drain_controller.start_draining?
64
+ @drain_controller.next(self)
65
+ end
66
+
67
+ rs, ws = @io.select(timeout: 5, write: want_write?)
58
68
 
59
69
  if !rs.empty? && want_read?
60
70
  do_read
@@ -138,6 +148,8 @@ module GrpcKit
138
148
  data = @streams[stream_id].pending_send_data.read(length)
139
149
  if data.nil?
140
150
  submit_trailer(stream_id, stream.trailer_data)
151
+ @io.wake!(:submit_trailer)
152
+
141
153
  # trailer header
142
154
  false
143
155
  else
@@ -170,6 +182,7 @@ module GrpcKit
170
182
  stream.inflight = true
171
183
  @dispatcher.schedule([stream, @control_queue])
172
184
  end
185
+
173
186
  when DS9::Frames::Headers
174
187
  if frame.end_stream?
175
188
  stream = @streams[frame.stream_id]
@@ -178,10 +191,7 @@ module GrpcKit
178
191
  when DS9::Frames::Ping
179
192
  if frame.ping_ack?
180
193
  GrpcKit.logger.debug('ping ack is received')
181
- # nghttp2 can't send any data once server sent actaul GoAway(not shutdown notice) frame.
182
- # We want to send data in case of ClientStreamer or BidiBstreamer which they are sending data in same stream
183
- # So we have to wait to send actual GoAway frame untill timeout or something
184
- # @drain_controller.recv_ping_ack
194
+ @drain_controller.recv_ping_ack
185
195
  end
186
196
  # when DS9::Frames::Goaway
187
197
  # when DS9::Frames::RstStream
@@ -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]
@@ -15,10 +15,10 @@ module GrpcKit
15
15
  def invoke(rpc)
16
16
  rpc.invoke(self, metadata: @transport.recv_headers.metadata)
17
17
  rescue GrpcKit::Errors::BadStatus => e
18
- GrpcKit.logger.debug(e)
18
+ GrpcKit.logger.warn(e)
19
19
  send_status(status: e.code, msg: e.reason, metadata: {}) # TODO: metadata should be set
20
20
  rescue StandardError => e
21
- GrpcKit.logger.debug(e)
21
+ GrpcKit.logger.warn(e)
22
22
  send_status(status: GrpcKit::StatusCodes::UNKNOWN, msg: e.message, metadata: {})
23
23
  end
24
24
 
@@ -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.5'
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.5
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: 2019-02-07 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
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.3
19
+ version: 1.4.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.3.3
26
+ version: 1.4.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: google-protobuf
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 3.6.1
33
+ version: 3.7.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 3.6.1
40
+ version: 3.7.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: googleapis-common-protos-types
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -252,9 +253,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
253
  - !ruby/object:Gem::Version
253
254
  version: '0'
254
255
  requirements: []
255
- rubyforge_project:
256
- rubygems_version: 2.7.6
257
- signing_key:
256
+ rubygems_version: 3.1.2
257
+ signing_key:
258
258
  specification_version: 4
259
259
  summary: A kit for creating gRPC server/client
260
260
  test_files: []
@@ -1,13 +0,0 @@
1
- language: ruby
2
- cache:
3
- - bundler
4
- dist: trusty
5
- rvm:
6
- - ruby-head
7
- - 2.5.3
8
- - 2.4.5
9
- before_install:
10
- - gem install bundler -v 1.17.0
11
- matrix:
12
- allow_failures:
13
- - rvm: ruby-head