nats-pure 2.0.0.pre.alpha → 2.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15a5820d95fec0e8c400dab340af35458c52b5bab82e4decdf2b717067dcc940
4
- data.tar.gz: 6b98522090c63c3b6338b21e2c42c8bf0172c666c79d5f39237b67a7fff63409
3
+ metadata.gz: 210e29c9b6fcc68e5c89e841a068c1a389ac78a67cadcb639161d0d5c02c4bf0
4
+ data.tar.gz: 2b181886b3a96277ae494cdda48b8bf16e9721666294ea69440976b85888b40c
5
5
  SHA512:
6
- metadata.gz: b31773f6a58cfaa0728bbb544089a4181016bb32b95ca74c1effd06458428b544163bd4bba5194792d7e18c2c20386b49605d701391203aa8c8053a1e597fc57
7
- data.tar.gz: 856e51e5c54dc25fba10f126d9072458ef1d7e145fbb9303ef643542c08cfcc155ddbd8c5bacc4d94ecf1c690a08877c6c8a9657fe91a7167d21b4bbedd513ce
6
+ metadata.gz: 910ffb697d692a3040fd6b40e835d1a2486f16525a5e814ea33dfdb2e732dd6ba3f1876b5e6dbc7dee07e9491154a6c181b41b2db08988434d9e48900ae3dcf0
7
+ data.tar.gz: a4e4d9cc9be95d1c67331576c814841b7b7784986a6ac9503ec44846e5dfbf2af978990fe482c8a9ea1064112ab54b4c65cfc619a931a20904f712d2896547a1
@@ -75,6 +75,10 @@ module NATS
75
75
 
76
76
  # When the client is attempting to connect to a NATS Server for the first time.
77
77
  CONNECTING = 4
78
+
79
+ # When the client is draining a connection before closing.
80
+ DRAINING_SUBS = 5
81
+ DRAINING_PUBS = 6
78
82
  end
79
83
 
80
84
  # Client creates a connection to the NATS Server.
@@ -182,6 +186,14 @@ module NATS
182
186
  @user_nkey_cb = nil
183
187
  @user_jwt_cb = nil
184
188
  @signature_cb = nil
189
+
190
+ # Tokens
191
+ @auth_token = nil
192
+
193
+ @inbox_prefix = "_INBOX"
194
+
195
+ # Draining
196
+ @drain_t = nil
185
197
  end
186
198
 
187
199
  # Establishes a connection to NATS.
@@ -229,6 +241,7 @@ module NATS
229
241
  opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
230
242
  opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
231
243
  opts[:connect_timeout] ||= NATS::IO::DEFAULT_CONNECT_TIMEOUT
244
+ opts[:drain_timeout] ||= NATS::IO::DEFAULT_DRAIN_TIMEOUT
232
245
  @options = opts
233
246
 
234
247
  # Process servers in the NATS cluster and pick one to connect
@@ -254,13 +267,24 @@ module NATS
254
267
  end
255
268
 
256
269
  # NKEYS
270
+ @signature_cb ||= opts[:user_signature_cb]
271
+ @user_jwt_cb ||= opts[:user_jwt_cb]
272
+ @user_nkey_cb ||= opts[:user_nkey_cb]
257
273
  @user_credentials ||= opts[:user_credentials]
258
274
  @nkeys_seed ||= opts[:nkeys_seed]
275
+
259
276
  setup_nkeys_connect if @user_credentials or @nkeys_seed
260
277
 
278
+ # Tokens, if set will take preference over the user@server uri token
279
+ @auth_token ||= opts[:auth_token]
280
+
261
281
  # Check for TLS usage
262
282
  @tls = @options[:tls]
263
283
 
284
+ @inbox_prefix = opts.fetch(:custom_inbox_prefix, @inbox_prefix)
285
+
286
+ validate_settings!
287
+
264
288
  srv = nil
265
289
  begin
266
290
  srv = select_next_server
@@ -387,6 +411,8 @@ module NATS
387
411
  # Create subscription which is dispatched asynchronously
388
412
  # messages to a callback.
389
413
  def subscribe(subject, opts={}, &callback)
414
+ raise NATS::IO::ConnectionDrainingError.new("nats: connection draining") if draining?
415
+
390
416
  sid = nil
391
417
  sub = nil
392
418
  synchronize do
@@ -640,7 +666,7 @@ module NATS
640
666
  end
641
667
 
