grumlin 0.3.0 → 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: e532964db5d5afd11d89978a132a5f4f7d1a4d98c740f639acadac260c93be89
4
- data.tar.gz: 9015cb71bbab17895d8809dc7ff514b90e9d1ceb81ed6a9eec446a0db7ec0994
3
+ metadata.gz: 11e434ab0f5cd463d357e69dd3cbf5083f85a1bdd32e4d2c96023ce262704020
4
+ data.tar.gz: f24dd70f8b0abe9b370f77587d590802b32aa3e7b8a20380b9518004868cc6c7
5
5
  SHA512:
6
- metadata.gz: a4295b9e6041726c36c05cc0aed94a645e8d7103c99351dfadef7077f14037b81daf87a4ffb1fd48105d8c1fa888e96ce173aa3be42c9b35ec58de3e289c01ed
7
- data.tar.gz: 51824ca159be63be91fc798b40777a25d55f0ce423b07a9ce4ce9880002e94b4e4196bc0d92b4ca84bee86356f17fd90f3cc2dd9daa594f906fc2e93f11be422
6
+ metadata.gz: 5bfbd0d4db9ef46d35839b8ca5654cdc1f5038ee2c4ca004b678471380095d1fb587fa89a8df0c5a4769e3943ceb4e0b88734b701593ccf1047a05ab85baaaf5
7
+ data.tar.gz: a7d87e630e6b2076bedd079365e709018b3fc61f1577564c504593ffdeb4deab88f56d8785c30877489171b6c9d7fd245982475a75f4c639647c923e2ca296ef
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.3.0)
4
+ grumlin (0.4.0)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
 
@@ -59,7 +59,7 @@ GEM
59
59
  kramdown-parser-gfm (1.1.0)
60
60
  kramdown (~> 2.0)
61
61
  minitest (5.14.4)
62
- nio4r (2.5.7)
62
+ nio4r (2.5.8)
63
63
  nokogiri (1.11.7-x86_64-linux)
64
64
  racc (~> 1.4)
65
65
  overcommit (0.57.0)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ # Channel is a wrapper around Async::Queue that provides
5
+ # a protocol and handy tools for passing data, exceptions and closing.
6
+ # It is designed to be used with only one publisher and one subscriber
7
+ class Channel
8
+ class ChannelError < StandardError; end
9
+
10
+ class ChannelClosedError < ChannelError; end
11
+
12
+ def initialize
13
+ @queue = Async::Queue.new
14
+ @closed = false
15
+ end
16
+
17
+ def closed?
18
+ @closed
19
+ end
20
+
21
+ # Methods for a publisher
22
+ def <<(payload)
23
+ raise(ChannelClosedError, "Cannot send to a closed channel") if @closed
24
+
25
+ @queue << [:payload, payload]
26
+ end
27
+
28
+ def exception(exception)
29
+ raise(ChannelClosedError, "Cannot send to a closed channel") if closed?
30
+
31
+ @queue << [:exception, exception]
32
+ end
33
+
34
+ def close
35
+ raise(ChannelClosedError, "Cannot close a closed channel") if closed?
36
+
37
+ @queue << [:close]
38
+ @closed = true
39
+ end
40
+
41
+ # Methods for a subscriber
42
+ def dequeue
43
+ each do |payload| # rubocop:disable Lint/UnreachableLoop this is intended
44
+ return payload
45
+ end
46
+ end
47
+
48
+ def each # rubocop:disable Metrics/MethodLength
49
+ raise(ChannelClosedError, "Cannot receive from a closed channel") if closed?
50
+
51
+ @queue.each do |type, payload|
52
+ case type
53
+ when :exception
54
+ payload.set_backtrace(caller + (payload.backtrace || [])) # A hack to preserve full backtrace
55
+ raise payload
56
+ when :payload
57
+ yield payload
58
+ when :close
59
+ break
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/grumlin.rb CHANGED
@@ -12,6 +12,8 @@ require "async/barrier"
12
12
  require "async/http/endpoint"
13
13
  require "async/websocket/client"
14
14
 
15
+ require_relative "async/channel"
16
+
15
17
  require_relative "grumlin/version"
16
18
  require_relative "grumlin/exceptions"
17
19
 
@@ -38,12 +40,13 @@ require_relative "grumlin/sugar"
38
40
 
39
41
  module Grumlin
40
42
  class Config
41
- attr_accessor :url, :pool_size, :client_concurrency
43
+ attr_accessor :url, :pool_size, :client_concurrency, :client_factory
42
44
 
43
- # For some reason, client_concurrency must be greather pool_size
45
+ # For some reason, client_concurrency must be greater than pool_size
44
46
  def initialize
