@@ -46,7 +46,26 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
4646
4747 self ._timeout_ceil_threshold : Optional [float ] = 5
4848
49- self .closed : asyncio .Future [None ] = self ._loop .create_future ()
49+ self ._closed : Union [None , asyncio .Future [None ]] = None
50+ self ._connection_lost_called = False
51+
52+ @property
53+ def closed (self ) -> Union [None , asyncio .Future [None ]]:
54+ """Future that is set when the connection is closed.
55+
56+ This property returns a Future that will be completed when the connection
57+ is closed. The Future is created lazily on first access to avoid creating
58+ futures that will never be awaited.
59+
60+ Returns:
61+ - A Future[None] if the connection is still open or was closed after
62+ this property was accessed
63+ - None if connection_lost() was already called before this property
64+ was ever accessed (indicating no one is waiting for the closure)
65+ """
66+ if self ._closed is None and not self ._connection_lost_called :
67+ self ._closed = self ._loop .create_future ()
68+ return self ._closed
5069
5170 @property
5271 def upgraded (self ) -> bool :
@@ -80,30 +99,31 @@ def is_connected(self) -> bool:
8099 return self .transport is not None and not self .transport .is_closing ()
81100
82101 def connection_lost (self , exc : Optional [BaseException ]) -> None :
102+ self ._connection_lost_called = True
83103 self ._drop_timeout ()
84104
85105 original_connection_error = exc
86106 reraised_exc = original_connection_error
87107
88108 connection_closed_cleanly = original_connection_error is None
89109
90- if connection_closed_cleanly :
91- set_result ( self . closed , None )
92- else :
93- assert original_connection_error is not None
94- set_exception (
95- self . closed ,
96- ClientConnectionError (
97- f"Connection lost: { original_connection_error !s } " ,
98- ),
99- original_connection_error ,
100- )
101- # Mark the exception as retrieved to prevent
102- # "Future exception was never retrieved" warnings
103- # The exception is always passed on through
104- # other means, so this is safe
105- with suppress ( Exception ):
106- self . closed . exception ( )
110+ if self . _closed is not None :
111+ # If someone is waiting for the closed future,
112+ # we should set it to None or an exception. If
113+ # self._closed is None, it means that
114+ # connection_lost() was called already
115+ # or nobody is waiting for it.
116+ if connection_closed_cleanly :
117+ set_result ( self . _closed , None )
118+ else :
119+ assert original_connection_error is not None
120+ set_exception (
121+ self . _closed ,
122+ ClientConnectionError (
123+ f"Connection lost: { original_connection_error !s } " ,
124+ ),
125+ original_connection_error ,
126+ )
107127
108128 if self ._payload_parser is not None :
109129 with suppress (Exception ): # FIXME: log this somehow?
0 commit comments