grumlin 0.1.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1696d48d04fdfc23de590598ed655a9d2fc59e66e87eb04c5cd579df3805f972
4
- data.tar.gz: cfa3d4f3ce67d35d5d1b0e5af78024ddb3461b9f4272b1a49015a3527fcc85f0
3
+ metadata.gz: 11e434ab0f5cd463d357e69dd3cbf5083f85a1bdd32e4d2c96023ce262704020
4
+ data.tar.gz: f24dd70f8b0abe9b370f77587d590802b32aa3e7b8a20380b9518004868cc6c7
5
5
  SHA512:
6
- metadata.gz: 97b8df1e4b6c02d874c9659d7c506704d21f9cfaf419c427c3e00378e3b8a3f3ca30ad53e04b0e6b18dc9161302ed63ddcfcd4df6948edd777beba435424fcf5
7
- data.tar.gz: c6b0ad7814fdefa25909c4c008ead2a130e807760eed5528d728e854ea88e2c08dbcd9dd5c30caec2c737178285e6c929c0348cf24ce01e4468c09ce72a3723d
6
+ metadata.gz: 5bfbd0d4db9ef46d35839b8ca5654cdc1f5038ee2c4ca004b678471380095d1fb587fa89a8df0c5a4769e3943ceb4e0b88734b701593ccf1047a05ab85baaaf5
7
+ data.tar.gz: a7d87e630e6b2076bedd079365e709018b3fc61f1577564c504593ffdeb4deab88f56d8785c30877489171b6c9d7fd245982475a75f4c639647c923e2ca296ef
@@ -48,7 +48,7 @@ jobs:
48
48
  needs:
49
49
  - lint
50
50
  - test
51
- # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
51
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
52
52
  steps:
53
53
  - uses: actions/checkout@v2
54
54
 
data/.overcommit.yml ADDED
@@ -0,0 +1,8 @@
1
+ PreCommit:
2
+ RuboCop:
3
+ enabled: true
4
+ on_warn: fail # Treat all warnings as failures
5
+
6
+ PrePush:
7
+ RSpec:
8
+ enabled: true
data/.rubocop.yml CHANGED
@@ -33,3 +33,6 @@ RSpec/NestedGroups:
33
33
 
34
34
  RSpec/ExampleLength:
35
35
  Enabled: false
36
+
37
+ Style/MultilineBlockChain:
38
+ Enabled: false
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "nokogiri"
7
8
  gem "rubocop"
8
9
  gem "rubocop-performance"
9
10
  gem "rubocop-rspec"
@@ -12,5 +13,6 @@ gem "solargraph"
12
13
 
13
14
  gem "async-rspec"
14
15
  gem "factory_bot"
16
+ gem "overcommit"
15
17
  gem "rspec"
16
18
  gem "simplecov"
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.1.1)
5
- async-websocket (~> 0.18)
4
+ grumlin (0.4.0)
5
+ async-pool (~> 0.3)
6
+ async-websocket (~> 0.19)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -14,33 +15,34 @@ GEM
14
15
  tzinfo (~> 2.0)
15
16
  zeitwerk (~> 2.3)
16
17
  ast (2.4.2)
17
- async (1.29.0)
18
+ async (1.30.1)
18
19
  console (~> 1.10)
19
20
  nio4r (~> 2.3)
20
21
  timers (~> 4.1)
21
- async-http (0.56.2)
22
- async (~> 1.25)
23
- async-io (~> 1.28)
24
- async-pool (~> 0.2)
22
+ async-http (0.56.5)
23
+ async (>= 1.25)
24
+ async-io (>= 1.28)
25
+ async-pool (>= 0.2)
25
26
  protocol-http (~> 0.22.0)
26
27
  protocol-http1 (~> 0.14.0)
27
28
  protocol-http2 (~> 0.14.0)
