grumlin 0.2.0 → 0.5.1

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: 5fb5b809483aeed3b56aeafc11e0f08919a68ce420a14ae918e3aecab791e266
4
+ data.tar.gz: e42c886a034faeb5ca512882811e78a39f1f5f386b697b0f4061c5be5d5a5f0b
5
5
  SHA512:
6
- metadata.gz: 3846fe491a606989f51f0f609e8c7ae2049b8a1005272ea531f085402e7d998a7f8723ccdafae01ce7d913f9ad9272f9da1e86a307cb7aeb5066ef4ad4024e4b
7
- data.tar.gz: 1b20d251d16c95cb12657c6becbbb4592b8e206e9e83a7defee42281bbdd7c3f84d04785b2cc89bcc1328d05d11aa14b865b6bc6832899d3a73ce7b86283be5b
6
+ metadata.gz: 2bd8e23cd4e6ab7526e2e4aaf92dddfa2b1e0dc51fd2e15e9e0a09541e9716c741673a8e6f85b077e5fd1a3a5f16519ae533748347b1aff0ba5de47a8394ed47
7
+ data.tar.gz: 673985aeb319810c061234e1d0964cf6e40b95363288423d7cda4e0932a7e271d51b957dccc5605a160e54e680d2a869bfa32769b393d19387e61e0ad15b7794
@@ -1,6 +1,6 @@
1
1
  name: Ruby
2
2
 
3
- on: [push, pull_request]
3
+ on: [push]
4
4
 
5
5
  jobs:
6
6
  lint:
data/.rubocop.yml CHANGED
@@ -7,24 +7,44 @@ require:
7
7
  - rubocop-performance
8
8
  - rubocop-rspec
9
9
 
10
- Style/StringLiterals:
11
- Enabled: true
12
- EnforcedStyle: double_quotes
13
-
14
- Style/StringLiteralsInInterpolation:
15
- Enabled: true
16
- EnforcedStyle: double_quotes
17
-
18
- Style/Documentation:
19
- Enabled: false
20
-
21
10
  Layout/LineLength:
22
11
  Max: 120
12
+ Exclude:
13
+ - spec/**/*_spec.rb
23
14
 
24
15
  Metrics/BlockLength:
25
16
  Exclude:
26
17
  - spec/**/*_spec.rb
27
18
 
19
+ Metrics/MethodLength:
20
+ Max: 20
21
+
22
+ Metrics/ParameterLists:
23
+ Max: 6
24
+
25
+ Naming/MethodName:
26
+ IgnoredPatterns:
27
+ - toList
28
+ - inVLabel
29
+ - outVLabel
30
+ - inV
31
+ - outV
32
+
33
+ Naming/VariableName:
34
+ AllowedIdentifiers:
35
+ - inV
36
+ - outV
37
+ - inVLabel
38
+ - outVLabel
39
+
40
+ Naming/MethodParameterName:
41
+ AllowedNames:
42
+ - id
43
+ - inV
44
+ - outV
45
+ - inVLabel
46
+ - outVLabel
47
+
28
48
  RSpec/NamedSubject:
29
49
  Enabled: false
30
50
 
@@ -34,5 +54,26 @@ RSpec/NestedGroups:
34
54
  RSpec/ExampleLength:
35
55
  Enabled: false
36
56
 
57
+ RSpec/MultipleExpectations:
58
+ Enabled: false
59
+
60
+ RSpec/DescribeClass:
61
+ Enabled: false
62
+
63
+ Style/WordArray:
64
+ Exclude:
65
+ - spec/**/*_spec.rb
66
+
67
+ Style/StringLiterals:
68
+ Enabled: true
69
+ EnforcedStyle: double_quotes
70
+
71
+ Style/StringLiteralsInInterpolation:
72
+ Enabled: true
73
+ EnforcedStyle: double_quotes
74
+
75
+ Style/Documentation:
76
+ Enabled: false
77
+
37
78
  Style/MultilineBlockChain:
38
- Enabled: false
79
+ Enabled: false
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.5.1)
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)
@@ -58,7 +59,7 @@ GEM
58
59
  kramdown-parser-gfm (1.1.0)
59
60
  kramdown (~> 2.0)
60
61
  minitest (5.14.4)
61
- nio4r (2.5.7)
62
+ nio4r (2.5.8)
62
63
  nokogiri (1.11.7-x86_64-linux)
63
64
  racc (~> 1.4)
64
65
  overcommit (0.57.0)
@@ -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
@@ -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
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
@@ -2,120 +2,107 @@
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 < Async::Pool::Resource
6
+ attr_reader :client
30
7
 
31
- def_delegators :@transport, :connect, :disconnect, :requests
8
+ def self.call
9
+ config = Grumlin.config
10
+ new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
11
+ end
32
12
 
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
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
+ end
41
17
 
