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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -0
  3. data/README.md +251 -0
  4. data/lib/nats/io/client.rb +226 -151
  5. data/lib/nats/io/errors.rb +6 -0
  6. data/lib/nats/io/jetstream/api.rb +305 -0
  7. data/lib/nats/io/jetstream/errors.rb +104 -0
  8. data/lib/nats/io/jetstream/js/config.rb +26 -0
  9. data/lib/nats/io/jetstream/js/header.rb +31 -0
  10. data/lib/nats/io/jetstream/js/status.rb +27 -0
  11. data/lib/nats/io/jetstream/js/sub.rb +30 -0
  12. data/lib/nats/io/jetstream/js.rb +93 -0
  13. data/lib/nats/io/jetstream/manager.rb +284 -0
  14. data/lib/nats/io/jetstream/msg/ack.rb +57 -0
  15. data/lib/nats/io/jetstream/msg/ack_methods.rb +111 -0
  16. data/lib/nats/io/jetstream/msg/metadata.rb +37 -0
  17. data/lib/nats/io/jetstream/msg.rb +26 -0
  18. data/lib/nats/io/jetstream/pull_subscription.rb +260 -0
  19. data/lib/nats/io/jetstream/push_subscription.rb +42 -0
  20. data/lib/nats/io/jetstream.rb +269 -0
  21. data/lib/nats/io/kv/api.rb +39 -0
  22. data/lib/nats/io/kv/bucket_status.rb +38 -0
  23. data/lib/nats/io/kv/errors.rb +60 -0
  24. data/lib/nats/io/kv/manager.rb +89 -0
  25. data/lib/nats/io/kv.rb +5 -157
  26. data/lib/nats/io/msg.rb +4 -2
  27. data/lib/nats/io/rails.rb +29 -0
  28. data/lib/nats/io/subscription.rb +70 -5
  29. data/lib/nats/io/version.rb +1 -1
  30. data/lib/nats/io/websocket.rb +75 -0
  31. data/sig/nats/io/client.rbs +304 -0
  32. data/sig/nats/io/errors.rbs +54 -0
  33. data/sig/nats/io/jetstream/api.rbs +35 -0
  34. data/sig/nats/io/jetstream/errors.rbs +54 -0
  35. data/sig/nats/io/jetstream/js/config.rbs +11 -0
  36. data/sig/nats/io/jetstream/js/header.rbs +17 -0
  37. data/sig/nats/io/jetstream/js/status.rbs +13 -0
  38. data/sig/nats/io/jetstream/js/sub.rbs +14 -0
  39. data/sig/nats/io/jetstream/js.rbs +27 -0
  40. data/sig/nats/io/jetstream/manager.rbs +33 -0
  41. data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
  42. data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
  43. data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
  44. data/sig/nats/io/jetstream/msg.rbs +6 -0
  45. data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
  46. data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
  47. data/sig/nats/io/jetstream.rbs +15 -0
  48. data/sig/nats/io/kv/api.rbs +8 -0
  49. data/sig/nats/io/kv/bucket_status.rbs +17 -0
  50. data/sig/nats/io/kv/errors.rbs +30 -0
  51. data/sig/nats/io/kv/manager.rbs +11 -0
  52. data/sig/nats/io/kv.rbs +39 -0
  53. data/sig/nats/io/msg.rbs +14 -0
  54. data/sig/nats/io/parser.rbs +32 -0
  55. data/sig/nats/io/subscription.rbs +33 -0
  56. data/sig/nats/io/version.rbs +9 -0
  57. data/sig/nats/nuid.rbs +32 -0
  58. metadata +68 -5
  59. data/lib/nats/io/js.rb +0 -1434
