grumlin 0.18.1 → 0.19.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: 573e2c9cda13465c0560d36e9e10d8bcdda5e0ea01ea4bbf89cb8d6cb943f6de
4
- data.tar.gz: 8dd6fa7264875f372c6117352f2e87c7984457ff3f94da57f8f19d91344c4dd2
3
+ metadata.gz: f899b66f4f483334c271ca60f6ade26d5b403bc10552e03700595e983e0268cb
4
+ data.tar.gz: ea689ad161f0b220e37af0fd5aa32adaa50990ebdcdc6d8e8cef7b12b53067e5
5
5
  SHA512:
6
- metadata.gz: 2eea2219d92cd29ef311993dcd0bcd0e9d8906d39474d93733c8406952e93babf85d4468b6acd15ac1d114c940c49533603960fa496c8f757223d016b5619a03
7
- data.tar.gz: 39c8821a89cb1194d973c7b2dee5e96ad90288e2a611ef42a080586987759949453a2c7bade5be56d500588a8655a485c6c415c075c1295534bbc8a419bf2d43
6
+ metadata.gz: f9d3c770966a9f44efc25c7f7ad3a33203daafc0e1f3887db22ef6cd9c37415073c72cb90dd7cb7658c12a5e9ac2aa587c48d590ff417a7ba4c26b1f2586214d
7
+ data.tar.gz: 5a9ed3d9e7ff5c717083b5d36b8de17a4ef8b3ed4310c67b35b8ff56d5f7b49e2d74770e47cd1aa96f3544549f9fad9ddba5bf5695ca4bf9e23c04bb4d15f90b
data/.gitignore CHANGED
@@ -10,3 +10,5 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+ profile.flat.txt
14
+ profile.graph.html
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ Layout/LineLength:
15
15
  Metrics/BlockLength:
16
16
  Exclude:
17
17
  - spec/**/*_spec.rb
18
+ - "*.gemspec"
18
19
 
19
20
  Metrics/MethodLength:
20
21
  Max: 20
data/Gemfile CHANGED
@@ -17,3 +17,7 @@ gem "overcommit"
17
17
  gem "rake"
18
18
  gem "rspec"
19
19
  gem "simplecov"
20
+
21
+ gem "benchmark-ips"
22
+ gem "memory_profiler"
23
+ gem "ruby-prof"
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.18.1)
4
+ grumlin (0.19.0)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
  oj (~> 3.12)
8
+ retryable (~> 3.0)
8
9
  zeitwerk (~> 2.4)
9
10
 
10
11
  GEM
@@ -21,13 +22,14 @@ GEM
21
22
  console (~> 1.10)
22
23
  nio4r (~> 2.3)
23
24
  timers (~> 4.1)
24
- async-http (0.56.5)
25
+ async-http (0.56.6)
25
26
  async (>= 1.25)
26
27
  async-io (>= 1.28)
27
28
  async-pool (>= 0.2)
28
29
  protocol-http (~> 0.22.0)
29
30
  protocol-http1 (~> 0.14.0)
30
31
  protocol-http2 (~> 0.14.0)
32
+ traces (~> 0.4.0)
31
33
  async-io (1.33.0)
32
34
  async
33
35
  async-pool (0.3.9)
@@ -42,6 +44,7 @@ GEM
42
44
  protocol-websocket (~> 0.7.0)
43
45
  backport (1.2.0)
44
46
  benchmark (0.1.1)
47
+ benchmark-ips (2.10.0)
45
48
  childprocess (4.0.0)
46
49
  concurrent-ruby (1.1.8)
47
50
  console (1.15.0)
@@ -60,6 +63,7 @@ GEM
60
63
  rexml
61
64
  kramdown-parser-gfm (1.1.0)
62
65
  kramdown (~> 2.0)
66
+ memory_profiler (1.0.0)
63
67
  mini_portile2 (2.7.1)
64
68
  minitest (5.14.4)
65
69
  nio4r (2.5.8)
