nats-pure 2.1.2 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 154ce22f3fa8a4256d0a0bc45fd3356286956c6f774f8332e5892c0be6af45d8
4
- data.tar.gz: de9022e08deff77f48c3ef8dd4e538d307ddc44eea57ac884b862893df354750
3
+ metadata.gz: f1d8302826b885f0c3a173530bd3d504b5e7a4eb7ec684ac3b43b1ee8f9cf4e3
4
+ data.tar.gz: 04aa8801b7ddf434155d1ebf40b03fdd6b89bfc6f95d968ff4a501684081ca02
5
5
  SHA512:
6
- metadata.gz: 92f10f12b63062c756a5b041e1e2f604d23f7b372d9c3a22425d91e0cb2e6d0ff09f805722cdbe30eb6599566ec3715006127a144c2e9df92724dd307dc2d8b7
7
- data.tar.gz: 3ec16e14c8ecfc15d9d8f00c2af4413c7ca35f372ac5ca38d7577df9629ca0b22a7885b0abd7c9665e9004b8c1592823ce23bb37285b3609a320c086efc98be9
6
+ metadata.gz: a98b75d47e4b8256c05d284324399807e0bbaf210f2970856b106b46a1264cc12bd96cb00516cb007fd9c68cab001bf08516aee1db46718552bd7397cf8ccb2f
7
+ data.tar.gz: b752276fa498c423ecdd0002e530639514ef39a1f5c93e73606c6a6f0351cf275817e13c0339d880fadef9e956fdf7b1f63cc816ff0cd2a2e254db73e071344c
@@ -1936,6 +1936,8 @@ module NATS
1936
1936
  end
1937
1937
  end
1938
1938
 
1939
+ NANOSECONDS = 1_000_000_000
1940
+
1939
1941
  class MonotonicTime
1940
1942
  # Implementation of MonotonicTime adapted from
1941
1943
  # https://github.com/ruby-concurrency/concurrent-ruby/
data/lib/nats/io/js.rb CHANGED
@@ -19,7 +19,6 @@ require 'time'
19
19
  require 'base64'
20
20
 
21
21
  module NATS
22
-
23
22
  # JetStream returns a context with a similar API as the NATS::Client
24
23
  # but with enhanced functions to persist and consume messages from
25
24
  # the NATS JetStream engine.
@@ -185,7 +184,7 @@ module NATS
185
184
  config.flow_control = flow_control
186
185
  if idle_heartbeat or config.idle_heartbeat
187
186
  idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
188
- idle_heartbeat = idle_heartbeat * 1_000_000_000
187
+ idle_heartbeat = idle_heartbeat * ::NATS::NANOSECONDS
189
188
  config.idle_heartbeat = idle_heartbeat
190
189
  end
191
190
 
@@ -223,7 +222,9 @@ module NATS
223
222
  # @option params [Hash] :config Configuration for the consumer.
224
223
  # @return [NATS::JetStream::PullSubscription]
225
224
  def pull_subscribe(subject, durable, params={})
226
- raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name") if durable.empty?
225
+ if durable.empty? && !params[:consumer]
226
+ raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name")
227
+ end
227
228
  params[:consumer] ||= durable
228
229
  stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
229
230
 
@@ -330,7 +331,16 @@ module NATS
330
331
  else
331
332
  config
332
333
  end
333
- req_subject = if config[:durable_name]
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]
334
344
  "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
335
345
  else
336
346
  "#{@prefix}.CONSUMER.CREATE.#{stream}"
@@ -340,7 +350,11 @@ module NATS
340
350
  # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
341
351
  if config[:ack_wait]
342
352
  raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
343
- config[:ack_wait] = config[:ack_wait] * 1_000_000_000
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
344
358
  end
345
359
 