28
- async-io (1.31.0)
29
- async (~> 1.14)
30
- async-pool (0.3.6)
31
- async (~> 1.25)
29
+ async-io (1.32.2)
30
+ async
31
+ async-pool (0.3.8)
32
+ async (>= 1.25)
32
33
  async-rspec (1.16.0)
33
34
  rspec (~> 3.0)
34
35
  rspec-files (~> 1.0)
35
36
  rspec-memory (~> 1.0)
36
- async-websocket (0.18.0)
37
+ async-websocket (0.19.0)
37
38
  async-http (~> 0.54)
38
39
  async-io (~> 1.23)
39
40
  protocol-websocket (~> 0.7.0)
40
- backport (1.1.2)
41
+ backport (1.2.0)
41
42
  benchmark (0.1.1)
43
+ childprocess (4.0.0)
42
44
  concurrent-ruby (1.1.8)
43
- console (1.12.0)
45
+ console (1.13.1)
44
46
  fiber-local
45
47
  diff-lcs (1.4.4)
46
48
  docile (1.4.0)
@@ -50,20 +52,24 @@ GEM
50
52
  fiber-local (1.0.0)
51
53
  i18n (1.8.10)
52
54
  concurrent-ruby (~> 1.0)
55
+ iniparse (1.5.0)
53
56
  jaro_winkler (1.5.4)
54
57
  kramdown (2.3.1)
55
58
  rexml
56
59
  kramdown-parser-gfm (1.1.0)
57
60
  kramdown (~> 2.0)
58
61
  minitest (5.14.4)
59
- nio4r (2.5.7)
60
- nokogiri (1.11.5-x86_64-linux)
62
+ nio4r (2.5.8)
63
+ nokogiri (1.11.7-x86_64-linux)
61
64
  racc (~> 1.4)
65
+ overcommit (0.57.0)
66
+ childprocess (>= 0.6.3, < 5)
67
+ iniparse (~> 1.4)
62
68
  parallel (1.20.1)
63
69
  parser (3.0.1.1)
64
70
  ast (~> 2.4.1)
65
71
  protocol-hpack (1.4.2)
66
- protocol-http (0.22.0)
72
+ protocol-http (0.22.5)
67
73
  protocol-http1 (0.14.1)
68
74
  protocol-http (~> 0.22)
69
75
  protocol-http2 (0.14.2)
@@ -95,16 +101,16 @@ GEM
95
101
  diff-lcs (>= 1.2.0, < 2.0)
96
102
  rspec-support (~> 3.10.0)
97
103
  rspec-support (3.10.2)
98
- rubocop (1.15.0)
104
+ rubocop (1.16.1)
99
105
  parallel (~> 1.10)
100
106
  parser (>= 3.0.0.0)
101
107
  rainbow (>= 2.2.2, < 4.0)
102
108
  regexp_parser (>= 1.8, < 3.0)
103
109
  rexml
104
- rubocop-ast (>= 1.5.0, < 2.0)
110
+ rubocop-ast (>= 1.7.0, < 2.0)
105
111
  ruby-progressbar (~> 1.7)
106
112
  unicode-display_width (>= 1.4.0, < 3.0)
107
- rubocop-ast (1.5.0)
113
+ rubocop-ast (1.7.0)
108
114
  parser (>= 3.0.1.1)
109
115
  rubocop-performance (1.11.3)
110
116
  rubocop (>= 1.7.0, < 2.0)
@@ -119,10 +125,11 @@ GEM
119
125
  simplecov_json_formatter (~> 0.1)
120
126
  simplecov-html (0.12.3)
121
127
  simplecov_json_formatter (0.1.3)
122
- solargraph (0.40.4)
123
- backport (~> 1.1)
128
+ solargraph (0.43.0)
129
+ backport (~> 1.2)
124
130
  benchmark
125
131
  bundler (>= 1.17.2)
132
+ diff-lcs (~> 1.4)
126
133
  e2mmap
127
134
  jaro_winkler (~> 1.5)
128
135
  kramdown (~> 2.3)
@@ -149,6 +156,8 @@ DEPENDENCIES
149
156
  async-rspec
