nats-pure 2.6.0 → 2.7.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.
@@ -32,11 +32,28 @@ module NATS
32
32
  req[:batch] = next_req[:batch]
33
33
  req[:expires] = next_req[:expires].to_i if next_req[:expires]
34
34
  req[:no_wait] = next_req[:no_wait] if next_req[:no_wait]
35
+ # Priority groups (requires nats-server v2.11.0).
36
+ req.merge!(next_req.slice(:group, :min_pending, :min_ack_pending, :priority, :id).compact)
35
37
  req.to_json
36
38
  end
37
39
 
40
+ # parse_time parses a time from the server, which sends Go's zero
41
+ # time for a time that is not set.
42
+ def parse_time(time)
43
+ return if time.nil?
44
+
45
+ time = ::Time.parse(time)
46
+ time unless time.year == 1
47
+ end
48
+
49
+ # is_status_msg tells whether the server sent a message as a status:
50
+ # one with a Status header and no reply. A message of a stream can
51
+ # have a Status header too, but a consumer delivers it with a reply
52
+ # to ack it.
38
53
  def is_status_msg(msg)
39
- (!msg.nil? and (!msg.header.nil? and msg.header[Header::Status]))
54
+ return false if msg.nil? || msg.header.nil?
55
+
56
+ !msg.header[Header::Status].nil? && msg.reply.to_s.empty?
40
57
  end
41
58
 
42
59
  # check_503_error raises exception when a NATS::Msg has a 503 status header.
@@ -61,6 +78,8 @@ module NATS
61
78
  check_503_error(msg)
62
79
  code = msg.header[JS::Header::Status]
63
80
  desc = msg.header[JS::Header::Desc]
81
+ return ::NATS::JetStream::Error::PinIdMismatch.new({description: desc}) if code == Status::PinIdMismatch
82
+
64
83
  ::NATS::JetStream::API::Error.new({code: code, description: desc})
65
84
  end
66
85
 
@@ -83,7 +102,16 @@ module NATS
83
102
  ::NATS::JetStream::Error::NotFound.new(err)
84
103
  end
85
104
  when 400
86
- ::NATS::JetStream::Error::BadRequest.new(err)
105
+ case err[:err_code]
106
+ when 10148
107
+ ::NATS::JetStream::Error::ConsumerAlreadyExists.new(err)
108
+ when 10149
109
+ ::NATS::JetStream::Error::ConsumerDoesNotExist.new(err)
110
+ when 10204
111
+ ::NATS::JetStream::Error::ConsumerInvalidReset.new(err)
112
+ else
113
+ ::NATS::JetStream::Error::BadRequest.new(err)
114
+ end
87
115
  else
88
116
  ::NATS::JetStream::API::Error.new(err)
89
117
  end
@@ -28,6 +28,23 @@ module NATS
28
28
  #
29
29
  #
30
30
  module Manager
31
+ # The defaults of the stream settings of nats-server 2.11 to 2.14,
32
+ # which are not sent, as nats.go does not send them either, so that
33
+ # servers that do not know the settings take configs that leave them
34
+ # at their defaults. Nor is discard_new_per_subject at its default,
35
+ # which the client did not send before it knew the setting.
36
+ UNSENT_STREAM_DEFAULTS = {
37
+ discard_new_per_subject: [false],
38
+ allow_msg_ttl: [false],
39
+ subject_delete_marker_ttl: [0],
40
+ allow_msg_counter: [false],
41
+ allow_atomic: [false],
42
+ allow_msg_schedules: [false],
43
+ persist_mode: ["", "default"],
44
+ allow_batched: [false]
45
+ }.freeze
46
+ private_constant :UNSENT_STREAM_DEFAULTS
47
+
31
48
  # add_stream creates a stream with a given config.
32
49
  # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
33
50
  # @param params [Hash] Options to customize API request.
@@ -44,8 +61,7 @@ module NATS
44
61
  raise ArgumentError.new("Spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited in stream names") if stream =~ /(\s|\.|>|\*)/
45
62
  req_subject = "#{@prefix}.STREAM.CREATE.#{stream}"
46
63
 
47
- cfg = config.to_h.compact
48
- result = api_request(req_subject, cfg.to_json, params)
64
+ result = api_request(req_subject, stream_config_json(config), params)
49
65
  JetStream::API::StreamCreateResponse.new(result)
