grumlin 0.2.0 → 0.5.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/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +53 -12
- data/Gemfile.lock +5 -4
- data/bin/console +1 -1
- data/grumlin.gemspec +1 -0
- data/lib/async/channel.rb +64 -0
- data/lib/grumlin/client.rb +72 -85
- data/lib/grumlin/edge.rb +1 -3
- data/lib/grumlin/order.rb +2 -6
- data/lib/grumlin/pop.rb +1 -5
- data/lib/grumlin/request_dispatcher.rb +84 -0
- data/lib/grumlin/step.rb +10 -6
- data/lib/grumlin/sugar.rb +3 -17
- data/lib/grumlin/t.rb +1 -5
- data/lib/grumlin/test/rspec/gremlin_context.rb +8 -3
- data/lib/grumlin/transport.rb +86 -0
- data/lib/grumlin/traversal.rb +3 -7
- data/lib/grumlin/u.rb +1 -6
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin.rb +20 -7
- metadata +19 -3
- data/lib/grumlin/transport/async.rb +0 -95
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fb5b809483aeed3b56aeafc11e0f08919a68ce420a14ae918e3aecab791e266
|
4
|
+
data.tar.gz: e42c886a034faeb5ca512882811e78a39f1f5f386b697b0f4061c5be5d5a5f0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2bd8e23cd4e6ab7526e2e4aaf92dddfa2b1e0dc51fd2e15e9e0a09541e9716c741673a8e6f85b077e5fd1a3a5f16519ae533748347b1aff0ba5de47a8394ed47
|
7
|
+
data.tar.gz: 673985aeb319810c061234e1d0964cf6e40b95363288423d7cda4e0932a7e271d51b957dccc5605a160e54e680d2a869bfa32769b393d19387e61e0ad15b7794
|
data/.github/workflows/main.yml
CHANGED
data/.rubocop.yml
CHANGED
@@ -7,24 +7,44 @@ require:
|
|
7
7
|
- rubocop-performance
|
8
8
|
- rubocop-rspec
|
9
9
|
|
10
|
-
Style/StringLiterals:
|
11
|
-
Enabled: true
|
12
|
-
EnforcedStyle: double_quotes
|
13
|
-
|
14
|
-
Style/StringLiteralsInInterpolation:
|
15
|
-
Enabled: true
|
16
|
-
EnforcedStyle: double_quotes
|
17
|
-
|
18
|
-
Style/Documentation:
|
19
|
-
Enabled: false
|
20
|
-
|
21
10
|
Layout/LineLength:
|
22
11
|
Max: 120
|
12
|
+
Exclude:
|
13
|
+
- spec/**/*_spec.rb
|
23
14
|
|
24
15
|
Metrics/BlockLength:
|
25
16
|
Exclude:
|
26
17
|
- spec/**/*_spec.rb
|
27
18
|
|
19
|
+
Metrics/MethodLength:
|
20
|
+
Max: 20
|
21
|
+
|
22
|
+
Metrics/ParameterLists:
|
23
|
+
Max: 6
|
24
|
+
|
25
|
+
Naming/MethodName:
|
26
|
+
IgnoredPatterns:
|
27
|
+
- toList
|
28
|
+
- inVLabel
|
29
|
+
- outVLabel
|
30
|
+
- inV
|
31
|
+
- outV
|
32
|
+
|
33
|
+
Naming/VariableName:
|
34
|
+
AllowedIdentifiers:
|
35
|
+
- inV
|
36
|
+
- outV
|
37
|
+
- inVLabel
|
38
|
+
- outVLabel
|
39
|
+
|
40
|
+
Naming/MethodParameterName:
|
41
|
+
AllowedNames:
|
42
|
+
- id
|
43
|
+
- inV
|
44
|
+
- outV
|
45
|
+
- inVLabel
|
46
|
+
- outVLabel
|
47
|
+
|
28
48
|
RSpec/NamedSubject:
|
29
49
|
Enabled: false
|
30
50
|
|
@@ -34,5 +54,26 @@ RSpec/NestedGroups:
|
|
34
54
|
RSpec/ExampleLength:
|
35
55
|
Enabled: false
|
36
56
|
|
57
|
+
RSpec/MultipleExpectations:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
RSpec/DescribeClass:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
Style/WordArray:
|
64
|
+
Exclude:
|
65
|
+
- spec/**/*_spec.rb
|
66
|
+
|
67
|
+
Style/StringLiterals:
|
68
|
+
Enabled: true
|
69
|
+
EnforcedStyle: double_quotes
|
70
|
+
|
71
|
+
Style/StringLiteralsInInterpolation:
|
72
|
+
Enabled: true
|
73
|
+
EnforcedStyle: double_quotes
|
74
|
+
|
75
|
+
Style/Documentation:
|
76
|
+
Enabled: false
|
77
|
+
|
37
78
|
Style/MultilineBlockChain:
|
38
|
-
Enabled: false
|
79
|
+
Enabled: false
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
grumlin (0.
|
4
|
+
grumlin (0.5.1)
|
5
|
+
async-pool (~> 0.3)
|
5
6
|
async-websocket (~> 0.19)
|
6
7
|
|
7
8
|
GEM
|
@@ -14,7 +15,7 @@ GEM
|
|
14
15
|
tzinfo (~> 2.0)
|
15
16
|
zeitwerk (~> 2.3)
|
16
17
|
ast (2.4.2)
|
17
|
-
async (1.30.
|
18
|
+
async (1.30.1)
|
18
19
|
console (~> 1.10)
|
19
20
|
nio4r (~> 2.3)
|
20
21
|
timers (~> 4.1)
|
@@ -58,7 +59,7 @@ GEM
|
|
58
59
|
kramdown-parser-gfm (1.1.0)
|
59
60
|
kramdown (~> 2.0)
|
60
61
|
minitest (5.14.4)
|
61
|
-
nio4r (2.5.
|
62
|
+
nio4r (2.5.8)
|
62
63
|
nokogiri (1.11.7-x86_64-linux)
|
63
64
|
racc (~> 1.4)
|
64
65
|
overcommit (0.57.0)
|
@@ -124,7 +125,7 @@ GEM
|
|
124
125
|
simplecov_json_formatter (~> 0.1)
|
125
126
|
simplecov-html (0.12.3)
|
126
127
|
simplecov_json_formatter (0.1.3)
|
127
|
-
solargraph (0.
|
128
|
+
solargraph (0.43.0)
|
128
129
|
backport (~> 1.2)
|
129
130
|
benchmark
|
130
131
|
bundler (>= 1.17.2)
|
data/bin/console
CHANGED
data/grumlin.gemspec
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
# Channel is a wrapper around Async::Queue that provides
|
5
|
+
# a protocol and handy tools for passing data, exceptions and closing.
|
6
|
+
# It is designed to be used with only one publisher and one subscriber
|
7
|
+
class Channel
|
8
|
+
class ChannelError < StandardError; end
|
9
|
+
|
10
|
+
class ChannelClosedError < ChannelError; end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@queue = Async::Queue.new
|
14
|
+
@closed = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def closed?
|
18
|
+
@closed
|
19
|
+
end
|
20
|
+
|
21
|
+
# Methods for a publisher
|
22
|
+
def <<(payload)
|
23
|
+
raise(ChannelClosedError, "Cannot send to a closed channel") if @closed
|
24
|
+
|
25
|
+
@queue << [:payload, payload]
|
26
|
+
end
|
27
|
+
|
28
|
+
def exception(exception)
|
29
|
+
raise(ChannelClosedError, "Cannot send to a closed channel") if closed?
|
30
|
+
|
31
|
+
@queue << [:exception, exception]
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
raise(ChannelClosedError, "Cannot close a closed channel") if closed?
|
36
|
+
|
37
|
+
@queue << [:close]
|
38
|
+
@closed = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Methods for a subscriber
|
42
|
+
def dequeue
|
43
|
+
each do |payload| # rubocop:disable Lint/UnreachableLoop this is intended
|
44
|
+
return payload
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def each
|
49
|
+
raise(ChannelClosedError, "Cannot receive from a closed channel") if closed?
|
50
|
+
|
51
|
+
@queue.each do |type, payload|
|
52
|
+
case type
|
53
|
+
when :exception
|
54
|
+
payload.set_backtrace(caller + (payload.backtrace || [])) # A hack to preserve full backtrace
|
55
|
+
raise payload
|
56
|
+
when :payload
|
57
|
+
yield payload
|
58
|
+
when :close
|
59
|
+
break
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/grumlin/client.rb
CHANGED
@@ -2,120 +2,107 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
class Client
|
5
|
-
|
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(url, autoconnect: true)
|
26
|
-
@url = url
|
27
|
-
@transport = Transport::Async.new(url)
|
28
|
-
connect if autoconnect
|
29
|
-
end
|
5
|
+
class PoolResource < Async::Pool::Resource
|
6
|
+
attr_reader :client
|
30
7
|
|
31
|
-
|
8
|
+
def self.call
|
9
|
+
config = Grumlin.config
|
10
|
+
new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
|
11
|
+
end
|
32
12
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
wait_for_response(request_id, queue)
|
38
|
-
ensure
|
39
|
-
@transport.close_request(request_id)
|
40
|
-
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
|
41
17
|
|
42
|
-
|
43
|
-
|
44
|
-
|
18
|
+
def closed?
|
19
|
+
!@client.connected?
|
20
|
+
end
|
45
21
|
|
46
|
-
|
22
|
+
def close
|
23
|
+
@client.close
|
24
|
+
end
|
47
25
|
|
48
|
-
|
26
|
+
def write(*args)
|
27
|
+
@client.write(*args)
|
28
|
+
end
|
29
|
+
end
|
49
30
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
31
|
+
def initialize(url, parent: Async::Task.current, **client_options)
|
32
|
+
@url = url
|
33
|
+
@client_options = client_options
|
34
|
+
@parent = parent
|
35
|
+
reset!
|
36
|
+
end
|
53
37
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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)
|
60
45
|
end
|
46
|
+
rescue StandardError
|
47
|
+
close
|
61
48
|
end
|
62
|
-
rescue ::Async::Stop
|
63
|
-
retry if @transport.ongoing_request?(request_id)
|
64
|
-
raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
|
65
49
|
end
|
66
50
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
bytecode_query_message(request_id, Translator.to_bytecode_query(message))
|
73
|
-
end
|
51
|
+
def close
|
52
|
+
@transport.close
|
53
|
+
raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
|
54
|
+
|
55
|
+
reset!
|
74
56
|
end
|
75
57
|
|
76
|
-
def
|
77
|
-
|
58
|
+
def connected?
|
59
|
+
@transport&.connected? || false
|
60
|
+
end
|
78
61
|
|
79
|
-
|
62
|
+
# TODO: support yielding
|
63
|
+
def write(*args)
|
64
|
+
raise NotConnectedError unless connected?
|
80
65
|
|
81
|
-
|
82
|
-
|
66
|
+
request_id = SecureRandom.uuid
|
67
|
+
request = to_query(request_id, args)
|
68
|
+
channel = @request_dispatcher.add_request(request)
|
69
|
+
@transport.write(request)
|
70
|
+
|
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"
|
83
76
|
end
|
84
|
-
|
85
|
-
return unless SUCCESS[status[:code]].nil?
|
86
|
-
|
87
|
-
raise(UnknownResponseStatus, status)
|
88
77
|
end
|
89
78
|
|
90
|
-
def
|
91
|
-
|
92
|
-
rescue StandardError
|
93
|
-
raise UnknownError
|
79
|
+
def inspect
|
80
|
+
"<#{self.class} url=#{@url} connected=#{connected?}>"
|
94
81
|
end
|
95
82
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
op: "eval",
|
100
|
-
processor: "",
|
101
|
-
args: {
|
102
|
-
gremlin: query,
|
103
|
-
bindings: bindings,
|
104
|
-
language: "gremlin-groovy"
|
105
|
-
}
|
106
|
-
}
|
107
|
-
end
|
83
|
+
alias to_s inspect
|
84
|
+
|
85
|
+
private
|
108
86
|
|
109
|
-
def
|
87
|
+
def to_query(request_id, message)
|
110
88
|
{
|
111
89
|
requestId: request_id,
|
112
90
|
op: "bytecode",
|
113
91
|
processor: "traversal",
|
114
92
|
args: {
|
115
|
-
gremlin: Typing.to_bytecode(
|
93
|
+
gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
|
116
94
|
aliases: { g: :g }
|
117
95
|
}
|
118
96
|
}
|
119
97
|
end
|
98
|
+
|
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)
|
106
|
+
end
|
120
107
|
end
|
121
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
|
@@ -24,4 +23,3 @@ module Grumlin
|
|
24
23
|
alias to_s inspect
|
25
24
|
end
|
26
25
|
end
|
27
|
-
# rubocop:enable Naming/MethodParameterName,Naming/VariableName,Naming/MethodName
|
data/lib/grumlin/order.rb
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
module Order
|
5
|
-
|
5
|
+
class << self
|
6
6
|
DESC = { "@type": "g:Order", "@value": "desc" }.freeze
|
7
|
-
ASC = { "@type": "g:Order", "@value": "
|
8
|
-
|
9
|
-
extend self # rubocop:disable Style/ModuleFunction
|
7
|
+
ASC = { "@type": "g:Order", "@value": "asc" }.freeze
|
10
8
|
|
11
9
|
def asc
|
12
10
|
ASC
|
@@ -16,7 +14,5 @@ module Grumlin
|
|
16
14
|
DESC
|
17
15
|
end
|
18
16
|
end
|
19
|
-
|
20
|
-
extend Order
|
21
17
|
end
|
22
18
|
end
|
data/lib/grumlin/pop.rb
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
module Pop
|
5
|
-
|
6
|
-
extend self # rubocop:disable Style/ModuleFunction
|
7
|
-
|
5
|
+
class << self
|
8
6
|
FIRST = { "@type": "g:Pop", "@value": "first" }.freeze
|
9
7
|
LAST = { "@type": "g:Pop", "@value": "last" }.freeze
|
10
8
|
ALL = { "@type": "g:Pop", "@value": "all" }.freeze
|
@@ -26,7 +24,5 @@ module Grumlin
|
|
26
24
|
MIXED
|
27
25
|
end
|
28
26
|
end
|
29
|
-
|
30
|
-
extend Pop
|
31
27
|
end
|
32
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
|
data/lib/grumlin/step.rb
CHANGED
@@ -4,9 +4,9 @@ module Grumlin
|
|
4
4
|
class Step < AnonymousStep
|
5
5
|
attr_reader :client
|
6
6
|
|
7
|
-
def initialize(
|
7
|
+
def initialize(pool, name, *args, previous_steps: [])
|
8
8
|
super(name, *args, previous_steps: previous_steps)
|
9
|
-
@
|
9
|
+
@pool = pool
|
10
10
|
end
|
11
11
|
|
12
12
|
def next
|
@@ -14,18 +14,22 @@ module Grumlin
|
|
14
14
|
@enum.next
|
15
15
|
end
|
16
16
|
|
17
|
-
def toList
|
18
|
-
@
|
17
|
+
def toList
|
18
|
+
@pool.acquire do |client|
|
19
|
+
client.write(*steps)
|
20
|
+
end
|
19
21
|
end
|
20
22
|
|
21
23
|
def iterate
|
22
|
-
@
|
24
|
+
@pool.acquire do |client|
|
25
|
+
client.write(*(steps + [nil]))
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
private
|
26
30
|
|
27
31
|
def add_step(step_name, args, previous_steps:)
|
28
|
-
self.class.new(@
|
32
|
+
self.class.new(@pool, step_name, *args, previous_steps: previous_steps)
|
29
33
|
end
|
30
34
|
end
|
31
35
|
end
|
data/lib/grumlin/sugar.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
module Sugar
|
5
|
-
# TODO: how to use it in specs?
|
6
5
|
HELPERS = [
|
7
6
|
Grumlin::U,
|
8
7
|
Grumlin::T,
|
@@ -12,25 +11,12 @@ module Grumlin
|
|
12
11
|
].freeze
|
13
12
|
|
14
13
|
def self.included(base)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
module ClassMethods
|
19
|
-
def const_missing(name)
|
20
|
-
helper = HELPERS.find { |h| h.const_defined?(name) }
|
21
|
-
super if helper.nil?
|
22
|
-
|
23
|
-
const_set(name, helper)
|
14
|
+
HELPERS.each do |helper|
|
15
|
+
name = helper.name.split("::").last
|
16
|
+
base.const_set(name, helper)
|
24
17
|
end
|
25
18
|
end
|
26
19
|
|
27
|
-
def const_missing(name)
|
28
|
-
helper = HELPERS.find { |h| h.const_defined?(name) }
|
29
|
-
super if helper.nil?
|
30
|
-
|
31
|
-
const_set(name, helper)
|
32
|
-
end
|
33
|
-
|
34
20
|
def __
|
35
21
|
Grumlin::U
|
36
22
|
end
|
data/lib/grumlin/t.rb
CHANGED
@@ -2,12 +2,10 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
module T
|
5
|
-
|
5
|
+
class << self
|
6
6
|
T_ID = { :@type => "g:T", :@value => "id" }.freeze # TODO: replace with a class?
|
7
7
|
T_LABEL = { :@type => "g:T", :@value => "label" }.freeze # TODO: replace with a class?
|
8
8
|
|
9
|
-
extend self # rubocop:disable Style/ModuleFunction
|
10
|
-
|
11
9
|
def id
|
12
10
|
T_ID
|
13
11
|
end
|
@@ -16,7 +14,5 @@ module Grumlin
|
|
16
14
|
T_LABEL
|
17
15
|
end
|
18
16
|
end
|
19
|
-
|
20
|
-
extend T
|
21
17
|
end
|
22
18
|
end
|
@@ -8,12 +8,17 @@ module Grumlin
|
|
8
8
|
|
9
9
|
::RSpec.shared_context GremlinContext do
|
10
10
|
include GremlinContext
|
11
|
+
include Grumlin::Sugar
|
11
12
|
|
12
|
-
|
13
|
+
before do
|
14
|
+
Grumlin::Sugar::HELPERS.each do |helper|
|
15
|
+
name = helper.name.split("::").last
|
16
|
+
stub_const(name, helper)
|
17
|
+
end
|
18
|
+
end
|
13
19
|
|
14
20
|
after do
|
15
|
-
|
16
|
-
Grumlin.config.default_client.disconnect
|
21
|
+
Grumlin.config.default_pool.close
|
17
22
|
Grumlin.config.reset!
|
18
23
|
end
|
19
24
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grumlin
|
4
|
+
class Transport
|
5
|
+
# A transport based on https://github.com/socketry/async
|
6
|
+
# and https://github.com/socketry/async-websocket
|
7
|
+
|
8
|
+
attr_reader :url
|
9
|
+
|
10
|
+
def initialize(url, parent: Async::Task.current, **client_options)
|
11
|
+
@url = url
|
12
|
+
@parent = parent
|
13
|
+
@client_options = client_options
|
14
|
+
@request_channel = Async::Channel.new
|
15
|
+
@response_channel = Async::Channel.new
|
16
|
+
reset!
|
17
|
+
end
|
18
|
+
|
19
|
+
def connected?
|
20
|
+
@connected
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
24
|
+
raise AlreadyConnectedError if connected?
|
25
|
+
|
26
|
+
@connection = Async::WebSocket::Client.connect(Async::HTTP::Endpoint.parse(@url), **@client_options)
|
27
|
+
|
28
|
+
@response_task = @parent.async do
|
29
|
+
loop do
|
30
|
+
data = @connection.read
|
31
|
+
@response_channel << data
|
32
|
+
end
|
33
|
+
rescue Async::Stop
|
34
|
+
@response_channel.close
|
35
|
+
rescue StandardError => e
|
36
|
+
@response_channel.exception(e)
|
37
|
+
end
|
38
|
+
|
39
|
+
@request_task = @parent.async do
|
40
|
+
@request_channel.each do |message|
|
41
|
+
@connection.write(message)
|
42
|
+
@connection.flush
|
43
|
+
end
|
44
|
+
rescue StandardError => e
|
45
|
+
@response_channel.exception(e)
|
46
|
+
end
|
47
|
+
|
48
|
+
@connected = true
|
49
|
+
|
50
|
+
@response_channel
|
51
|
+
end
|
52
|
+
|
53
|
+
def write(message)
|
54
|
+
raise NotConnectedError unless connected?
|
55
|
+
|
56
|
+
@request_channel << message
|
57
|
+
end
|
58
|
+
|
59
|
+
def close
|
60
|
+
return unless connected?
|
61
|
+
|
62
|
+
@request_channel.close
|
63
|
+
@request_task.wait
|
64
|
+
|
65
|
+
@response_task.stop
|
66
|
+
@response_task.wait
|
67
|
+
|
68
|
+
begin
|
69
|
+
@connection.close
|
70
|
+
rescue Errno::EPIPE
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
reset!
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def reset!
|
80
|
+
@connected = false
|
81
|
+
@connection = nil
|
82
|
+
@response_task = nil
|
83
|
+
@request_task = nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/grumlin/traversal.rb
CHANGED
@@ -4,18 +4,14 @@ module Grumlin
|
|
4
4
|
class Traversal
|
5
5
|
attr_reader :connection
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
Grumlin::Client.new(client_or_url)
|
10
|
-
else
|
11
|
-
client_or_url
|
12
|
-
end
|
7
|
+
def initialize(pool = Grumlin.config.default_pool)
|
8
|
+
@pool = pool
|
13
9
|
end
|
14
10
|
|
15
11
|
# TODO: add other start steps
|
16
12
|
%w[addV addE V E].each do |step|
|
17
13
|
define_method step do |*args|
|
18
|
-
Step.new(@
|
14
|
+
Step.new(@pool, step, *args)
|
19
15
|
end
|
20
16
|
end
|
21
17
|
|
data/lib/grumlin/u.rb
CHANGED
@@ -2,17 +2,12 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
module U
|
5
|
-
|
6
|
-
extend self # rubocop:disable Style/ModuleFunction
|
7
|
-
|
5
|
+
class << self
|
8
6
|
%w[addV V has count out values unfold].each do |step|
|
9
7
|
define_method step do |*args|
|
10
8
|
AnonymousStep.new(step, *args)
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
14
|
-
|
15
|
-
# TODO: add alias __
|
16
|
-
extend U
|
17
12
|
end
|
18
13
|
end
|
data/lib/grumlin/version.rb
CHANGED
data/lib/grumlin.rb
CHANGED
@@ -4,27 +4,33 @@ require "securerandom"
|
|
4
4
|
require "json"
|
5
5
|
|
6
6
|
require "async"
|
7
|
+
require "async/pool"
|
8
|
+
require "async/pool/resource"
|
9
|
+
require "async/pool/controller"
|
7
10
|
require "async/queue"
|
8
11
|
require "async/barrier"
|
9
12
|
require "async/http/endpoint"
|
10
13
|
require "async/websocket/client"
|
11
14
|
|
15
|
+
require_relative "async/channel"
|
16
|
+
|
12
17
|
require_relative "grumlin/version"
|
13
18
|
require_relative "grumlin/exceptions"
|
14
19
|
|
15
|
-
require_relative "grumlin/transport
|
20
|
+
require_relative "grumlin/transport"
|
21
|
+
require_relative "grumlin/client"
|
16
22
|
|
17
23
|
require_relative "grumlin/vertex"
|
18
24
|
require_relative "grumlin/edge"
|
19
25
|
require_relative "grumlin/path"
|
20
26
|
require_relative "grumlin/typing"
|
21
|
-
require_relative "grumlin/client"
|
22
27
|
require_relative "grumlin/traversal"
|
28
|
+
require_relative "grumlin/request_dispatcher"
|
29
|
+
require_relative "grumlin/translator"
|
23
30
|
|
24
31
|
require_relative "grumlin/anonymous_step"
|
25
32
|
require_relative "grumlin/step"
|
26
33
|
|
27
|
-
require_relative "grumlin/translator"
|
28
34
|
require_relative "grumlin/t"
|
29
35
|
require_relative "grumlin/order"
|
30
36
|
require_relative "grumlin/u"
|
@@ -34,14 +40,21 @@ require_relative "grumlin/sugar"
|
|
34
40
|
|
35
41
|
module Grumlin
|
36
42
|
class Config
|
37
|
-
attr_accessor :url
|
43
|
+
attr_accessor :url, :pool_size, :client_concurrency, :client_factory
|
44
|
+
|
45
|
+
# For some reason, client_concurrency must be greater than pool_size
|
46
|
+
def initialize
|
47
|
+
@pool_size = 10
|
48
|
+
@client_concurrency = 20
|
49
|
+
@client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
|
50
|
+
end
|
38
51
|
|
39
|
-
def
|
40
|
-
@
|
52
|
+
def default_pool
|
53
|
+
@default_pool ||= Async::Pool::Controller.new(Grumlin::Client::PoolResource, limit: pool_size)
|
41
54
|
end
|
42
55
|
|
43
56
|
def reset!
|
44
|
-
@
|
57
|
+
@default_pool = nil
|
45
58
|
end
|
46
59
|
end
|
47
60
|
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grumlin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.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: 2021-
|
11
|
+
date: 2021-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async-pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.3'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: async-websocket
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -48,6 +62,7 @@ files:
|
|
48
62
|
- gremlin_server/Dockerfile
|
49
63
|
- gremlin_server/tinkergraph-empty.properties
|
50
64
|
- grumlin.gemspec
|
65
|
+
- lib/async/channel.rb
|
51
66
|
- lib/grumlin.rb
|
52
67
|
- lib/grumlin/anonymous_step.rb
|
53
68
|
- lib/grumlin/client.rb
|
@@ -57,6 +72,7 @@ files:
|
|
57
72
|
- lib/grumlin/p.rb
|
58
73
|
- lib/grumlin/path.rb
|
59
74
|
- lib/grumlin/pop.rb
|
75
|
+
- lib/grumlin/request_dispatcher.rb
|
60
76
|
- lib/grumlin/step.rb
|
61
77
|
- lib/grumlin/sugar.rb
|
62
78
|
- lib/grumlin/t.rb
|
@@ -64,7 +80,7 @@ files:
|
|
64
80
|
- lib/grumlin/test/rspec/db_cleaner_context.rb
|
65
81
|
- lib/grumlin/test/rspec/gremlin_context.rb
|
66
82
|
- lib/grumlin/translator.rb
|
67
|
-
- lib/grumlin/transport
|
83
|
+
- lib/grumlin/transport.rb
|
68
84
|
- lib/grumlin/traversal.rb
|
69
85
|
- lib/grumlin/typing.rb
|
70
86
|
- lib/grumlin/u.rb
|
@@ -1,95 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Grumlin
|
4
|
-
module Transport
|
5
|
-
# A transport based on https://github.com/socketry/async
|
6
|
-
# and https://github.com/socketry/async-websocket
|
7
|
-
class Async
|
8
|
-
attr_reader :requests
|
9
|
-
|
10
|
-
def initialize(url, task: ::Async::Task.current)
|
11
|
-
@task = task
|
12
|
-
@endpoint = ::Async::HTTP::Endpoint.parse(url)
|
13
|
-
|
14
|
-
@requests = {}
|
15
|
-
@query_queue = ::Async::Queue.new
|
16
|
-
end
|
17
|
-
|
18
|
-
def connect
|
19
|
-
raise AlreadyConnectedError if connected?
|
20
|
-
|
21
|
-
@client = ::Async::WebSocket::Client.open(@endpoint)
|
22
|
-
@connection = @client.connect(@endpoint.authority, @endpoint.path)
|
23
|
-
|
24
|
-
@tasks_barrier = ::Async::Barrier.new(parent: @task)
|
25
|
-
|
26
|
-
@tasks_barrier.async { query_task }
|
27
|
-
@tasks_barrier.async { response_task }
|
28
|
-
rescue StandardError
|
29
|
-
raise ConnectionError
|
30
|
-
end
|
31
|
-
|
32
|
-
def disconnect
|
33
|
-
raise NotConnectedError unless connected?
|
34
|
-
|
35
|
-
@tasks_barrier.tasks.each(&:stop)
|
36
|
-
@tasks_barrier.wait
|
37
|
-
|
38
|
-
@connection.close
|
39
|
-
@client.close
|
40
|
-
|
41
|
-
@client = nil
|
42
|
-
@connection = nil
|
43
|
-
@tasks_barrier = nil
|
44
|
-
|
45
|
-
raise ResourceLeakError, "ongoing requests list is not empty: #{@requests.count} items" unless @requests.empty?
|
46
|
-
raise ResourceLeakError, "query queue empty: #{@query.count} items" unless @query_queue.empty?
|
47
|
-
end
|
48
|
-
|
49
|
-
# Raw message
|
50
|
-
def submit(message)
|
51
|
-
raise NotConnectedError unless connected?
|
52
|
-
|
53
|
-
uuid = message[:requestId]
|
54
|
-
::Async::Queue.new.tap do |queue|
|
55
|
-
@requests[uuid] = queue
|
56
|
-
@query_queue << message
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def close_request(request_id)
|
61
|
-
@requests.delete(request_id)
|
62
|
-
end
|
63
|
-
|
64
|
-
def ongoing_request?(request_id)
|
65
|
-
@requests.key?(request_id)
|
66
|
-
end
|
67
|
-
|
68
|
-
def connected?
|
69
|
-
!@connection.nil?
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
def query_task
|
75
|
-
@query_queue.each do |query|
|
76
|
-
@connection.write(query)
|
77
|
-
@connection.flush
|
78
|
-
end
|
79
|
-
rescue StandardError
|
80
|
-
raise DisconnectError
|
81
|
-
end
|
82
|
-
|
83
|
-
def response_task
|
84
|
-
loop do
|
85
|
-
response = @connection.read
|
86
|
-
# TODO: sometimes response does not include requestID, no idea how to handle it so far.
|
87
|
-
response_queue = @requests[response[:requestId]]
|
88
|
-
response_queue << [:response, response]
|
89
|
-
end
|
90
|
-
rescue StandardError
|
91
|
-
raise DisconnectError
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|