grumlin 0.2.0 → 0.3.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: c0290e4d77bd50f701a803390ff2a0489b039f4beb231e8883ced690180c7abc
4
- data.tar.gz: b4948d687c09ffea5bf9da26eda8644fc3fcffa1954e4e61fe2d628192984e7e
3
+ metadata.gz: e532964db5d5afd11d89978a132a5f4f7d1a4d98c740f639acadac260c93be89
4
+ data.tar.gz: 9015cb71bbab17895d8809dc7ff514b90e9d1ceb81ed6a9eec446a0db7ec0994
5
5
  SHA512:
6
- metadata.gz: 3846fe491a606989f51f0f609e8c7ae2049b8a1005272ea531f085402e7d998a7f8723ccdafae01ce7d913f9ad9272f9da1e86a307cb7aeb5066ef4ad4024e4b
7
- data.tar.gz: 1b20d251d16c95cb12657c6becbbb4592b8e206e9e83a7defee42281bbdd7c3f84d04785b2cc89bcc1328d05d11aa14b865b6bc6832899d3a73ce7b86283be5b
6
+ metadata.gz: a4295b9e6041726c36c05cc0aed94a645e8d7103c99351dfadef7077f14037b81daf87a4ffb1fd48105d8c1fa888e96ce173aa3be42c9b35ec58de3e289c01ed
7
+ data.tar.gz: 51824ca159be63be91fc798b40777a25d55f0ce423b07a9ce4ce9880002e94b4e4196bc0d92b4ca84bee86356f17fd90f3cc2dd9daa594f906fc2e93f11be422
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.2.0)
4
+ grumlin (0.3.0)
5
+ async-pool (~> 0.3)
5
6
  async-websocket (~> 0.19)
6
7
 
7
8
  GEM
@@ -14,7 +15,7 @@ GEM
14
15
  tzinfo (~> 2.0)
15
16
  zeitwerk (~> 2.3)
16
17
  ast (2.4.2)
17
- async (1.30.0)
18
+ async (1.30.1)
18
19
  console (~> 1.10)
19
20
  nio4r (~> 2.3)
20
21
  timers (~> 4.1)
@@ -124,7 +125,7 @@ GEM
124
125
  simplecov_json_formatter (~> 0.1)
125
126
  simplecov-html (0.12.3)
126
127
  simplecov_json_formatter (0.1.3)
127
- solargraph (0.42.4)
128
+ solargraph (0.43.0)
128
129
  backport (~> 1.2)
129
130
  benchmark
130
131
  bundler (>= 1.17.2)
data/bin/console CHANGED
@@ -19,5 +19,5 @@ Async do
19
19
  rescue StandardError
20
20
  raise
21
21
  ensure
22
- Grumlin.config.default_client&.disconnect
22
+ Grumlin.config.default_pool.close
23
23
  end
data/grumlin.gemspec CHANGED
@@ -23,5 +23,6 @@ Gem::Specification.new do |spec|
23
23
  end
24
24
  spec.require_paths = ["lib"]
25
25
 
26
+ spec.add_dependency "async-pool", "~> 0.3"
26
27
  spec.add_dependency "async-websocket", "~> 0.19"
27
28
  end
data/lib/grumlin.rb CHANGED
@@ -4,6 +4,9 @@ require "securerandom"
4
4
  require "json"
5
5
 
6
6
  require "async"
7
+ require "async/pool"
8
+ require "async/pool/resource"
9
+ require "async/pool/controller"
7
10
  require "async/queue"
8
11
  require "async/barrier"
9
12
  require "async/http/endpoint"
@@ -12,19 +15,20 @@ require "async/websocket/client"
12
15
  require_relative "grumlin/version"
13
16
  require_relative "grumlin/exceptions"
14
17
 
15
- require_relative "grumlin/transport/async"
18
+ require_relative "grumlin/transport"
19
+ require_relative "grumlin/client"
16
20
 
17
21
  require_relative "grumlin/vertex"
18
22
  require_relative "grumlin/edge"
19
23
  require_relative "grumlin/path"
20
24
  require_relative "grumlin/typing"
21
- require_relative "grumlin/client"
22
25
  require_relative "grumlin/traversal"
26
+ require_relative "grumlin/request_dispatcher"
27
+ require_relative "grumlin/translator"
23
28
 
24
29
  require_relative "grumlin/anonymous_step"
25
30
  require_relative "grumlin/step"
