2828except ImportError :
2929 import _thread as thread
3030
31+ try :
32+ from collections .abc import MutableMapping
33+ except ImportError :
34+ from collections import MutableMapping
35+
3136from newrelic .core .config import global_settings
3237from newrelic .core .loop_node import LoopNode
3338
@@ -92,15 +97,15 @@ class TraceCacheActiveTraceError(RuntimeError):
9297 pass
9398
9499
95- class TraceCache (object ):
100+ class TraceCache (MutableMapping ):
96101 asyncio = cached_module ("asyncio" )
97102 greenlet = cached_module ("greenlet" )
98103
99104 def __init__ (self ):
100105 self ._cache = weakref .WeakValueDictionary ()
101106
102107 def __repr__ (self ):
103- return "<%s object at 0x%x %s>" % (self .__class__ .__name__ , id (self ), str (dict (self ._cache . items ())))
108+ return "<%s object at 0x%x %s>" % (self .__class__ .__name__ , id (self ), str (dict (self .items ())))
104109
105110 def current_thread_id (self ):
106111 """Returns the thread ID for the caller.
@@ -135,22 +140,22 @@ def current_thread_id(self):
135140 def task_start (self , task ):
136141 trace = self .current_trace ()
137142 if trace :
138- self . _cache [id (task )] = trace
143+ self [id (task )] = trace
139144
140145 def task_stop (self , task ):
141- self ._cache . pop (id (task ), None )
146+ self .pop (id (task ), None )
142147
143148 def current_transaction (self ):
144149 """Return the transaction object if one exists for the currently
145150 executing thread.
146151
147152 """
148153
149- trace = self ._cache . get (self .current_thread_id ())
154+ trace = self .get (self .current_thread_id ())
150155 return trace and trace .transaction
151156
152157 def current_trace (self ):
153- return self ._cache . get (self .current_thread_id ())
158+ return self .get (self .current_thread_id ())
154159
155160 def active_threads (self ):
156161 """Returns an iterator over all current stack frames for all
@@ -169,7 +174,7 @@ def active_threads(self):
169174 # First yield up those for real Python threads.
170175
171176 for thread_id , frame in sys ._current_frames ().items ():
172- trace = self ._cache . get (thread_id )
177+ trace = self .get (thread_id )
173178 transaction = trace and trace .transaction
174179 if transaction is not None :
175180 if transaction .background_task :
@@ -197,7 +202,7 @@ def active_threads(self):
197202 debug = global_settings ().debug
198203
199204 if debug .enable_coroutine_profiling :
200- for thread_id , trace in list ( self ._cache . items () ):
205+ for thread_id , trace in self .items ():
201206 transaction = trace .transaction
202207 if transaction and transaction ._greenlet is not None :
203208 gr = transaction ._greenlet ()
@@ -212,7 +217,7 @@ def prepare_for_root(self):
212217 trace in the cache is from a different task (for asyncio). Returns the
213218 current trace after the cache is updated."""
214219 thread_id = self .current_thread_id ()
215- trace = self ._cache . get (thread_id )
220+ trace = self .get (thread_id )
216221 if not trace :
217222 return None
218223
@@ -221,11 +226,11 @@ def prepare_for_root(self):
221226
222227 task = current_task (self .asyncio )
223228 if task is not None and id (trace ._task ) != id (task ):
224- self ._cache . pop (thread_id , None )
229+ self .pop (thread_id , None )
225230 return None
226231
227232 if trace .root and trace .root .exited :
228- self ._cache . pop (thread_id , None )
233+ self .pop (thread_id , None )
229234 return None
230235
231236 return trace
@@ -240,8 +245,8 @@ def save_trace(self, trace):
240245
241246 thread_id = trace .thread_id
242247
243- if thread_id in self . _cache :
244- cache_root = self . _cache [thread_id ].root
248+ if thread_id in self :
249+ cache_root = self [thread_id ].root
245250 if cache_root and cache_root is not trace .root and not cache_root .exited :
246251 # Cached trace exists and has a valid root still
247252 _logger .error (
@@ -253,7 +258,7 @@ def save_trace(self, trace):
253258
254259 raise TraceCacheActiveTraceError ("transaction already active" )
255260
256- self . _cache [thread_id ] = trace
261+ self [thread_id ] = trace
257262
258263 # We judge whether we are actually running in a coroutine by
259264 # seeing if the current thread ID is actually listed in the set
@@ -284,7 +289,7 @@ def pop_current(self, trace):
284289
285290 thread_id = trace .thread_id
286291 parent = trace .parent
287- self . _cache [thread_id ] = parent
292+ self [thread_id ] = parent
288293
289294 def complete_root (self , root ):
290295 """Completes a trace specified by the given root
@@ -301,7 +306,7 @@ def complete_root(self, root):
301306 to_complete = []
302307
303308 for task_id in task_ids :
304- entry = self ._cache . get (task_id )
309+ entry = self .get (task_id )
305310
306311 if entry and entry is not root and entry .root is root :
307312 to_complete .append (entry )
@@ -316,12 +321,12 @@ def complete_root(self, root):
316321
317322 thread_id = root .thread_id
318323
319- if thread_id not in self . _cache :
324+ if thread_id not in self :
320325 thread_id = self .current_thread_id ()
321- if thread_id not in self . _cache :
326+ if thread_id not in self :
322327 raise TraceCacheNoActiveTraceError ("no active trace" )
323328
324- current = self ._cache . get (thread_id )
329+ current = self .get (thread_id )
325330
326331 if root is not current :
327332 _logger .error (
@@ -333,7 +338,7 @@ def complete_root(self, root):
333338
334339 raise RuntimeError ("not the current trace" )
335340
336- del self . _cache [thread_id ]
341+ del self [thread_id ]
337342 root ._greenlet = None
338343
339344 def record_event_loop_wait (self , start_time , end_time ):
@@ -359,7 +364,7 @@ def record_event_loop_wait(self, start_time, end_time):
359364 task = getattr (transaction .root_span , "_task" , None )
360365 loop = get_event_loop (task )
361366
362- for trace in list ( self ._cache . values () ):
367+ for trace in self .values ():
363368 if trace in seen :
364369 continue
365370
@@ -390,6 +395,62 @@ def record_event_loop_wait(self, start_time, end_time):
390395 root .increment_child_count ()
391396 root .add_child (node )
392397
398+ # MutableMapping methods
399+
400+ def items (self ):
401+ """
402+ Safely iterates on self._cache.items() indirectly using a list of value references
403+ to avoid RuntimeErrors from size changes during iteration.
404+ """
405+ for wr in self ._cache .valuerefs ():
406+ value = wr () # Dereferenced value is potentially no longer live.
407+ if (
408+ value is not None
409+ ): # weakref is None means weakref has been garbage collected and is no longer live. Ignore.
410+ yield wr .key , value # wr.key is the original dict key
411+
412+ def keys (self ):
413+ """
414+ Iterates on self._cache.keys() indirectly using a list of value references
415+ to avoid RuntimeErrors from size changes during iteration.
416+
417+ NOTE: Returned keys are keys to weak references which may at any point be garbage collected.
418+ It is only safe to retrieve values from the trace cache using trace_cache.get(key, None).
419+ Retrieving values using trace_cache[key] can cause a KeyError if the item has been garbage collected.
420+ """
421+ for wr in self ._cache .valuerefs ():
422+ yield wr .key # wr.key is the original dict key
423+
424+ def values (self ):
425+ """
426+ Safely iterates on self._cache.values() indirectly using a list of value references
427+ to avoid RuntimeErrors from size changes during iteration.
428+ """
429+ for wr in self ._cache .valuerefs ():
430+ value = wr () # Dereferenced value is potentially no longer live.
431+ if (
432+ value is not None
433+ ): # weakref is None means weakref has been garbage collected and is no longer live. Ignore.
434+ yield value
435+
436+ def __getitem__ (self , key ):
437+ return self ._cache .__getitem__ (key )
438+
439+ def __setitem__ (self , key , value ):
440+ self ._cache .__setitem__ (key , value )
441+
442+ def __delitem__ (self , key ):
443+ self ._cache .__delitem__ (key )
444+
445+ def __iter__ (self ):
446+ return self .keys ()
447+
448+ def __len__ (self ):
449+ return self ._cache .__len__ ()
450+
451+ def __bool__ (self ):
452+ return bool (self ._cache .__len__ ())
453+
393454
394455_trace_cache = TraceCache ()
395456
0 commit comments