grumlin 0.20.1 → 0.21.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: f1f9f9d0ec31cd18d6852dc7920ee15de0d4799d229ed7bfa92ee1dccfe48d9d
4
- data.tar.gz: 0fb3bd378cb520ef310c4926c914fd2ddb4dbc1d674ef245a727d4bb776c5164
3
+ metadata.gz: 95963424bb38728bb21c2d3746a93be60daf303b2c2897b24d99684fd1fb7bff
4
+ data.tar.gz: 809b4b589b2bc5fd8687f0f63a6f73f17aab82060990b3a5942164c2ac92ef83
5
5
  SHA512:
6
- metadata.gz: aaf7fcc34cb5c14705e72fea99cf013bbebcec4c06b2c5bd39d863a97ca13a14ff3c87ec70f209752f9e09c5964f49b4447bb66c5420405b9b6a61660d807f81
7
- data.tar.gz: 5626a23f9279d2989ce781141a388ac97e6f1f1cedcfe9b7b9f7b91e5455dc4c8f98c913575901ee3d94cecfce11c437abd28918cdde9544eb9564fa175fb450
6
+ metadata.gz: b095f3958ebf4b70577e607a0b10e27fc073055fe08d6fe55787c71fd104e77cf78cdcd0a54d06e7881c9183cab479ef6e92e6117a1c1b418945d1ebb417655c
7
+ data.tar.gz: 0477a8ce8ccfc5ea1cc3b95b7b48501b05387207918478adc4eecb8e286e8495910b6bd718e1d5c00ab63e61e02ca6e89212913ec35d8df49b267cb75281e9eb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.20.1)
4
+ grumlin (0.21.1)
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,33 @@ 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. Currently there is no difference
68
+ in behaviour when working with different providers.
69
+
70
+ To check current providers supported features use
71
+
72
+ ```ruby
73
+ Grumlin.features
74
+ ```
75
+
52
76
  ### Traversing graphs
53
77
 
54
78
  **Warning**: Not all steps and expressions defined in the reference documentation are supported.
@@ -231,6 +255,10 @@ All of them support 3 different modes for error handling: `:retry`, `:ignore` an
231
255
  with [retryable](https://github.com/nfedyashev/retryable). **params will be merged to the default config for upserts
232
256
  and passed to `Retryable.retryable`. In case if you want to modify retryable behaviour you are to do so.
233
257
 
258
+ If you don't want to define you own repository, simply use
259
+
260
+ `Grumlin::Repository.new` returns an instance of an anonymous class extending `Grumlin::Repository`.
261
+
234
262
  **Usage**
235
263
 
236
264
  To execute the query defined in a query block one simply needs to call a method with the same name:
@@ -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
data/lib/definitions.yml CHANGED
@@ -82,6 +82,7 @@ steps:
82
82
  configuration:
83
83
  - withSack
84
84
  - withSideEffect
85
+ - withStrategies
85
86
  expressions:
86
87
  cardinality:
87
88
  - list
@@ -4,14 +4,15 @@ 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?
@@ -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,14 @@ 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
+ ensure
30
+ @count += 1
31
+ end
32
+
33
+ def finalize_tx(action, session_id)
34
+ @client.finalize_tx(action, session_id)
29
35
  ensure
30
36
  @count += 1
31
37
  end
@@ -94,19 +100,19 @@ module Grumlin
94
100
  end
95
101
 
96
102
  # TODO: support yielding
97
- def write(bytecode)
103
+ def write(bytecode, session_id: nil)
98
104
  raise NotConnectedError unless connected?
99
105
 
100
- request = to_query(bytecode)
101
- channel = @request_dispatcher.add_request(request)
102
- @transport.write(request)
106
+ request = to_query(bytecode, session_id: session_id)
107
+ submit_request(request)
108
+ end
103
109
 
104
- begin
105
- channel.dequeue.flat_map { |item| Typing.cast(item) }
106
- rescue Async::Stop, Async::TimeoutError
107
- close(check_requests: false)
108
- raise
109
- end
110
+ def finalize_tx(action, session_id)
111
+ raise NotConnectedError unless connected?
112
+ raise ArgumentError, "session_id cannot be nil" if session_id.nil?
113
+
114
+ request = finalize_tx_query(action, session_id)
115
+ submit_request(request)
110
116
  end
111
117
 
112
118
  def inspect
@@ -119,20 +125,52 @@ module Grumlin
119
125
 
120
126
  private
121
127
 
128
+ def submit_request(request)
129
+ channel = @request_dispatcher.add_request(request)
130
+ @transport.write(request)
131
+
132
+ begin
133
+ channel.dequeue.flat_map { |item| Typing.cast(item) }
134
+ rescue Async::Stop, Async::TimeoutError
135
+ close(check_requests: false)
136
+ raise
137
+ end
138
+ end
139
+
122
140
  # This might be overridden in successors
123
141
  def build_transport
124
142
  Transport.new(@url, parent: @parent, **@client_options)
125
143
  end
126
144
 
127
- def to_query(bytecode)
145
+ def to_query(bytecode, session_id:)
146
+ {
147
+ requestId: SecureRandom.uuid,
148
+ op: :bytecode,
149
+ processor: session_id ? :session : :traversal,
150
+ args: {
151
+ gremlin: {
152
+ :@type => "g:Bytecode",
153
+ :@value => bytecode.serialize
154
+ },
155
+ aliases: { g: :g },
156
+ session: session_id
157
+ }.compact
158
+ }
159
+ end
160
+
161
+ def finalize_tx_query(action, session_id)
128
162
  {
129
163
  requestId: SecureRandom.uuid,
130
- op: "bytecode",
131
- processor: "traversal",
164
+ op: :bytecode,
165
+ processor: session_id ? :session : :traversal,
132
166
  args: {
133
- gremlin: { :@type => "g:Bytecode", :@value => bytecode.serialize },
134
- aliases: { g: :g }
135
- }
167
+ gremlin: {
168
+ :@type => "g:Bytecode",
169
+ :@value => { source: [[:tx, action]] }
170
+ },
171
+ aliases: { g: :g },
172
+ session: session_id
173
+ }.compact
136
174
  }
