pulsar-ruby 0.1.0.pre
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 +7 -0
- data/LICENSE +204 -0
- data/README.md +198 -0
- data/lib/pulsar/client.rb +136 -0
- data/lib/pulsar/consumer.rb +47 -0
- data/lib/pulsar/errors.rb +18 -0
- data/lib/pulsar/internal/bounded_queue.rb +78 -0
- data/lib/pulsar/internal/broker_error_mapper.rb +23 -0
- data/lib/pulsar/internal/command_factory.rb +118 -0
- data/lib/pulsar/internal/connection.rb +287 -0
- data/lib/pulsar/internal/consumer_impl.rb +130 -0
- data/lib/pulsar/internal/frame_codec.rb +62 -0
- data/lib/pulsar/internal/lookup_service.rb +28 -0
- data/lib/pulsar/internal/producer_impl.rb +146 -0
- data/lib/pulsar/internal/promise.rb +53 -0
- data/lib/pulsar/internal/tcp_transport.rb +76 -0
- data/lib/pulsar/internal/thread_runtime.rb +54 -0
- data/lib/pulsar/internal.rb +7 -0
- data/lib/pulsar/message.rb +21 -0
- data/lib/pulsar/message_id.rb +43 -0
- data/lib/pulsar/producer.rb +39 -0
- data/lib/pulsar/proto/PulsarApi_pb.rb +1002 -0
- data/lib/pulsar/proto/pulsar_api_pb.rb +3 -0
- data/lib/pulsar/version.rb +5 -0
- data/lib/pulsar.rb +26 -0
- data/proto/PulsarApi.proto +1360 -0
- metadata +84 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Converts broker protocol error codes into typed client exceptions.
|
|
6
|
+
class BrokerErrorMapper
|
|
7
|
+
ERROR_CLASSES = {
|
|
8
|
+
AuthenticationError: AuthenticationError,
|
|
9
|
+
AuthorizationError: AuthorizationError,
|
|
10
|
+
TopicNotFound: TopicNotFoundError,
|
|
11
|
+
ProducerBusy: ProducerBusyError,
|
|
12
|
+
ProducerFenced: ProducerBusyError,
|
|
13
|
+
ConsumerBusy: ConsumerBusyError
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.from(server_error, message)
|
|
17
|
+
error_class = ERROR_CLASSES.fetch(server_error, BrokerError)
|
|
18
|
+
text = error_class == BrokerError ? "#{server_error}: #{message}" : message
|
|
19
|
+
error_class.new(text)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Builds protobuf commands for the Pulsar binary protocol.
|
|
6
|
+
class CommandFactory
|
|
7
|
+
def self.producer(topic:, producer_id:, request_id:)
|
|
8
|
+
Proto::BaseCommand.new(
|
|
9
|
+
type: :PRODUCER,
|
|
10
|
+
producer: Proto::CommandProducer.new(
|
|
11
|
+
topic: topic,
|
|
12
|
+
producer_id: producer_id,
|
|
13
|
+
request_id: request_id
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.send_message(producer_id:, sequence_id:, producer_name:, publish_time:, properties: {}, key: nil,
|
|
19
|
+
event_time: nil)
|
|
20
|
+
command = Proto::BaseCommand.new(
|
|
21
|
+
type: :SEND,
|
|
22
|
+
send: Proto::CommandSend.new(
|
|
23
|
+
producer_id: producer_id,
|
|
24
|
+
sequence_id: sequence_id
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
metadata = Proto::MessageMetadata.new(
|
|
28
|
+
producer_name: producer_name,
|
|
29
|
+
sequence_id: sequence_id,
|
|
30
|
+
publish_time: publish_time,
|
|
31
|
+
properties: properties.map { |key_value, value| Proto::KeyValue.new(key: key_value.to_s, value: value.to_s) }
|
|
32
|
+
)
|
|
33
|
+
metadata.partition_key = key if key
|
|
34
|
+
metadata.event_time = event_time if event_time
|
|
35
|
+
|
|
36
|
+
[command, metadata]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.subscribe(topic:, subscription:, consumer_id:, request_id:, subscription_type: :Exclusive)
|
|
40
|
+
Proto::BaseCommand.new(
|
|
41
|
+
type: :SUBSCRIBE,
|
|
42
|
+
subscribe: Proto::CommandSubscribe.new(
|
|
43
|
+
topic: topic,
|
|
44
|
+
subscription: subscription,
|
|
45
|
+
subType: subscription_type,
|
|
46
|
+
consumer_id: consumer_id,
|
|
47
|
+
request_id: request_id
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.flow(consumer_id:, permits:)
|
|
53
|
+
Proto::BaseCommand.new(
|
|
54
|
+
type: :FLOW,
|
|
55
|
+
flow: Proto::CommandFlow.new(
|
|
56
|
+
consumer_id: consumer_id,
|
|
57
|
+
messagePermits: permits
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.ack(consumer_id:, message_id:)
|
|
63
|
+
Proto::BaseCommand.new(
|
|
64
|
+
type: :ACK,
|
|
65
|
+
ack: Proto::CommandAck.new(
|
|
66
|
+
consumer_id: consumer_id,
|
|
67
|
+
ack_type: :Individual,
|
|
68
|
+
message_id: [
|
|
69
|
+
Proto::MessageIdData.new(
|
|
70
|
+
ledgerId: message_id.ledger_id,
|
|
71
|
+
entryId: message_id.entry_id,
|
|
72
|
+
partition: message_id.partition_index,
|
|
73
|
+
batch_index: message_id.batch_index
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.lookup(topic:, request_id:)
|
|
81
|
+
Proto::BaseCommand.new(
|
|
82
|
+
type: :LOOKUP,
|
|
83
|
+
lookupTopic: Proto::CommandLookupTopic.new(
|
|
84
|
+
topic: topic,
|
|
85
|
+
request_id: request_id
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.close_producer(producer_id:, request_id:)
|
|
91
|
+
Proto::BaseCommand.new(
|
|
92
|
+
type: :CLOSE_PRODUCER,
|
|
93
|
+
close_producer: Proto::CommandCloseProducer.new(
|
|
94
|
+
producer_id: producer_id,
|
|
95
|
+
request_id: request_id
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.close_consumer(consumer_id:, request_id:)
|
|
101
|
+
Proto::BaseCommand.new(
|
|
102
|
+
type: :CLOSE_CONSUMER,
|
|
103
|
+
close_consumer: Proto::CommandCloseConsumer.new(
|
|
104
|
+
consumer_id: consumer_id,
|
|
105
|
+
request_id: request_id
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.pong
|
|
111
|
+
Proto::BaseCommand.new(
|
|
112
|
+
type: :PONG,
|
|
113
|
+
pong: Proto::CommandPong.new
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Owns a broker socket, request correlation, and reader thread.
|
|
6
|
+
class Connection
|
|
7
|
+
PROTOCOL_VERSION = 21
|
|
8
|
+
|
|
9
|
+
attr_reader :server_version, :protocol_version, :max_message_size
|
|
10
|
+
|
|
11
|
+
def self.connect(host:, port:, connection_timeout:, operation_timeout:, client_version:)
|
|
12
|
+
transport = TcpTransport.connect(host: host, port: port, connection_timeout: connection_timeout)
|
|
13
|
+
new(
|
|
14
|
+
transport: transport,
|
|
15
|
+
operation_timeout: operation_timeout,
|
|
16
|
+
client_version: client_version
|
|
17
|
+
).tap(&:connect)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(transport:, operation_timeout:, client_version:)
|
|
21
|
+
@transport = transport
|
|
22
|
+
@operation_timeout = operation_timeout
|
|
23
|
+
@client_version = client_version
|
|
24
|
+
@connected = false
|
|
25
|
+
@closed = false
|
|
26
|
+
@state_mutex = Mutex.new
|
|
27
|
+
@write_mutex = Mutex.new
|
|
28
|
+
@request_id = 0
|
|
29
|
+
@pending_requests = {}
|
|
30
|
+
@pending_sends = {}
|
|
31
|
+
@consumers = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def connect
|
|
35
|
+
write_connect_command
|
|
36
|
+
command = read_command(timeout: @operation_timeout)
|
|
37
|
+
raise ProtocolError, "expected CONNECTED response, got #{command.type}" unless command.type == :CONNECTED
|
|
38
|
+
|
|
39
|
+
@server_version = command.connected.server_version
|
|
40
|
+
@protocol_version = command.connected.protocol_version
|
|
41
|
+
@max_message_size = command.connected.max_message_size
|
|
42
|
+
@connected = true
|
|
43
|
+
start_reader_thread
|
|
44
|
+
self
|
|
45
|
+
rescue Error
|
|
46
|
+
close
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def connected?
|
|
51
|
+
@connected && !closed?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
return nil if closed?
|
|
56
|
+
|
|
57
|
+
@closed = true
|
|
58
|
+
@connected = false
|
|
59
|
+
@transport.close
|
|
60
|
+
reject_pending(ClosedError.new('connection is closed'))
|
|
61
|
+
@reader_thread&.join unless Thread.current == @reader_thread
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def closed?
|
|
66
|
+
@closed
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def next_request_id
|
|
70
|
+
@state_mutex.synchronize do
|
|
71
|
+
@request_id += 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def request(command, timeout: @operation_timeout)
|
|
76
|
+
ensure_connected!
|
|
77
|
+
promise = Promise.new
|
|
78
|
+
request_id = request_id_for(command)
|
|
79
|
+
add_pending_request(request_id, promise)
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
write_frame(FrameCodec.encode_command(command))
|
|
83
|
+
promise.wait(timeout: timeout)
|
|
84
|
+
ensure
|
|
85
|
+
remove_pending_request(request_id)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_message(command, metadata, payload, timeout: @operation_timeout)
|
|
90
|
+
ensure_connected!
|
|
91
|
+
promise = Promise.new
|
|
92
|
+
send_key = [command['send'].producer_id, command['send'].sequence_id]
|
|
93
|
+
add_pending_send(send_key, promise)
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
write_frame(FrameCodec.encode_message(command, metadata, payload))
|
|
97
|
+
promise.wait(timeout: timeout)
|
|
98
|
+
ensure
|
|
99
|
+
remove_pending_send(send_key)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def write_command(command)
|
|
104
|
+
ensure_connected!
|
|
105
|
+
|
|
106
|
+
write_frame(FrameCodec.encode_command(command))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def register_consumer(consumer_id, consumer)
|
|
110
|
+
@state_mutex.synchronize { @consumers[consumer_id] = consumer }
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def unregister_consumer(consumer_id)
|
|
115
|
+
@state_mutex.synchronize { @consumers.delete(consumer_id) }
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def read_frame(timeout: @operation_timeout)
|
|
120
|
+
ensure_connected!
|
|
121
|
+
|
|
122
|
+
read_decoded_frame(timeout: timeout)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def write_connect_command
|
|
128
|
+
command = Proto::BaseCommand.new(
|
|
129
|
+
type: :CONNECT,
|
|
130
|
+
connect: Proto::CommandConnect.new(
|
|
131
|
+
client_version: @client_version,
|
|
132
|
+
protocol_version: PROTOCOL_VERSION
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
write_frame(FrameCodec.encode_command(command))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def read_command(timeout:)
|
|
139
|
+
read_decoded_frame(timeout: timeout).command
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def read_decoded_frame(timeout:)
|
|
143
|
+
size_prefix = @transport.read_exact(4, timeout: timeout)
|
|
144
|
+
size = size_prefix.unpack1('N')
|
|
145
|
+
frame = size_prefix + @transport.read_exact(size, timeout: timeout)
|
|
146
|
+
FrameCodec.decode_frame(frame)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def ensure_connected!
|
|
150
|
+
raise ClosedError, 'connection is closed' if closed?
|
|
151
|
+
raise ConnectionError, 'connection is not connected' unless connected?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def write_frame(frame)
|
|
155
|
+
@write_mutex.synchronize { @transport.write(frame) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def start_reader_thread
|
|
159
|
+
@reader_thread = Thread.new { reader_loop }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reader_loop
|
|
163
|
+
loop do
|
|
164
|
+
break if closed?
|
|
165
|
+
|
|
166
|
+
route_frame(read_decoded_frame(timeout: @operation_timeout))
|
|
167
|
+
end
|
|
168
|
+
rescue ClosedError
|
|
169
|
+
reject_pending(ClosedError.new('connection is closed')) unless closed?
|
|
170
|
+
rescue ConnectionError => e
|
|
171
|
+
if closed?
|
|
172
|
+
reject_pending(ClosedError.new('connection is closed'))
|
|
173
|
+
else
|
|
174
|
+
fail_connection(ConnectionError.new("connection lost: #{e.message}"))
|
|
175
|
+
end
|
|
176
|
+
rescue Error => e
|
|
177
|
+
reject_pending(e)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def route_frame(decoded)
|
|
181
|
+
command = decoded.command
|
|
182
|
+
|
|
183
|
+
case command.type
|
|
184
|
+
when :MESSAGE
|
|
185
|
+
consumer_for(command.message.consumer_id)&.handle_message(command.message, decoded.headers_and_payload)
|
|
186
|
+
when :PING
|
|
187
|
+
write_command(CommandFactory.pong)
|
|
188
|
+
when :SEND_RECEIPT
|
|
189
|
+
fulfill_pending_send(command.send_receipt.producer_id, command.send_receipt.sequence_id, command)
|
|
190
|
+
when :SEND_ERROR
|
|
191
|
+
reject_pending_send(
|
|
192
|
+
command.send_error.producer_id,
|
|
193
|
+
command.send_error.sequence_id,
|
|
194
|
+
BrokerErrorMapper.from(command.send_error.error, command.send_error.message)
|
|
195
|
+
)
|
|
196
|
+
when :ERROR
|
|
197
|
+
reject_pending_request(command.error.request_id, BrokerErrorMapper.from(command.error.error, command.error.message))
|
|
198
|
+
else
|
|
199
|
+
fulfill_pending_request(response_request_id(command), command)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def add_pending_request(request_id, promise)
|
|
204
|
+
@state_mutex.synchronize { @pending_requests[request_id] = promise }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def remove_pending_request(request_id)
|
|
208
|
+
@state_mutex.synchronize { @pending_requests.delete(request_id) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def fulfill_pending_request(request_id, command)
|
|
212
|
+
promise = @state_mutex.synchronize { @pending_requests.delete(request_id) }
|
|
213
|
+
promise&.fulfill(command)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def reject_pending_request(request_id, error)
|
|
217
|
+
promise = @state_mutex.synchronize { @pending_requests.delete(request_id) }
|
|
218
|
+
promise&.reject(error)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def add_pending_send(send_key, promise)
|
|
222
|
+
@state_mutex.synchronize { @pending_sends[send_key] = promise }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def remove_pending_send(send_key)
|
|
226
|
+
@state_mutex.synchronize { @pending_sends.delete(send_key) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def fulfill_pending_send(producer_id, sequence_id, command)
|
|
230
|
+
promise = @state_mutex.synchronize { @pending_sends.delete([producer_id, sequence_id]) }
|
|
231
|
+
promise&.fulfill(command)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def reject_pending_send(producer_id, sequence_id, error)
|
|
235
|
+
promise = @state_mutex.synchronize { @pending_sends.delete([producer_id, sequence_id]) }
|
|
236
|
+
promise&.reject(error)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def consumer_for(consumer_id)
|
|
240
|
+
@state_mutex.synchronize { @consumers[consumer_id] }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def reject_pending(error)
|
|
244
|
+
requests, sends = @state_mutex.synchronize do
|
|
245
|
+
[@pending_requests.values.tap { @pending_requests.clear },
|
|
246
|
+
@pending_sends.values.tap { @pending_sends.clear }]
|
|
247
|
+
end
|
|
248
|
+
(requests + sends).each { |promise| promise.reject(error) }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def fail_connection(error)
|
|
252
|
+
@state_mutex.synchronize { @connected = false }
|
|
253
|
+
reject_pending(error)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def request_id_for(command)
|
|
257
|
+
case command.type
|
|
258
|
+
when :PRODUCER
|
|
259
|
+
command.producer.request_id
|
|
260
|
+
when :SUBSCRIBE
|
|
261
|
+
command.subscribe.request_id
|
|
262
|
+
when :LOOKUP
|
|
263
|
+
command.lookupTopic.request_id
|
|
264
|
+
when :CLOSE_PRODUCER
|
|
265
|
+
command.close_producer.request_id
|
|
266
|
+
when :CLOSE_CONSUMER
|
|
267
|
+
command.close_consumer.request_id
|
|
268
|
+
else
|
|
269
|
+
raise ProtocolError, "command #{command.type} does not have a request id"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def response_request_id(command)
|
|
274
|
+
case command.type
|
|
275
|
+
when :PRODUCER_SUCCESS
|
|
276
|
+
command.producer_success.request_id
|
|
277
|
+
when :SUCCESS
|
|
278
|
+
command.success.request_id
|
|
279
|
+
when :LOOKUP_RESPONSE
|
|
280
|
+
command.lookupTopicResponse.request_id
|
|
281
|
+
else
|
|
282
|
+
raise ProtocolError, "unexpected broker command #{command.type}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Implements broker-side subscription, flow, receive, and ack behavior.
|
|
6
|
+
class ConsumerImpl
|
|
7
|
+
attr_reader :topic, :subscription, :consumer_id
|
|
8
|
+
|
|
9
|
+
def self.create(topic:, subscription:, consumer_id:, operation_timeout:, receiver_queue_size:,
|
|
10
|
+
connection: nil, connection_provider: nil)
|
|
11
|
+
connection_provider ||= -> { connection }
|
|
12
|
+
new(
|
|
13
|
+
connection_provider: connection_provider,
|
|
14
|
+
topic: topic,
|
|
15
|
+
subscription: subscription,
|
|
16
|
+
consumer_id: consumer_id,
|
|
17
|
+
operation_timeout: operation_timeout,
|
|
18
|
+
receiver_queue_size: receiver_queue_size
|
|
19
|
+
).tap(&:attach)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(connection_provider:, topic:, subscription:, consumer_id:, operation_timeout:, receiver_queue_size:)
|
|
23
|
+
@connection_provider = connection_provider
|
|
24
|
+
@connection = nil
|
|
25
|
+
@topic = topic
|
|
26
|
+
@subscription = subscription
|
|
27
|
+
@consumer_id = consumer_id
|
|
28
|
+
@operation_timeout = operation_timeout
|
|
29
|
+
@receiver_queue_size = receiver_queue_size
|
|
30
|
+
@receiver_queue = BoundedQueue.new(capacity: receiver_queue_size)
|
|
31
|
+
@closed = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handle_message(command_message, headers_and_payload)
|
|
35
|
+
raise ClosedError, 'consumer is closed' if closed?
|
|
36
|
+
|
|
37
|
+
decoded = FrameCodec.decode_message_data(headers_and_payload)
|
|
38
|
+
@receiver_queue.push(
|
|
39
|
+
Message.new(
|
|
40
|
+
payload: decoded.payload,
|
|
41
|
+
message_id: message_id_from(command_message.message_id),
|
|
42
|
+
properties: decoded.metadata.properties.to_h { |property| [property.key, property.value] },
|
|
43
|
+
key: decoded.metadata.partition_key,
|
|
44
|
+
publish_time: decoded.metadata.publish_time,
|
|
45
|
+
event_time: decoded.metadata.event_time
|
|
46
|
+
),
|
|
47
|
+
timeout: @operation_timeout
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def receive(timeout: nil)
|
|
52
|
+
raise ClosedError, 'consumer is closed' if closed?
|
|
53
|
+
|
|
54
|
+
attach unless attached?
|
|
55
|
+
@receiver_queue.pop(timeout: timeout || @operation_timeout).tap do
|
|
56
|
+
flow(1)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ack(message_or_message_id)
|
|
61
|
+
raise ClosedError, 'consumer is closed' if closed?
|
|
62
|
+
|
|
63
|
+
attach unless attached?
|
|
64
|
+
message_id = message_or_message_id.respond_to?(:message_id) ? message_or_message_id.message_id : message_or_message_id
|
|
65
|
+
@connection.write_command(CommandFactory.ack(consumer_id: consumer_id, message_id: message_id))
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def close
|
|
70
|
+
return nil if closed?
|
|
71
|
+
|
|
72
|
+
if attached?
|
|
73
|
+
request_id = @connection.next_request_id
|
|
74
|
+
command = CommandFactory.close_consumer(consumer_id: consumer_id, request_id: request_id)
|
|
75
|
+
response = @connection.request(command, timeout: @operation_timeout)
|
|
76
|
+
raise BrokerError, "consumer close failed: #{response.type}" unless response.type == :SUCCESS
|
|
77
|
+
|
|
78
|
+
@connection.unregister_consumer(consumer_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@receiver_queue.close
|
|
82
|
+
@closed = true
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def closed?
|
|
87
|
+
@closed
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def flow(permits)
|
|
91
|
+
raise ClosedError, 'consumer is closed' if closed?
|
|
92
|
+
|
|
93
|
+
attach unless attached?
|
|
94
|
+
@connection.write_command(CommandFactory.flow(consumer_id: consumer_id, permits: permits))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def attach
|
|
100
|
+
@connection = @connection_provider.call
|
|
101
|
+
request_id = @connection.next_request_id
|
|
102
|
+
command = CommandFactory.subscribe(
|
|
103
|
+
topic: topic,
|
|
104
|
+
subscription: subscription,
|
|
105
|
+
consumer_id: consumer_id,
|
|
106
|
+
request_id: request_id
|
|
107
|
+
)
|
|
108
|
+
response = @connection.request(command, timeout: @operation_timeout)
|
|
109
|
+
raise BrokerError, "subscribe failed: #{response.type}" unless response.type == :SUCCESS
|
|
110
|
+
|
|
111
|
+
@connection.register_consumer(consumer_id, self)
|
|
112
|
+
@connection.write_command(CommandFactory.flow(consumer_id: consumer_id, permits: @receiver_queue_size))
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def attached?
|
|
117
|
+
@connection&.connected?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def message_id_from(data)
|
|
121
|
+
MessageId.new(
|
|
122
|
+
ledger_id: data.ledgerId,
|
|
123
|
+
entry_id: data.entryId,
|
|
124
|
+
partition_index: data.partition,
|
|
125
|
+
batch_index: data.batch_index
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Encodes and decodes Pulsar binary protocol frames.
|
|
6
|
+
class FrameCodec
|
|
7
|
+
DecodedFrame = Struct.new(:command, :headers_and_payload, keyword_init: true)
|
|
8
|
+
DecodedMessageData = Struct.new(:metadata, :payload, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def self.encode_command(command)
|
|
11
|
+
encoded_command = Proto::BaseCommand.encode(command)
|
|
12
|
+
[4 + encoded_command.bytesize, encoded_command.bytesize].pack('NN') + encoded_command
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.encode_message(command, metadata, payload)
|
|
16
|
+
encoded_command = Proto::BaseCommand.encode(command)
|
|
17
|
+
encoded_metadata = Proto::MessageMetadata.encode(metadata)
|
|
18
|
+
payload = String(payload).b
|
|
19
|
+
total_size = 4 + encoded_command.bytesize + 4 + encoded_metadata.bytesize + payload.bytesize
|
|
20
|
+
|
|
21
|
+
[total_size, encoded_command.bytesize].pack('NN') +
|
|
22
|
+
encoded_command + [encoded_metadata.bytesize].pack('N') + encoded_metadata + payload
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.decode_frame(frame)
|
|
26
|
+
frame = String(frame).b
|
|
27
|
+
raise ProtocolError, 'frame size prefix is incomplete' if frame.bytesize < 4
|
|
28
|
+
|
|
29
|
+
total_size = frame.byteslice(0, 4).unpack1('N')
|
|
30
|
+
raise ProtocolError, 'frame is incomplete' if frame.bytesize < 4 + total_size
|
|
31
|
+
raise ProtocolError, 'command size prefix is incomplete' if total_size < 4
|
|
32
|
+
|
|
33
|
+
command_size = frame.byteslice(4, 4).unpack1('N')
|
|
34
|
+
raise ProtocolError, 'command exceeds frame size' if command_size > total_size - 4
|
|
35
|
+
|
|
36
|
+
command_bytes = frame.byteslice(8, command_size)
|
|
37
|
+
headers_and_payload = frame.byteslice(8 + command_size, total_size - 4 - command_size) || +''
|
|
38
|
+
|
|
39
|
+
DecodedFrame.new(
|
|
40
|
+
command: Proto::BaseCommand.decode(command_bytes),
|
|
41
|
+
headers_and_payload: headers_and_payload.b
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.decode_message_data(headers_and_payload)
|
|
46
|
+
headers_and_payload = String(headers_and_payload).b
|
|
47
|
+
raise ProtocolError, 'metadata size prefix is incomplete' if headers_and_payload.bytesize < 4
|
|
48
|
+
|
|
49
|
+
metadata_size = headers_and_payload.byteslice(0, 4).unpack1('N')
|
|
50
|
+
raise ProtocolError, 'metadata exceeds message data size' if metadata_size > headers_and_payload.bytesize - 4
|
|
51
|
+
|
|
52
|
+
metadata_bytes = headers_and_payload.byteslice(4, metadata_size)
|
|
53
|
+
payload = headers_and_payload.byteslice(4 + metadata_size, headers_and_payload.bytesize - 4 - metadata_size) || +''
|
|
54
|
+
|
|
55
|
+
DecodedMessageData.new(
|
|
56
|
+
metadata: Proto::MessageMetadata.decode(metadata_bytes),
|
|
57
|
+
payload: payload.b
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pulsar
|
|
4
|
+
module Internal
|
|
5
|
+
# Resolves the broker service URL that owns a topic.
|
|
6
|
+
class LookupService
|
|
7
|
+
def initialize(connection:, operation_timeout:)
|
|
8
|
+
@connection = connection
|
|
9
|
+
@operation_timeout = operation_timeout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def lookup(topic)
|
|
13
|
+
request_id = @connection.next_request_id
|
|
14
|
+
response = @connection.request(
|
|
15
|
+
CommandFactory.lookup(topic: topic, request_id: request_id),
|
|
16
|
+
timeout: @operation_timeout
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
raise BrokerError, "lookup failed: #{response.type}" unless response.type == :LOOKUP_RESPONSE
|
|
20
|
+
|
|
21
|
+
lookup = response.lookupTopicResponse
|
|
22
|
+
raise BrokerError, "lookup failed: #{lookup.message}" unless lookup.response == :Connect
|
|
23
|
+
|
|
24
|
+
lookup.brokerServiceUrl
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|