From 1ac53aabe0dfec69c347eaa6b14bd67f6129d39d Mon Sep 17 00:00:00 2001 From: Naomi Saad Date: Thu, 2 Jul 2026 11:20:44 +0100 Subject: [PATCH 1/4] Remove disabled taps from smart scheduling and remove smart_interval_mask on when a schedule is redistributed --- CHANGELOG.md | 5 +++++ cicada/commands/smart_schedule.py | 5 ++++- cicada/commands/spread_schedules.py | 1 + setup.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7824096..ae97e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.10.2 +----- +- Prevent disabled taps from being included in smart scheduling calculations +- Reset the smart_interval_mask when running spread_schedules + 0.10.1 ----- - Fix bug in delete_schedule introduced in 0.10.0 diff --git a/cicada/commands/smart_schedule.py b/cicada/commands/smart_schedule.py index dfbe54f..72bc87a 100644 --- a/cicada/commands/smart_schedule.py +++ b/cicada/commands/smart_schedule.py @@ -29,7 +29,10 @@ def _create_schedule_objects(schedule_ids, db_cur): # Fetch details for each schedule and convert to Schedule objects for schedule_id in schedule_ids: details = scheduler.get_schedule_details(db_cur, schedule_id) - if schedule_id in blocklisted_schedules: + if details['is_enabled'] == 0: + print(f"Skipping disabled schedule {schedule_id}") + continue + if schedule_id in blocklisted_schedules : details['blocklisted'] = True else: details['blocklisted'] = False diff --git a/cicada/commands/spread_schedules.py b/cicada/commands/spread_schedules.py index 23aca17..ddffdfc 100644 --- a/cicada/commands/spread_schedules.py +++ b/cicada/commands/spread_schedules.py @@ -96,6 +96,7 @@ def main(spread_details, dbname=None): current_schedule_details = scheduler.get_schedule_details(db_cur, schedule_id) new_schedule_details = current_schedule_details.copy() new_schedule_details["server_id"] = valid_target_servers[next_enabled_server] + new_schedule_details["smart_interval_mask"] = None # Reset smart_interval_mask to None when spreading schedules next_enabled_server += 1 if next_enabled_server == valid_server_count: diff --git a/setup.py b/setup.py index 81114a8..4ef6326 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="cicada", - version="0.10.1", + version="0.10.2", description="Lightweight, agent-based, distributed scheduler", long_description=long_description, long_description_content_type="text/markdown", From dc5a3d5cd1c77d409f36c4220d8eec7503f6ddc2 Mon Sep 17 00:00:00 2001 From: Naomi Saad Date: Thu, 2 Jul 2026 11:59:42 +0100 Subject: [PATCH 2/4] Add test for disabled schedules being excluded in smart_schedules --- tests/test_smart_scheduling.py | 67 +++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/tests/test_smart_scheduling.py b/tests/test_smart_scheduling.py index d365c0a..5f2f51a 100644 --- a/tests/test_smart_scheduling.py +++ b/tests/test_smart_scheduling.py @@ -883,6 +883,39 @@ def test_multiple_overlapping_schedules_evaluation(self, db_setup): db_cur.close() db_conn.close() + def test_create_schedule_objects_skips_disabled_schedules(self, db_setup): + """Test that _create_schedule_objects skips disabled schedules""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create server and schedules + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', '192.168.1.1')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command, is_enabled) + VALUES ('sched-1', 1, '0 * * * *', 'echo test1', 1), + ('sched-2', 1, '*/30 * * * *', 'echo test2', 0), + ('sched-3', 1, '*/15 * * * *', 'echo test3', 1), + ('sched-4', 1, '0 0 * * *', 'echo test4', 0)""" + ) + + # Get schedule IDs and create Schedule objects + schedule_ids = ['sched-1', 'sched-2', 'sched-3', 'sched-4'] + schedules = smart_schedule._create_schedule_objects(schedule_ids, db_cur) + + # Verify that disabled schedules were skipped + assert len(schedules) == 2, f"Expected 2 enabled schedules, got {len(schedules)}" + + enabled_schedule_ids = [s.schedule_id for s in schedules] + assert 'sched-1' in enabled_schedule_ids, "Enabled schedule 'sched-1' should be included" + assert 'sched-3' in enabled_schedule_ids, "Enabled schedule 'sched-3' should be included" + assert 'sched-2' not in enabled_schedule_ids, "Disabled schedule 'sched-2' should be skipped" + assert 'sched-4' not in enabled_schedule_ids, "Disabled schedule 'sched-4' should be skipped" + finally: + db_cur.close() + db_conn.close() + class TestSmartSchedulingCommand: """Tests for the smart scheduling command""" @@ -1280,12 +1313,12 @@ def test_optimise_with_custom_db_connection_single_server(self, db_setup): VALUES (1, 'test-server', 'test-server.local', '192.168.1.1')""" ) query_test_db( - """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command) - VALUES ('sched-1', 1, '*/10 * * * *', 'echo test1'), - ('sched-2', 1, '0 * * * *', 'echo test2'), - ('sched-3', 1, '0 * * * *', 'echo test3'), - ('sched-4', 1, '0 * * * *', 'echo test4'), - ('sched-5', 1, '0 * * * *', 'echo test5')""" + """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command, is_enabled) + VALUES ('sched-1', 1, '*/10 * * * *', 'echo test1', 1), + ('sched-2', 1, '0 * * * *', 'echo test2', 1), + ('sched-3', 1, '0 * * * *', 'echo test3', 1), + ('sched-4', 1, '0 * * * *', 'echo test4', 1), + ('sched-5', 1, '0 * * * *', 'echo test5', 1)""" ) # Call optimise with custom db_cur @@ -1323,17 +1356,17 @@ def test_optimise_with_custom_db_connection_multiple_servers(self, db_setup): (2, 'server-2', 'server-2.local', '192.168.1.2')""" ) query_test_db( - """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command) - VALUES ('sched-1a', 1, '*/10 * * * *', 'echo test1'), - ('sched-2a', 1, '0 * * * *', 'echo test2'), - ('sched-3a', 1, '0 * * * *', 'echo test3'), - ('sched-4a', 1, '0 * * * *', 'echo test4'), - ('sched-5a', 1, '0 * * * *', 'echo test5'), - ('sched-1b', 2, '*/10 * * * *', 'echo test1'), - ('sched-2b', 2, '0 * * * *', 'echo test2'), - ('sched-3b', 2, '0 * * * *', 'echo test3'), - ('sched-4b', 2, '0 * * * *', 'echo test4'), - ('sched-5b', 2, '0 * * * *', 'echo test5') + """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command, is_enabled) + VALUES ('sched-1a', 1, '*/10 * * * *', 'echo test1', 1), + ('sched-2a', 1, '0 * * * *', 'echo test2', 1), + ('sched-3a', 1, '0 * * * *', 'echo test3', 1), + ('sched-4a', 1, '0 * * * *', 'echo test4', 1), + ('sched-5a', 1, '0 * * * *', 'echo test5', 1), + ('sched-1b', 2, '*/10 * * * *', 'echo test1', 1), + ('sched-2b', 2, '0 * * * *', 'echo test2', 1), + ('sched-3b', 2, '0 * * * *', 'echo test3', 1), + ('sched-4b', 2, '0 * * * *', 'echo test4', 1), + ('sched-5b', 2, '0 * * * *', 'echo test5', 1) """ ) From cbeb00e6a639b14278e2526e798998829439a726 Mon Sep 17 00:00:00 2001 From: Naomi Saad Date: Thu, 2 Jul 2026 15:16:43 +0100 Subject: [PATCH 3/4] Slightly change the evaluation strategy (+change tests accordingly) --- CHANGELOG.md | 1 + cicada/lib/smart_scheduling/evaluation.py | 3 ++- tests/test_smart_scheduling.py | 17 ++++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae97e0a..24a0613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ----- - Prevent disabled taps from being included in smart scheduling calculations - Reset the smart_interval_mask when running spread_schedules +- Modify evaluation criteria to disincentivise overlaps for the first minute of the run 0.10.1 ----- diff --git a/cicada/lib/smart_scheduling/evaluation.py b/cicada/lib/smart_scheduling/evaluation.py index 88e553e..d373654 100644 --- a/cicada/lib/smart_scheduling/evaluation.py +++ b/cicada/lib/smart_scheduling/evaluation.py @@ -35,7 +35,8 @@ def evaluate_usage_and_peak(start_times: Sequence[int], schedules: Sequence[Sche # schedule runs in, we add the usage at the starting minute and subtract it at the end minute. while minute < mins_per_day: end = min(minute + run_time, mins_per_day) - diff[minute] += 1 + diff[minute] += 2 + diff[minute+1] -= 1 diff[end] -= 1 minute += freq diff --git a/tests/test_smart_scheduling.py b/tests/test_smart_scheduling.py index 5f2f51a..6c83245 100644 --- a/tests/test_smart_scheduling.py +++ b/tests/test_smart_scheduling.py @@ -167,10 +167,11 @@ def test_evaluate_single_schedule_no_overlap(self, db_setup): usage, peak = evaluate_usage_and_peak(start_blocks, [test_schedule]) assert usage.shape == (1440,) - assert peak == 1 + assert peak == 2 for i in range(24): mins = i * 60 - assert (usage[mins : mins + 5] == 1).all() + assert (usage[mins] == 2) + assert (usage[mins + 1 : mins + 5] == 1).all() assert (usage[mins + 5 : (i + 1) * 60] == 0).all() finally: db_cur.close() @@ -219,11 +220,13 @@ def test_evaluate_multiple_schedules_no_overlap(self, db_setup): start_blocks = [0, 30] usage, peak = evaluate_usage_and_peak(start_blocks, [schedule1, schedule2]) - assert (usage[0:5] == 1).all() + assert (usage[0] == 2) + assert (usage[1:5] == 1).all() assert (usage[6:30] == 0.0).all() - assert (usage[30:35] == 1).all() + assert (usage[30] == 2) + assert (usage[31:35] == 1).all() assert (usage[35:60] == 0.0).all() - assert peak == 1 + assert peak == 2 finally: db_cur.close() db_conn.close() @@ -271,8 +274,8 @@ def test_evaluate_overlapping_schedules(self, db_setup): start_blocks = [0, 0] usage, peak = evaluate_usage_and_peak(start_blocks, [schedule1, schedule2]) - assert peak == 2 - assert usage[0] == 2 + assert peak == 4 + assert usage[0] == 4 assert usage[5] == 1 finally: db_cur.close() From 2195fcce5d55f4f09772a545016c018e59993a94 Mon Sep 17 00:00:00 2001 From: Naomi Saad Date: Thu, 2 Jul 2026 15:48:02 +0100 Subject: [PATCH 4/4] Fix small nitpick --- cicada/commands/smart_schedule.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cicada/commands/smart_schedule.py b/cicada/commands/smart_schedule.py index 72bc87a..bf23699 100644 --- a/cicada/commands/smart_schedule.py +++ b/cicada/commands/smart_schedule.py @@ -32,10 +32,7 @@ def _create_schedule_objects(schedule_ids, db_cur): if details['is_enabled'] == 0: print(f"Skipping disabled schedule {schedule_id}") continue - if schedule_id in blocklisted_schedules : - details['blocklisted'] = True - else: - details['blocklisted'] = False + details['blocklisted'] = schedule_id in blocklisted_schedules try: schedule = Schedule( @@ -43,7 +40,7 @@ def _create_schedule_objects(schedule_ids, db_cur): server_id = details['server_id'], interval_mask = details['interval_mask'], smart_interval_mask = details.get('smart_interval_mask'), - blocklisted = details.get('blocklisted', False), + blocklisted = details['blocklisted'], db_cur = db_cur ) # Ignore the few schedules that have irregular cron expressions for now.