@@ -0,0 +1,60 @@
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
+
15
+ require_relative '../errors'
16
+
17
+ module NATS
18
+ class KeyValue
19
+ class Error < NATS::Error; end
20
+
21
+ # When a key is not found.
22
+ class KeyNotFoundError < Error
23
+ attr_reader :entry, :op
24
+ def initialize(params={})
25
+ @entry = params[:entry]
26
+ @op = params[:op]
27
+ @message = params[:message]
28
+ end
29
+
30
+ def to_s
31
+ msg = "nats: key not found"
32
+ msg = "#{msg}: #{@message}" if @message
33
+ msg
34
+ end
35
+ end
36
+
37
+ # When a key is not found because it was deleted.
38
+ class KeyDeletedError < KeyNotFoundError
39
+ def to_s
40
+ "nats: key was deleted"
41
+ end
42
+ end
43
+
44
+ # When there was no bucket present.
45
+ class BucketNotFoundError < Error; end
46
+
47
+ # When it is an invalid bucket.
48
+ class BadBucketError < Error; end
49
+
50
+ # When the result is an unexpected sequence.
51
+ class KeyWrongLastSequenceError < Error
52
+ def initialize(msg)
53
+ @msg = msg
54
+ end
55
+ def to_s
56
+ "nats: #{@msg}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,89 @@
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
+
15
+ module NATS
16
+ class KeyValue
17
+ module Manager
18
+ def key_value(bucket)
19
+ stream = "KV_#{bucket}"
20
+ begin
21
+ si = stream_info(stream)
22
+ rescue NATS::JetStream::Error::NotFound
23
+ raise BucketNotFoundError.new("nats: bucket not found")
24
+ end
25
+ if si.config.max_msgs_per_subject < 1
26
+ raise BadBucketError.new("nats: bad bucket")
27
+ end
28
+
29
+ KeyValue.new(
30
+ name: bucket,
31
+ stream: stream,
32
+ pre: "$KV.#{bucket}.",
33
+ js: self,
34
+ direct: si.config.allow_direct
35
+ )
36
+ end
37
+
38
+ def create_key_value(config)
39
+ config = if not config.is_a?(KeyValue::API::KeyValueConfig)
40
+ KeyValue::API::KeyValueConfig.new(config)
41
+ else
42
+ config
43
+ end
44
+ config.history ||= 1
45
+ config.replicas ||= 1
46
+ duplicate_window = 2 * 60 # 2 minutes
47
+ if config.ttl
48
+ if config.ttl < duplicate_window
49
+ duplicate_window = config.ttl
50
+ end
51
+ config.ttl = config.ttl * ::NATS::NANOSECONDS
52
+ end
53
+
54
+ stream = JetStream::API::StreamConfig.new(
55
+ name: "KV_#{config.bucket}",
56
+ description: config.description,
57
+ subjects: ["$KV.#{config.bucket}.>"],
58
+ allow_direct: config.direct,
59
+ allow_rollup_hdrs: true,
60
+ deny_delete: true,
61
+ discard: "new",
62
+ duplicate_window: duplicate_window * ::NATS::NANOSECONDS,
63
+ max_age: config.ttl,
64
+ max_bytes: config.max_bytes,
65
+ max_consumers: -1,
66
+ max_msg_size: config.max_value_size,
67
+ max_msgs: -1,
68
+ max_msgs_per_subject: config.history,
69
+ num_replicas: config.replicas,
70
+ storage: config.storage,
71
+ republish: config.republish,
72
+ )
73
+
74
+ si = add_stream(stream)
75
+ KeyValue.new(
76
+ name: config.bucket,
77
+ stream: stream.name,
78
+ pre: "$KV.#{config.bucket}.",
79
+ js: self,
80
+ direct: si.config.allow_direct
81
+ )
82
+ end
83
+
84
+ def delete_key_value(bucket)
85
+ delete_stream("KV_#{bucket}")
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/nats/io/kv.rb CHANGED
@@ -11,7 +11,11 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  #
14
- require_relative 'errors'
14
+
15
+ require_relative 'kv/api'
16
+ require_relative 'kv/bucket_status'
17
+ require_relative 'kv/errors'
18
+ require_relative 'kv/manager'
15
19
 
16
20
  module NATS
17
21
  class KeyValue
@@ -30,47 +34,6 @@ module NATS
30
34
  @direct = opts[:direct]
31
35
  end
