grumlin 0.20.2 → 0.22.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: e2a547fb7cbcfe53c2fab5486236b4d1021b8c4fe487307003571b24ea9e04d8
4
- data.tar.gz: b1f73d016d9779f63a0d19c2c02afc5b975544a479aa9e9a1f5424edfde2911f
3
+ metadata.gz: 519d93b97b2cd8f07a1e11e75e45b0b7ca780947e82b3624d5e6c7c6d719ec27
4
+ data.tar.gz: 5eed9eab9e99fe02246995adbc8c01988bd21d150df234946a575d01cda518f2
5
5
  SHA512:
6
- metadata.gz: 5e8c6fa1bea702a974a9c0b22ec7c3f914ccceaf7c12f4bc8b5f8ce94eace7edd6dce4e47f318189e3e1b7af8c5e8ace6b00479283823274e3badfe25b6b0b07
7
- data.tar.gz: cbb5bbfd192542785f041157232097ed01fe0d1444c20fc7c5606c1873229a7d7577183b8919f475085b5e7bf6700fae6cc987e0024fe6263cfab61f44ce898c
6
+ metadata.gz: 76cd4990acd94d119b2a716e25b10d35bdc970e794a77de4d25dcc7e3f67f5129236ff1c037a4f0f0c3208643afca9695b6dc602bb84ae01d61a84c1b89364fe
7
+ data.tar.gz: afda51dc7010f1fc508e96b203727ebee971597b6425aa2ec29932d7957e1b2b12d8959526a5fc10d92e46926e94dab1ca3d62eb583bbb36498f99bbd3e11689
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.20.2)
4
+ grumlin (0.22.0)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
  oj (~> 3.13)
@@ -21,11 +21,11 @@ GEM
21
21
  console (~> 1.10)
22
22
  nio4r (~> 2.3)
23
23
  timers (~> 4.1)
24
- async-http (0.56.6)
24
+ async-http (0.57.0)
25
25
  async (>= 1.25)
26
26
  async-io (>= 1.28)
27
27
  async-pool (>= 0.2)
28
- protocol-http (~> 0.22.0)
28
+ protocol-http (~> 0.23.1)
29
29
  protocol-http1 (~> 0.14.0)
30
30
  protocol-http2 (~> 0.14.0)
31
31
  traces (~> 0.4.0)
@@ -37,7 +37,7 @@ GEM
37
37
  rspec (~> 3.0)
38
38
  rspec-files (~> 1.0)
39
39
  rspec-memory (~> 1.0)
40
- async-websocket (0.19.0)
40
+ async-websocket (0.19.2)
41
41
  async-http (~> 0.54)
42
42
  async-io (~> 1.23)
43
43
  protocol-websocket (~> 0.7.0)
@@ -72,7 +72,7 @@ GEM
72
72
  racc (~> 1.4)
73
73
  nokogiri (1.13.6-x86_64-linux)
74
74
  racc (~> 1.4)
75
- oj (3.13.17)
75
+ oj (3.13.20)
76
76
  overcommit (0.59.1)
77
77
  childprocess (>= 0.6.3, < 5)
78
78
  iniparse (~> 1.4)
@@ -81,7 +81,7 @@ GEM
81
81
  parser (3.1.2.0)
82
82
  ast (~> 2.4.1)
83
83
  protocol-hpack (1.4.2)
84
- protocol-http (0.22.6)
84
+ protocol-http (0.23.1)
85
85
  protocol-http1 (0.14.4)
86
86
  protocol-http (~> 0.22)
87
87
  protocol-http2 (0.14.2)
data/README.md CHANGED
@@ -46,9 +46,38 @@ Or install it yourself as:
46
46
  ```ruby
47
47
  Grumlin.configure do |config|
48
48
  config.url = "ws://localhost:8182/gremlin"
49
+
50
+ # make sure you select right provider for better compatibility
51
+ config.provider = :tinkergraph
49
52
  end
50
53
  ```
51
54
 
55
+ #### Providers
56
+
57
+ Currently `Grumlin` supports 2 providers:
58
+ - tinkergraph (default)
59
+ - neptune
60
+
61
+ As different providers may have or may have not support for specific features it's recommended to
62
+ explicitly specify the provider you use.
63
+
64
+ #### Provider features
65
+
66
+ Every provider is described by a set of features. In the future `Grumlin` may decide to disable or enable
67
+ some parts of it's functionality to comply provider's supported features.
68
+
69
+ To check current providers supported features use
70
+
71
+ ```ruby
72
+ Grumlin.features
73
+ ```
74
+
75
+ Current differences between providers:
76
+
77
+ | Feature | TinkerGraph |AWS Neptune|
78
+ |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
79
+ | Transactions | Transaction semantic is ignoroed, data is always writen, `tx.rollback` does nothing, an info is printed every time transactions are used with TinkerGraph |Full support
80
+
52
81
  ### Traversing graphs
53
82
 
54
83
  **Warning**: Not all steps and expressions defined in the reference documentation are supported.
@@ -216,21 +245,32 @@ Each `return_mode` is mapped to a particular termination step:
216
245
  - `:traversal` - do not execute the query and return the traversal as is