50
66
  end
51
67
 
@@ -62,7 +78,11 @@ module NATS
62
78
  JetStream::API::StreamInfo.new(result)
63
79
  end
64
80
 
65
- # update_stream edits an existed stream with a given config.
81
+ # update_stream edits an existed stream with a given config. The config
82
+ # replaces the stream's, so pass all of it, as from stream_info.
83
+ # Updating a mirror without its mirror setting makes it a stream of its
84
+ # own for good, no longer a mirror (nats-server v2.12.0; older servers
85
+ # refuse the update).
66
86
  # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
67
87
  # @param params [Hash] Options to customize API request.
68
88
  # @option params [Float] :timeout Time to wait for response.
@@ -77,8 +97,7 @@ module NATS
77
97
  raise ArgumentError.new(":name is required to create streams") unless stream
78
98
  raise ArgumentError.new("Spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited in stream names") if stream =~ /(\s|\.|>|\*)/
79
99
  req_subject = "#{@prefix}.STREAM.UPDATE.#{stream}"
80
- cfg = config.to_h.compact
81
- result = api_request(req_subject, cfg.to_json, params)
100
+ result = api_request(req_subject, stream_config_json(config), params)
82
101
  JetStream::API::StreamCreateResponse.new(result)
83
102
  end
84
103
 
@@ -95,75 +114,45 @@ module NATS
95
114
  result[:success]
96
115
  end
97
116
 
98
- # add_consumer creates a consumer with a given config.
117
+ # add_consumer creates a consumer with a given config, or updates the
118
+ # consumer if one with the same name already exists.
99
119
  # @param stream [String] Name of the stream.
100
120
  # @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
101
121
  # @param params [Hash] Options to customize API request.
102
122
  # @option params [Float] :timeout Time to wait for response.
103
123
  # @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
104
124
  def add_consumer(stream, config, params = {})
105
- raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? || stream.empty?
106
- config = if !config.is_a?(JetStream::API::ConsumerConfig)
107
- JetStream::API::ConsumerConfig.new(config)
108
- else
109
- config
110
- end
111
- config[:name] ||= config[:durable_name]
112
- req_subject = if config[:name]
113
- ###############################################################################
114
- # #
115
- # Using names is the supported way of creating consumers (NATS +v2.9.0. #
116
- # #
117
- ###############################################################################
118
- if config[:filter_subject] && config[:filter_subject] != ">"
119
- "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
120
- else
121
- ##############################################################################
122
- # #
123
- # Endpoint to support creating ANY consumer with multi-filters (NATS +v2.10) #
124
- # #
125
- ##############################################################################
126
- "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
127
- end
128
- elsif config[:durable_name]
129
- ###############################################################################
130
- # #
131
- # Endpoint to support creating DURABLES before NATS v2.9.0. #
132
- # #
133
- ###############################################################################
134
- "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
135
- else
136
- ###############################################################################
137
- # #
138
- # Endpoint to support creating EPHEMERALS before NATS v2.9.0. #
139
- # #
140
- ###############################################################################
141
- "#{@prefix}.CONSUMER.CREATE.#{stream}"
142
- end
143
-
144
- config[:ack_policy] ||= JS::Config::AckExplicit
145
- # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
146
- if config[:ack_wait]
147
- raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
148
- config[:ack_wait] = config[:ack_wait] * ::NATS::NANOSECONDS
149
- end
150
- if config[:inactive_threshold]
151
- raise ArgumentError.new("nats: invalid inactive threshold") unless config[:inactive_threshold].is_a?(Integer)
152
- config[:inactive_threshold] = config[:inactive_threshold] * ::NATS::NANOSECONDS
153
- end
154
- if config[:idle_heartbeat]
155
- raise ArgumentError.new("nats: invalid idle heartbeat") unless config[:idle_heartbeat].is_a?(Integer)
156
- config[:idle_heartbeat] = config[:idle_heartbeat] * ::NATS::NANOSECONDS
157
- end
125
+ upsert_consumer(stream, config, nil, params)
126
+ end
158
127
 
