blob: 9aa4ce601a4ac7885c4f1078936e9c38af3db9d8 [file] [log] [blame]
import abc
import time
import logging
import os.path
from typing import Any, List, Optional, Callable, Iterable, cast
from concurrent.futures import ThreadPoolExecutor, wait
from ..utils import StopTestError, get_time_interval_printable_info
from ..node_interfaces import IRPCNode
from ..result_classes import SuiteConfig, JobConfig, TimeSeries, IResultStorage
logger = logging.getLogger("wally")
__doc__ = "Contains base classes for performance tests"
class PerfTest(metaclass=abc.ABCMeta):
"""Base class for all tests"""
name = None # type: str
max_retry = 3
retry_time = 30
job_config_cls = None # type: type
def __init__(self, storage: IResultStorage, suite: SuiteConfig, on_idle: Callable[[], None] = None) -> None:
self.suite = suite
self.stop_requested = False
self.sorted_nodes_ids = sorted(node.node_id for node in self.suite.nodes)
self.on_idle = on_idle
self.storage = storage
def request_stop(self) -> None:
self.stop_requested = True
def join_remote(self, path: str) -> str:
return os.path.join(self.suite.remote_dir, path)
@abc.abstractmethod
def run(self) -> None:
pass
@abc.abstractmethod
def format_for_console(self, data: Any) -> str:
pass
class ThreadedTest(PerfTest, metaclass=abc.ABCMeta):
"""Base class for tests, which spawn separated thread for each node"""
# max allowed time difference between starts and stops of run of the same test on different test nodes
# used_max_diff = max((min_run_time * max_rel_time_diff), max_time_diff)
max_time_diff = 5
max_rel_time_diff = 0.05
load_profile_name = None # type: str
def __init__(self, *args, **kwargs) -> None:
PerfTest.__init__(self, *args, **kwargs)
self.job_configs = None # type: List[JobConfig]
@abc.abstractmethod
def get_expected_runtime(self, iter_cfg: JobConfig) -> Optional[int]:
pass
def get_not_done_jobs(self) -> Iterable[JobConfig]:
jobs_map = {job.storage_id: job for job in self.job_configs}
already_in_storage = set()
for db_config in cast(List[JobConfig], self.storage.iter_job(self.suite)):
if db_config.storage_id in jobs_map:
job = jobs_map[db_config.storage_id]
if job != db_config:
logger.error("Test info at '%s.%s' is not equal to expected config for iteration %s.%s." +
" Maybe configuration was changed before test was restarted. " +
"DB cfg is:\n %s\nExpected cfg is:\n %s\nFix DB or rerun test from beginning",
self.suite.storage_id, job.storage_id, self.name, job.summary,
str(db_config).replace("\n", "\n "),
str(job).replace("\n", "\n "))
raise StopTestError()
logger.info("Test iteration %s.%s found in storage and will be skipped", self.name, job.summary)
already_in_storage.add(db_config.storage_id)
return [job for job in self.job_configs if job.storage_id not in already_in_storage]
def run(self) -> None:
self.storage.put_or_check_suite(self.suite)
not_in_storage = list(self.get_not_done_jobs())
if not not_in_storage:
logger.info("All test iteration in storage already. Skip test")
return
logger.debug("Run test %s with profile %r on nodes %s.", self.name,
self.load_profile_name,
",".join(self.sorted_nodes_ids))
logger.debug("Prepare nodes")
with ThreadPoolExecutor(len(self.suite.nodes)) as pool:
# config nodes
list(pool.map(self.config_node, self.suite.nodes))
run_times = list(map(self.get_expected_runtime, not_in_storage))
if None not in run_times:
# +5% - is a rough estimation for additional operations
expected_run_time = int(sum(run_times) * 1.05)
exec_time_s, end_dt_s = get_time_interval_printable_info(expected_run_time)
logger.info("Entire test should takes around %s and finished at %s", exec_time_s, end_dt_s)
for job in not_in_storage:
results = [] # type: List[TimeSeries]
for retry_idx in range(self.max_retry):
logger.debug("Prepare job %s", job.params.summary)
# prepare nodes for new iterations
wait([pool.submit(self.prepare_iteration, node, job) for node in self.suite.nodes])
expected_job_time = self.get_expected_runtime(job)
exec_time_s, end_dt_s = get_time_interval_printable_info(expected_job_time)
logger.info("Job should takes around %s and finished at %s", exec_time_s, end_dt_s)
jfutures = [pool.submit(self.run_iteration, node, job) for node in self.suite.nodes]
failed = False
for future in jfutures:
try:
results.extend(future.result())
except EnvironmentError:
failed = True
if not failed:
break
if self.max_retry - 1 == retry_idx:
logger.exception("Fio failed")
raise StopTestError()
logger.exception("During fio run")
logger.info("Sleeping %ss and retrying job", self.retry_time)
time.sleep(self.retry_time)
results = []
# per node jobs start and stop times
start_times = [] # type: List[int]
stop_times = [] # type: List[int]
for ts in results:
self.storage.put_ts(ts)
if len(ts.times) >= 2: # type: ignore
start_times.append(ts.times[0])
stop_times.append(ts.times[-1])
if len(start_times) > 0:
min_start_time = min(start_times)
max_start_time = max(start_times)
min_stop_time = min(stop_times)
max_allowed_time_diff = int((min_stop_time - max_start_time) * self.max_rel_time_diff)
max_allowed_time_diff = max(max_allowed_time_diff, self.max_time_diff)
if min_start_time + self.max_time_diff < max_allowed_time_diff:
logger.warning("Too large difference in %s:%s start time - %s. " +
"Max recommended difference is %s",
self.name, job.summary,
max_start_time - min_start_time, self.max_time_diff)
if min_stop_time + self.max_time_diff < max_allowed_time_diff:
logger.warning("Too large difference in %s:%s stop time - %s. " +
"Max recommended difference is %s",
self.name, job.summary,
max_start_time - min_start_time, self.max_time_diff)
job.reliable_info_range = (int(max_start_time), int(min_stop_time))
self.storage.put_job(self.suite, job)
self.storage.sync()
if self.on_idle is not None:
self.on_idle()
@abc.abstractmethod
def config_node(self, node: IRPCNode) -> None:
pass
@abc.abstractmethod
def prepare_iteration(self, node: IRPCNode, job: JobConfig) -> None:
pass
@abc.abstractmethod
def run_iteration(self, node: IRPCNode, job: JobConfig) -> List[TimeSeries]:
pass
class TwoScriptTest(ThreadedTest, metaclass=abc.ABCMeta):
def __init__(self, *dt, **mp) -> None:
ThreadedTest.__init__(self, *dt, **mp)
self.prerun_script = self.suite.params['prerun_script']
self.run_script = self.suite.params['run_script']
self.prerun_tout = self.suite.params.get('prerun_tout', 3600)
self.run_tout = self.suite.params.get('run_tout', 3600)
# TODO: fix job_configs field
raise NotImplementedError("Fix job configs")
def get_expected_runtime(self, job: JobConfig) -> Optional[int]:
return None
def config_node(self, node: IRPCNode) -> None:
node.copy_file(self.run_script, self.join_remote(self.run_script))
node.copy_file(self.prerun_script, self.join_remote(self.prerun_script))
cmd = self.join_remote(self.prerun_script)
cmd += ' ' + self.suite.params.get('prerun_opts', '')
node.run(cmd, timeout=self.prerun_tout)
def prepare_iteration(self, node: IRPCNode, job: JobConfig) -> None:
pass
def run_iteration(self, node: IRPCNode, job: JobConfig) -> List[TimeSeries]:
# TODO: have to store logs
cmd = self.join_remote(self.run_script)
cmd += ' ' + self.suite.params.get('run_opts', '')
return self.parse_results(node.run(cmd, timeout=self.run_tout))
@abc.abstractmethod
def parse_results(self, data: str) -> List[TimeSeries]:
pass