150
157
  factory_bot
151
158
  grumlin!
159
+ nokogiri
160
+ overcommit
152
161
  rspec
153
162
  rubocop
154
163
  rubocop-performance
data/bin/console CHANGED
@@ -5,9 +5,12 @@ require "bundler/setup"
5
5
  require "grumlin"
6
6
  require "irb"
7
7
 
8
+ Grumlin.configure do |config|
9
+ config.url = ENV["GREMLIN_URL"] || "ws://localhost:8182/gremlin"
10
+ end
11
+
8
12
  Async do
9
- client = Grumlin::Client.new("ws://localhost:8182/gremlin", mode: :bytecode)
10
- g = Grumlin::Traversal.new(client)
13
+ g = Grumlin::Traversal.new
11
14
 
12
15
  IRB.setup(nil)
13
16
  workspace = IRB::WorkSpace.new(binding)
@@ -16,5 +19,5 @@ Async do
16
19
  rescue StandardError
17
20
  raise
18
21
  ensure
19
- client.disconnect
22
+ Grumlin.config.default_pool.close
20
23
  end
data/bin/setup CHANGED
@@ -4,5 +4,6 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
+ bundle exec overcommit --sign
7
8
 
8
9
  # Do any other automated setup that you need to do here
data/grumlin.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Gleb Sinyavskiy"]
9
9
  spec.email = ["zhulik.gleb@gmail.com"]
10
10
 
11
- spec.summary = "A ruby client for Gremlin query language."
12
- spec.description = "A ruby client for Gremlin query language."
11
+ spec.summary = "Gremlin query language DSL for Ruby."
12
+ spec.description = "Gremlin query language DSL for Ruby."
13
13
  spec.homepage = "https://github.com/zhulik/grumlin"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
@@ -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-websocket", "~> 0.18"
26
+ spec.add_dependency "async-pool", "~> 0.3"
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 # rubocop:disable Metrics/MethodLength
49
+ raise(ChannelClosedError, "Cannot receive from a closed channel") if closed?
50
+
51
+ @queue.each do |type, payload|
52
+ case type
53
+ when :exception
54
+ payload.set_backtrace(caller + (payload.backtrace || [])) # A hack to preserve full backtrace
55
+ raise payload
56
+ when :payload
57
+ yield payload
58
+ when :close
59
+ break
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/grumlin.rb CHANGED
@@ -4,21 +4,67 @@ 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"
11
+ require "async/barrier"
8
12
  require "async/http/endpoint"
9
13
  require "async/websocket/client"
10
14
 
15
+ require_relative "async/channel"
16
+
11
17
  require_relative "grumlin/version"
12
18
  require_relative "grumlin/exceptions"
13
19
 
20
+ require_relative "grumlin/transport"
21
+ require_relative "grumlin/client"
22
+
14
23
  require_relative "grumlin/vertex"
15
24
  require_relative "grumlin/edge"
25
+ require_relative "grumlin/path"
16
26
  require_relative "grumlin/typing"
17
- require_relative "grumlin/client"
18
27
  require_relative "grumlin/traversal"
19
- require_relative "grumlin/step"
28
+ require_relative "grumlin/request_dispatcher"
20
29
  require_relative "grumlin/translator"
21
- require_relative "grumlin/traversing_context"
30
+
31
+ require_relative "grumlin/anonymous_step"
32
+ require_relative "grumlin/step"
33
+
34
+ require_relative "grumlin/t"
35
+ require_relative "grumlin/order"
36
+ require_relative "grumlin/u"
37
+ require_relative "grumlin/p"
38
+ require_relative "grumlin/pop"
39
+ require_relative "grumlin/sugar"
22
40
 
23
41
  module Grumlin