42
- def inspect
43
- "<#{self.class} @url=#{@url}>"
44
- end
18
+ def closed?
19
+ !@client.connected?
20
+ end
45
21
 
46
- alias to_s inspect
22
+ def close
23
+ @client.close
24
+ end
47
25
 
48
- private
26
+ def write(*args)
27
+ @client.write(*args)
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, **client_options)
32
+ @url = url
33
+ @client_options = client_options
34
+ @parent = parent
35
+ reset!
36
+ end
53
37
 
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 []
38
+ def connect
39
+ @transport = build_transport
40
+ response_channel = @transport.connect
41
+ @request_dispatcher = RequestDispatcher.new
42
+ @parent.async do
43
+ response_channel.each do |response|
44
+ @request_dispatcher.add_response(response)
60
45
  end
46
+ rescue StandardError
47
+ close
61
48
  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
49
  end
66
50
 
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
51
+ def close
52
+ @transport.close
53
+ raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
54
+
55
+ reset!
74
56
  end
75
57
 
76
- def check_errors!(_request_id, status, response)
77
- reraise_error!(response) if status == :error
58
+ def connected?
59
+ @transport&.connected? || false
60
+ end
78
61
 
79
- status = response[:status]
62
+ # TODO: support yielding
63
+ def write(*args)
64
+ raise NotConnectedError unless connected?
80
65
 
81
- if (error = ERRORS[status[:code]])
82
- raise(error, status)
66
+ request_id = SecureRandom.uuid
67
+ request = to_query(request_id, args)
68
+ channel = @request_dispatcher.add_request(request)
69
+ @transport.write(request)
70
+
71
+ begin
72
+ channel.dequeue.flat_map { |item| Typing.cast(item) }
73
+ rescue Async::Stop
74
+ retry if @request_dispatcher.ongoing_request?(request_id)
75
+ raise Grumlin::UnknownRequestStoppedError, "#{request_id} is not in the ongoing requests list"
83
76
  end
84
-
85
- return unless SUCCESS[status[:code]].nil?
86
-
87
- raise(UnknownResponseStatus, status)
88
77
  end
89
78
 
90
- def reraise_error!(error)
91
- raise error
92
- rescue StandardError
93
- raise UnknownError
79
+ def inspect
80
+ "<#{self.class} url=#{@url} connected=#{connected?}>"
94
81
  end
95
82
 
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
83
+ alias to_s inspect
84
+
85
+ private
108
86
 
109
- def bytecode_query_message(request_id, bytecode)
87
+ def to_query(request_id, message)
110
88
  {
111
89
  requestId: request_id,
112
90
  op: "bytecode",
113
91
  processor: "traversal",
114
92
  args: {
115
- gremlin: Typing.to_bytecode(bytecode),
93
+ gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
116
94
  aliases: { g: :g }
117
95
  }
118
96
  }
119
97
  end
98
+
99
+ def reset!
100
+ @request_dispatcher = nil
101
+ @transport = nil
102
+ end
103
+
104
+ def build_transport
105
+ Transport.new(@url, parent: @parent, **@client_options)
106
+ end
120
107
  end
121
108
  end
data/lib/grumlin/edge.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Naming/VariableName,Naming/MethodParameterName,Naming/MethodName
4
3
  module Grumlin
5
4
  class Edge
6
5
  attr_reader :label, :id, :inVLabel, :outVLabel, :inV, :outV
7
6
 
8
- def initialize(label:, id:, inVLabel:, outVLabel:, inV:, outV:) # rubocop:disable Metrics/ParameterLists
7
+ def initialize(label:, id:, inVLabel:, outVLabel:, inV:, outV:)
9
8
  @label = label
10
9
  @id = Typing.cast(id)
11
10
  @inVLabel = inVLabel
@@ -24,4 +23,3 @@ module Grumlin
24
23
  alias to_s inspect
25
24
  end
26
25
  end
27
- # rubocop:enable Naming/MethodParameterName,Naming/VariableName,Naming/MethodName
data/lib/grumlin/order.rb CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  module Grumlin
4
4
  module Order
5
- module Order
5
+ class << self
6
6
  DESC = { "@type": "g:Order", "@value": "desc" }.freeze
7
- ASC = { "@type": "g:Order", "@value": "desc" }.freeze
8
-
9
- extend self # rubocop:disable Style/ModuleFunction
7
+ ASC = { "@type": "g:Order", "@value": "asc" }.freeze
10
8
 
11
9
  def asc
12
10
  ASC
@@ -16,7 +14,5 @@ module Grumlin
16
14
  DESC
17
15
  end
18
16
  end
19
-
20
- extend Order
21
17
  end
22
18
  end
data/lib/grumlin/pop.rb CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Grumlin
4
4
  module Pop
