1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-12-02 23:58:38 +02:00

Refactor preview functionality to use temp tables (#194)

This commit is contained in:
Diego Cepeda
2018-08-02 16:04:34 -07:00
committed by Joe Gillotti
parent 025793a592
commit 57fae06277
5 changed files with 94 additions and 194 deletions

View File

@@ -13,6 +13,7 @@ def on_get(req, resp, schedule_id):
Run the scheduler on demand from a given point in time. Unlike populate it doen't permanently delete or insert anything.
"""
start_time = float(req.get_param('start', required=True))
table_name = 'temp_event'
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
@@ -26,6 +27,14 @@ def on_get(req, resp, schedule_id):
scheduler = load_scheduler(scheduler_name)
schedule = get_schedules({'id': schedule_id})[0]
check_team_auth(schedule['team'], req)
scheduler.preview(schedule, start_time, (connection, cursor), req, resp)
start__lt = req.get_param('start__lt', required=True)
end__ge = req.get_param('end__ge', required=True)
team__eq = req.get_param('team__eq', required=True)
cursor.execute('CREATE TEMPORARY TABLE IF NOT EXISTS `temp_event` AS (SELECT * FROM `event` WHERE `start` < %s AND `end` > %s)', (start__lt, end__ge))
scheduler.populate(schedule, start_time, (connection, cursor), table_name)
resp.body = scheduler.build_preview_response(cursor, start__lt, end__ge, team__eq, table_name)
cursor.close()
connection.close()

View File

@@ -14,56 +14,22 @@ SECONDS_IN_A_DAY = 24 * 60 * 60
SECONDS_IN_A_WEEK = SECONDS_IN_A_DAY * 7
columns = {
'id': '`event`.`id` as `id`',
'start': '`event`.`start` as `start`',
'end': '`event`.`end` as `end`',
'id': '`temp_event`.`id` as `id`',
'start': '`temp_event`.`start` as `start`',
'end': '`temp_event`.`end` as `end`',
'role': '`role`.`name` as `role`',
'team': '`team`.`name` as `team`',
'user': '`user`.`name` as `user`',
'full_name': '`user`.`full_name` as `full_name`',
'schedule_id': '`event`.`schedule_id`',
'link_id': '`event`.`link_id`',
'note': '`event`.`note`',
'schedule_id': '`temp_event`.`schedule_id`',
'link_id': '`temp_event`.`link_id`',
'note': '`temp_event`.`note`',
}
constraints = {
'id': '`event`.`id` = %s',
'id__eq': '`event`.`id` = %s',
'id__ne': '`event`.`id` != %s',
'id__gt': '`event`.`id` > %s',
'id__ge': '`event`.`id` >= %s',
'id__lt': '`event`.`id` < %s',
'id__le': '`event`.`id` <= %s',
'start': '`event`.`start` = %s',
'start__eq': '`event`.`start` = %s',
'start__ne': '`event`.`start` != %s',
'start__gt': '`event`.`start` > %s',
'start__ge': '`event`.`start` >= %s',
'start__lt': '`event`.`start` < %s',
'start__le': '`event`.`start` <= %s',
'end': '`event`.`end` = %s',
'end__eq': '`event`.`end` = %s',
'end__ne': '`event`.`end` != %s',
'end__gt': '`event`.`end` > %s',
'end__ge': '`event`.`end` >= %s',
'end__lt': '`event`.`end` < %s',
'end__le': '`event`.`end` <= %s',
'role': '`role`.`name` = %s',
'role__eq': '`role`.`name` = %s',
'role__contains': '`role`.`name` LIKE CONCAT("%%", %s, "%%")',
'role__startswith': '`role`.`name` LIKE CONCAT(%s, "%%")',
'role__endswith': '`role`.`name` LIKE CONCAT("%%", %s)',
'team': '`team`.`name` = %s',
'start__lt': '`temp_event`.`start` < %s',
'end__ge': '`temp_event`.`end` >= %s',
'team__eq': '`team`.`name` = %s',
'team__contains': '`team`.`name` LIKE CONCAT("%%", %s, "%%")',
'team__startswith': '`team`.`name` LIKE CONCAT(%s, "%%")',
'team__endswith': '`team`.`name` LIKE CONCAT("%%", %s)',
'team_id': '`team`.`id` = %s',
'user': '`user`.`name` = %s',
'user__eq': '`user`.`name` = %s',
'user__contains': '`user`.`name` LIKE CONCAT("%%", %s, "%%")',
'user__startswith': '`user`.`name` LIKE CONCAT(%s, "%%")',
'user__endswith': '`user`.`name` LIKE CONCAT("%%", %s)'
}
all_columns = ', '.join(columns.values())
@@ -79,9 +45,9 @@ class Scheduler(object):
role_id = cursor.fetchone()['id']
return role_id
def get_schedule_last_event_end(self, schedule, cursor):
cursor.execute('SELECT `end` FROM `event` WHERE `schedule_id` = %r ORDER BY `end` DESC LIMIT 1',
schedule['id'])
def get_schedule_last_event_end(self, schedule, cursor, table_name='event'):
query = 'SELECT `end` FROM `%s` WHERE `schedule_id` = %%r ORDER BY `end` DESC LIMIT 1' % table_name
cursor.execute(query, schedule['id'])
if cursor.rowcount != 0:
return cursor.fetchone()['end']
else:
@@ -103,7 +69,7 @@ class Scheduler(object):
AND `user`.`active` = TRUE''', roster_id)
return [r['user_id'] for r in cursor]
def get_busy_user_by_event_range(self, user_ids, team_id, events, cursor):
def get_busy_user_by_event_range(self, user_ids, team_id, events, cursor, table_name='event'):
''' Find which users have overlapping events for the same team in this time range'''
query_params = [user_ids]
range_check = []
@@ -123,50 +89,53 @@ class Scheduler(object):
query_params += [sub['subscription_id'], sub['role_id']]
query = '''
SELECT DISTINCT `user_id` FROM `event`
SELECT DISTINCT `user_id` FROM `%s`
WHERE `user_id` in %%s AND (%s) AND (%s)
''' % (' OR '.join(range_check), ' OR '.join(team_check))
''' % (table_name, ' OR '.join(range_check), ' OR '.join(team_check))
cursor.execute(query, query_params)
return [r['user_id'] for r in cursor.fetchall()]
def find_least_active_user_id_by_team(self, user_ids, team_id, start_time, role_id, cursor):
def find_least_active_user_id_by_team(self, user_ids, team_id, start_time, role_id, cursor, table_name='event'):
'''
Of the people who have been oncall before, finds those who haven't been oncall for the longest. Start
time refers to the start time of the event being created, so we don't accidentally look at future
events when determining who was oncall in the past. Done on a per-role basis, so we don't take manager
or vacation shifts into account
'''
cursor.execute('''
SELECT `user_id`, MAX(`end`) AS `last_end` FROM `event`
WHERE `team_id` = %s AND `user_id` IN %s AND `end` <= %s
AND `role_id` = %s
query = '''
SELECT `user_id`, MAX(`end`) AS `last_end` FROM `%s`
WHERE `team_id` = %%s AND `user_id` IN %%s AND `end` <= %%s
AND `role_id` = %%s
GROUP BY `user_id`
''', (team_id, user_ids, start_time, role_id))
''' % table_name
cursor.execute(query, (team_id, user_ids, start_time, role_id))
if cursor.rowcount != 0:
# Grab user id with lowest last scheduled time
return min(cursor.fetchall(), key=operator.itemgetter('last_end'))['user_id']
else:
return None
def find_new_user_in_roster(self, roster_id, team_id, start_time, role_id, cursor):
def find_new_user_in_roster(self, roster_id, team_id, start_time, role_id, cursor, table_name='event'):
'''
Return roster users who haven't been scheduled for any event on this team's calendar for this schedule's role.
Ignores events from other teams.
'''
query = '''
SELECT DISTINCT `user`.`id` FROM `roster_user`
JOIN `user` ON `user`.`id` = `roster_user`.`user_id` AND `roster_user`.`roster_id` = %s
LEFT JOIN `event` ON `event`.`user_id` = `user`.`id` AND `event`.`team_id` = %s AND `event`.`end` <= %s
AND `event`.`role_id` = %s
WHERE `roster_user`.`in_rotation` = 1 AND `event`.`id` IS NULL
'''
JOIN `user` ON `user`.`id` = `roster_user`.`user_id` AND `roster_user`.`roster_id` = %%s
LEFT JOIN `%s` ON `%s`.`user_id` = `user`.`id` AND `%s`.`team_id` = %%s AND `%s`.`end` <= %%s
AND `%s`.`role_id` = %%s
WHERE `roster_user`.`in_rotation` = 1 AND `%s`.`id` IS NULL
''' % (table_name, table_name, table_name, table_name, table_name, table_name)
cursor.execute(query, (roster_id, team_id, start_time, role_id))
if cursor.rowcount != 0:
logger.debug('Found new guy')
return {row['id'] for row in cursor}
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True):
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True, table_name='event'):
if len(events) == 0:
return
# Skip creating this epoch of events if matching events exist
@@ -176,7 +145,10 @@ class Scheduler(object):
for ev in events:
query_params += [ev['start'], ev['end'], role_id, team_id]
cursor.execute('SELECT COUNT(*) AS num_events FROM event WHERE %s' % matching, query_params)
query = 'SELECT COUNT(*) AS num_events FROM %s WHERE %s' % (table_name, matching)
cursor.execute(query, query_params)
if cursor.fetchone()['num_events'] == len(events):
return
@@ -185,11 +157,11 @@ class Scheduler(object):
event_args = (team_id, schedule_id, event['start'], event['end'], user_id, role_id)
logger.debug('inserting event: %s', event_args)
query = '''
INSERT INTO `event` (
INSERT INTO `%s` (
`team_id`, `schedule_id`, `start`, `end`, `user_id`, `role_id`
) VALUES (
%s, %s, %s, %s, %s, %s
)'''
%%s, %%s, %%s, %%s, %%s, %%s
)''' % table_name
cursor.execute(query, event_args)
else:
link_id = gen_link_id()
@@ -197,11 +169,11 @@ class Scheduler(object):
event_args = (team_id, schedule_id, event['start'], event['end'], user_id, role_id, link_id)
logger.debug('inserting event: %s', event_args)
query = '''
INSERT INTO `event` (
INSERT INTO `%s` (
`team_id`, `schedule_id`, `start`, `end`, `user_id`, `role_id`, `link_id`
) VALUES (
%s, %s, %s, %s, %s, %s, %s
)'''
%%s, %%s, %%s, %%s, %%s, %%s, %%s
)''' % table_name
cursor.execute(query, event_args)
def set_last_epoch(self, schedule_id, last_epoch, cursor):
@@ -311,7 +283,7 @@ class Scheduler(object):
# Return future events and the last epoch events were scheduled for.
return future_events, self.utc_from_naive_date(next_epoch - timedelta(days=7 * period), schedule)
def find_next_user_id(self, schedule, future_events, cursor):
def find_next_user_id(self, schedule, future_events, cursor, table_name='event'):
team_id = schedule['team_id']
role_id = schedule['role_id']
roster_id = schedule['roster_id']
@@ -323,19 +295,19 @@ class Scheduler(object):
return None
logger.debug('filtering users: %s', user_ids)
start = min([e['start'] for e in future_events])
for uid in self.get_busy_user_by_event_range(user_ids, team_id, future_events, cursor):
for uid in self.get_busy_user_by_event_range(user_ids, team_id, future_events, cursor, table_name):
user_ids.remove(uid)
if not user_ids:
logger.info('All users have conflicting events, skipping...')
return None
new_user_ids = self.find_new_user_in_roster(roster_id, team_id, start, role_id, cursor)
new_user_ids = self.find_new_user_in_roster(roster_id, team_id, start, role_id, cursor, table_name)
available_and_new = new_user_ids & user_ids
if available_and_new:
logger.info('Picking new and available user from %s', available_and_new)
return available_and_new.pop()
logger.debug('picking user between: %s, team: %s', user_ids, team_id)
return self.find_least_active_user_id_by_team(user_ids, team_id, start, role_id, cursor)
return self.find_least_active_user_id_by_team(user_ids, team_id, start, role_id, cursor, table_name)
def schedule(self, team, schedules, dbinfo):
connection, cursor = dbinfo
@@ -364,77 +336,20 @@ class Scheduler(object):
self.create_events(team['id'], schedule['id'], user_id, epoch, schedule['role_id'], cursor)
connection.commit()
def preview(self, schedule, start_time, dbinfo, req, resp):
connection, cursor = dbinfo
delete_list = []
response_list = []
start_dt = datetime.fromtimestamp(start_time, utc)
start_epoch = self.epoch_from_datetime(start_dt)
# Get schedule info
role_id = schedule['role_id']
team_id = schedule['team_id']
first_event_start = min(ev['start'] for ev in schedule['events'])
period = self.get_period_len(schedule)
handoff = start_epoch + timedelta(seconds=first_event_start)
handoff = timezone(schedule['timezone']).localize(handoff)
# Start scheduling from the next occurrence of the hand-off time.
if start_dt > handoff:
start_epoch += timedelta(weeks=period)
handoff += timedelta(weeks=period)
if handoff < utc.localize(datetime.utcnow()):
raise HTTPBadRequest('Invalid populate request', 'cannot populate starting in the past')
future_events, last_epoch = self.calculate_future_events(schedule, cursor, start_epoch)
self.set_last_epoch(schedule['id'], last_epoch, cursor)
# Delete existing events from the start of the first event
future_events = [filter(lambda x: x['start'] >= start_time, evs) for evs in future_events]
future_events = filter(lambda x: x != [], future_events)
if future_events:
first_event_start = min(future_events[0], key=lambda x: x['start'])['start']
# store the events that will be deleted so they can get aded back in later
cursor.execute('SELECT * FROM event WHERE schedule_id = %s AND start >= %s', (schedule['id'], first_event_start))
delete_list = cursor.fetchall()
cursor.execute('DELETE FROM event WHERE schedule_id = %s AND start >= %s', (schedule['id'], first_event_start))
# Create events in the db, associating a user to them
for epoch in future_events:
user_id = self.find_next_user_id(schedule, epoch, cursor)
if not user_id:
continue
self.create_events(team_id, schedule['id'], user_id, epoch, role_id, cursor)
def build_preview_response(self, cursor, start__lt, end__ge, team__eq, table_name='temp_event'):
# get existing events
cols = all_columns
query = '''SELECT %s FROM `event`
JOIN `user` ON `user`.`id` = `event`.`user_id`
JOIN `team` ON `team`.`id` = `event`.`team_id`
JOIN `role` ON `role`.`id` = `event`.`role_id`''' % cols
where_params = []
where_vals = []
# Build where clause. If including subscriptions, deal with team parameters later
params = {'start__lt': req.get_param('start__lt', required=True), 'end__ge': req.get_param('end__ge', required=True)}
for key in params:
val = req.get_param(key)
where_params.append(constraints[key])
where_vals.append(val)
query = '''SELECT %s FROM `%s`
JOIN `user` ON `user`.`id` = `%s`.`user_id`
JOIN `team` ON `team`.`id` = `%s`.`team_id`
JOIN `role` ON `role`.`id` = `%s`.`role_id`''' % (cols, table_name, table_name, table_name, table_name)
where_params = [constraints['start__lt'], constraints['end__ge']]
where_vals = [start__lt, end__ge]
# Deal with team subscriptions and team parameters
team_where = []
subs_vals = []
team_params = {'team__eq': req.get_param('team__eq', required=True)}
for key in team_params:
val = req.get_param(key)
team_where.append(constraints[key])
subs_vals.append(val)
team_where = [constraints['team__eq']]
subs_vals = [team__eq]
subs_and = ' AND '.join(team_where)
cursor.execute('''SELECT `subscription_id`, `role_id` FROM `team_subscription`
JOIN `team` ON `team_id` = `team`.`id`
@@ -451,29 +366,9 @@ class Scheduler(object):
query = '%s WHERE %s' % (query, where_query)
cursor.execute(query, where_vals)
data = cursor.fetchall()
response_list = data
return json_dumps(data)
# delete new inserted events
if future_events:
first_event_start = min(future_events[0], key=lambda x: x['start'])['start']
cursor.execute('DELETE FROM event WHERE schedule_id = %s AND start >= %s', (schedule['id'], first_event_start))
# re insert deleted events
for event in delete_list:
event_args = (event['user_id'], event['schedule_id'], event['link_id'], event['note'], event['start'],
event['team_id'], event['end'], event['role_id'], event['id'])
logger.debug('inserting event: %s', event_args)
query = '''
INSERT INTO `event` (
`user_id`, `schedule_id`, `link_id`, `note`, `start`, `team_id`, `end`,`role_id`, `id`
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s
)'''
cursor.execute(query, event_args)
resp.body = json_dumps(response_list)
def populate(self, schedule, start_time, dbinfo):
def populate(self, schedule, start_time, dbinfo, table_name='event'):
connection, cursor = dbinfo
start_dt = datetime.fromtimestamp(start_time, utc)
start_epoch = self.epoch_from_datetime(start_dt)
@@ -501,12 +396,13 @@ class Scheduler(object):
future_events = filter(lambda x: x != [], future_events)
if future_events:
first_event_start = min(future_events[0], key=lambda x: x['start'])['start']
cursor.execute('DELETE FROM event WHERE schedule_id = %s AND start >= %s', (schedule['id'], first_event_start))
query = 'DELETE FROM %s WHERE schedule_id = %%s AND start >= %%s' % table_name
cursor.execute(query, (schedule['id'], first_event_start))
# Create events in the db, associating a user to them
for epoch in future_events:
user_id = self.find_next_user_id(schedule, epoch, cursor)
user_id = self.find_next_user_id(schedule, epoch, cursor, table_name)
if not user_id:
continue
self.create_events(team_id, schedule['id'], user_id, epoch, role_id, cursor)
self.create_events(team_id, schedule['id'], user_id, epoch, role_id, cursor, table_name=table_name)
connection.commit()

View File

@@ -2,5 +2,5 @@ import default
class Scheduler(default.Scheduler):
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True):
super(Scheduler, self).create_events(team_id, schedule_id, user_id, events, role_id, cursor, skip_match=False)
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True, table_name='event'):
super(Scheduler, self).create_events(team_id, schedule_id, user_id, events, role_id, cursor, skip_match=False, table_name='event')

View File

@@ -7,22 +7,23 @@ logger = logging.getLogger()
class Scheduler(default.Scheduler):
def guess_last_scheduled_user(self, schedule, start, roster, cursor):
cursor.execute('''
def guess_last_scheduled_user(self, schedule, start, roster, cursor, table_name='event'):
query = '''
SELECT `last_start`, `user_id` FROM
(SELECT `user_id`, MAX(`start`) AS `last_start` FROM `event`
WHERE `team_id` = %s AND `user_id` IN %s AND `start` <= %s
AND `role_id` = %s
(SELECT `user_id`, MAX(`start`) AS `last_start` FROM `%s`
WHERE `team_id` = %%s AND `user_id` IN %%s AND `start` <= %%s
AND `role_id` = %%s
GROUP BY `user_id`
ORDER BY `last_start` DESC) t
LIMIT 1
''', (schedule['team_id'], roster, start, schedule['role_id']))
''' % table_name
cursor.execute(query, (schedule['team_id'], roster, start, schedule['role_id']))
if cursor.rowcount != 0:
return cursor.fetchone()['user_id']
else:
return None
def find_next_user_id(self, schedule, future_events, cursor):
def find_next_user_id(self, schedule, future_events, cursor, table_name='event'):
cursor.execute('''SELECT `user_id` FROM `roster_user`
WHERE `roster_id` = %s AND in_rotation = TRUE''',
schedule['roster_id'])
@@ -42,24 +43,24 @@ class Scheduler(default.Scheduler):
# If this user is no longer in the roster or last_scheduled_user is NULL, try to find
# the last scheduled user using the calendar
start = min(e['start'] for e in future_events)
last_user = self.guess_last_scheduled_user(schedule, start, roster, cursor)
last_user = self.guess_last_scheduled_user(schedule, start, roster, cursor, table_name)
if last_user is None:
# If this doesn't work, return the first user in the roster
return roster[0]
last_idx = roster.index(last_user)
return roster[(last_idx + 1) % len(roster)]
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True):
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, skip_match=True, table_name='event'):
if len(events) == 1:
[event] = events
event_args = (team_id, schedule_id, event['start'], event['end'], user_id, role_id)
logger.debug('inserting event: %s', event_args)
query = '''
INSERT INTO `event` (
INSERT INTO `%s` (
`team_id`, `schedule_id`, `start`, `end`, `user_id`, `role_id`
) VALUES (
%s, %s, %s, %s, %s, %s
)'''
%%s, %%s, %%s, %%s, %%s, %%s
)''' % table_name
cursor.execute(query, event_args)
else:
link_id = gen_link_id()
@@ -67,22 +68,16 @@ class Scheduler(default.Scheduler):
event_args = (team_id, schedule_id, event['start'], event['end'], user_id, role_id, link_id)
logger.debug('inserting event: %s', event_args)
query = '''
INSERT INTO `event` (
INSERT INTO `%s` (
`team_id`, `schedule_id`, `start`, `end`, `user_id`, `role_id`, `link_id`
) VALUES (
%s, %s, %s, %s, %s, %s, %s
)'''
%%s, %%s, %%s, %%s, %%s, %%s, %%s
)''' % table_name
cursor.execute(query, event_args)
cursor.execute('UPDATE `schedule` SET `last_scheduled_user_id` = %s WHERE `id` = %s', (user_id, schedule_id))
def populate(self, schedule, start_time, dbinfo):
def populate(self, schedule, start_time, dbinfo, table_name='event'):
_, cursor = dbinfo
# Null last_scheduled_user to force find_next_user to determine that from the calendar
cursor.execute('UPDATE `schedule` SET `last_scheduled_user_id` = NULL WHERE `id` = %s', schedule['id'])
super(Scheduler, self).populate(schedule, start_time, dbinfo)
def preview(self, schedule, start_time, dbinfo, req, resp):
_, cursor = dbinfo
# Null last_scheduled_user to force find_next_user to determine that from the calendar
cursor.execute('UPDATE `schedule` SET `last_scheduled_user_id` = NULL WHERE `id` = %s', schedule['id'])
super(Scheduler, self).preview(schedule, start_time, dbinfo, req, resp)
super(Scheduler, self).populate(schedule, start_time, dbinfo, table_name='event')

View File

@@ -242,7 +242,7 @@ def test_find_least_active_available_user(mocker):
mock_busy_user_by_range = mocker.patch('oncall.scheduler.default.Scheduler.get_busy_user_by_event_range')
mock_active_user_by_team = mocker.patch('oncall.scheduler.default.Scheduler.find_least_active_user_id_by_team')
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor):
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor, table_name='event'):
assert user_ids == set(mock_user_ids)
return [123]
@@ -251,9 +251,9 @@ def test_find_least_active_available_user(mocker):
{'start': 570, 'end': 588},
{'start': 600, 'end': 700}]
scheduler = oncall.scheduler.default.Scheduler()
scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None)
scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None, 'event')
mock_active_user_by_team.assert_called_with({456, 789}, 1, 440, 2, None)
mock_active_user_by_team.assert_called_with({456, 789}, 1, 440, 2, None, 'event')
def test_find_least_active_available_user_conflicts(mocker):
@@ -263,13 +263,13 @@ def test_find_least_active_available_user_conflicts(mocker):
mock_busy_user_by_range = mocker.patch('oncall.scheduler.default.Scheduler.get_busy_user_by_event_range')
mock_active_user_by_team = mocker.patch('oncall.scheduler.default.Scheduler.find_least_active_user_id_by_team')
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor):
def mock_busy_user_by_range_side_effect(user_ids, team_id, events, cursor, table_name='event'):
assert user_ids == set(mock_user_ids)
return [123, 456, 789]
mock_busy_user_by_range.side_effect = mock_busy_user_by_range_side_effect
future_events = [{'start': 440, 'end': 570}]
scheduler = oncall.scheduler.default.Scheduler()
assert scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None) is None
assert scheduler.find_next_user_id(MOCK_SCHEDULE, future_events, None, table_name='event') is None
mock_active_user_by_team.assert_not_called()