@@ -358,6 +358,15 @@ async def subscribe_bind(
358
358
sub = sub ,
359
359
ccreq = config ,
360
360
)
361
+
362
+ if config .idle_heartbeat :
363
+ sub ._jsi ._hbtask = asyncio .create_task (sub ._jsi .activity_check ())
364
+
365
+ if ordered_consumer :
366
+ sub ._jsi ._fctask = asyncio .create_task (
367
+ sub ._jsi .check_flow_control_response ()
368
+ )
369
+
361
370
return psub
362
371
363
372
@staticmethod
@@ -375,7 +384,7 @@ async def new_callback(msg: Msg) -> None:
375
384
async def pull_subscribe (
376
385
self ,
377
386
subject : str ,
378
- durable : str ,
387
+ durable : Optional [ str ] = None ,
379
388
stream : Optional [str ] = None ,
380
389
config : Optional [api .ConsumerConfig ] = None ,
381
390
pending_msgs_limit : int = DEFAULT_JS_SUB_PENDING_MSGS_LIMIT ,
@@ -415,16 +424,30 @@ async def main():
415
424
if stream is None :
416
425
stream = await self ._jsm .find_stream_name_by_subject (subject )
417
426
427
+ should_create = True
418
428
try :
419
- # TODO: Detect configuration drift with the consumer.
420
- await self ._jsm .consumer_info (stream , durable )
429
+ if durable :
430
+ await self ._jsm .consumer_info (stream , durable )
431
+ should_create = False
421
432
except nats .js .errors .NotFoundError :
433
+ pass
434
+
435
+ consumer_name = durable
436
+ if should_create :
422
437
# If not found then attempt to create with the defaults.
423
438
if config is None :
424
439
config = api .ConsumerConfig ()
440
+
425
441
# Auto created consumers use the filter subject.
442
+ # config.name = durable
426
443
config .filter_subject = subject
427
- config .durable_name = durable
444
+ if durable :
445
+ config .name = durable
446
+ config .durable_name = durable
447
+ else :
448
+ consumer_name = self ._nc ._nuid .next ().decode ()
449
+ config .name = consumer_name
450
+
428
451
await self ._jsm .add_consumer (stream , config = config )
429
452
430
453
return await self .pull_subscribe_bind (
@@ -433,6 +456,7 @@ async def main():
433
456
inbox_prefix = inbox_prefix ,
434
457
pending_bytes_limit = pending_bytes_limit ,
435
458
pending_msgs_limit = pending_msgs_limit ,
459
+ name = consumer_name ,
436
460
)
437
461
438
462
async def pull_subscribe_bind (
@@ -442,6 +466,7 @@ async def pull_subscribe_bind(
442
466
inbox_prefix : bytes = api .INBOX_PREFIX ,
443
467
pending_msgs_limit : int = DEFAULT_JS_SUB_PENDING_MSGS_LIMIT ,
444
468
pending_bytes_limit : int = DEFAULT_JS_SUB_PENDING_BYTES_LIMIT ,
469
+ name : Optional [str ] = None ,
445
470
) -> JetStreamContext .PullSubscription :
446
471
"""
447
472
pull_subscribe returns a `PullSubscription` that can be delivered messages
@@ -475,11 +500,16 @@ async def main():
475
500
pending_msgs_limit = pending_msgs_limit ,
476
501
pending_bytes_limit = pending_bytes_limit
477
502
)
503
+ consumer = None
504
+ if durable :
505
+ consumer = durable
506
+ else :
507
+ consumer = name
478
508
return JetStreamContext .PullSubscription (
479
509
js = self ,
480
510
sub = sub ,
481
511
stream = stream ,
482
- consumer = durable ,
512
+ consumer = consumer ,
483
513
deliver = deliver ,
484
514
)
485
515
@@ -533,18 +563,76 @@ def __init__(
533
563
self ._sub = sub
534
564
self ._ccreq = ccreq
535
565
566
+ # Heartbeat
567
+ self ._hbtask = None
568
+ self ._hbi = None
569
+ if ccreq and ccreq .idle_heartbeat :
570
+ self ._hbi = ccreq .idle_heartbeat
571
+
572
+ # Ordered Consumer implementation.
536
573
self ._dseq = 1
537
574
self ._sseq = 0
538
575
self ._cmeta : Optional [str ] = None
576
+ self ._fcr : Optional [str ] = None
577
+ self ._fcd = 0
539
578
self ._fciseq = 0
540
- self ._active : Optional [bool ] = None
579
+ self ._active : Optional [bool ] = True
580
+ self ._fctask = None
541
581
542
582
def track_sequences (self , reply : str ) -> None :
543
583
self ._fciseq += 1
544
584
self ._cmeta = reply
545
585
546
586
def schedule_flow_control_response (self , reply : str ) -> None :
547
- pass
587
+ self ._active = True
588
+ self ._fcr = reply
589
+ self ._fcd = self ._fciseq
590
+
591
+ def get_js_delivered (self ):
592
+ if self ._sub ._cb :
593
+ return self ._sub .delivered
594
+ return self ._fciseq - self ._sub ._pending_queue .qsize ()
595
+
596
+ async def activity_check (self ):
597
+ # Can at most miss two heartbeats.
598
+ hbc_threshold = 2
599
+ while True :
600
+ try :
601
+ if self ._conn .is_closed :
602
+ break
603
+
604
+ # Wait for all idle heartbeats to be received,
605
+ # one of them would have toggled the state of the
606
+ # consumer back to being active.
607
+ await asyncio .sleep (self ._hbi * hbc_threshold )
608
+ active = self ._active
609
+ self ._active = False
610
+ if not active :
611
+ if self ._ordered :
612
+ await self .reset_ordered_consumer (
613
+ self ._sseq + 1
614
+ )
615
+ except asyncio .CancelledError :
616
+ break
617
+
618
+ async def check_flow_control_response (self ):
619
+ while True :
620
+ try :
621
+ if self ._conn .is_closed :
622
+ break
623
+
624
+ if (self ._fciseq - self ._psub ._pending_queue .qsize ()) >= self ._fcd :
625
+ fc_reply = self ._fcr
626
+ try :
627
+ if fc_reply :
628
+ await self ._conn .publish (fc_reply )
629
+ except Exception :
630
+ pass
631
+ self ._fcr = None
632
+ self ._fcd = 0
633
+ await asyncio .sleep (0.25 )
634
+ except asyncio .CancelledError :
635
+ break
548
636
549
637
async def check_for_sequence_mismatch (self ,
550
638
msg : Msg ) -> Optional [bool ]:
@@ -684,6 +772,38 @@ def delivered(self) -> int:
684
772
"""
685
773
return self ._sub ._received
686
774
775
+ @delivered .setter
776
+ def delivered (self , value ):
777
+ self ._sub ._received = value
778
+
779
+ @property
780
+ def _pending_size (self ):
781
+ return self ._sub ._pending_size
782
+
783
+ @_pending_size .setter
784
+ def _pending_size (self , value ):
785
+ self ._sub ._pending_size = value
786
+
787
+ async def next_msg (self , timeout : Optional [float ] = 1.0 ) -> Msg :
788
+ """
789
+ :params timeout: Time in seconds to wait for next message before timing out.
790
+ :raises nats.errors.TimeoutError:
791
+
792
+ next_msg can be used to retrieve the next message from a stream of messages using
793
+ await syntax, this only works when not passing a callback on `subscribe`::
794
+ """
795
+ msg = await self ._sub .next_msg (timeout )
796
+
797
+ # In case there is a flow control reply present need to handle here.
798
+ if self ._sub and self ._sub ._jsi :
799
+ self ._sub ._jsi ._active = True
800
+ if self ._sub ._jsi .get_js_delivered () >= self ._sub ._jsi ._fciseq :
801
+ fc_reply = self ._sub ._jsi ._fcr
802
+ if fc_reply :
803
+ await self ._conn .publish (fc_reply )
804
+ self ._sub ._jsi ._fcr = None
805
+ return msg
806
+
687
807
class PullSubscription :
688
808
"""
689
809
PullSubscription is a subscription that can fetch messages.
0 commit comments