217
246
 
218
247
  `Grumlin::Repository` also provides a set of generic CRUD operations:
219
- - `add_vertex(label, id = nil, **properties)`
220
- - `add_edge(label, id = nil, from:, to:, **properties)`
221
- - `drop_vertex(id)`
222
- - `drop_edge(id = nil, from: nil, to: nil, label: nil)`
248
+ - `add_vertex(label, id = nil, start: g, **properties)`
249
+ - `add_edge(label, id = nil, from:, to:, start: g, **properties)`
250
+ - `drop_vertex(id, start: g)`
251
+ - `drop_edge(id = nil, from: nil, to: nil, label: nil, start: g)`
223
252
 
224
253
  and a few methods that emulate upserts:
225
- - `upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
226
- - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
227
- - `upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)`
228
- - `upsert_vertices(edges, batch_size: 100, on_failure: :retry, **params)`
254
+ - `upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, start: g, **params)`
255
+ - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, start: g, **params)`
256
+ - `upsert_edges(edges, batch_size: 100, on_failure: :retry, start: g, **params)`
257
+ - `upsert_vertices(edges, batch_size: 100, on_failure: :retry, start: g, **params)`
229
258
 
230
259
  All of them support 3 different modes for error handling: `:retry`, `:ignore` and `:raise`. Retry mode is implemented
231
260
  with [retryable](https://github.com/nfedyashev/retryable). **params will be merged to the default config for upserts
232
261
  and passed to `Retryable.retryable`. In case if you want to modify retryable behaviour you are to do so.
233
262
 
263
+ If you want to use these methods inside a transaction simply pass your `gtx` as `start` parameter:
264
+ ```ruby
265
+ g.tx do |gtx|
266
+ add_vertex(:vertex, start: gtx)
267
+ end
268
+ ```
269
+
270
+ If you don't want to define you own repository, simply use
271
+
272
+ `Grumlin::Repository.new` returns an instance of an anonymous class extending `Grumlin::Repository`.
273
+
234
274
  **Usage**
235
275
 
236
276
  To execute the query defined in a query block one simply needs to call a method with the same name:
@@ -254,6 +294,24 @@ it may be useful for debugging. Note that one needs to call a termination step m
254
294
 
255
295
  method will return profiling data of the results.
256
296
 
297
+ #### Transactions
298
+
299
+ Since 0.22.0 `Grumlin` supports transactions when working with providers that supports them:
300
+ ```ruby
301
+ # Using Transaction directly
302
+ tx = g.tx
303
+ gtx = tx.begin
304
+ gtx.addV(:vertex).iterate
305
+ tx.commit # or tx.rollback
306
+
307
+ # Using with a block
308
+ g.tx do |gtx|
309
+ gtx.addV(:vertex).iterate
310
+ # raise Grumlin::Rollback to manually rollback
311
+ # any other exception will also rollback the transaction and will be reraised
312
+ end # commits automatically
313
+ ```
314
+
257
315
  #### IRB
258
316
 
259
317
  An example of how to start an IRB session with support for executing gremlin queries:
@@ -1,4 +1,4 @@
1
- FROM tinkerpop/gremlin-server
1
+ FROM tinkerpop/gremlin-server:3.5.3
2
2
 
3
3
  RUN rm -rf /opt/gremlin-server/ext/tinkergraph-gremlin
4
4
 
data/grumlin.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.metadata["homepage_uri"] = spec.homepage
22
22
  spec.metadata["source_code_uri"] = "https://github.com/zhulik/grumlin"
23
- spec.metadata["changelog_uri"] = "https://github.com/zhulik/grumlin/blob/master/CHANGELOG.md"
23
+ spec.metadata["changelog_uri"] = "https://github.com/babbel/grumlin/releases"
24
24
  spec.metadata["rubygems_mfa_required"] = "true"
25
25
 
26
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -4,18 +4,19 @@ module Grumlin
4
4
  class Action < Steppable
5
5
  attr_reader :name, :args, :params, :next_step, :configuration_steps, :previous_step, :shortcut
6
6
 
7
- def initialize(name, args: [], params: {}, previous_step: nil, pool: nil)
8
- super()
7
+ # client is only used when a traversal is a part of transaction
8
+ def initialize(name, args: [], params: {}, previous_step: nil, pool: Grumlin.default_pool, session_id: nil)
9
+ super(pool: pool, session_id: session_id)
10
+
9
11
  @name = name.to_sym
10
12
  @args = args # TODO: add recursive validation: only json types or Action
11
13
  @params = params # TODO: add recursive validation: only json types
12
14
  @previous_step = previous_step
13
15
  @shortcut = shortcuts[@name]
14
- @pool = pool || Grumlin.default_pool
15
16
  end
16
17
 
17
18
  def configuration_step?
18
- CONFIGURATION_STEPS.include?(@name)
19
+ CONFIGURATION_STEPS.include?(@name) || name.to_sym == :tx
19
20
  end
20
21
 
21
22
  def start_step?
@@ -73,14 +74,18 @@ module Grumlin
73
74
  end
74
75
 
75
76
  def toList
76
- @pool.acquire do |client|
77
- client.write(bytecode)
78
- end
77
+ client_write(bytecode)
79
78
  end
80
79
 
81
80
  def iterate
81
+ client_write(bytecode(no_return: true))
82
+ end
83
+
84
+ private
85
+
86
+ def client_write(payload)
82
87
  @pool.acquire do |client|
83
- client.write(bytecode(no_return: true))
88
+ client.write(payload, session_id: @session_id)
84
89
  end
85
90
  end
86
91
  end
@@ -24,8 +24,8 @@ module Grumlin
24
24
  @client.close
25
25
  end
26
26
 
27
- def write(bytecode)
28
- @client.write(bytecode)
27
+ def write(bytecode, session_id: nil)
28
+ @client.write(bytecode, session_id: session_id)
29
29
  ensure
30
30
  @count += 1
31
31
  end
@@ -94,14 +94,14 @@ module Grumlin
94
94
  end
95
95
 
96
96
  # TODO: support yielding
97
- def write(bytecode)
97
+ def write(bytecode, session_id: nil)
98
98
  raise NotConnectedError unless connected?
99
99
 
100
- request = to_query(bytecode)
101
- channel = @request_dispatcher.add_request(request)
102
- @transport.write(request)
100
+ request = to_query(bytecode, session_id: session_id)
103
101
 
102
+ channel = @request_dispatcher.add_request(request)
104
103
  begin
104
+ @transport.write(request)
105
105
  channel.dequeue.flat_map { |item| Typing.cast(item) }
106
106
  rescue Async::Stop, Async::TimeoutError
107
107
  close(check_requests: false)
@@ -124,15 +124,19 @@ module Grumlin
124
124
  Transport.new(@url, parent: @parent, **@client_options)
125
125
  end
126
126
 
127
- def to_query(bytecode)
127
+ def to_query(bytecode, session_id:)
128
128
  {
129
129
  requestId: SecureRandom.uuid,
130
- op: "bytecode",
131
- processor: "traversal",
130
+ op: :bytecode,
131
+ processor: session_id ? :session : :traversal,
132
132
  args: {
133
- gremlin: { :@type => "g:Bytecode", :@value => bytecode.serialize },
134
- aliases: { g: :g }
135
- }
133
+ gremlin: {
134
+ :@type => "g:Bytecode",
135
+ :@value => bytecode.serialize
136
+ },
137
+ aliases: { g: :g },
138
+ session: session_id
139
+ }.compact
136
140
  }