45
47
  @pool_size = 10
46
48
  @client_concurrency = 20
49
+ @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
47
50
  end
48
51
 
49
52
  def default_pool
@@ -2,45 +2,49 @@
2
2
 
3
3
  module Grumlin
4
4
  class Client
5
- class PoolResource < self
6
- attr :concurrency, :count
5
+ class PoolResource < Async::Pool::Resource
6
+ attr_reader :client
7
7
 
8
8
  def self.call
9
- new(Grumlin.config.url, concurrency: Grumlin.config.client_concurrency).tap(&:connect)
9
+ config = Grumlin.config
10
+ new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
10
11
  end
11
12
 
12
- def initialize(url, concurrency: 1, parent: Async::Task.current)
13
- super(url, parent: parent)
14
- @concurrency = concurrency
15
- @count = 0
13
+ def initialize(url, client_factory:, concurrency: 1, parent: Async::Task.current)
14
+ super(concurrency)
15
+ @client = client_factory.call(url, parent).tap(&:connect)
16
16
  end
17
17
 
18
- def viable?
19
- connected?
18
+ def closed?
19
+ !@client.connected?
20
20
  end
21
21
 
22
- def closed?
23
- connected?
22
+ def close
23
+ @client.close
24
24
  end
25
25
 
26
- def reusable?
27
- true
26
+ def write(*args)
27
+ @client.write(*args)
28
28
  end
29
29
  end
30
30
 
31
- def initialize(url, parent: Async::Task.current)
31
+ def initialize(url, parent: Async::Task.current, **client_options)
32
+ @url = url
33
+ @client_options = client_options
32
34
  @parent = parent
33
- @transport = Transport.new(url)
34
35
  reset!
35
36
  end
36
37
 
37
38
  def connect
38
- response_queue = @transport.connect
39
+ @transport = build_transport
40
+ response_channel = @transport.connect
39
41
  @request_dispatcher = RequestDispatcher.new
40
42
  @parent.async do
41
- response_queue.each do |response|
43
+ response_channel.each do |response|
42
44
  @request_dispatcher.add_response(response)
43
45
  end
46
+ rescue StandardError
47
+ close
44
48
  end
45
49
  end
46
50
 
@@ -52,31 +56,28 @@ module Grumlin
52
56
  end
53
57
 
54
58
  def connected?
55
- @transport.connected?
59
+ @transport&.connected? || false
56
60
  end
57
61
 
58
62
  # TODO: support yielding
59
63
  def write(*args) # rubocop:disable Metrics/MethodLength
64
+ raise NotConnectedError unless connected?
65
+
60
66
  request_id = SecureRandom.uuid
61
67
  request = to_query(request_id, args)
62
- queue = @request_dispatcher.add_request(request)
68
+ channel = @request_dispatcher.add_request(request)
63
69
  @transport.write(request)
64
70
 
65
71
  begin
66
- msg, response = queue.dequeue
67
- raise response if msg == :error
68
-
69
- return response.flat_map { |item| Typing.cast(item) } if msg == :result
70
-
71
- raise "ERROR"
72
+ channel.dequeue.flat_map { |item| Typing.cast(item) }
72
73
  rescue Async::Stop
73
74
  retry if @request_dispatcher.ongoing_request?(request_id)
74
- raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
75
+ raise Grumlin::UnknownRequestStoppedError, "#{request_id} is not in the ongoing requests list"
75
76
  end
76
77
  end
77
78
 
78
79
  def inspect
79
- "<#{self.class} url=#{@transport.url}>"
80
+ "<#{self.class} url=#{@url} connected=#{connected?}>"
80
81
  end
81
82
 
82
83
  alias to_s inspect
@@ -97,6 +98,11 @@ module Grumlin
97
98
 
98
99
  def reset!
99
100
  @request_dispatcher = nil
101
+ @transport = nil
102
+ end
103
+
104
+ def build_transport
105
+ Transport.new(@url, parent: @parent, **@client_options)
100
106
  end
101
107
  end
102
108
  end
@@ -29,12 +29,12 @@ module Grumlin
29
29
  def add_request(request)
30
30
  raise "ERROR" if @requests.key?(request[:requestId])
31
31
 
32
- Async::Queue.new.tap do |queue|
33
- @requests[request[:requestId]] = { request: request, result: [], queue: queue }
32
+ Async::Channel.new.tap do |channel|
33
+ @requests[request[:requestId]] = { request: request, result: [], channel: channel }
34
34
  end
35
35
  end
36
36
 