346
360
  req = {
@@ -397,28 +411,96 @@ module NATS
397
411
  result[:streams].first
398
412
  end
399
413
 
400
- def get_last_msg(stream_name, subject)
401
- req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
402
- req = {'last_by_subj': subject}
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
+
403
431
  data = req.to_json
404
- resp = api_request(req_subject, data)
405
- JetStream::API::RawStreamMsg.new(resp[:message])
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")
406
460
  end
407
461
 
408
462
  private
409
463
 
410
464
  def api_request(req_subject, req="", params={})
411
465
  params[:timeout] ||= @opts[:timeout]
412
- result = begin
413
- msg = @nc.request(req_subject, req, **params)
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
414
475
  JSON.parse(msg.data, symbolize_names: true)
415
- rescue NATS::IO::NoRespondersError
416
- raise JetStream::Error::ServiceUnavailable
417
476
  end
418
- raise JS.from_error(result[:error]) if result[:error]
477
+ if result.is_a?(Hash) and result[:error]
478
+ raise JS.from_error(result[:error])
479
+ end
419
480
 
420
481
  result
421
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
422
504
  end
423
505
 
424
506
  # PushSubscription is included into NATS::Subscription so that it
@@ -1107,7 +1189,7 @@ module NATS
1107
1189
  opts[:created] = Time.parse(opts[:created])
1108
1190
  opts[:ack_floor] = SequenceInfo.new(opts[:ack_floor])
1109
1191
  opts[:delivered] = SequenceInfo.new(opts[:delivered])
1110
- opts[:config][:ack_wait] = opts[:config][:ack_wait] / 1_000_000_000
1192
+ opts[:config][:ack_wait] = opts[:config][:ack_wait] / ::NATS::NANOSECONDS
1111
1193
  opts[:config] = ConsumerConfig.new(opts[:config])
1112
1194
  opts.delete(:cluster)
1113
1195
  # Filter unrecognized fields just in case.
@@ -1136,7 +1218,7 @@ module NATS
1136
1218
  # @return [Integer]
1137
1219
  # @!attribute max_ack_pending
1138
1220
  # @return [Integer]
1139
- ConsumerConfig = Struct.new(:durable_name, :description,
1221
+ ConsumerConfig = Struct.new(:name, :durable_name, :description,
1140
1222
  :deliver_policy, :opt_start_seq, :opt_start_time,
1141
1223
  :ack_policy, :ack_wait, :max_deliver, :backoff,
1142
1224
  :filter_subject, :replay_policy, :rate_limit_bps,
@@ -1153,7 +1235,7 @@ module NATS
1153
1235
  # now can be configured directly.
1154
1236
  :num_replicas,
1155
1237
  # Force memory storage
1156
- :memory_storage,
1238
+ :mem_storage,
1157
1239
  keyword_init: true) do
1158
1240
  def initialize(opts={})
1159
1241
  # Filter unrecognized fields just in case.
@@ -1207,11 +1289,32 @@ module NATS
1207
1289
  # @return [Integer]
1208
1290
  # @!attribute duplicate_window
1209
1291
  # @return [Integer]
1210
- StreamConfig = Struct.new(:name, :description, :subjects, :retention, :max_consumers,
1211
- :max_msgs, :max_bytes, :discard, :max_age,
1212
- :max_msgs_per_subject, :max_msg_size,
1213
- :storage, :num_replicas, :no_ack, :duplicate_window,
1214
- :placement, :allow_direct,
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,
1215
1318
  keyword_init: true) do
1216
1319
  def initialize(opts={})
1217
1320
  # Filter unrecognized fields just in case.
@@ -1244,7 +1347,7 @@ module NATS
1244
1347
  def initialize(opts={})
1245
1348
  opts[:config] = StreamConfig.new(opts[:config])
1246
1349
  opts[:state] = StreamState.new(opts[:state])
1247
- opts[:created] = Time.parse(opts[:created])
1350
+ opts[:created] = ::Time.parse(opts[:created])
1248
1351
 
1249
1352
  # Filter fields and freeze.
1250
1353
  rem = opts.keys - members
data/lib/nats/io/kv.rb CHANGED
@@ -20,46 +20,181 @@ module NATS
20
20
  KV_PURGE = "PURGE"
21
21
  MSG_ROLLUP_SUBJECT = "sub"
22
22
  MSG_ROLLUP_ALL = "all"
23
+ ROLLUP = "Nats-Rollup"
23
24
 
24
25
  def initialize(opts={})
25
26
  @name = opts[:name]
26
27
  @stream = opts[:stream]
27
28
  @pre = opts[:pre]
28
29
  @js = opts[:js]
30
+ @direct = opts[:direct]
31
+ end
32
+
33
+ class Error < NATS::Error; end
34
+
35
+ # When a key is not found.
36
+ class KeyNotFoundError < Error
37
+ attr_reader :entry, :op
38
+ def initialize(params={})
39
+ @entry = params[:entry]
40
+ @op = params[:op]
41
+ @message = params[:message]
42
+ end
43
+
44
+ def to_s
45
+ msg = "nats: key not found"
46
+ msg = "#{msg}: #{@message}" if @message
47
+ msg
48
+ end
29
49
  end
30
50
 
31
51
  # When a key is not found because it was deleted.
32
- class KeyDeletedError < NATS::Error; end
52
+ class KeyDeletedError < KeyNotFoundError
53
+ def to_s
54
+ "nats: key was deleted"
55
+ end
56
+ end
33
57
 
34
58
  # When there was no bucket present.
35
- class BucketNotFoundError < NATS::Error; end
59
+ class BucketNotFoundError < Error; end
36
60
 
37
61
  # When it is an invalid bucket.
38
- class BadBucketError < NATS::Error; end
62
+ class BadBucketError < Error; end
63
+
64
+ # When the result is an unexpected sequence.
65
+ class KeyWrongLastSequenceError < Error
66
+ def initialize(msg)
67
+ @msg = msg
68
+ end
69
+ def to_s
70
+ "nats: #{@msg}"
71
+ end
72
+ end
39
73
 
40
74
  # get returns the latest value for the key.
41
- def get(key)
42
- msg = @js.get_last_msg(@stream, "#{@pre}#{key}")
75
+ def get(key, params={})
76
+ entry = nil
77
+ begin
78
+ entry = _get(key, params)
79
+ rescue KeyDeletedError
80
+ raise KeyNotFoundError
81
+ end
82
+
83
+ entry
84
+ end
85
+
86
+ def _get(key, params={})
87
+ msg = nil
88
+ subject = "#{@pre}#{key}"
89
+
90
+ if params[:revision]
91
+ msg = @js.get_msg(@stream,
92
+ seq: params[:revision],
93
+ direct: @direct)
94
+ else
95
+ msg = @js.get_msg(@stream,
96
+ subject: subject,
97
+ seq: params[:revision],
98
+ direct: @direct)
99
+ end
100
+
43
101
  entry = Entry.new(bucket: @name, key: key, value: msg.data, revision: msg.seq)
44
102
 
103
+ if subject != msg.subject
104
+ raise KeyNotFoundError.new(
105
+ entry: entry,
106
+ message: "expected '#{subject}', but got '#{msg.subject}'"
107
+ )
108
+ end
109
+
45
110
  if not msg.headers.nil?
46
111
  op = msg.headers[KV_OP]
47
- raise KeyDeletedError.new("nats: key was deleted") if op == KV_DEL or op == KV_PURGE
112
+ if op == KV_DEL or op == KV_PURGE
113
+ raise KeyDeletedError.new(entry: entry, op: op)
114
+ end
48
115
  end
49
116
 
50
117
  entry
118
+ rescue NATS::JetStream::Error::NotFound
119
+ raise KeyNotFoundError
51
120
  end
121
+ private :_get
52
122
 
53
123
  # put will place the new value for the key into the store
54
124
  # and return the revision number.
55
125
  def put(key, value)
56
- @js.publish("#{@pre}#{key}", value)
126
+ ack = @js.publish("#{@pre}#{key}", value)
127
+ ack.seq
128
+ end
129
+
130
+ # create will add the key/value pair iff it does not exist.
131
+ def create(key, value)
132
+ pa = nil
133
+ begin
134
+ pa = update(key, value, last: 0)
135
+ rescue KeyWrongLastSequenceError => err
136
+ # In case of attempting to recreate an already deleted key,
137
+ # the client would get a KeyWrongLastSequenceError. When this happens,
138
+ # it is needed to fetch latest revision number and attempt to update.
139
+ begin
140
+ # NOTE: This reimplements the following behavior from Go client.
141
+ #
142
+ # Since we have tombstones for DEL ops for watchers, this could be from that
143
+ # so we need to double check.
144
+ #
145
+ _get(key)
146
+
147
+ # No exception so not a deleted key, so reraise the original KeyWrongLastSequenceError.
148
+ # If it was deleted then the error exception will contain metadata
149
+ # to recreate using the last revision.
150
+ raise err
151
+ rescue KeyDeletedError => err
152
+ pa = update(key, value, last: err.entry.revision)
153
+ end
154
+ end
155
+
156
+ pa
157
+ end
158
+
159
+ EXPECTED_LAST_SUBJECT_SEQUENCE = "Nats-Expected-Last-Subject-Sequence"
160
+
161
+ # update will update the value iff the latest revision matches.
162
+ def update(key, value, params={})
163
+ hdrs = {}
164
+ last = (params[:last] ||= 0)
165
+ hdrs[EXPECTED_LAST_SUBJECT_SEQUENCE] = last.to_s
166
+ ack = nil
167
+ begin
168
+ ack = @js.publish("#{@pre}#{key}", value, header: hdrs)
169
+ rescue NATS::JetStream::Error::APIError => err
170
+ if err.err_code == 10071
171
+ raise KeyWrongLastSequenceError.new(err.description)
172
+ else
173
+ raise err
174
+ end
175
+ end
176
+
177
+ ack.seq
57
178
  end
58
179
 
59
180
  # delete will place a delete marker and remove all previous revisions.
60
- def delete(key)
181
+ def delete(key, params={})
61
182
  hdrs = {}
62
183
  hdrs[KV_OP] = KV_DEL
184
+ last = (params[:last] ||= 0)
185
+ if last > 0
186
+ hdrs[EXPECTED_LAST_SUBJECT_SEQUENCE] = last.to_s
187
+ end
188
+ ack = @js.publish("#{@pre}#{key}", header: hdrs)
189
+
190
+ ack.seq
191
+ end
192
+
193
+ # purge will remove the key and all revisions.
194
+ def purge(key)
195
+ hdrs = {}
196
+ hdrs[KV_OP] = KV_PURGE
197
+ hdrs[ROLLUP] = MSG_ROLLUP_SUBJECT
63
198
  @js.publish("#{@pre}#{key}", header: hdrs)
64
199
  end
65
200
 
@@ -69,7 +204,7 @@ module NATS
69
204
  BucketStatus.new(info, @name)
70
205
  end
71
206
 
72
- Entry = Struct.new(:bucket, :key, :value, :revision, keyword_init: true) do
207
+ Entry = Struct.new(:bucket, :key, :value, :revision, :delta, :created, :operation, keyword_init: true) do
73
208
  def initialize(opts={})
74
209
  rem = opts.keys - members
75
210
  opts.delete_if { |k| rem.include?(k) }
@@ -94,14 +229,24 @@ module NATS
94
229
  end
95
230
 
96
231
  def ttl
97
- @nfo.config.max_age / 1_000_000_000
232
+ @nfo.config.max_age / ::NATS::NANOSECONDS
98
233
  end
99
234
  end
100
-
101
- module API
102
- KeyValueConfig = Struct.new(:bucket, :description, :max_value_size,
103
- :history, :ttl, :max_bytes, :storage, :replicas,
104
- keyword_init: true) do
235
+
236
+ module API
237
+ KeyValueConfig = Struct.new(
238
+ :bucket,
239
+ :description,
240
+ :max_value_size,
241
+ :history,
242
+ :ttl,
243
+ :max_bytes,
244
+ :storage,
245
+ :replicas,
246
+ :placement,
247
+ :republish,
248
+ :direct,
249
+ keyword_init: true) do
105
250
  def initialize(opts={})
106
251
  rem = opts.keys - members
107
252
  opts.delete_if { |k| rem.include?(k) }
@@ -127,40 +272,53 @@ module NATS
127
272
  stream: stream,
128
273
  pre: "$KV.#{bucket}.",
129
274
  js: self,
275
+ direct: si.config.allow_direct
130
276
  )
131
277
  end
132
278
 
133
279
  def create_key_value(config)
134
- config = if not config.is_a?(JetStream::API::StreamConfig)
280
+ config = if not config.is_a?(KeyValue::API::KeyValueConfig)
135
281
  KeyValue::API::KeyValueConfig.new(config)
136
282
  else
137
283
  config
138
284
  end
139
285
  config.history ||= 1
140
286
  config.replicas ||= 1
287
+ duplicate_window = 2 * 60 # 2 minutes
141
288
  if config.ttl
142
- config.ttl = config.ttl * 1_000_000_000
289
+ if config.ttl < duplicate_window
290
+ duplicate_window = config.ttl
291
+ end
292
+ config.ttl = config.ttl * ::NATS::NANOSECONDS
143
293
  end
144
294
 
145
295
  stream = JetStream::API::StreamConfig.new(
146
296
  name: "KV_#{config.bucket}",
297
+ description: config.description,
147
298
  subjects: ["$KV.#{config.bucket}.>"],
148
- max_msgs_per_subject: config.history,
149
- max_bytes: config.max_bytes,
299
+ allow_direct: config.direct,
300
+ allow_rollup_hdrs: true,
301
+ deny_delete: true,
302
+ discard: "new",
303
+ duplicate_window: duplicate_window * ::NATS::NANOSECONDS,
150
304
  max_age: config.ttl,
305
+ max_bytes: config.max_bytes,
306
+ max_consumers: -1,
151
307
  max_msg_size: config.max_value_size,
152
- storage: config.storage,
308
+ max_msgs: -1,
309
+ max_msgs_per_subject: config.history,
153
310
  num_replicas: config.replicas,
154
- allow_rollup_hdrs: true,
155
- deny_delete: true,
311
+ storage: config.storage,
312
+ republish: config.republish,
156
313
  )
157
- resp = add_stream(stream)
158
314
 
315
+ si = add_stream(stream)
159
316
  KeyValue.new(
160
317
  name: config.bucket,
161
318
  stream: stream.name,
162
319
  pre: "$KV.#{config.bucket}.",
163
320
  js: self,
321
+ direct: si.config.allow_direct
164
322
  )
165
323
  end
166
324
 
@@ -15,7 +15,7 @@
15
15
  module NATS
16
16
  module IO
17
17
  # VERSION is the version of the client announced on CONNECT to the server.
18
- VERSION = "2.1.2".freeze
18
+ VERSION = "2.2.0".freeze
19
19
 
20
20
  # LANG is the lang runtime of the client announced on CONNECT to the server.
21
21
  LANG = "#{RUBY_ENGINE}#{RUBY_VERSION}".freeze
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats-pure
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Waldemar Quevedo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-29 00:00:00.000000000 Z
11
+ date: 2022-10-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: NATS is an open-source, high-performance, lightweight cloud messaging
14
14
  system.