grumlin 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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