642
668
  # Send a ping and wait for a pong back within a timeout.
643
- def flush(timeout=60)
669
+ def flush(timeout=10)
644
670
  # Schedule sending a PING, and block until we receive PONG back,
645
671
  # or raise a timeout in case the response is past the deadline.
646
672
  pong = @pongs.new_cond
@@ -674,7 +700,7 @@ module NATS
674
700
  # new_inbox returns a unique inbox used for subscriptions.
675
701
  # @return [String]
676
702
  def new_inbox
677
- "_INBOX.#{@nuid.next}"
703
+ "#{@inbox_prefix}.#{@nuid.next}"
678
704
  end
679
705
 
680
706
  def connected_server
@@ -697,6 +723,19 @@ module NATS
697
723
  @status == CLOSED
698
724
  end
699
725
 
726
+ def draining?
727
+ if @status == DRAINING_PUBS or @status == DRAINING_SUBS
728
+ return true
729
+ end
730
+
731
+ is_draining = false
732
+ synchronize do
733
+ is_draining = true if @drain_t
734
+ end
735
+
736
+ is_draining
737
+ end
738
+
700
739
  def on_error(&callback)
701
740
  @err_cb = callback
702
741
  end
@@ -719,6 +758,19 @@ module NATS
719
758
  end
720
759
  end
721
760
 
761
+ # drain will put a connection into a drain state. All subscriptions will
762
+ # immediately be put into a drain state. Upon completion, the publishers
763
+ # will be drained and can not publish any additional messages. Upon draining
764
+ # of the publishers, the connection will be closed. Use the `on_close`
765
+ # callback option to know when the connection has moved from draining to closed.
766
+ def drain
767
+ return if draining?
768
+
769
+ synchronize do
770
+ @drain_t ||= Thread.new { do_drain }
771
+ end
772
+ end
773
+
722
774
  # Create a JetStream context.
723
775
  # @param opts [Hash] Options to customize the JetStream context.
724
776
  # @option params [String] :prefix JetStream API prefix to use for the requests.
@@ -733,6 +785,13 @@ module NATS
733
785
 
734
786
  private
735
787
 
788
+ def validate_settings!
789
+ raise(NATS::IO::ClientError, "custom inbox may not include '>'") if @inbox_prefix.include?(">")
790
+ raise(NATS::IO::ClientError, "custom inbox may not include '*'") if @inbox_prefix.include?("*")
791
+ raise(NATS::IO::ClientError, "custom inbox may not end in '.'") if @inbox_prefix.end_with?(".")
792
+ raise(NATS::IO::ClientError, "custom inbox may not begin with '.'") if @inbox_prefix.start_with?(".")
793
+ end
794
+
736
795
  def process_info(line)
737
796
  parsed_info = JSON.parse(line)
738
797
 
@@ -995,6 +1054,83 @@ module NATS
995
1054
  end
996
1055
  end
997
1056
 
1057
+ def drain_sub(sub)
1058
+ sid = nil
1059
+ closed = nil
1060
+ sub.synchronize do
1061
+ sid = sub.sid
1062
+ closed = sub.closed
1063
+ end
1064
+ return if closed
1065
+
1066
+ send_command("UNSUB #{sid}#{CR_LF}")
1067
+ @flush_queue << :drain
1068
+
1069
+ synchronize { sub = @subs[sid] }
1070
+ return unless sub
1071
+ end
1072
+
1073
+ def do_drain
1074
+ synchronize { @status = DRAINING_SUBS }
1075
+
1076
+ # Do unsubscribe protocol for all the susbcriptions, then have a single thread
1077
+ # waiting until all subs are done or drain timeout error reported to async error cb.
1078
+ subs = []
1079
+ @subs.each do |_, sub|
1080
+ next if sub == @resp_sub
1081
+ drain_sub(sub)
1082
+ subs << sub
1083
+ end
1084
+ force_flush!
1085
+
1086
+ # Wait until all subs have no pending messages.
1087
+ drain_timeout = MonotonicTime::now + @options[:drain_timeout]
1088
+ to_delete = []
1089
+
1090
+ loop do
1091
+ break if MonotonicTime::now > drain_timeout
1092
+ sleep 0.1
1093
+
1094
+ # Wait until all subs are done.
1095
+ @subs.each do |_, sub|
1096
+ if sub != @resp_sub and sub.pending_queue.size == 0
1097
+ to_delete << sub
1098
+ end
1099
+ end
1100
+ next if to_delete.empty?
1101
+
1102
+ to_delete.each do |sub|
1103
+ @subs.delete(sub.sid)
1104
+ # Stop messages delivery thread for async subscribers
1105
+ if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1106
+ sub.wait_for_msgs_t.exit
1107
+ sub.pending_queue.clear
1108
+ end
1109
+ end
1110
+ to_delete.clear
1111
+
1112
+ # Wait until only the resp mux is remaining or there are no subscriptions.
1113
+ if @subs.count == 1
1114
+ sid, sub = @subs.first
1115
+ if sub == @resp_sub
1116
+ break
1117
+ end
1118
+ elsif @subs.count == 0
1119
+ break
1120
+ end
1121
+ end
1122
+
1123
+ if MonotonicTime::now > drain_timeout
1124
+ e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1125
+ err_cb_call(self, e, nil) if @err_cb
1126
+ end
1127
+ synchronize { @status = DRAINING_PUBS }
1128
+
1129
+ # Remove resp mux handler in case there is one.
1130
+ unsubscribe(@resp_sub) if @resp_sub
1131
+ close
1132
+ end
1133
+
998
1134
  def send_flush_queue(s)