@@ -76,8 +80,8 @@ GEM
76
80
  parser (3.0.3.2)
77
81
  ast (~> 2.4.1)
78
82
  protocol-hpack (1.4.2)
79
- protocol-http (0.22.5)
80
- protocol-http1 (0.14.2)
83
+ protocol-http (0.22.6)
84
+ protocol-http1 (0.14.4)
81
85
  protocol-http (~> 0.22)
82
86
  protocol-http2 (0.14.2)
83
87
  protocol-hpack (~> 1.4)
@@ -89,6 +93,7 @@ GEM
89
93
  rainbow (3.0.0)
90
94
  rake (13.0.3)
91
95
  regexp_parser (2.2.0)
96
+ retryable (3.0.5)
92
97
  reverse_markdown (2.0.0)
93
98
  nokogiri
94
99
  rexml (3.2.5)
@@ -125,6 +130,7 @@ GEM
125
130
  rubocop-ast (>= 0.4.0)
126
131
  rubocop-rspec (2.6.0)
127
132
  rubocop (~> 1.19)
133
+ ruby-prof (1.4.3)
128
134
  ruby-progressbar (1.11.0)
129
135
  simplecov (0.21.2)
130
136
  docile (~> 1.1)
@@ -150,6 +156,7 @@ GEM
150
156
  thor (1.1.0)
151
157
  tilt (2.0.10)
152
158
  timers (4.3.3)
159
+ traces (0.4.1)
153
160
  tzinfo (2.0.4)
154
161
  concurrent-ruby (~> 1.0)
155
162
  unicode-display_width (2.1.0)
@@ -162,8 +169,10 @@ PLATFORMS
162
169
 
163
170
  DEPENDENCIES
164
171
  async-rspec
172
+ benchmark-ips
165
173
  factory_bot
166
174
  grumlin!
175
+ memory_profiler
167
176
  nokogiri
168
177
  overcommit
169
178
  rake
@@ -171,6 +180,7 @@ DEPENDENCIES
171
180
  rubocop
172
181
  rubocop-performance
173
182
  rubocop-rspec
183
+ ruby-prof
174
184
  simplecov
175
185
  solargraph
176
186
 
data/README.md CHANGED
@@ -188,8 +188,16 @@ Each `return_mode` is mapped to a particular termination step:
188
188
  - `add_edge(label, id = nil, from:, to:, **properties)`
189
189
  - `drop_vertex(id)`
190
190
  - `drop_edge(id = nil, from: nil, to: nil, label: nil)`
