The ``CallbacksManager`` class considers, by default, that the events starting with "before_" and "precommit_" can raise an Exception (``CallbackFailure``) in case that the callbacks associated to these methods exit with an error. However there are some other events (those started with "after_") that won't generate an exception in case of error. The error will be logged but the process will continue. This new functionality adds the possibility of adding any kind of event and mark is as "cancellable". The ``CallbacksManager`` instance will check the errors returned by the callback methods and if any of them is marked as "cancellable", the manager will raise a ``CallbackFailure`` exception, terminating the process. In case of being a Neutron worker, for example, the ``oslo_service.service.Services`` class will restart the process again. Related-Bug: #2036607 Change-Id: Ie1e7be6d70cca957c1b1b6c15b402e8bc6523865
452 lines
18 KiB
Python
452 lines
18 KiB
Python
# Copyright 2015 OpenStack Foundation
|
|
#
|
|
# 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.
|
|
|
|
from unittest import mock
|
|
|
|
import ddt
|
|
from oslo_db import exception as db_exc
|
|
from oslotest import base
|
|
|
|
from neutron_lib.callbacks import events
|
|
from neutron_lib.callbacks import exceptions
|
|
from neutron_lib.callbacks import manager
|
|
from neutron_lib.callbacks import priority_group
|
|
from neutron_lib.callbacks import resources
|
|
|
|
|
|
PRI_HIGH = 0
|
|
PRI_MED = 5000
|
|
PRI_LOW = 10000
|
|
|
|
|
|
class ObjectWithCallback(object):
|
|
|
|
def __init__(self):
|
|
self.counter = 0
|
|
|
|
def callback(self, *args, **kwargs):
|
|
self.counter += 1
|
|
|
|
|
|
class GloriousObjectWithCallback(ObjectWithCallback):
|
|
pass
|
|
|
|
|
|
def callback_1(*args, **kwargs):
|
|
callback_1.counter += 1
|
|
|
|
|
|
callback_id_1 = manager._get_id(callback_1)
|
|
|
|
|
|
def callback_2(*args, **kwargs):
|
|
callback_2.counter += 1
|
|
|
|
|
|
callback_id_2 = manager._get_id(callback_2)
|
|
|
|
|
|
def callback_raise(*args, **kwargs):
|
|
raise Exception()
|
|
|
|
|
|
def callback_raise_retriable(*args, **kwargs):
|
|
raise db_exc.DBDeadlock()
|
|
|
|
|
|
def callback_raise_not_retriable(*args, **kwargs):
|
|
raise Exception()
|
|
|
|
|
|
def callback_3(resource, event, trigger, payload):
|
|
callback_3.counter += 1
|
|
|
|
|
|
@ddt.ddt
|
|
class CallBacksManagerTestCase(base.BaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(CallBacksManagerTestCase, self).setUp()
|
|
self.manager = manager.CallbacksManager()
|
|
self.event_payload = events.EventPayload(object())
|
|
callback_1.counter = 0
|
|
callback_2.counter = 0
|
|
callback_3.counter = 0
|
|
|
|
@ddt.data(True, False)
|
|
def test_subscribe(self, cancellable):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE,
|
|
cancellable=cancellable)
|
|
self.assertIsNotNone(
|
|
self.manager._callbacks[resources.PORT][events.BEFORE_CREATE])
|
|
self.assertIn(callback_id_1, self.manager._index)
|
|
self.assertEqual(self.__module__ + '.callback_1-%s' %
|
|
hash(callback_1), callback_id_1)
|
|
self.assertEqual(cancellable,
|
|
self.manager._callbacks[resources.PORT]
|
|
[events.BEFORE_CREATE][0][2])
|
|
|
|
def test_subscribe_unknown(self):
|
|
self.manager.subscribe(
|
|
callback_1, 'my_resource', 'my-event')
|
|
self.assertIsNotNone(
|
|
self.manager._callbacks['my_resource']['my-event'])
|
|
self.assertIn(callback_id_1, self.manager._index)
|
|
|
|
def test_subscribe_is_idempotent(self):
|
|
for cancellable in (True, False):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE,
|
|
cancellable=cancellable)
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE,
|
|
cancellable=cancellable)
|
|
self.assertEqual(
|
|
1,
|
|
len(self.manager._callbacks[resources.PORT][events.BEFORE_CREATE]))
|
|
# The first event registered had cancellable=True.
|
|
self.assertTrue(self.manager._callbacks[resources.PORT]
|
|
[events.BEFORE_CREATE][0][2])
|
|
callbacks = self.manager._index[callback_id_1][resources.PORT]
|
|
self.assertEqual(1, len(callbacks))
|
|
|
|
def test_subscribe_multiple_callbacks(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertEqual(2, len(self.manager._index))
|
|
self.assertEqual(
|
|
1,
|
|
len(self.manager._callbacks[resources.PORT][events.BEFORE_CREATE]))
|
|
self.assertEqual(
|
|
2,
|
|
len(self.manager._callbacks
|
|
[resources.PORT][events.BEFORE_CREATE][0][1]))
|
|
|
|
def test_unsubscribe_during_iteration(self):
|
|
def unsub(r, e, *a, **k):
|
|
return self.manager.unsubscribe(unsub, r, e)
|
|
|
|
self.manager.subscribe(unsub, resources.PORT,
|
|
events.BEFORE_CREATE)
|
|
self.manager.publish(resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=self.event_payload)
|
|
self.assertNotIn(unsub, self.manager._index)
|
|
|
|
@ddt.data(True, False)
|
|
def test_unsubscribe(self, cancellable):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE,
|
|
cancellable=cancellable)
|
|
self.manager.unsubscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertNotIn(
|
|
callback_id_1,
|
|
self.manager._callbacks[resources.PORT][events.BEFORE_CREATE])
|
|
self.assertNotIn(callback_id_1, self.manager._index)
|
|
|
|
def test_unsubscribe_unknown_callback(self):
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.unsubscribe(callback_1, mock.ANY, mock.ANY)
|
|
self.assertEqual(1, len(self.manager._index))
|
|
|
|
def test_fail_to_unsubscribe(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertRaises(exceptions.Invalid,
|
|
self.manager.unsubscribe,
|
|
callback_1, resources.PORT, None)
|
|
self.assertRaises(exceptions.Invalid,
|
|
self.manager.unsubscribe,
|
|
callback_1, None, events.BEFORE_CREATE)
|
|
|
|
@ddt.data(True, False)
|
|
def test_unsubscribe_is_idempotent(self, cancellable):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE,
|
|
cancellable=cancellable)
|
|
self.manager.unsubscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.unsubscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertNotIn(callback_id_1, self.manager._index)
|
|
self.assertNotIn(callback_id_1,
|
|
self.manager._callbacks[resources.PORT]
|
|
[events.BEFORE_CREATE])
|
|
|
|
def test_unsubscribe_by_resource(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_DELETE)
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.BEFORE_DELETE)
|
|
self.manager.unsubscribe_by_resource(callback_1, resources.PORT)
|
|
self.assertEqual(
|
|
0,
|
|
len(self.manager._callbacks
|
|
[resources.PORT][events.BEFORE_CREATE]))
|
|
self.assertEqual(
|
|
1,
|
|
len(self.manager._callbacks[resources.PORT][events.BEFORE_DELETE]))
|
|
self.assertIn(
|
|
callback_id_2,
|
|
self.manager._callbacks
|
|
[resources.PORT][events.BEFORE_DELETE][0][1])
|
|
self.assertNotIn(callback_id_1, self.manager._index)
|
|
|
|
def test_unsubscribe_all(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_DELETE)
|
|
self.manager.subscribe(
|
|
callback_1, resources.ROUTER, events.BEFORE_CREATE)
|
|
self.manager.unsubscribe_all(callback_1)
|
|
self.assertNotIn(
|
|
callback_id_1,
|
|
self.manager._callbacks[resources.PORT][events.BEFORE_CREATE])
|
|
self.assertNotIn(callback_id_1, self.manager._index)
|
|
|
|
def test_publish_none(self):
|
|
self.manager.publish(resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=self.event_payload)
|
|
self.assertEqual(0, callback_1.counter)
|
|
self.assertEqual(0, callback_2.counter)
|
|
|
|
def test_feebly_referenced_callback(self):
|
|
self.manager.subscribe(lambda *x, **y: None, resources.PORT,
|
|
events.BEFORE_CREATE)
|
|
self.manager.publish(resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=self.event_payload)
|
|
|
|
def test_publish_with_exception(self):
|
|
with mock.patch.object(self.manager, '_notify_loop') as n:
|
|
n.return_value = ['error']
|
|
self.assertRaises(exceptions.CallbackFailure,
|
|
self.manager.publish,
|
|
mock.ANY, events.BEFORE_CREATE, mock.ANY,
|
|
payload=self.event_payload)
|
|
expected_calls = [
|
|
mock.call(mock.ANY, 'before_create', mock.ANY,
|
|
self.event_payload),
|
|
mock.call(mock.ANY, 'abort_create', mock.ANY,
|
|
self.event_payload)
|
|
]
|
|
n.assert_has_calls(expected_calls)
|
|
|
|
def test_publish_with_precommit_exception(self):
|
|
with mock.patch.object(self.manager, '_notify_loop') as n:
|
|
n.return_value = ['error']
|
|
self.assertRaises(exceptions.CallbackFailure,
|
|
self.manager.publish,
|
|
mock.ANY, events.PRECOMMIT_UPDATE, mock.ANY,
|
|
payload=self.event_payload)
|
|
expected_calls = [
|
|
mock.call(mock.ANY, 'precommit_update', mock.ANY,
|
|
self.event_payload),
|
|
]
|
|
n.assert_has_calls(expected_calls)
|
|
|
|
def test_publish_handle_exception(self):
|
|
self.manager.subscribe(
|
|
callback_raise, resources.PORT, events.BEFORE_CREATE)
|
|
e = self.assertRaises(exceptions.CallbackFailure, self.manager.publish,
|
|
resources.PORT, events.BEFORE_CREATE, self,
|
|
payload=self.event_payload)
|
|
self.assertIsInstance(e.errors[0], exceptions.NotificationError)
|
|
|
|
def test_publish_handle_retriable_exception(self):
|
|
self.manager.subscribe(
|
|
callback_raise_retriable, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertRaises(db_exc.RetryRequest, self.manager.publish,
|
|
resources.PORT, events.BEFORE_CREATE, self,
|
|
payload=self.event_payload)
|
|
|
|
def test_publish_handle_not_retriable_exception(self):
|
|
self.manager.subscribe(
|
|
callback_raise_not_retriable, resources.PORT, events.BEFORE_CREATE)
|
|
self.assertRaises(exceptions.CallbackFailure, self.manager.publish,
|
|
resources.PORT, events.BEFORE_CREATE, self,
|
|
payload=self.event_payload)
|
|
|
|
def test_publish_handle_not_retriable_exception_no_cancellable_flag(self):
|
|
self.manager.subscribe(
|
|
callback_raise_not_retriable, resources.PORT, events.AFTER_INIT)
|
|
# No exception is raised.
|
|
self.manager.publish(resources.PORT, events.AFTER_INIT, self,
|
|
payload=self.event_payload)
|
|
|
|
def test_publish_handle_not_retriable_exception_cancellable_flag(self):
|
|
self.manager.subscribe(
|
|
callback_raise_not_retriable, resources.PORT, events.AFTER_INIT,
|
|
cancellable=True)
|
|
self.assertRaises(exceptions.CallbackFailure, self.manager.publish,
|
|
resources.PORT, events.AFTER_INIT, self,
|
|
payload=self.event_payload)
|
|
|
|
def test_publish_called_once_with_no_failures(self):
|
|
with mock.patch.object(self.manager, '_notify_loop') as n:
|
|
n.return_value = False
|
|
self.manager.publish(resources.PORT, events.BEFORE_CREATE,
|
|
mock.ANY,
|
|
payload=self.event_payload)
|
|
n.assert_called_once_with(
|
|
resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
self.event_payload)
|
|
|
|
def test__notify_loop_single_event(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager._notify_loop(
|
|
resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=mock.ANY)
|
|
self.assertEqual(1, callback_1.counter)
|
|
self.assertEqual(1, callback_2.counter)
|
|
|
|
def test__notify_loop_multiple_events(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_1, resources.ROUTER, events.BEFORE_DELETE)
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager._notify_loop(
|
|
resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=mock.ANY)
|
|
self.manager._notify_loop(
|
|
resources.ROUTER, events.BEFORE_DELETE, mock.ANY,
|
|
payload=mock.ANY)
|
|
self.assertEqual(2, callback_1.counter)
|
|
self.assertEqual(1, callback_2.counter)
|
|
|
|
def test_clearing_subscribers(self):
|
|
self.manager.subscribe(
|
|
callback_1, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_2, resources.PORT, events.AFTER_CREATE)
|
|
self.assertEqual(2, len(self.manager._callbacks[resources.PORT]))
|
|
self.assertEqual(2, len(self.manager._index))
|
|
self.manager.clear()
|
|
self.assertEqual(0, len(self.manager._callbacks))
|
|
self.assertEqual(0, len(self.manager._index))
|
|
|
|
def test_callback_priority(self):
|
|
pri_first = priority_group.PRIORITY_DEFAULT - 100
|
|
pri_last = priority_group.PRIORITY_DEFAULT + 100
|
|
# lowest priority value should be first in the _callbacks
|
|
self.manager.subscribe(callback_1, 'my-resource', 'my-event')
|
|
self.manager.subscribe(callback_2, 'my-resource',
|
|
'my-event', pri_last)
|
|
self.manager.subscribe(callback_3, 'my-resource',
|
|
'my-event', pri_first)
|
|
callbacks = self.manager._callbacks['my-resource']['my-event']
|
|
# callbacks should be sorted based on priority for resource and event
|
|
self.assertEqual(3, len(callbacks))
|
|
self.assertEqual(pri_first, callbacks[0][0])
|
|
self.assertEqual(priority_group.PRIORITY_DEFAULT, callbacks[1][0])
|
|
self.assertEqual(pri_last, callbacks[2][0])
|
|
|
|
@mock.patch('neutron_lib.callbacks.manager.CallbacksManager._del_callback')
|
|
def test_del_callback_called_on_unsubscribe(self, mock_cb):
|
|
self.manager.subscribe(callback_1, 'my-resource', 'my-event')
|
|
callback_id = self.manager._find(callback_1)
|
|
callbacks = self.manager._callbacks['my-resource']['my-event']
|
|
self.assertEqual(1, len(callbacks))
|
|
self.manager.unsubscribe(callback_1, 'my-resource', 'my-event')
|
|
mock_cb.assert_called_once_with(callbacks, callback_id)
|
|
|
|
@mock.patch("neutron_lib.callbacks.manager.LOG")
|
|
def test_callback_order(self, _logger):
|
|
self.manager.subscribe(callback_1, 'my-resource', 'my-event', PRI_MED)
|
|
self.manager.subscribe(callback_2, 'my-resource', 'my-event', PRI_HIGH)
|
|
self.manager.subscribe(callback_3, 'my-resource', 'my-event', PRI_LOW)
|
|
self.assertEqual(
|
|
3, len(self.manager._callbacks['my-resource']['my-event']))
|
|
self.manager.unsubscribe(callback_3, 'my-resource', 'my-event')
|
|
self.manager.publish('my-resource', 'my-event', mock.ANY,
|
|
payload=self.event_payload)
|
|
# callback_3 should be deleted and not executed
|
|
self.assertEqual(
|
|
2, len(self.manager._callbacks['my-resource']['my-event']))
|
|
self.assertEqual(0, callback_3.counter)
|
|
# executed callbacks should have counter incremented
|
|
self.assertEqual(1, callback_2.counter)
|
|
self.assertEqual(1, callback_1.counter)
|
|
callback_ids = _logger.debug.mock_calls[4][1][1]
|
|
# callback_2 should be first in exceution as it has higher priority
|
|
self.assertEqual(callback_id_2, callback_ids[0])
|
|
self.assertEqual(callback_id_1, callback_ids[1])
|
|
|
|
@mock.patch("neutron_lib.callbacks.manager.LOG")
|
|
def test__notify_loop_skip_log_errors(self, _logger):
|
|
self.manager.subscribe(
|
|
callback_raise, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.subscribe(
|
|
callback_raise, resources.PORT, events.PRECOMMIT_CREATE)
|
|
self.manager._notify_loop(
|
|
resources.PORT, events.BEFORE_CREATE, mock.ANY, payload=mock.ANY)
|
|
self.manager._notify_loop(
|
|
resources.PORT, events.PRECOMMIT_CREATE, mock.ANY,
|
|
payload=mock.ANY)
|
|
self.assertFalse(_logger.exception.call_count)
|
|
self.assertTrue(_logger.debug.call_count)
|
|
|
|
def test_object_instances_as_subscribers(self):
|
|
"""Ensures that the manager doesn't think these are equivalent."""
|
|
a = GloriousObjectWithCallback()
|
|
b = ObjectWithCallback()
|
|
c = ObjectWithCallback()
|
|
for o in (a, b, c):
|
|
self.manager.subscribe(
|
|
o.callback, resources.PORT, events.BEFORE_CREATE)
|
|
# ensure idempotency remains for a single object
|
|
self.manager.subscribe(
|
|
o.callback, resources.PORT, events.BEFORE_CREATE)
|
|
self.manager.publish(resources.PORT, events.BEFORE_CREATE, mock.ANY,
|
|
payload=events.EventPayload(object()))
|
|
self.assertEqual(1, a.counter)
|
|
self.assertEqual(1, b.counter)
|
|
self.assertEqual(1, c.counter)
|
|
|
|
def test_publish_invalid_payload(self):
|
|
self.assertRaises(exceptions.Invalid, self.manager.publish,
|
|
resources.PORT, events.AFTER_DELETE, self,
|
|
payload=object())
|
|
|
|
def test_publish_empty_payload(self):
|
|
notify_payload = []
|
|
|
|
def _memo(resource, event, trigger, payload=None):
|
|
notify_payload.append(payload)
|
|
|
|
self.manager.subscribe(_memo, 'x', 'y')
|
|
self.manager.publish('x', 'y', self)
|
|
self.assertIsNone(notify_payload[0])
|
|
|
|
def test_publish_payload(self):
|
|
notify_payload = []
|
|
|
|
def _memo(resource, event, trigger, payload=None):
|
|
notify_payload.append(payload)
|
|
|
|
self.manager.subscribe(_memo, 'x', 'y')
|
|
self.manager.publish('x', 'y', self, payload=self.event_payload)
|
|
self.assertEqual(self.event_payload, notify_payload[0])
|