nats-pure 2.2.0 → 2.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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +251 -0
- data/lib/nats/io/client.rb +226 -151
- data/lib/nats/io/errors.rb +6 -0
- data/lib/nats/io/jetstream/api.rb +305 -0
- data/lib/nats/io/jetstream/errors.rb +104 -0
- data/lib/nats/io/jetstream/js/config.rb +26 -0
- data/lib/nats/io/jetstream/js/header.rb +31 -0
- data/lib/nats/io/jetstream/js/status.rb +27 -0
- data/lib/nats/io/jetstream/js/sub.rb +30 -0
- data/lib/nats/io/jetstream/js.rb +93 -0
- data/lib/nats/io/jetstream/manager.rb +284 -0
- data/lib/nats/io/jetstream/msg/ack.rb +57 -0
- data/lib/nats/io/jetstream/msg/ack_methods.rb +111 -0
- data/lib/nats/io/jetstream/msg/metadata.rb +37 -0
- data/lib/nats/io/jetstream/msg.rb +26 -0
- data/lib/nats/io/jetstream/pull_subscription.rb +260 -0
- data/lib/nats/io/jetstream/push_subscription.rb +42 -0
- data/lib/nats/io/jetstream.rb +269 -0
- data/lib/nats/io/kv/api.rb +39 -0
- data/lib/nats/io/kv/bucket_status.rb +38 -0
- data/lib/nats/io/kv/errors.rb +60 -0
- data/lib/nats/io/kv/manager.rb +89 -0
- data/lib/nats/io/kv.rb +5 -157
- data/lib/nats/io/msg.rb +4 -2
- data/lib/nats/io/rails.rb +29 -0
- data/lib/nats/io/subscription.rb +70 -5
- data/lib/nats/io/version.rb +1 -1
- data/lib/nats/io/websocket.rb +75 -0
- data/sig/nats/io/client.rbs +304 -0
- data/sig/nats/io/errors.rbs +54 -0
- data/sig/nats/io/jetstream/api.rbs +35 -0
- data/sig/nats/io/jetstream/errors.rbs +54 -0
- data/sig/nats/io/jetstream/js/config.rbs +11 -0
- data/sig/nats/io/jetstream/js/header.rbs +17 -0
- data/sig/nats/io/jetstream/js/status.rbs +13 -0
- data/sig/nats/io/jetstream/js/sub.rbs +14 -0
- data/sig/nats/io/jetstream/js.rbs +27 -0
- data/sig/nats/io/jetstream/manager.rbs +33 -0
- data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
- data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
- data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
- data/sig/nats/io/jetstream/msg.rbs +6 -0
- data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
- data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
- data/sig/nats/io/jetstream.rbs +15 -0
- data/sig/nats/io/kv/api.rbs +8 -0
- data/sig/nats/io/kv/bucket_status.rbs +17 -0
- data/sig/nats/io/kv/errors.rbs +30 -0
- data/sig/nats/io/kv/manager.rbs +11 -0
- data/sig/nats/io/kv.rbs +39 -0
- data/sig/nats/io/msg.rbs +14 -0
- data/sig/nats/io/parser.rbs +32 -0
- data/sig/nats/io/subscription.rbs +33 -0
- data/sig/nats/io/version.rbs +9 -0
- data/sig/nats/nuid.rbs +32 -0
- metadata +68 -5
- data/lib/nats/io/js.rb +0 -1434
data/lib/nats/io/js.rb
DELETED
@@ -1,1434 +0,0 @@
|
|
1
|
-
# Copyright 2021 The NATS Authors
|
2
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
3
|
-
# you may not use this file except in compliance with the License.
|
4
|
-
# You may obtain a copy of the License at
|
5
|
-
#
|
6
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
7
|
-
#
|
8
|
-
# Unless required by applicable law or agreed to in writing, software
|
9
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
10
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
11
|
-
# See the License for the specific language governing permissions and
|
12
|
-
# limitations under the License.
|
13
|
-
#
|
14
|
-
require_relative 'msg'
|
15
|
-
require_relative 'client'
|
16
|
-
require_relative 'errors'
|
17
|
-
require_relative 'kv'
|
18
|
-
require 'time'
|
19
|
-
require 'base64'
|
20
|
-
|
21
|
-
module NATS
|
22
|
-
# JetStream returns a context with a similar API as the NATS::Client
|
23
|
-
# but with enhanced functions to persist and consume messages from
|
24
|
-
# the NATS JetStream engine.
|
25
|
-
#
|
26
|
-
# @example
|
27
|
-
# nc = NATS.connect("demo.nats.io")
|
28
|
-
# js = nc.jetstream()
|
29
|
-
#
|
30
|
-
class JetStream
|
31
|
-
# Create a new JetStream context for a NATS connection.
|
32
|
-
#
|
33
|
-
# @param conn [NATS::Client]
|
34
|
-
# @param params [Hash] Options to customize JetStream context.
|
35
|
-
# @option params [String] :prefix JetStream API prefix to use for the requests.
|
36
|
-
# @option params [String] :domain JetStream Domain to use for the requests.
|
37
|
-
# @option params [Float] :timeout Default timeout to use for JS requests.
|
38
|
-
def initialize(conn, params={})
|
39
|
-
@nc = conn
|
40
|
-
@prefix = if params[:prefix]
|
41
|
-
params[:prefix]
|
42
|
-
elsif params[:domain]
|
43
|
-
"$JS.#{params[:domain]}.API"
|
44
|
-
else
|
45
|
-
JS::DefaultAPIPrefix
|
46
|
-
end
|
47
|
-
@opts = params
|
48
|
-
@opts[:timeout] ||= 5 # seconds
|
49
|
-
params[:prefix] = @prefix
|
50
|
-
|
51
|
-
# Include JetStream::Manager
|
52
|
-
extend Manager
|
53
|
-
extend KeyValue::Manager
|
54
|
-
end
|
55
|
-
|
56
|
-
# PubAck is the API response from a successfully published message.
|
57
|
-
#
|
58
|
-
# @!attribute [stream] stream
|
59
|
-
# @return [String] Name of the stream that processed the published message.
|
60
|
-
# @!attribute [seq] seq
|
61
|
-
# @return [Fixnum] Sequence of the message in the stream.
|
62
|
-
# @!attribute [duplicate] duplicate
|
63
|
-
# @return [Boolean] Indicates whether the published message is a duplicate.
|
64
|
-
# @!attribute [domain] domain
|
65
|
-
# @return [String] JetStream Domain that processed the ack response.
|
66
|
-
PubAck = Struct.new(:stream, :seq, :duplicate, :domain, keyword_init: true)
|
67
|
-
|
68
|
-
# publish produces a message for JetStream.
|
69
|
-
#
|
70
|
-
# @param subject [String] The subject from a stream where the message will be sent.
|
71
|
-
# @param payload [String] The payload of the message.
|
72
|
-
# @param params [Hash] Options to customize the publish message request.
|
73
|
-
# @option params [Float] :timeout Time to wait for an PubAck response or an error.
|
74
|
-
# @option params [Hash] :header NATS Headers to use for the message.
|
75
|
-
# @option params [String] :stream Expected Stream to which the message is being published.
|
76
|
-
# @raise [NATS::Timeout] When it takes too long to receive an ack response.
|
77
|
-
# @return [PubAck] The pub ack response.
|
78
|
-
def publish(subject, payload="", **params)
|
79
|
-
params[:timeout] ||= @opts[:timeout]
|
80
|
-
if params[:stream]
|
81
|
-
params[:header] ||= {}
|
82
|
-
params[:header][JS::Header::ExpectedStream] = params[:stream]
|
83
|
-
end
|
84
|
-
|
85
|
-
# Send message with headers.
|
86
|
-
msg = NATS::Msg.new(subject: subject,
|
87
|
-
data: payload,
|
88
|
-
header: params[:header])
|
89
|
-
|
90
|
-
begin
|
91
|
-
resp = @nc.request_msg(msg, **params)
|
92
|
-
result = JSON.parse(resp.data, symbolize_names: true)
|
93
|
-
rescue ::NATS::IO::NoRespondersError
|
94
|
-
raise JetStream::Error::NoStreamResponse.new("nats: no response from stream")
|
95
|
-
end
|
96
|
-
raise JS.from_error(result[:error]) if result[:error]
|
97
|
-
|
98
|
-
PubAck.new(result)
|
99
|
-
end
|
100
|
-
|
101
|
-
# subscribe binds or creates a push subscription to a JetStream pull consumer.
|
102
|
-
#
|
103
|
-
# @param subject [String] Subject from which the messages will be fetched.
|
104
|
-
# @param params [Hash] Options to customize the PushSubscription.
|
105
|
-
# @option params [String] :stream Name of the Stream to which the consumer belongs.
|
106
|
-
# @option params [String] :consumer Name of the Consumer to which the PushSubscription will be bound.
|
107
|
-
# @option params [String] :durable Consumer durable name from where the messages will be fetched.
|
108
|
-
# @option params [Hash] :config Configuration for the consumer.
|
109
|
-
# @return [NATS::JetStream::PushSubscription]
|
110
|
-
def subscribe(subject, params={}, &cb)
|
111
|
-
params[:consumer] ||= params[:durable]
|
112
|
-
stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
|
113
|
-
|
114
|
-
queue = params[:queue]
|
115
|
-
durable = params[:durable]
|
116
|
-
flow_control = params[:flow_control]
|
117
|
-
manual_ack = params[:manual_ack]
|
118
|
-
idle_heartbeat = params[:idle_heartbeat]
|
119
|
-
flow_control = params[:flow_control]
|
120
|
-
config = params[:config]
|
121
|
-
|
122
|
-
if queue
|
123
|
-
if durable and durable != queue
|
124
|
-
raise NATS::JetStream::Error.new("nats: cannot create queue subscription '#{queue}' to consumer '#{durable}'")
|
125
|
-
else
|
126
|
-
durable = queue
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
cinfo = nil
|
131
|
-
consumer_found = false
|
132
|
-
should_create = false
|
133
|
-
|
134
|
-
if not durable
|
135
|
-
should_create = true
|
136
|
-
else
|
137
|
-
begin
|
138
|
-
cinfo = consumer_info(stream, durable)
|
139
|
-
config = cinfo.config
|
140
|
-
consumer_found = true
|
141
|
-
consumer = durable
|
142
|
-
rescue NATS::JetStream::Error::NotFound
|
143
|
-
should_create = true
|
144
|
-
consumer_found = false
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
if consumer_found
|
149
|
-
if not config.deliver_group
|
150
|
-
if queue
|
151
|
-
raise NATS::JetStream::Error.new("nats: cannot create a queue subscription for a consumer without a deliver group")
|
152
|
-
elsif cinfo.push_bound
|
153
|
-
raise NATS::JetStream::Error.new("nats: consumer is already bound to a subscription")
|
154
|
-
end
|
155
|
-
else
|
156
|
-
if not queue
|
157
|
-
raise NATS::JetStream::Error.new("nats: cannot create a subscription for a consumer with a deliver group #{config.deliver_group}")
|
158
|
-
elsif queue != config.deliver_group
|
159
|
-
raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
|
160
|
-
end
|
161
|
-
end
|
162
|
-
elsif should_create
|
163
|
-
# Auto-create consumer if none found.
|
164
|
-
if config.nil?
|
165
|
-
# Defaults
|
166
|
-
config = JetStream::API::ConsumerConfig.new({ack_policy: "explicit"})
|
167
|
-
elsif config.is_a?(Hash)
|
168
|
-
config = JetStream::API::ConsumerConfig.new(config)
|
169
|
-
elsif !config.is_a?(JetStream::API::ConsumerConfig)
|
170
|
-
raise NATS::JetStream::Error.new("nats: invalid ConsumerConfig")
|
171
|
-
end
|
172
|
-
|
173
|
-
config.durable_name = durable if not config.durable_name
|
174
|
-
config.deliver_group = queue if not config.deliver_group
|
175
|
-
|
176
|
-
# Create inbox for push consumer.
|
177
|
-
deliver = @nc.new_inbox
|
178
|
-
config.deliver_subject = deliver
|
179
|
-
|
180
|
-
# Auto created consumers use the filter subject.
|
181
|
-
config.filter_subject = subject
|
182
|
-
|
183
|
-
# Heartbeats / FlowControl
|
184
|
-
config.flow_control = flow_control
|
185
|
-
if idle_heartbeat or config.idle_heartbeat
|
186
|
-
idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
|
187
|
-
idle_heartbeat = idle_heartbeat * ::NATS::NANOSECONDS
|
188
|
-
config.idle_heartbeat = idle_heartbeat
|
189
|
-
end
|
190
|
-
|
191
|
-
# Auto create the consumer.
|
192
|
-
cinfo = add_consumer(stream, config)
|
193
|
-
consumer = cinfo.name
|
194
|
-
end
|
195
|
-
|
196
|
-
# Enable auto acking for async callbacks unless disabled.
|
197
|
-
if cb and not manual_ack
|
198
|
-
ocb = cb
|
199
|
-
new_cb = proc do |msg|
|
200
|
-
ocb.call(msg)
|
201
|
-
msg.ack rescue JetStream::Error::MsgAlreadyAckd
|
202
|
-
end
|
203
|
-
cb = new_cb
|
204
|
-
end
|
205
|
-
sub = @nc.subscribe(config.deliver_subject, queue: config.deliver_group, &cb)
|
206
|
-
sub.extend(PushSubscription)
|
207
|
-
sub.jsi = JS::Sub.new(
|
208
|
-
js: self,
|
209
|
-
stream: stream,
|
210
|
-
consumer: consumer,
|
211
|
-
)
|
212
|
-
sub
|
213
|
-
end
|
214
|
-
|
215
|
-
# pull_subscribe binds or creates a subscription to a JetStream pull consumer.
|
216
|
-
#
|
217
|
-
# @param subject [String] Subject from which the messages will be fetched.
|
218
|
-
# @param durable [String] Consumer durable name from where the messages will be fetched.
|
219
|
-
# @param params [Hash] Options to customize the PullSubscription.
|
220
|
-
# @option params [String] :stream Name of the Stream to which the consumer belongs.
|
221
|
-
# @option params [String] :consumer Name of the Consumer to which the PullSubscription will be bound.
|
222
|
-
# @option params [Hash] :config Configuration for the consumer.
|
223
|
-
# @return [NATS::JetStream::PullSubscription]
|
224
|
-
def pull_subscribe(subject, durable, params={})
|
225
|
-
if durable.empty? && !params[:consumer]
|
226
|
-
raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name")
|
227
|
-
end
|
228
|
-
params[:consumer] ||= durable
|
229
|
-
stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
|
230
|
-
|
231
|
-
begin
|
232
|
-
consumer_info(stream, params[:consumer])
|
233
|
-
rescue NATS::JetStream::Error::NotFound => e
|
234
|
-
# If attempting to bind, then this is a hard error.
|
235
|
-
raise e if params[:stream]
|
236
|
-
|
237
|
-
config = if not params[:config]
|
238
|
-
JetStream::API::ConsumerConfig.new
|
239
|
-
elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
|
240
|
-
params[:config]
|
241
|
-
else
|
242
|
-
JetStream::API::ConsumerConfig.new(params[:config])
|
243
|
-
end
|
244
|
-
config[:durable_name] = durable
|
245
|
-
config[:ack_policy] ||= JS::Config::AckExplicit
|
246
|
-
add_consumer(stream, config)
|
247
|
-
end
|
248
|
-
|
249
|
-
deliver = @nc.new_inbox
|
250
|
-
sub = @nc.subscribe(deliver)
|
251
|
-
sub.extend(PullSubscription)
|
252
|
-
|
253
|
-
consumer = params[:consumer]
|
254
|
-
subject = "#{@prefix}.CONSUMER.MSG.NEXT.#{stream}.#{consumer}"
|
255
|
-
sub.jsi = JS::Sub.new(
|
256
|
-
js: self,
|
257
|
-
stream: stream,
|
258
|
-
consumer: params[:consumer],
|
259
|
-
nms: subject
|
260
|
-
)
|
261
|
-
sub
|
262
|
-
end
|
263
|
-
|
264
|
-
# A JetStream::Manager can be used to make requests to the JetStream API.
|
265
|
-
#
|
266
|
-
# @example
|
267
|
-
# require 'nats/client'
|
268
|
-
#
|
269
|
-
# nc = NATS.connect("demo.nats.io")
|
270
|
-
#
|
271
|
-
# config = JetStream::API::StreamConfig.new()
|
272
|
-
# nc.jsm.add_stream(config)
|
273
|
-
#
|
274
|
-
#
|
275
|
-
module Manager
|
276
|
-
# add_stream creates a stream with a given config.
|
277
|
-
# @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
|
278
|
-
# @param params [Hash] Options to customize API request.
|
279
|
-
# @option params [Float] :timeout Time to wait for response.
|
280
|
-
# @return [JetStream::API::StreamCreateResponse] The result of creating a Stream.
|
281
|
-
def add_stream(config, params={})
|
282
|
-
config = if not config.is_a?(JetStream::API::StreamConfig)
|
283
|
-
JetStream::API::StreamConfig.new(config)
|
284
|
-
else
|
285
|
-
config
|
286
|
-
end
|
287
|
-
stream = config[:name]
|
288
|
-
raise ArgumentError.new(":name is required to create streams") unless stream
|
289
|
-
raise ArgumentError.new("Spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited in stream names") if stream =~ /(\s|\.|\>|\*)/
|
290
|
-
req_subject = "#{@prefix}.STREAM.CREATE.#{stream}"
|
291
|
-
result = api_request(req_subject, config.to_json, params)
|
292
|
-
JetStream::API::StreamCreateResponse.new(result)
|
293
|
-
end
|
294
|
-
|
295
|
-
# stream_info retrieves the current status of a stream.
|
296
|
-
# @param stream [String] Name of the stream.
|
297
|
-
# @param params [Hash] Options to customize API request.
|
298
|
-
# @option params [Float] :timeout Time to wait for response.
|
299
|
-
# @return [JetStream::API::StreamInfo] The latest StreamInfo of the stream.
|
300
|
-
def stream_info(stream, params={})
|
301
|
-
raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
|
302
|
-
|
303
|
-
req_subject = "#{@prefix}.STREAM.INFO.#{stream}"
|
304
|
-
result = api_request(req_subject, '', params)
|
305
|
-
JetStream::API::StreamInfo.new(result)
|
306
|
-
end
|
307
|
-
|
308
|
-
# delete_stream deletes a stream.
|
309
|
-
# @param stream [String] Name of the stream.
|
310
|
-
# @param params [Hash] Options to customize API request.
|
311
|
-
# @option params [Float] :timeout Time to wait for response.
|
312
|
-
# @return [Boolean]
|
313
|
-
def delete_stream(stream, params={})
|
314
|
-
raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
|
315
|
-
|
316
|
-
req_subject = "#{@prefix}.STREAM.DELETE.#{stream}"
|
317
|
-
result = api_request(req_subject, '', params)
|
318
|
-
result[:success]
|
319
|
-
end
|
320
|
-
|
321
|
-
# add_consumer creates a consumer with a given config.
|
322
|
-
# @param stream [String] Name of the stream.
|
323
|
-
# @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
|
324
|
-
# @param params [Hash] Options to customize API request.
|
325
|
-
# @option params [Float] :timeout Time to wait for response.
|
326
|
-
# @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
|
327
|
-
def add_consumer(stream, config, params={})
|
328
|
-
raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
|
329
|
-
config = if not config.is_a?(JetStream::API::ConsumerConfig)
|
330
|
-
JetStream::API::ConsumerConfig.new(config)
|
331
|
-
else
|
332
|
-
config
|
333
|
-
end
|
334
|
-
|
335
|
-
req_subject = case
|
336
|
-
when config[:name]
|
337
|
-
# NOTE: Only supported after nats-server v2.9.0
|
338
|
-
if config[:filter_subject] && config[:filter_subject] != ">"
|
339
|
-
"#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
|
340
|
-
else
|
341
|
-
"#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
|
342
|
-
end
|
343
|
-
when config[:durable_name]
|
344
|
-
"#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
|
345
|
-
else
|
346
|
-
"#{@prefix}.CONSUMER.CREATE.#{stream}"
|
347
|
-
end
|
348
|
-
|
349
|
-
config[:ack_policy] ||= JS::Config::AckExplicit
|
350
|
-
# Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
|
351
|
-
if config[:ack_wait]
|
352
|
-
raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
|
353
|
-
config[:ack_wait] = config[:ack_wait] * ::NATS::NANOSECONDS
|
354
|
-
end
|
355
|
-
if config[:inactive_threshold]
|
356
|
-
raise ArgumentError.new("nats: invalid inactive threshold") unless config[:inactive_threshold].is_a?(Integer)
|
357
|
-
config[:inactive_threshold] = config[:inactive_threshold] * ::NATS::NANOSECONDS
|
358
|
-
end
|
359
|
-
|
360
|
-
req = {
|
361
|
-
stream_name: stream,
|
362
|
-
config: config
|
363
|
-
}
|
364
|
-
|
365
|
-
result = api_request(req_subject, req.to_json, params)
|
366
|
-
JetStream::API::ConsumerInfo.new(result).freeze
|
367
|
-
end
|
368
|
-
|
369
|
-
# consumer_info retrieves the current status of a consumer.
|
370
|
-
# @param stream [String] Name of the stream.
|
371
|
-
# @param consumer [String] Name of the consumer.
|
372
|
-
# @param params [Hash] Options to customize API request.
|
373
|
-
# @option params [Float] :timeout Time to wait for response.
|
374
|
-
# @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
|
375
|
-
def consumer_info(stream, consumer, params={})
|
376
|
-
raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
|
377
|
-
raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
|
378
|
-
|
379
|
-
req_subject = "#{@prefix}.CONSUMER.INFO.#{stream}.#{consumer}"
|
380
|
-
result = api_request(req_subject, '', params)
|
381
|
-
JetStream::API::ConsumerInfo.new(result)
|
382
|
-
end
|
383
|
-
|
384
|
-
# delete_consumer deletes a consumer.
|
385
|
-
# @param stream [String] Name of the stream.
|
386
|
-
# @param consumer [String] Name of the consumer.
|
387
|
-
# @param params [Hash] Options to customize API request.
|
388
|
-
# @option params [Float] :timeout Time to wait for response.
|
389
|
-
# @return [Boolean]
|
390
|
-
def delete_consumer(stream, consumer, params={})
|
391
|
-
raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
|
392
|
-
raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
|
393
|
-
|
394
|
-
req_subject = "#{@prefix}.CONSUMER.DELETE.#{stream}.#{consumer}"
|
395
|
-
result = api_request(req_subject, '', params)
|
396
|
-
result[:success]
|
397
|
-
end
|
398
|
-
|
399
|
-
# find_stream_name_by_subject does a lookup for the stream to which
|
400
|
-
# the subject belongs.
|
401
|
-
# @param subject [String] The subject that belongs to a stream.
|
402
|
-
# @param params [Hash] Options to customize API request.
|
403
|
-
# @option params [Float] :timeout Time to wait for response.
|
404
|
-
# @return [String] The name of the JetStream stream for the subject.
|
405
|
-
def find_stream_name_by_subject(subject, params={})
|
406
|
-
req_subject = "#{@prefix}.STREAM.NAMES"
|
407
|
-
req = { subject: subject }
|
408
|
-
result = api_request(req_subject, req.to_json, params)
|
409
|
-
raise JetStream::Error::NotFound unless result[:streams]
|
410
|
-
|
411
|
-
result[:streams].first
|
412
|
-
end
|
413
|
-
|
414
|
-
# get_msg retrieves a message from the stream.
|
415
|
-
# @param next [Boolean] Fetch the next message for a subject.
|
416
|
-
# @param seq [Integer] Sequence number of a message.
|
417
|
-
# @param subject [String] Subject of the message.
|
418
|
-
# @param direct [Boolean] Use direct mode to for faster access (requires NATS v2.9.0)
|
419
|
-
def get_msg(stream_name, params={})
|
420
|
-
req = {}
|
421
|
-
case
|
422
|
-
when params[:next]
|
423
|
-
req[:seq] = params[:seq]
|
424
|
-
req[:next_by_subj] = params[:subject]
|
425
|
-
when params[:seq]
|
426
|
-
req[:seq] = params[:seq]
|
427
|
-
when params[:subject]
|
428
|
-
req[:last_by_subj] = params[:subject]
|
429
|
-
end
|
430
|
-
|
431
|
-
data = req.to_json
|
432
|
-
if params[:direct]
|
433
|
-
if params[:subject] and not params[:seq]
|
434
|
-
# last_by_subject type request requires no payload.
|
435
|
-
data = ''
|
436
|
-
req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}.#{params[:subject]}"
|
437
|
-
else
|
438
|
-
req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}"
|
439
|
-
end
|
440
|
-
else
|
441
|
-
req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
|
442
|
-
end
|
443
|
-
resp = api_request(req_subject, data, direct: params[:direct])
|
444
|
-
msg = if params[:direct]
|
445
|
-
_lift_msg_to_raw_msg(resp)
|
446
|
-
else
|
447
|
-
JetStream::API::RawStreamMsg.new(resp[:message])
|
448
|
-
end
|
449
|
-
|
450
|
-
msg
|
451
|
-
end
|
452
|
-
|
453
|
-
def get_last_msg(stream_name, subject, params={})
|
454
|
-
params[:subject] = subject
|
455
|
-
get_msg(stream_name, params)
|
456
|
-
end
|
457
|
-
|
458
|
-
def account_info
|
459
|
-
api_request("#{@prefix}.INFO")
|
460
|
-
end
|
461
|
-
|
462
|
-
private
|
463
|
-
|
464
|
-
def api_request(req_subject, req="", params={})
|
465
|
-
params[:timeout] ||= @opts[:timeout]
|
466
|
-
msg = begin
|
467
|
-
@nc.request(req_subject, req, **params)
|
468
|
-
rescue NATS::IO::NoRespondersError
|
469
|
-
raise JetStream::Error::ServiceUnavailable
|
470
|
-
end
|
471
|
-
|
472
|
-
result = if params[:direct]
|
473
|
-
msg
|
474
|
-
else
|
475
|
-
JSON.parse(msg.data, symbolize_names: true)
|
476
|
-
end
|
477
|
-
if result.is_a?(Hash) and result[:error]
|
478
|
-
raise JS.from_error(result[:error])
|
479
|
-
end
|
480
|
-
|
481
|
-
result
|
482
|
-
end
|
483
|
-
|
484
|
-
def _lift_msg_to_raw_msg(msg)
|
485
|
-
if msg.header and msg.header['Status']
|
486
|
-
status = msg.header['Status']
|
487
|
-
if status == '404'
|
488
|
-
raise ::NATS::JetStream::Error::NotFound.new
|
489
|
-
else
|
490
|
-
raise JS.from_msg(msg)
|
491
|
-
end
|
492
|
-
end
|
493
|
-
subject = msg.header['Nats-Subject']
|
494
|
-
seq = msg.header['Nats-Sequence']
|
495
|
-
raw_msg = JetStream::API::RawStreamMsg.new(
|
496
|
-
subject: subject,
|
497
|
-
seq: seq,
|
498
|
-
headers: msg.header,
|
499
|
-
)
|
500
|
-
raw_msg.data = msg.data
|
501
|
-
|
502
|
-
raw_msg
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
# PushSubscription is included into NATS::Subscription so that it
|
507
|
-
#
|
508
|
-
# @example Create a push subscription using JetStream context.
|
509
|
-
#
|
510
|
-
# require 'nats/client'
|
511
|
-
#
|
512
|
-
# nc = NATS.connect
|
513
|
-
# js = nc.jetstream
|
514
|
-
# sub = js.subscribe("foo", "bar")
|
515
|
-
# msg = sub.next_msg
|
516
|
-
# msg.ack
|
517
|
-
# sub.unsubscribe
|
518
|
-
#
|
519
|
-
# @!visibility public
|
520
|
-
module PushSubscription
|
521
|
-
# consumer_info retrieves the current status of the pull subscription consumer.
|
522
|
-
# @param params [Hash] Options to customize API request.
|
523
|
-
# @option params [Float] :timeout Time to wait for response.
|
524
|
-
# @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
|
525
|
-
def consumer_info(params={})
|
526
|
-
@jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
|
527
|
-
end
|
528
|
-
end
|
529
|
-
private_constant :PushSubscription
|
530
|
-
|
531
|
-
# PullSubscription is included into NATS::Subscription so that it
|
532
|
-
# can be used to fetch messages from a pull based consumer from
|
533
|
-
# JetStream.
|
534
|
-
#
|
535
|
-
# @example Create a pull subscription using JetStream context.
|
536
|
-
#
|
537
|
-
# require 'nats/client'
|
538
|
-
#
|
539
|
-
# nc = NATS.connect
|
540
|
-
# js = nc.jetstream
|
541
|
-
# psub = js.pull_subscribe("foo", "bar")
|
542
|
-
#
|
543
|
-
# loop do
|
544
|
-
# msgs = psub.fetch(5)
|
545
|
-
# msgs.each do |msg|
|
546
|
-
# msg.ack
|
547
|
-
# end
|
548
|
-
# end
|
549
|
-
#
|
550
|
-
# @!visibility public
|
551
|
-
module PullSubscription
|
552
|
-
# next_msg is not available for pull based subscriptions.
|
553
|
-
# @raise [NATS::JetStream::Error]
|
554
|
-
def next_msg(params={})
|
555
|
-
raise ::NATS::JetStream::Error.new("nats: pull subscription cannot use next_msg")
|
556
|
-
end
|
557
|
-
|
558
|
-
# fetch makes a request to be delivered more messages from a pull consumer.
|
559
|
-
#
|
560
|
-
# @param batch [Fixnum] Number of messages to pull from the stream.
|
561
|
-
# @param params [Hash] Options to customize the fetch request.
|
562
|
-
# @option params [Float] :timeout Duration of the fetch request before it expires.
|
563
|
-
# @return [Array<NATS::Msg>]
|
564
|
-
def fetch(batch=1, params={})
|
565
|
-
if batch < 1
|
566
|
-
raise ::NATS::JetStream::Error.new("nats: invalid batch size")
|
567
|
-
end
|
568
|
-
|
569
|
-
t = MonotonicTime.now
|
570
|
-
timeout = params[:timeout] ||= 5
|
571
|
-
expires = (timeout * 1_000_000_000) - 100_000
|
572
|
-
next_req = {
|
573
|
-
batch: batch
|
574
|
-
}
|
575
|
-
|
576
|
-
msgs = []
|
577
|
-
case
|
578
|
-
when batch < 1
|
579
|
-
raise ::NATS::JetStream::Error.new("nats: invalid batch size")
|
580
|
-
when batch == 1
|
581
|
-
####################################################
|
582
|
-
# Fetch (1) #
|
583
|
-
####################################################
|
584
|
-
|
585
|
-
# Check if there is any pending message in the queue that is
|
586
|
-
# ready to be consumed.
|
587
|
-
synchronize do
|
588
|
-
unless @pending_queue.empty?
|
589
|
-
msg = @pending_queue.pop
|
590
|
-
@pending_size -= msg.data.size
|
591
|
-
# Check for a no msgs response status.
|
592
|
-
if JS.is_status_msg(msg)
|
593
|
-
case msg.header["Status"]
|
594
|
-
when JS::Status::NoMsgs
|
595
|
-
msg = nil
|
596
|
-
when JS::Status::RequestTimeout
|
597
|
-
# Skip
|
598
|
-
else
|
599
|
-
raise JS.from_msg(msg)
|
600
|
-
end
|
601
|
-
else
|
602
|
-
msgs << msg
|
603
|
-
end
|
604
|
-
end
|
605
|
-
end
|
606
|
-
|
607
|
-
# Make lingering request with expiration.
|
608
|
-
next_req[:expires] = expires
|
609
|
-
if msgs.empty?
|
610
|
-
# Make publish request and wait for response.
|
611
|
-
@nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
|
612
|
-
|
613
|
-
# Wait for result of fetch or timeout.
|
614
|
-
synchronize { wait_for_msgs_cond.wait(timeout) }
|
615
|
-
|
616
|
-
unless @pending_queue.empty?
|
617
|
-
msg = @pending_queue.pop
|
618
|
-
@pending_size -= msg.data.size
|
619
|
-
|
620
|
-
msgs << msg
|
621
|
-
end
|
622
|
-
|
623
|
-
duration = MonotonicTime.since(t)
|
624
|
-
if duration > timeout
|
625
|
-
raise ::NATS::Timeout.new("nats: fetch timeout")
|
626
|
-
end
|
627
|
-
|
628
|
-
# Should have received at least a message at this point,
|
629
|
-
# if that is not the case then error already.
|
630
|
-
if JS.is_status_msg(msgs.first)
|
631
|
-
msg = msgs.first
|
632
|
-
case msg.header[JS::Header::Status]
|
633
|
-
when JS::Status::RequestTimeout
|
634
|
-
raise NATS::Timeout.new("nats: fetch request timeout")
|
635
|
-
else
|
636
|
-
raise JS.from_msg(msgs.first)
|
637
|
-
end
|
638
|
-
end
|
639
|
-
end
|
640
|
-
when batch > 1
|
641
|
-
####################################################
|
642
|
-
# Fetch (n) #
|
643
|
-
####################################################
|
644
|
-
|
645
|
-
# Check if there already enough in the pending buffer.
|
646
|
-
synchronize do
|
647
|
-
if batch <= @pending_queue.size
|
648
|
-
batch.times do
|
649
|
-
msg = @pending_queue.pop
|
650
|
-
@pending_size -= msg.data.size
|
651
|
-
|
652
|
-
# Check for a no msgs response status.
|
653
|
-
if JS.is_status_msg(msg)
|
654
|
-
case msg.header[JS::Header::Status]
|
655
|
-
when JS::Status::NoMsgs, JS::Status::RequestTimeout
|
656
|
-
# Skip these
|
657
|
-
next
|
658
|
-
else
|
659
|
-
raise JS.from_msg(msg)
|
660
|
-
end
|
661
|
-
else
|
662
|
-
msgs << msg
|
663
|
-
end
|
664
|
-
end
|
665
|
-
|
666
|
-
return msgs
|
667
|
-
end
|
668
|
-
end
|
669
|
-
|
670
|
-
# Make publish request and wait any response.
|
671
|
-
next_req[:no_wait] = true
|
672
|
-
@nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
|
673
|
-
|
674
|
-
# Not receiving even one is a timeout.
|
675
|
-
start_time = MonotonicTime.now
|
676
|
-
msg = nil
|
677
|
-
|
678
|
-
synchronize do
|
679
|
-
wait_for_msgs_cond.wait(timeout)
|
680
|
-
|
681
|
-
unless @pending_queue.empty?
|
682
|
-
msg = @pending_queue.pop
|
683
|
-
@pending_size -= msg.data.size
|
684
|
-
end
|
685
|
-
end
|
686
|
-
|
687
|
-
# Check if the first message was a response saying that
|
688
|
-
# there are no messages.
|
689
|
-
if !msg.nil? && JS.is_status_msg(msg)
|
690
|
-
case msg.header[JS::Header::Status]
|
691
|
-
when JS::Status::NoMsgs
|
692
|
-
# Make another request that does wait.
|
693
|
-
next_req[:expires] = expires
|
694
|
-
next_req.delete(:no_wait)
|
695
|
-
|
696
|
-
@nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
|
697
|
-
when JS::Status::RequestTimeout
|
698
|
-
raise NATS::Timeout.new("nats: fetch request timeout")
|
699
|
-
else
|
700
|
-
raise JS.from_msg(msg)
|
701
|
-
end
|
702
|
-
else
|
703
|
-
msgs << msg unless msg.nil?
|
704
|
-
end
|
705
|
-
|
706
|
-
# Check if have not received yet a single message.
|
707
|
-
duration = MonotonicTime.since(start_time)
|
708
|
-
if msgs.empty? and duration > timeout
|
709
|
-
raise NATS::Timeout.new("nats: fetch timeout")
|
710
|
-
end
|
711
|
-
|
712
|
-
needed = batch - msgs.count
|
713
|
-
while needed > 0 and MonotonicTime.since(start_time) < timeout
|
714
|
-
duration = MonotonicTime.since(start_time)
|
715
|
-
|
716
|
-
# Wait for the rest of the messages.
|
717
|
-
synchronize do
|
718
|
-
|
719
|
-
# Wait until there is a message delivered.
|
720
|
-
if @pending_queue.empty?
|
721
|
-
deadline = timeout - duration
|
722
|
-
wait_for_msgs_cond.wait(deadline) if deadline > 0
|
723
|
-
|
724
|
-
duration = MonotonicTime.since(start_time)
|
725
|
-
if msgs.empty? && @pending_queue.empty? and duration > timeout
|
726
|
-
raise NATS::Timeout.new("nats: fetch timeout")
|
727
|
-
end
|
728
|
-
else
|
729
|
-
msg = @pending_queue.pop
|
730
|
-
@pending_size -= msg.data.size
|
731
|
-
|
732
|
-
if JS.is_status_msg(msg)
|
733
|
-
case msg.header[JS::Header::Status]
|
734
|
-
when JS::Status::NoMsgs, JS::Status::RequestTimeout
|
735
|
-
duration = MonotonicTime.since(start_time)
|
736
|
-
|
737
|
-
if duration > timeout
|
738
|
-
# Only received a subset of the messages.
|
739
|
-
if !msgs.empty?
|
740
|
-
return msgs
|
741
|
-
else
|
742
|
-
raise NATS::Timeout.new("nats: fetch timeout")
|
743
|
-
end
|
744
|
-
end
|
745
|
-
else
|
746
|
-
raise JS.from_msg(msg)
|
747
|
-
end
|
748
|
-
|
749
|
-
else
|
750
|
-
# Add to the set of messages that will be returned.
|
751
|
-
msgs << msg
|
752
|
-
needed -= 1
|
753
|
-
end
|
754
|
-
end
|
755
|
-
end # :end: synchronize
|
756
|
-
end
|
757
|
-
end
|
758
|
-
|
759
|
-
msgs
|
760
|
-
end
|
761
|
-
|
762
|
-
# consumer_info retrieves the current status of the pull subscription consumer.
|
763
|
-
# @param params [Hash] Options to customize API request.
|
764
|
-
# @option params [Float] :timeout Time to wait for response.
|
765
|
-
# @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
|
766
|
-
def consumer_info(params={})
|
767
|
-
@jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
|
768
|
-
end
|
769
|
-
end
|
770
|
-
private_constant :PullSubscription
|
771
|
-
|
772
|
-
#######################################
|
773
|
-
# #
|
774
|
-
# JetStream Message and Ack Methods #
|
775
|
-
# #
|
776
|
-
#######################################
|
777
|
-
|
778
|
-
# JetStream::Msg module includes the methods so that a regular NATS::Msg
|
779
|
-
# can be enhanced with JetStream features like acking and metadata.
|
780
|
-
module Msg
|
781
|
-
module Ack
|
782
|
-
# Ack types
|
783
|
-
Ack = ("+ACK".freeze)
|
784
|
-
Nak = ("-NAK".freeze)
|
785
|
-
Progress = ("+WPI".freeze)
|
786
|
-
Term = ("+TERM".freeze)
|
787
|
-
|
788
|
-
Empty = (''.freeze)
|
789
|
-
DotSep = ('.'.freeze)
|
790
|
-
NoDomainName = ('_'.freeze)
|
791
|
-
|
792
|
-
# Position
|
793
|
-
Prefix0 = ('$JS'.freeze)
|
794
|
-
Prefix1 = ('ACK'.freeze)
|
795
|
-
Domain = 2
|
796
|
-
AccHash = 3
|
797
|
-
Stream = 4
|
798
|
-
Consumer = 5
|
799
|
-
NumDelivered = 6
|
800
|
-
StreamSeq = 7
|
801
|
-
ConsumerSeq = 8
|
802
|
-
Timestamp = 9
|
803
|
-
NumPending = 10
|
804
|
-
|
805
|
-
# Subject without domain:
|
806
|
-
# $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
|
807
|
-
#
|
808
|
-
V1TokenCounts = 9
|
809
|
-
|
810
|
-
# Subject with domain:
|
811
|
-
# $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>.<a token with a random value>
|
812
|
-
#
|
813
|
-
V2TokenCounts = 12
|
814
|
-
|
815
|
-
SequencePair = Struct.new(:stream, :consumer)
|
816
|
-
end
|
817
|
-
private_constant :Ack
|
818
|
-
|
819
|
-
class Metadata
|
820
|
-
attr_reader :sequence, :num_delivered, :num_pending, :timestamp, :stream, :consumer, :domain
|
821
|
-
|
822
|
-
def initialize(opts)
|
823
|
-
@sequence = Ack::SequencePair.new(opts[Ack::StreamSeq].to_i, opts[Ack::ConsumerSeq].to_i)
|
824
|
-
@domain = opts[Ack::Domain]
|
825
|
-
@num_delivered = opts[Ack::NumDelivered].to_i
|
826
|
-
@num_pending = opts[Ack::NumPending].to_i
|
827
|
-
@timestamp = Time.at((opts[Ack::Timestamp].to_i / 1_000_000_000.0))
|
828
|
-
@stream = opts[Ack::Stream]
|
829
|
-
@consumer = opts[Ack::Consumer]
|
830
|
-
# TODO: Not exposed in Go client either right now.
|
831
|
-
# account = opts[Ack::AccHash]
|
832
|
-
end
|
833
|
-
end
|
834
|
-
|
835
|
-
module AckMethods
|
836
|
-
def ack(**params)
|
837
|
-
ensure_is_acked_once!
|
838
|
-
|
839
|
-
resp = if params[:timeout]
|
840
|
-
@nc.request(@reply, Ack::Ack, **params)
|
841
|
-
else
|
842
|
-
@nc.publish(@reply, Ack::Ack)
|
843
|
-
end
|
844
|
-
@sub.synchronize { @ackd = true }
|
845
|
-
|
846
|
-
resp
|
847
|
-
end
|
848
|
-
|
849
|
-
def ack_sync(**params)
|
850
|
-
ensure_is_acked_once!
|
851
|
-
|
852
|
-
params[:timeout] ||= 0.5
|
853
|
-
resp = @nc.request(@reply, Ack::Ack, **params)
|
854
|
-
@sub.synchronize { @ackd = true }
|
855
|
-
|
856
|
-
resp
|
857
|
-
end
|
858
|
-
|
859
|
-
def nak(**params)
|
860
|
-
ensure_is_acked_once!
|
861
|
-
|
862
|
-
resp = if params[:timeout]
|
863
|
-
@nc.request(@reply, Ack::Nak, **params)
|
864
|
-
else
|
865
|
-
@nc.publish(@reply, Ack::Nak)
|
866
|
-
end
|
867
|
-
@sub.synchronize { @ackd = true }
|
868
|
-
|
869
|
-
resp
|
870
|
-
end
|
871
|
-
|
872
|
-
def term(**params)
|
873
|
-
ensure_is_acked_once!
|
874
|
-
|
875
|
-
resp = if params[:timeout]
|
876
|
-
@nc.request(@reply, Ack::Term, **params)
|
877
|
-
else
|
878
|
-
@nc.publish(@reply, Ack::Term)
|
879
|
-
end
|
880
|
-
@sub.synchronize { @ackd = true }
|
881
|
-
|
882
|
-
resp
|
883
|
-
end
|
884
|
-
|
885
|
-
def in_progress(**params)
|
886
|
-
params[:timeout] ? @nc.request(@reply, Ack::Progress, **params) : @nc.publish(@reply, Ack::Progress)
|
887
|
-
end
|
888
|
-
|
889
|
-
def metadata
|
890
|
-
@meta ||= parse_metadata(reply)
|
891
|
-
end
|
892
|
-
|
893
|
-
private
|
894
|
-
|
895
|
-
def ensure_is_acked_once!
|
896
|
-
@sub.synchronize do
|
897
|
-
if @ackd
|
898
|
-
raise JetStream::Error::MsgAlreadyAckd.new("nats: message was already acknowledged: #{self}")
|
899
|
-
end
|
900
|
-
end
|
901
|
-
end
|
902
|
-
|
903
|
-
def parse_metadata(reply)
|
904
|
-
tokens = reply.split(Ack::DotSep)
|
905
|
-
n = tokens.count
|
906
|
-
|
907
|
-
case
|
908
|
-
when n < Ack::V1TokenCounts || (n > Ack::V1TokenCounts and n < Ack::V2TokenCounts)
|
909
|
-
raise NotJSMessage.new("nats: not a jetstream message")
|
910
|
-
when tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
|
911
|
-
raise NotJSMessage.new("nats: not a jetstream message")
|
912
|
-
when n == Ack::V1TokenCounts
|
913
|
-
tokens.insert(Ack::Domain, Ack::Empty)
|
914
|
-
tokens.insert(Ack::AccHash, Ack::Empty)
|
915
|
-
when tokens[Ack::Domain] == Ack::NoDomainName
|
916
|
-
tokens[Ack::Domain] = Ack::Empty
|
917
|
-
end
|
918
|
-
|
919
|
-
Metadata.new(tokens)
|
920
|
-
end
|
921
|
-
end
|
922
|
-
end
|
923
|
-
|
924
|
-
####################################
|
925
|
-
# #
|
926
|
-
# JetStream Configuration Options #
|
927
|
-
# #
|
928
|
-
####################################
|
929
|
-
|
930
|
-
# Misc internal functions to support JS API.
|
931
|
-
# @private
|
932
|
-
module JS
|
933
|
-
DefaultAPIPrefix = ("$JS.API".freeze)
|
934
|
-
|
935
|
-
module Status
|
936
|
-
CtrlMsg = ("100".freeze)
|
937
|
-
NoMsgs = ("404".freeze)
|
938
|
-
NotFound = ("404".freeze)
|
939
|
-
RequestTimeout = ("408".freeze)
|
940
|
-
ServiceUnavailable = ("503".freeze)
|
941
|
-
end
|
942
|
-
|
943
|
-
module Header
|
944
|
-
Status = ("Status".freeze)
|
945
|
-
Desc = ("Description".freeze)
|
946
|
-
MsgID = ("Nats-Msg-Id".freeze)
|
947
|
-
ExpectedStream = ("Nats-Expected-Stream".freeze)
|
948
|
-
ExpectedLastSeq = ("Nats-Expected-Last-Sequence".freeze)
|
949
|
-
ExpectedLastSubjSeq = ("Nats-Expected-Last-Subject-Sequence".freeze)
|
950
|
-
ExpectedLastMsgID = ("Nats-Expected-Last-Msg-Id".freeze)
|
951
|
-
LastConsumerSeq = ("Nats-Last-Consumer".freeze)
|
952
|
-
LastStreamSeq = ("Nats-Last-Stream".freeze)
|
953
|
-
end
|
954
|
-
|
955
|
-
module Config
|
956
|
-
# AckPolicy
|
957
|
-
AckExplicit = ("explicit".freeze)
|
958
|
-
AckAll = ("all".freeze)
|
959
|
-
AckNone = ("none".freeze)
|
960
|
-
end
|
961
|
-
|
962
|
-
class Sub
|
963
|
-
attr_reader :js, :stream, :consumer, :nms
|
964
|
-
|
965
|
-
def initialize(opts={})
|
966
|
-
@js = opts[:js]
|
967
|
-
@stream = opts[:stream]
|
968
|
-
@consumer = opts[:consumer]
|
969
|
-
@nms = opts[:nms]
|
970
|
-
end
|
971
|
-
end
|
972
|
-
|
973
|
-
class << self
|
974
|
-
def next_req_to_json(next_req)
|
975
|
-
req = {}
|
976
|
-
req[:batch] = next_req[:batch]
|
977
|
-
req[:expires] = next_req[:expires].to_i if next_req[:expires]
|
978
|
-
req[:no_wait] = next_req[:no_wait] if next_req[:no_wait]
|
979
|
-
req.to_json
|
980
|
-
end
|
981
|
-
|
982
|
-
def is_status_msg(msg)
|
983
|
-
return (!msg.nil? and (!msg.header.nil? and msg.header[Header::Status]))
|
984
|
-
end
|
985
|
-
|
986
|
-
# check_503_error raises exception when a NATS::Msg has a 503 status header.
|
987
|
-
# @param msg [NATS::Msg] The message with status headers.
|
988
|
-
# @raise [NATS::JetStream::Error::ServiceUnavailable]
|
989
|
-
def check_503_error(msg)
|
990
|
-
return if msg.nil? or msg.header.nil?
|
991
|
-
if msg.header[Header::Status] == Status::ServiceUnavailable
|
992
|
-
raise ::NATS::JetStream::Error::ServiceUnavailable
|
993
|
-
end
|
994
|
-
end
|
995
|
-
|
996
|
-
# from_msg takes a plain NATS::Msg and checks its headers to confirm
|
997
|
-
# if it was an error:
|
998
|
-
#
|
999
|
-
# msg.header={"Status"=>"503"})
|
1000
|
-
# msg.header={"Status"=>"408", "Description"=>"Request Timeout"})
|
1001
|
-
#
|
1002
|
-
# @param msg [NATS::Msg] The message with status headers.
|
1003
|
-
# @return [NATS::JetStream::API::Error]
|
1004
|
-
def from_msg(msg)
|
1005
|
-
check_503_error(msg)
|
1006
|
-
code = msg.header[JS::Header::Status]
|
1007
|
-
desc = msg.header[JS::Header::Desc]
|
1008
|
-
return ::NATS::JetStream::API::Error.new({code: code, description: desc})
|
1009
|
-
end
|
1010
|
-
|
1011
|
-
# from_error takes an API response that errored and maps the error
|
1012
|
-
# into a JetStream error type based on the status and error code.
|
1013
|
-
def from_error(err)
|
1014
|
-
return unless err
|
1015
|
-
case err[:code]
|
1016
|
-
when 503
|
1017
|
-
::NATS::JetStream::Error::ServiceUnavailable.new(err)
|
1018
|
-
when 500
|
1019
|
-
::NATS::JetStream::Error::ServerError.new(err)
|
1020
|
-
when 404
|
1021
|
-
case err[:err_code]
|
1022
|
-
when 10059
|
1023
|
-
::NATS::JetStream::Error::StreamNotFound.new(err)
|
1024
|
-
when 10014
|
1025
|
-
::NATS::JetStream::Error::ConsumerNotFound.new(err)
|
1026
|
-
else
|
1027
|
-
::NATS::JetStream::Error::NotFound.new(err)
|
1028
|
-
end
|
1029
|
-
when 400
|
1030
|
-
::NATS::JetStream::Error::BadRequest.new(err)
|
1031
|
-
else
|
1032
|
-
::NATS::JetStream::API::Error.new(err)
|
1033
|
-
end
|
1034
|
-
end
|
1035
|
-
end
|
1036
|
-
end
|
1037
|
-
private_constant :JS
|
1038
|
-
|
1039
|
-
#####################
|
1040
|
-
# #
|
1041
|
-
# JetStream Errors #
|
1042
|
-
# #
|
1043
|
-
#####################
|
1044
|
-
|
1045
|
-
# Error is any error that may arise when interacting with JetStream.
|
1046
|
-
class Error < Error
|
1047
|
-
|
1048
|
-
# When there is a NATS::IO::NoResponders error after making a publish request.
|
1049
|
-
class NoStreamResponse < Error; end
|
1050
|
-
|
1051
|
-
# When an invalid durable or consumer name was attempted to be used.
|
1052
|
-
class InvalidDurableName < Error; end
|
1053
|
-
|
1054
|
-
# When an ack not longer valid.
|
1055
|
-
class InvalidJSAck < Error; end
|
1056
|
-
|
1057
|
-
# When an ack has already been acked.
|
1058
|
-
class MsgAlreadyAckd < Error; end
|
1059
|
-
|
1060
|
-
# When the delivered message does not behave as a message delivered by JetStream,
|
1061
|
-
# for example when the ack reply has unrecognizable fields.
|
1062
|
-
class NotJSMessage < Error; end
|
1063
|
-
|
1064
|
-
# When the stream name is invalid.
|
1065
|
-
class InvalidStreamName < Error; end
|
1066
|
-
|
1067
|
-
# When the consumer name is invalid.
|
1068
|
-
class InvalidConsumerName < Error; end
|
1069
|
-
|
1070
|
-
# When the server responds with an error from the JetStream API.
|
1071
|
-
class APIError < Error
|
1072
|
-
attr_reader :code, :err_code, :description, :stream, :seq
|
1073
|
-
|
1074
|
-
def initialize(params={})
|
1075
|
-
@code = params[:code]
|
1076
|
-
@err_code = params[:err_code]
|
1077
|
-
@description = params[:description]
|
1078
|
-
@stream = params[:stream]
|
1079
|
-
@seq = params[:seq]
|
1080
|
-
end
|
1081
|
-
|
1082
|
-
def to_s
|
1083
|
-
"#{@description} (status_code=#{@code}, err_code=#{@err_code})"
|
1084
|
-
end
|
1085
|
-
end
|
1086
|
-
|
1087
|
-
# When JetStream is not currently available, this could be due to JetStream
|
1088
|
-
# not being enabled or temporarily unavailable due to a leader election when
|
1089
|
-
# running in cluster mode.
|
1090
|
-
# This condition is represented with a message that has 503 status code header.
|
1091
|
-
class ServiceUnavailable < APIError
|
1092
|
-
def initialize(params={})
|
1093
|
-
super(params)
|
1094
|
-
@code ||= 503
|
1095
|
-
end
|
1096
|
-
end
|
1097
|
-
|
1098
|
-
# When there is a hard failure in the JetStream.
|
1099
|
-
# This condition is represented with a message that has 500 status code header.
|
1100
|
-
class ServerError < APIError
|
1101
|
-
def initialize(params={})
|
1102
|
-
super(params)
|
1103
|
-
@code ||= 500
|
1104
|
-
end
|
1105
|
-
end
|
1106
|
-
|
1107
|
-
# When a JetStream object was not found.
|
1108
|
-
# This condition is represented with a message that has 404 status code header.
|
1109
|
-
class NotFound < APIError
|
1110
|
-
def initialize(params={})
|
1111
|
-
super(params)
|
1112
|
-
@code ||= 404
|
1113
|
-
end
|
1114
|
-
end
|
1115
|
-
|
1116
|
-
# When the stream is not found.
|
1117
|
-
class StreamNotFound < NotFound; end
|
1118
|
-
|
1119
|
-
# When the consumer or durable is not found by name.
|
1120
|
-
class ConsumerNotFound < NotFound; end
|
1121
|
-
|
1122
|
-
# When the JetStream client makes an invalid request.
|
1123
|
-
# This condition is represented with a message that has 400 status code header.
|
1124
|
-
class BadRequest < APIError
|
1125
|
-
def initialize(params={})
|
1126
|
-
super(params)
|
1127
|
-
@code ||= 400
|
1128
|
-
end
|
1129
|
-
end
|
1130
|
-
end
|
1131
|
-
|
1132
|
-
#######################
|
1133
|
-
# #
|
1134
|
-
# JetStream API Types #
|
1135
|
-
# #
|
1136
|
-
#######################
|
1137
|
-
|
1138
|
-
# JetStream::API are the types used to interact with the JetStream API.
|
1139
|
-
module API
|
1140
|
-
# When the server responds with an error from the JetStream API.
|
1141
|
-
Error = ::NATS::JetStream::Error::APIError
|
1142
|
-
|
1143
|
-
# SequenceInfo is a pair of consumer and stream sequence and last activity.
|
1144
|
-
# @!attribute consumer_seq
|
1145
|
-
# @return [Integer] The consumer sequence.
|
1146
|
-
# @!attribute stream_seq
|
1147
|
-
# @return [Integer] The stream sequence.
|
1148
|
-
SequenceInfo = Struct.new(:consumer_seq, :stream_seq, :last_active,
|
1149
|
-
keyword_init: true) do
|
1150
|
-
def initialize(opts={})
|
1151
|
-
# Filter unrecognized fields and freeze.
|
1152
|
-
rem = opts.keys - members
|
1153
|
-
opts.delete_if { |k| rem.include?(k) }
|
1154
|
-
super(opts)
|
1155
|
-
freeze
|
1156
|
-
end
|
1157
|
-
end
|
1158
|
-
|
1159
|
-
# ConsumerInfo is the current status of a JetStream consumer.
|
1160
|
-
#
|
1161
|
-
# @!attribute stream_name
|
1162
|
-
# @return [String] name of the stream to which the consumer belongs.
|
1163
|
-
# @!attribute name
|
1164
|
-
# @return [String] name of the consumer.
|
1165
|
-
# @!attribute created
|
1166
|
-
# @return [String] time when the consumer was created.
|
1167
|
-
# @!attribute config
|
1168
|
-
# @return [ConsumerConfig] consumer configuration.
|
1169
|
-
# @!attribute delivered
|
1170
|
-
# @return [SequenceInfo]
|
1171
|
-
# @!attribute ack_floor
|
1172
|
-
# @return [SequenceInfo]
|
1173
|
-
# @!attribute num_ack_pending
|
1174
|
-
# @return [Integer]
|
1175
|
-
# @!attribute num_redelivered
|
1176
|
-
# @return [Integer]
|
1177
|
-
# @!attribute num_waiting
|
1178
|
-
# @return [Integer]
|
1179
|
-
# @!attribute num_pending
|
1180
|
-
# @return [Integer]
|
1181
|
-
# @!attribute cluster
|
1182
|
-
# @return [Hash]
|
1183
|
-
ConsumerInfo = Struct.new(:type, :stream_name, :name, :created,
|
1184
|
-
:config, :delivered, :ack_floor,
|
1185
|
-
:num_ack_pending, :num_redelivered, :num_waiting,
|
1186
|
-
:num_pending, :cluster, :push_bound,
|
1187
|
-
keyword_init: true) do
|
1188
|
-
def initialize(opts={})
|
1189
|
-
opts[:created] = Time.parse(opts[:created])
|
1190
|
-
opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
|
1191
|
-
opts[:delivered] = SequenceInfo.new(opts[:delivered])
|
1192
|
-
opts[:config][:ack_wait] = opts[:config][:ack_wait] / ::NATS::NANOSECONDS
|
1193
|
-
opts[:config] = ConsumerConfig.new(opts[:config])
|
1194
|
-
opts.delete(:cluster)
|
1195
|
-
# Filter unrecognized fields just in case.
|
1196
|
-
rem = opts.keys - members
|
1197
|
-
opts.delete_if { |k| rem.include?(k) }
|
1198
|
-
super(opts)
|
1199
|
-
freeze
|
1200
|
-
end
|
1201
|
-
end
|
1202
|
-
|
1203
|
-
# ConsumerConfig is the consumer configuration.
|
1204
|
-
#
|
1205
|
-
# @!attribute durable_name
|
1206
|
-
# @return [String]
|
1207
|
-
# @!attribute deliver_policy
|
1208
|
-
# @return [String]
|
1209
|
-
# @!attribute ack_policy
|
1210
|
-
# @return [String]
|
1211
|
-
# @!attribute ack_wait
|
1212
|
-
# @return [Integer]
|
1213
|
-
# @!attribute max_deliver
|
1214
|
-
# @return [Integer]
|
1215
|
-
# @!attribute replay_policy
|
1216
|
-
# @return [String]
|
1217
|
-
# @!attribute max_waiting
|
1218
|
-
# @return [Integer]
|
1219
|
-
# @!attribute max_ack_pending
|
1220
|
-
# @return [Integer]
|
1221
|
-
ConsumerConfig = Struct.new(:name, :durable_name, :description,
|
1222
|
-
:deliver_policy, :opt_start_seq, :opt_start_time,
|
1223
|
-
:ack_policy, :ack_wait, :max_deliver, :backoff,
|
1224
|
-
:filter_subject, :replay_policy, :rate_limit_bps,
|
1225
|
-
:sample_freq, :max_waiting, :max_ack_pending,
|
1226
|
-
:flow_control, :idle_heartbeat, :headers_only,
|
1227
|
-
|
1228
|
-
# Pull based options
|
1229
|
-
:max_batch, :max_expires,
|
1230
|
-
# Push based consumers
|
1231
|
-
:deliver_subject, :deliver_group,
|
1232
|
-
# Ephemeral inactivity threshold
|
1233
|
-
:inactive_threshold,
|
1234
|
-
# Generally inherited by parent stream and other markers,
|
1235
|
-
# now can be configured directly.
|
1236
|
-
:num_replicas,
|
1237
|
-
# Force memory storage
|
1238
|
-
:mem_storage,
|
1239
|
-
keyword_init: true) do
|
1240
|
-
def initialize(opts={})
|
1241
|
-
# Filter unrecognized fields just in case.
|
1242
|
-
rem = opts.keys - members
|
1243
|
-
opts.delete_if { |k| rem.include?(k) }
|
1244
|
-
super(opts)
|
1245
|
-
end
|
1246
|
-
|
1247
|
-
def to_json(*args)
|
1248
|
-
config = self.to_h
|
1249
|
-
config.delete_if { |_k, v| v.nil? }
|
1250
|
-
config.to_json(*args)
|
1251
|
-
end
|
1252
|
-
end
|
1253
|
-
|
1254
|
-
# StreamConfig represents the configuration of a stream from JetStream.
|
1255
|
-
#
|
1256
|
-
# @!attribute type
|
1257
|
-
# @return [String]
|
1258
|
-
# @!attribute config
|
1259
|
-
# @return [Hash]
|
1260
|
-
# @!attribute created
|
1261
|
-
# @return [String]
|
1262
|
-
# @!attribute state
|
1263
|
-
# @return [StreamState]
|
1264
|
-
# @!attribute did_create
|
1265
|
-
# @return [Boolean]
|
1266
|
-
# @!attribute name
|
1267
|
-
# @return [String]
|
1268
|
-
# @!attribute subjects
|
1269
|
-
# @return [Array]
|
1270
|
-
# @!attribute retention
|
1271
|
-
# @return [String]
|
1272
|
-
# @!attribute max_consumers
|
1273
|
-
# @return [Integer]
|
1274
|
-
# @!attribute max_msgs
|
1275
|
-
# @return [Integer]
|
1276
|
-
# @!attribute max_bytes
|
1277
|
-
# @return [Integer]
|
1278
|
-
# @!attribute max_age
|
1279
|
-
# @return [Integer]
|
1280
|
-
# @!attribute max_msgs_per_subject
|
1281
|
-
# @return [Integer]
|
1282
|
-
# @!attribute max_msg_size
|
1283
|
-
# @return [Integer]
|
1284
|
-
# @!attribute discard
|
1285
|
-
# @return [String]
|
1286
|
-
# @!attribute storage
|
1287
|
-
# @return [String]
|
1288
|
-
# @!attribute num_replicas
|
1289
|
-
# @return [Integer]
|
1290
|
-
# @!attribute duplicate_window
|
1291
|
-
# @return [Integer]
|
1292
|
-
StreamConfig = Struct.new(
|
1293
|
-
:name,
|
1294
|
-
:description,
|
1295
|
-
:subjects,
|
1296
|
-
:retention,
|
1297
|
-
:max_consumers,
|
1298
|
-
:max_msgs,
|
1299
|
-
:max_bytes,
|
1300
|
-
:discard,
|
1301
|
-
:max_age,
|
1302
|
-
:max_msgs_per_subject,
|
1303
|
-
:max_msg_size,
|
1304
|
-
:storage,
|
1305
|
-
:num_replicas,
|
1306
|
-
:no_ack,
|
1307
|
-
:duplicate_window,
|
1308
|
-
:placement,
|
1309
|
-
:mirror,
|
1310
|
-
:sources,
|
1311
|
-
:sealed,
|
1312
|
-
:deny_delete,
|
1313
|
-
:deny_purge,
|
1314
|
-
:allow_rollup_hdrs,
|
1315
|
-
:republish,
|
1316
|
-
:allow_direct,
|
1317
|
-
:mirror_direct,
|
1318
|
-
keyword_init: true) do
|
1319
|
-
def initialize(opts={})
|
1320
|
-
# Filter unrecognized fields just in case.
|
1321
|
-
rem = opts.keys - members
|
1322
|
-
opts.delete_if { |k| rem.include?(k) }
|
1323
|
-
super(opts)
|
1324
|
-
end
|
1325
|
-
|
1326
|
-
def to_json(*args)
|
1327
|
-
config = self.to_h
|
1328
|
-
config.delete_if { |_k, v| v.nil? }
|
1329
|
-
config.to_json(*args)
|
1330
|
-
end
|
1331
|
-
end
|
1332
|
-
|
1333
|
-
# StreamInfo is the info about a stream from JetStream.
|
1334
|
-
#
|
1335
|
-
# @!attribute type
|
1336
|
-
# @return [String]
|
1337
|
-
# @!attribute config
|
1338
|
-
# @return [Hash]
|
1339
|
-
# @!attribute created
|
1340
|
-
# @return [String]
|
1341
|
-
# @!attribute state
|
1342
|
-
# @return [Hash]
|
1343
|
-
# @!attribute domain
|
1344
|
-
# @return [String]
|
1345
|
-
StreamInfo = Struct.new(:type, :config, :created, :state, :domain,
|
1346
|
-
keyword_init: true) do
|
1347
|
-
def initialize(opts={})
|
1348
|
-
opts[:config] = StreamConfig.new(opts[:config])
|
1349
|
-
opts[:state] = StreamState.new(opts[:state])
|
1350
|
-
opts[:created] = ::Time.parse(opts[:created])
|
1351
|
-
|
1352
|
-
# Filter fields and freeze.
|
1353
|
-
rem = opts.keys - members
|
1354
|
-
opts.delete_if { |k| rem.include?(k) }
|
1355
|
-
super(opts)
|
1356
|
-
freeze
|
1357
|
-
end
|
1358
|
-
end
|
1359
|
-
|
1360
|
-
# StreamState is the state of a stream.
|
1361
|
-
#
|
1362
|
-
# @!attribute messages
|
1363
|
-
# @return [Integer]
|
1364
|
-
# @!attribute bytes
|
1365
|
-
# @return [Integer]
|
1366
|
-
# @!attribute first_seq
|
1367
|
-
# @return [Integer]
|
1368
|
-
# @!attribute last_seq
|
1369
|
-
# @return [Integer]
|
1370
|
-
# @!attribute consumer_count
|
1371
|
-
# @return [Integer]
|
1372
|
-
StreamState = Struct.new(:messages, :bytes, :first_seq, :first_ts,
|
1373
|
-
:last_seq, :last_ts, :consumer_count,
|
1374
|
-
keyword_init: true) do
|
1375
|
-
def initialize(opts={})
|
1376
|
-
rem = opts.keys - members
|
1377
|
-
opts.delete_if { |k| rem.include?(k) }
|
1378
|
-
super(opts)
|
1379
|
-
end
|
1380
|
-
end
|
1381
|
-
|
1382
|
-
# StreamCreateResponse is the response from the JetStream $JS.API.STREAM.CREATE API.
|
1383
|
-
#
|
1384
|
-
# @!attribute type
|
1385
|
-
# @return [String]
|
1386
|
-
# @!attribute config
|
1387
|
-
# @return [StreamConfig]
|
1388
|
-
# @!attribute created
|
1389
|
-
# @return [String]
|
1390
|
-
# @!attribute state
|
1391
|
-
# @return [StreamState]
|
1392
|
-
# @!attribute did_create
|
1393
|
-
# @return [Boolean]
|
1394
|
-
StreamCreateResponse = Struct.new(:type, :config, :created, :state, :did_create,
|
1395
|
-
keyword_init: true) do
|
1396
|
-
def initialize(opts={})
|
1397
|
-
rem = opts.keys - members
|
1398
|
-
opts.delete_if { |k| rem.include?(k) }
|
1399
|
-
opts[:config] = StreamConfig.new(opts[:config])
|
1400
|
-
opts[:state] = StreamState.new(opts[:state])
|
1401
|
-
super(opts)
|
1402
|
-
freeze
|
1403
|
-
end
|
1404
|
-
end
|
1405
|
-
|
1406
|
-
RawStreamMsg = Struct.new(:subject, :seq, :data, :headers, keyword_init: true) do
|
1407
|
-
def initialize(opts)
|
1408
|
-
opts[:data] = Base64.decode64(opts[:data]) if opts[:data]
|
1409
|
-
if opts[:hdrs]
|
1410
|
-
header = Base64.decode64(opts[:hdrs])
|
1411
|
-
hdr = {}
|
1412
|
-
lines = header.lines
|
1413
|
-
lines.slice(1, header.size).each do |line|
|
1414
|
-
line.rstrip!
|
1415
|
-
next if line.empty?
|
1416
|
-
key, value = line.strip.split(/\s*:\s*/, 2)
|
1417
|
-
hdr[key] = value
|
1418
|
-
end
|
1419
|
-
opts[:headers] = hdr
|
1420
|
-
end
|
1421
|
-
|
1422
|
-
# Filter out members not present.
|
1423
|
-
rem = opts.keys - members
|
1424
|
-
opts.delete_if { |k| rem.include?(k) }
|
1425
|
-
super(opts)
|
1426
|
-
end
|
1427
|
-
|
1428
|
-
def sequence
|
1429
|
-
self.seq
|
1430
|
-
end
|
1431
|
-
end
|
1432
|
-
end
|
1433
|
-
end
|
1434
|
-
end
|