5
- module Pop
6
- extend self # rubocop:disable Style/ModuleFunction
7
-
5
+ class << self
8
6
  FIRST = { "@type": "g:Pop", "@value": "first" }.freeze
9
7
  LAST = { "@type": "g:Pop", "@value": "last" }.freeze
10
8
  ALL = { "@type": "g:Pop", "@value": "all" }.freeze
@@ -26,7 +24,5 @@ module Grumlin
26
24
  MIXED
27
25
  end
28
26
  end
29
-
30
- extend Pop
31
27
  end
32
28
  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::Channel.new.tap do |channel|
33
+ @requests[request[:requestId]] = { request: request, result: [], channel: channel }
34
+ end
35
+ end
36
+
37
+ # builds a response object, when it's ready sends it to the client via a channel
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
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[:channel] << 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[:channel] << []
54
+ close_request(request_id)
55
+ end
56
+ rescue StandardError => e
57
+ request[:channel].exception(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[:channel].close
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
@@ -14,18 +14,22 @@ module Grumlin
14
14
  @enum.next
15
15
  end
16
16
 
17
- def toList # rubocop:disable Naming/MethodName
18
- @toList ||= @client.submit(*steps) # rubocop:disable Naming/VariableName
17
+ def toList
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
data/lib/grumlin/sugar.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Grumlin
4
4
  module Sugar
5
- # TODO: how to use it in specs?
6
5
  HELPERS = [
7
6
  Grumlin::U,
8
7
  Grumlin::T,
@@ -12,25 +11,12 @@ module Grumlin
12
11
  ].freeze
13
12
 
14
13
  def self.included(base)
15
- base.extend ClassMethods
16
- end
17
-
18
- module ClassMethods
19
- def const_missing(name)
20
- helper = HELPERS.find { |h| h.const_defined?(name) }
21
- super if helper.nil?
22
-
23
- const_set(name, helper)
14
+ HELPERS.each do |helper|
15
+ name = helper.name.split("::").last
16
+ base.const_set(name, helper)
24
17
  end
25
18
  end
26
19
 
27
- def const_missing(name)
28
- helper = HELPERS.find { |h| h.const_defined?(name) }
29
- super if helper.nil?
30
-
31
- const_set(name, helper)
32
- end
33
-
34
20
  def __
35
21
  Grumlin::U
36
22
  end
data/lib/grumlin/t.rb CHANGED
@@ -2,12 +2,10 @@
2
2
 
3
3
  module Grumlin
4
4
  module T
5
- module T
5
+ class << self
6
6
  T_ID = { :@type => "g:T", :@value => "id" }.freeze # TODO: replace with a class?
7
7
  T_LABEL = { :@type => "g:T", :@value => "label" }.freeze # TODO: replace with a class?
8
8
 
9
- extend self # rubocop:disable Style/ModuleFunction
10
-
11
9
  def id
12
10
  T_ID
13
11
  end
@@ -16,7 +14,5 @@ module Grumlin
16
14
  T_LABEL
17
15
  end
18
16
  end
19
-
20
- extend T
21
17
  end
22
18
  end
@@ -8,12 +8,17 @@ module Grumlin
8
8
 
9
9
  ::RSpec.shared_context GremlinContext do
10
10
  include GremlinContext
11
+ include Grumlin::Sugar
11
12
 
12
- let(:g) { Grumlin::Traversal.new }
13
+ before do
14
+ Grumlin::Sugar::HELPERS.each do |helper|
15
+ name = helper.name.split("::").last
16
+ stub_const(name, helper)
17
+ end
18
+ end
13
19
 
14
20
  after do
15
- expect(Grumlin.config.default_client.requests).to be_empty
16
- Grumlin.config.default_client.disconnect
21
+ Grumlin.config.default_pool.close
17
22
  Grumlin.config.reset!
18
23
  end
19
24
  end
@@ -0,0 +1,86 @@
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
+
8
+ attr_reader :url
9
+
10
+ def initialize(url, parent: Async::Task.current, **client_options)
11
+ @url = url
12
+ @parent = parent
13
+ @client_options = client_options
14
+ @request_channel = Async::Channel.new
15
+ @response_channel = Async::Channel.new
16
+ reset!
17
+ end
18
+
19
+ def connected?
20
+ @connected
21
+ end
22
+
23
+ def connect # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
24
+ raise AlreadyConnectedError if connected?
25
+
26
+ @connection = Async::WebSocket::Client.connect(Async::HTTP::Endpoint.parse(@url), **@client_options)
27
+
28
+ @response_task = @parent.async do
29
+ loop do
30
+ data = @connection.read
31
+ @response_channel << data
32
+ end
33
+ rescue Async::Stop
34
+ @response_channel.close
35
+ rescue StandardError => e
36
+ @response_channel.exception(e)
37
+ end
38
+
39
+ @request_task = @parent.async do
40
+ @request_channel.each do |message|
41
+ @connection.write(message)
42
+ @connection.flush
43
+ end
44
+ rescue StandardError => e
45
+ @response_channel.exception(e)
46
+ end
47
+
48
+ @connected = true
49
+
50
+ @response_channel
51
+ end
52
+
53
+ def write(message)
54
+ raise NotConnectedError unless connected?
55
+
56
+ @request_channel << message
57
+ end
58
+
59
+ def close
60
+ return unless connected?
61
+
62
+ @request_channel.close
63
+ @request_task.wait
64
+
65
+ @response_task.stop
66
+ @response_task.wait
67
+
68
+ begin
69
+ @connection.close
70
+ rescue Errno::EPIPE
71
+ nil
72
+ end
73
+
74
+ reset!
75
+ end
76
+
77
+ private
78
+
79
+ def reset!
80
+ @connected = false
81
+ @connection = nil
82
+ @response_task = nil
83
+ @request_task = nil
84
+ end
85
+ end
86
+ 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
 
data/lib/grumlin/u.rb CHANGED
@@ -2,17 +2,12 @@
2
2
 
3
3
  module Grumlin
4
4
  module U
5
- module U
6
- extend self # rubocop:disable Style/ModuleFunction
7
-
5
+ class << self
8
6
  %w[addV V has count out values unfold].each do |step|
9
7
  define_method step do |*args|
10
8
  AnonymousStep.new(step, *args)
11
9
  end
12
10
  end
13
11
  end
14
-
15
- # TODO: add alias __
16
- extend U
17
12
  end
18
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -4,27 +4,33 @@ 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"
10
13
  require "async/websocket/client"
11
14
 
15
+ require_relative "async/channel"
16
+
12
17
  require_relative "grumlin/version"
13
18
  require_relative "grumlin/exceptions"
14
19
 
15
- require_relative "grumlin/transport/async"
20
+ require_relative "grumlin/transport"
21
+ require_relative "grumlin/client"
16
22
 
17
23
  require_relative "grumlin/vertex"
18
24
  require_relative "grumlin/edge"
19
25
  require_relative "grumlin/path"
20
26
  require_relative "grumlin/typing"
21
- require_relative "grumlin/client"
22
27
  require_relative "grumlin/traversal"
28
+ require_relative "grumlin/request_dispatcher"
29
+ require_relative "grumlin/translator"
23
30
 
24
31
  require_relative "grumlin/anonymous_step"
25
32
  require_relative "grumlin/step"
26
33
 
27
- require_relative "grumlin/translator"
28
34
  require_relative "grumlin/t"
29
35
  require_relative "grumlin/order"
30
36
  require_relative "grumlin/u"
@@ -34,14 +40,21 @@ require_relative "grumlin/sugar"
34
40
 
35
41
  module Grumlin
36
42
  class Config
37
- attr_accessor :url
43
+ attr_accessor :url, :pool_size, :client_concurrency, :client_factory
44
+
45
+ # For some reason, client_concurrency must be greater than pool_size
46
+ def initialize
47
+ @pool_size = 10
48
+ @client_concurrency = 20
49
+ @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
50
+ end
38
51
 
39
- def default_client
40
- @default_client ||= Grumlin::Client.new(url)
52
+ def default_pool
53
+ @default_pool ||= Async::Pool::Controller.new(Grumlin::Client::PoolResource, limit: pool_size)
41
54
  end
42
55
 
43
56
  def reset!
44
- @default_client = nil
57
+ @default_pool = nil
45
58
  end
46
59
  end
47
60
 
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.5.1
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-08-23 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
@@ -48,6 +62,7 @@ files:
48
62
  - gremlin_server/Dockerfile
49
63
  - gremlin_server/tinkergraph-empty.properties
50
64
  - grumlin.gemspec
65
+ - lib/async/channel.rb
51
66
  - lib/grumlin.rb
52
67
  - lib/grumlin/anonymous_step.rb
53
68
  - lib/grumlin/client.rb
@@ -57,6 +72,7 @@ files:
57
72
  - lib/grumlin/p.rb
58
73
  - lib/grumlin/path.rb
59
74
  - lib/grumlin/pop.rb
75
+ - lib/grumlin/request_dispatcher.rb
60
76
  - lib/grumlin/step.rb
61
77
  - lib/grumlin/sugar.rb
62
78
  - lib/grumlin/t.rb
@@ -64,7 +80,7 @@ files:
64
80
  - lib/grumlin/test/rspec/db_cleaner_context.rb
65
81
  - lib/grumlin/test/rspec/gremlin_context.rb
66
82
  - lib/grumlin/translator.rb
67
- - lib/grumlin/transport/async.rb
83
+ - lib/grumlin/transport.rb
68
84
  - lib/grumlin/traversal.rb
69
85
  - lib/grumlin/typing.rb
70
86
  - 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