class SchedulerTestCase(TestCase):
    """
    Tests for :mod:`SchedulerService`
    """

    def setUp(self):
        """
        mock all the dependencies of SchedulingService that includes cass store,
        store's fetch and delete events methods, scaling group on which controller
        will execute scaling policy. Hence, controller.maybe_execute_scaling_policy.
        twisted.internet.task.Clock is used to simulate time
        """

        self.mock_store = iMock(IScalingGroupCollection, IScalingScheduleCollection)
        self.mock_group = iMock(IScalingGroup)
        self.mock_store.get_scaling_group.return_value = self.mock_group

        self.returns = [None]

        def _responses(*args):
            result = self.returns.pop(0)
            if isinstance(result, Exception):
                return defer.fail(result)
            return defer.succeed(result)

        self.mock_store.fetch_batch_of_events.side_effect = _responses

        self.mock_store.update_delete_events.return_value = defer.succeed(None)

        self.mock_generate_transaction_id = patch(
            self, 'otter.scheduler.generate_transaction_id',
            return_value='transaction-id')
        set_store(self.mock_store)
        self.addCleanup(set_store, None)

        # mock out modify state
        self.mock_state = mock.MagicMock(spec=[])  # so nothing can call it

        def _mock_modify_state(modifier, *args, **kwargs):
            modifier(self.mock_group, self.mock_state, *args, **kwargs)
            return defer.succeed(None)

        self.mock_group.modify_state.side_effect = _mock_modify_state

        self.maybe_exec_policy = patch(self, 'otter.scheduler.maybe_execute_scaling_policy')

        def _mock_with_lock(lock, func, *args, **kwargs):
            return defer.maybeDeferred(func, *args, **kwargs)

        self.mock_lock = patch(self, 'otter.scheduler.BasicLock')
        self.mock_with_lock = patch(self, 'otter.scheduler.with_lock')
        self.mock_with_lock.side_effect = _mock_with_lock
        self.slv_client = mock.MagicMock()
        self.otter_log = patch(self, 'otter.scheduler.otter_log')

        self.clock = Clock()
        self.scheduler_service = SchedulerService(100, 1, self.slv_client, self.clock)

        self.otter_log.bind.assert_called_once_with(system='otter.scheduler')
        self.log = self.otter_log.bind.return_value

        self.next_cron_occurrence = patch(self, 'otter.scheduler.next_cron_occurrence')
        self.next_cron_occurrence.return_value = 'newtrigger'

    def validate_calls(self, d, fetch_returns, update_delete_args):
        """
        Validate all the calls made in the service w.r.t to the events
        """
        fetch_call_count = len(fetch_returns)
        events = [event for fetch_return in fetch_returns for event in fetch_return]
        num_events = len(events)
        self.assertIsNone(self.successResultOf(d))
        self.assertEqual(self.mock_store.fetch_batch_of_events.call_count, fetch_call_count)
        if update_delete_args:
            self.assertEqual(self.mock_store.update_delete_events.call_args_list,
                             [mock.call(delete_events, update_events)
                              for delete_events, update_events in update_delete_args])
        self.assertEqual(self.mock_group.modify_state.call_count, num_events)
        self.assertEqual(self.mock_store.get_scaling_group.call_args_list,
                         [mock.call(mock.ANY, e['tenantId'], e['groupId']) for e in events])
        self.assertEqual(self.maybe_exec_policy.mock_calls,
                         [mock.call(mock.ANY, 'transaction-id', self.mock_group,
                          self.mock_state, policy_id=event['policyId']) for event in events])

    @mock.patch('otter.scheduler.generate_transaction_id', return_value='transid')
    @mock.patch('otter.scheduler.datetime', spec=['utcnow'])
    def test_empty(self, mock_datetime, mock_gentransid):
        """
        No policies are executed when ``fetch_batch_of_events`` return empty list
        i.e. no events are there before now
        """
        mock_datetime.utcnow.return_value = time = datetime(
            2012, 10, 10, 03, 20, 30, 0, None)
        self.returns = [[]]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [[]], None)
        self.assertFalse(self.mock_store.update_delete_events.called)

        self.log.bind.assert_called_once_with(scheduler_run_id='transid', utcnow=time)
        self.log.bind.return_value.msg.assert_called_once_with('Checking for events')

    def test_one(self):
        """
        policy is executed when its corresponding event is there before now
        """
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': None}]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events], [(['pol44'], [])])

    def test_policy_exec_logs(self):
        """
        The scheduler logs `CannotExecutePolicyError` as msg instead of err
        """
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': 'c1'}]
        self.returns = [events]
        self.mock_group.modify_state.side_effect = (
            lambda *_: defer.fail(CannotExecutePolicyError('t', 'g', 'p', 'w')))

        d = self.scheduler_service.check_for_events(100)

        self.assertIsNone(self.successResultOf(d))
        self.log.bind.return_value.bind(tenant_id='1234', scaling_group_id='scal44',
                                        policy_id='pol44')
        self.log.bind.return_value.bind.return_value.msg.assert_has_calls(
            [mock.call('Executing policy'),
             mock.call('Cannot execute policy',
                       reason=CheckFailure(CannotExecutePolicyError))])
        self.assertFalse(self.log.bind.return_value.bind.return_value.err.called)

    def test_many(self):
        """
        Events are fetched and processed as batches of 100. Its corresponding policies
        are executed.
        """
        events1 = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                    'trigger': 'now', 'cron': None} for i in range(100)]
        events2 = [{'tenantId': '1234', 'groupId': 'scal45', 'policyId': 'pol45',
                    'trigger': 'now', 'cron': None} for i in range(100)]
        self.returns = [events1, events2, []]
        fetch_returns = self.returns[:]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, fetch_returns, [(['pol44'] * 100, []), (['pol45'] * 100, [])])

    def test_timer_works(self):
        """
        The scheduler executes every x seconds
        """
        events1 = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                    'trigger': 'now', 'cron': None} for i in range(30)]
        events2 = [{'tenantId': '1234', 'groupId': 'scal45', 'policyId': 'pol45',
                    'trigger': 'now', 'cron': None} for i in range(20)]
        self.returns = [events1, events2]

        # events not fetched before startService
        self.validate_calls(defer.succeed(None), [], None)

        # events fetched after calling startService
        self.scheduler_service.startService()
        self.validate_calls(defer.succeed(None), [events1], [(['pol44'] * 30, [])])

        # events are fetched again after timer expires
        self.clock.advance(1)
        self.validate_calls(defer.succeed(None), [events1, events2],
                            [(['pol44'] * 30, []), (['pol45'] * 20, [])])

    def test_timer_works_on_error(self):
        """
        The scheduler executes every x seconds even if an occurs occurs while fetching events
        """
        # Copy fetch function from setUp and set it to fail
        fetch_func = self.mock_store.fetch_batch_of_events.side_effect
        self.mock_store.fetch_batch_of_events.side_effect = None
        self.mock_store.fetch_batch_of_events.return_value = defer.fail(TimedOutException())

        # Start service and see if update_delete_events got called
        self.scheduler_service.startService()
        self.assertFalse(self.mock_store.update_delete_events.called)

        # fix fetch function and advance clock to see if works next time
        self.mock_store.fetch_batch_of_events.side_effect = fetch_func
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': None} for i in range(30)]
        self.returns = [events]
        self.clock.advance(1)
        self.validate_calls(defer.succeed(None),
                            [[], events],  # first [] to account for failed fetch call
                            [(['pol44'] * 30, [])])

    def test_called_with_lock(self):
        """
        ``fetch_and_process`` is called with a lock
        """
        events1 = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                    'trigger': 'now', 'cron': None} for i in range(100)]
        events2 = [{'tenantId': '1234', 'groupId': 'scal45', 'policyId': 'pol45',
                    'trigger': 'now', 'cron': None} for i in range(20)]
        self.returns = [events1, events2]

        self.mock_lock.assert_called_once_with(self.slv_client, LOCK_TABLE_NAME, 'schedule',
                                               max_retry=0)

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events1, events2],
                            [(['pol44'] * 100, []), (['pol45'] * 20, [])])

        lock = self.mock_lock.return_value
        self.assertEqual(self.mock_with_lock.call_count, 2)
        self.assertEqual(self.mock_with_lock.mock_calls,
                         [mock.call(lock, self.scheduler_service.fetch_and_process, 100)] * 2)

    def test_does_nothing_on_no_lock(self):
        """
        ``check_for_events`` gracefully does nothing when it does not get a lock. It
        does not call ``fetch_and_process``
        """
        events1 = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                    'trigger': 'now', 'cron': None} for i in range(100)]
        events2 = [{'tenantId': '1234', 'groupId': 'scal45', 'policyId': 'pol45',
                    'trigger': 'now', 'cron': None} for i in range(20)]
        self.returns = [events1, events2]

        self.mock_lock.assert_called_once_with(self.slv_client, LOCK_TABLE_NAME, 'schedule',
                                               max_retry=0)
        with_lock_impl = lambda *args: defer.fail(BusyLockError(LOCK_TABLE_NAME, 'schedule'))
        self.mock_with_lock.side_effect = with_lock_impl

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [], None)
        lock = self.mock_lock.return_value
        self.assertEqual(self.mock_with_lock.mock_calls,
                         [mock.call(lock, self.scheduler_service.fetch_and_process, 100)])
        self.log.msg.assert_called_once_with("Couldn't get lock to process events",
                                             reason=CheckFailure(BusyLockError))

    def test_does_nothing_on_no_lock_second_time(self):
        """
        ``check_for_events`` gracefully does nothing when it does not get a lock after
        finishing first batch of 100 events. It does not call ``fetch_and_process`` second time
        """
        events1 = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                    'trigger': 'now', 'cron': None} for i in range(100)]
        events2 = [{'tenantId': '1234', 'groupId': 'scal45', 'policyId': 'pol45',
                    'trigger': 'now', 'cron': None} for i in range(20)]
        self.returns = [events1, events2]

        self.mock_lock.assert_called_once_with(self.slv_client, LOCK_TABLE_NAME, 'schedule',
                                               max_retry=0)

        _with_lock_first_time = [True]

        def _with_lock(lock, func, *args, **kwargs):
            if _with_lock_first_time[0]:
                _with_lock_first_time[0] = False
                return defer.maybeDeferred(func, *args, **kwargs)
            return defer.fail(BusyLockError(LOCK_TABLE_NAME, 'schedule'))

        self.mock_with_lock.side_effect = _with_lock

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events1], [(['pol44'] * 100, [])])
        lock = self.mock_lock.return_value
        self.assertEqual(self.mock_with_lock.mock_calls,
                         [mock.call(lock, self.scheduler_service.fetch_and_process, 100)] * 2)
        self.log.msg.assert_called_once_with("Couldn't get lock to process events",
                                             reason=CheckFailure(BusyLockError))

    def test_cron_updates(self):
        """
        The scheduler updates cron events
        """
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': 'c1'} for i in range(30)]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        exp_updated_events = []
        for event in events:
            event['trigger'] = 'newtrigger'
            exp_updated_events.append(event)
        self.validate_calls(d, [events], [([], exp_updated_events)])

    def test_cron_updates_and_deletes(self):
        """
        The scheduler updates cron events and deletes at-style events
        """
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': 'c1'},
                  {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol45',
                   'trigger': 'now', 'cron': None},
                  {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol46',
                   'trigger': 'now', 'cron': 'c2'}]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        exp_deleted_events = ['pol45']
        exp_updated_events = []
        for i in [0, 2]:
            event = events[i]
            event['trigger'] = 'newtrigger'
            exp_updated_events.append(event)
        self.validate_calls(d, [events], [(exp_deleted_events, exp_updated_events)])

    def test_nopolicy_or_group_events_deleted(self):
        """
        The scheduler does not update deleted policy/group's (that give NoSuchPolicyError or
        NoSuchScalingGroupError) events (for cron-style events) and deletes them
        """
        events = [{'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                   'trigger': 'now', 'cron': 'c1'},
                  {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol45',
                   'trigger': 'now', 'cron': 'c2'},
                  {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol46',
                   'trigger': 'now', 'cron': 'c3'},
                  {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol47',
                   'trigger': 'now', 'cron': None}]
        self.returns = [events]

        events_indexes = range(len(events))

        def _mock_modify_state(modifier, *args, **kwargs):
            index = events_indexes.pop(0)
            if index == 0:
                return defer.fail(NoSuchPolicyError('1234', 'scal44', 'pol44'))
            if index == 1:
                return defer.fail(NoSuchScalingGroupError('1234', 'scal44'))
            modifier(self.mock_group, self.mock_state, *args, **kwargs)
            return defer.succeed(None)

        self.mock_group.modify_state.side_effect = _mock_modify_state

        d = self.scheduler_service.check_for_events(100)

        exp_delete_events = ['pol44', 'pol45', 'pol47']
        events[2]['trigger'] = 'newtrigger'
        exp_update_events = [events[2]]

        # Not using validate_call since maybe_execute_scaling_policy calls do not match
        self.assertIsNone(self.successResultOf(d))
        self.assertEqual(self.mock_store.fetch_batch_of_events.call_count, 1)
        self.mock_store.update_delete_events.assert_called_once_with(exp_delete_events,
                                                                     exp_update_events)
        self.assertEqual(self.mock_group.modify_state.call_count, len(events))
        self.assertEqual(self.mock_store.get_scaling_group.call_args_list,
                         [mock.call(mock.ANY, e['tenantId'], e['groupId']) for e in events])

    def test_exec_event_logs(self):
        """
        `execute_event` logs error with all the ids bound
        """
        log = mock_log()
        log.err.return_value = None
        event = {'tenantId': '1234', 'groupId': 'scal44', 'policyId': 'pol44',
                 'trigger': 'now', 'cron': 'c1'}
        self.mock_group.modify_state.side_effect = lambda *_: defer.fail(ValueError('meh'))

        d = self.scheduler_service.execute_event(log, event, mock.Mock())

        self.assertIsNone(self.successResultOf(d))
        log.err.assert_called_once_with(CheckFailure(ValueError),
                                        'Scheduler failed to execute policy', tenant_id='1234',
                                        scaling_group_id='scal44', policy_id='pol44')
示例#2
0
class SchedulerTestCase(TestCase):
    """
    Tests for :mod:`SchedulerService`
    """

    def setUp(self):
        """
        mock all the dependencies of SchedulingService that includes cass store,
        store's fetch and delete events methods, scaling group on which controller
        will execute scaling policy. Hence, controller.maybe_execute_scaling_policy.
        twisted.internet.task.Clock is used to simulate time
        """

        self.mock_store = iMock(IScalingGroupCollection, IScalingScheduleCollection)
        self.mock_group = iMock(IScalingGroup)
        self.mock_store.get_scaling_group.return_value = self.mock_group

        self.returns = [None]

        def _responses(*args):
            result = self.returns.pop(0)
            if isinstance(result, Exception):
                return defer.fail(result)
            return defer.succeed(result)

        self.mock_store.fetch_batch_of_events.side_effect = _responses

        self.mock_store.update_delete_events.return_value = defer.succeed(None)

        self.mock_generate_transaction_id = patch(
            self, "otter.scheduler.generate_transaction_id", return_value="transaction-id"
        )

        # mock out modify state
        self.mock_state = mock.MagicMock(spec=[])  # so nothing can call it

        def _mock_modify_state(modifier, *args, **kwargs):
            modifier(self.mock_group, self.mock_state, *args, **kwargs)
            return defer.succeed(None)

        self.mock_group.modify_state.side_effect = _mock_modify_state

        self.maybe_exec_policy = patch(self, "otter.scheduler.maybe_execute_scaling_policy")

        def _mock_with_lock(lock, func, *args, **kwargs):
            return defer.maybeDeferred(func, *args, **kwargs)

        self.mock_lock = patch(self, "otter.scheduler.BasicLock")
        self.mock_with_lock = patch(self, "otter.scheduler.with_lock")
        self.mock_with_lock.side_effect = _mock_with_lock
        self.slv_client = mock.MagicMock()

        otter_log = patch(self, "otter.scheduler.otter_log")
        self.log = mock_log()
        otter_log.bind.return_value = self.log

        self.clock = Clock()
        self.scheduler_service = SchedulerService(100, 1, self.slv_client, self.mock_store, self.clock)

        otter_log.bind.assert_called_once_with(system="otter.scheduler")

        self.next_cron_occurrence = patch(self, "otter.scheduler.next_cron_occurrence")
        self.next_cron_occurrence.return_value = "newtrigger"

    def validate_calls(self, d, fetch_returns, update_delete_args):
        """
        Validate all the calls made in the service w.r.t to the events
        """
        fetch_call_count = len(fetch_returns)
        events = [event for fetch_return in fetch_returns for event in fetch_return]
        num_events = len(events)
        self.assertIsNone(self.successResultOf(d))
        self.assertEqual(self.mock_store.fetch_batch_of_events.call_count, fetch_call_count)
        if update_delete_args:
            self.assertEqual(
                self.mock_store.update_delete_events.call_args_list,
                [mock.call(delete_events, update_events) for delete_events, update_events in update_delete_args],
            )
        self.assertEqual(self.mock_group.modify_state.call_count, num_events)
        self.assertEqual(
            self.mock_store.get_scaling_group.call_args_list,
            [mock.call(mock.ANY, e["tenantId"], e["groupId"]) for e in events],
        )
        self.assertEqual(
            self.maybe_exec_policy.mock_calls,
            [
                mock.call(mock.ANY, "transaction-id", self.mock_group, self.mock_state, policy_id=event["policyId"])
                for event in events
            ],
        )

    @mock.patch("otter.scheduler.generate_transaction_id", return_value="transid")
    @mock.patch("otter.scheduler.datetime", spec=["utcnow"])
    def test_empty(self, mock_datetime, mock_gentransid):
        """
        No policies are executed when ``fetch_batch_of_events`` return empty list
        i.e. no events are there before now
        """
        mock_datetime.utcnow.return_value = datetime(2012, 10, 10, 03, 20, 30, 0, None)
        self.returns = [[]]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [[]], None)
        self.assertFalse(self.mock_store.update_delete_events.called)
        self.assertFalse(self.log.msg.called)

    def test_one(self):
        """
        policy is executed when its corresponding event is there before now
        """
        events = [{"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events], [(["pol44"], [])])

    def test_logging(self):
        """
        All the necessary messages are logged
        """
        events = [{"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}]
        self.returns = [events]

        self.scheduler_service.check_for_events(100)

        calls = [
            ("Processing {num_events} events", dict(num_events=1)),
            ("Executing policy", dict(tenant_id="1234", policy_id="pol44", scaling_group_id="scal44")),
            ("Deleting {policy_ids_deleting} events", dict(policy_ids_deleting=1)),
            ("Updating {policy_ids_updating} events", dict(policy_ids_updating=0)),
        ]

        self.log.msg.assert_has_calls(
            [mock.call(msg, scheduler_run_id="transaction-id", utcnow=mock.ANY, **kwargs) for msg, kwargs in calls]
        )

    def test_policy_exec_err_logs(self):
        """
        The scheduler logs `CannotExecutePolicyError` as msg instead of err
        """
        events = [{"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": "c1"}]
        self.returns = [events]
        self.mock_group.modify_state.side_effect = lambda *_: defer.fail(CannotExecutePolicyError("t", "g", "p", "w"))

        d = self.scheduler_service.check_for_events(100)

        self.assertIsNone(self.successResultOf(d))
        kwargs = dict(
            scheduler_run_id="transaction-id",
            utcnow=mock.ANY,
            tenant_id="1234",
            scaling_group_id="scal44",
            policy_id="pol44",
        )
        self.assertEqual(
            self.log.msg.mock_calls[2],
            mock.call("Cannot execute policy", reason=CheckFailure(CannotExecutePolicyError), **kwargs),
        )
        self.assertFalse(self.log.err.called)

    def test_many(self):
        """
        Events are fetched and processed as batches of 100. Its corresponding policies
        are executed.
        """
        events1 = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(100)
        ]
        events2 = [
            {"tenantId": "1234", "groupId": "scal45", "policyId": "pol45", "trigger": "now", "cron": None}
            for i in range(100)
        ]
        self.returns = [events1, events2, []]
        fetch_returns = self.returns[:]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, fetch_returns, [(["pol44"] * 100, []), (["pol45"] * 100, [])])

    def test_timer_works(self):
        """
        The scheduler executes every x seconds
        """
        events1 = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(30)
        ]
        events2 = [
            {"tenantId": "1234", "groupId": "scal45", "policyId": "pol45", "trigger": "now", "cron": None}
            for i in range(20)
        ]
        self.returns = [events1, events2]

        # events not fetched before startService
        self.validate_calls(defer.succeed(None), [], None)

        # events fetched after calling startService
        self.scheduler_service.startService()
        self.validate_calls(defer.succeed(None), [events1], [(["pol44"] * 30, [])])

        # events are fetched again after timer expires
        self.clock.advance(1)
        self.validate_calls(defer.succeed(None), [events1, events2], [(["pol44"] * 30, []), (["pol45"] * 20, [])])

    def test_timer_works_on_error(self):
        """
        The scheduler executes every x seconds even if an occurs occurs while fetching events
        """
        # Copy fetch function from setUp and set it to fail
        fetch_func = self.mock_store.fetch_batch_of_events.side_effect
        self.mock_store.fetch_batch_of_events.side_effect = None
        self.mock_store.fetch_batch_of_events.return_value = defer.fail(TimedOutException())

        # Start service and see if update_delete_events got called
        self.scheduler_service.startService()
        self.assertFalse(self.mock_store.update_delete_events.called)

        # fix fetch function and advance clock to see if works next time
        self.mock_store.fetch_batch_of_events.side_effect = fetch_func
        events = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(30)
        ]
        self.returns = [events]
        self.clock.advance(1)
        self.validate_calls(
            defer.succeed(None), [[], events], [(["pol44"] * 30, [])]  # first [] to account for failed fetch call
        )

    def test_called_with_lock(self):
        """
        ``fetch_and_process`` is called with a lock
        """
        events1 = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(100)
        ]
        events2 = [
            {"tenantId": "1234", "groupId": "scal45", "policyId": "pol45", "trigger": "now", "cron": None}
            for i in range(20)
        ]
        self.returns = [events1, events2]

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events1, events2], [(["pol44"] * 100, []), (["pol45"] * 20, [])])

        self.assertEqual(
            self.mock_lock.mock_calls,
            [mock.call(self.slv_client, LOCK_TABLE_NAME, "schedule", max_retry=0, log=mock.ANY)] * 2,
        )
        lock = self.mock_lock.return_value
        self.assertEqual(
            self.mock_with_lock.mock_calls, [mock.call(lock, self.scheduler_service.fetch_and_process, 100)] * 2
        )

    def test_does_nothing_on_no_lock(self):
        """
        ``check_for_events`` gracefully does nothing when it does not get a lock. It
        does not call ``fetch_and_process``
        """
        events1 = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(100)
        ]
        events2 = [
            {"tenantId": "1234", "groupId": "scal45", "policyId": "pol45", "trigger": "now", "cron": None}
            for i in range(20)
        ]
        self.returns = [events1, events2]

        with_lock_impl = lambda *args: defer.fail(BusyLockError(LOCK_TABLE_NAME, "schedule"))
        self.mock_with_lock.side_effect = with_lock_impl

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [], None)
        self.mock_lock.assert_called_once_with(self.slv_client, LOCK_TABLE_NAME, "schedule", max_retry=0, log=mock.ANY)
        lock = self.mock_lock.return_value
        self.mock_with_lock.assert_called_once_with(lock, self.scheduler_service.fetch_and_process, 100)
        self.log.msg.assert_called_once_with(
            "Couldn't get lock to process events", reason=CheckFailure(BusyLockError), category="locking"
        )

    def test_does_nothing_on_no_lock_second_time(self):
        """
        ``check_for_events`` gracefully does nothing when it does not get a lock after
        finishing first batch of 100 events. It does not call ``fetch_and_process`` second time
        """
        events1 = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": None}
            for i in range(100)
        ]
        events2 = [
            {"tenantId": "1234", "groupId": "scal45", "policyId": "pol45", "trigger": "now", "cron": None}
            for i in range(20)
        ]
        self.returns = [events1, events2]

        _with_lock_first_time = [True]

        def _with_lock(lock, func, *args, **kwargs):
            if _with_lock_first_time[0]:
                _with_lock_first_time[0] = False
                return defer.maybeDeferred(func, *args, **kwargs)
            return defer.fail(BusyLockError(LOCK_TABLE_NAME, "schedule"))

        self.mock_with_lock.side_effect = _with_lock

        d = self.scheduler_service.check_for_events(100)

        self.validate_calls(d, [events1], [(["pol44"] * 100, [])])
        self.assertEqual(
            self.mock_lock.mock_calls,
            [mock.call(self.slv_client, LOCK_TABLE_NAME, "schedule", max_retry=0, log=mock.ANY)] * 2,
        )
        lock = self.mock_lock.return_value
        self.assertEqual(
            self.mock_with_lock.mock_calls, [mock.call(lock, self.scheduler_service.fetch_and_process, 100)] * 2
        )
        self.log.msg.assert_called_with(
            "Couldn't get lock to process events", reason=CheckFailure(BusyLockError), category="locking"
        )

    def test_cron_updates(self):
        """
        The scheduler updates cron events
        """
        events = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": "c1"}
            for i in range(30)
        ]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        exp_updated_events = []
        for event in events:
            event["trigger"] = "newtrigger"
            exp_updated_events.append(event)
        self.validate_calls(d, [events], [([], exp_updated_events)])

    def test_cron_updates_and_deletes(self):
        """
        The scheduler updates cron events and deletes at-style events
        """
        events = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": "c1"},
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol45", "trigger": "now", "cron": None},
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol46", "trigger": "now", "cron": "c2"},
        ]
        self.returns = [events]

        d = self.scheduler_service.check_for_events(100)

        exp_deleted_events = ["pol45"]
        exp_updated_events = []
        for i in [0, 2]:
            event = events[i]
            event["trigger"] = "newtrigger"
            exp_updated_events.append(event)
        self.validate_calls(d, [events], [(exp_deleted_events, exp_updated_events)])

    def test_nopolicy_or_group_events_deleted(self):
        """
        The scheduler does not update deleted policy/group's (that give NoSuchPolicyError or
        NoSuchScalingGroupError) events (for cron-style events) and deletes them
        """
        events = [
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": "c1"},
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol45", "trigger": "now", "cron": "c2"},
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol46", "trigger": "now", "cron": "c3"},
            {"tenantId": "1234", "groupId": "scal44", "policyId": "pol47", "trigger": "now", "cron": None},
        ]
        self.returns = [events]

        events_indexes = range(len(events))

        def _mock_modify_state(modifier, *args, **kwargs):
            index = events_indexes.pop(0)
            if index == 0:
                return defer.fail(NoSuchPolicyError("1234", "scal44", "pol44"))
            if index == 1:
                return defer.fail(NoSuchScalingGroupError("1234", "scal44"))
            modifier(self.mock_group, self.mock_state, *args, **kwargs)
            return defer.succeed(None)

        self.mock_group.modify_state.side_effect = _mock_modify_state

        d = self.scheduler_service.check_for_events(100)

        exp_delete_events = ["pol44", "pol45", "pol47"]
        events[2]["trigger"] = "newtrigger"
        exp_update_events = [events[2]]

        # Not using validate_call since maybe_execute_scaling_policy calls do not match
        self.assertIsNone(self.successResultOf(d))
        self.assertEqual(self.mock_store.fetch_batch_of_events.call_count, 1)
        self.mock_store.update_delete_events.assert_called_once_with(exp_delete_events, exp_update_events)
        self.assertEqual(self.mock_group.modify_state.call_count, len(events))
        self.assertEqual(
            self.mock_store.get_scaling_group.call_args_list,
            [mock.call(mock.ANY, e["tenantId"], e["groupId"]) for e in events],
        )

    def test_exec_event_logs(self):
        """
        `execute_event` logs error with all the ids bound
        """
        log = mock_log()
        log.err.return_value = None
        event = {"tenantId": "1234", "groupId": "scal44", "policyId": "pol44", "trigger": "now", "cron": "c1"}
        self.mock_group.modify_state.side_effect = lambda *_: defer.fail(ValueError("meh"))

        d = self.scheduler_service.execute_event(log, event, mock.Mock())

        self.assertIsNone(self.successResultOf(d))
        log.err.assert_called_once_with(
            CheckFailure(ValueError),
            "Scheduler failed to execute policy",
            tenant_id="1234",
            scaling_group_id="scal44",
            policy_id="pol44",
        )