191
- - `upsert_vertex(label, id, create_properties: {}, update_properties: {})`
192
- - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {})`
191
+
192
+ and a few methods that emulate upserts:
193
+ - `upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
194
+ - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
195
+ - `upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)`
196
+ - `upsert_vertices(edges, batch_size: 100, on_failure: :retry, **params)`
197
+
198
+ All of them support 3 different modes for error handling: `:retry`, `:ignore` and `:raise`. Retry mode is implemented
199
+ with [retryable](https://github.com/nfedyashev/retryable). **params will be merged to the default config for upserts
200
+ and passed to `Retryable.retryable`. In case if you want to modify retryable behaviour you are to do so.
193
201
 
194
202
  **Usage**
195
203
 
data/grumlin.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency "async-pool", "~> 0.3"
31
31
  spec.add_dependency "async-websocket", "~> 0.19"
32
32
  spec.add_dependency "oj", "~> 3.12"
33
+ spec.add_dependency "retryable", "~> 3.0"
33
34
  spec.add_dependency "zeitwerk", "~> 2.4"
34
35
  spec.metadata = {
35
36
  "rubygems_mfa_required" => "true"
data/lib/async/channel.rb CHANGED
@@ -18,6 +18,10 @@ module Async
18
18
  @closed
19
19
  end
20
20
 
21
+ def open?
22
+ !@closed
23
+ end
24
+
21
25
  # Methods for a publisher
22
26
  def <<(payload)
23
27
  raise(ChannelClosedError, "Cannot send to a closed channel") if @closed
@@ -38,6 +42,13 @@ module Async
38
42
  @closed = true
39
43
  end
40
44
 
45
+ def close!
46
+ return if closed?
47
+
48
+ exception(ChannelClosedError.new("Channel was forcefully closed"))
49
+ close
50
+ end
51
+
41
52
  # Methods for a subscriber
42
53
  def dequeue
43
54
  each do |payload| # rubocop:disable Lint/UnreachableLoop this is intended
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Repository
5
+ class ErrorHandlingStrategy
6
+ def initialize(mode: :retry, **params)
7
+ @mode = mode
8
+ @params = params
9
+ @on_exceptions = params[:on]
10
+ end
11
+
12
+ def raise?
13
+ @mode == :raise
14
+ end
15
+
16
+ def ignore?
17
+ @mode == :ignore
18
+ end
19
+
20
+ def retry?
21
+ @mode == :retry
22
+ end
23
+
24
+ def apply!(&block)
25
+ return yield if raise?
26
+ return ignore_errors!(&block) if ignore?
27
+
28
+ retry_errors!(&block)
29
+ end
30
+
31
+ private
32
+
33
+ def ignore_errors!
34
+ yield
35
+ rescue *@on_exceptions
36
+ # ignore errors
37
+ end
38
+
39
+ def retry_errors!(&block)
40
+ Retryable.retryable(**@params, &block)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Repository
5
+ module InstanceMethods
6
+ include Grumlin::Expressions
7
+
8
+ UPSERT_RETRY_PARAMS = {
9
+ on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentInsertFailedError],
10
+ sleep_method: ->(n) { Async::Task.current.sleep(n) },
11
+ tries: 3,
12
+ sleep: ->(n) { (n**2) + 1 + rand }
13
+ }.freeze
14
+
15
+ DEFAULT_ERROR_HANDLING_STRATEGY = ErrorHandlingStrategy.new(mode: :retry, **UPSERT_RETRY_PARAMS)
16
+
17
+ def __
18
+ @__ ||= TraversalStart.new(self.class.shortcuts)
19
+ end
20
+
21
+ def g
22
+ @g ||= TraversalStart.new(self.class.shortcuts)
23
+ end
24
+
25
+ def drop_vertex(id)
26
+ g.V(id).drop.iterate
27
+ end
28
+
29
+ def drop_edge(id = nil, from: nil, to: nil, label: nil) # rubocop:disable Metrics/AbcSize
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?
32
+
33
+ raise ArgumentError, "from:, to: and label: must be passed" if [from, to, label].any?(&:nil?)
34
+
35
+ g.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
36
+ end
37
+
38
+ def add_vertex(label, id = nil, **properties)
39
+ id ||= properties[T.id]
40
+ properties = except(properties, T.id)
41
+
42
+ t = g.addV(label)
43
+ t = t.props(T.id => id) unless id.nil?
44
+ t.props(**properties).next
45
+ end
46
+
47
+ def add_edge(label, id = nil, from:, to:, **properties)
48
+ id ||= properties[T.id]
49
+ properties = except(properties, T.label)
50
+ properties[T.id] = id
51
+
52
+ g.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
53
+ end
54
+
55
+ def upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)
56
+ with_upsert_error_handling(on_failure, params) do
57
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties)
58
+
59
+ g.upsertV(label, id, create_properties, update_properties).next
60
+ end
61
+ end
62
+
63
+ # vertices:
64
+ # [["label", "id", {create: :properties}, {update: properties}]]
65
+ # params can override Retryable config from UPSERT_RETRY_PARAMS
66
+ def upsert_vertices(vertices, batch_size: 100, on_failure: :retry, **params)
67
+ with_upsert_error_handling(on_failure, params) do
68
+ vertices.each_slice(batch_size) do |slice|
69
+ slice.reduce(g) do |t, (label, id, create_properties, update_properties)|
70
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties)
71
+
72
+ t.upsertV(label, id, create_properties, update_properties)
73
+ end.iterate
74
+ end
75
+ end
76
+ end
77
+
78
+ # Only from and to are used to find the existing edge, if one wants to assign an id to a created edge,
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
81
+ with_upsert_error_handling(on_failure, params) do
82
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
83
+ g.upsertE(label, from, to, create_properties, update_properties).next
84
+ end
85
+ end
86
+
87
+ # edges:
88
+ # [["label", "id", {create: :properties}, {update: properties}]]
89
+ # params can override Retryable config from UPSERT_RETRY_PARAMS
90
+ def upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)
91
+ with_upsert_error_handling(on_failure, params) do
92
+ edges.each_slice(batch_size) do |slice|
93
+ slice.reduce(g) do |t, (label, from, to, create_properties, update_properties)|
94
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
95
+
96
+ t.upsertE(label, from, to, create_properties, update_properties)
97
+ end.iterate
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def with_upsert_error_handling(on_failure, params, &block)
105
+ if params.any?
106
+ ErrorHandlingStrategy.new(mode: on_failure, **UPSERT_RETRY_PARAMS.merge(params))
107
+ else
108
+ DEFAULT_ERROR_HANDLING_STRATEGY
109
+ end.apply!(&block)
110
+ end
111
+
112
+ def with_upsert_retry(retry_params, &block)
113
+ retry_params = UPSERT_RETRY_PARAMS.merge((retry_params))
114
+ Retryable.retryable(**retry_params, &block)
115
+ end
116
+
117
+ # A polyfill for Hash#except for ruby 2.x environments without ActiveSupport
118
+ # TODO: delete and use native Hash#except after ruby 2.7 is deprecated.
119
+ def except(hash, *keys)
120
+ return hash.except(*keys) if hash.respond_to?(:except)
121
+
122
+ hash.each_with_object({}) do |(k, v), res|
123
+ res[k] = v unless keys.include?(k)
124
+ end
125
+ end
126
+
127
+ def cleanup_properties(create_properties, update_properties, *props_to_cleanup)
128
+ props_to_cleanup = [T.id, T.label] if props_to_cleanup.empty?
129
+ [create_properties, update_properties].map do |props|
130
+ except(props, props_to_cleanup)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -9,92 +9,12 @@ module Grumlin
9
9
  traversal: :nil
