grumlin 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,172 +1,102 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- class Client # rubocop:disable Metrics/ClassLength
5
- SUCCESS_STATUS = 200
6
- NO_CONTENT_STATUS = 204
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
- @connection_task = @task.async do |subtask|
36
- Async::WebSocket::Client.connect(@endpoint) do |connection|
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
- def query(*args) # rubocop:disable Metrics/MethodLength
58
- response_queue, request_id = schedule_query(args)
59
- result = []
60
-
61
- response_queue.each do |status, response|
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
- status = response[:status]
18
+ def viable?
19
+ connected?
20
+ end
65
21
 
66
- if status[:code] == NO_CONTENT_STATUS
67
- close_request(request_id)
68
- return []
69
- end
22
+ def closed?
23
+ connected?
24
+ end
70
25
 
71
- check_errors!(status, request_id)
26
+ def reusable?
27
+ true
28
+ end
29
+ end
72
30
 
73
- page = Typing.cast(response.dig(:result, :data))
31
+ def initialize(url, parent: Async::Task.current)
32
+ @parent = parent
33
+ @transport = Transport.new(url)
34
+ reset!
35
+ end
74
36
 
75
- case status[:code]
76
- when SUCCESS_STATUS
77
- close_request(request_id)
78
- return result + page
79
- when PARTIAL_CONTENT_STATUS
80
- result += page
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
- private
88
-
89
- def schedule_query(args)
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
- [queue, uuid]
51
+ reset!
96
52
  end
97
53
 
98
- def to_query(uuid, message)
99
- case message.first
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
- def check_errors!(status, request_id)
108
- error = ERRORS[status[:code]]
109
- close_request(request_id)
110
- raise(error, status) if error
111
- end
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
- def close_request(request_id)
114
- @requests.delete(request_id)
115
- end
65
+ begin
66
+ msg, response = queue.dequeue
67
+ raise response if msg == :error
116
68
 
117
- def reraise_error!(error)
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
- def query_task(connection)
124
- loop do
125
- connection.write @query_queue.dequeue
126
- connection.flush
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 response_task(connection)
131
- loop do
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
- def string_query_message(uuid, query, bindings)
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
- def bytecode_query_message(uuid, bytecode)
84
+ private
85
+
86
+ def to_query(request_id, message)
152
87
  {
153
- requestId: uuid,
88
+ requestId: request_id,
154
89
  op: "bytecode",
155
90
  processor: "traversal",
156
91
  args: {
157
- gremlin: { "@type": "g:Bytecode", "@value": { step: bytecode } },
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 build_query(uuid, steps)
164
- case @mode
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
- "<E #{@label}(#{@id})>"
22
+ "e[#{@id}][#{@inV}-#{@label}->#{@outV}]"
23
23
  end
24
24
  alias to_s inspect
25
25
  end
@@ -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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Path
5
+ def initialize(path)
6
+ @labels = Typing.cast(path[:labels])
7
+ @objects = Typing.cast(path[:objects])
8
+ end
9
+
10
+ def inspect
11
+ "p[#{@objects}]"
12
+ end
13
+ alias to_s inspect
14
+ end
15
+ end
@@ -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