999
1135
  @flush_queue << s
1000
1136
  end
@@ -1037,16 +1173,18 @@ module NATS
1037
1173
  else
1038
1174
  cs[:auth_token] = @uri.user
1039
1175
  end
1040
- when @user_credentials
1176
+ when @user_jwt_cb && @signature_cb
1041
1177
  nonce = @server_info[:nonce]
1042
1178
  cs[:jwt] = @user_jwt_cb.call
1043
1179
  cs[:sig] = @signature_cb.call(nonce)
1044
- when @nkeys_seed
1180
+ when @user_nkey_cb && @signature_cb
1045
1181
  nonce = @server_info[:nonce]
1046
1182
  cs[:nkey] = @user_nkey_cb.call
1047
1183
  cs[:sig] = @signature_cb.call(nonce)
1048
1184
  end
1049
1185
 
1186
+ cs[:auth_token] = @auth_token if @auth_token
1187
+
1050
1188
  if @server_info[:headers]
1051
1189
  cs[:headers] = @server_info[:headers]
1052
1190
  cs[:no_responders] = if @options[:no_responders] == false
@@ -1150,22 +1288,7 @@ module NATS
1150
1288
  # Skip in case nothing remains pending already.
1151
1289
  next if @pending_queue.empty?
1152
1290
 
1153
- # FIXME: should limit how many commands to take at once
1154
- # since producers could be adding as many as possible
1155
- # until reaching the max pending queue size.
1156
- cmds = []
1157
- cmds << @pending_queue.pop until @pending_queue.empty?
1158
- begin
1159
- @io.write(cmds.join) unless cmds.empty?
1160
- rescue => e
1161
- synchronize do
1162
- @last_err = e
1163
- err_cb_call(self, e, nil) if @err_cb
1164
- end
1165
-
1166
- process_op_error(e)
1167
- return
1168
- end if @io
1291
+ force_flush!
1169
1292
 
1170
1293
  synchronize do
1171
1294
  @pending_size = 0
@@ -1173,6 +1296,25 @@ module NATS
1173
1296
  end
1174
1297
  end
1175
1298
 
1299
+ def force_flush!
1300
+ # FIXME: should limit how many commands to take at once
1301
+ # since producers could be adding as many as possible
1302
+ # until reaching the max pending queue size.
1303
+ cmds = []
1304
+ cmds << @pending_queue.pop until @pending_queue.empty?
1305
+ begin
1306
+ @io.write(cmds.join) unless cmds.empty?
1307
+ rescue => e
1308
+ synchronize do
1309
+ @last_err = e
1310
+ err_cb_call(self, e, nil) if @err_cb
1311
+ end
1312
+
1313
+ process_op_error(e)
1314
+ return
1315
+ end if @io
1316
+ end
1317
+
1176
1318
  def ping_interval_loop
1177
1319
  loop do
1178
1320
  sleep @options[:ping_interval]
@@ -1446,7 +1588,7 @@ module NATS
1446
1588
  # Prepares requests subscription that handles the responses
1447
1589
  # for the new style request response.
1448
1590
  def start_resp_mux_sub!