10
10
  }.freeze
11
11
 
12
- module InstanceMethods
13
- include Grumlin::Expressions
14
-
15
- def __
16
- TraversalStart.new(self.class.shortcuts)
17
- end
18
-
19
- def g
20
- TraversalStart.new(self.class.shortcuts)
21
- end
22
-
23
- def drop_vertex(id)
24
- g.V(id).drop.iterate
25
- end
26
-
27
- def drop_edge(id = nil, from: nil, to: nil, label: nil) # rubocop:disable Metrics/AbcSize
28
- raise ArgumentError, "either id or from:, to: and label: must be passed" if [id, from, to, label].all?(&:nil?)
29
- return g.E(id).drop.iterate unless id.nil?
30
-
31
- raise ArgumentError, "from:, to: and label: must be passed" if [from, to, label].any?(&:nil?)
32
-
33
- g.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
34
- end
35
-
36
- def add_vertex(label, id = nil, **properties)
37
- id ||= properties[T.id]
38
- properties = except(properties, T.id)
39
-
40
- t = g.addV(label)
41
- t = t.props(T.id => id) unless id.nil?
42
- t.props(**properties).next
43
- end
44
-
45
- def add_edge(label, id = nil, from:, to:, **properties)
46
- id ||= properties[T.id]
47
- properties = except(properties, T.label)
48
- properties[T.id] = id
49
-
50
- g.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
51
- end
52
-
53
- def upsert_vertex(label, id, create_properties: {}, update_properties: {}) # rubocop:disable Metrics/AbcSize
54
- create_properties = except(create_properties, T.id, T.label)
55
- update_properties = except(update_properties, T.id, T.label)
56
- g.V(id)
57
- .fold
58
- .coalesce(
59
- __.unfold,
60
- __.addV(label).props(**create_properties.merge(T.id => id))
61
- ).props(**update_properties)
62
- .next
63
- end
64
-
65
- # Only from and to are used to find the existing edge, if one wants to assign an id to a created edge,
66
- # it must be passed as T.id in via create_properties.
67
- def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}) # rubocop:disable Metrics/AbcSize
68
- create_properties = except(create_properties, T.label)
69
- update_properties = except(update_properties, T.id, T.label)
70
-
71
- g.V(from)
72
- .outE(label).where(__.inV.hasId(to))
73
- .fold
74
- .coalesce(
75
- __.unfold,
76
- __.addE(label).from(__.V(from)).to(__.V(to)).props(**create_properties)
77
- ).props(**update_properties).next
78
- end
79
-
80
- private
81
-
82
- # A polyfill for Hash#except for ruby 2.x environments without ActiveSupport
83
- # TODO: delete and use native Hash#except when after ruby 2.7 is deprecated.
84
- def except(hash, *keys)
85
- return hash.except(*keys) if hash.respond_to?(:except)
86
-
87
- hash.each_with_object({}) do |(k, v), res|
88
- res[k] = v unless keys.include?(k)
89
- end
90
- end
91
- end
92
-
93
12
  def self.extended(base)