37
- # builds a response object, when it's ready sends it to the client via a queue
37
+ # builds a response object, when it's ready sends it to the client via a channel
38
38
  # TODO: sometimes response does not include requestID, no idea how to handle it so far.
39
39
  def add_response(response) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
40
40
  request_id = response[:requestId]
@@ -46,15 +46,15 @@ module Grumlin
46
46
 
47
47
  case SUCCESS[response.dig(:status, :code)]
48
48
  when :success
49
- request[:queue] << [:result, request[:result] + [response.dig(:result, :data)]]
49
+ request[:channel] << request[:result] + [response.dig(:result, :data)]
50
50
  close_request(request_id)
51
51
  when :partial_content then request[:result] << response.dig(:result, :data)
52
52
  when :no_content
53
- request[:queue] << [:result, []]
53
+ request[:channel] << []
54
54
  close_request(request_id)
55
55
  end
56
56
  rescue StandardError => e
57
- request[:queue] << [:error, e]
57
+ request[:channel].exception(e)
58
58
  close_request(request_id)
59
59
  end
60
60
 
@@ -62,7 +62,7 @@ module Grumlin
62
62
  raise "ERROR" unless ongoing_request?(request_id)
63
63
 
64
64
  request = @requests.delete(request_id)
65
- request[:queue] << nil
65
+ request[:channel].close
66
66
  end
67
67
 
68
68
  def ongoing_request?(request_id)
@@ -4,64 +4,72 @@ module Grumlin
4
4
  class Transport
5
5
  # A transport based on https://github.com/socketry/async
6
6
  # and https://github.com/socketry/async-websocket
7
- def initialize(url, parent: Async::Task.current)
8
- @endpoint = Async::HTTP::Endpoint.parse(url)
7
+
8
+ attr_reader :url
9
+
10
+ def initialize(url, parent: Async::Task.current, **client_options)
11
+ @url = url
9
12
  @parent = parent
10
- @request_queue = Async::Queue.new
11
- @response_queue = Async::Queue.new
13
+ @client_options = client_options
14
+ @request_channel = Async::Channel.new
15
+ @response_channel = Async::Channel.new
12
16
  reset!
13
17
  end
14
18
 
15
- def url
16
- @endpoint.url
17
- end
18
-
19
19
  def connected?
20
20
  @connected
21
21
  end
22
22
 
23
- def connect # rubocop:disable Metrics/MethodLength
23
+ def connect # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
24
24
  raise AlreadyConnectedError if connected?
25
25
 
26
- @connection = Async::WebSocket::Client.connect(@endpoint)
26
+ @connection = Async::WebSocket::Client.connect(Async::HTTP::Endpoint.parse(@url), **@client_options)
27
27
 
28
28
  @response_task = @parent.async do
29
29
  loop do
30
30
  data = @connection.read
31
- @response_queue << data
31
+ @response_channel << data
32
32
  end
33
33
  rescue Async::Stop
34
- @response_queue << nil
34
+ @response_channel.close
35
+ rescue StandardError => e
36
+ @response_channel.exception(e)
35
37
  end
36
38
 
37
39
  @request_task = @parent.async do
38
- @request_queue.each do |message|
40
+ @request_channel.each do |message|
39
41
  @connection.write(message)
40
42
  @connection.flush
41
43
  end
44
+ rescue StandardError => e
45
+ @response_channel.exception(e)
42
46
  end
43
47
 
44
48
  @connected = true
45
49
 
46
- @response_queue
50
+ @response_channel
47
51
  end
48
52
 
49
53
  def write(message)
50
54
  raise NotConnectedError unless connected?
51
55
 
52
- @request_queue << message
56
+ @request_channel << message
53
57
  end
54
58
 
55
- def close
56
- raise NotConnectedError unless connected?
59
+ def close # rubocop:disable Metrics/MethodLength
60
+ return unless connected?
57
61
 
58
- @request_queue << nil
62
+ @request_channel.close
59
63
  @request_task.wait
60
64
 
61
65
  @response_task.stop
62
66
  @response_task.wait
63
67
 
64
- @connection.close
68
+ begin
69
+ @connection.close
70
+ rescue Errno::EPIPE
71
+ nil
72
+ end
65
73
 
66
74
  reset!
67
75
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-29 00:00:00.000000000 Z
11
+ date: 2021-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -62,6 +62,7 @@ files:
62
62
  - gremlin_server/Dockerfile
63
63
  - gremlin_server/tinkergraph-empty.properties
64
64
  - grumlin.gemspec
65
+ - lib/async/channel.rb
65
66
  - lib/grumlin.rb
66
67
  - lib/grumlin/anonymous_step.rb
67
68
  - lib/grumlin/client.rb