137
141
  end
138
142
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Config
5
+ attr_accessor :url, :pool_size, :client_concurrency, :client_factory, :provider
6
+
7
+ SUPPORTED_PROVIDERS = %i[neptune tinkergraph].freeze
8
+
9
+ class ConfigurationError < Grumlin::Error; end
10
+
11
+ class UnknownProviderError < ConfigurationError; end
12
+
13
+ def initialize
14
+ @pool_size = 10
15
+ @client_concurrency = 5
16
+ @provider = :tinkergraph
17
+ @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
18
+ end
19
+
20
+ def validate!
21
+ return if SUPPORTED_PROVIDERS.include?(provider.to_sym)
22
+
23
+ raise UnknownProviderError, "provider '#{provider}' is unknown. Supported providers: #{SUPPORTED_PROVIDERS}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class DummyTransaction < Transaction
5
+ attr_reader :uuid
6
+
7
+ include Console
8
+
9
+ def initialize(traversal_start_class, pool: Grumlin.default_pool) # rubocop:disable Lint/MissingSuper, Lint/UnusedMethodArgument
10
+ @traversal_start_class = traversal_start_class
11
+
12
+ logger.info(self) do
13
+ "#{Grumlin.config.provider} does not support transactions. commit and rollback are ignored, data will be saved"
14
+ end
15
+ end
16
+
17
+ def commit
18
+ nil
19
+ end
20
+
21
+ def rollback
22
+ nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class FeatureList
6
+ def user_supplied_ids?
7
+ raise(NotImplementedError) if @user_supplied_ids.nil?
8
+
9
+ @user_supplied_ids
10
+ end
11
+
12
+ def supports_transactions?
13
+ raise(NotImplementedError) if @supports_transactions.nil?
14
+
15
+ @supports_transactions
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class NeptuneFeatures < FeatureList
6
+ def initialize
7
+ super
8
+ @user_supplied_ids = true
9
+ @supports_transactions = true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class TinkergraphFeatures < FeatureList
6
+ def initialize
7
+ super
8
+ @user_supplied_ids = true
9
+ @supports_transactions = false
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class << self
6
+ FEATURES = {
7
+ neptune: NeptuneFeatures.new,
8
+ tinkergraph: TinkergraphFeatures.new
9
+ }.freeze
10
+
11
+ def for(provider)
12
+ FEATURES[provider]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -8,7 +8,7 @@ module Grumlin
8
8
  extend Forwardable
9
9
 
10
10
  UPSERT_RETRY_PARAMS = {
11
- on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentInsertFailedError],
11
+ on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentModificationError],
12
12
  sleep_method: ->(n) { Async::Task.current.sleep(n) },
