Files
neutron-lib/neutron_lib/tests/unit/callbacks/test_manager.py
Rodolfo Alonso Hernandez 08b8c6d33b Add the "cancellable" flag to the `CallbacksManager` events
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
2023-09-12 04:32:26 +00:00

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])