42
+ class Config
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
51
+
52
+ def default_pool
53
+ @default_pool ||= Async::Pool::Controller.new(Grumlin::Client::PoolResource, limit: pool_size)
54
+ end
55
+
56
+ def reset!
57
+ @default_pool = nil
58
+ end
59
+ end
60
+
61
+ class << self
62
+ def configure
63
+ yield config
64
+ end
65
+
66
+ def config
67
+ @config ||= Config.new
68
+ end
69
+ end
24
70
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class AnonymousStep
5
+ attr_reader :name, :args
6
+
7
+ def initialize(name, *args, previous_steps: [])
8
+ @name = name
9
+ @previous_steps = previous_steps
10
+ @args = args
11
+ end
12
+
13
+ %w[addV addE V E limit count drop property valueMap select from to as order by has hasLabel values hasNot
14
+ not outE groupCount label group in out fold unfold inV path dedup project coalesce repeat emit
15
+ elementMap where].each do |step|
16
+ define_method step do |*args|
17
+ add_step(step, args, previous_steps: steps)
18
+ end
19
+ end
20
+
21
+ alias addVertex addV
22
+ alias addEdge addE
23
+
24
+ def inspect
25
+ @inspect ||= to_bytecode.to_s
26
+ end
27
+
28
+ alias to_s inspect
29
+
30
+ def to_bytecode
31
+ @to_bytecode ||= (@previous_steps.last&.to_bytecode || []) + [Translator.to_bytecode(self)]
32
+ end
33
+
34
+ def steps
35
+ (@previous_steps + [self])
36
+ end
37
+
38
+ private
39
+
40
+ def add_step(step_name, args, previous_steps:)
41
+ self.class.new(step_name, *args, previous_steps: previous_steps)
42
+ end
43
+ end
44
+ end
@@ -1,172 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- class Client # rubocop:disable Metrics/ClassLength
5
- SUCCESS_STATUS = 200
6
- NO_CONTENT_STATUS = 204
7
- PARTIAL_CONTENT_STATUS = 206
8
-
9
- ERRORS = {
10
- 499 => InvalidRequestArgumentsError,
11
- 500 => ServerError,
12
- 597 => ScriptEvaluationError,
13
- 599 => ServerSerializationError,
14
- 598 => ServerTimeoutError,
15
-
16
- 401 => ClientSideError,
17
- 407 => ClientSideError,
18
- 498 => ClientSideError
19
- }.freeze
20
-
21
- def initialize(url, task: Async::Task.current, autoconnect: true, mode: :bytecode)
22
- @task = task
23
- @endpoint = Async::HTTP::Endpoint.parse(url)
24
- @mode = mode
25
-
26
- @requests = {}
27
- @query_queue = Async::Queue.new
28
-
29
- connect if autoconnect
30
- end
31
-
32
- def connect # rubocop:disable Metrics/MethodLength
33
- raise AlreadyConnectedError unless @connection_task.nil?
4
+ class Client
5
+ class PoolResource < Async::Pool::Resource
6
+ attr_reader :client
34
7
 
35
- @connection_task = @task.async do |subtask|
36
- Async::WebSocket::Client.connect(@endpoint) do |connection|
37
- subtask.async { query_task(connection) }
38
- response_task(connection)
39
- end
40
- rescue StandardError => e
41
- @requests.each_value do |queue|
42
- queue << [:error, e]
43
- end
44
- disconnect
8
+ def self.call
9
+ config = Grumlin.config
10
+ new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
45
11
  end
46
- end
47
-
48
- def disconnect
49
- raise NotConnectedError if @connection_task.nil?
50
-
51
- @connection_task&.stop
52
- @connection_task&.wait
53
- @connection_task = nil
54
- @requests = {}
55
- end
56
-
57
- def query(*args) # rubocop:disable Metrics/MethodLength
58
- response_queue, request_id = schedule_query(args)
59
- result = []
60
12
 
61
- response_queue.each do |status, response|
62
- reraise_error!(response) if status == :error
63
-
64
- status = response[:status]
65
-
66
- if status[:code] == NO_CONTENT_STATUS
67
- close_request(request_id)
68
- return []
69
- 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
70
17
 
