@@ -271,6 +271,22 @@ class Coupling:
271271 substitutions : list [tuple [str , int ]]
272272
273273
274+ @dataclass
275+ class SharedSubjectListener :
276+ """One transport listener shared by all topics bound to the same subject-ID."""
277+
278+ handle : Closable
279+ owners : set [Topic ] = field (default_factory = set )
280+
281+
282+ @dataclass
283+ class SharedSubjectWriter :
284+ """One transport writer shared by all topics bound to the same subject-ID."""
285+
286+ handle : SubjectWriter
287+ owners : set [Topic ] = field (default_factory = set )
288+
289+
274290@dataclass
275291class PublishTracker :
276292 """Tracks a pending reliable publication awaiting ACKs."""
@@ -365,34 +381,32 @@ def tag_seqno(self, tag: int) -> int:
365381
366382 def ensure_writer (self ) -> SubjectWriter :
367383 if self .pub_writer is None :
368- self .pub_writer = self ._node .transport .subject_advertise (self .subject_id )
369- _logger .info ("Writer created for '%s' sid=%d" , self ._name , self .subject_id )
384+ sid = self .subject_id
385+ self .pub_writer = self ._node .acquire_subject_writer (self , sid )
386+ _logger .info ("Writer acquired for '%s' sid=%d" , self ._name , sid )
370387 return self .pub_writer
371388
372389 def ensure_listener (self ) -> None :
373390 if self .sub_listener is None and self .couplings :
374391 sid = self .subject_id
375-
376- def handler (arrival : TransportArrival ) -> None :
377- self ._node .on_subject_arrival (sid , arrival )
378-
379- self .sub_listener = self ._node .transport .subject_listen (sid , handler )
380- _logger .info ("Listener created for '%s' sid=%d" , self ._name , sid )
392+ self .sub_listener = self ._node .acquire_subject_listener (self , sid )
393+ _logger .info ("Listener acquired for '%s' sid=%d" , self ._name , sid )
381394
382395 def sync_listener (self ) -> None :
383396 if self .couplings :
384397 self .ensure_listener ()
385398 elif self .sub_listener is not None :
386- self .sub_listener . close ( )
399+ self ._node . release_subject_listener ( self , self . subject_id )
387400 self .sub_listener = None
388401 _logger .info ("Listener released for '%s'" , self ._name )
389402
390403 def release_transport_handles (self ) -> None :
404+ sid = self .subject_id
391405 if self .pub_writer is not None :
392- self .pub_writer . close ( )
406+ self ._node . release_subject_writer ( self , sid )
393407 self .pub_writer = None
394408 if self .sub_listener is not None :
395- self .sub_listener . close ( )
409+ self ._node . release_subject_listener ( self , sid )
396410 self .sub_listener = None
397411
398412 def compute_is_implicit (self ) -> bool :
@@ -465,6 +479,8 @@ def broadcast_handler(arrival: TransportArrival) -> None:
465479 # Gossip shard state: lazily created per shard.
466480 self .gossip_shard_writers : dict [int , SubjectWriter ] = {}
467481 self .gossip_shard_listeners : dict [int , Closable ] = {}
482+ self .shared_subject_writers : dict [int , SharedSubjectWriter ] = {}
483+ self .shared_subject_listeners : dict [int , SharedSubjectListener ] = {}
468484
469485 # Register unicast handler.
470486 transport .unicast_listen (self .on_unicast_arrival )
@@ -600,13 +616,11 @@ def topic_allocate(self, topic: TopicImpl, new_evictions: int, now: float) -> No
600616 elif left_wins (t .lage (now ), t .hash , collider .lage (now ), collider .hash ):
601617 # Our topic wins: take the slot, evict the collider.
602618 t .release_transport_handles ()
603- # Preserve the collider's writer if we can reuse it.
604- if collider .pub_writer is not None :
605- t .pub_writer = collider .pub_writer
606- collider .pub_writer = None
607619 t .evictions = ev
608620 del self .topics_by_subject_id [new_sid ]
609621 self .topics_by_subject_id [new_sid ] = t
622+ if collider .pub_writer is not None :
623+ t .pub_writer = self .acquire_subject_writer (t , new_sid )
610624 t .sync_listener ()
611625 self .schedule_gossip_urgent (t )
612626 # Schedule collider for reallocation.
@@ -708,6 +722,48 @@ def handler(arrival: TransportArrival) -> None:
708722 _logger .debug ("Gossip shard writer/listener for sid=%d" , shard_sid )
709723 return writer
710724
725+ def acquire_subject_writer (self , topic : TopicImpl , subject_id : int ) -> SubjectWriter :
726+ entry = self .shared_subject_writers .get (subject_id )
727+ if entry is None :
728+ entry = SharedSubjectWriter (handle = self .transport .subject_advertise (subject_id ))
729+ self .shared_subject_writers [subject_id ] = entry
730+ _logger .debug ("Shared subject writer created sid=%d" , subject_id )
731+ entry .owners .add (topic )
732+ return entry .handle
733+
734+ def release_subject_writer (self , topic : TopicImpl , subject_id : int ) -> None :
735+ entry = self .shared_subject_writers .get (subject_id )
736+ if entry is None :
737+ return
738+ entry .owners .discard (topic )
739+ if not entry .owners :
740+ entry .handle .close ()
741+ del self .shared_subject_writers [subject_id ]
742+ _logger .debug ("Shared subject writer released sid=%d" , subject_id )
743+
744+ def acquire_subject_listener (self , topic : TopicImpl , subject_id : int ) -> Closable :
745+ entry = self .shared_subject_listeners .get (subject_id )
746+ if entry is None :
747+
748+ def handler (arrival : TransportArrival ) -> None :
749+ self .on_subject_arrival (subject_id , arrival )
750+
751+ entry = SharedSubjectListener (handle = self .transport .subject_listen (subject_id , handler ))
752+ self .shared_subject_listeners [subject_id ] = entry
753+ _logger .debug ("Shared subject listener created sid=%d" , subject_id )
754+ entry .owners .add (topic )
755+ return entry .handle
756+
757+ def release_subject_listener (self , topic : TopicImpl , subject_id : int ) -> None :
758+ entry = self .shared_subject_listeners .get (subject_id )
759+ if entry is None :
760+ return
761+ entry .owners .discard (topic )
762+ if not entry .owners :
763+ entry .handle .close ()
764+ del self .shared_subject_listeners [subject_id ]
765+ _logger .debug ("Shared subject listener released sid=%d" , subject_id )
766+
711767 def schedule_gossip (self , topic : TopicImpl ) -> None :
712768 """Start periodic gossip for an explicit topic."""
713769 if topic .gossip_task is not None :
@@ -1304,8 +1360,14 @@ def close(self) -> None:
13041360 topic .release_transport_handles ()
13051361 self .broadcast_writer .close ()
13061362 self .broadcast_listener .close ()
1363+ for shared_writer in list (self .shared_subject_writers .values ()):
1364+ shared_writer .handle .close ()
1365+ self .shared_subject_writers .clear ()
1366+ for shared_listener in list (self .shared_subject_listeners .values ()):
1367+ shared_listener .handle .close ()
1368+ self .shared_subject_listeners .clear ()
13071369 for w in self .gossip_shard_writers .values ():
13081370 w .close ()
1309- for listener in self .gossip_shard_listeners .values ():
1310- listener .close ()
1371+ for gossip_listener in self .gossip_shard_listeners .values ():
1372+ gossip_listener .close ()
13111373 self .transport .close ()
0 commit comments