grpc_kit 0.3.5 → 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: 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