13
13
  tries: 3,
14
14
  sleep: ->(n) { (n**2) + 1 + rand }
@@ -22,51 +22,51 @@ module Grumlin
22
22
  self.class.shortcuts
23
23
  end
24
24
 
25
- def drop_vertex(id)
26
- g.V(id).drop.iterate
25
+ def drop_vertex(id, start: g)
26
+ start.V(id).drop.iterate
27
27
  end
28
28
 
29
- def drop_edge(id = nil, from: nil, to: nil, label: nil) # rubocop:disable Metrics/AbcSize
29
+ def drop_edge(id = nil, from: nil, to: nil, label: nil, start: g) # rubocop:disable Metrics/AbcSize
30
30
  raise ArgumentError, "either id or from:, to: and label: must be passed" if [id, from, to, label].all?(&:nil?)
31
- return g.E(id).drop.iterate unless id.nil?
31
+ return start.E(id).drop.iterate unless id.nil?
32
32
 
33
33
  raise ArgumentError, "from:, to: and label: must be passed" if [from, to, label].any?(&:nil?)
34
34
 
35
- g.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
35
+ start.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
36
36
  end
37
37
 
38
- def add_vertex(label, id = nil, **properties)
38
+ def add_vertex(label, id = nil, start: g, **properties)
39
39
  id ||= properties[T.id]
40
40
  properties = except(properties, T.id)
41
41
 
42
- t = g.addV(label)
42
+ t = start.addV(label)
43
43
  t = t.props(T.id => id) unless id.nil?
44
44
  t.props(**properties).next
45
45
  end
46
46
 
47
- def add_edge(label, id = nil, from:, to:, **properties)
47
+ def add_edge(label, id = nil, from:, to:, start: g, **properties)
48
48
  id ||= properties[T.id]
49
49
  properties = except(properties, T.label)
50
50
  properties[T.id] = id
51
51
 
52
- g.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
52
+ start.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
53
53
  end
54
54
 
55
- def upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)
55
+ def upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, start: g, **params) # rubocop:disable Metrics/ParameterLists
56
56
  with_upsert_error_handling(on_failure, params) do
57
57
  create_properties, update_properties = cleanup_properties(create_properties, update_properties)
58
58
 
59
- g.upsertV(label, id, create_properties, update_properties).id.next
59
+ start.upsertV(label, id, create_properties, update_properties).id.next
60
60
  end
61
61
  end
62
62
 
63
63
  # vertices:
64
64
  # [["label", "id", {create: :properties}, {update: properties}]]
65
65
  # params can override Retryable config from UPSERT_RETRY_PARAMS
66
- def upsert_vertices(vertices, batch_size: 100, on_failure: :retry, **params)
66
+ def upsert_vertices(vertices, batch_size: 100, on_failure: :retry, start: g, **params)
67
67
  vertices.each_slice(batch_size) do |slice|
68
68
  with_upsert_error_handling(on_failure, params) do
69
- slice.reduce(g) do |t, (label, id, create_properties, update_properties)|
69
+ slice.reduce(start) do |t, (label, id, create_properties, update_properties)|
70
70
  create_properties, update_properties = cleanup_properties(create_properties, update_properties)
71
71
 
72
72
  t.upsertV(label, id, create_properties, update_properties)
@@ -77,20 +77,21 @@ module Grumlin
77
77
 
78
78
  # Only from and to are used to find the existing edge, if one wants to assign an id to a created edge,
79
79
  # it must be passed as T.id in create_properties.
80
- def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params) # rubocop:disable Metrics/ParameterLists
80
+ def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, # rubocop:disable Metrics/ParameterLists
81
+ on_failure: :retry, start: g, **params)
81
82
  with_upsert_error_handling(on_failure, params) do
82
83
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
83
- g.upsertE(label, from, to, create_properties, update_properties).id.next
84
+ start.upsertE(label, from, to, create_properties, update_properties).id.next
84
85
  end
85
86
  end
86
87
 
87
88
  # edges:
88
89
  # [["label", "from", "to", {create: :properties}, {update: properties}]]
89
90
  # params can override Retryable config from UPSERT_RETRY_PARAMS
90
- def upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)
91
+ def upsert_edges(edges, batch_size: 100, on_failure: :retry, start: g, **params)
91
92
  edges.each_slice(batch_size) do |slice|
92
93
  with_upsert_error_handling(on_failure, params) do
93
- slice.reduce(g) do |t, (label, from, to, create_properties, update_properties)|
94
+ slice.reduce(start) do |t, (label, from, to, create_properties, update_properties)|
94
95
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
95
96
 
96
97
  t.upsertE(label, from, to, create_properties, update_properties)
@@ -10,6 +10,7 @@ module Grumlin
10
10
  }.freeze
11
11
 
12
12
  def self.extended(base)
13
+ super
13
14
  base.extend(Grumlin::Shortcuts)
14
15
  base.include(Repository::InstanceMethods)
15
16
 
@@ -17,6 +18,12 @@ module Grumlin
17
18
  base.shortcuts_from(Grumlin::Shortcuts::Upserts)