1449
- @resp_sub_prefix = "_INBOX.#{@nuid.next}"
1591
+ @resp_sub_prefix = new_inbox
1450
1592
  @resp_map = Hash.new { |h,k| h[k] = { }}
1451
1593
 
1452
1594
  @resp_sub = Subscription.new
@@ -1529,72 +1671,90 @@ module NATS
1529
1671
 
1530
1672
  case
1531
1673
  when @nkeys_seed
1532
- @user_nkey_cb = proc {
1533
- seed = File.read(@nkeys_seed).chomp
1534
- kp = NKEYS::from_seed(seed)
1674
+ @user_nkey_cb = nkey_cb_for_nkey_file(@nkeys_seed)
1675
+ @signature_cb = signature_cb_for_nkey_file(@nkeys_seed)
1676
+ when @user_credentials
1677
+ # When the credentials are within a single decorated file.
1678
+ @user_jwt_cb = jwt_cb_for_creds_file(@user_credentials)
1679
+ @signature_cb = signature_cb_for_creds_file(@user_credentials)
1680
+ end
1681
+ end
1535
1682
 
1536
- # Take a copy since original will be gone with the wipe.
1537
- pub_key = kp.public_key.dup
1538
- kp.wipe!
1683
+ def signature_cb_for_nkey_file(nkey)
1684
+ proc { |nonce|
1685
+ seed = File.read(nkey).chomp
1686
+ kp = NKEYS::from_seed(seed)
1687
+ raw_signed = kp.sign(nonce)
1688
+ kp.wipe!
1689
+ encoded = Base64.urlsafe_encode64(raw_signed)
1690
+ encoded.gsub('=', '')
1691
+ }
1692
+ end
1539
1693
 
1540
- pub_key
1541
- }
1694
+ def nkey_cb_for_nkey_file(nkey)
1695
+ proc {
1696
+ seed = File.read(nkey).chomp
1697
+ kp = NKEYS::from_seed(seed)
1542
1698
 
1543
- @signature_cb = proc { |nonce|
1544
- seed = File.read(@nkeys_seed).chomp
1545
- kp = NKEYS::from_seed(seed)
1546
- raw_signed = kp.sign(nonce)
1547
- kp.wipe!
1548
- encoded = Base64.urlsafe_encode64(raw_signed)
1549
- encoded.gsub('=', '')
1550
- }
1551
- when @user_credentials
1552
- # When the credentials are within a single decorated file.
1553
- @user_jwt_cb = proc {
1554
- jwt_start = "BEGIN NATS USER JWT".freeze
1555
- found = false
1556
- jwt = nil
1557
- File.readlines(@user_credentials).each do |line|
1558
- case
1559
- when found
1560
- jwt = line.chomp
1561
- break
1562
- when line.include?(jwt_start)
1563
- found = true
1564
- end
1699
+ # Take a copy since original will be gone with the wipe.
1700
+ pub_key = kp.public_key.dup
1701
+ kp.wipe!
1702
+
1703
+ pub_key
1704
+ }
1705
+ end
1706
+
1707
+ def jwt_cb_for_creds_file(creds)
1708
+ proc {
1709
+ jwt_start = "BEGIN NATS USER JWT".freeze
1710
+ found = false
1711
+ jwt = nil
1712
+
1713
+ File.readlines(creds).each do |line|
1714
+ case
1715
+ when found
1716
+ jwt = line.chomp
1717
+ break
1718
+ when line.include?(jwt_start)
1719
+ found = true
1565
1720
  end
1566
- raise(Error, "No JWT found in #{@user_credentials}") if not found
1721
+ end
1567
1722
 
1568
- jwt
1569
- }
1723
+ raise(Error, "No JWT found in #{creds}") if not found
1570
1724
 
1571
- @signature_cb = proc { |nonce|
1572
- seed_start = "BEGIN USER NKEY SEED".freeze
1573
- found = false
1574
- seed = nil
1575
- File.readlines(@user_credentials).each do |line|
1576
- case
1577
- when found
1578
- seed = line.chomp
1579
- break
1580
- when line.include?(seed_start)
1581
- found = true
1582
- end
1725
+ jwt
1726
+ }
1727
+ end
1728
+
1729
+ def signature_cb_for_creds_file(creds)
1730
+ proc { |nonce|
1731
+ seed_start = "BEGIN USER NKEY SEED".freeze
1732
+ found = false
1733
+ seed = nil
1734
+
1735
+ File.readlines(creds).each do |line|
1736
+ case
1737
+ when found
1738
+ seed = line.chomp
1739
+ break
1740
+ when line.include?(seed_start)
1741
+ found = true
1583
1742
  end
1584
- raise(Error, "No nkey user seed found in #{@user_credentials}") if not found
1743
+ end
1585
1744
 
1586
- kp = NKEYS::from_seed(seed)
1587
- raw_signed = kp.sign(nonce)
1745
+ raise(Error, "No nkey user seed found in #{creds}") if not found
1588
1746
 
1589
- # seed is a reference so also cleared when doing wipe,
1590
- # which can be done since Ruby strings are mutable.
1591
- kp.wipe
1592
- encoded = Base64.urlsafe_encode64(raw_signed)
1747
+ kp = NKEYS::from_seed(seed)
1748
+ raw_signed = kp.sign(nonce)
1593
1749
 
1594
- # Remove padding
1595
- encoded.gsub('=', '')
1596
- }
1597
- end
1750
+ # seed is a reference so also cleared when doing wipe,
1751
+ # which can be done since Ruby strings are mutable.
1752
+ kp.wipe
1753
+ encoded = Base64.urlsafe_encode64(raw_signed)
1754
+
1755
+ # Remove padding
1756
+ encoded.gsub('=', '')
1757
+ }
1598
1758
  end
