grumlin 0.2.0 → 0.5.1

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: 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