18
19
  end
19
20
 
21
+ def self.new
22
+ @repository ||= Class.new do # rubocop:disable Naming/MemoizedInstanceVariableName
23
+ extend Grumlin::Repository
24
+ end.new
25
+ end
26
+
20
27
  def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
21
28
  return_mode = validate_return_mode!(return_mode)
22
29
  postprocess_with = validate_postprocess_with!(postprocess_with)
@@ -10,23 +10,6 @@ module Grumlin
10
10
  206 => :partial_content
11
11
  }.freeze
12
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
- VERTEX_ALREADY_EXISTS = "Vertex with id already exists:"
26
- EDGE_ALREADY_EXISTS = "Edge with id already exists:"
27
- CONCURRENT_VERTEX_INSERT_FAILED = "Failed to complete Insert operation for a Vertex due to conflicting concurrent"
28
- CONCURRENT_EDGE_INSERT_FAILED = "Failed to complete Insert operation for an Edge due to conflicting concurrent"
29
-
30
13
  class DispatcherError < Grumlin::Error; end
31
14
 
32
15
  class RequestAlreadyAddedError < DispatcherError; end
@@ -47,14 +30,16 @@ module Grumlin
47
30
 
48
31
  # builds a response object, when it's ready sends it to the client via a channel
49
32
  # TODO: sometimes response does not include requestID, no idea how to handle it so far.
50
- def add_response(response) # rubocop:disable Metrics/AbcSize
33
+ def add_response(response) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
51
34
  request_id = response[:requestId]
52
35
  raise UnknownRequestError unless ongoing_request?(request_id)
53
36
 
54
37
  begin
55
38
  request = @requests[request_id]
56
39
 
57
- check_errors!(response[:status], request[:request])
40
+ RequestErrorFactory.build(request, response).tap do |err|
41
+ raise err unless err.nil?
42
+ end
58
43
 
59
44
  case SUCCESS[response.dig(:status, :code)]
60
45
  when :success
@@ -91,29 +76,5 @@ module Grumlin
91
76
  request = @requests.delete(request_id)
92
77
  request[:channel].close
93
78
  end
94
-
95
- def check_errors!(status, query)
96
- if (error = ERRORS[status[:code]])
97
- raise (
98
- already_exists_error(status) ||
99
- concurrent_insert_error(status) ||
100
- error
101
- ).new(status, query)
102
- end
103
-
104
- return unless SUCCESS[status[:code]].nil?
105
-
106
- raise(UnknownResponseStatus, status)
107
- end
108
-
109
- def already_exists_error(status)
110
- return VertexAlreadyExistsError if status[:message]&.include?(VERTEX_ALREADY_EXISTS)
111
- return EdgeAlreadyExistsError if status[:message]&.include?(EDGE_ALREADY_EXISTS)
112
- end
113
-
114
- def concurrent_insert_error(status)
115
- return ConcurrentVertexInsertFailedError if status[:message]&.include?(CONCURRENT_VERTEX_INSERT_FAILED)
116
- return ConcurrentEdgeInsertFailedError if status[:message]&.include?(CONCURRENT_EDGE_INSERT_FAILED)
117
- end
118
79
  end
119
80
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class RequestErrorFactory
5
+ ERRORS = {
6
+ 499 => InvalidRequestArgumentsError,
7
+ 500 => ServerError,
8
+ 597 => ScriptEvaluationError,
9
+ 599 => ServerSerializationError,
10
+ 598 => ServerTimeoutError,
11
+
12
+ 401 => ClientSideError,
13
+ 407 => ClientSideError,
14
+ 498 => ClientSideError
15
+ }.freeze
16
+
17
+ # Neptune presumably returns message as a JSON string of format
18
+ # {"detailedMessage":"",
19
+ # "requestId":"UUID",
20
+ # "code":"ConcurrentModificationException"}
21
+ # Currencly we simply search for substings to identify the exact error
22
+ # TODO: parse json and use `code` instead
23
+ VERTEX_ALREADY_EXISTS = "Vertex with id already exists:"
24
+ EDGE_ALREADY_EXISTS = "Edge with id already exists:"
25
+ CONCURRENT_VERTEX_INSERT_FAILED = "Failed to complete Insert operation for a Vertex due to conflicting concurrent"
26
+ CONCURRENT_EDGE_INSERT_FAILED = "Failed to complete Insert operation for an Edge due to conflicting concurrent"
27
+ CONCURRENCT_MODIFICATION_FAILED = "Failed to complete operation due to conflicting concurrent"
28
+
29
+ class << self
30
+ def build(request, response)
31
+ status = response[:status]
32
+ query = request[:request]
33
+
34
+ if (error = ERRORS[status[:code]])
35
+ return (
36
+ already_exists_error(status) ||
37
+ concurrent_modification_error(status) ||
38
+ error
39
+ ).new(status, query)
40
+ end
41
+
42
+ return unless RequestDispatcher::SUCCESS[status[:code]].nil?
43
+
44
+ UnknownResponseStatus.new(status)
45
+ end
46
+
47
+ def already_exists_error(status)
48
+ return VertexAlreadyExistsError if status[:message]&.include?(VERTEX_ALREADY_EXISTS)
49
+ return EdgeAlreadyExistsError if status[:message]&.include?(EDGE_ALREADY_EXISTS)
50
+ end
51
+
52
+ def concurrent_modification_error(status)
53
+ return ConcurrentVertexInsertFailedError if status[:message]&.include?(CONCURRENT_VERTEX_INSERT_FAILED)
54
+ return ConcurrentEdgeInsertFailedError if status[:message]&.include?(CONCURRENT_EDGE_INSERT_FAILED)
55
+ return ConcurrentModificationError if status[:message]&.include?(CONCURRENCT_MODIFICATION_FAILED)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,11 +5,12 @@ module Grumlin
5
5
  module Properties