32
36
 
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
49
- end
50
-
51
- # When a key is not found because it was deleted.
52
- class KeyDeletedError < KeyNotFoundError
53
- def to_s
54
- "nats: key was deleted"
55
- end
56
- end
57
-
58
- # When there was no bucket present.
59
- class BucketNotFoundError < Error; end
60
-
61
- # When it is an invalid bucket.
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
73
-
74
37
  # get returns the latest value for the key.
75
38
  def get(key, params={})
76
39
  entry = nil
@@ -211,120 +174,5 @@ module NATS
211
174
  super(opts)
212
175
  end
213
176
  end
214
-
215
- class BucketStatus
216
- attr_reader :bucket
217
-
218
- def initialize(info, bucket)
219
- @nfo = info
220
- @bucket = bucket
221
- end
222
-
223
- def values
224
- @nfo.state.messages
225
- end
226
-
227
- def history
228
- @nfo.config.max_msgs_per_subject
229
- end
230
-
231
- def ttl
232
- @nfo.config.max_age / ::NATS::NANOSECONDS
233
- end
234
- end
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
250
- def initialize(opts={})
251
- rem = opts.keys - members
252
- opts.delete_if { |k| rem.include?(k) }
253
- super(opts)
254
- end
255
- end
256
- end
257
-
258
- module Manager
259
- def key_value(bucket)
260
- stream = "KV_#{bucket}"
261
- begin
262
- si = stream_info(stream)
263
- rescue NATS::JetStream::Error::NotFound
264
- raise BucketNotFoundError.new("nats: bucket not found")
265
- end
266
- if si.config.max_msgs_per_subject < 1
267
- raise BadBucketError.new("nats: bad bucket")
268
- end
269
-
270
- KeyValue.new(
271
- name: bucket,
272
- stream: stream,
273
- pre: "$KV.#{bucket}.",
274
- js: self,
275
- direct: si.config.allow_direct
276
- )
277
- end
278
-
279
- def create_key_value(config)
280
- config = if not config.is_a?(KeyValue::API::KeyValueConfig)
281
- KeyValue::API::KeyValueConfig.new(config)
282
- else
283
- config
284
- end
285
- config.history ||= 1
286
- config.replicas ||= 1
287
- duplicate_window = 2 * 60 # 2 minutes
288
- if config.ttl
289
- if config.ttl < duplicate_window
290
- duplicate_window = config.ttl
291
- end
292
- config.ttl = config.ttl * ::NATS::NANOSECONDS
293
- end
294
-
295
- stream = JetStream::API::StreamConfig.new(
296
- name: "KV_#{config.bucket}",
297
- description: config.description,
298
- subjects: ["$KV.#{config.bucket}.>"],
299
- allow_direct: config.direct,
300
- allow_rollup_hdrs: true,
301
- deny_delete: true,
302
- discard: "new",
303
- duplicate_window: duplicate_window * ::NATS::NANOSECONDS,
304
- max_age: config.ttl,
305
- max_bytes: config.max_bytes,
306
- max_consumers: -1,
307
- max_msg_size: config.max_value_size,
308
- max_msgs: -1,
309
- max_msgs_per_subject: config.history,
310
- num_replicas: config.replicas,
311
- storage: config.storage,
312
- republish: config.republish,
313
- )
314
-
315
- si = add_stream(stream)
316
- KeyValue.new(
317
- name: config.bucket,
318
- stream: stream.name,
319
- pre: "$KV.#{config.bucket}.",
320
- js: self,
321
- direct: si.config.allow_direct
322
- )
323
- end
324
-
325
- def delete_key_value(bucket)
326
- delete_stream("KV_#{bucket}")
327
- end
328
- end
329
177
  end
330
178
  end
data/lib/nats/io/msg.rb CHANGED
@@ -11,7 +11,7 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  #
14
- require_relative 'js'
14
+ require_relative 'jetstream'
15
15
 
16
16
  module NATS
17
17
  class Msg
@@ -50,7 +50,9 @@ module NATS
50
50
 
51
51
  def inspect
52
52
  hdr = ", header=#{@header}" if @header
