nats-pure 2.2.0 → 2.2.1

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.
@@ -0,0 +1,284 @@
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
+ # A JetStream::Manager can be used to make requests to the JetStream API.
18
+ #
19
+ # @example
20
+ # require 'nats/client'
21
+ #
22
+ # nc = NATS.connect("demo.nats.io")
23
+ #
24
+ # config = JetStream::API::StreamConfig.new()
25
+ # nc.jsm.add_stream(config)
26
+ #
27
+ #
28
+ module Manager
29
+ # add_stream creates a stream with a given config.
30
+ # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
31
+ # @param params [Hash] Options to customize API request.
32
+ # @option params [Float] :timeout Time to wait for response.
33
+ # @return [JetStream::API::StreamCreateResponse] The result of creating a Stream.
34
+ def add_stream(config, params={})
35
+ config = if not config.is_a?(JetStream::API::StreamConfig)
36
+ JetStream::API::StreamConfig.new(config)
37
+ else
38
+ config
39
+ end
40
+ stream = config[:name]
41
+ raise ArgumentError.new(":name is required to create streams") unless stream
42
+ raise ArgumentError.new("Spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited in stream names") if stream =~ /(\s|\.|\>|\*)/
43
+ req_subject = "#{@prefix}.STREAM.CREATE.#{stream}"
44
+
45
+ cfg = config.to_h.compact
46
+ result = api_request(req_subject, cfg.to_json, params)
47
+ JetStream::API::StreamCreateResponse.new(result)
48
+ end
49
+
50
+ # stream_info retrieves the current status of a stream.
51
+ # @param stream [String] Name of the stream.
52
+ # @param params [Hash] Options to customize API request.
53
+ # @option params [Float] :timeout Time to wait for response.
54
+ # @return [JetStream::API::StreamInfo] The latest StreamInfo of the stream.
55
+ def stream_info(stream, params={})
56
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
57
+
58
+ req_subject = "#{@prefix}.STREAM.INFO.#{stream}"
59
+ result = api_request(req_subject, '', params)
60
+ JetStream::API::StreamInfo.new(result)
61
+ end
62
+
63
+ # update_stream edits an existed stream with a given config.
64
+ # @param config [JetStream::API::StreamConfig] Configuration of the stream to create.
65
+ # @param params [Hash] Options to customize API request.
66
+ # @option params [Float] :timeout Time to wait for response.
67
+ # @return [JetStream::API::StreamCreateResponse] The result of creating a Stream.
68
+ def update_stream(config, params={})
69
+ config = if not config.is_a?(JetStream::API::StreamConfig)
70
+ JetStream::API::StreamConfig.new(config)
71
+ else
72
+ config
73
+ end
74
+ stream = config[:name]
75
+ raise ArgumentError.new(":name is required to create streams") unless stream
76
+ raise ArgumentError.new("Spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited in stream names") if stream =~ /(\s|\.|\>|\*)/
77
+ req_subject = "#{@prefix}.STREAM.UPDATE.#{stream}"
78
+ cfg = config.to_h.compact
79
+ result = api_request(req_subject, cfg.to_json, params)
80
+ JetStream::API::StreamCreateResponse.new(result)
81
+ end
82
+
83
+ # delete_stream deletes a stream.
84
+ # @param stream [String] Name of the stream.
85
+ # @param params [Hash] Options to customize API request.
86
+ # @option params [Float] :timeout Time to wait for response.
87
+ # @return [Boolean]
88
+ def delete_stream(stream, params={})
89
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
90
+
91
+ req_subject = "#{@prefix}.STREAM.DELETE.#{stream}"
92
+ result = api_request(req_subject, '', params)
93
+ result[:success]
94
+ end
95
+
96
+ # add_consumer creates a consumer with a given config.
97
+ # @param stream [String] Name of the stream.
98
+ # @param config [JetStream::API::ConsumerConfig] Configuration of the consumer to create.
99
+ # @param params [Hash] Options to customize API request.
100
+ # @option params [Float] :timeout Time to wait for response.
101
+ # @return [JetStream::API::ConsumerInfo] The result of creating a Consumer.
102
+ def add_consumer(stream, config, params={})
103
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
104
+ config = if not config.is_a?(JetStream::API::ConsumerConfig)
105
+ JetStream::API::ConsumerConfig.new(config)
106
+ else
107
+ config
108
+ end
109
+
110
+ req_subject = case
111
+ when config[:name]
112
+ # NOTE: Only supported after nats-server v2.9.0
113
+ if config[:filter_subject] && config[:filter_subject] != ">"
114
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
115
+ else
116
+ "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
117
+ end
118
+ when config[:durable_name]
119
+ "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
120
+ else
121
+ "#{@prefix}.CONSUMER.CREATE.#{stream}"
122
+ end
123
+
124
+ config[:ack_policy] ||= JS::Config::AckExplicit
125
+ # Check if have to normalize ack wait so that it is in nanoseconds for Go compat.
126
+ if config[:ack_wait]
127
+ raise ArgumentError.new("nats: invalid ack wait") unless config[:ack_wait].is_a?(Integer)
128
+ config[:ack_wait] = config[:ack_wait] * ::NATS::NANOSECONDS
129
+ end
130
+ if config[:inactive_threshold]
131
+ raise ArgumentError.new("nats: invalid inactive threshold") unless config[:inactive_threshold].is_a?(Integer)
132
+ config[:inactive_threshold] = config[:inactive_threshold] * ::NATS::NANOSECONDS
133
+ end
134
+
135
+ cfg = config.to_h.compact
136
+ req = {
137
+ stream_name: stream,
138
+ config: cfg
139
+ }
140
+
141
+ result = api_request(req_subject, req.to_json, params)
142
+ JetStream::API::ConsumerInfo.new(result).freeze
143
+ end
144
+
145
+ # consumer_info retrieves the current status of a consumer.
146
+ # @param stream [String] Name of the stream.
147
+ # @param consumer [String] Name of the consumer.
148
+ # @param params [Hash] Options to customize API request.
149
+ # @option params [Float] :timeout Time to wait for response.
150
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
151
+ def consumer_info(stream, consumer, params={})
152
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
153
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
154
+
155
+ req_subject = "#{@prefix}.CONSUMER.INFO.#{stream}.#{consumer}"
156
+ result = api_request(req_subject, '', params)
157
+ JetStream::API::ConsumerInfo.new(result)
158
+ end
159
+
160
+ # delete_consumer deletes a consumer.
161
+ # @param stream [String] Name of the stream.
162
+ # @param consumer [String] Name of the consumer.
163
+ # @param params [Hash] Options to customize API request.
164
+ # @option params [Float] :timeout Time to wait for response.
165
+ # @return [Boolean]
166
+ def delete_consumer(stream, consumer, params={})
167
+ raise JetStream::Error::InvalidStreamName.new("nats: invalid stream name") if stream.nil? or stream.empty?
168
+ raise JetStream::Error::InvalidConsumerName.new("nats: invalid consumer name") if consumer.nil? or consumer.empty?
169
+
170
+ req_subject = "#{@prefix}.CONSUMER.DELETE.#{stream}.#{consumer}"
171
+ result = api_request(req_subject, '', params)
172
+ result[:success]
173
+ end
174
+
175
+ # find_stream_name_by_subject does a lookup for the stream to which
176
+ # the subject belongs.
177
+ # @param subject [String] The subject that belongs to a stream.
178
+ # @param params [Hash] Options to customize API request.
179
+ # @option params [Float] :timeout Time to wait for response.
180
+ # @return [String] The name of the JetStream stream for the subject.
181
+ def find_stream_name_by_subject(subject, params={})
182
+ req_subject = "#{@prefix}.STREAM.NAMES"
183
+ req = { subject: subject }
184
+ result = api_request(req_subject, req.to_json, params)
185
+ raise JetStream::Error::NotFound unless result[:streams]
186
+
187
+ result[:streams].first
188
+ end
189
+
190
+ # get_msg retrieves a message from the stream.
191
+ # @param stream_name [String] The stream_name.
192
+ # @param params [Hash] Options to customize API request.
193
+ # @option next [Boolean] Fetch the next message for a subject.
194
+ # @option seq [Integer] Sequence number of a message.
195
+ # @option subject [String] Subject of the message.
196
+ # @option direct [Boolean] Use direct mode to for faster access (requires NATS v2.9.0)
197
+ def get_msg(stream_name, params={})
198
+ req = {}
199
+ case
200
+ when params[:next]
201
+ req[:seq] = params[:seq]
202
+ req[:next_by_subj] = params[:subject]
203
+ when params[:seq]
204
+ req[:seq] = params[:seq]
205
+ when params[:subject]
206
+ req[:last_by_subj] = params[:subject]
207
+ end
208
+
209
+ data = req.to_json
210
+ if params[:direct]
211
+ if params[:subject] and not params[:seq]
212
+ # last_by_subject type request requires no payload.
213
+ data = ''
214
+ req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}.#{params[:subject]}"
215
+ else
216
+ req_subject = "#{@prefix}.DIRECT.GET.#{stream_name}"
217
+ end
218
+ else
219
+ req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
220
+ end
221
+ resp = api_request(req_subject, data, direct: params[:direct])
222
+ msg = if params[:direct]
223
+ _lift_msg_to_raw_msg(resp)
224
+ else
225
+ JetStream::API::RawStreamMsg.new(resp[:message])
226
+ end
227
+
228
+ msg
229
+ end
230
+
231
+ def get_last_msg(stream_name, subject, params={})
232
+ params[:subject] = subject
233
+ get_msg(stream_name, params)
234
+ end
235
+
236
+ def account_info
237
+ api_request("#{@prefix}.INFO")
238
+ end
239
+
240
+ private
241
+
242
+ def api_request(req_subject, req="", params={})
243
+ params[:timeout] ||= @opts[:timeout]
244
+ msg = begin
245
+ @nc.request(req_subject, req, **params)
246
+ rescue NATS::IO::NoRespondersError
247
+ raise JetStream::Error::ServiceUnavailable
248
+ end
249
+
250
+ result = if params[:direct]
251
+ msg
252
+ else
253
+ JSON.parse(msg.data, symbolize_names: true)
254
+ end
255
+ if result.is_a?(Hash) and result[:error]
256
+ raise JS.from_error(result[:error])
257
+ end
258
+
259
+ result
260
+ end
261
+
262
+ def _lift_msg_to_raw_msg(msg)
263
+ if msg.header and msg.header['Status']
264
+ status = msg.header['Status']
265
+ if status == '404'
266
+ raise ::NATS::JetStream::Error::NotFound.new
267
+ else
268
+ raise JS.from_msg(msg)
269
+ end
270
+ end
271
+ subject = msg.header['Nats-Subject']
272
+ seq = msg.header['Nats-Sequence']
273
+ raw_msg = JetStream::API::RawStreamMsg.new(
274
+ subject: subject,
275
+ seq: seq,
276
+ headers: msg.header,
277
+ )
278
+ raw_msg.data = msg.data
279
+
280
+ raw_msg
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,57 @@
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
+ module Msg
18
+ module Ack
19
+ # Ack types
20
+ Ack = ("+ACK".freeze)
21
+ Nak = ("-NAK".freeze)
22
+ Progress = ("+WPI".freeze)
23
+ Term = ("+TERM".freeze)
24
+
25
+ Empty = (''.freeze)
26
+ DotSep = ('.'.freeze)
27
+ NoDomainName = ('_'.freeze)
28
+
29
+ # Position
30
+ Prefix0 = ('$JS'.freeze)
31
+ Prefix1 = ('ACK'.freeze)
32
+ Domain = 2
33
+ AccHash = 3
34
+ Stream = 4
35
+ Consumer = 5
36
+ NumDelivered = 6
37
+ StreamSeq = 7
38
+ ConsumerSeq = 8
39
+ Timestamp = 9
40
+ NumPending = 10
41
+
42
+ # Subject without domain:
43
+ # $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
44
+ #
45
+ V1TokenCounts = 9
46
+
47
+ # Subject with domain:
48
+ # $JS.ACK.<domain>.<account hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>.<a token with a random value>
49
+ #
50
+ V2TokenCounts = 12
51
+
52
+ SequencePair = Struct.new(:stream, :consumer)
53
+ end
54
+ private_constant :Ack
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,107 @@
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
+ module Msg
18
+ module AckMethods
19
+ def ack(**params)
20
+ ensure_is_acked_once!
21
+
22
+ resp = if params[:timeout]
23
+ @nc.request(@reply, Ack::Ack, **params)
24
+ else
25
+ @nc.publish(@reply, Ack::Ack)
26
+ end
27
+ @sub.synchronize { @ackd = true }
28
+
29
+ resp
30
+ end
31
+
32
+ def ack_sync(**params)
33
+ ensure_is_acked_once!
34
+
35
+ params[:timeout] ||= 0.5
36
+ resp = @nc.request(@reply, Ack::Ack, **params)
37
+ @sub.synchronize { @ackd = true }
38
+
39
+ resp
40
+ end
41
+
42
+ def nak(**params)
43
+ ensure_is_acked_once!
44
+
45
+ resp = if params[:timeout]
46
+ @nc.request(@reply, Ack::Nak, **params)
47
+ else
48
+ @nc.publish(@reply, Ack::Nak)
49
+ end
50
+ @sub.synchronize { @ackd = true }
51
+
52
+ resp
53
+ end
54
+
55
+ def term(**params)
56
+ ensure_is_acked_once!
57
+
58
+ resp = if params[:timeout]
59
+ @nc.request(@reply, Ack::Term, **params)
60
+ else
61
+ @nc.publish(@reply, Ack::Term)
62
+ end
63
+ @sub.synchronize { @ackd = true }
64
+
65
+ resp
66
+ end
67
+
68
+ def in_progress(**params)
69
+ params[:timeout] ? @nc.request(@reply, Ack::Progress, **params) : @nc.publish(@reply, Ack::Progress)
70
+ end
71
+
72
+ def metadata
73
+ @meta ||= parse_metadata(reply)
74
+ end
75
+
76
+ private
77
+
78
+ def ensure_is_acked_once!
79
+ @sub.synchronize do
80
+ if @ackd
81
+ raise JetStream::Error::MsgAlreadyAckd.new("nats: message was already acknowledged: #{self}")
82
+ end
83
+ end
84
+ end
85
+
86
+ def parse_metadata(reply)
87
+ tokens = reply.split(Ack::DotSep)
88
+ n = tokens.count
89
+
90
+ case
91
+ when n < Ack::V1TokenCounts || (n > Ack::V1TokenCounts and n < Ack::V2TokenCounts)
92
+ raise NotJSMessage.new("nats: not a jetstream message")
93
+ when tokens[0] != Ack::Prefix0 || tokens[1] != Ack::Prefix1
94
+ raise NotJSMessage.new("nats: not a jetstream message")
95
+ when n == Ack::V1TokenCounts
96
+ tokens.insert(Ack::Domain, Ack::Empty)
97
+ tokens.insert(Ack::AccHash, Ack::Empty)
98
+ when tokens[Ack::Domain] == Ack::NoDomainName
99
+ tokens[Ack::Domain] = Ack::Empty
100
+ end
101
+
102
+ Metadata.new(tokens)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,37 @@
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 'time'
16
+
17
+ module NATS
18
+ class JetStream
19
+ module Msg
20
+ class Metadata
21
+ attr_reader :sequence, :num_delivered, :num_pending, :timestamp, :stream, :consumer, :domain
22
+
23
+ def initialize(opts)
24
+ @sequence = Ack::SequencePair.new(opts[Ack::StreamSeq].to_i, opts[Ack::ConsumerSeq].to_i)
25
+ @domain = opts[Ack::Domain]
26
+ @num_delivered = opts[Ack::NumDelivered].to_i
27
+ @num_pending = opts[Ack::NumPending].to_i
28
+ @timestamp = Time.at((opts[Ack::Timestamp].to_i / 1_000_000_000.0))
29
+ @stream = opts[Ack::Stream]
30
+ @consumer = opts[Ack::Consumer]
31
+ # TODO: Not exposed in Go client either right now.
32
+ # account = opts[Ack::AccHash]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
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 'msg/ack'
16
+ require_relative 'msg/ack_methods'
17
+ require_relative 'msg/metadata'
18
+
19
+ module NATS
20
+ class JetStream
21
+ # JetStream::Msg module includes the methods so that a regular NATS::Msg
22
+ # can be enhanced with JetStream features like acking and metadata.
23
+ module Msg
24
+ end
25
+ end
26
+ end