nats-pure 2.2.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,260 @@
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 JetStream
19
+ # PullSubscription is included into NATS::Subscription so that it
20
+ # can be used to fetch messages from a pull based consumer from
21
+ # JetStream.
22
+ #
23
+ # @example Create a pull subscription using JetStream context.
24
+ #
25
+ # require 'nats/client'
26
+ #
27
+ # nc = NATS.connect
28
+ # js = nc.jetstream
29
+ # psub = js.pull_subscribe("foo", "bar")
30
+ #
31
+ # loop do
32
+ # msgs = psub.fetch(5)
33
+ # msgs.each do |msg|
34
+ # msg.ack
35
+ # end
36
+ # end
37
+ #
38
+ # @!visibility public
39
+ module PullSubscription
40
+ # next_msg is not available for pull based subscriptions.
41
+ # @raise [NATS::JetStream::Error]
42
+ def next_msg(params={})
43
+ raise ::NATS::JetStream::Error.new("nats: pull subscription cannot use next_msg")
44
+ end
45
+
46
+ # fetch makes a request to be delivered more messages from a pull consumer.
47
+ #
48
+ # @param batch [Fixnum] Number of messages to pull from the stream.
49
+ # @param params [Hash] Options to customize the fetch request.
50
+ # @option params [Float] :timeout Duration of the fetch request before it expires.
51
+ # @return [Array<NATS::Msg>]
52
+ def fetch(batch=1, params={})
53
+ if batch < 1
54
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
55
+ end
56
+
57
+ t = MonotonicTime.now
58
+ timeout = params[:timeout] ||= 5
59
+ expires = (timeout * 1_000_000_000) - 100_000
60
+ next_req = {
61
+ batch: batch
62
+ }
63
+
64
+ msgs = []
65
+ case
66
+ when batch < 1
67
+ raise ::NATS::JetStream::Error.new("nats: invalid batch size")
68
+ when batch == 1
69
+ ####################################################
70
+ # Fetch (1) #
71
+ ####################################################
72
+
73
+ # Check if there is any pending message in the queue that is
74
+ # ready to be consumed.
75
+ synchronize do
76
+ unless @pending_queue.empty?
77
+ msg = @pending_queue.pop
78
+ @pending_size -= msg.data.size
79
+ # Check for a no msgs response status.
80
+ if JS.is_status_msg(msg)
81
+ case msg.header["Status"]
82
+ when JS::Status::NoMsgs
83
+ msg = nil
84
+ when JS::Status::RequestTimeout
85
+ # Skip
86
+ else
87
+ raise JS.from_msg(msg)
88
+ end
89
+ else
90
+ msgs << msg
91
+ end
92
+ end
93
+ end
94
+
95
+ # Make lingering request with expiration.
96
+ next_req[:expires] = expires
97
+ if msgs.empty?
98
+ # Make publish request and wait for response.
99
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
100
+
101
+ # Wait for result of fetch or timeout.
102
+ synchronize { wait_for_msgs_cond.wait(timeout) }
103
+
104
+ unless @pending_queue.empty?
105
+ msg = @pending_queue.pop
106
+ @pending_size -= msg.data.size
107
+
108
+ msgs << msg
109
+ end
110
+
111
+ duration = MonotonicTime.since(t)
112
+ if duration > timeout
113
+ raise ::NATS::Timeout.new("nats: fetch timeout")
114
+ end
115
+
116
+ # Should have received at least a message at this point,
117
+ # if that is not the case then error already.
118
+ if JS.is_status_msg(msgs.first)
119
+ msg = msgs.first
120
+ case msg.header[JS::Header::Status]
121
+ when JS::Status::RequestTimeout
122
+ raise NATS::Timeout.new("nats: fetch request timeout")
123
+ else
124
+ raise JS.from_msg(msgs.first)
125
+ end
126
+ end
127
+ end
128
+ when batch > 1
129
+ ####################################################
130
+ # Fetch (n) #
131
+ ####################################################
132
+
133
+ # Check if there already enough in the pending buffer.
134
+ synchronize do
135
+ if batch <= @pending_queue.size
136
+ batch.times do
137
+ msg = @pending_queue.pop
138
+ @pending_size -= msg.data.size
139
+
140
+ # Check for a no msgs response status.
141
+ if JS.is_status_msg(msg)
142
+ case msg.header[JS::Header::Status]
143
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
144
+ # Skip these
145
+ next
146
+ else
147
+ raise JS.from_msg(msg)
148
+ end
149
+ else
150
+ msgs << msg
151
+ end
152
+ end
153
+
154
+ return msgs
155
+ end
156
+ end
157
+
158
+ # Make publish request and wait any response.
159
+ next_req[:no_wait] = true
160
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
161
+
162
+ # Not receiving even one is a timeout.
163
+ start_time = MonotonicTime.now
164
+ msg = nil
165
+
166
+ synchronize do
167
+ wait_for_msgs_cond.wait(timeout)
168
+
169
+ unless @pending_queue.empty?
170
+ msg = @pending_queue.pop
171
+ @pending_size -= msg.data.size
172
+ end
173
+ end
174
+
175
+ # Check if the first message was a response saying that
176
+ # there are no messages.
177
+ if !msg.nil? && JS.is_status_msg(msg)
178
+ case msg.header[JS::Header::Status]
179
+ when JS::Status::NoMsgs
180
+ # Make another request that does wait.
181
+ next_req[:expires] = expires
182
+ next_req.delete(:no_wait)
183
+
184
+ @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
185
+ when JS::Status::RequestTimeout
186
+ raise NATS::Timeout.new("nats: fetch request timeout")
187
+ else
188
+ raise JS.from_msg(msg)
189
+ end
190
+ else
191
+ msgs << msg unless msg.nil?
192
+ end
193
+
194
+ # Check if have not received yet a single message.
195
+ duration = MonotonicTime.since(start_time)
196
+ if msgs.empty? and duration > timeout
197
+ raise NATS::Timeout.new("nats: fetch timeout")
198
+ end
199
+
200
+ needed = batch - msgs.count
201
+ while needed > 0 and MonotonicTime.since(start_time) < timeout
202
+ duration = MonotonicTime.since(start_time)
203
+
204
+ # Wait for the rest of the messages.
205
+ synchronize do
206
+
207
+ # Wait until there is a message delivered.
208
+ if @pending_queue.empty?
209
+ deadline = timeout - duration
210
+ wait_for_msgs_cond.wait(deadline) if deadline > 0
211
+
212
+ duration = MonotonicTime.since(start_time)
213
+ if msgs.empty? && @pending_queue.empty? and duration > timeout
214
+ raise NATS::Timeout.new("nats: fetch timeout")
215
+ end
216
+ else
217
+ msg = @pending_queue.pop
218
+ @pending_size -= msg.data.size
219
+
220
+ if JS.is_status_msg(msg)
221
+ case msg.header[JS::Header::Status]
222
+ when JS::Status::NoMsgs, JS::Status::RequestTimeout
223
+ duration = MonotonicTime.since(start_time)
224
+
225
+ if duration > timeout
226
+ # Only received a subset of the messages.
227
+ if !msgs.empty?
228
+ return msgs
229
+ else
230
+ raise NATS::Timeout.new("nats: fetch timeout")
231
+ end
232
+ end
233
+ else
234
+ raise JS.from_msg(msg)
235
+ end
236
+
237
+ else
238
+ # Add to the set of messages that will be returned.
239
+ msgs << msg
240
+ needed -= 1
241
+ end
242
+ end
243
+ end # :end: synchronize
244
+ end
245
+ end
246
+
247
+ msgs
248
+ end
249
+
250
+ # consumer_info retrieves the current status of the pull subscription consumer.
251
+ # @param params [Hash] Options to customize API request.
252
+ # @option params [Float] :timeout Time to wait for response.
253
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
254
+ def consumer_info(params={})
255
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
256
+ end
257
+ end
258
+ private_constant :PullSubscription
259
+ end
260
+ end
@@ -0,0 +1,42 @@
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 JetStream
17
+ # PushSubscription is included into NATS::Subscription so that it
18
+ #
19
+ # @example Create a push subscription using JetStream context.
20
+ #
21
+ # require 'nats/client'
22
+ #
23
+ # nc = NATS.connect
24
+ # js = nc.jetstream
25
+ # sub = js.subscribe("foo", "bar")
26
+ # msg = sub.next_msg
27
+ # msg.ack
28
+ # sub.unsubscribe
29
+ #
30
+ # @!visibility public
31
+ module PushSubscription
32
+ # consumer_info retrieves the current status of the pull subscription consumer.
33
+ # @param params [Hash] Options to customize API request.
34
+ # @option params [Float] :timeout Time to wait for response.
35
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
36
+ def consumer_info(params={})
37
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
38
+ end
39
+ end
40
+ private_constant :PushSubscription
41
+ end
42
+ end
@@ -0,0 +1,269 @@
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_relative 'kv'
18
+ require_relative 'jetstream/api'
19
+ require_relative 'jetstream/errors'
20
+ require_relative 'jetstream/js'
21
+ require_relative 'jetstream/manager'
22
+ require_relative 'jetstream/msg'
23
+ require_relative 'jetstream/pull_subscription'
24
+ require_relative 'jetstream/push_subscription'
25
+
26
+ module NATS
27
+ # JetStream returns a context with a similar API as the NATS::Client
28
+ # but with enhanced functions to persist and consume messages from
29
+ # the NATS JetStream engine.
30
+ #
31
+ # @example
32
+ # nc = NATS.connect("demo.nats.io")
33
+ # js = nc.jetstream()
34
+ #
35
+ class JetStream
36
+ # Create a new JetStream context for a NATS connection.
37
+ #
38
+ # @param conn [NATS::Client]
39
+ # @param params [Hash] Options to customize JetStream context.
40
+ # @option params [String] :prefix JetStream API prefix to use for the requests.
41
+ # @option params [String] :domain JetStream Domain to use for the requests.
42
+ # @option params [Float] :timeout Default timeout to use for JS requests.
43
+ def initialize(conn, params={})
44
+ @nc = conn
45
+ @prefix = if params[:prefix]
46
+ params[:prefix]
47
+ elsif params[:domain]
48
+ "$JS.#{params[:domain]}.API"
49
+ else
50
+ JS::DefaultAPIPrefix
51
+ end
52
+ @opts = params
53
+ @opts[:timeout] ||= 5 # seconds
54
+ params[:prefix] = @prefix
55
+
56
+ # Include JetStream::Manager
57
+ extend Manager
58
+ extend KeyValue::Manager
59
+ end
60
+
61
+ # PubAck is the API response from a successfully published message.
62
+ #
63
+ # @!attribute [stream] stream
64
+ # @return [String] Name of the stream that processed the published message.
65
+ # @!attribute [seq] seq
66
+ # @return [Fixnum] Sequence of the message in the stream.
67
+ # @!attribute [duplicate] duplicate
68
+ # @return [Boolean] Indicates whether the published message is a duplicate.
69
+ # @!attribute [domain] domain
70
+ # @return [String] JetStream Domain that processed the ack response.
71
+ PubAck = Struct.new(:stream, :seq, :duplicate, :domain, keyword_init: true)
72
+
73
+ # publish produces a message for JetStream.
74
+ #
75
+ # @param subject [String] The subject from a stream where the message will be sent.
76
+ # @param payload [String] The payload of the message.
77
+ # @param params [Hash] Options to customize the publish message request.
78
+ # @option params [Float] :timeout Time to wait for an PubAck response or an error.
79
+ # @option params [Hash] :header NATS Headers to use for the message.
80
+ # @option params [String] :stream Expected Stream to which the message is being published.
81
+ # @raise [NATS::Timeout] When it takes too long to receive an ack response.
82
+ # @return [PubAck] The pub ack response.
83
+ def publish(subject, payload="", **params)
84
+ params[:timeout] ||= @opts[:timeout]
85
+ if params[:stream]
86
+ params[:header] ||= {}
87
+ params[:header][JS::Header::ExpectedStream] = params[:stream]
88
+ end
89
+
90
+ # Send message with headers.
91
+ msg = NATS::Msg.new(subject: subject,
92
+ data: payload,
93
+ header: params[:header])
94
+
95
+ begin
96
+ resp = @nc.request_msg(msg, **params)
97
+ result = JSON.parse(resp.data, symbolize_names: true)
98
+ rescue ::NATS::IO::NoRespondersError
99
+ raise JetStream::Error::NoStreamResponse.new("nats: no response from stream")
100
+ end
101
+ raise JS.from_error(result[:error]) if result[:error]
102
+
103
+ PubAck.new(result)
104
+ end
105
+
106
+ # subscribe binds or creates a push subscription to a JetStream pull consumer.
107
+ #
108
+ # @param subject [String] Subject from which the messages will be fetched.
109
+ # @param params [Hash] Options to customize the PushSubscription.
110
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
111
+ # @option params [String] :consumer Name of the Consumer to which the PushSubscription will be bound.
112
+ # @option params [String] :durable Consumer durable name from where the messages will be fetched.
113
+ # @option params [Hash] :config Configuration for the consumer.
114
+ # @return [NATS::JetStream::PushSubscription]
115
+ def subscribe(subject, params={}, &cb)
116
+ params[:consumer] ||= params[:durable]
117
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
118
+
119
+ queue = params[:queue]
120
+ durable = params[:durable]
121
+ flow_control = params[:flow_control]
122
+ manual_ack = params[:manual_ack]
123
+ idle_heartbeat = params[:idle_heartbeat]
124
+ flow_control = params[:flow_control]
125
+ config = params[:config]
126
+
127
+ if queue
128
+ if durable and durable != queue
129
+ raise NATS::JetStream::Error.new("nats: cannot create queue subscription '#{queue}' to consumer '#{durable}'")
130
+ else
131
+ durable = queue
132
+ end
133
+ end
134
+
135
+ cinfo = nil
136
+ consumer_found = false
137
+ should_create = false
138
+
139
+ if not durable
140
+ should_create = true
141
+ else
142
+ begin
143
+ cinfo = consumer_info(stream, durable)
144
+ config = cinfo.config
145
+ consumer_found = true
146
+ consumer = durable
147
+ rescue NATS::JetStream::Error::NotFound
148
+ should_create = true
149
+ consumer_found = false
150
+ end
151
+ end
152
+
153
+ if consumer_found
154
+ if not config.deliver_group
155
+ if queue
156
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription for a consumer without a deliver group")
157
+ elsif cinfo.push_bound
158
+ raise NATS::JetStream::Error.new("nats: consumer is already bound to a subscription")
159
+ end
160
+ else
161
+ if not queue
162
+ raise NATS::JetStream::Error.new("nats: cannot create a subscription for a consumer with a deliver group #{config.deliver_group}")
163
+ elsif queue != config.deliver_group
164
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
165
+ end
166
+ end
167
+ elsif should_create
168
+ # Auto-create consumer if none found.
169
+ if config.nil?
170
+ # Defaults
171
+ config = JetStream::API::ConsumerConfig.new({ack_policy: "explicit"})
172
+ elsif config.is_a?(Hash)
173
+ config = JetStream::API::ConsumerConfig.new(config)
174
+ elsif !config.is_a?(JetStream::API::ConsumerConfig)
175
+ raise NATS::JetStream::Error.new("nats: invalid ConsumerConfig")
176
+ end
177
+
178
+ config.durable_name = durable if not config.durable_name
179
+ config.deliver_group = queue if not config.deliver_group
180
+
181
+ # Create inbox for push consumer.
182
+ deliver = @nc.new_inbox
183
+ config.deliver_subject = deliver
184
+
185
+ # Auto created consumers use the filter subject.
186
+ config.filter_subject = subject
187
+
188
+ # Heartbeats / FlowControl
189
+ config.flow_control = flow_control
190
+ if idle_heartbeat or config.idle_heartbeat
191
+ idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
192
+ idle_heartbeat = idle_heartbeat * ::NATS::NANOSECONDS
193
+ config.idle_heartbeat = idle_heartbeat
194
+ end
195
+
196
+ # Auto create the consumer.
197
+ cinfo = add_consumer(stream, config)
198
+ consumer = cinfo.name
199
+ end
200
+
201
+ # Enable auto acking for async callbacks unless disabled.
202
+ if cb and not manual_ack
203
+ ocb = cb
204
+ new_cb = proc do |msg|
205
+ ocb.call(msg)
206
+ msg.ack rescue JetStream::Error::MsgAlreadyAckd
207
+ end
208
+ cb = new_cb
209
+ end
210
+ sub = @nc.subscribe(config.deliver_subject, queue: config.deliver_group, &cb)
211
+ sub.extend(PushSubscription)
212
+ sub.jsi = JS::Sub.new(
213
+ js: self,
214
+ stream: stream,
215
+ consumer: consumer,
216
+ )
217
+ sub
218
+ end
219
+
220
+ # pull_subscribe binds or creates a subscription to a JetStream pull consumer.
221
+ #
222
+ # @param subject [String] Subject from which the messages will be fetched.
223
+ # @param durable [String] Consumer durable name from where the messages will be fetched.
224
+ # @param params [Hash] Options to customize the PullSubscription.
225
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
226
+ # @option params [String] :consumer Name of the Consumer to which the PullSubscription will be bound.
227
+ # @option params [Hash] :config Configuration for the consumer.
228
+ # @return [NATS::JetStream::PullSubscription]
229
+ def pull_subscribe(subject, durable, params={})
230
+ if durable.empty? && !params[:consumer]
231
+ raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name")
232
+ end
233
+ params[:consumer] ||= durable
234
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
235
+
236
+ begin
237
+ consumer_info(stream, params[:consumer])
238
+ rescue NATS::JetStream::Error::NotFound => e
239
+ # If attempting to bind, then this is a hard error.
240
+ raise e if params[:stream]
241
+
242
+ config = if not params[:config]
243
+ JetStream::API::ConsumerConfig.new
244
+ elsif params[:config].is_a?(JetStream::API::ConsumerConfig)
245
+ params[:config]
246
+ else
247
+ JetStream::API::ConsumerConfig.new(params[:config])
248
+ end
249
+ config[:durable_name] = durable
250
+ config[:ack_policy] ||= JS::Config::AckExplicit
251
+ add_consumer(stream, config)
252
+ end
253
+
254
+ deliver = @nc.new_inbox
255
+ sub = @nc.subscribe(deliver)
256
+ sub.extend(PullSubscription)
257
+
258
+ consumer = params[:consumer]
259
+ subject = "#{@prefix}.CONSUMER.MSG.NEXT.#{stream}.#{consumer}"
260
+ sub.jsi = JS::Sub.new(
261
+ js: self,
262
+ stream: stream,
263
+ consumer: params[:consumer],
264
+ nms: subject
265
+ )
266
+ sub
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,39 @@
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 API
18
+ KeyValueConfig = Struct.new(
19
+ :bucket,
20
+ :description,
21
+ :max_value_size,
22
+ :history,
23
+ :ttl,
24
+ :max_bytes,
25
+ :storage,
26
+ :replicas,
27
+ :placement,
28
+ :republish,
29
+ :direct,
30
+ keyword_init: true) do
31
+ def initialize(opts={})
32
+ rem = opts.keys - members
33
+ opts.delete_if { |k| rem.include?(k) }
34
+ super(opts)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
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
+ class BucketStatus
18
+ attr_reader :bucket
19
+
20
+ def initialize(info, bucket)
21
+ @nfo = info
22
+ @bucket = bucket
23
+ end
24
+
25
+ def values
26
+ @nfo.state.messages
27
+ end
28
+
29
+ def history
30
+ @nfo.config.max_msgs_per_subject
31
+ end
32
+
33
+ def ttl
34
+ @nfo.config.max_age / ::NATS::NANOSECONDS
35
+ end
36
+ end
37
+ end
38
+ end