nats-pure 0.7.2 → 2.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/lib/nats/io/js.rb ADDED
@@ -0,0 +1,1090 @@
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 'time'
18
+
19
+ module NATS
20
+
21
+ # JetStream returns a context with a similar API as the NATS::Client
22
+ # but with enhanced functions to persist and consume messages from
23
+ # the NATS JetStream engine.
24
+ #
25
+ # @example
26
+ # nc = NATS.connect("demo.nats.io")
27
+ # js = nc.jetstream()
28
+ #
29
+ class JetStream
30
+ # Create a new JetStream context for a NATS connection.
31
+ #
32
+ # @param conn [NATS::Client]
33
+ # @param params [Hash] Options to customize JetStream context.
34
+ # @option params [String] :prefix JetStream API prefix to use for the requests.
35
+ # @option params [String] :domain JetStream Domain to use for the requests.
36
+ # @option params [Float] :timeout Default timeout to use for JS requests.
37
+ def initialize(conn, params={})
38
+ @nc = conn
39
+ @prefix = if params[:prefix]
40
+ params[:prefix]
41
+ elsif params[:domain]
42
+ "$JS.#{params[:domain]}.API"
43
+ else
44
+ JS::DefaultAPIPrefix
45
+ end
46
+ @opts = params
47
+ @opts[:timeout] ||= 5 # seconds
48
+ params[:prefix] = @prefix
49
+
50
+ # Include JetStream::Manager
51
+ extend Manager
52
+ end
53
+
54
+ # PubAck is the API response from a successfully published message.
55
+ #
56
+ # @!attribute [stream] stream
57
+ # @return [String] Name of the stream that processed the published message.
58
+ # @!attribute [seq] seq
59
+ # @return [Fixnum] Sequence of the message in the stream.
60
+ # @!attribute [duplicate] duplicate
61
+ # @return [Boolean] Indicates whether the published message is a duplicate.
62
+ # @!attribute [domain] domain
63
+ # @return [String] JetStream Domain that processed the ack response.
64
+ PubAck = Struct.new(:stream, :seq, :duplicate, :domain, keyword_init: true)
65
+
66
+ # publish produces a message for JetStream.
67
+ #
68
+ # @param subject [String] The subject from a stream where the message will be sent.
69
+ # @param payload [String] The payload of the message.
70
+ # @param params [Hash] Options to customize the publish message request.
71
+ # @option params [Float] :timeout Time to wait for an PubAck response or an error.
72
+ # @option params [Hash] :header NATS Headers to use for the message.
73
+ # @option params [String] :stream Expected Stream to which the message is being published.
74
+ # @raise [NATS::Timeout] When it takes too long to receive an ack response.
75
+ # @return [PubAck] The pub ack response.
76
+ def publish(subject, payload="", **params)
77
+ params[:timeout] ||= @opts[:timeout]
78
+ if params[:stream]
79
+ params[:header] ||= {}
80
+ params[:header][JS::Header::ExpectedStream] = params[:stream]
81
+ end
82
+
83
+ # Send message with headers.
84
+ msg = NATS::Msg.new(subject: subject,
85
+ data: payload,
86
+ header: params[:header])
87
+
88
+ begin
89
+ resp = @nc.request_msg(msg, **params)
90
+ result = JSON.parse(resp.data, symbolize_names: true)
91
+ rescue ::NATS::IO::NoRespondersError
92
+ raise JetStream::Error::NoStreamResponse.new("nats: no response from stream")
93
+ end
94
+ raise JS.from_error(result[:error]) if result[:error]
95
+
96
+ PubAck.new(result)
97
+ end
98
+
99
+ # pull_subscribe binds or creates a subscription to a JetStream pull consumer.
100
+ #
101
+ # @param subject [String] Subject from which the messages will be fetched.
102
+ # @param durable [String] Consumer durable name from where the messages will be fetched.
103
+ # @param params [Hash] Options to customize the PullSubscription.
104
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
105
+ # @option params [String] :consumer Name of the Consumer to which the PullSubscription will be bound.
106
+ # @return [NATS::JetStream::PullSubscription]
107
+ def pull_subscribe(subject, durable, params={})
108
+ raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name") if durable.empty?
109
+ params[:consumer] ||= durable
110
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
111
+
112
+ begin
113
+ consumer_info(stream, params[:consumer])
114
+ rescue NATS::JetStream::Error::NotFound => e
115
+ # If attempting to bind, then this is a hard error.
116
+ raise e if params[:stream]
117
+
118
+ config = if not params[:config]
119
+ JetStream::API::ConsumerConfig.new
120
+ elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
121
+ params[:config]
122
+ else
123
+ JetStream::API::ConsumerConfig.new(params[:config])
124
+ end
125
+ config[:durable_name] = durable
126
+ config[:ack_policy] ||= JS::Config::AckExplicit
127
+ add_consumer(stream, config)
128
+ end
129
+
130
+ deliver = @nc.new_inbox
131
+ sub = @nc.subscribe(deliver)
132
+ sub.extend(PullSubscription)
133
+
134
+ consumer = params[:consumer]
135
+ subject = "#{@prefix}.CONSUMER.MSG.NEXT.#{stream}.#{consumer}"
136
+ sub.jsi = JS::Sub.new(
137
+ js: self,
138
+ stream: stream,
139
+ consumer: params[:consumer],
140
+ nms: subject
141
+ )
142
+ sub
143
+ end
144
+
145
+ # A JetStream::Manager can be used to make requests to the JetStream API.
146
+ #
147
+ # @example
148
+ # require 'nats/client'
149
+ #
150
+ # nc = NATS.connect("demo.nats.io")
151
+ #
152
+ # config = JetStream::API::StreamConfig.new()
153
+ # nc.jsm.add_stream(config)
154
+ #
155
+ #
156
+ module Manager
157
+ # add_stream creates a stream with a given config.
158
+ # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
159
+ # @param params [Hash] Options to customize API request.
160
+ # @option params [Float] :timeout Time to wait for response.
161
+ # @return [JetStream::API::StreamCreateResponse] The result of creating a Stream.
162
+ def add_stream(config, params={})
163
+ config = if not config.is_a?(JetStream::API::StreamConfig)
164
+ JetStream::API::StreamConfig.new(config)
165
+ else
166
+ config
167
+ end
168
+ stream = config[:name]
169
+ raise ArgumentError.new(":name is required to create streams") unless stream
170
+ req_subject = "#{@prefix}.STREAM.CREATE.#{stream}"
171
+ result = api_request(req_subject, config.to_json, params)
172
+ JetStream::API::StreamCreateResponse.new(result)
173
+ end
174
+
175
+ # stream_info retrieves the current status of a stream.
176
+ # @param stream [String] Name of the stream.
177
+ # @param params [Hash] Options to customize API request.
178
+ # @option params [Float] :timeout Time to wait for response.
179
+ # @return [JetStream::API::StreamInfo] The latest StreamInfo of the stream.
180
+ def stream_info(stream, params={})
181
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
182
+
183
+ req_subject = "#{@prefix}.STREAM.INFO.#{stream}"
184
+ result = api_request(req_subject, '', params)
185
+ JetStream::API::StreamInfo.new(result)
186
+ end
187
+
188
+ # delete_stream deletes a stream.
189
+ # @param stream [String] Name of the stream.
190
+ # @param params [Hash] Options to customize API request.
191
+ # @option params [Float] :timeout Time to wait for response.
192
+ # @return [Boolean]
193
+ def delete_stream(stream, params={})
194
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
195
+
196
+ req_subject = "#{@prefix}.STREAM.DELETE.#{stream}"
197
+ result = api_request(req_subject, '', params)
198
+ result[:success]
199
+ end
200
+
201
+ # add_consumer creates a consumer with a given config.
202
+ # @param stream [String] Name of the stream.
203
+ # @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
204
+ # @param params [Hash] Options to customize API request.
205
+ # @option params [Float] :timeout Time to wait for response.
206
+ # @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
207
+ def add_consumer(stream, config, params={})
208
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
209
+ config = if not config.is_a?(JetStream::API::ConsumerConfig)
210
+ JetStream::API::ConsumerConfig.new(config)
211
+ else
212
+ config
213
+ end
214
+ req_subject = if config[:durable_name]
215
+ "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
216
+ else
217
+ "#{@prefix}.CONSUMER.CREATE.#{stream}"
218
+ end
219
+
220
+ config[:ack_policy] ||= JS::Config::AckExplicit
221
+ # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
222
+ if config[:ack_wait]
223
+ raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
224
+ config[:ack_wait] = config[:ack_wait] * 1_000_000_000
225
+ end
226
+
227
+ req = {
228
+ stream_name: stream,
229
+ config: config
230
+ }
231
+ result = api_request(req_subject, req.to_json, params)
232
+ JetStream::API::ConsumerInfo.new(result).freeze
233
+ end
234
+
235
+ # consumer_info retrieves the current status of a consumer.
236
+ # @param stream [String] Name of the stream.
237
+ # @param consumer [String] Name of the consumer.
238
+ # @param params [Hash] Options to customize API request.
239
+ # @option params [Float] :timeout Time to wait for response.
240
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
241
+ def consumer_info(stream, consumer, params={})
242
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
243
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
244
+
245
+ req_subject = "#{@prefix}.CONSUMER.INFO.#{stream}.#{consumer}"
246
+ result = api_request(req_subject, '', params)
247
+ JetStream::API::ConsumerInfo.new(result)
248
+ end
249
+
250
+ # delete_consumer deletes a consumer.
251
+ # @param stream [String] Name of the stream.
252
+ # @param consumer [String] Name of the consumer.
253
+ # @param params [Hash] Options to customize API request.
254
+ # @option params [Float] :timeout Time to wait for response.
255
+ # @return [Boolean]
256
+ def delete_consumer(stream, consumer, params={})
257
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
258
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
259
+
260
+ req_subject = "#{@prefix}.CONSUMER.DELETE.#{stream}.#{consumer}"
261
+ result = api_request(req_subject, '', params)
262
+ result[:success]
263
+ end
264
+
265
+ # find_stream_name_by_subject does a lookup for the stream to which
266
+ # the subject belongs.
267
+ # @param subject [String] The subject that belongs to a stream.
268
+ # @param params [Hash] Options to customize API request.
269
+ # @option params [Float] :timeout Time to wait for response.
270
+ # @return [String] The name of the JetStream stream for the subject.
271
+ def find_stream_name_by_subject(subject, params={})
272
+ req_subject = "#{@prefix}.STREAM.NAMES"
273
+ req = { subject: subject }
274
+ result = api_request(req_subject, req.to_json, params)
275
+ raise JetStream::Error::NotFound unless result[:streams]
276
+
277
+ result[:streams].first
278
+ end
279
+
280
+ private
281
+
282
+ def api_request(req_subject, req="", params={})
283
+ params[:timeout] ||= @opts[:timeout]
284
+ result = begin
285
+ msg = @nc.request(req_subject, req, **params)
286
+ JSON.parse(msg.data, symbolize_names: true)
287
+ rescue NATS::IO::NoRespondersError
288
+ raise JetStream::Error::ServiceUnavailable
289
+ end
290
+ raise JS.from_error(result[:error]) if result[:error]
291
+
292
+ result
293
+ end
294
+ end
295
+
296
+ # PullSubscription is included into NATS::Subscription so that it
297
+ # can be used to fetch messages from a pull based consumer from
298
+ # JetStream.
299
+ #
300
+ # @example Create a pull subscription using JetStream context.
301
+ #
302
+ # require 'nats/client'
303
+ #
304
+ # nc = NATS.connect
305
+ # js = nc.jetstream
306
+ # psub = js.pull_subscribe("foo", "bar")
307
+ #
308
+ # loop do
309
+ # msgs = psub.fetch(5)
310
+ # msgs.each do |msg|
311
+ # msg.ack
312
+ # end
313
+ # end
314
+ #
315
+ # @!visibility public
316
+ module PullSubscription
317
+ # next_msg is not available for pull based subscriptions.
318
+ # @raise [NATS::JetStream::Error]
319
+ def next_msg(params={})
320
+ raise ::NATS::JetStream::Error.new("nats: pull subscription cannot use next_msg")
321
+ end
322
+
323
+ # fetch makes a request to be delivered more messages from a pull consumer.
324
+ #
325
+ # @param batch [Fixnum] Number of messages to pull from the stream.
326
+ # @param params [Hash] Options to customize the fetch request.
327
+ # @option params [Float] :timeout Duration of the fetch request before it expires.
328
+ # @return [Array<NATS::Msg>]
329
+ def fetch(batch=1, params={})
330
+ if batch < 1
331
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
332
+ end
333
+
334
+ t = MonotonicTime.now
335
+ timeout = params[:timeout] ||= 5
336
+ expires = (timeout * 1_000_000_000) - 100_000
337
+ next_req = {
338
+ batch: batch
339
+ }
340
+
341
+ msgs = []
342
+ case
343
+ when batch < 1
344
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
345
+ when batch == 1
346
+ ####################################################
347
+ # Fetch (1) #
348
+ ####################################################
349
+
350
+ # Check if there is any pending message in the queue that is
351
+ # ready to be consumed.
352
+ synchronize do
353
+ unless @pending_queue.empty?
354
+ msg = @pending_queue.pop
355
+ # Check for a no msgs response status.
356
+ if JS.is_status_msg(msg)
357
+ case msg.header["Status"]
358
+ when JS::Status::NoMsgs
359
+ msg = nil
360
+ else
361
+ raise JS.from_msg(msg)
362
+ end
363
+ else
364
+ msgs << msg
365
+ end
366
+ end
367
+ end
368
+
369
+ # Make lingering request with expiration.
370
+ next_req[:expires] = expires
371
+ if msgs.empty?
372
+ # Make publish request and wait for response.
373
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
374
+
375
+ # Wait for result of fetch or timeout.
376
+ synchronize { wait_for_msgs_cond.wait(timeout) }
377
+
378
+ msgs << @pending_queue.pop unless @pending_queue.empty?
379
+
380
+ duration = MonotonicTime.since(t)
381
+ if duration > timeout
382
+ raise ::NATS::Timeout.new("nats: fetch timeout")
383
+ end
384
+
385
+ # Should have received at least a message at this point,
386
+ # if that is not the case then error already.
387
+ if JS.is_status_msg(msgs.first)
388
+ raise JS.from_msg(msgs.first)
389
+ end
390
+ end
391
+ when batch > 1
392
+ ####################################################
393
+ # Fetch (n) #
394
+ ####################################################
395
+
396
+ # Check if there already enough in the pending buffer.
397
+ synchronize do
398
+ if batch <= @pending_queue.size
399
+ batch.times { msgs << @pending_queue.pop }
400
+
401
+ return msgs
402
+ end
403
+ end
404
+
405
+ # Make publish request and wait any response.
406
+ next_req[:no_wait] = true
407
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
408
+
409
+ # Not receiving even one is a timeout.
410
+ start_time = MonotonicTime.now
411
+ msg = nil
412
+ synchronize {
413
+ wait_for_msgs_cond.wait(timeout)
414
+ msg = @pending_queue.pop unless @pending_queue.empty?
415
+ }
416
+
417
+ # Check if the first message was a response saying that
418
+ # there are no messages.
419
+ if !msg.nil? && JS.is_status_msg(msg)
420
+ case msg.header[JS::Header::Status]
421
+ when JS::Status::NoMsgs
422
+ # Make another request that does wait.
423
+ next_req[:expires] = expires
424
+ next_req.delete(:no_wait)
425
+
426
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
427
+ else
428
+ raise JS.from_msg(msg)
429
+ end
430
+ else
431
+ msgs << msg
432
+ end
433
+
434
+ # Check if have not received yet a single message.
435
+ duration = MonotonicTime.since(start_time)
436
+ if msgs.empty? and duration > timeout
437
+ raise NATS::Timeout.new("nats: fetch timeout")
438
+ end
439
+
440
+ needed = batch - msgs.count
441
+ while needed > 0 and MonotonicTime.since(start_time) < timeout
442
+ duration = MonotonicTime.since(start_time)
443
+
444
+ # Wait for the rest of the messages.
445
+ synchronize do
446
+
447
+ # Wait until there is a message delivered.
448
+ if @pending_queue.empty?
449
+ deadline = timeout - duration
450
+ wait_for_msgs_cond.wait(deadline) if deadline > 0
451
+
452
+ duration = MonotonicTime.since(start_time)
453
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
454
+ raise NATS::Timeout.new("nats: fetch timeout")
455
+ end
456
+ else
457
+ msg = @pending_queue.pop
458
+
459
+ if JS.is_status_msg(msg)
460
+ case msg.header["Status"]
461
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
462
+ duration = MonotonicTime.since(start_time)
463
+
464
+ # Do not time out if we received at least some messages.
465
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
466
+ raise NATS::Timeout.new("nats: fetch timeout")
467
+ end
468
+
469
+ # Likely only received a subset of the messages.
470
+ return msgs
471
+ else
472
+ raise JS.from_msg(msg)
473
+ end
474
+ else
475
+ msgs << msg
476
+ needed -= 1
477
+ end
478
+ end
479
+ end # :end: synchronize
480
+ end
481
+ end
482
+
483
+ msgs
484
+ end
485
+
486
+ # consumer_info retrieves the current status of the pull subscription consumer.
487
+ # @param params [Hash] Options to customize API request.
488
+ # @option params [Float] :timeout Time to wait for response.
489
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
490
+ def consumer_info(params={})
491
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
492
+ end
493
+ end
494
+ private_constant :PullSubscription
495
+
496
+ #######################################
497
+ # #
498
+ # JetStream Message and Ack Methods #
499
+ # #
500
+ #######################################
501
+
502
+ # JetStream::Msg module includes the methods so that a regular NATS::Msg
503
+ # can be enhanced with JetStream features like acking and metadata.
504
+ module Msg
505
+ module Ack
506
+ # Ack types
507
+ Ack = ("+ACK".freeze)
508
+ Nak = ("-NAK".freeze)
509
+ Progress = ("+WPI".freeze)
510
+ Term = ("+TERM".freeze)
511
+
512
+ Empty = (''.freeze)
513
+ DotSep = ('.'.freeze)
514
+ NoDomainName = ('_'.freeze)
515
+
516
+ # Position
517
+ Prefix0 = ('$JS'.freeze)
518
+ Prefix1 = ('ACK'.freeze)
519
+ Domain = 2
520
+ AccHash = 3
521
+ Stream = 4
522
+ Consumer = 5
523
+ NumDelivered = 6
524
+ StreamSeq = 7
525
+ ConsumerSeq = 8
526
+ Timestamp = 9
527
+ NumPending = 10
528
+
529
+ # Subject without domain:
530
+ # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
531
+ #
532
+ V1TokenCounts = 9
533
+
534
+ # Subject with domain:
535
+ # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>.<a token with a random value>
536
+ #
537
+ V2TokenCounts = 12
538
+
539
+ SequencePair = Struct.new(:stream, :consumer)
540
+ end
541
+ private_constant :Ack
542
+
543
+ class Metadata
544
+ attr_reader :sequence, :num_delivered, :num_pending, :timestamp, :stream, :consumer, :domain
545
+
546
+ def initialize(opts)
547
+ @sequence = Ack::SequencePair.new(opts[Ack::StreamSeq].to_i, opts[Ack::ConsumerSeq].to_i)
548
+ @domain = opts[Ack::Domain]
549
+ @num_delivered = opts[Ack::NumDelivered].to_i
550
+ @num_pending = opts[Ack::NumPending].to_i
551
+ @timestamp = Time.at((opts[Ack::Timestamp].to_i / 1_000_000_000.0))
552
+ @stream = opts[Ack::Stream]
553
+ @consumer = opts[Ack::Consumer]
554
+ # TODO: Not exposed in Go client either right now.
555
+ # account = opts[Ack::AccHash]
556
+ end
557
+ end
558
+
559
+ module AckMethods
560
+ def ack(**params)
561
+ ensure_is_acked_once!
562
+
563
+ resp = if params[:timeout]
564
+ @nc.request(@reply, Ack::Ack, **params)
565
+ else
566
+ @nc.publish(@reply, Ack::Ack)
567
+ end
568
+ @sub.synchronize { @ackd = true }
569
+
570
+ resp
571
+ end
572
+
573
+ def ack_sync(**params)
574
+ ensure_is_acked_once!
575
+
576
+ params[:timeout] ||= 0.5
577
+ resp = @nc.request(@reply, Ack::Ack, **params)
578
+ @sub.synchronize { @ackd = true }
579
+
580
+ resp
581
+ end
582
+
583
+ def nak(**params)
584
+ ensure_is_acked_once!
585
+
586
+ resp = if params[:timeout]
587
+ @nc.request(@reply, Ack::Nak, **params)
588
+ else
589
+ @nc.publish(@reply, Ack::Nak)
590
+ end
591
+ @sub.synchronize { @ackd = true }
592
+
593
+ resp
594
+ end
595
+
596
+ def term(**params)
597
+ ensure_is_acked_once!
598
+
599
+ resp = if params[:timeout]
600
+ @nc.request(@reply, Ack::Term, **params)
601
+ else
602
+ @nc.publish(@reply, Ack::Term)
603
+ end
604
+ @sub.synchronize { @ackd = true }
605
+
606
+ resp
607
+ end
608
+
609
+ def in_progress(**params)
610
+ params[:timeout] ? @nc.request(@reply, Ack::Progress, **params) : @nc.publish(@reply, Ack::Progress)
611
+ end
612
+
613
+ def metadata
614
+ @meta ||= parse_metadata(reply)
615
+ end
616
+
617
+ private
618
+
619
+ def ensure_is_acked_once!
620
+ @sub.synchronize { raise JetStream::Error::InvalidJSAck.new("nats: invalid ack") if @ackd }
621
+ end
622
+
623
+ def parse_metadata(reply)
624
+ tokens = reply.split(Ack::DotSep)
625
+ n = tokens.count
626
+
627
+ case
628
+ when n < Ack::V1TokenCounts || (n > Ack::V1TokenCounts and n < Ack::V2TokenCounts)
629
+ raise NotJSMessage.new("nats: not a jetstream message")
630
+ when tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
631
+ raise NotJSMessage.new("nats: not a jetstream message")
632
+ when n == Ack::V1TokenCounts
633
+ tokens.insert(Ack::Domain, Ack::Empty)
634
+ tokens.insert(Ack::AccHash, Ack::Empty)
635
+ when tokens[Ack::Domain] == Ack::NoDomainName
636
+ tokens[Ack::Domain] = Ack::Empty
637
+ end
638
+
639
+ Metadata.new(tokens)
640
+ end
641
+ end
642
+ end
643
+
644
+ ####################################
645
+ # #
646
+ # JetStream Configuration Options #
647
+ # #
648
+ ####################################
649
+
650
+ # Misc internal functions to support JS API.
651
+ # @private
652
+ module JS
653
+ DefaultAPIPrefix = ("$JS.API".freeze)
654
+
655
+ module Status
656
+ CtrlMsg = ("100".freeze)
657
+ NoMsgs = ("404".freeze)
658
+ NotFound = ("404".freeze)
659
+ RequestTimeout = ("408".freeze)
660
+ ServiceUnavailable = ("503".freeze)
661
+ end
662
+
663
+ module Header
664
+ Status = ("Status".freeze)
665
+ Desc = ("Description".freeze)
666
+ MsgID = ("Nats-Msg-Id".freeze)
667
+ ExpectedStream = ("Nats-Expected-Stream".freeze)
668
+ ExpectedLastSeq = ("Nats-Expected-Last-Sequence".freeze)
669
+ ExpectedLastSubjSeq = ("Nats-Expected-Last-Subject-Sequence".freeze)
670
+ ExpectedLastMsgID = ("Nats-Expected-Last-Msg-Id".freeze)
671
+ LastConsumerSeq = ("Nats-Last-Consumer".freeze)
672
+ LastStreamSeq = ("Nats-Last-Stream".freeze)
673
+ end
674
+
675
+ module Config
676
+ # AckPolicy
677
+ AckExplicit = ("explicit".freeze)
678
+ AckAll = ("all".freeze)
679
+ AckNone = ("none".freeze)
680
+ end
681
+
682
+ class Sub
683
+ attr_reader :js, :stream, :consumer, :nms
684
+
685
+ def initialize(opts={})
686
+ @js = opts[:js]
687
+ @stream = opts[:stream]
688
+ @consumer = opts[:consumer]
689
+ @nms = opts[:nms]
690
+ end
691
+ end
692
+
693
+ class << self
694
+ def next_req_to_json(next_req)
695
+ req = {}
696
+ req[:batch] = next_req[:batch]
697
+ req[:expires] = next_req[:expires].to_i if next_req[:expires]
698
+ req[:no_wait] = next_req[:no_wait] if next_req[:no_wait]
699
+ req.to_json
700
+ end
701
+
702
+ def is_status_msg(msg)
703
+ return (!msg.nil? and (!msg.header.nil? and msg.header[Header::Status]))
704
+ end
705
+
706
+ # check_503_error raises exception when a NATS::Msg has a 503 status header.
707
+ # @param msg [NATS::Msg] The message with status headers.
708
+ # @raise [NATS::JetStream::Error::ServiceUnavailable]
709
+ def check_503_error(msg)
710
+ return if msg.nil? or msg.header.nil?
711
+ if msg.header[Header::Status] == Status::ServiceUnavailable
712
+ raise ::NATS::JetStream::Error::ServiceUnavailable
713
+ end
714
+ end
715
+
716
+ # from_msg takes a plain NATS::Msg and checks its headers to confirm
717
+ # if it was an error:
718
+ #
719
+ # msg.header={"Status"=>"503"})
720
+ # msg.header={"Status"=>"408", "Description"=>"Request Timeout"})
721
+ #
722
+ # @param msg [NATS::Msg] The message with status headers.
723
+ # @return [NATS::JetStream::API::Error]
724
+ def from_msg(msg)
725
+ check_503_error(msg)
726
+ code = msg.header[JS::Header::Status]
727
+ desc = msg.header[JS::Header::Desc]
728
+ return ::NATS::JetStream::API::Error.new({code: code, description: desc})
729
+ end
730
+
731
+ # from_error takes an API response that errored and maps the error
732
+ # into a JetStream error type based on the status and error code.
733
+ def from_error(err)
734
+ return unless err
735
+ case err[:code]
736
+ when 503
737
+ ::NATS::JetStream::Error::ServiceUnavailable.new(err)
738
+ when 500
739
+ ::NATS::JetStream::Error::ServerError.new(err)
740
+ when 404
741
+ case err[:err_code]
742
+ when 10059
743
+ ::NATS::JetStream::Error::StreamNotFound.new(err)
744
+ when 10014
745
+ ::NATS::JetStream::Error::ConsumerNotFound.new(err)
746
+ else
747
+ ::NATS::JetStream::Error::NotFound.new(err)
748
+ end
749
+ when 400
750
+ ::NATS::JetStream::Error::BadRequest.new(err)
751
+ else
752
+ ::NATS::JetStream::API::Error.new(err)
753
+ end
754
+ end
755
+ end
756
+ end
757
+ private_constant :JS
758
+
759
+ #####################
760
+ # #
761
+ # JetStream Errors #
762
+ # #
763
+ #####################
764
+
765
+ # Error is any error that may arise when interacting with JetStream.
766
+ class Error < Error
767
+
768
+ # When there is a NATS::IO::NoResponders error after making a publish request.
769
+ class NoStreamResponse < Error; end
770
+
771
+ # When an invalid durable or consumer name was attempted to be used.
772
+ class InvalidDurableName < Error; end
773
+
774
+ # When an ack is no longer valid, for example when it has been acked already.
775
+ class InvalidJSAck < Error; end
776
+
777
+ # When the delivered message does not behave as a message delivered by JetStream,
778
+ # for example when the ack reply has unrecognizable fields.
779
+ class NotJSMessage < Error; end
780
+
781
+ # When the stream name is invalid.
782
+ class InvalidStreamName < Error; end
783
+
784
+ # When the consumer name is invalid.
785
+ class InvalidConsumerName < Error; end
786
+
787
+ # When the server responds with an error from the JetStream API.
788
+ class APIError < Error
789
+ attr_reader :code, :err_code, :description, :stream, :seq
790
+
791
+ def initialize(params={})
792
+ @code = params[:code]
793
+ @err_code = params[:err_code]
794
+ @description = params[:description]
795
+ @stream = params[:stream]
796
+ @seq = params[:seq]
797
+ end
798
+
799
+ def to_s
800
+ "#{@description} (status_code=#{@code}, err_code=#{@err_code})"
801
+ end
802
+ end
803
+
804
+ # When JetStream is not currently available, this could be due to JetStream
805
+ # not being enabled or temporarily unavailable due to a leader election when
806
+ # running in cluster mode.
807
+ # This condition is represented with a message that has 503 status code header.
808
+ class ServiceUnavailable < APIError
809
+ def initialize(params={})
810
+ super(params)
811
+ @code ||= 503
812
+ end
813
+ end
814
+
815
+ # When there is a hard failure in the JetStream.
816
+ # This condition is represented with a message that has 500 status code header.
817
+ class ServerError < APIError
818
+ def initialize(params={})
819
+ super(params)
820
+ @code ||= 500
821
+ end
822
+ end
823
+
824
+ # When a JetStream object was not found.
825
+ # This condition is represented with a message that has 404 status code header.
826
+ class NotFound < APIError
827
+ def initialize(params={})
828
+ super(params)
829
+ @code ||= 404
830
+ end
831
+ end
832
+
833
+ # When the stream is not found.
834
+ class StreamNotFound < NotFound; end
835
+
836
+ # When the consumer or durable is not found by name.
837
+ class ConsumerNotFound < NotFound; end
838
+
839
+ # When the JetStream client makes an invalid request.
840
+ # This condition is represented with a message that has 400 status code header.
841
+ class BadRequest < APIError
842
+ def initialize(params={})
843
+ super(params)
844
+ @code ||= 400
845
+ end
846
+ end
847
+ end
848
+
849
+ #######################
850
+ # #
851
+ # JetStream API Types #
852
+ # #
853
+ #######################
854
+
855
+ # JetStream::API are the types used to interact with the JetStream API.
856
+ module API
857
+ # When the server responds with an error from the JetStream API.
858
+ Error = ::NATS::JetStream::Error::APIError
859
+
860
+ # SequenceInfo is a pair of consumer and stream sequence and last activity.
861
+ # @!attribute consumer_seq
862
+ # @return [Integer] The consumer sequence.
863
+ # @!attribute stream_seq
864
+ # @return [Integer] The stream sequence.
865
+ SequenceInfo = Struct.new(:consumer_seq, :stream_seq, :last_active,
866
+ keyword_init: true) do
867
+ def initialize(opts={})
868
+ # Filter unrecognized fields and freeze.
869
+ rem = opts.keys - members
870
+ opts.delete_if { |k| rem.include?(k) }
871
+ super(opts)
872
+ freeze
873
+ end
874
+ end
875
+
876
+ # ConsumerInfo is the current status of a JetStream consumer.
877
+ #
878
+ # @!attribute stream_name
879
+ # @return [String] name of the stream to which the consumer belongs.
880
+ # @!attribute name
881
+ # @return [String] name of the consumer.
882
+ # @!attribute created
883
+ # @return [String] time when the consumer was created.
884
+ # @!attribute config
885
+ # @return [ConsumerConfig] consumer configuration.
886
+ # @!attribute delivered
887
+ # @return [SequenceInfo]
888
+ # @!attribute ack_floor
889
+ # @return [SequenceInfo]
890
+ # @!attribute num_ack_pending
891
+ # @return [Integer]
892
+ # @!attribute num_redelivered
893
+ # @return [Integer]
894
+ # @!attribute num_waiting
895
+ # @return [Integer]
896
+ # @!attribute num_pending
897
+ # @return [Integer]
898
+ # @!attribute cluster
899
+ # @return [Hash]
900
+ ConsumerInfo = Struct.new(:type, :stream_name, :name, :created,
901
+ :config, :delivered, :ack_floor,
902
+ :num_ack_pending, :num_redelivered, :num_waiting,
903
+ :num_pending, :cluster, :push_bound,
904
+ keyword_init: true) do
905
+ def initialize(opts={})
906
+ opts[:created] = Time.parse(opts[:created])
907
+ opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
908
+ opts[:delivered] = SequenceInfo.new(opts[:delivered])
909
+ opts[:config][:ack_wait] = opts[:config][:ack_wait] / 1_000_000_000
910
+ opts[:config] = ConsumerConfig.new(opts[:config])
911
+ opts.delete(:cluster)
912
+ # Filter unrecognized fields just in case.
913
+ rem = opts.keys - members
914
+ opts.delete_if { |k| rem.include?(k) }
915
+ super(opts)
916
+ freeze
917
+ end
918
+ end
919
+
920
+ # ConsumerConfig is the consumer configuration.
921
+ #
922
+ # @!attribute durable_name
923
+ # @return [String]
924
+ # @!attribute deliver_policy
925
+ # @return [String]
926
+ # @!attribute ack_policy
927
+ # @return [String]
928
+ # @!attribute ack_wait
929
+ # @return [Integer]
930
+ # @!attribute max_deliver
931
+ # @return [Integer]
932
+ # @!attribute replay_policy
933
+ # @return [String]
934
+ # @!attribute max_waiting
935
+ # @return [Integer]
936
+ # @!attribute max_ack_pending
937
+ # @return [Integer]
938
+ ConsumerConfig = Struct.new(:durable_name, :description, :deliver_subject,
939
+ :deliver_group, :deliver_policy, :opt_start_seq,
940
+ :opt_start_time, :ack_policy, :ack_wait, :max_deliver,
941
+ :filter_subject, :replay_policy, :rate_limit_bps,
942
+ :sample_freq, :max_waiting, :max_ack_pending,
943
+ :flow_control, :idle_heartbeat, :headers_only,
944
+ keyword_init: true) do
945
+ def initialize(opts={})
946
+ # Filter unrecognized fields just in case.
947
+ rem = opts.keys - members
948
+ opts.delete_if { |k| rem.include?(k) }
949
+ super(opts)
950
+ end
951
+
952
+ def to_json(*args)
953
+ config = self.to_h
954
+ config.delete_if { |_k, v| v.nil? }
955
+ config.to_json(*args)
956
+ end
957
+ end
958
+
959
+ # StreamConfig represents the configuration of a stream from JetStream.
960
+ #
961
+ # @!attribute type
962
+ # @return [String]
963
+ # @!attribute config
964
+ # @return [Hash]
965
+ # @!attribute created
966
+ # @return [String]
967
+ # @!attribute state
968
+ # @return [StreamState]
969
+ # @!attribute did_create
970
+ # @return [Boolean]
971
+ # @!attribute name
972
+ # @return [String]
973
+ # @!attribute subjects
974
+ # @return [Array]
975
+ # @!attribute retention
976
+ # @return [String]
977
+ # @!attribute max_consumers
978
+ # @return [Integer]
979
+ # @!attribute max_msgs
980
+ # @return [Integer]
981
+ # @!attribute max_bytes
982
+ # @return [Integer]
983
+ # @!attribute max_age
984
+ # @return [Integer]
985
+ # @!attribute max_msgs_per_subject
986
+ # @return [Integer]
987
+ # @!attribute max_msg_size
988
+ # @return [Integer]
989
+ # @!attribute discard
990
+ # @return [String]
991
+ # @!attribute storage
992
+ # @return [String]
993
+ # @!attribute num_replicas
994
+ # @return [Integer]
995
+ # @!attribute duplicate_window
996
+ # @return [Integer]
997
+ StreamConfig = Struct.new(:name, :subjects, :retention, :max_consumers,
998
+ :max_msgs, :max_bytes, :max_age,
999
+ :max_msgs_per_subject, :max_msg_size,
1000
+ :discard, :storage, :num_replicas, :duplicate_window,
1001
+ keyword_init: true) do
1002
+ def initialize(opts={})
1003
+ # Filter unrecognized fields just in case.
1004
+ rem = opts.keys - members
1005
+ opts.delete_if { |k| rem.include?(k) }
1006
+ super(opts)
1007
+ end
1008
+
1009
+ def to_json(*args)
1010
+ config = self.to_h
1011
+ config.delete_if { |_k, v| v.nil? }
1012
+ config.to_json(*args)
1013
+ end
1014
+ end
1015
+
1016
+ # StreamInfo is the info about a stream from JetStream.
1017
+ #
1018
+ # @!attribute type
1019
+ # @return [String]
1020
+ # @!attribute config
1021
+ # @return [Hash]
1022
+ # @!attribute created
1023
+ # @return [String]
1024
+ # @!attribute state
1025
+ # @return [Hash]
1026
+ # @!attribute domain
1027
+ # @return [String]
1028
+ StreamInfo = Struct.new(:type, :config, :created, :state, :domain,
1029
+ keyword_init: true) do
1030
+ def initialize(opts={})
1031
+ opts[:config] = StreamConfig.new(opts[:config])
1032
+ opts[:state] = StreamState.new(opts[:state])
1033
+ opts[:created] = Time.parse(opts[:created])
1034
+
1035
+ # Filter fields and freeze.
1036
+ rem = opts.keys - members
1037
+ opts.delete_if { |k| rem.include?(k) }
1038
+ super(opts)
1039
+ freeze
1040
+ end
1041
+ end
1042
+
1043
+ # StreamState is the state of a stream.
1044
+ #
1045
+ # @!attribute messages
1046
+ # @return [Integer]
1047
+ # @!attribute bytes
1048
+ # @return [Integer]
1049
+ # @!attribute first_seq
1050
+ # @return [Integer]
1051
+ # @!attribute last_seq
1052
+ # @return [Integer]
1053
+ # @!attribute consumer_count
1054
+ # @return [Integer]
1055
+ StreamState = Struct.new(:messages, :bytes, :first_seq, :first_ts,
1056
+ :last_seq, :last_ts, :consumer_count,
1057
+ keyword_init: true) do
1058
+ def initialize(opts={})
1059
+ rem = opts.keys - members
1060
+ opts.delete_if { |k| rem.include?(k) }
1061
+ super(opts)
1062
+ end
1063
+ end
1064
+
1065
+ # StreamCreateResponse is the response from the JetStream $JS.API.STREAM.CREATE API.
1066
+ #
1067
+ # @!attribute type
1068
+ # @return [String]
1069
+ # @!attribute config
1070
+ # @return [StreamConfig]
1071
+ # @!attribute created
1072
+ # @return [String]
1073
+ # @!attribute state
1074
+ # @return [StreamState]
1075
+ # @!attribute did_create
1076
+ # @return [Boolean]
1077
+ StreamCreateResponse = Struct.new(:type, :config, :created, :state, :did_create,
1078
+ keyword_init: true) do
1079
+ def initialize(opts={})
1080
+ rem = opts.keys - members
1081
+ opts.delete_if { |k| rem.include?(k) }
1082
+ opts[:config] = StreamConfig.new(opts[:config])
1083
+ opts[:state] = StreamState.new(opts[:state])
1084
+ super(opts)
1085
+ freeze
1086
+ end
1087
+ end
1088
+ end
1089
+ end
1090
+ end