grumlin 0.1.3 → 0.5.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 +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.overcommit.yml +8 -0
- data/.rubocop.yml +55 -11
- data/Gemfile +2 -0
- data/Gemfile.lock +31 -22
- data/bin/console +6 -3
- data/bin/setup +1 -0
- data/grumlin.gemspec +2 -1
- data/lib/async/channel.rb +64 -0
- data/lib/grumlin.rb +49 -3
- data/lib/grumlin/anonymous_step.rb +44 -0
- data/lib/grumlin/client.rb +68 -132
- data/lib/grumlin/edge.rb +3 -5
- data/lib/grumlin/exceptions.rb +22 -2
- data/lib/grumlin/order.rb +18 -0
- data/lib/grumlin/p.rb +18 -0
- data/lib/grumlin/path.rb +15 -0
- data/lib/grumlin/pop.rb +28 -0
- data/lib/grumlin/request_dispatcher.rb +84 -0
- data/lib/grumlin/step.rb +19 -30
- data/lib/grumlin/sugar.rb +28 -0
- data/lib/grumlin/t.rb +18 -0
- data/lib/grumlin/test/rspec.rb +11 -0
- data/lib/grumlin/test/rspec/db_cleaner_context.rb +18 -0
- data/lib/grumlin/test/rspec/gremlin_context.rb +27 -0
- data/lib/grumlin/translator.rb +22 -21
- data/lib/grumlin/transport.rb +86 -0
- data/lib/grumlin/traversal.rb +4 -11
- data/lib/grumlin/typing.rb +25 -5
- data/lib/grumlin/u.rb +13 -0
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin/vertex.rb +2 -2
- metadata +34 -7
- data/bin/stress +0 -51
- data/lib/grumlin/traversing_context.rb +0 -17
data/lib/grumlin/client.rb
CHANGED
@@ -1,172 +1,108 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Grumlin
|
4
|
-
class Client
|
5
|
-
|
6
|
-
|
7
|
-
PARTIAL_CONTENT_STATUS = 206
|
8
|
-
|
9
|
-
ERRORS = {
|
10
|
-
499 => InvalidRequestArgumentsError,
|
11
|
-
500 => ServerError,
|
12
|
-
597 => ScriptEvaluationError,
|
13
|
-
599 => ServerSerializationError,
|
14
|
-
598 => ServerTimeoutError,
|
15
|
-
|
16
|
-
401 => ClientSideError,
|
17
|
-
407 => ClientSideError,
|
18
|
-
498 => ClientSideError
|
19
|
-
}.freeze
|
20
|
-
|
21
|
-
def initialize(url, task: Async::Task.current, autoconnect: true, mode: :bytecode)
|
22
|
-
@task = task
|
23
|
-
@endpoint = Async::HTTP::Endpoint.parse(url)
|
24
|
-
@mode = mode
|
25
|
-
|
26
|
-
@requests = {}
|
27
|
-
@query_queue = Async::Queue.new
|
28
|
-
|
29
|
-
connect if autoconnect
|
30
|
-
end
|
31
|
-
|
32
|
-
def connect # rubocop:disable Metrics/MethodLength
|
33
|
-
raise AlreadyConnectedError unless @connection_task.nil?
|
4
|
+
class Client
|
5
|
+
class PoolResource < Async::Pool::Resource
|
6
|
+
attr_reader :client
|
34
7
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
response_task(connection)
|
39
|
-
end
|
40
|
-
rescue StandardError => e
|
41
|
-
@requests.each_value do |queue|
|
42
|
-
queue << [:error, e]
|
43
|
-
end
|
44
|
-
disconnect
|
8
|
+
def self.call
|
9
|
+
config = Grumlin.config
|
10
|
+
new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
|
45
11
|
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def disconnect
|
49
|
-
raise NotConnectedError if @connection_task.nil?
|
50
|
-
|
51
|
-
@connection_task&.stop
|
52
|
-
@connection_task&.wait
|
53
|
-
@connection_task = nil
|
54
|
-
@requests = {}
|
55
|
-
end
|
56
|
-
|
57
|
-
def query(*args) # rubocop:disable Metrics/MethodLength
|
58
|
-
response_queue, request_id = schedule_query(args)
|
59
|
-
result = []
|
60
12
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
if status[:code] == NO_CONTENT_STATUS
|
67
|
-
close_request(request_id)
|
68
|
-
return []
|
69
|
-
end
|
13
|
+
def initialize(url, client_factory:, concurrency: 1, parent: Async::Task.current)
|
14
|
+
super(concurrency)
|
15
|
+
@client = client_factory.call(url, parent).tap(&:connect)
|
16
|
+
end
|
70
17
|
|
71
|
-
|
18
|
+
def closed?
|
19
|
+
!@client.connected?
|
20
|
+
end
|
72
21
|
|
73
|
-
|
22
|
+
def close
|
23
|
+
@client.close
|
24
|
+
end
|
74
25
|
|
75
|
-
|
76
|
-
|
77
|
-
close_request(request_id)
|
78
|
-
return result + page
|
79
|
-
when PARTIAL_CONTENT_STATUS
|
80
|
-
result += page
|
81
|
-
else
|
82
|
-
raise UnknownResponseStatus, status
|
83
|
-
end
|
26
|
+
def write(*args)
|
27
|
+
@client.write(*args)
|
84
28
|
end
|
85
29
|
end
|
86
30
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
@requests[uuid] = queue
|
93
|
-
@query_queue << to_query(uuid, args)
|
94
|
-
|
95
|
-
[queue, uuid]
|
31
|
+
def initialize(url, parent: Async::Task.current, **client_options)
|
32
|
+
@url = url
|
33
|
+
@client_options = client_options
|
34
|
+
@parent = parent
|
35
|
+
reset!
|
96
36
|
end
|
97
37
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
38
|
+
def connect
|
39
|
+
@transport = build_transport
|
40
|
+
response_channel = @transport.connect
|
41
|
+
@request_dispatcher = RequestDispatcher.new
|
42
|
+
@parent.async do
|
43
|
+
response_channel.each do |response|
|
44
|
+
@request_dispatcher.add_response(response)
|
45
|
+
end
|
46
|
+
rescue StandardError
|
47
|
+
close
|
104
48
|
end
|
105
49
|
end
|
106
50
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
raise(error, status) if error
|
111
|
-
end
|
51
|
+
def close
|
52
|
+
@transport.close
|
53
|
+
raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
|
112
54
|
|
113
|
-
|
114
|
-
@requests.delete(request_id)
|
55
|
+
reset!
|
115
56
|
end
|
116
57
|
|
117
|
-
def
|
118
|
-
|
119
|
-
rescue StandardError
|
120
|
-
raise ConnectionError
|
58
|
+
def connected?
|
59
|
+
@transport&.connected? || false
|
121
60
|
end
|
122
61
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
62
|
+
# TODO: support yielding
|
63
|
+
def write(*args)
|
64
|
+
raise NotConnectedError unless connected?
|
65
|
+
|
66
|
+
request_id = SecureRandom.uuid
|
67
|
+
request = to_query(request_id, args)
|
68
|
+
channel = @request_dispatcher.add_request(request)
|
69
|
+
@transport.write(request)
|
129
70
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
71
|
+
begin
|
72
|
+
channel.dequeue.flat_map { |item| Typing.cast(item) }
|
73
|
+
rescue Async::Stop
|
74
|
+
retry if @request_dispatcher.ongoing_request?(request_id)
|
75
|
+
raise Grumlin::UnknownRequestStoppedError, "#{request_id} is not in the ongoing requests list"
|
135
76
|
end
|
136
77
|
end
|
137
78
|
|
138
|
-
def
|
139
|
-
{
|
140
|
-
requestId: uuid,
|
141
|
-
op: "eval",
|
142
|
-
processor: "",
|
143
|
-
args: {
|
144
|
-
gremlin: query,
|
145
|
-
bindings: bindings,
|
146
|
-
language: "gremlin-groovy"
|
147
|
-
}
|
148
|
-
}
|
79
|
+
def inspect
|
80
|
+
"<#{self.class} url=#{@url} connected=#{connected?}>"
|
149
81
|
end
|
150
82
|
|
151
|
-
|
83
|
+
alias to_s inspect
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def to_query(request_id, message)
|
152
88
|
{
|
153
|
-
requestId:
|
89
|
+
requestId: request_id,
|
154
90
|
op: "bytecode",
|
155
91
|
processor: "traversal",
|
156
92
|
args: {
|
157
|
-
gremlin:
|
93
|
+
gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
|
158
94
|
aliases: { g: :g }
|
159
95
|
}
|
160
96
|
}
|
161
97
|
end
|
162
98
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
99
|
+
def reset!
|
100
|
+
@request_dispatcher = nil
|
101
|
+
@transport = nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_transport
|
105
|
+
Transport.new(@url, parent: @parent, **@client_options)
|
170
106
|
end
|
171
107
|
end
|
172
108
|
end
|
data/lib/grumlin/edge.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Naming/VariableName,Naming/MethodParameterName,Naming/MethodName
|
4
3
|
module Grumlin
|
5
4
|
class Edge
|
6
5
|
attr_reader :label, :id, :inVLabel, :outVLabel, :inV, :outV
|
7
6
|
|
8
|
-
def initialize(label:, id:, inVLabel:, outVLabel:, inV:, outV:)
|
7
|
+
def initialize(label:, id:, inVLabel:, outVLabel:, inV:, outV:)
|
9
8
|
@label = label
|
10
9
|
@id = Typing.cast(id)
|
11
10
|
@inVLabel = inVLabel
|
@@ -15,13 +14,12 @@ module Grumlin
|
|
15
14
|
end
|
16
15
|
|
17
16
|
def ==(other)
|
18
|
-
@label == other.label && @id == other.id
|
17
|
+
self.class == other.class && @label == other.label && @id == other.id
|
19
18
|
end
|
20
19
|
|
21
20
|
def inspect
|
22
|
-
"
|
21
|
+
"e[#{@id}][#{@inV}-#{@label}->#{@outV}]"
|
23
22
|
end
|
24
23
|
alias to_s inspect
|
25
24
|
end
|
26
25
|
end
|
27
|
-
# rubocop:enable Naming/MethodParameterName,Naming/VariableName,Naming/MethodName
|
data/lib/grumlin/exceptions.rb
CHANGED
@@ -3,8 +3,14 @@
|
|
3
3
|
module Grumlin
|
4
4
|
class Error < StandardError; end
|
5
5
|
|
6
|
+
class UnknownError < Error; end
|
7
|
+
|
6
8
|
class ConnectionError < Error; end
|
7
9
|
|
10
|
+
class CannotConnectError < ConnectionError; end
|
11
|
+
|
12
|
+
class DisconnectError < ConnectionError; end
|
13
|
+
|
8
14
|
class ConnectionStatusError < Error; end
|
9
15
|
|
10
16
|
class NotConnectedError < ConnectionStatusError; end
|
@@ -24,8 +30,6 @@ module Grumlin
|
|
24
30
|
|
25
31
|
class UnknownTypeError < ProtocolError; end
|
26
32
|
|
27
|
-
class ConnectionClosedError < Error; end
|
28
|
-
|
29
33
|
class StatusError < Error
|
30
34
|
attr_reader :status
|
31
35
|
|
@@ -48,4 +52,20 @@ module Grumlin
|
|
48
52
|
class ServerSerializationError < ServerSideError; end
|
49
53
|
|
50
54
|
class ServerTimeoutError < ServerSideError; end
|
55
|
+
|
56
|
+
class InternalClientError < Error; end
|
57
|
+
|
58
|
+
class UnknownRequestStoppedError < InternalClientError; end
|
59
|
+
|
60
|
+
class ResourceLeakError < InternalClientError; end
|
61
|
+
|
62
|
+
class UnknownMapKey < InternalClientError
|
63
|
+
attr_reader :key, :map
|
64
|
+
|
65
|
+
def initialize(key, map)
|
66
|
+
@key = key
|
67
|
+
@map = map
|
68
|
+
super("Cannot cast key #{key} in map #{map}")
|
69
|
+
end
|
70
|
+
end
|
51
71
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
module Order
|
5
|
+
class << self
|
6
|
+
DESC = { "@type": "g:Order", "@value": "desc" }.freeze
|
7
|
+
ASC = { "@type": "g:Order", "@value": "desc" }.freeze
|
8
|
+
|
9
|
+
def asc
|
10
|
+
ASC
|
11
|
+
end
|
12
|
+
|
13
|
+
def desc
|
14
|
+
DESC
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/grumlin/p.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
module P
|
5
|
+
module P
|
6
|
+
%w[within].each do |step|
|
7
|
+
define_method step do |*args|
|
8
|
+
{ # TODO: replace with a class?
|
9
|
+
"@type": "g:P",
|
10
|
+
"@value": { predicate: "within", value: { "@type": "g:List", "@value": args } }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
extend P
|
17
|
+
end
|
18
|
+
end
|
data/lib/grumlin/path.rb
ADDED
data/lib/grumlin/pop.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
module Pop
|
5
|
+
class << self
|
6
|
+
FIRST = { "@type": "g:Pop", "@value": "first" }.freeze
|
7
|
+
LAST = { "@type": "g:Pop", "@value": "last" }.freeze
|
8
|
+
ALL = { "@type": "g:Pop", "@value": "all" }.freeze
|
9
|
+
MIXED = { "@type": "g:Pop", "@value": "mixed" }.freeze
|
10
|
+
|
11
|
+
def first
|
12
|
+
FIRST
|
13
|
+
end
|
14
|
+
|
15
|
+
def last
|
16
|
+
LAST
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
ALL
|
21
|
+
end
|
22
|
+
|
23
|
+
def mixed
|
24
|
+
MIXED
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
class RequestDispatcher
|
5
|
+
attr_reader :requests
|
6
|
+
|
7
|
+
SUCCESS = {
|
8
|
+
200 => :success,
|
9
|
+
204 => :no_content,
|
10
|
+
206 => :partial_content
|
11
|
+
}.freeze
|
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
|
+
def initialize
|
26
|
+
@requests = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_request(request)
|
30
|
+
raise "ERROR" if @requests.key?(request[:requestId])
|
31
|
+
|
32
|
+
Async::Channel.new.tap do |channel|
|
33
|
+
@requests[request[:requestId]] = { request: request, result: [], channel: channel }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# builds a response object, when it's ready sends it to the client via a channel
|
38
|
+
# TODO: sometimes response does not include requestID, no idea how to handle it so far.
|
39
|
+
def add_response(response) # rubocop:disable Metrics/AbcSize
|
40
|
+
request_id = response[:requestId]
|
41
|
+
raise "ERROR" unless ongoing_request?(request_id)
|
42
|
+
|
43
|
+
request = @requests[request_id]
|
44
|
+
|
45
|
+
check_errors!(response[:status])
|
46
|
+
|
47
|
+
case SUCCESS[response.dig(:status, :code)]
|
48
|
+
when :success
|
49
|
+
request[:channel] << request[:result] + [response.dig(:result, :data)]
|
50
|
+
close_request(request_id)
|
51
|
+
when :partial_content then request[:result] << response.dig(:result, :data)
|
52
|
+
when :no_content
|
53
|
+
request[:channel] << []
|
54
|
+
close_request(request_id)
|
55
|
+
end
|
56
|
+
rescue StandardError => e
|
57
|
+
request[:channel].exception(e)
|
58
|
+
close_request(request_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def close_request(request_id)
|
62
|
+
raise "ERROR" unless ongoing_request?(request_id)
|
63
|
+
|
64
|
+
request = @requests.delete(request_id)
|
65
|
+
request[:channel].close
|
66
|
+
end
|
67
|
+
|
68
|
+
def ongoing_request?(request_id)
|
69
|
+
@requests.key?(request_id)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def check_errors!(status)
|
75
|
+
if (error = ERRORS[status[:code]])
|
76
|
+
raise(error, status)
|
77
|
+
end
|
78
|
+
|
79
|
+
return unless SUCCESS[status[:code]].nil?
|
80
|
+
|
81
|
+
raise(UnknownResponseStatus, status)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|