94
13
  base.extend(Grumlin::Shortcuts)
95
- base.include(InstanceMethods)
14
+ base.include(Repository::InstanceMethods)
96
15
 
97
16
  base.shortcuts_from(Grumlin::Shortcuts::Properties)
17
+ base.shortcuts_from(Grumlin::Shortcuts::Upserts)
98
18
  end
99
19
 
100
20
  def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -22,6 +22,11 @@ module Grumlin
22
22
  498 => ClientSideError
23
23
  }.freeze
24
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
+
25
30
  class DispatcherError < Grumlin::Error; end
26
31
 
27
32
  class RequestAlreadyAddedError < DispatcherError; end
@@ -46,29 +51,25 @@ module Grumlin
46
51
  request_id = response[:requestId]
47
52
  raise UnknownRequestError unless ongoing_request?(request_id)
48
53
 
49
- request = @requests[request_id]
50
-
51
- check_errors!(response[:status], request[:request])
52
-
53
- case SUCCESS[response.dig(:status, :code)]
54
- when :success
55
- request[:channel] << [*request[:result], response.dig(:result, :data)]
56
- close_request(request_id)
57
- when :partial_content then request[:result] << response.dig(:result, :data)
58
- when :no_content
59
- request[:channel] << []
54
+ begin
55
+ request = @requests[request_id]
56
+
57
+ check_errors!(response[:status], request[:request])
58
+
59
+ case SUCCESS[response.dig(:status, :code)]
60
+ when :success
61
+ request[:result] << response.dig(:result, :data)
62
+ request[:channel] << request[:result]
63
+ close_request(request_id)
64
+ when :partial_content then request[:result] << response.dig(:result, :data)
65
+ when :no_content
66
+ request[:channel] << []
67
+ close_request(request_id)
68
+ end
69
+ rescue StandardError => e
70
+ request[:channel].exception(e)
60
71
  close_request(request_id)
61
72
  end
62
- rescue StandardError => e
63
- request[:channel].exception(e)
64
- close_request(request_id)
65
- end
66
-
67
- def close_request(request_id)
68
- raise UnknownRequestError unless ongoing_request?(request_id)
69
-
70
- request = @requests.delete(request_id)
71
- request[:channel].close
72
73
  end
73
74
 
74
75
  def ongoing_request?(request_id)
@@ -76,19 +77,43 @@ module Grumlin
76
77
  end
77
78
 
78
79
  def clear
80
+ @requests.each do |_id, request|
81
+ request[:channel].close!
82
+ end
79
83
  @requests.clear