1599
1759
 
1600
1760
  def process_uri(uris)
@@ -1655,6 +1815,7 @@ module NATS
1655
1815
  # Default IO timeouts
1656
1816
  DEFAULT_CONNECT_TIMEOUT = 2
1657
1817
  DEFAULT_READ_WRITE_TIMEOUT = 2
1818
+ DEFAULT_DRAIN_TIMEOUT = 30
1658
1819
 
1659
1820
  # Default Pending Limits
1660
1821
  DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
@@ -52,6 +52,12 @@ module NATS
52
52
 
53
53
  # When a subscription hits the pending messages limit.
54
54
  class SlowConsumer < Error; end
55
+
56
+ # When an action cannot be done because client is draining.
57
+ class ConnectionDrainingError < Error; end
58
+
59
+ # When drain takes too long to complete.
60
+ class DrainTimeoutError < Error; end
55
61
  end
56
62
 
57
63
  # Timeout is raised when the client gives up waiting for a response from a service.
data/lib/nats/io/js.rb CHANGED
@@ -14,7 +14,9 @@
14
14
  require_relative 'msg'
15
15
  require_relative 'client'
16
16
  require_relative 'errors'
17
+ require_relative 'kv'
17
18
  require 'time'
19
+ require 'base64'
18
20
 
19
21
  module NATS
20
22
 
@@ -49,6 +51,7 @@ module NATS
49
51
 
50
52
  # Include JetStream::Manager
51
53
  extend Manager
54
+ extend KeyValue::Manager
52
55
  end
53
56
 
54
57
  # PubAck is the API response from a successfully published message.
@@ -96,6 +99,119 @@ module NATS
96
99
  PubAck.new(result)
97
100
  end
98
101
 
