nats-pure 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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