@@ -82,19 +82,65 @@ class SNMPReadOnly(SNMPException):
8282del obj
8383
8484
85+ class _LoopGuard :
86+ """Cancel pending tasks and close the event loop on thread exit.
87+
88+ pysnmp's AsyncioDispatcher creates a background handle_timeout()
89+ task that runs forever. When a thread exits, this task must be
90+ properly cancelled and awaited — otherwise asyncio emits "Task
91+ was destroyed but it is pending!" warnings.
92+
93+ This guard is stored in thread-local data alongside the engine
94+ and event loop. Because nothing else references it, it is the
95+ first object to reach refcount 0 when the thread-local dict is
96+ cleared, so its __del__ runs before the engine's dispatcher
97+ tries to clean up the same tasks."""
98+
99+ def __init__ (self , loop ):
100+ self ._loop = loop
101+
102+ def __del__ (self ):
103+ loop = self ._loop
104+ if loop .is_closed ():
105+ return
106+ pending = asyncio .all_tasks (loop )
107+ for task in pending :
108+ task .cancel ()
109+ if pending :
110+ try :
111+ loop .run_until_complete (
112+ asyncio .gather (* pending , return_exceptions = True ))
113+ except RuntimeError :
114+ pass
115+ loop .close ()
116+
117+
118+ class _SnimpyEngine :
119+ """Manage a per-thread SnmpEngine and event loop."""
120+
121+ _tls = threading .local ()
122+
123+ @classmethod
124+ def get (cls ):
125+ """Return the per-thread (SnmpEngine, loop) pair."""
126+ if not hasattr (cls ._tls , "engine" ):
127+ cls ._tls .loop = asyncio .new_event_loop ()
128+ asyncio .set_event_loop (cls ._tls .loop )
129+ cls ._tls .engine = SnmpEngine ()
130+ cls ._tls .guard = _LoopGuard (cls ._tls .loop )
131+ return cls ._tls .engine , cls ._tls .loop
132+
133+
85134class Session :
86135
87136 """SNMP session. An instance of this object will represent an SNMP
88137 session. From such an instance, one can get information from the
89138 associated agent."""
90139
91- _tls = threading .local ()
92-
93140 def _run (self , coro ):
94- """Run an async coroutine synchronously using a thread-local loop."""
95- if not hasattr (self ._tls , "loop" ):
96- self ._tls .loop = asyncio .new_event_loop ()
97- return self ._tls .loop .run_until_complete (coro )
141+ """Run an async coroutine synchronously."""
142+ _ , loop = _SnimpyEngine .get ()
143+ return loop .run_until_complete (coro )
98144
99145 def __init__ (self , host ,
100146 community = "public" , version = 2 ,
@@ -148,14 +194,8 @@ def __init__(self, host,
148194 self ._host = host
149195 self ._version = version
150196 self ._none = none
151- if version == 3 :
152- self ._engine = SnmpEngine ()
153- self ._contextname = contextname
154- else :
155- if not hasattr (self ._tls , "engine" ):
156- self ._tls .engine = SnmpEngine ()
157- self ._engine = self ._tls .engine
158- self ._contextname = None
197+ self ._engine , _ = _SnimpyEngine .get ()
198+ self ._contextname = contextname if version == 3 else None
159199 if version == 1 and none :
160200 raise ValueError ("None-GET requests not compatible with SNMPv1" )
161201
0 commit comments