71
- check_errors!(status, request_id)
18
+ def closed?
19
+ !@client.connected?
20
+ end
72
21
 
73
- page = Typing.cast(response.dig(:result, :data))
22
+ def close
23
+ @client.close
24
+ end
74
25
 
75
- case status[:code]
76
- when SUCCESS_STATUS
77
- close_request(request_id)
78
- return result + page
79
- when PARTIAL_CONTENT_STATUS
80
- result += page
81
- else
82
- raise UnknownResponseStatus, status
83
- end
26
+ def write(*args)
27
+ @client.write(*args)
84
28
  end
85
29
  end
86
30
 
87
- private
88
-
89
- def schedule_query(args)
90
- uuid = SecureRandom.uuid
91
- queue = Async::Queue.new
92
- @requests[uuid] = queue
93
- @query_queue << to_query(uuid, args)
94
-
95
- [queue, uuid]
31
+ def initialize(url, parent: Async::Task.current, **client_options)
32
+ @url = url
33
+ @client_options = client_options
34
+ @parent = parent
35
+ reset!
96
36
  end
97
37
 
98
- def to_query(uuid, message)
99
- case message.first
100
- when String
101
- string_query_message(uuid, *message)
102
- when Grumlin::Step
103
- build_query(uuid, message)
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)
45
+ end
46
+ rescue StandardError
47
+ close
104
48
  end
105
49
  end
106
50
 
107
- def check_errors!(status, request_id)
108
- error = ERRORS[status[:code]]
109
- close_request(request_id)
110
- raise(error, status) if error
111
- end
51
+ def close
52
+ @transport.close
53
+ raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
112
54
 
113
- def close_request(request_id)
114
- @requests.delete(request_id)
55
+ reset!
115
56
  end
116
57
 
117
- def reraise_error!(error)
118
- raise error
119
- rescue StandardError
120
- raise ConnectionError
58
+ def connected?
59
+ @transport&.connected? || false
121
60
  end
122
61
 
123
- def query_task(connection)
124
- loop do
125
- connection.write @query_queue.dequeue
126
- connection.flush
127
- end
128
- end
62
+ # TODO: support yielding
63
+ def write(*args) # rubocop:disable Metrics/MethodLength
64
+ raise NotConnectedError unless connected?
65
+
66
+ request_id = SecureRandom.uuid
67
+ request = to_query(request_id, args)
68
+ channel = @request_dispatcher.add_request(request)
69
+ @transport.write(request)
129
70
 
130
- def response_task(connection)
131
- loop do
132
- response = connection.read
133
- response_queue = @requests[response[:requestId]]
134
- response_queue << [:response, response]
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"
135
76
  end
136
77
  end
137
78
 
138
- def string_query_message(uuid, query, bindings)
139
- {
140
- requestId: uuid,
141
- op: "eval",
142
- processor: "",
143
- args: {
144
- gremlin: query,
145
- bindings: bindings,
146
- language: "gremlin-groovy"
147
- }
148
- }
79
+ def inspect
80
+ "<#{self.class} url=#{@url} connected=#{connected?}>"
149
81
  end
150
82
 
151
- def bytecode_query_message(uuid, bytecode)
83
+ alias to_s inspect
84
+
85
+ private
86
+
87
+ def to_query(request_id, message)
152
88
  {
153
- requestId: uuid,
89
+ requestId: request_id,
154
90
  op: "bytecode",
155
91
  processor: "traversal",
156
92
  args: {
157
- gremlin: { "@type": "g:Bytecode", "@value": { step: bytecode } },
93
+ gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
158
94
  aliases: { g: :g }
159
95
  }
160
96
  }
161
97
  end
162
98
 
163
- def build_query(uuid, steps)
164
- case @mode
165
- when :string
166
- string_query_message(uuid, *Translator.to_string_query(steps))
167
- else
168
- bytecode_query_message(uuid, Translator.to_bytecode_query(steps))
169
- end
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)
170
106
  end
171
107
  end
172
108
  end