6
6
  extend Grumlin::Shortcuts
7
7
 
8
- shortcut :props do |**props|
9
- props.reduce(self) do |tt, (prop, value)| # rubocop:disable Style/EachWithObject
10
- next tt.property(prop, value) unless value.nil? # nils are not supported
8
+ shortcut :props do |cardinality = nil, **props|
9
+ props.reduce(self) do |tt, (prop, value)|
10
+ next tt if value.nil? # nils are not supported
11
+ next tt.property(prop, value) if cardinality.nil?
11
12
 
12
- tt
13
+ tt.property(cardinality, prop, value)
13
14
  end
14
15
  end
15
16
 
@@ -10,8 +10,8 @@ module Grumlin
10
10
  .fold
11
11
  .coalesce(
12
12
  __.unfold,
13
- __.addV(label).props(**create_properties.merge(T.id => id))
14
- ).props(**update_properties)
13
+ __.addV(label).props(Cardinality.single, **create_properties.merge(T.id => id))
14
+ ).props(Cardinality.single, **update_properties)
15
15
  end
16
16
 
17
17
  shortcut :upsertE do |label, from, to, create_properties = {}, update_properties = {}|
@@ -4,13 +4,18 @@ module Grumlin
4
4
  class Steppable
5
5
  extend Forwardable
6
6
 
7
+ attr_reader :session_id
8
+
7
9
  START_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
8
10
  REGULAR_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
9
11
  CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
10
12
 
11
13
  ALL_STEPS = START_STEPS + CONFIGURATION_STEPS + REGULAR_STEPS
12
14
 
13
- def initialize
15
+ def initialize(pool: Grumlin.default_pool, session_id: nil)
16
+ @pool = pool
17
+ @session_id = session_id
18
+
14
19
  return if respond_to?(:shortcuts)
15
20
 
16
21
  raise "steppable must not be initialized directly, use Grumlin::Shortcuts::Storage#g or #__ instead"
@@ -18,12 +23,12 @@ module Grumlin
18
23
 
19
24
  ALL_STEPS.each do |step|
20
25
  define_method step do |*args, **params|
21
- shortcuts.action_class.new(step, args: args, params: params, previous_step: self)
26
+ shortcuts.action_class.new(step, args: args, params: params, previous_step: self, session_id: @session_id)
22
27
  end
23
28
  end
24
29
 
25
30
  def step(name, *args, **params)
26
- shortcuts.action_class.new(name, args: args, params: params, previous_step: self)
31
+ shortcuts.action_class.new(name, args: args, params: params, previous_step: self, session_id: @session_id)
27
32
  end
28
33
 
29
34
  def_delegator :shortcuts, :__
data/lib/grumlin/steps.rb CHANGED
@@ -32,7 +32,9 @@ module Grumlin
32
32
  end
33
33
 
34
34
  def add(name, args: [], params: {})
35
- return add_configuration_step(name, args: args, params: params) if CONFIGURATION_STEPS.include?(name)
35
+ if CONFIGURATION_STEPS.include?(name) || name.to_sym == :tx
36
+ return add_configuration_step(name, args: args, params: params)
37
+ end
36
38
 
37
39
  StepData.new(name, args: cast_arguments(args), params: params).tap do |step|
38
40
  @steps << step
@@ -10,17 +10,19 @@ module Grumlin
10
10
 
11
11
  def serialize
12
12
  steps = ShortcutsApplyer.call(@steps)
13
- no_return = @params[:no_return] || false
14
-
15
- {
16
- step: (steps.steps + (no_return ? [NONE_STEP] : [])).map { |s| serialize_step(s) }
17
- }.tap do |v|
18
- v.merge!(source: steps.configuration_steps.map { |s| serialize_step(s) }) if steps.configuration_steps.any?
13
+ no_return = @params.fetch(:no_return, false)
14
+ {}.tap do |result|
15
+ result[:step] = serialize_steps(steps.steps + (no_return ? [NONE_STEP] : [])) if steps.steps.any?
16
+ result[:source] = serialize_steps(steps.configuration_steps) if steps.configuration_steps.any?
19
17
  end
20
18
  end
21
19
 
22
20
  private
23
21
 
22
+ def serialize_steps(steps)
23
+ steps.map { |s| serialize_step(s) }
24
+ end
25
+
24
26
  def serialize_step(step)
25
27
  [step.name].tap do |result|