137
175
  end
138
176
  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,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 }
@@ -56,7 +56,7 @@ module Grumlin
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).next
59
+ g.upsertV(label, id, create_properties, update_properties).id.next
60
60
  end
61
61
  end
62
62
 
@@ -70,7 +70,7 @@ module Grumlin
70
70
  create_properties, update_properties = cleanup_properties(create_properties, update_properties)
71
71
 
72
72
  t.upsertV(label, id, create_properties, update_properties)
73
- end.iterate
73
+ end.id.iterate
74
74
  end
75
75
  end
76
76
  end
@@ -80,7 +80,7 @@ module Grumlin
80
80
  def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params) # rubocop:disable Metrics/ParameterLists
81
81
  with_upsert_error_handling(on_failure, params) do
82
82
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
83
- g.upsertE(label, from, to, create_properties, update_properties).next
83
+ g.upsertE(label, from, to, create_properties, update_properties).id.next
84
84
  end
85
85
  end
86
86
 
@@ -94,7 +94,7 @@ module Grumlin
94
94
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
95
95
 
96
96
  t.upsertE(label, from, to, create_properties, update_properties)
97
- end.iterate
97
+ end.id.iterate
98
98
  end
99
99
  end
100
100
  end
@@ -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,27 +4,31 @@ 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
- raise RuntimerError,
17
- "steppable must not be initialized directly, use Grumlin::Shortcuts::Storage#g or #__ instead"
21
+ raise "steppable must not be initialized directly, use Grumlin::Shortcuts::Storage#g or #__ instead"
18
22
  end
19
23
 
20
24
  ALL_STEPS.each do |step|
21
25
  define_method step do |*args, **params|
22
- 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)
23
27
  end
24
28
  end
25
29
 
26
30
  def step(name, *args, **params)
27
- 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)
28
32
  end
29
33
 
30
34
  def_delegator :shortcuts, :__
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Transaction
5
+ attr_reader :uuid
6
+
7
+ include Console
8
+
9
+ def initialize(traversal_start_class, pool: Grumlin.default_pool)
10
+ @traversal_start_class = traversal_start_class
11
+ @pool = pool
12
+
13
+ if supported?
14
+ @uuid = SecureRandom.uuid
15
+ return
16
+ end
17
+
18
+ logger.info(self) do
19
+ "#{Grumlin.config.provider} does not support transactions. commit and rollback are ignored, data will be saved"
20
+ end
21
+ end
22
+
23
+ def supported?
24
+ Grumlin.features.supports_transactions?
25
+ end
26
+
27
+ def begin
28
+ @traversal_start_class.new(session_id: @uuid)
29
+ end
30
+
31
+ def commit
32
+ return unless supported?
33
+
34
+ finalize(:commit)
35
+ end
36
+
37
+ def rollback
38
+ return unless supported?
39
+
40
+ finalize(:rollback)
41
+ end
42
+
43
+ private
44
+
45
+ def finalize(action)
46
+ @pool.acquire do |client|
47
+ client.finalize_tx(action, @uuid)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,17 @@
2
2
 
3
3
  module Grumlin
4
4
  class TraversalStart < Steppable
5
+ include WithExtension
6
+
7
+ class TraversalError < Grumlin::Error; end
8
+ class AlreadyBoundToTransationError < TraversalError; end
9
+
10
+ def tx
11
+ raise AlreadyBoundToTransationError if @session_id
12
+
13
+ Transaction.new(self.class, pool: @pool)
14
+ end
15
+
5
16
  def to_s(*)
6
17
  self.class.to_s
