@@ -16,43 +16,79 @@ class ClientConnections:
1616 thread safe.
1717 """
1818
19- def __init__ (self , logger ):
19+ def __init__ (self , logger , loop ):
2020 """
2121 Initialize empty client dictionary
22+
23+ :param logger: System logger.
24+ :param loop: Main asyncio event loop.
2225 """
2326 self ._clients = dict ()
27+ self ._saved_messages = dict ()
2428 self ._logger = logger
29+ self ._loop = loop
2530
2631 def add_client (self , id ):
2732 """
2833 Register new client which wants to receive messages to identifier 'id'.
29- Only one subscriber per stream is allowed. Latter overrides previous one.
34+ If there are any such messages, they are sent immediately. There can be
35+ more subscribers per stream.
3036
3137 :param id: Identifier of required stream of messages
3238 :return: Returns new asyncio.Queue on which can be wait for by
3339 'yield from' command.
3440 """
3541 new_queue = asyncio .Queue ()
36- self ._clients [id ] = new_queue
42+ if id not in self ._clients .keys ():
43+ self ._clients [id ] = []
44+ self ._clients [id ].append (new_queue )
45+
46+ # if there are already any messages, send them
47+ if id in self ._saved_messages .keys ():
48+ for msg in self ._saved_messages [id ]:
49+ new_queue .put_nowait (msg )
50+
3751 self ._logger .debug ("client connection: new client '{}' registered" .format (id ))
3852 return new_queue
3953
40- def remove_client (self , id ):
54+ def remove_channel (self , id ):
4155 """
42- Remove client listening on 'id' message stream . This means removing associated
43- queue and deleting the entry from internal dictionary .
44- If no such client exists, nothing is done .
56+ Remove all clients listening on 'id' channel . This means removing all associated
57+ queues and received messages. If no such channel exists, nothing is done .
58+ This method is called 5 minutes after last message of each channel .
4559
4660 :param id: Identifier of required stream of messages
4761 :return: Nothing
4862 """
63+ if id in self ._saved_messages .keys ():
64+ del self ._saved_messages [id ]
65+
4966 if id in self ._clients .keys ():
5067 del self ._clients [id ]
68+ self ._logger .debug ("client connection: channel '{}' removed" .format (id ))
69+ else :
70+ self ._logger .debug ("client connection: channel '{}' removing failed - "
71+ " not present" .format (id ))
72+
73+ def remove_client (self , id , queue ):
74+ """
75+ Remove client listening on 'id' message stream with queue 'queue'.
76+ This means removing associated queue and deleting the entry from internal dictionary.
77+ If no such client exists, nothing is done.
78+
79+ :param id: Identifier of required stream of messages
80+ :param queue: Queue associated with client to be removed
81+ :return: Nothing
82+ """
83+ if id in self ._clients .keys ():
84+ clients = self ._clients [id ]
85+ clients .remove (queue )
5186 self ._logger .debug ("client connection: client '{}' removed" .format (id ))
5287 else :
5388 self ._logger .debug ("client connection: client '{}' removing failed - "
5489 " not present" .format (id ))
5590
91+
5692 def remove_all_clients (self ):
5793 """
5894 Remove all registered clients. This method could be called on app
@@ -61,26 +97,31 @@ def remove_all_clients(self):
6197 :return: Nothing
6298 """
6399 self ._clients .clear ()
100+ self ._saved_messages .clear ()
64101 self ._logger .debug ("client connection: all clients removed" )
65102
66103 def send_message (self , id , message ):
67104 """
68105 Send 'message' to client listening on stream with 'id'. If 'id' is not
69- known, the message is silently dropped. The message is put into queue,
70- so no message will get lost.
106+ known, the message is saved for latter use. Messages for connected
107+ clients are put into queues, so no message will get lost.
71108
72109 :param id: Identifier of required stream of messages
73110 :param message: String containing text to be sent
74- :return: Returns True if message was sent, False otherwise
111+ :return: Nothing
75112 """
113+
114+ if id not in self ._saved_messages .keys ():
115+ self ._saved_messages [id ] = []
116+ self ._saved_messages [id ].append (message )
117+
76118 if id in self ._clients .keys ():
77- queue = self ._clients [id ]
78- queue .put_nowait (message )
79- return True
80- else :
81- self ._logger .warning ("client connection: Dropping message '{}' for "
82- "non-existing stream '{}'" .format (message , id ))
83- return False
119+ for queue in self ._clients [id ]:
120+ queue .put_nowait (message )
121+
122+ # on last message schedule removing whole channel after 5 minute wait
123+ if message is None :
124+ self ._loop .call_later (5 * 60 , self .remove_channel , id )
84125
85126
86127class WebsocketServer (threading .Thread ):
@@ -118,8 +159,8 @@ def connection_handler(self, websocket, path):
118159
119160 :param websocket: Socket with request
120161 :param path: Requested path of socket (not used)
121- :return: Returns when socket is closed or future from ClientConnections
122- is cancelled .
162+ :return: Returns when socket is closed or poison pill is found in message queue
163+ from ClientConnections .
123164 """
124165 wanted_id = None
125166 try :
@@ -138,7 +179,8 @@ def connection_handler(self, websocket, path):
138179 except websockets .ConnectionClosed :
139180 self ._logger .info ("websocket server: connection closed for channel '{}'" . format (wanted_id ))
140181 finally :
141- self ._connections .remove_client (wanted_id )
182+ self ._connections .remove_client (wanted_id , queue )
183+
142184
143185 def run (self ):
144186 """
0 commit comments