26
28
  step.args.each do |arg|
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Transaction
5
+ attr_reader :session_id
6
+
7
+ include Console
8
+
9
+ COMMIT = Grumlin::Repository.new.g.step(:tx, :commit).bytecode
10
+ ROLLBACK = Grumlin::Repository.new.g.step(:tx, :rollback).bytecode
11
+
12
+ def initialize(traversal_start_class, pool: Grumlin.default_pool)
13
+ @traversal_start_class = traversal_start_class
14
+ @pool = pool
15
+
16
+ @session_id = SecureRandom.uuid
17
+ end
18
+
19
+ def begin
20
+ @traversal_start_class.new(session_id: @session_id)
21
+ end
22
+
23
+ def commit
24
+ finalize(COMMIT)
25
+ end
26
+
27
+ def rollback
28
+ finalize(ROLLBACK)
29
+ end
30
+
31
+ private
32
+
33
+ def finalize(action)
34
+ @pool.acquire do |client|
35
+ client.write(action, session_id: @session_id)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -4,6 +4,27 @@ module Grumlin
4
4
  class TraversalStart < Steppable
5
5
  include WithExtension
6
6
 
7
+ class TraversalError < Grumlin::Error; end
8
+ class AlreadyBoundToTransactionError < TraversalError; end
9
+
10
+ def tx
11
+ raise AlreadyBoundToTransactionError if @session_id
12
+
13
+ transaction = tx_class.new(self.class, pool: @pool)
14
+ return transaction unless block_given?
15
+
16
+ begin
17
+ yield transaction.begin
18
+ rescue Grumlin::Rollback
19
+ transaction.rollback
20
+ rescue StandardError
21
+ transaction.rollback
22
+ raise
23
+ else
24
+ transaction.commit
25
+ end
26
+ end
27
+
7
28
  def to_s(*)
8
29
  self.class.to_s
9
30
  end
@@ -11,5 +32,11 @@ module Grumlin
11
32
  def inspect
12
33
  self.class.inspect
13
34
  end
35
+
36
+ private
37
+
38
+ def tx_class
39
+ Grumlin.features.supports_transactions? ? Transaction : DummyTransaction
40
+ end
14
41
  end
15
42
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.20.2"
4
+ VERSION = "0.22.0"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -33,6 +33,9 @@ loader.do_not_eager_load(test_helpers)
33
33
  module Grumlin
34
34
  class Error < StandardError; end
35
35
 
36
+ class TransactionError < Error; end
37
+ class Rollback < TransactionError; end
38
+
36
39
  class UnknownError < Error; end
37
40
 
38
41
  class ConnectionError < Error; end
@@ -98,7 +101,8 @@ module Grumlin
98
101
  class VertexAlreadyExistsError < AlreadyExistsError; end
99
102
  class EdgeAlreadyExistsError < AlreadyExistsError; end
100
103
 
101
- class ConcurrentInsertFailedError < ServerError; end
104
+ class ConcurrentModificationError < ServerError; end
105
+ class ConcurrentInsertFailedError < ConcurrentModificationError; end
102
106
 
103
107
  class ConcurrentVertexInsertFailedError < ConcurrentInsertFailedError; end
104
108
  class ConcurrentEdgeInsertFailedError < ConcurrentInsertFailedError; end
@@ -127,27 +131,26 @@ module Grumlin
127
131
 
128
132
  class WrongQueryResult < RepositoryError; end
129
133
 
130
- class Config
131
- attr_accessor :url, :pool_size, :client_concurrency, :client_factory
132
-
133
- def initialize
134
- @pool_size = 10
135
- @client_concurrency = 5
136
- @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
137
- end
138
- end
139
-
140
134
  @pool_mutex = Mutex.new
141
135
 
142
136
  class << self
143
137
  def configure
144
138
  yield config
139
+
140
+ config.validate!
145
141
  end
146
142
 
147
143
  def config
148
144
  @config ||= Config.new
149
145
  end
150
146
 
147
+ # returns a subset of features for currently configured backend.
148
+ # The features lists are hardcoded as there is no way to get them
149
+ # from the remote server.
150
+ def features
151
+ Features.for(config.provider) # no memoization as provider may be changed
152
+ end
153
+
151
154
  def default_pool
152
155
  if Thread.current.thread_variable_get(:grumlin_default_pool)
153
156
  return Thread.current.thread_variable_get(:grumlin_default_pool)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.2
4
+ version: 0.22.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: 2022-07-22 00:00:00.000000000 Z
11
+ date: 2022-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -96,7 +96,6 @@ files:
96
96
  - ".rspec"
97
97
  - ".rubocop.yml"
98
98
  - ".tool-versions"
99
- - CHANGELOG.md
100
99
  - CODE_OF_CONDUCT.md
101
100
  - Gemfile
102
101
  - Gemfile.lock
@@ -117,6 +116,8 @@ files:
117
116
  - lib/grumlin/action.rb
118
117
  - lib/grumlin/benchmark/repository.rb
119
118
  - lib/grumlin/client.rb
119
+ - lib/grumlin/config.rb
120
+ - lib/grumlin/dummy_transaction.rb
120
121
  - lib/grumlin/edge.rb