7
18
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module TraversalStrategies
5
+ class OptionsStrategy < TypedValue
6
+ def initialize(value)
7
+ super(type: "OptionsStrategy", value: value)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.20.1"
4
+ VERSION = "0.21.1"
5
5
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module WithExtension
5
+ def with(name, value)
6
+ prev = self
7
+ strategy = if is_a?(with_action_class)
8
+ prev = previous_step
9
+ TraversalStrategies::OptionsStrategy.new(args.first.value.merge(name => value))
10
+ else
11
+ TraversalStrategies::OptionsStrategy.new({ name => value })
12
+ end
13
+ with_action_class.new(:withStrategies, args: [strategy], previous_step: prev)
14
+ end
15
+
16
+ private
17
+
18
+ def with_action_class
19
+ @with_action_class ||= Class.new(shortcuts.action_class) do
20
+ include WithExtension
21
+
22
+ def with_action_class
23
+ self.class
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/grumlin.rb CHANGED
@@ -98,7 +98,8 @@ module Grumlin
98
98
  class VertexAlreadyExistsError < AlreadyExistsError; end
99
99
  class EdgeAlreadyExistsError < AlreadyExistsError; end
100
100
 
101
- class ConcurrentInsertFailedError < ServerError; end
101
+ class ConcurrentModificationError < ServerError; end
102
+ class ConcurrentInsertFailedError < ConcurrentModificationError; end
102
103
 
103
104
  class ConcurrentVertexInsertFailedError < ConcurrentInsertFailedError; end
104
105
  class ConcurrentEdgeInsertFailedError < ConcurrentInsertFailedError; end
@@ -127,27 +128,26 @@ module Grumlin
127
128
 
128
129
  class WrongQueryResult < RepositoryError; end
129
130
 
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
131
  @pool_mutex = Mutex.new
141
132
 
142
133
  class << self
143
134
  def configure
144
135
  yield config
136
+
137
+ config.validate!
145
138
  end
146
139
 
147
140
  def config
148
141
  @config ||= Config.new
149
142
  end
150
143
 
144
+ # returns a subset of features for currently configured backend.
145
+ # The features lists are hardcoded as there is no way to get them
146
+ # from the remote server.
147
+ def features
148
+ Features.for(config.provider) # no memoization as provider may be changed
149
+ end
150
+
151
151
  def default_pool
152
152
  if Thread.current.thread_variable_get(:grumlin_default_pool)
153
153
  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.1
4
+ version: 0.21.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: 2022-07-20 00:00:00.000000000 Z
11
+ date: 2022-08-11 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,7 @@ 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
120
  - lib/grumlin/edge.rb
121
121
  - lib/grumlin/expressions/cardinality.rb
122
122
  - lib/grumlin/expressions/column.rb
@@ -129,12 +129,17 @@ files:
129
129
  - lib/grumlin/expressions/t.rb
130
130
  - lib/grumlin/expressions/text_p.rb
131
131
  - lib/grumlin/expressions/with_options.rb
132
+ - lib/grumlin/features.rb
133
+ - lib/grumlin/features/feature_list.rb
134
+ - lib/grumlin/features/neptune_features.rb
135
+ - lib/grumlin/features/tinkergraph_features.rb
132
136
  - lib/grumlin/path.rb
133
137
  - lib/grumlin/property.rb
134
138
  - lib/grumlin/repository.rb
135
139
  - lib/grumlin/repository/error_handling_strategy.rb
136
140
  - lib/grumlin/repository/instance_methods.rb
137
141
  - lib/grumlin/request_dispatcher.rb
142
+ - lib/grumlin/request_error_factory.rb
138
143
  - lib/grumlin/shortcut.rb
139
144
  - lib/grumlin/shortcuts.rb
140
145
  - lib/grumlin/shortcuts/properties.rb
@@ -152,13 +157,16 @@ files:
152
157
  - lib/grumlin/test/rspec.rb
153
158
  - lib/grumlin/test/rspec/db_cleaner_context.rb
154
159
  - lib/grumlin/test/rspec/gremlin_context.rb
160
+ - lib/grumlin/transaction.rb
155
161
  - lib/grumlin/transport.rb
156
162
  - lib/grumlin/traversal_start.rb
163
+ - lib/grumlin/traversal_strategies/options_strategy.rb
157
164
  - lib/grumlin/traverser.rb
158
165
  - lib/grumlin/typed_value.rb
159
166
  - lib/grumlin/typing.rb
160
167
  - lib/grumlin/version.rb
161
168
  - lib/grumlin/vertex.rb
169
+ - lib/grumlin/with_extension.rb
162
170
  - lib/tasks/benchmark.rake
163
171
  homepage: https://github.com/zhulik/grumlin
164
172
  licenses:
@@ -166,7 +174,7 @@ licenses:
166
174
  metadata:
167
175
  homepage_uri: https://github.com/zhulik/grumlin
168
176
  source_code_uri: https://github.com/zhulik/grumlin
169
- changelog_uri: https://github.com/zhulik/grumlin/blob/master/CHANGELOG.md
177
+ changelog_uri: https://github.com/babbel/grumlin/releases
170
178
  rubygems_mfa_required: 'true'
171
179
  post_install_message:
172
180
  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