26
31
 
27
- require_relative "grumlin/translator"
28
32
  require_relative "grumlin/t"
29
33
  require_relative "grumlin/order"
30
34
  require_relative "grumlin/u"
@@ -34,14 +38,20 @@ require_relative "grumlin/sugar"
34
38
 
35
39
  module Grumlin
36
40
  class Config
37
- attr_accessor :url
41
+ attr_accessor :url, :pool_size, :client_concurrency
42
+
43
+ # For some reason, client_concurrency must be greather pool_size
44
+ def initialize
45
+ @pool_size = 10
46
+ @client_concurrency = 20
47
+ end
38
48
 
39
- def default_client
40
- @default_client ||= Grumlin::Client.new(url)
49
+ def default_pool
50
+ @default_pool ||= Async::Pool::Controller.new(Grumlin::Client::PoolResource, limit: pool_size)
41
51
  end
42
52
 
43
53
  def reset!
44
- @default_client = nil
54
+ @default_pool = nil
45
55
  end
46
56
  end
47
57
 
@@ -2,120 +2,101 @@
2
2
 
3
3
  module Grumlin
4
4
  class Client
5
- extend Forwardable
6
-
7
- SUCCESS = {
8
- 200 => :success,
9
- 204 => :no_content,
10
- 206 => :partial_content
11
- }.freeze
12
-
13
- ERRORS = {
14
- 499 => InvalidRequestArgumentsError,
15
- 500 => ServerError,
16
- 597 => ScriptEvaluationError,
17
- 599 => ServerSerializationError,
18
- 598 => ServerTimeoutError,
19
-
20
- 401 => ClientSideError,
21
- 407 => ClientSideError,
22
- 498 => ClientSideError
23
- }.freeze
24
-
25
- def initialize(url, autoconnect: true)
26
- @url = url
27
- @transport = Transport::Async.new(url)
28
- connect if autoconnect
29
- end
5
+ class PoolResource < self
6
+ attr :concurrency, :count
30
7
 
31
- def_delegators :@transport, :connect, :disconnect, :requests
8
+ def self.call
9
+ new(Grumlin.config.url, concurrency: Grumlin.config.client_concurrency).tap(&:connect)
10
+ end
32
11
 
33
- # TODO: support yielding
34
- def submit(*args)
35
- request_id = SecureRandom.uuid
36
- queue = @transport.submit(to_query(request_id, args))
37
- wait_for_response(request_id, queue)
38
- ensure
39
- @transport.close_request(request_id)
40
- end
12
+ def initialize(url, concurrency: 1, parent: Async::Task.current)
13
+ super(url, parent: parent)
14
+ @concurrency = concurrency
15
+ @count = 0
16
+ end
41
17
 
42
- def inspect
43
- "<#{self.class} @url=#{@url}>"
44
- end
18
+ def viable?
19
+ connected?
20
+ end
45
21
 
46
- alias to_s inspect
22
+ def closed?
23
+ connected?
24
+ end
47
25
 
48
- private
26
+ def reusable?
27
+ true
28
+ end
29
+ end
49
30
 
50
- def wait_for_response(request_id, queue, result: []) # rubocop:disable Metrics/MethodLength
51
- queue.each do |status, response|
52
- check_errors!(request_id, status, response)
31
+ def initialize(url, parent: Async::Task.current)
32
+ @parent = parent
33
+ @transport = Transport.new(url)
34
+ reset!
35
+ end
53
36
 
54
- case SUCCESS[response.dig(:status, :code)]
55
- when :success
56
- return result + Typing.cast(response.dig(:result, :data))
57
- when :partial_content then result += Typing.cast(response.dig(:result, :data))
58
- when :no_content
59
- return []
37
+ def connect
38
+ response_queue = @transport.connect
39
+ @request_dispatcher = RequestDispatcher.new
40
+ @parent.async do
41
+ response_queue.each do |response|
42
+ @request_dispatcher.add_response(response)
60
43
  end
61
44
  end
62
- rescue ::Async::Stop
63
- retry if @transport.ongoing_request?(request_id)
64
- raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
65
45
  end
66
46
 
67
- def to_query(request_id, message)
68
- case message.first # TODO: properly handle unknown type of message
69
- when String
70
- string_query_message(request_id, *message)
71
- when Grumlin::Step
72
- bytecode_query_message(request_id, Translator.to_bytecode_query(message))
73
- end
47
+ def close
48
+ @transport.close
49
+ raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
50
+
51
+ reset!
74
52
  end
