grumlin 0.18.0 → 0.19.1
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 +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +14 -4
- data/README.md +10 -2
- data/grumlin.gemspec +1 -0
- data/lib/async/channel.rb +11 -0
- data/lib/grumlin/repository/error_handling_strategy.rb +44 -0
- data/lib/grumlin/repository/instance_methods.rb +135 -0
- data/lib/grumlin/repository.rb +2 -82
- data/lib/grumlin/request_dispatcher.rb +47 -22
- data/lib/grumlin/shortcuts/properties.rb +4 -2
- data/lib/grumlin/shortcuts/upserts.rb +28 -0
- data/lib/grumlin/shortcuts_applyer.rb +27 -31
- data/lib/grumlin/steps_serializers/bytecode.rb +6 -1
- data/lib/grumlin/traversal_start.rb +1 -1
- data/lib/grumlin/typing.rb +1 -1
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin.rb +23 -0
- metadata +20 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0aea0105e4e4a5a87a4d7cfc025c05218ba5cdec2e9fef267a39f013e30d47fd
|
|
4
|
+
data.tar.gz: e115950e317f51ff52cfea2b17df2c390566c04212fd7df0a0b083ac5c286765
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef169bdc88d8be5c589f15ddf4223eb32adfd0a8e34f3ef67671a21c5fb7fecd44f43dfbe57d215047488e0c8e498a3d9f76e8a1c3517b9d055854e76ee42ccd
|
|
7
|
+
data.tar.gz: 1b1fdf883a92daef8b0e40b4576c3d7f4f4b6bb3fa95c774002f57b7d5d0ff4b0010a3a91c8a500ea9c629ea368649d7e45cc97f3d074f64a02b5d906a136d98
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
grumlin (0.
|
|
4
|
+
grumlin (0.19.1)
|
|
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.
|
|
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.
|
|
80
|
-
protocol-http1 (0.14.
|
|
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
|
-
|
|
192
|
-
|
|
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
|
+
vertices.each_slice(batch_size) do |slice|
|
|
68
|
+
with_upsert_error_handling(on_failure, params) do
|
|
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
|
+
edges.each_slice(batch_size) do |slice|
|
|
92
|
+
with_upsert_error_handling(on_failure, params) do
|
|
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
|
data/lib/grumlin/repository.rb
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
request[:
|
|
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
|
|
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.
|
|
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
|
-
|
|
8
|
-
end
|
|
9
|
-
end
|
|
7
|
+
return steps unless steps.uses_shortcuts?
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
return steps unless steps.uses_shortcuts?
|
|
9
|
+
shortcuts = steps.shortcuts
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
configuration_steps = process_steps(steps.configuration_steps, shortcuts)
|
|
12
|
+
regular_steps = process_steps(steps.steps, shortcuts)
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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)
|
data/lib/grumlin/typing.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Grumlin
|
|
|
18
18
|
# "g:VertexProperty"=> ->(value) { value }, # TODO: implement me
|
|
19
19
|
"g:TraversalMetrics" => ->(value) { cast_map(value[:@value]) },
|
|
20
20
|
"g:Metrics" => ->(value) { cast_map(value[:@value]) },
|
|
21
|
-
"g:T" => ->(value) { T.public_send(value) }
|
|
21
|
+
"g:T" => ->(value) { Grumlin::Expressions::T.public_send(value) }
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
24
|
CASTABLE_TYPES = [Hash, String, Integer, TrueClass, FalseClass].freeze
|
data/lib/grumlin/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.19.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-
|
|
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
|
|
@@ -155,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
155
172
|
- !ruby/object:Gem::Version
|
|
156
173
|
version: '0'
|
|
157
174
|
requirements: []
|
|
158
|
-
rubygems_version: 3.2.
|
|
175
|
+
rubygems_version: 3.2.33
|
|
159
176
|
signing_key:
|
|
160
177
|
specification_version: 4
|
|
161
178
|
summary: Gremlin graph traversal language DSL and client for Ruby.
|