102
+ # subscribe binds or creates a push subscription to a JetStream pull consumer.
103
+ #
104
+ # @param subject [String] Subject from which the messages will be fetched.
105
+ # @param params [Hash] Options to customize the PushSubscription.
106
+ # @option params [String] :stream Name of the Stream to which the consumer belongs.
107
+ # @option params [String] :consumer Name of the Consumer to which the PushSubscription will be bound.
108
+ # @option params [String] :durable Consumer durable name from where the messages will be fetched.
109
+ # @option params [Hash] :config Configuration for the consumer.
110
+ # @return [NATS::JetStream::PushSubscription]
111
+ def subscribe(subject, params={}, &cb)
112
+ params[:consumer] ||= params[:durable]
113
+ stream = params[:stream].nil? ? find_stream_name_by_subject(subject) : params[:stream]
114
+
115
+ queue = params[:queue]
116
+ durable = params[:durable]
117
+ flow_control = params[:flow_control]
118
+ manual_ack = params[:manual_ack]
119
+ idle_heartbeat = params[:idle_heartbeat]
120
+ flow_control = params[:flow_control]
121
+
122
+ if queue
123
+ if durable and durable != queue
124
+ raise NATS::JetStream::Error.new("nats: cannot create queue subscription '#{queue}' to consumer '#{durable}'")
125
+ else
126
+ durable = queue
127
+ end
128
+ end
129
+
130
+ cinfo = nil
131
+ consumer_found = false
132
+ should_create = false
133
+
134
+ if not durable
135
+ should_create = true
136
+ else
137
+ begin
138
+ cinfo = consumer_info(stream, durable)
139
+ config = cinfo.config
140
+ consumer_found = true
141
+ consumer = durable
142
+ rescue NATS::JetStream::Error::NotFound
143
+ should_create = true
144
+ consumer_found = false
145
+ end
146
+ end
147
+
148
+ if consumer_found
149
+ if not config.deliver_group
150
+ if queue
151
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription for a consumer without a deliver group")
152
+ elsif cinfo.push_bound
153
+ raise NATS::JetStream::Error.new("nats: consumer is already bound to a subscription")
154
+ end
155
+ else
156
+ if not queue
157
+ raise NATS::JetStream::Error.new("nats: cannot create a subscription for a consumer with a deliver group #{config.deliver_group}")
158
+ elsif queue != config.deliver_group
159
+ raise NATS::JetStream::Error.new("nats: cannot create a queue subscription #{queue} for a consumer with a deliver group #{config.deliver_group}")
160
+ end
161
+ end
162
+ elsif should_create
163
+ # Auto-create consumer if none found.
164
+ if config.nil?
165
+ # Defaults
166
+ config = JetStream::API::ConsumerConfig.new({ack_policy: "explicit"})
167
+ elsif config.is_a?(Hash)
168
+ config = JetStream::API::ConsumerConfig.new(config)
169
+ elsif !config.is_a?(JetStream::API::ConsumerConfig)
170
+ raise NATS::JetStream::Error.new("nats: invalid ConsumerConfig")
171
+ end
172
+
173
+ config.durable_name = durable if not config.durable_name
174
+ config.deliver_group = queue if not config.deliver_group
175
+
176
+ # Create inbox for push consumer.
177
+ deliver = @nc.new_inbox
178
+ config.deliver_subject = deliver
179
+
180
+ # Auto created consumers use the filter subject.
181
+ config.filter_subject = subject
182
+
183
+ # Heartbeats / FlowControl
184
+ config.flow_control = flow_control
185
+ if idle_heartbeat or config.idle_heartbeat
186
+ idle_heartbeat = config.idle_heartbeat if config.idle_heartbeat
187
+ idle_heartbeat = idle_heartbeat * 1_000_000_000
188
+ config.idle_heartbeat = idle_heartbeat
189
+ end
190
+
191
+ # Auto create the consumer.
192
+ cinfo = add_consumer(stream, config)
193
+ consumer = cinfo.name
194
+ end
195
+
196
+ # Enable auto acking for async callbacks unless disabled.
197
+ if cb and not manual_ack
198
+ ocb = cb
199
+ new_cb = proc do |msg|
200
+ ocb.call(msg)
201
+ msg.ack rescue JetStream::Error::MsgAlreadyAckd
202
+ end
203
+ cb = new_cb
204
+ end
205
+ sub = @nc.subscribe(config.deliver_subject, queue: config.deliver_group, &cb)
206
+ sub.extend(PushSubscription)
207
+ sub.jsi = JS::Sub.new(
208
+ js: self,
209
+ stream: stream,
210
+ consumer: consumer,
211
+ )
212
+ sub
213
+ end
214
+
99
215
  # pull_subscribe binds or creates a subscription to a JetStream pull consumer.
100
216
  #
101
217
  # @param subject [String] Subject from which the messages will be fetched.
@@ -103,6 +219,7 @@ module NATS
103
219
  # @param params [Hash] Options to customize the PullSubscription.
104
220
  # @option params [String] :stream Name of the Stream to which the consumer belongs.
105
221
  # @option params [String] :consumer Name of the Consumer to which the PullSubscription will be bound.
222
+ # @option params [Hash] :config Configuration for the consumer.
106
223
  # @return [NATS::JetStream::PullSubscription]
107
224
  def pull_subscribe(subject, durable, params={})
108
225
  raise JetStream::Error::InvalidDurableName.new("nats: invalid durable name") if durable.empty?
@@ -277,6 +394,14 @@ module NATS
277
394
  result[:streams].first
278
395
  end
279
396
 