159
- cfg = config.to_h.compact
160
- req = {
161
- stream_name: stream,
162
- config: cfg
163
- }
128
+ # create_consumer creates a consumer with a given config. Creating a
129
+ # consumer that already exists with the same config succeeds; one that
130
+ # exists with a different config raises ConsumerAlreadyExists.
131
+ # Requires nats-server v2.10.0.
132
+ # @param stream [String] Name of the stream.
133
+ # @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
134
+ # @param params [Hash] Options to customize API request.
135
+ # @option params [Float] :timeout Time to wait for response.
136
+ # @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
137
+ def create_consumer(stream, config, params = {})
138
+ upsert_consumer(stream, config, "create", params)
139
+ end
164
140
 
165
- result = api_request(req_subject, req.to_json, params)
166
- JetStream::API::ConsumerInfo.new(result).freeze
141
+ # update_consumer replaces the config of an existing consumer, named by
142
+ # the config's name or durable_name, so fields left out take their
143
+ # defaults: pass the whole config, with the changes. The config from
144
+ # consumer_info lacks the settings this client does not know, and has
145
+ # its durations rounded down to whole seconds. A consumer that does
146
+ # not exist raises ConsumerDoesNotExist; the server decides which
147
+ # fields may change, and keeps the pause of the consumer, which
148
+ # pause_consumer and resume_consumer change. Requires nats-server v2.10.0.
149
+ # @param stream [String] Name of the stream.
150
+ # @param config [JetStream::API::ConsumerConfig] New configuration of the consumer.
151
+ # @param params [Hash] Options to customize API request.
152
+ # @option params [Float] :timeout Time to wait for response.
153
+ # @return [JetStream::API::ConsumerInfo] The updated Consumer.
154
+ def update_consumer(stream, config, params = {})
155
+ upsert_consumer(stream, config, "update", params)
167
156
  end
168
157
 
169
158
  # consumer_info retrieves the current status of a consumer.
@@ -196,6 +185,96 @@ module NATS
196
185
  result[:success]
197
186
  end
198
187
 
