grumlin 0.2.0 → 0.3.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: 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