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.
@@ -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