188
+ # pause_consumer pauses a consumer, so that it delivers no messages
189
+ # until the given time. Requires nats-server v2.11.0: older servers
190
+ # let the request time out.
191
+ # @param stream [String] Name of the stream.
192
+ # @param consumer [String] Name of the consumer.
193
+ # @param pause_until [Time, String] When to resume, as a Time or as an RFC 3339 String.
194
+ # @param params [Hash] Options to customize API request.
195
+ # @option params [Float] :timeout Time to wait for response.
196
+ # @return [JetStream::API::ConsumerPauseResponse]
197
+ # @raise [ArgumentError] When pause_until is nil: resume_consumer resumes.
198
+ # @raise [JetStream::Error::StreamNotFound] When the stream does not exist.
199
+ # @raise [JetStream::Error::ConsumerNotFound] When the consumer does not exist.
200
+ # @raise [NATS::Timeout] With a server before v2.11.0.
201
+ def pause_consumer(stream, consumer, pause_until, params = {})
202
+ raise ArgumentError.new("nats: a time to pause until is required, resume_consumer resumes") if pause_until.nil?
203
+
204
+ request_pause(stream, consumer, {pause_until: rfc3339(pause_until)}, params)
205
+ end
206
+
207
+ # resume_consumer resumes a paused consumer. Requires nats-server
208
+ # v2.11.0: older servers let the request time out.
209
+ # @param stream [String] Name of the stream.
210
+ # @param consumer [String] Name of the consumer.
211
+ # @param params [Hash] Options to customize API request.
212
+ # @option params [Float] :timeout Time to wait for response.
213
+ # @return [JetStream::API::ConsumerPauseResponse]
214
+ # @raise [JetStream::Error::StreamNotFound] When the stream does not exist.
215
+ # @raise [JetStream::Error::ConsumerNotFound] When the consumer does not exist.
216
+ # @raise [NATS::Timeout] With a server before v2.11.0.
217
+ def resume_consumer(stream, consumer, params = {})
218
+ request_pause(stream, consumer, {}, params)
219
+ end
220
+
221
+ # unpin_consumer unpins a priority group of a consumer with the
222
+ # pinned_client priority policy from its subscription, so that the
223
+ # next subscription to pull is pinned instead. The server tells the
224
+ # subscription that was pinned once it has a message for its pull, and
225
+ # its fetch then raises PinIdMismatch; without messages, the fetch
226
+ # times out. Requires nats-server v2.11.0.
227
+ # @param stream [String] Name of the stream.
228
+ # @param consumer [String] Name of the consumer.
229
+ # @param group [String] Name of the priority group.
230
+ # @param params [Hash] Options to customize API request.
231
+ # @option params [Float] :timeout Time to wait for response.
232
+ # @return [Boolean]
233
+ # @raise [JetStream::Error::StreamNotFound] When the stream does not exist.
234
+ # @raise [JetStream::Error::ConsumerNotFound] When the consumer does not exist.
235
+ # @raise [JetStream::Error::BadRequest] When the consumer has no such group.
236
+ # @raise [NATS::Timeout] With a server before v2.11.0.
237
+ def unpin_consumer(stream, consumer, group, params = {})
238
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? || stream.empty?
239
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? || consumer.empty?
240
+
241
+ req_subject = "#{@prefix}.CONSUMER.UNPIN.#{stream}.#{consumer}"
242
+ api_request(req_subject, {group: group}.to_json, params)
243
+ true
244
+ end
245
+
246
+ # reset_consumer resets the delivery state of a consumer, as if it had
247
+ # been created again to start from a stream sequence: by default the
248
+ # one after its ack floor, so that it redelivers the messages that
249
+ # await acks. Only a consumer that delivers all messages, or from a
250
+ # start sequence or time, can be given a sequence, and not one before
251
+ # its start. Requires nats-server v2.14.0: an older server, like a
252
+ # stream or consumer that does not exist, lets the request time out.
253
+ # @param stream [String] Name of the stream.
254
+ # @param consumer [String] Name of the consumer.
255
+ # @param params [Hash] Options to customize API request.
256
+ # @option params [Integer] :seq Stream sequence to deliver from; 0, like
257
+ # none, for the one after the ack floor.
258
+ # @option params [Float] :timeout Time to wait for response.
259
+ # @return [JetStream::API::ConsumerResetResponse]
260
+ # @raise [ArgumentError] When seq is not an integer of 0 or more.
261
+ # @raise [JetStream::Error::ConsumerInvalidReset] When the consumer cannot be reset to seq.
262
+ # @raise [NATS::Timeout] When the stream or consumer does not exist, or the server
263
+ # cannot reset consumers.
264
+ def reset_consumer(stream, consumer, params = {})
265
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? || stream.empty?
266
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? || consumer.empty?
267
+ seq = params[:seq]
268
+ if !seq.nil? && !(seq.is_a?(Integer) && seq >= 0)
269
+ raise ArgumentError.new("nats: seq should be an integer of 0 or more")
270
+ end
271
+
272
+ req_subject = "#{@prefix}.CONSUMER.RESET.#{stream}.#{consumer}"
273
+ req = {seq: seq}.compact
274
+ result = api_request(req_subject, req.to_json, params.except(:seq))
275
+ JetStream::API::ConsumerResetResponse.new(result)
276
+ end
277
+
199
278
  # find_stream_name_by_subject does a lookup for the stream to which
200
279
  # the subject belongs.
201
280
  # @param subject [String] The subject that belongs to a stream.
@@ -260,6 +339,102 @@ module NATS
260
339
 
261
340
  private
262
341
 