80
84
  end
81
85
 
82
86
  private
83
87
 
88
+ def close_request(request_id)
89
+ raise UnknownRequestError unless ongoing_request?(request_id)
90
+
91
+ request = @requests.delete(request_id)
92
+ request[:channel].close
93
+ end
94
+
84
95
  def check_errors!(status, query)
85
96
  if (error = ERRORS[status[:code]])
86
- raise error.new(status, query)
97
+ raise (
98
+ already_exists_error(status) ||
99
+ concurrent_insert_error(status) ||
100
+ error
101
+ ).new(status, query)
87
102
  end
88
103
 
89
104
  return unless SUCCESS[status[:code]].nil?
90
105
 
91
106
  raise(UnknownResponseStatus, status)
92
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
93
118
  end
94
119
  end
@@ -6,8 +6,10 @@ module Grumlin
6
6
  extend Grumlin::Shortcuts
7
7
 
8
8
  shortcut :props do |**props|
9
- props.compact.reduce(self) do |tt, (prop, value)|
10
- tt.property(prop, value)
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
11
+
12
+ tt
11
13
  end
12
14
  end
13
15
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Shortcuts
5
+ module Upserts
6
+ extend Grumlin::Shortcuts
7
+
8
+ shortcut :upsertV do |label, id, create_properties, update_properties|
9
+ self.V(id)
10
+ .fold
11
+ .coalesce( # TODO: extract upsert pattern to a shortcut
12
+ __.unfold,
13
+ __.addV(label).props(**create_properties.merge(T.id => id))
14
+ ).props(**update_properties)
15
+ end
16
+
17
+ shortcut :upsertE do |label, from, to, create_properties, update_properties|
18
+ self.V(from)
19
+ .outE(label).where(__.inV.hasId(to))
20
+ .fold
21
+ .coalesce(
22
+ __.unfold,
23
+ __.addE(label).from(__.V(from)).to(__.V(to)).props(**create_properties)
24
+ ).props(**update_properties)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,43 +4,39 @@ module Grumlin
4
4
  class ShortcutsApplyer
5
5
  class << self
6
6
  def call(steps)
7
- new.call(steps)
8
- end
9
- end
7
+ return steps unless steps.uses_shortcuts?
10
8
 
11
- def call(steps)
12
- return steps unless steps.uses_shortcuts?
9
+ shortcuts = steps.shortcuts
13
10
 
14
- shortcuts = steps.shortcuts
11
+ configuration_steps = process_steps(steps.configuration_steps, shortcuts)
12
+ regular_steps = process_steps(steps.steps, shortcuts)
15
13
 
16
- configuration_steps = process_steps(steps.configuration_steps, shortcuts)
17
- regular_steps = process_steps(steps.steps, shortcuts)
18
-
19
- Steps.new(shortcuts).tap do |processed_steps|
20
- (configuration_steps + regular_steps).each do |step|
21
- processed_steps.add(step.name, args: step.args, params: step.params)
14
+ Steps.new(shortcuts).tap do |processed_steps|
15
+ (configuration_steps + regular_steps).each do |step|
16
+ processed_steps.add(step.name, args: step.args, params: step.params)
17
+ end
22
18
  end
23
19
  end
24
- end
25
-
26
- private
27
-
28
- def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
29
- steps.each_with_object([]) do |step, result|
30
- args = step.args.map do |arg|
31
- arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
32
- end
33
-
34
- if shortcuts.include?(step.name)
35
- t = TraversalStart.new(shortcuts)
36
- action = shortcuts[step.name].apply(t, *args, **step.params)
37
- next if action.nil? || action == t # Shortcut did not add any steps
38
20
 