75
53
 
76
- def check_errors!(_request_id, status, response)
77
- reraise_error!(response) if status == :error
54
+ def connected?
55
+ @transport.connected?
56
+ end
78
57
 
79
- status = response[:status]
58
+ # TODO: support yielding
59
+ def write(*args) # rubocop:disable Metrics/MethodLength
60
+ request_id = SecureRandom.uuid
61
+ request = to_query(request_id, args)
62
+ queue = @request_dispatcher.add_request(request)
63
+ @transport.write(request)
80
64
 
81
- if (error = ERRORS[status[:code]])
82
- raise(error, status)
83
- end
65
+ begin
66
+ msg, response = queue.dequeue
67
+ raise response if msg == :error
84
68
 
85
- return unless SUCCESS[status[:code]].nil?
69
+ return response.flat_map { |item| Typing.cast(item) } if msg == :result
86
70
 
87
- raise(UnknownResponseStatus, status)
71
+ raise "ERROR"
72
+ rescue Async::Stop
73
+ retry if @request_dispatcher.ongoing_request?(request_id)
74
+ raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
75
+ end
88
76
  end
89
77
 
90
- def reraise_error!(error)
91
- raise error
92
- rescue StandardError
93
- raise UnknownError
78
+ def inspect
79
+ "<#{self.class} url=#{@transport.url}>"
94
80
  end
95
81
 
96
- def string_query_message(request_id, query, bindings)
97
- {
98
- requestId: request_id,
99
- op: "eval",
100
- processor: "",
101
- args: {
102
- gremlin: query,
103
- bindings: bindings,
104
- language: "gremlin-groovy"
105
- }
106
- }
107
- end
82
+ alias to_s inspect
108
83
 
109
- def bytecode_query_message(request_id, bytecode)
84
+ private
85
+
86
+ def to_query(request_id, message)
110
87
  {
111
88
  requestId: request_id,
112
89
  op: "bytecode",
113
90
  processor: "traversal",
114
91
  args: {
115
- gremlin: Typing.to_bytecode(bytecode),
92
+ gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
116
93
  aliases: { g: :g }
117
94
  }
118
95
  }
119
96
  end
97
+
98
+ def reset!
99
+ @request_dispatcher = nil
100
+ end
120
101
  end
121
102
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class RequestDispatcher
5
+ attr_reader :requests
6
+
7
+ SUCCESS = {
8
+ 200 => :success,
9
+ 204 => :no_content,
10
+ 206 => :partial_content
11
+ }.freeze
12
+
13
+ ERRORS = {
14
+ 499 => InvalidRequestArgumentsError,
15
+ 500 => ServerError,
16
+ 597 => ScriptEvaluationError,
17
+ 599 => ServerSerializationError,
18
+ 598 => ServerTimeoutError,
19
+
20
+ 401 => ClientSideError,
21
+ 407 => ClientSideError,
22
+ 498 => ClientSideError
23
+ }.freeze
24
+
25
+ def initialize
26
+ @requests = {}
27
+ end
28
+
29
+ def add_request(request)
30
+ raise "ERROR" if @requests.key?(request[:requestId])
31
+
32
+ Async::Queue.new.tap do |queue|
33
+ @requests[request[:requestId]] = { request: request, result: [], queue: queue }
34
+ end
35
+ end
36
+
37
+ # builds a response object, when it's ready sends it to the client via a queue
38
+ # TODO: sometimes response does not include requestID, no idea how to handle it so far.
39
+ def add_response(response) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
40
+ request_id = response[:requestId]
41
+ raise "ERROR" unless ongoing_request?(request_id)
42
+
43
+ request = @requests[request_id]
44
+
45
+ check_errors!(response[:status])
46
+
47
+ case SUCCESS[response.dig(:status, :code)]
48
+ when :success
49
+ request[:queue] << [:result, request[:result] + [response.dig(:result, :data)]]
50
+ close_request(request_id)
51
+ when :partial_content then request[:result] << response.dig(:result, :data)
52
+ when :no_content
53
+ request[:queue] << [:result, []]
54
+ close_request(request_id)
55
+ end
56
+ rescue StandardError => e
57
+ request[:queue] << [:error, e]
58
+ close_request(request_id)
59
+ end
60
+
61
+ def close_request(request_id)
62
+ raise "ERROR" unless ongoing_request?(request_id)
63
+
64
+ request = @requests.delete(request_id)
65
+ request[:queue] << nil
66
+ end
67
+
68
+ def ongoing_request?(request_id)
69
+ @requests.key?(request_id)
70
+ end
71
+
72
+ private
73
+
74
+ def check_errors!(status)
75
+ if (error = ERRORS[status[:code]])
76
+ raise(error, status)
77
+ end
78
+
79
+ return unless SUCCESS[status[:code]].nil?
80
+
81
+ raise(UnknownResponseStatus, status)
82
+ end
83
+ end
84
+ end
data/lib/grumlin/step.rb CHANGED
@@ -4,9 +4,9 @@ module Grumlin
4
4
  class Step < AnonymousStep