342
+ # upsert_consumer sends a consumer create request. The action is
343
+ # "create", "update", or nil to create or update.
344
+ def upsert_consumer(stream, config, action, params)
345
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? || stream.empty?
346
+ # Work on a copy: the durations are converted to nanoseconds below,
347
+ # and the caller may send the same config again.
348
+ config = if !config.is_a?(JetStream::API::ConsumerConfig)
349
+ JetStream::API::ConsumerConfig.new(config)
350
+ else
351
+ config.dup
352
+ end
353
+ config[:name] ||= config[:durable_name]
354
+ if action == "update" && (config[:name].nil? || config[:name].empty?)
355
+ raise ArgumentError.new("nats: the consumer to update needs a name or durable name")
356
+ end
357
+ req_subject = if config[:name]
358
+ ###############################################################################
359
+ # #
360
+ # Using names is the supported way of creating consumers (NATS +v2.9.0. #
361
+ # #
362
+ ###############################################################################
363
+ if config[:filter_subject] && config[:filter_subject] != ">"
364
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
365
+ else
366
+ ##############################################################################
367
+ # #
368
+ # Endpoint to support creating ANY consumer with multi-filters (NATS +v2.10) #
369
+ # #
370
+ ##############################################################################
371
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
372
+ end
373
+ elsif config[:durable_name]
374
+ ###############################################################################
375
+ # #
376
+ # Endpoint to support creating DURABLES before NATS v2.9.0. #
377
+ # #
378
+ ###############################################################################
379
+ "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
380
+ else
381
+ ###############################################################################
382
+ # #
383
+ # Endpoint to support creating EPHEMERALS before NATS v2.9.0. #
384
+ # #
385
+ ###############################################################################
386
+ "#{@prefix}.CONSUMER.CREATE.#{stream}"
387
+ end
388
+
389
+ config[:ack_policy] ||= JS::Config::AckExplicit
390
+ # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
391
+ if config[:ack_wait]
392
+ raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
393
+ config[:ack_wait] = config[:ack_wait] * ::NATS::NANOSECONDS
394
+ end
395
+ if config[:inactive_threshold]
396
+ raise ArgumentError.new("nats: invalid inactive threshold") unless config[:inactive_threshold].is_a?(Integer)
397
+ config[:inactive_threshold] = config[:inactive_threshold] * ::NATS::NANOSECONDS
398
+ end
399
+ if config[:idle_heartbeat]
400
+ raise ArgumentError.new("nats: invalid idle heartbeat") unless config[:idle_heartbeat].is_a?(Integer)
401
+ config[:idle_heartbeat] = config[:idle_heartbeat] * ::NATS::NANOSECONDS
402
+ end
403
+ if config[:priority_timeout]
404
+ raise ArgumentError.new("nats: invalid priority timeout") unless config[:priority_timeout].is_a?(Integer)
405
+ config[:priority_timeout] = config[:priority_timeout] * ::NATS::NANOSECONDS
406
+ end
407
+ config[:pause_until] = rfc3339(config[:pause_until])
408
+
409
+ cfg = config.to_h.compact
410
+ req = {
411
+ stream_name: stream,
412
+ config: cfg
413
+ }
414
+ # Without an action the server creates or updates.
415
+ req[:action] = action if action
416
+
417
+ result = api_request(req_subject, req.to_json, params)
418
+ JetStream::API::ConsumerInfo.new(result).freeze
419
+ end
420
+
421
+ # request_pause sends a consumer pause request, which resumes the
422
+ # consumer when it has no pause time.
423
+ def request_pause(stream, consumer, req, params)
424
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? || stream.empty?
425
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? || consumer.empty?
426
+
427
+ req_subject = "#{@prefix}.CONSUMER.PAUSE.#{stream}.#{consumer}"
428
+ result = api_request(req_subject, req.to_json, params)
429
+ JetStream::API::ConsumerPauseResponse.new(result)
430
+ end
431
+
432
+ # rfc3339 formats a Time as the server expects it; a String is
433
+ # sent as is.
434
+ def rfc3339(time)
435
+ time.is_a?(Time) ? time.getutc.iso8601(9) : time
436
+ end
437
+
263
438
  def api_request(req_subject, req = "", params = {})
264
439
  params[:timeout] ||= @opts[:timeout]
265
440
  msg = begin
@@ -300,6 +475,11 @@ module NATS
300
475
 
301
476
  raw_msg
302
477
  end
478
+
479
+ # stream_config_json makes the request to create or update a stream.
480
+ def stream_config_json(config)
481
+ config.to_h.compact.reject { |key, value| UNSENT_STREAM_DEFAULTS[key]&.include?(value) }.to_json
482
+ end
303
483
  end
304
484
  end
305
485
  end
@@ -58,13 +58,19 @@ module NATS
58
58
  resp
59
59
  end
60
60
 
61
+ # term tells the server not to redeliver the message. A :reason is
62
+ # passed on in the server's terminated advisory. Servers before
63
+ # v2.10.4 do not terminate a message given a reason, so it will
64
+ # be redelivered.
61
65
  def term(**params)
62
66
  ensure_is_acked_once!
63
67
 
68
+ reason = params.delete(:reason)
69
+ payload = reason.to_s.strip.empty? ? Ack::Term : "#{Ack::Term} #{reason}"
64
70
  resp = if params[:timeout]
65
- @nc.request(@reply, Ack::Term, **params)
71
+ @nc.request(@reply, payload, **params)
66
72
  else
67
- @nc.publish(@reply, Ack::Term)
73
+ @nc.publish(@reply, payload)
68
74
  end
69
75
  @sub.synchronize { @ackd = true }
70
76