yandex-tank/yandextank/plugins/Autostop.py

470 lines
16 KiB
Python
Raw Normal View History

2014-02-25 09:54:37 +00:00
""" Autostop facility """
import copy
2012-09-12 13:18:58 +00:00
import logging
import re
2014-03-13 09:36:16 +00:00
import json
2014-02-11 10:49:16 +00:00
2015-02-02 17:24:32 +00:00
from Aggregator import AggregatorPlugin, AggregateResultListener
from ConsoleOnline import AbstractInfoWidget, ConsoleOnlinePlugin
from yandextank.core import AbstractPlugin
import yandextank.core as tankcore
2014-10-15 10:37:09 +00:00
import time
2012-09-12 13:18:58 +00:00
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
class AutostopPlugin(AbstractPlugin, AggregateResultListener):
2014-02-25 09:54:37 +00:00
""" Plugin that accepts criteria classes and triggers autostop """
2012-09-12 13:18:58 +00:00
SECTION = 'autostop'
def __init__(self, core):
2012-09-13 10:19:33 +00:00
AbstractPlugin.__init__(self, core)
2012-09-12 13:18:58 +00:00
self.cause_criteria = None
self.criterias = []
self.custom_criterias = []
self.counting = []
self.criteria_str = ''
2012-09-12 13:18:58 +00:00
@staticmethod
def get_key():
2012-10-18 10:46:50 +00:00
return __file__
2012-09-12 13:18:58 +00:00
def get_counting(self):
2014-02-25 09:54:37 +00:00
""" get criterias that are activated """
2012-09-12 13:18:58 +00:00
return self.counting
def add_counting(self, obj):
2014-02-25 09:54:37 +00:00
""" add criteria that activated """
2012-09-12 13:18:58 +00:00
self.counting += [obj]
def add_criteria_class(self, criteria_class):
2014-02-25 09:54:37 +00:00
""" add new criteria class """
2012-09-12 13:18:58 +00:00
self.custom_criterias += [criteria_class]
2014-02-11 10:49:16 +00:00
2013-03-22 13:55:13 +00:00
def get_available_options(self):
return ["autostop"]
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def configure(self):
aggregator = self.core.get_plugin_of_type(AggregatorPlugin)
aggregator.add_result_listener(self)
2012-09-13 10:19:33 +00:00
self.criteria_str = " ".join(self.get_option("autostop", '').split("\n"))
2012-09-12 13:18:58 +00:00
self.add_criteria_class(AvgTimeCriteria)
self.add_criteria_class(NetCodesCriteria)
self.add_criteria_class(HTTPCodesCriteria)
self.add_criteria_class(QuantileCriteria)
2014-03-13 09:36:16 +00:00
self.add_criteria_class(SteadyCumulativeQuantilesCriteria)
2014-10-15 10:37:09 +00:00
self.add_criteria_class(TimeLimitCriteria)
2012-09-12 13:18:58 +00:00
def prepare_test(self):
for criteria_str in self.criteria_str.strip().split(")"):
2014-02-11 10:49:16 +00:00
if not criteria_str:
2012-09-12 13:18:58 +00:00
continue
self.log.debug("Criteria string: %s", criteria_str)
self.criterias.append(self.__create_criteria(criteria_str))
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.log.debug("Criteria object: %s", self.criterias)
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
try:
console = self.core.get_plugin_of_type(ConsoleOnlinePlugin)
except Exception, ex:
self.log.debug("Console not found: %s", ex)
console = None
2014-02-11 10:49:16 +00:00
if console:
2012-09-12 13:18:58 +00:00
console.add_info_widget(AutostopWidget(self))
def is_test_finished(self):
if self.cause_criteria:
self.log.info("Autostop criteria requested test stop: %s", self.cause_criteria.explain())
return self.cause_criteria.get_rc()
else:
return -1
def __create_criteria(self, criteria_str):
2014-02-25 09:54:37 +00:00
""" instantiate criteria from config string """
2012-09-12 13:18:58 +00:00
parsed = criteria_str.split("(")
type_str = parsed[0].strip().lower()
parsed[1] = parsed[1].split(")")[0].strip()
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
for criteria_class in self.custom_criterias:
if criteria_class.get_type_string() == type_str:
return criteria_class(self, parsed[1])
raise ValueError("Unsupported autostop criteria type: %s" % criteria_str)
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def aggregate_second(self, second_aggregate_data):
self.counting = []
2014-02-11 10:49:16 +00:00
if not self.cause_criteria:
for criteria in self.criterias:
if criteria.notify(second_aggregate_data):
self.log.debug("Autostop criteria requested test stop: %s", criteria)
self.cause_criteria = criteria
class AutostopWidget(AbstractInfoWidget):
2014-02-25 09:54:37 +00:00
""" widget that displays counting criterias """
2014-02-11 10:49:16 +00:00
def __init__(self, sender):
AbstractInfoWidget.__init__(self)
2014-02-11 10:49:16 +00:00
self.owner = sender
def get_index(self):
return 25
def render(self, screen):
res = []
candidates = self.owner.get_counting()
for candidate in candidates:
text, perc = candidate.widget_explain()
if perc >= 0.95:
res += [screen.markup.RED_DARK + text + screen.markup.RESET]
elif perc >= 0.8:
res += [screen.markup.RED + text + screen.markup.RESET]
elif perc >= 0.5:
res += [screen.markup.YELLOW + text + screen.markup.RESET]
else:
res += [text]
2014-02-11 10:49:16 +00:00
if res:
return "Autostop:\n " + ("\n ".join(res))
else:
return ''
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
class AbstractCriteria:
2014-02-25 09:54:37 +00:00
""" parent class for all criterias """
2012-09-12 13:18:58 +00:00
RC_TIME = 21
RC_HTTP = 22
RC_NET = 23
2014-03-13 09:36:16 +00:00
RC_STEADY = 33
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def __init__(self):
self.log = logging.getLogger(__name__)
2012-10-01 12:48:10 +00:00
self.cause_second = None
2014-02-11 10:49:16 +00:00
2014-02-25 09:54:37 +00:00
@staticmethod
def count_matched_codes(codes_regex, codes_dict):
""" helper to aggregate codes by mask """
2012-09-12 13:18:58 +00:00
total = 0
for code, count in codes_dict.items():
2012-09-20 15:45:27 +00:00
if codes_regex.match(str(code)):
2012-09-12 13:18:58 +00:00
total += count
return total
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def notify(self, aggregate_second):
2014-02-25 09:54:37 +00:00
""" notification about aggregate data goes here """
raise NotImplementedError("Abstract methods requires overriding")
2012-09-12 13:18:58 +00:00
def get_rc(self):
2014-02-25 09:54:37 +00:00
""" get return code for test """
raise NotImplementedError("Abstract methods requires overriding")
2012-09-12 13:18:58 +00:00
def explain(self):
2014-02-25 09:54:37 +00:00
""" long explanation to show after test stop """
raise NotImplementedError("Abstract methods requires overriding")
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def widget_explain(self):
2014-02-25 09:54:37 +00:00
""" short explanation to display in right panel """
return self.explain(), 0
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
@staticmethod
def get_type_string():
2014-02-25 09:54:37 +00:00
""" returns string that used as config name for criteria """
raise NotImplementedError("Abstract methods requires overriding")
2012-09-12 13:18:58 +00:00
class AvgTimeCriteria(AbstractCriteria):
2014-02-25 09:54:37 +00:00
""" average response time criteria """
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
@staticmethod
def get_type_string():
return 'time'
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.seconds_count = 0
2012-10-03 16:49:13 +00:00
self.rt_limit = tankcore.expand_to_milliseconds(param_str.split(',')[0])
self.seconds_limit = tankcore.expand_to_seconds(param_str.split(',')[1])
2012-09-12 13:18:58 +00:00
self.autostop = autostop
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def notify(self, aggregate_second):
if aggregate_second.overall.avg_response_time > self.rt_limit:
if not self.seconds_count:
self.cause_second = aggregate_second
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.log.debug(self.explain())
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.seconds_count += 1
self.autostop.add_counting(self)
if self.seconds_count >= self.seconds_limit:
return True
else:
self.seconds_count = 0
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
return False
def get_rc(self):
return self.RC_TIME
def explain(self):
2012-09-20 15:45:27 +00:00
items = (self.rt_limit, self.seconds_count, self.cause_second.time)
return "Average response time higher than %sms for %ss, since %s" % items
2012-09-12 13:18:58 +00:00
def widget_explain(self):
items = (self.rt_limit, self.seconds_count, self.seconds_limit)
2014-02-25 09:54:37 +00:00
return "Avg Time >%sms for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
class HTTPCodesCriteria(AbstractCriteria):
2014-02-25 09:54:37 +00:00
""" HTTP codes criteria """
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
@staticmethod
def get_type_string():
return 'http'
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.seconds_count = 0
self.codes_mask = param_str.split(',')[0].lower()
self.codes_regex = re.compile(self.codes_mask.replace("x", '.'))
self.autostop = autostop
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
level_str = param_str.split(',')[1].strip()
if level_str[-1:] == '%':
self.level = float(level_str[:-1]) / 100
self.is_relative = True
else:
self.level = int(level_str)
self.is_relative = False
2012-10-03 16:49:13 +00:00
self.seconds_limit = tankcore.expand_to_seconds(param_str.split(',')[2])
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def notify(self, aggregate_second):
matched_responses = self.count_matched_codes(self.codes_regex, aggregate_second.overall.http_codes)
if self.is_relative:
if aggregate_second.overall.RPS:
matched_responses = float(matched_responses) / aggregate_second.overall.RPS
else:
2012-09-25 13:49:53 +00:00
matched_responses = 0
2012-09-12 13:18:58 +00:00
self.log.debug("HTTP codes matching mask %s: %s/%s", self.codes_mask, matched_responses, self.level)
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
if matched_responses >= self.level:
if not self.seconds_count:
self.cause_second = aggregate_second
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.log.debug(self.explain())
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.seconds_count += 1
self.autostop.add_counting(self)
if self.seconds_count >= self.seconds_limit:
return True
else:
self.seconds_count = 0
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
return False
def get_rc(self):
return self.RC_HTTP
def get_level_str(self):
2014-02-25 09:54:37 +00:00
""" format level str """
2012-09-12 13:18:58 +00:00
if self.is_relative:
level_str = str(100 * self.level) + "%"
else:
level_str = self.level
return level_str
def explain(self):
2012-09-20 15:45:27 +00:00
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.cause_second.time)
2014-02-11 10:49:16 +00:00
return "%s codes count higher than %s for %ss, since %s" % items
2012-09-12 13:18:58 +00:00
def widget_explain(self):
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.seconds_limit)
2014-02-25 09:54:37 +00:00
return "HTTP %s>%s for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit
2012-09-12 13:18:58 +00:00
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
class NetCodesCriteria(AbstractCriteria):
2014-02-25 09:54:37 +00:00
""" Net codes criteria """
2012-09-12 13:18:58 +00:00
@staticmethod
def get_type_string():
return 'net'
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.seconds_count = 0
self.codes_mask = param_str.split(',')[0].lower()
self.codes_regex = re.compile(self.codes_mask.replace("x", '.'))
self.autostop = autostop
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
level_str = param_str.split(',')[1].strip()
if level_str[-1:] == '%':
self.level = float(level_str[:-1]) / 100
self.is_relative = True
else:
self.level = int(level_str)
self.is_relative = False
2012-10-03 16:49:13 +00:00
self.seconds_limit = tankcore.expand_to_seconds(param_str.split(',')[2])
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
def notify(self, aggregate_second):
2012-09-21 10:15:06 +00:00
codes = copy.deepcopy(aggregate_second.overall.net_codes)
2014-02-11 10:49:16 +00:00
if '0' in codes.keys():
codes.pop('0')
2012-09-12 13:18:58 +00:00
matched_responses = self.count_matched_codes(self.codes_regex, codes)
if self.is_relative:
if aggregate_second.overall.RPS:
matched_responses = float(matched_responses) / aggregate_second.overall.RPS
else:
2012-09-25 13:49:53 +00:00
matched_responses = 0
2012-09-12 13:18:58 +00:00
self.log.debug("Net codes matching mask %s: %s/%s", self.codes_mask, matched_responses, self.level)
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
if matched_responses >= self.level:
if not self.seconds_count:
self.cause_second = aggregate_second
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.log.debug(self.explain())
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
self.seconds_count += 1
self.autostop.add_counting(self)
if self.seconds_count >= self.seconds_limit:
return True
else:
self.seconds_count = 0
2014-02-11 10:49:16 +00:00
2012-09-12 13:18:58 +00:00
return False
def get_rc(self):
return self.RC_NET
def get_level_str(self):
2014-02-25 09:54:37 +00:00
""" format level str """
2012-09-12 13:18:58 +00:00
if self.is_relative:
level_str = str(100 * self.level) + "%"
else:
level_str = self.level
return level_str
def explain(self):
2012-09-20 15:45:27 +00:00
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.cause_second.time)
2014-02-11 10:49:16 +00:00
return "%s net codes count higher than %s for %ss, since %s" % items
2012-09-12 13:18:58 +00:00
def widget_explain(self):
items = (self.codes_mask, self.get_level_str(), self.seconds_count, self.seconds_limit)
2014-02-25 09:54:37 +00:00
return "Net %s>%s for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit
2012-09-12 13:18:58 +00:00
class QuantileCriteria(AbstractCriteria):
2014-02-25 09:54:37 +00:00
""" quantile criteria """
@staticmethod
def get_type_string():
2013-03-15 10:52:36 +00:00
return 'quantile'
2014-02-11 10:49:16 +00:00
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.seconds_count = 0
2013-03-15 10:52:36 +00:00
self.quantile = float(param_str.split(',')[0])
2012-10-03 16:49:13 +00:00
self.rt_limit = tankcore.expand_to_milliseconds(param_str.split(',')[1])
self.seconds_limit = tankcore.expand_to_seconds(param_str.split(',')[2])
self.autostop = autostop
2014-02-11 10:49:16 +00:00
def notify(self, aggregate_second):
2013-03-15 10:52:36 +00:00
if not (self.quantile in aggregate_second.overall.quantiles.keys()):
2013-03-15 11:04:41 +00:00
self.log.warning("No quantile %s in %s", self.quantile, aggregate_second.overall.quantiles)
2013-03-15 10:52:36 +00:00
if self.quantile in aggregate_second.overall.quantiles.keys() \
and aggregate_second.overall.quantiles[self.quantile] > self.rt_limit:
if not self.seconds_count:
self.cause_second = aggregate_second
2014-02-11 10:49:16 +00:00
self.log.debug(self.explain())
2014-02-11 10:49:16 +00:00
self.seconds_count += 1
self.autostop.add_counting(self)
if self.seconds_count >= self.seconds_limit:
return True
2012-09-12 13:18:58 +00:00
else:
self.seconds_count = 0
2014-02-11 10:49:16 +00:00
return False
def get_rc(self):
return self.RC_TIME
def explain(self):
2013-03-15 10:52:36 +00:00
items = (self.quantile, self.rt_limit, self.seconds_count, self.cause_second.time)
return "Percentile %s higher than %sms for %ss, since %s" % items
def widget_explain(self):
2013-03-15 10:52:36 +00:00
items = (self.quantile, self.rt_limit, self.seconds_count, self.seconds_limit)
2014-02-25 09:54:37 +00:00
return "%s%% >%sms for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit
2014-03-13 09:36:16 +00:00
class SteadyCumulativeQuantilesCriteria(AbstractCriteria):
""" quantile criteria """
@staticmethod
def get_type_string():
return 'steady_cumulative'
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.seconds_count = 0
self.hash = ""
self.seconds_limit = tankcore.expand_to_seconds(param_str.split(',')[0])
2014-03-13 09:36:16 +00:00
self.autostop = autostop
def notify(self, aggregate_second):
hash = json.dumps(aggregate_second.cumulative.quantiles)
logging.debug("Cumulative quantiles hash: %s", hash)
if self.hash == hash:
if not self.seconds_count:
self.cause_second = aggregate_second
self.log.debug(self.explain())
self.seconds_count += 1
self.autostop.add_counting(self)
if self.seconds_count >= self.seconds_limit:
return True
else:
self.seconds_count = 0
self.hash = hash
return False
def get_rc(self):
return self.RC_STEADY
def explain(self):
items = (self.seconds_count, self.cause_second.time)
return "Cumulative percentiles are steady for %ss, since %s" % items
def widget_explain(self):
items = (self.seconds_count, self.seconds_limit)
return "Steady for %s/%ss" % items, float(self.seconds_count) / self.seconds_limit
2014-10-15 10:37:09 +00:00
class TimeLimitCriteria(AbstractCriteria):
""" time limit criteria """
@staticmethod
def get_type_string():
return 'limit'
def __init__(self, autostop, param_str):
AbstractCriteria.__init__(self)
self.start_time = time.time()
self.end_time = time.time()
self.time_limit = tankcore.expand_to_seconds(param_str)
def notify(self, aggregate_second):
self.end_time = time.time()
return (self.end_time - self.start_time) > self.time_limit
def get_rc(self):
return self.RC_TIME
def explain(self):
return "Test time elapsed. Limit: %ss, actual time: %ss" % (self.time_limit, self.end_time - self.start_time)
def widget_explain(self):
return "Time limit: %ss, actual time: %ss" % (self.time_limit, self.end_time - self.start_time)