397
+ def get_last_msg(stream_name, subject)
398
+ req_subject = "#{@prefix}.STREAM.MSG.GET.#{stream_name}"
399
+ req = {'last_by_subj': subject}
400
+ data = req.to_json
401
+ resp = api_request(req_subject, data)
402
+ JetStream::API::RawStreamMsg.new(resp[:message])
403
+ end
404
+
280
405
  private
281
406
 
282
407
  def api_request(req_subject, req="", params={})
@@ -293,6 +418,31 @@ module NATS
293
418
  end
294
419
  end
295
420
 
421
+ # PushSubscription is included into NATS::Subscription so that it
422
+ #
423
+ # @example Create a push subscription using JetStream context.
424
+ #
425
+ # require 'nats/client'
426
+ #
427
+ # nc = NATS.connect
428
+ # js = nc.jetstream
429
+ # sub = js.subscribe("foo", "bar")
430
+ # msg = sub.next_msg
431
+ # msg.ack
432
+ # sub.unsubscribe
433
+ #
434
+ # @!visibility public
435
+ module PushSubscription
436
+ # consumer_info retrieves the current status of the pull subscription consumer.
437
+ # @param params [Hash] Options to customize API request.
438
+ # @option params [Float] :timeout Time to wait for response.
439
+ # @return [JetStream::API::ConsumerInfo] The latest ConsumerInfo of the consumer.
440
+ def consumer_info(params={})
441
+ @jsi.js.consumer_info(@jsi.stream, @jsi.consumer, params)
442
+ end
443
+ end
444
+ private_constant :PushSubscription
445
+
296
446
  # PullSubscription is included into NATS::Subscription so that it
297
447
  # can be used to fetch messages from a pull based consumer from
298
448
  # JetStream.
@@ -617,7 +767,11 @@ module NATS
617
767
  private
618
768
 
619
769
  def ensure_is_acked_once!
620
- @sub.synchronize { raise JetStream::Error::InvalidJSAck.new("nats: invalid ack") if @ackd }
770
+ @sub.synchronize do
771
+ if @ackd
772
+ raise JetStream::Error::MsgAlreadyAckd.new("nats: message was already acknowledged: #{self}")
773
+ end
774
+ end
621
775
  end
622
776
 
623
777
  def parse_metadata(reply)
@@ -771,9 +925,12 @@ module NATS
771
925
  # When an invalid durable or consumer name was attempted to be used.
772
926
  class InvalidDurableName < Error; end
773
927
 
774
- # When an ack is no longer valid, for example when it has been acked already.
928
+ # When an ack not longer valid.
775
929
  class InvalidJSAck < Error; end
776
930
 
931
+ # When an ack has already been acked.
932
+ class MsgAlreadyAckd < Error; end
933
+
777
934
  # When the delivered message does not behave as a message delivered by JetStream,
778
935
  # for example when the ack reply has unrecognizable fields.
779
936
  class NotJSMessage < Error; end
@@ -1085,6 +1242,33 @@ module NATS
1085
1242
  freeze
1086
1243
  end
1087
1244
  end
1245
+
1246
+ RawStreamMsg = Struct.new(:subject, :seq, :data, :headers, keyword_init: true) do
1247
+ def initialize(opts)
1248
+ opts[:data] = Base64.decode64(opts[:data]) if opts[:data]
1249
+ if opts[:hdrs]
1250
+ header = Base64.decode64(opts[:hdrs])
1251
+ hdr = {}
1252
+ lines = header.lines
1253
+ lines.slice(1, header.size).each do |line|
1254
+ line.rstrip!
1255
+ next if line.empty?
1256
+ key, value = line.strip.split(/\s*:\s*/, 2)
1257
+ hdr[key] = value
1258
+ end
1259
+ opts[:headers] = hdr
1260
+ end
1261
+
1262
+ # Filter out members not present.
1263
+ rem = opts.keys - members
1264
+ opts.delete_if { |k| rem.include?(k) }
1265
+ super(opts)
1266
+ end
1267
+
1268
+ def sequence
1269
+ self.seq
1270
+ end
1271
+ end
1088
1272
  end
1089
1273
  end
1090
1274
  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.0.0-alpha".freeze
18
+ VERSION = "2.0.0-rc1".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.0.0.pre.alpha
4
+ version: 2.0.0.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Waldemar Quevedo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-27 00:00:00.000000000 Z
11
+ date: 2021-12-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: NATS is an open-source, high-performance, lightweight cloud messaging
14
14
  system.