39
- new_steps = ShortcutsApplyer.call(Steps.from(action))
40
- result.concat(new_steps.configuration_steps)
41
- result.concat(new_steps.steps)
42
- else
43
- result << StepData.new(step.name, args: args, params: step.params)
21
+ private
22
+
23
+ def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
24
+ steps.each_with_object([]) do |step, result|
25
+ args = step.args.map do |arg|
26
+ arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
27
+ end
28
+
29
+ if shortcuts.include?(step.name)
30
+ t = TraversalStart.new(shortcuts)
31
+ action = shortcuts[step.name].apply(t, *args, **step.params)
32
+ next if action.nil? || action == t # Shortcut did not add any steps
33
+
34
+ new_steps = ShortcutsApplyer.call(Steps.from(action))
35
+ result.concat(new_steps.configuration_steps)
36
+ result.concat(new_steps.steps)
37
+ else
38
+ result << StepData.new(step.name, args: args, params: step.params)
39
+ end
44
40
  end
45
41
  end
46
42
  end
@@ -22,7 +22,12 @@ module Grumlin
22
22
  private
23
23
 
24
24
  def serialize_step(step)
25
- [step.name, *step.args.map { |arg| serialize_arg(arg) }, step.params.any? ? step.params : nil].compact
25
+ [step.name].tap do |result|
26
+ step.args.each do |arg|
27
+ result << serialize_arg(arg)
28
+ end
29
+ result << step.params if step.params.any?
30
+ end
26
31
  end
27
32
 
28
33
  def serialize_arg(arg)
@@ -31,7 +31,7 @@ module Grumlin
31
31
  end
32
32
 
33
33
  def __
34
- TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
34
+ @__ ||= TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
35
35
  end
36
36
 
37
37
  def to_s(*)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.18.1"
4
+ VERSION = "0.19.0"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -17,6 +17,8 @@ require "async/barrier"
17
17
  require "async/http/endpoint"
18
18
  require "async/websocket/client"
19
19
 
20
+ require "retryable"
21
+
20
22
  require "zeitwerk"
21
23
 
22
24
  loader = Zeitwerk::Loader.for_gem
@@ -80,6 +82,27 @@ module Grumlin
80
82
 
81
83
  class ServerError < ServerSideError; end
82
84
 
85
+ class AlreadyExistsError < ServerError
86
+ attr_reader :id
87
+
88
+ def initialize(status, query)
89
+ super
90
+ id = status[:message].split(":").last.strip
91
+ @id = id == "" ? nil : id
92
+ end
93
+
94
+ # TODO: parse message and assign @id
95
+ # NOTE: Neptune does not return id.
96
+ end
97
+
98
+ class VertexAlreadyExistsError < AlreadyExistsError; end
99
+ class EdgeAlreadyExistsError < AlreadyExistsError; end
100
+
101
+ class ConcurrentInsertFailedError < ServerError; end
102
+
103
+ class ConcurrentVertexInsertFailedError < ConcurrentInsertFailedError; end
104
+ class ConcurrentEdgeInsertFailedError < ConcurrentInsertFailedError; end
105
+
83
106
  class ServerSerializationError < ServerSideError; end
84
107
 
85
108
  class ServerTimeoutError < ServerSideError; end
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.18.1
4
+ version: 0.19.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-04-14 00:00:00.000000000 Z
11
+ date: 2022-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: retryable
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: zeitwerk
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -113,10 +127,13 @@ files:
113
127
  - lib/grumlin/path.rb
114
128
  - lib/grumlin/property.rb
115
129
  - lib/grumlin/repository.rb
130
+ - lib/grumlin/repository/error_handling_strategy.rb
131
+ - lib/grumlin/repository/instance_methods.rb
116
132
  - lib/grumlin/request_dispatcher.rb
117
133
  - lib/grumlin/shortcut.rb
118
134
  - lib/grumlin/shortcuts.rb
119
135
  - lib/grumlin/shortcuts/properties.rb
136
+ - lib/grumlin/shortcuts/upserts.rb
120
137
  - lib/grumlin/shortcuts_applyer.rb
121
138
  - lib/grumlin/step_data.rb
122
139
  - lib/grumlin/steps.rb