53
- "#<NATS::Msg(subject: \"#{@subject}\", reply: \"#{@reply}\", data: #{@data.slice(0, 10).inspect}#{hdr})>"
53
+ dot = '...' if @data.length > 10
54
+ dat = "#{data.slice(0, 10)}#{dot}"
55
+ "#<NATS::Msg(subject: \"#{@subject}\", reply: \"#{@reply}\", data: #{dat.inspect}#{hdr})>"
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,29 @@
1
+ require "rails"
2
+
3
+ module NATS
4
+ class Rails < ::Rails::Engine
5
+ # This class is used to free resources managed by Rails (e.g. database connections)
6
+ # that were implicitly acquired in subscription callbacks
7
+ # Implementation is based on https://github.com/sidekiq/sidekiq/blob/5e1a77a6d03193dd977fbfe8961ab78df91bb392/lib/sidekiq/rails.rb
8
+ class Reloader
9
+ def initialize(app = ::Rails.application)
10
+ @app = app
11
+ end
12
+
13
+ def call
14
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "gem.nats"} : {}
15
+ @app.reloader.wrap(**params) do
16
+ yield
17
+ end
18
+ end
19
+
20
+ def inspect
21
+ "#<NATS::Rails::Reloader @app=#{@app.class.name}>"
22
+ end
23
+ end
24
+
25
+ config.after_initialize do
26
+ NATS::Client.default_reloader = NATS::Rails::Reloader.new
27
+ end
28
+ end
29
+ end
@@ -28,14 +28,14 @@ module NATS
28
28
  include MonitorMixin
29
29
 
30
30
  attr_accessor :subject, :queue, :future, :callback, :response, :received, :max, :pending, :sid
31
- attr_accessor :pending_queue, :pending_size, :wait_for_msgs_t, :wait_for_msgs_cond, :is_slow_consumer
31
+ attr_accessor :pending_queue, :pending_size, :wait_for_msgs_cond, :concurrency_semaphore
32
32
  attr_accessor :pending_msgs_limit, :pending_bytes_limit
33
33
  attr_accessor :nc
34
34
  attr_accessor :jsi
35
35
  attr_accessor :closed
36
36
 
37
- def initialize
38
- super # required to initialize monitor
37
+ def initialize(**opts)
38
+ super() # required to initialize monitor
39
39
  @subject = ''
40
40
  @queue = nil
41
41
  @future = nil
@@ -53,11 +53,27 @@ module NATS
53
53
  @pending_size = 0
54
54
  @pending_msgs_limit = nil
55
55
  @pending_bytes_limit = nil
56
- @wait_for_msgs_t = nil
57
- @is_slow_consumer = false
58
56
 
59
57
  # Sync subscriber
60
58
  @wait_for_msgs_cond = nil
59
+
60
+ # To limit number of concurrent messages being processed (1 to only allow sequential processing)
61
+ @processing_concurrency = opts.fetch(:processing_concurrency, NATS::IO::DEFAULT_SINGLE_SUB_CONCURRENCY)
62
+ end
63
+
64
+ # Concurrency of message processing for a single subscription.
65
+ # 1 means sequential processing
66
+ # 2+ allow processed concurrently and possibly out of order.
67
+ def processing_concurrency=(value)
68
+ raise ArgumentError, "nats: subscription processing concurrency must be positive integer" unless value.positive?
69
+ return if @processing_concurrency == value
70
+
71
+ @processing_concurrency = value
72
+ @concurrency_semaphore = Concurrent::Semaphore.new(value)
73
+ end
74
+
75
+ def concurrency_semaphore
76
+ @concurrency_semaphore ||= Concurrent::Semaphore.new(@processing_concurrency)
61
77
  end
62
78
 
63
79
  # Auto unsubscribes the server by sending UNSUB command and throws away
@@ -88,5 +104,54 @@ module NATS
88
104
  def inspect
89
105
  "#<NATS::Subscription(subject: \"#{@subject}\", queue: \"#{@queue}\", sid: #{@sid})>"
90
106
  end
