grumlin 0.1.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,172 +1,108 @@
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 < Async::Pool::Resource
6
+ attr_reader :client
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
+ 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
- response_queue.each do |status, response|
62
- reraise_error!(response) if status == :error
63
-
64
- status = response[:status]
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
- check_errors!(status, request_id)
18
+ def closed?
19
+ !@client.connected?
20
+ end
72
21
 
73
- page = Typing.cast(response.dig(:result, :data))
22
+ def close
23
+ @client.close
24
+ end
74
25
 
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
83
- end
26
+ def write(*args)
27
+ @client.write(*args)
84
28
  end
85
29
  end
86
30
 
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)
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 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)
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 check_errors!(status, request_id)
108
- error = ERRORS[status[:code]]
109
- close_request(request_id)
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
- def close_request(request_id)
114
- @requests.delete(request_id)
55
+ reset!
115
56
  end
116
57
 
117
- def reraise_error!(error)
118
- raise error
119
- rescue StandardError
120
- raise ConnectionError
58
+ def connected?
59
+ @transport&.connected? || false
121
60
  end
122
61
 
123
- def query_task(connection)
124
- loop do
125
- connection.write @query_queue.dequeue
126
- connection.flush
127
- end
128
- end
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
- def response_task(connection)
131
- loop do
132
- response = connection.read
133
- response_queue = @requests[response[:requestId]]
134
- response_queue << [:response, response]
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 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
- }
79
+ def inspect
80
+ "<#{self.class} url=#{@url} connected=#{connected?}>"
149
81
  end
150
82
 
151
- def bytecode_query_message(uuid, bytecode)
83
+ alias to_s inspect
84
+
85
+ private
86
+
87
+ def to_query(request_id, message)
152
88
  {
153
- requestId: uuid,
89
+ requestId: request_id,
154
90
  op: "bytecode",
155
91
  processor: "traversal",
156
92
  args: {
157
- gremlin: { "@type": "g:Bytecode", "@value": { step: bytecode } },
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 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
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:) # rubocop:disable Metrics/ParameterLists
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
- "<E #{@label}(#{@id})>"
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
@@ -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
@@ -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,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