Files
deb-python-oslo.service/tests/unit/test_periodic.py
Matt Riedemann 0248e1f588 Make unspecified periodic spaced tasks run on default interval
When there are multiple tasks with a defined spacing, they can get
somewhat staggered and force the periodic task processor to run
additional iterations. Since tasks with no spacing run every time the
periodic task processor runs, they get run more frequently than one
would expect.

Some may argue that this change in behavior could cause some task to
behave differently than it used to. However, there was never any
guarantee that the task would run more often than every 60 seconds, and
in many cases the tasks may already run less frequently than that (due
to other long-running tasks). So this change should not introduce any
new issues related to the timing of task execution; it would only serve
to make the timing more regular.

We should also make the default interval configurable but that's
reserved for a separate change.

DocImpact: periodic tasks without a specific spacing interval will now
run on the default interval (currently every 60 seconds) rather than
whenever the task processor runs.

UpgradeImpact: see above; periodic tasks without a specific spacing
interval will now run on the default interval which may be less
often than some tasks run now, but with this change they will run
on a more consistent interval.

Closes-Bug: #1319232
Related-Bug: #1276203
Related-Bug: #1272830

Change-Id: I4dd20f68175165d311fa4d3f459c86f2dcac911b
2014-05-21 15:12:26 -07:00

281 lines
9.5 KiB
Python

# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Unit Tests for periodic_task decorator and PeriodicTasks class.
"""
import mock
from oslotest import base as test_base
from openstack.common.fixture import config
from openstack.common import periodic_task
from testtools import matchers
class AnException(Exception):
pass
class AService(periodic_task.PeriodicTasks):
def __init__(self):
super(AService, self).__init__()
self.called = {'doit': 0, 'urg': 0, 'ticks': 0, 'tocks': 0}
@periodic_task.periodic_task
def doit(self, context):
self.called['doit'] += 1
@periodic_task.periodic_task
def crashit(self, context):
self.called['urg'] += 1
raise AnException('urg')
@periodic_task.periodic_task(spacing=10 + periodic_task.DEFAULT_INTERVAL,
run_immediately=True)
def doit_with_ticks(self, context):
self.called['ticks'] += 1
@periodic_task.periodic_task(spacing=10 + periodic_task.DEFAULT_INTERVAL)
def doit_with_tocks(self, context):
self.called['tocks'] += 1
class PeriodicTasksTestCase(test_base.BaseTestCase):
"""Test cases for PeriodicTasks."""
@mock.patch('time.time')
def test_called_thrice(self, mock_time):
serv = AService()
now = serv._periodic_last_run['doit_with_tocks']
time = now + periodic_task.DEFAULT_INTERVAL
mock_time.return_value = time
serv.run_periodic_tasks(None)
self.assertEqual(serv.called['doit'], 1)
self.assertEqual(serv.called['urg'], 1)
self.assertEqual(serv.called['ticks'], 1)
self.assertEqual(serv.called['tocks'], 0)
time = time + 9 + periodic_task.DEFAULT_INTERVAL
mock_time.return_value = time
serv.run_periodic_tasks(None)
self.assertEqual(serv.called['doit'], 2)
self.assertEqual(serv.called['urg'], 2)
# doit_with_ticks will only be called the first time because its
# spacing time interval will not have elapsed between the calls.
self.assertEqual(serv.called['ticks'], 1)
# doit_with_tocks will run since idle_for is equal to DEFAULT_INTERVAL
# and the task processor will pick the minimum of those two to
# determine when to run, and the idle time will have passed.
self.assertEqual(serv.called['tocks'], 1)
time = time + 10 + periodic_task.DEFAULT_INTERVAL
mock_time.return_value = time
serv.run_periodic_tasks(None)
self.assertEqual(serv.called['doit'], 3)
self.assertEqual(serv.called['urg'], 3)
self.assertEqual(serv.called['ticks'], 2)
self.assertEqual(serv.called['tocks'], 2)
@mock.patch('time.time')
def test_raises(self, mock_time):
serv = AService()
now = serv._periodic_last_run['crashit']
mock_time.return_value = now + periodic_task.DEFAULT_INTERVAL
self.assertRaises(AnException,
serv.run_periodic_tasks,
None, raise_on_error=True)
class ManagerMetaTestCase(test_base.BaseTestCase):
"""Tests for the meta class which manages the creation of periodic tasks.
"""
def test_meta(self):
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task
def foo(self):
return 'foo'
@periodic_task.periodic_task(spacing=4)
def bar(self):
return 'bar'
@periodic_task.periodic_task(enabled=False)
def baz(self):
return 'baz'
m = Manager()
self.assertThat(m._periodic_tasks, matchers.HasLength(2))
self.assertEqual(periodic_task.DEFAULT_INTERVAL,
m._periodic_spacing['foo'])
self.assertEqual(4, m._periodic_spacing['bar'])
self.assertThat(
m._periodic_spacing, matchers.Not(matchers.Contains('baz')))
class ManagerTestCase(test_base.BaseTestCase):
"""Tests the periodic tasks portion of the manager class."""
def setUp(self):
super(ManagerTestCase, self).setUp()
self.config = self.useFixture(config.Config()).config
def test_periodic_tasks_with_idle(self):
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=200)
def bar(self):
return 'bar'
m = Manager()
self.assertThat(m._periodic_tasks, matchers.HasLength(1))
self.assertEqual(200, m._periodic_spacing['bar'])
# Now a single pass of the periodic tasks
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(60, idle, 1)
def test_periodic_tasks_constant(self):
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=0)
def bar(self):
return 'bar'
m = Manager()
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(60, idle, 1)
@mock.patch('time.time')
def test_periodic_tasks_idle_calculation(self, mock_time):
fake_time = 32503680000.0
mock_time.return_value = fake_time
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=10)
def bar(self, context):
return 'bar'
m = Manager()
# Ensure initial values are correct
self.assertEqual(1, len(m._periodic_tasks))
task_name, task = m._periodic_tasks[0]
# Test task values
self.assertEqual('bar', task_name)
self.assertEqual(10, task._periodic_spacing)
self.assertEqual(True, task._periodic_enabled)
self.assertEqual(False, task._periodic_external_ok)
self.assertEqual(False, task._periodic_immediate)
self.assertAlmostEqual(32503680000.0,
task._periodic_last_run)
# Test the manager's representation of those values
self.assertEqual(10, m._periodic_spacing[task_name])
self.assertAlmostEqual(32503680000.0,
m._periodic_last_run[task_name])
mock_time.return_value = fake_time + 5
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(5, idle, 1)
self.assertAlmostEqual(32503680000.0,
m._periodic_last_run[task_name])
mock_time.return_value = fake_time + 10
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(10, idle, 1)
self.assertAlmostEqual(32503680010.0,
m._periodic_last_run[task_name])
@mock.patch('time.time')
def test_periodic_tasks_immediate_runs_now(self, mock_time):
fake_time = 32503680000.0
mock_time.return_value = fake_time
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=10, run_immediately=True)
def bar(self, context):
return 'bar'
m = Manager()
# Ensure initial values are correct
self.assertEqual(1, len(m._periodic_tasks))
task_name, task = m._periodic_tasks[0]
# Test task values
self.assertEqual('bar', task_name)
self.assertEqual(10, task._periodic_spacing)
self.assertEqual(True, task._periodic_enabled)
self.assertEqual(False, task._periodic_external_ok)
self.assertEqual(True, task._periodic_immediate)
self.assertIsNone(task._periodic_last_run)
# Test the manager's representation of those values
self.assertEqual(10, m._periodic_spacing[task_name])
self.assertIsNone(m._periodic_last_run[task_name])
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(32503680000.0,
m._periodic_last_run[task_name])
self.assertAlmostEqual(10, idle, 1)
mock_time.return_value = fake_time + 5
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(5, idle, 1)
def test_periodic_tasks_disabled(self):
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=-1)
def bar(self):
return 'bar'
m = Manager()
idle = m.run_periodic_tasks(None)
self.assertAlmostEqual(60, idle, 1)
def test_external_running_here(self):
self.config(run_external_periodic_tasks=True)
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=200, external_process_ok=True)
def bar(self):
return 'bar'
m = Manager()
self.assertThat(m._periodic_tasks, matchers.HasLength(1))
def test_external_running_elsewhere(self):
self.config(run_external_periodic_tasks=False)
class Manager(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=200, external_process_ok=True)
def bar(self):
return 'bar'
m = Manager()
self.assertEqual([], m._periodic_tasks)