107
+
108
+ def dispatch(msg)
109
+ pending_queue << msg
110
+ synchronize { self.pending_size += msg.data.size }
111
+
112
+ # For async subscribers, send message for processing to the thread pool.
113
+ enqueue_processing(@nc.subscription_executor) if callback
114
+
115
+ # For sync subscribers, signal that there is a new message.
116
+ wait_for_msgs_cond&.signal
117
+ end
118
+
119
+ def process(msg)
120
+ return unless callback
121
+
122
+ # Decrease pending size since consumed already
123
+ synchronize { self.pending_size -= msg.data.size }
124
+
125
+ nc.reloader.call do
126
+ # Note: Keep some of the alternative arity versions to slightly
127
+ # improve backwards compatibility. Eventually fine to deprecate
128
+ # since recommended version would be arity of 1 to get a NATS::Msg.
129
+ case callback.arity
130
+ when 0 then callback.call
131
+ when 1 then callback.call(msg)
132
+ when 2 then callback.call(msg.data, msg.reply)
133
+ when 3 then callback.call(msg.data, msg.reply, msg.subject)
134
+ else callback.call(msg.data, msg.reply, msg.subject, msg.header)
135
+ end
136
+ rescue => e
137
+ synchronize { nc.send(:err_cb_call, nc, e, self) }
138
+ end
139
+ end
140
+
141
+ # Send a message for its processing to a separate thread
142
+ def enqueue_processing(executor)
143
+ concurrency_semaphore.try_acquire || return # Previous message is being executed, let it finish and enqueue next one.
144
+ executor.post do
145
+ msg = pending_queue.pop(true)
146
+ process(msg)
147
+ rescue ThreadError # queue is empty
148
+ concurrency_semaphore.release
149
+ ensure
150
+ concurrency_semaphore.release
151
+ [concurrency_semaphore.available_permits, pending_queue.size].min.times do
152
+ enqueue_processing(executor)
153
+ end
154
+ end
155
+ end
91
156
  end
92
157
  end
@@ -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.2.0".freeze
18
+ VERSION = "2.3.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
@@ -0,0 +1,75 @@
1
+ begin
2
+ require 'websocket'
3
+ rescue LoadError
4
+ raise LoadError, "Please add `websocket` gem to your Gemfile to connect to NATS via WebSocket."
5
+ end
6
+
7
+ module NATS
8
+ module IO
9
+ # WebSocket to connect to NATS via WebSocket and automatically decode and encode frames.
10
+
11
+ # @see https://docs.nats.io/running-a-nats-service/configuration/websocket
12
+
13
+ class WebSocket < Socket
14
+ class HandshakeError < RuntimeError; end
15
+
16
+ attr_accessor :socket
17
+
18
+ def initialize(options={})
19
+ super
20
+ end
21
+
22
+ def connect
23
+ super
24
+
25
+ setup_tls! if @uri.scheme == "wss" # WebSocket connection must be made over TLS from the beginning
26
+
27
+ @handshake = ::WebSocket::Handshake::Client.new url: @uri.to_s
28
+ @frame = ::WebSocket::Frame::Incoming::Client.new
29
+ @handshaked = false
30
+
31
+ @socket.write @handshake.to_s
32
+
33
+ until @handshaked
34
+ @handshake << method(:read).super_method.call(MAX_SOCKET_READ_BYTES)
35
+ if @handshake.finished?
36
+ @handshaked = true
37
+ end
38
+ end
39
+ end
40
+
41
+ def setup_tls!
42
+ return if @socket.is_a? OpenSSL::SSL::SSLSocket
43
+
44
+ super
45
+ end
46
+
47
+ def read(max_bytes=MAX_SOCKET_READ_BYTES, deadline=nil)
48
+ data = super
49
+ @frame << data
50
+ result = []
51
+ while msg = @frame.next
52
+ result << msg
53
+ end
54
+ result.join
55
+ end
56
+
57
+ def read_line(deadline=nil)
58
+ data = super
59
+ @frame << data
60
+ result = []
61
+ while msg = @frame.next
62
+ result << msg
63
+ end
64
+ result.join
65
+ end
66
+
67
+ def write(data, deadline=nil)
68
+ raise HandshakeError, "Attempted to write to socket while WebSocket handshake is in progress" unless @handshaked
69
+
70
+ frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: :binary, version: @handshake.version)
71
+ super frame.to_s
72
+ end
73
+ end
74
+ end
75
+ end