121
122
  - lib/grumlin/expressions/cardinality.rb
122
123
  - lib/grumlin/expressions/column.rb
@@ -129,12 +130,17 @@ files:
129
130
  - lib/grumlin/expressions/t.rb
130
131
  - lib/grumlin/expressions/text_p.rb
131
132
  - lib/grumlin/expressions/with_options.rb
133
+ - lib/grumlin/features.rb
134
+ - lib/grumlin/features/feature_list.rb
135
+ - lib/grumlin/features/neptune_features.rb
136
+ - lib/grumlin/features/tinkergraph_features.rb
132
137
  - lib/grumlin/path.rb
133
138
  - lib/grumlin/property.rb
134
139
  - lib/grumlin/repository.rb
135
140
  - lib/grumlin/repository/error_handling_strategy.rb
136
141
  - lib/grumlin/repository/instance_methods.rb
137
142
  - lib/grumlin/request_dispatcher.rb
143
+ - lib/grumlin/request_error_factory.rb
138
144
  - lib/grumlin/shortcut.rb
139
145
  - lib/grumlin/shortcuts.rb
140
146
  - lib/grumlin/shortcuts/properties.rb
@@ -152,6 +158,7 @@ files:
152
158
  - lib/grumlin/test/rspec.rb
153
159
  - lib/grumlin/test/rspec/db_cleaner_context.rb
154
160
  - lib/grumlin/test/rspec/gremlin_context.rb
161
+ - lib/grumlin/transaction.rb
155
162
  - lib/grumlin/transport.rb
156
163
  - lib/grumlin/traversal_start.rb
157
164
  - lib/grumlin/traversal_strategies/options_strategy.rb
@@ -168,7 +175,7 @@ licenses:
168
175
  metadata:
169
176
  homepage_uri: https://github.com/zhulik/grumlin
170
177
  source_code_uri: https://github.com/zhulik/grumlin
171
- changelog_uri: https://github.com/zhulik/grumlin/blob/master/CHANGELOG.md
178
+ changelog_uri: https://github.com/babbel/grumlin/releases
172
179
  rubygems_mfa_required: 'true'
173
180
  post_install_message:
174
181
  rdoc_options: []
data/CHANGELOG.md DELETED
@@ -1,71 +0,0 @@
1
- ## [0.16.0] - 2022-03-11
2
-
3
- - Query building is rewritten from scratch. No public APIs were changed. [Details](https://github.com/babbel/grumlin/pull/64)
4
- - Add support for [TextP](https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/process/traversal/TextP.html)
5
-
6
- ## [0.15.4] - 2022-01-20
7
-
8
- - Move step and expression definitions to a yaml file for better diffs
9
- - Add `definitions:format` rake task
10
-
11
- ## [0.15.3] - 2022-01-18
12
-
13
- - Fix passing nils as step arguments. Even if they are not supported by the server, they should not be omitted.
14
-
15
- ## [0.15.2] - 2022-01-17
16
-
17
- - New steps: `map` and `identity`
18
-
19
- ## [0.15.1] - 2022-01-17
20
-
21
- - Fix passing arrays as step arguments
22
-
23
- ## [0.15.0] - 2022-01-11
24
-
25
- - Add `properties` step
26
- - Add proper support for bulked results
27
- - Add support for `Property` objects
28
-
29
- ## [0.14.5] - 2021-12-27
30
-
31
- - Fix params handling
32
- - Add `aggregate` step
33
- - Add `Order.shuffle`
34
-
35
- ## [0.14.4] - 2021-12-17
36
-
37
- - `Grumlin::Repository.shorcuts_from` do not raise `ArgumentError` when importing an already existing shortcut
38
- pointing to the same block. This fixes importing shortcuts from another repository.
39
-
40
- ## [0.14.2] - 2021-12-13
41
-
42
- - Fix `Module` bloating
43
- - Add `Operator` expressions
44
- - Add `__.coalesce` and `__.constant`
45
- - Add steps: `sum`, `sack`
46
- - Add configuration steps: `withSack`
47
- - Rename `Grumlin::Expressions::Tool` to `Grumlin::Expressions::Expression`
48
-
49
-
50
- ## [0.14.2] - 2021-12-12
51
-
52
- - Better exceptions
53
- - Add `choose` step
54
- - Add `__.hasNot`, `__.is`, `__.select`
55
-
56
- ## [0.14.0] - 2021-12-07
57
-
58
- - Add initial support for [configuration steps](https://tinkerpop.apache.org/docs/current/reference/#configuration-steps)
59
- - Add the `withSideEffect` configuration step
60
- - Fix passing keyword arguments to regular steps
61
- - *Drop support for ruby 2.6*
62
-
63
- ## [0.13.0] - 2021-12-03
64
-
65
- - Add `Shortcuts` and `Repository`
66
- - Allow executing any gremlin steps by name using `Grumlin::AnonymousStep#step`
67
- - Rename `Grumlin::Tools` to `Grumlin::Expressions`
68
-
69
- ## [0.1.0] - 2021-05-25
70
-
71
- - Initial release