5
5
  attr_reader :client
6
6
 
7
- def initialize(client, name, *args, previous_steps: [])
7
+ def initialize(pool, name, *args, previous_steps: [])
8
8
  super(name, *args, previous_steps: previous_steps)
9
- @client = client
9
+ @pool = pool
10
10
  end
11
11
 
12
12
  def next
@@ -15,17 +15,21 @@ module Grumlin
15
15
  end
16
16
 
17
17
  def toList # rubocop:disable Naming/MethodName
18
- @toList ||= @client.submit(*steps) # rubocop:disable Naming/VariableName
18
+ @pool.acquire do |client|
19
+ client.write(*steps)
20
+ end
19
21
  end
20
22
 
21
23
  def iterate
22
- @client.submit(*(steps + [nil]))
24
+ @pool.acquire do |client|
25
+ client.write(*(steps + [nil]))
26
+ end
23
27
  end
24
28
 
25
29
  private
26
30
 
27
31
  def add_step(step_name, args, previous_steps:)
28
- self.class.new(@client, step_name, *args, previous_steps: previous_steps)
32
+ self.class.new(@pool, step_name, *args, previous_steps: previous_steps)
29
33
  end
30
34
  end
31
35
  end
@@ -12,8 +12,7 @@ module Grumlin
12
12
  let(:g) { Grumlin::Traversal.new }
13
13
 
14
14
  after do
15
- expect(Grumlin.config.default_client.requests).to be_empty
16
- Grumlin.config.default_client.disconnect
15
+ Grumlin.config.default_pool.close
17
16
  Grumlin.config.reset!
18
17
  end
19
18
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Transport
5
+ # A transport based on https://github.com/socketry/async
6
+ # and https://github.com/socketry/async-websocket
7
+ def initialize(url, parent: Async::Task.current)
8
+ @endpoint = Async::HTTP::Endpoint.parse(url)
9
+ @parent = parent
10
+ @request_queue = Async::Queue.new
11
+ @response_queue = Async::Queue.new
12
+ reset!
13
+ end
14
+
15
+ def url
16
+ @endpoint.url
17
+ end
18
+
19
+ def connected?
20
+ @connected
21
+ end
22
+
23
+ def connect # rubocop:disable Metrics/MethodLength
24
+ raise AlreadyConnectedError if connected?
25
+
26
+ @connection = Async::WebSocket::Client.connect(@endpoint)
27
+
28
+ @response_task = @parent.async do
29
+ loop do
30
+ data = @connection.read
31
+ @response_queue << data
32
+ end
33
+ rescue Async::Stop
34
+ @response_queue << nil
35
+ end
36
+
37
+ @request_task = @parent.async do
38
+ @request_queue.each do |message|
39
+ @connection.write(message)
40
+ @connection.flush
41
+ end
42
+ end
43
+
44
+ @connected = true
45
+
46
+ @response_queue
47
+ end
48
+
49
+ def write(message)
50
+ raise NotConnectedError unless connected?
51
+
52
+ @request_queue << message
53
+ end
54
+
55
+ def close
56
+ raise NotConnectedError unless connected?
57
+
58
+ @request_queue << nil
59
+ @request_task.wait
60
+
61
+ @response_task.stop
62
+ @response_task.wait
63
+
64
+ @connection.close
65
+
66
+ reset!
67
+ end
68
+
69
+ private
70
+
71
+ def reset!
72
+ @connected = false
73
+ @connection = nil
74
+ @response_task = nil
75
+ @request_task = nil
76
+ end
77
+ end
78
+ end
@@ -4,18 +4,14 @@ module Grumlin
4
4
  class Traversal
