Files
scylladb/tools/scyllatop/livedata.py
Amnon Heiman 52db99f25f scyllatop/livedata.py: Safe iteration over metrics
This patch change the code that iterates over the metrics to use a copy
of the metrics names to make it safe to remove the metrics from the
metrics object.

Fixes #7488

Signed-off-by: Amnon Heiman <amnon@scylladb.com>
2020-10-27 22:44:02 +02:00

96 lines
3.4 KiB
Python

import logging
import parseexception
import fnmatch
import time
import metric
import defaults
class LiveData(object):
def __init__(self, metricPatterns, interval, metric_source, ttl=None):
logging.info('will query metric_source {} every {} seconds'.format(metric_source, interval))
if ttl:
logging.info('absent data will be kept for {} seconds'.format(ttl))
self._startedAt = time.time()
self._results = {}
self._interval = interval
self._ttl = ttl
self._metric_source = metric_source
if metricPatterns and len(metricPatterns) > 0:
self._metricPatterns = metricPatterns
else:
self._metricPatterns = defaults.DEFAULT_METRIC_PATTERNS
self._results = self._discoverMetrics()
self._views = []
self._stop = False
def addView(self, view):
logging.debug('addView {}'.format(view))
self._views.append(view)
@property
def results(self):
return self._results
@property
def measurements(self):
return self._results.values()
def _discoverMetrics(self):
results = metric.Metric.discover(self._metric_source)
logging.debug('_discoverMetrics: {} results discovered'.format(len(results)))
for symbol in list(results):
if not self._matches(symbol, self._metricPatterns):
results.pop(symbol)
logging.debug('_initializeMetrics: {} results matched'.format(len(results)))
return results
def _matches(self, symbol, metricPatterns):
for pattern in metricPatterns:
match = fnmatch.fnmatch(symbol, pattern)
if match:
return True
return False
def go(self, mainLoop):
num_updated = 0
num_absent = 0
num_expired = 0
num_added = 0
now = time.time()
while not self._stop:
new_results = self._discoverMetrics();
expiration = time.time() + self._ttl if self._ttl else None
for metric in self._results:
if not metric in new_results:
metric_obj = self._results[metric]
if not metric_obj.is_absent:
metric_obj.markAbsent(expiration)
num_absent += 1
elif metric_obj.expiration and now >= metric_obj.expiration:
self._results.pop(metric)
num_expired += 1
else:
num_absent += 1
num_updated = len(self._results) - num_absent
num_added = len(new_results) - num_updated
self._results.update(new_results)
logging.debug('go: updated {} measurements, added {}, {} marked absent, {} expired'.format(num_updated, num_added, num_absent, num_expired))
for view in self._views:
logging.debug('go: updating view {}'.format(view))
view.update(self)
logging.debug('go: sleeping for {} seconds'.format(self._interval))
time.sleep(self._interval)
logging.debug('go: drawing screen...')
mainLoop.draw_screen()
def _update(self, metric_obj):
try:
metric_obj.update()
except parseexception.ParseException:
logging.exception('exception while updating metric {0}'.format(metric_obj))
def stop(self):
self._stop = True