grumlin 0.1.0 → 0.3.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 +67 -12
- data/.gitignore +1 -0
- data/.overcommit.yml +8 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +30 -23
- data/bin/console +6 -3
- data/bin/setup +1 -0
- data/docker-compose.yml +6 -0
- data/gremlin_server/Dockerfile +3 -0
- data/gremlin_server/tinkergraph-empty.properties +3 -0
- data/grumlin.gemspec +4 -3
- data/lib/grumlin.rb +46 -3
- data/lib/grumlin/anonymous_step.rb +44 -0
- data/lib/grumlin/client.rb +61 -131
- data/lib/grumlin/edge.rb +2 -2
- data/lib/grumlin/exceptions.rb +22 -2
- data/lib/grumlin/order.rb +22 -0
- data/lib/grumlin/p.rb +18 -0
- data/lib/grumlin/path.rb +15 -0
- data/lib/grumlin/pop.rb +32 -0
- data/lib/grumlin/request_dispatcher.rb +84 -0
- data/lib/grumlin/step.rb +18 -29
- data/lib/grumlin/sugar.rb +42 -0
- data/lib/grumlin/t.rb +22 -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 +21 -0
- data/lib/grumlin/translator.rb +22 -21
- data/lib/grumlin/transport.rb +78 -0
- data/lib/grumlin/traversal.rb +4 -11
- data/lib/grumlin/typing.rb +25 -5
- data/lib/grumlin/u.rb +18 -0
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin/vertex.rb +2 -2
- metadata +41 -13
- data/Rakefile +0 -12
- data/bin/stress +0 -51
- data/lib/grumlin/traversing_context.rb +0 -17
data/lib/grumlin/client.rb
CHANGED
@@ -1,172 +1,102 @@
|
|
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 < self
|
6
|
+
attr :concurrency, :count
|
34
7
|
|
35
|
-
|
36
|
-
|
37
|
-
subtask.async { query_task(connection) }
|
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
|
+
new(Grumlin.config.url, concurrency: Grumlin.config.client_concurrency).tap(&:connect)
|
45
10
|
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
11
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
reraise_error!(response) if status == :error
|
12
|
+
def initialize(url, concurrency: 1, parent: Async::Task.current)
|
13
|
+
super(url, parent: parent)
|
14
|
+
@concurrency = concurrency
|
15
|
+
@count = 0
|
16
|
+
end
|
63
17
|
|
64
|
-
|
18
|
+
def viable?
|
19
|
+
connected?
|
20
|
+
end
|
65
21
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
22
|
+
def closed?
|
23
|
+
connected?
|
24
|
+
end
|
70
25
|
|
71
|
-
|
26
|
+
def reusable?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
72
30
|
|
73
|
-
|
31
|
+
def initialize(url, parent: Async::Task.current)
|
32
|
+
@parent = parent
|
33
|
+
@transport = Transport.new(url)
|
34
|
+
reset!
|
35
|
+
end
|
74
36
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
else
|
82
|
-
raise UnknownResponseStatus, status
|
37
|
+
def connect
|
38
|
+
response_queue = @transport.connect
|
39
|
+
@request_dispatcher = RequestDispatcher.new
|
40
|
+
@parent.async do
|
41
|
+
response_queue.each do |response|
|
42
|
+
@request_dispatcher.add_response(response)
|
83
43
|
end
|
84
44
|
end
|
85
45
|
end
|
86
46
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
uuid = SecureRandom.uuid
|
91
|
-
queue = Async::Queue.new
|
92
|
-
@requests[uuid] = queue
|
93
|
-
@query_queue << to_query(uuid, args)
|
47
|
+
def close
|
48
|
+
@transport.close
|
49
|
+
raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
|
94
50
|
|
95
|
-
|
51
|
+
reset!
|
96
52
|
end
|
97
53
|
|
98
|
-
def
|
99
|
-
|
100
|
-
when String
|
101
|
-
string_query_message(uuid, *message)
|
102
|
-
when Grumlin::Step
|
103
|
-
build_query(uuid, message)
|
104
|
-
end
|
54
|
+
def connected?
|
55
|
+
@transport.connected?
|
105
56
|
end
|
106
57
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
58
|
+
# TODO: support yielding
|
59
|
+
def write(*args) # rubocop:disable Metrics/MethodLength
|
60
|
+
request_id = SecureRandom.uuid
|
61
|
+
request = to_query(request_id, args)
|
62
|
+
queue = @request_dispatcher.add_request(request)
|
63
|
+
@transport.write(request)
|
112
64
|
|
113
|
-
|
114
|
-
|
115
|
-
|
65
|
+
begin
|
66
|
+
msg, response = queue.dequeue
|
67
|
+
raise response if msg == :error
|
116
68
|
|
117
|
-
|
118
|
-
raise error
|
119
|
-
rescue StandardError
|
120
|
-
raise ConnectionError
|
121
|
-
end
|
69
|
+
return response.flat_map { |item| Typing.cast(item) } if msg == :result
|
122
70
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
71
|
+
raise "ERROR"
|
72
|
+
rescue Async::Stop
|
73
|
+
retry if @request_dispatcher.ongoing_request?(request_id)
|
74
|
+
raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
|
127
75
|
end
|
128
76
|
end
|
129
77
|
|
130
|
-
def
|
131
|
-
|
132
|
-
response = connection.read
|
133
|
-
response_queue = @requests[response[:requestId]]
|
134
|
-
response_queue << [:response, response]
|
135
|
-
end
|
78
|
+
def inspect
|
79
|
+
"<#{self.class} url=#{@transport.url}>"
|
136
80
|
end
|
137
81
|
|
138
|
-
|
139
|
-
{
|
140
|
-
requestId: uuid,
|
141
|
-
op: "eval",
|
142
|
-
processor: "",
|
143
|
-
args: {
|
144
|
-
gremlin: query,
|
145
|
-
bindings: bindings,
|
146
|
-
language: "gremlin-groovy"
|
147
|
-
}
|
148
|
-
}
|
149
|
-
end
|
82
|
+
alias to_s inspect
|
150
83
|
|
151
|
-
|
84
|
+
private
|
85
|
+
|
86
|
+
def to_query(request_id, message)
|
152
87
|
{
|
153
|
-
requestId:
|
88
|
+
requestId: request_id,
|
154
89
|
op: "bytecode",
|
155
90
|
processor: "traversal",
|
156
91
|
args: {
|
157
|
-
gremlin:
|
92
|
+
gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
|
158
93
|
aliases: { g: :g }
|
159
94
|
}
|
160
95
|
}
|
161
96
|
end
|
162
97
|
|
163
|
-
def
|
164
|
-
|
165
|
-
when :string
|
166
|
-
string_query_message(uuid, *Translator.to_string_query(steps))
|
167
|
-
else
|
168
|
-
bytecode_query_message(uuid, Translator.to_bytecode_query(steps))
|
169
|
-
end
|
98
|
+
def reset!
|
99
|
+
@request_dispatcher = nil
|
170
100
|
end
|
171
101
|
end
|
172
102
|
end
|
data/lib/grumlin/edge.rb
CHANGED
@@ -15,11 +15,11 @@ module Grumlin
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def ==(other)
|
18
|
-
@label == other.label && @id == other.id
|
18
|
+
self.class == other.class && @label == other.label && @id == other.id
|
19
19
|
end
|
20
20
|
|
21
21
|
def inspect
|
22
|
-
"
|
22
|
+
"e[#{@id}][#{@inV}-#{@label}->#{@outV}]"
|
23
23
|
end
|
24
24
|
alias to_s inspect
|
25
25
|
end
|
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,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
module Order
|
5
|
+
module Order
|
6
|
+
DESC = { "@type": "g:Order", "@value": "desc" }.freeze
|
7
|
+
ASC = { "@type": "g:Order", "@value": "desc" }.freeze
|
8
|
+
|
9
|
+
extend self # rubocop:disable Style/ModuleFunction
|
10
|
+
|
11
|
+
def asc
|
12
|
+
ASC
|
13
|
+
end
|
14
|
+
|
15
|
+
def desc
|
16
|
+
DESC
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
extend Order
|
21
|
+
end
|
22
|
+
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,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
module Pop
|
5
|
+
module Pop
|
6
|
+
extend self # rubocop:disable Style/ModuleFunction
|
7
|
+
|
8
|
+
FIRST = { "@type": "g:Pop", "@value": "first" }.freeze
|
9
|
+
LAST = { "@type": "g:Pop", "@value": "last" }.freeze
|
10
|
+
ALL = { "@type": "g:Pop", "@value": "all" }.freeze
|
11
|
+
MIXED = { "@type": "g:Pop", "@value": "mixed" }.freeze
|
12
|
+
|
13
|
+
def first
|
14
|
+
FIRST
|
15
|
+
end
|
16
|
+
|
17
|
+
def last
|
18
|
+
LAST
|
19
|
+
end
|
20
|
+
|
21
|
+
def all
|
22
|
+
ALL
|
23
|
+
end
|
24
|
+
|
25
|
+
def mixed
|
26
|
+
MIXED
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
extend Pop
|
31
|
+
end
|
32
|
+
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::Queue.new.tap do |queue|
|
33
|
+
@requests[request[:requestId]] = { request: request, result: [], queue: queue }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# builds a response object, when it's ready sends it to the client via a queue
|
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, Metrics/MethodLength
|
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[:queue] << [:result, 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[:queue] << [:result, []]
|
54
|
+
close_request(request_id)
|
55
|
+
end
|
56
|
+
rescue StandardError => e
|
57
|
+
request[:queue] << [:error, 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[:queue] << nil
|
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
|