Move & refactor atomic actions

* Move atomic actions from rally.task.scenario to
  newly created rally.task.atomic

* Introduce atomic.ActionTimerMixin which should be
  used as a base for class that support atomic actions

* Simplify atomic.ActionTimer code (it was over designed)

* Unify atomic actions mechanism so it will be possible
  to use in other places (e.g. context)

Change-Id: I5702224a5cba4672c0c2f80f00b67aade19dc29c
This commit is contained in:
Boris Pavlovic 2015-08-25 00:37:03 -07:00
parent 8f69f06fa0
commit 70b9027b5c
5 changed files with 134 additions and 104 deletions

View File

@ -47,8 +47,7 @@ def tempest_log_wrapper(func):
total, tests = scenario_obj.context["verifier"].parse_results(
kwargs["log_file"])
if total and tests:
scenario_obj._add_atomic_actions("test_execution",
total.get("time"))
scenario_obj._atomic_actions["test_execution"] = total.get("time")
if total.get("errors") or total.get("failures"):
raise TempestBenchmarkFailure([
test for test in six.itervalues(tests)

View File

@ -13,11 +13,75 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
from rally.task import scenario
from rally.common import costilius
from rally.common import utils
# TODO(boris-42): In future we will move code from scenario to atomic action
# to simplify migration to new code we will make just links for
# now
ActionTimer = scenario.AtomicAction
action_timer = scenario.atomic_action_timer
class ActionTimerMixin(object):
def __init__(self):
self._atomic_actions = costilius.OrderedDict()
def atomic_actions(self):
"""Returns the content of each atomic action."""
return self._atomic_actions
class ActionTimer(utils.Timer):
"""A class to measure the duration of atomic operations
This would simplify the way measure atomic operation duration
in certain cases. For example if we want to get the duration
for each operation which runs in an iteration
for i in range(repetitions):
with atomic.ActionTimer(instance_of_action_timer, "name_of_action"):
self.clients(<client>).<operation>
"""
def __init__(self, instance, name):
"""Create a new instance of the AtomicAction.
:param instance: instance of subclass of ActionTimerMixin
:param name: name of the ActionBuilder
"""
super(ActionTimer, self).__init__()
self.instance = instance
self.name = self._get_atomic_action_name(name)
self.instance._atomic_actions[self.name] = None
def _get_atomic_action_name(self, name):
# TODO(boris-42): It was quite bad idea to store atomic actions
# inside {}. We should refactor this in 0.2.0 release
# and store them inside array, that will allow us to
# store atomic actions with the same name
if name not in self.instance._atomic_actions:
return name
name_template = name + " (%i)"
i = 2
while name_template % i in self.instance._atomic_actions:
i += 1
return name_template % i
def __exit__(self, type_, value, tb):
super(ActionTimer, self).__exit__(type_, value, tb)
if type_ is None:
self.instance._atomic_actions[self.name] = self.duration()
def action_timer(name):
"""Provide measure of execution time.
Decorates methods of the Scenario class.
This provides duration in seconds of each atomic action.
"""
def wrap(func):
@functools.wraps(func)
def func_atomic_actions(self, *args, **kwargs):
with ActionTimer(self, name):
f = func(self, *args, **kwargs)
return f
return func_atomic_actions
return wrap

View File

@ -13,18 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
import random
import time
import six
from rally.common import costilius
from rally.common import log as logging
from rally.common.plugin import plugin
from rally.common import utils
from rally import consts
from rally import exceptions
from rally.task import atomic
from rally.task import functional
@ -91,7 +91,9 @@ class ConfigurePluginMeta(type):
@six.add_metaclass(ConfigurePluginMeta)
class Scenario(plugin.Plugin, functional.FunctionalMixin):
class Scenario(plugin.Plugin,
atomic.ActionTimerMixin,
functional.FunctionalMixin):
"""This is base class for any benchmark scenario.
You should create subclass of this class. And your test scenarios will
@ -101,9 +103,9 @@ class Scenario(plugin.Plugin, functional.FunctionalMixin):
RESOURCE_NAME_LENGTH = 10
def __init__(self, context=None):
super(Scenario, self).__init__()
self.context = context
self._idle_duration = 0
self._atomic_actions = costilius.OrderedDict()
# TODO(amaretskiy): consider about prefix part of benchmark uuid
@classmethod
@ -167,76 +169,3 @@ class Scenario(plugin.Plugin, functional.FunctionalMixin):
def idle_duration(self):
"""Returns duration of all sleep_between."""
return self._idle_duration
def _register_atomic_action(self, name):
"""Registers an atomic action by its name."""
self._atomic_actions[name] = None
def _atomic_action_registered(self, name):
"""Checks whether an atomic action has been already registered."""
return name in self._atomic_actions
def _add_atomic_actions(self, name, duration):
"""Adds the duration of an atomic action by its name."""
self._atomic_actions[name] = duration
def atomic_actions(self):
"""Returns the content of each atomic action."""
return self._atomic_actions
def atomic_action_timer(name):
"""Provide measure of execution time.
Decorates methods of the Scenario class.
This provides duration in seconds of each atomic action.
"""
def wrap(func):
@functools.wraps(func)
def func_atomic_actions(self, *args, **kwargs):
with AtomicAction(self, name):
f = func(self, *args, **kwargs)
return f
return func_atomic_actions
return wrap
class AtomicAction(utils.Timer):
"""A class to measure the duration of atomic operations
This would simplify the way measure atomic operation duration
in certain cases. For example if we want to get the duration
for each operation which runs in an iteration
for i in range(repetitions):
with scenario_utils.AtomicAction(instance_of_base_scenario_subclass,
"name_of_action"):
self.clients(<client>).<operation>
"""
def __init__(self, scenario_instance, name):
"""Create a new instance of the AtomicAction.
:param scenario_instance: instance of subclass of base scenario
:param name: name of the ActionBuilder
"""
super(AtomicAction, self).__init__()
self.scenario_instance = scenario_instance
self.name = self._get_atomic_action_name(name)
self.scenario_instance._register_atomic_action(self.name)
def _get_atomic_action_name(self, name):
if not self.scenario_instance._atomic_action_registered(name):
return name
else:
name_template = name + " (%i)"
atomic_action_iteration = 2
while self.scenario_instance._atomic_action_registered(
name_template % atomic_action_iteration):
atomic_action_iteration += 1
return name_template % atomic_action_iteration
def __exit__(self, type_, value, tb):
super(AtomicAction, self).__exit__(type_, value, tb)
if type_ is None:
self.scenario_instance._add_atomic_actions(self.name,
self.duration())

View File

@ -0,0 +1,57 @@
# Copyright 2015: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from rally.common import costilius
from rally.task import atomic
from tests.unit import test
class ActionTimerMixinTestCase(test.TestCase):
def test_atomic_actions(self):
inst = atomic.ActionTimerMixin()
self.assertEqual(inst._atomic_actions, inst.atomic_actions())
class AtomicActionTestCase(test.TestCase):
@mock.patch("time.time", side_effect=[1, 3, 6, 10, 15, 21])
def test_action_timer_context(self, mock_time):
inst = atomic.ActionTimerMixin()
with atomic.ActionTimer(inst, "test"):
with atomic.ActionTimer(inst, "test"):
with atomic.ActionTimer(inst, "some"):
pass
expected = [("test", 20), ("test (2)", 12), ("some", 4)]
self.assertEqual(costilius.OrderedDict(expected),
inst.atomic_actions())
@mock.patch("time.time", side_effect=[1, 3])
def test_action_timer_decorator(self, mock_time):
class Some(atomic.ActionTimerMixin):
@atomic.action_timer("some")
def some_func(self, a, b):
return a + b
inst = Some()
self.assertEqual(5, inst.some_func(2, 3))
self.assertEqual(costilius.OrderedDict({"some": 2}),
inst.atomic_actions())

View File

@ -271,22 +271,3 @@ class ScenarioTestCase(test.TestCase):
self.assertEqual(
len_by_prefix(result, scenario.Scenario.RESOURCE_NAME_PREFIX),
range_num)
class AtomicActionTestCase(test.TestCase):
def test__init__(self):
fake_scenario_instance = fakes.FakeScenario()
c = scenario.AtomicAction(fake_scenario_instance, "asdf")
self.assertEqual(c.scenario_instance, fake_scenario_instance)
self.assertEqual(c.name, "asdf")
@mock.patch("tests.unit.fakes.FakeScenario._add_atomic_actions")
@mock.patch("rally.common.utils.time")
def test__exit__(self, mock_time, mock_fake_scenario__add_atomic_actions):
fake_scenario_instance = fakes.FakeScenario()
self.start = mock_time.time()
with scenario.AtomicAction(fake_scenario_instance, "asdf"):
pass
duration = mock_time.time() - self.start
mock_fake_scenario__add_atomic_actions.assert_called_once_with(
"asdf", duration)