5
5
  attr_reader :connection
6
6
 
7
- def initialize(client_or_url = Grumlin.config.default_client)
8
- @client = if client_or_url.is_a?(String)
9
- Grumlin::Client.new(client_or_url)
10
- else
11
- client_or_url
12
- end
7
+ def initialize(pool = Grumlin.config.default_pool)
8
+ @pool = pool
13
9
  end
14
10
 
15
11
  # TODO: add other start steps
16
12
  %w[addV addE V E].each do |step|
17
13
  define_method step do |*args|
18
- Step.new(@client, step, *args)
14
+ Step.new(@pool, step, *args)
19
15
  end
20
16
  end
21
17
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.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-26 00:00:00.000000000 Z
11
+ date: 2021-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async-pool
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: async-websocket
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -57,6 +71,7 @@ files:
57
71
  - lib/grumlin/p.rb
58
72
  - lib/grumlin/path.rb
59
73
  - lib/grumlin/pop.rb
74
+ - lib/grumlin/request_dispatcher.rb
60
75
  - lib/grumlin/step.rb
61
76
  - lib/grumlin/sugar.rb
62
77
  - lib/grumlin/t.rb
@@ -64,7 +79,7 @@ files:
64
79
  - lib/grumlin/test/rspec/db_cleaner_context.rb
65
80
  - lib/grumlin/test/rspec/gremlin_context.rb
66
81
  - lib/grumlin/translator.rb
67
- - lib/grumlin/transport/async.rb
82
+ - lib/grumlin/transport.rb
68
83
  - lib/grumlin/traversal.rb
69
84
  - lib/grumlin/typing.rb
70
85
  - lib/grumlin/u.rb
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- module Transport
5
- # A transport based on https://github.com/socketry/async
6
- # and https://github.com/socketry/async-websocket
7
- class Async
8
- attr_reader :requests
9
-
10
- def initialize(url, task: ::Async::Task.current)
11
- @task = task
12
- @endpoint = ::Async::HTTP::Endpoint.parse(url)
13
-
14
- @requests = {}
15
- @query_queue = ::Async::Queue.new
16
- end
17
-
18
- def connect
19
- raise AlreadyConnectedError if connected?
20
-
21
- @client = ::Async::WebSocket::Client.open(@endpoint)
22
- @connection = @client.connect(@endpoint.authority, @endpoint.path)
23
-
24
- @tasks_barrier = ::Async::Barrier.new(parent: @task)
25
-
26
- @tasks_barrier.async { query_task }
27
- @tasks_barrier.async { response_task }
28
- rescue StandardError
29
- raise ConnectionError
30
- end
31
-
32
- def disconnect
33
- raise NotConnectedError unless connected?
34
-
35
- @tasks_barrier.tasks.each(&:stop)
36
- @tasks_barrier.wait
37
-
38
- @connection.close
39
- @client.close
40
-
41
- @client = nil
42
- @connection = nil
43
- @tasks_barrier = nil
44
-
45
- raise ResourceLeakError, "ongoing requests list is not empty: #{@requests.count} items" unless @requests.empty?
46
- raise ResourceLeakError, "query queue empty: #{@query.count} items" unless @query_queue.empty?
47
- end
48
-
49
- # Raw message
50
- def submit(message)
51
- raise NotConnectedError unless connected?
52
-
53
- uuid = message[:requestId]
54
- ::Async::Queue.new.tap do |queue|
55
- @requests[uuid] = queue
56
- @query_queue << message
57
- end
58
- end
59
-
60
- def close_request(request_id)
61
- @requests.delete(request_id)
62
- end
63
-
64
- def ongoing_request?(request_id)
65
- @requests.key?(request_id)
66
- end
67
-
68
- def connected?
69
- !@connection.nil?
70
- end
71
-
72
- private
73
-
74
- def query_task
75
- @query_queue.each do |query|
76
- @connection.write(query)
77
- @connection.flush
78
- end
79
- rescue StandardError
80
- raise DisconnectError
81
- end
82
-
83
- def response_task
84
- loop do
85
- response = @connection.read
86
- # TODO: sometimes response does not include requestID, no idea how to handle it so far.
87
- response_queue = @requests[response[:requestId]]
88
- response_queue << [:response, response]
89
- end
90
- rescue StandardError
91
- raise DisconnectError
92
- end
93
- end
94
- end
95
- end