diff --git a/CHANGELOG.md b/CHANGELOG.md index cebf20a..04c8b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.10.0 +----- +- Add smart_schedule command with optimise and rollback options (as well as blocklist functionality) +- Adds new column to existing table and new tables connected to smart_schedule command + 0.9.0 ----- - Verify compatibility with Ubuntu 22.04 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6f26684 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,182 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Cicada** is a centralized, distributed job scheduler for Pipelinewise schedules. It acts as a lightweight management layer between Linux CRON and executables, allowing jobs to be scheduled across multiple nodes via a central database rather than local cron. + +Key architectural concepts: +- **Nodes/Servers**: Machines that register with Cicada and pull scheduling information from the central database. They execute `cicada exec_server_schedules` via cron. +- **Schedules**: Jobs defined in the database with cron expressions, parameters, and target servers. +- **SmartScheduling**: A Genetic Algorithm (GA) optimization module that shifts job start times to distribute load across a 24-hour period, avoiding resource conflicts. + +## Development Setup + +### Install and Build +```bash +make dev # Create venv with dev dependencies (black, flake8, pytest) +make # Create venv with only production dependencies +``` + +The project uses a standard Python venv setup. The `Makefile` is the single source of truth for build commands. + +### Run Tests +```bash +make pytest # Run all tests with coverage (must be ≥80%) +``` + +To run a single test file or specific test: +```bash +. venv/bin/activate +pytest tests/test_lib_scheduler.py -v +pytest tests/test_lib_scheduler.py::test_function_name -v +``` + +### Code Quality +```bash +make flake8 # Lint (checks E9, F63, F7, F82 only, max line length 120) +make black # Format check (line length 120) +``` + +Black is used for code style; run it with `black --line-length 120 cicada/ tests/ --diff` to preview changes before committing. + +## Codebase Structure + +### Core Modules + +**`cicada/lib/scheduler.py`** +- Central scheduling logic: retrieving schedules, managing execution state, cron parsing +- Functions like `get_schedule_details()`, `get_all_schedule_ids_per_server()`, `get_server_id()` +- Uses `croniter` for cron expression parsing +- Contains SQL queries for the main `schedules` and `servers` tables + +**`cicada/lib/postgres.py`** +- Database connection management and helpers +- Connection pooling and statement execution + +**`cicada/lib/utils.py`** +- Utility functions and decorators for exception handling and logging + +**`cicada/cli.py`** +- Command dispatcher using argparse +- Routes subcommands to handlers in `cicada/commands/` + +### Commands +Commands are located in `cicada/commands/` and implement specific operations: +- `exec_server_schedules.py` – Main loop executed by cron on each node; fetches and runs scheduled jobs +- `upsert_schedule.py`, `show_schedule.py`, `delete_schedule.py` – CRUD operations on schedules +- `smart_schedule.py` – Invokes GA optimization (see SmartScheduling below) +- `spread_schedules.py` – Distributes schedules across servers +- `rollback.py` – Reverts SmartScheduling changes using checkpoint history +- `register_server.py`, `archive_schedule_log.py`, `ping_slack.py` – Administrative operations + +### SmartScheduling Module +Located in `cicada/lib/SmartScheduling/` + +**`domain.py`** +- `Schedule` dataclass: represents a schedule as a "schedule" (job) with properties: + - `schedule_id`, `server_id`, `interval_mask` (cron expression) + - `frequency_minutes`, `median_runtime_minutes` + - `shift`: offset in minutes applied to shift job start time + - `blocklisted`: flag to exclude from GA optimization + +**`config.py`** +- `GAConfig` dataclass: hyperparameters for the genetic algorithm + - `num_generations`, `sol_per_pop`, `mutation_percent_genes`, etc. + +**`pygad.py`** +- Wraps the external `pygad` library (genetic algorithm) +- Fitness function: evaluates how well a shift assignment distributes load +- Implements crossover and mutation operations on shifts + +**`evaluation.py`** +- Scoring logic: calculates resource contention, overlap penalties, and fitness metrics + +### Database Schema +Key tables: +- `servers` – Registered nodes with hostname, FQDN, IP address +- `schedules` – Job definitions with cron expressions, parameters, execution state +- `schedule_logs` – Historical execution records with runtime, status, output +- `snapshots` – Metadata about optimization/rollback events (reason, timestamp, server_id) +- `schedule_backups` – Schedule state snapshots: stores `interval_mask` and `smart_interval_mask` at each snapshot for potential rollback +- `schedule_changes` – Linked-list audit trail of all changes to schedules (replaces older snapshot model); each entry has `previous_change_id` for chain traversal, `changes_delta` (JSON) for what changed + +Database setup SQL is in `setup/db_and_user.sql` and `setup/schema.sql`. Migration script: `setup/migrate_snapshots_to_changes.sql`. Example schedule setup for smart scheduling in `setup/create_test_tap_setup`. + +## Key Architectural Patterns + +### Cron Expression Handling +- All scheduling uses standard cron format (5 fields: minute hour dom month dow) +- `croniter` library parses expressions and calculates next/previous execution times + +### Command Execution +- Jobs are executed as shell commands by `exec_server_schedules` +- Commands can include parameters via template substitution +- Outputs and exit codes are logged to `schedule_logs` table + +### Configuration +- Database connection details from `config/definitions.yml` (user must create from `config/example.yml`) +- Each command may accept CLI flags (e.g., `--schedule_id`, `--adhoc_execute`) + +### SmartScheduling Workflow +1. **Load schedules**: Fetch all schedules for a server via `get_schedules_per_server()` +2. **Create Schedule objects**: Convert schedule details to Schedule instances; filter unsupported schedules (irregular cron, too frequent, blocklisted) +3. **Run GA optimization**: PyGAD evolves shifts over N generations to minimize resource conflicts +4. **Apply and checkpoint**: Save optimized shifts back to DB; record change entry via `record_schedule_change()` for audit trail and rollback + +### Rollback System +Cicada supports two rollback mechanisms: + +**Full Rollback** (`--full` flag): +- Sets `smart_interval_mask = NULL` for affected schedules, reverting to original `interval_mask` +- Works per-schedule or per-server +- Records a `ROLLBACK_FULL` change entry in `schedule_changes` + +**Rollback to Specific Change** (`--change-id` flag): +- Uses linked-list traversal via `compute_schedule_state_at_change()` to reconstruct schedule state at any historical change +- Requires `schedule_id` and `change_id` +- Records a `ROLLBACK_TO_CHANGE` entry documenting what was restored +- Marks the target change as reverted + +**Change History** (`--history` flag): +- Displays complete audit trail for a schedule via `get_schedule_history()` +- Each entry shows reason, timestamp, and delta (what changed) + +**Migration Note**: Old snapshot/schedule_backups model supported only last 3 snapshots. New `schedule_changes` model retains unlimited history via linked-list structure. + +## Testing + +Tests are in `tests/` and use `pytest` with fixtures: +- `test_functional_main.py` – Integration tests for the main execution loop +- `test_functional_cli_entrypoint.py` – CLI command tests +- `test_functional_spread_schedules.py` – SmartScheduling and load distribution tests +- `test_lib_scheduler.py` – Unit tests for scheduler utility functions +- `test_lib_postgres.py` – Database connection tests + +Mock fixtures often include a test PostgreSQL database or in-memory alternatives. Freezegun is used for time-based testing. + +## Common Development Tasks + +### Adding a New CLI Command +- Create a new file in `cicada/commands/` with a `main()` function +- Import and add an entry point in `cicada/cli.py` +- Add tests in `tests/test_functional_cli_entrypoint.py` + +### Modifying Schedule Logic +- Edit `cicada/lib/scheduler.py` for core logic changes (e.g., new state transitions) +- Update `cicada/lib/SmartScheduling/domain.py` if Schedule validation rules change +- Update tests in `test_lib_scheduler.py` to cover new behavior + +### Database Schema Changes +- Modify SQL in `setup/schema.sql` (note: existing deployments require migration scripts) +- Update query strings in `scheduler.py` and corresponding test fixtures + +## Important Notes + +- **PostgreSQL only**: Only PostgreSQL is supported (versions 12.9–15.14 verified) +- **No external APIs**: Uses only core Python and database; runs offline +- **Cron safety**: Jobs execute only when registered server node is running; they respect cron expressions and database state +- **Rollback support**: SmartScheduling changes can be rolled back via checkpoints stored in the database +- **Line length**: Maximum 120 characters (enforced by Black and Flake8) +- **Code coverage**: Must maintain ≥80% test coverage for commits diff --git a/cicada/cli.py b/cicada/cli.py index 93b5c6b..1c10656 100644 --- a/cicada/cli.py +++ b/cicada/cli.py @@ -8,6 +8,7 @@ from cicada.lib import utils from cicada.commands import register_server +from cicada.commands import smart_schedule_rollback from cicada.commands import list_server_schedules from cicada.commands import exec_server_schedules from cicada.commands import upsert_schedule @@ -18,6 +19,8 @@ from cicada.commands import ping_slack from cicada.commands import list_schedules from cicada.commands import delete_schedule +from cicada.commands import smart_schedule +from cicada.commands import blocklist_schedule as blocklist_schedule_cmd @utils.named_exception_handler("Cicada") @@ -29,6 +32,7 @@ def __init__(self): "register_server", "list_server_schedules", "exec_server_schedules", + "smart_schedule", "show_schedule", "upsert_schedule", "exec_schedule", @@ -273,6 +277,133 @@ def delete_schedule(): args = parser.parse_args(sys.argv[2:]) delete_schedule.main(args.schedule_id) + @staticmethod + def smart_schedule(): + """Generate smart schedules for a server using genetic algorithm, or rollback/manage blocklist""" + parser = argparse.ArgumentParser( + allow_abbrev=False, + add_help=True, + prog=inspect.stack()[0][3], + description="Generate smart schedules for a server using genetic algorithm, or rollback previous changes, or manage schedule blocklist", + ) + + # Subcommands: optimise, rollback, blocklist + subparsers = parser.add_subparsers(dest="action", help="Action to perform. Options: optimise (default), rollback, or blocklist") + + # (Default) optimise subcommand + optimise_parser = subparsers.add_parser( + "optimise", + help="optimise schedules using genetic algorithm", + add_help=True, + ) + optimise_parser.add_argument("--server_id", type=int, required=False, help="ID of the server") + + # Optional GA Configurations + ga_config = optimise_parser.add_argument_group("ga_config", "Optional configurations for the genetic algorithm optimiser") + ga_config.add_argument("--num_generations",type=int,required=False, help="Number of generations for the genetic algorithm. Default: 20") + ga_config.add_argument("--sol_per_pop",type=int,required=False, help="Number of solutions per population for the genetic algorithm. Default: 40") + ga_config.add_argument("--num_parents_mating",type=int,required=False, help="Number of parents mating for the genetic algorithm. Default: 10") + ga_config.add_argument("--mutation_percent_genes",type=int,required=False, help="Mutation percentage of genes for the genetic algorithm. Default: 20") + ga_config.add_argument("--parent_selection_type",type=str,required=False, help="Parent selection type for the genetic algorithm. Allowed values: ['sss', 'rws', 'sus', 'tournament', 'rank', 'random']. Default: rank") + ga_config.add_argument("--crossover_type",type=str,required=False, help="Crossover type for the genetic algorithm. Allowed values: ['single_point', 'two_point', 'uniform']. Default: uniform") + ga_config.add_argument("--mutation_type",type=str,required=False, help="Mutation type for the genetic algorithm. Allowed values: ['random', 'swap', 'inversion', 'scramble']. Default: random") + ga_config.add_argument("--keep_elitism",type=int,required=False, help="Number of elite solutions to keep for the next generation. Default: 2") + ga_config.add_argument("--random_seed",type=int,required=False, help="Set a random seed to get repeatable results. Default: None") + + # Rollback subcommand + rollback_parser = subparsers.add_parser( + "rollback", + help="Rollback to original or previous cron schedules", + add_help=True, + prog=inspect.stack()[0][3], + description="Rollback for smart scheduling, it resets the schedule to its original cron in case of any issues", + ) + + # Mutually exclusive flags for rollback mode + rollback_mode = rollback_parser.add_mutually_exclusive_group(required=True) + rollback_mode.add_argument( + "--full", + default=False, + action="store_true", + help="Rollback to original schedule (set smart_interval_mask to NULL)", + ) + rollback_mode.add_argument( + "--previous", + default=False, + action="store_true", + help="Rollback to most recent snapshot (step back one optimization)", + ) + + # Add mutually exclusive arguments for rollback subcommand to specify either server_id or schedule_id for targeted rollback + group = rollback_parser.add_mutually_exclusive_group() + group.add_argument( + "--server_id", + type=int, + required=False, + help="ID of the server to rollback, if not specified will rollback all servers", + ) + group.add_argument("--schedule_id", type=str, required=False, help="ID of the schedule to rollback") + + + # Blocklist subcommand + blocklist_parser = subparsers.add_parser( + "blocklist", + help="Add or remove a schedule from the blocklist (excluded from smart scheduling optimization)", + add_help=True, + ) + blocklist_parser.add_argument( + "--schedule_id", + type=str, + required=True, + help="Id of the schedule to blocklist/unblocklist", + ) + blocklist_parser.add_argument( + "--remove", + default=False, + action="store_true", + help="Remove the schedule from the blocklist instead of adding it", + ) + blocklist_parser.add_argument( + "--reason", + type=str, + required=False, + help="Reason for blocklisting (optional, only used when adding)", + ) + + # Parse arguments and call smart_schedule.main with appropriate arguments based on subcommand + args = parser.parse_args(sys.argv[2:]) + + if args.action == "optimise" or args.action is None: + optimise_args = optimise_parser.parse_args(sys.argv[3:]) + smart_schedule.main( + server_id=optimise_args.server_id, + ga_config={ + "num_generations": optimise_args.num_generations, + "sol_per_pop": optimise_args.sol_per_pop, + "num_parents_mating": optimise_args.num_parents_mating, + "mutation_percent_genes": optimise_args.mutation_percent_genes, + "parent_selection_type": optimise_args.parent_selection_type, + "crossover_type": optimise_args.crossover_type, + "mutation_type": optimise_args.mutation_type, + "keep_elitism": optimise_args.keep_elitism, + "random_seed": optimise_args.random_seed, + }, + ) + elif args.action == "rollback": + rollback_args = rollback_parser.parse_args(sys.argv[3:]) + smart_schedule_rollback.main( + server_id=rollback_args.server_id, + schedule_id=rollback_args.schedule_id, + full=rollback_args.full, + previous=rollback_args.previous) + elif args.action == "blocklist": + blocklist_args = blocklist_parser.parse_args(sys.argv[3:]) + blocklist_schedule_cmd.main( + schedule_id=blocklist_args.schedule_id, + remove=blocklist_args.remove, + reason=blocklist_args.reason, + ) + @staticmethod def version(): """Return version of cicada package""" diff --git a/cicada/commands/blocklist_schedule.py b/cicada/commands/blocklist_schedule.py new file mode 100644 index 0000000..4f988dd --- /dev/null +++ b/cicada/commands/blocklist_schedule.py @@ -0,0 +1,54 @@ +"""Add or remove schedules from the blocklist (excluded from smart scheduling optimization).""" + +from typing import Optional +from cicada.lib import postgres, utils +from cicada.lib import scheduler + + +@utils.named_exception_handler("blocklist_schedule") +def main(schedule_id: str, remove: bool = False, reason: Optional[str] = None, dbname=None): + """ + Add or remove a schedule from the blocklist. + + Blocklisted schedules are excluded from smart scheduling optimizations. + + Args: + schedule_id: The schedule_id to blocklist or unblocklist. + remove: True to remove from blocklist, False to add to blocklist. + reason: Optional reason for blocklisting (used when remove=False). + dbname: Optional database name to connect to. + """ + + if not schedule_id or not isinstance(schedule_id, str): + raise TypeError("schedule_id must be a non-empty string") + + db_conn = postgres.db_cicada(dbname) + db_cur = db_conn.cursor() + db_cur.execute("BEGIN;") + + try: + if remove: + scheduler.remove_blocklist_schedule(db_cur, schedule_id=schedule_id) + print(f"Schedule {schedule_id} has been removed from the blocklist successfully.") + + else: + schedule_details = scheduler.get_schedule_details(db_cur, schedule_id) + if not schedule_details or not schedule_details.get('schedule_id'): + print(f"ERROR: Schedule {schedule_id} not found") + return + scheduler.blocklist_schedule(db_cur, schedule_id=schedule_id, reason=reason) + print(f"Schedule {schedule_id} has been blocklisted successfully.") + scheduler.full_rollback(db_cur, schedule_id=schedule_id) + print(f"Schedule {schedule_id} has been rolled back to original settings successfully.") + scheduler.reset_schedule_backups(db_cur, schedule_id=schedule_id) + print(f"Backups for schedule {schedule_id} have been removed successfully.") + db_cur.execute("COMMIT;") + + except Exception as e: + db_cur.execute("ROLLBACK;") + print("Database changes have been rolled back due to the error.") + raise Exception(f"Error during blocklist operation for schedule_id {schedule_id}: {e}") + + finally: + db_cur.close() + db_conn.close() diff --git a/cicada/commands/delete_schedule.py b/cicada/commands/delete_schedule.py index 734b11a..7de6e94 100644 --- a/cicada/commands/delete_schedule.py +++ b/cicada/commands/delete_schedule.py @@ -16,6 +16,7 @@ def main(schedule_id, dbname=None): db_conn = postgres.db_cicada(dbname) db_cur = db_conn.cursor() scheduler.delete_schedule(db_cur, str(schedule_id)) + scheduler.reset_schedule_backups(db_cur, str(schedule_id)) db_cur.close() db_conn.close() diff --git a/cicada/commands/smart_schedule.py b/cicada/commands/smart_schedule.py new file mode 100644 index 0000000..dfbe54f --- /dev/null +++ b/cicada/commands/smart_schedule.py @@ -0,0 +1,208 @@ +"""Shifts the schedules on a node to distribute the load""" + +from __future__ import annotations +import sys +from typing import List +from croniter import croniter +from cicada.lib import postgres, utils +from cicada.lib import scheduler +from cicada.lib.smart_scheduling.ga_pygad import GAPyGADScheduler +from cicada.lib.smart_scheduling.domain import Schedule + +def _get_schedules_per_server(server_id, db_cur=None): + """Get all schedules for a given server_id.""" + schedule_ids = [row[0] for row in scheduler.get_all_schedule_ids_per_server(db_cur, server_id)] + + if not schedule_ids: + raise ValueError(f"No schedules found for server_id {server_id}. Check if server_id exists and has schedules assigned.") + + return schedule_ids + + + +def _create_schedule_objects(schedule_ids, db_cur): + """Create Schedule objects from schedule_ids.""" + + schedules : list[Schedule] = [] + blocklisted_schedules = scheduler.get_blocklisted_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: + details['blocklisted'] = True + else: + details['blocklisted'] = False + + try: + schedule = Schedule( + schedule_id = details['schedule_id'], + server_id = details['server_id'], + interval_mask = details['interval_mask'], + smart_interval_mask = details.get('smart_interval_mask'), + blocklisted = details.get('blocklisted', False), + db_cur = db_cur + ) + # Ignore the few schedules that have irregular cron expressions for now. + # There are few enough that this shouldn't impact the optimisation and is not worth the effort to try and support these in the GA + if not schedule.is_regular_schedule(): + print(f"Skipping irregular schedule {schedule.schedule_id} with cron expression {schedule.interval_mask}") + else: + schedules.append(schedule) + except Exception as e: + print(f"Skipping schedule {schedule_id} due to error: {e}") + + return schedules + +def _update_schedule_cron(schedule : Schedule): + """ + Uses the start_time to shift the cron expression accordingly. Gene space is already limited from 0 to the frequency of the schedule + + Ex. form of cron expression: "8-59/15 * * * *" (every 15 minutes starting at minute 8 of each hour) + + Args: + schedule (Schedule): Schedule object with updated shift attribute based on GA solution + Returns: + Updated Schedule object with new interval_mask based on the shift calculated by the GA optimizer + """ + + frequency = schedule.frequency_minutes + start_time_mins = schedule.start_time_mins + + if schedule.shifted == False or start_time_mins is None: + return # No shift needed + + if frequency == 1440: # For daily schedules, we can shift within the hour + hour = start_time_mins // 60 + minute = (start_time_mins - hour * 60) % 60 + schedule.smart_interval_mask = f"{minute} {hour} * * *" + # Check that the new cron expression is valid + if not croniter.is_valid(schedule.smart_interval_mask): + raise ValueError(f"Invalid cron expression generated: {schedule.smart_interval_mask} for schedule {schedule.schedule_id}") + return + elif frequency == 60: # For hourly schedules, we can shift within the hour + if start_time_mins >= frequency: + raise ValueError(f"Shift {start_time_mins} cannot be greater than or equal to frequency {frequency} for schedule {schedule.schedule_id}") + schedule.smart_interval_mask = f"{start_time_mins} * * * *" + # Check that the new cron expression is valid + if not croniter.is_valid(schedule.smart_interval_mask): + raise ValueError(f"Invalid cron expression generated: {schedule.smart_interval_mask} for schedule {schedule.schedule_id}") + return + elif frequency < 60: + if start_time_mins >= frequency: + raise ValueError(f"Shift {start_time_mins} cannot be greater than or equal to frequency {frequency} for schedule {schedule.schedule_id}") + schedule.smart_interval_mask = f"{start_time_mins}-59/{frequency} * * * *" + # Check that the new cron expression is valid + if not croniter.is_valid(schedule.smart_interval_mask): + raise ValueError(f"Invalid cron expression generated: {schedule.smart_interval_mask} for schedule {schedule.schedule_id}") + return + + + +def _assign_new_schedules(optimised_schedules: List[Schedule], db_cur): + """Assign new schedules based on the optimal schedule found.""" + + schedule_details_list = [] + schedule_ids = [] + + # For each schedule, update the schedule in the DB with the new interval_mask based on the start_time_mins calculated by the GA optimizer + for schedule in optimised_schedules: + _update_schedule_cron(schedule) + if schedule.shifted: + print(f"- {schedule.schedule_id} : {schedule.smart_interval_mask}") + schedule._determine_start_time_mins() + + schedule_details = { + "adhoc_parameters": None, + "adhoc_execute": None, + "schedule_group_id": None, + "parameters": None, + "server_id": None, + "last_run_date": None, + "is_enabled": None, + "interval_mask": None, + "schedule_description": None, + "auto_update_time": None, + "schedule_order": None, + "schedule_id": schedule.schedule_id, + "is_async": None, + "abort_running": None, + "exec_command": None, + "first_run_date": None, + "is_running": None, + "smart_interval_mask": schedule.smart_interval_mask + } + schedule_details_list.append(schedule_details) + schedule_ids.append(schedule.schedule_id) + + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=schedule_details_list) + + +@utils.named_exception_handler("smart_schedule") +def main(server_id=None, dbname=None, ga_config=None): + if server_id and type(server_id) != int: raise TypeError(f"server_id should be int not {type(server_id)}") + + db_conn = postgres.db_cicada(dbname) + db_cur = db_conn.cursor() + optimise(db_cur=db_cur, server_id=server_id, ga_config=ga_config) + db_cur.close() + db_conn.close() + + +def optimise(db_cur, server_id=None, ga_config=None): + if not server_id: + # Recursively call main for each server_id if no specific server_id is provided + server_ids = scheduler.get_all_server_ids(db_cur) + for id in server_ids: + optimise(db_cur=db_cur, server_id=id[0], ga_config=ga_config) + + else: + + if not scheduler.validate_server_id(db_cur, server_id=server_id): + raise ValueError(f"Server with server_id={server_id} does not exist in the database") + + # Get schedules for the server_id + print("\n-----------------Schedule Setup----------------------") + # Prevents process from progressing if no schedules are found for the server_id + # however allows optimisation to still run for other server_ids if running for all servers (server_id=None) + try: + schedule_ids = _get_schedules_per_server(server_id=server_id, db_cur=db_cur) + except ValueError as e: + print(e) + return + print(f"Found {len(schedule_ids)} schedules for server_id {server_id}") + + # Build schedule objects + schedules = _create_schedule_objects(schedule_ids, db_cur=db_cur) + if not schedules: + raise ValueError(f"No valid schedules found to optimize for server_id {server_id}") + print("-------------------------------------------------\n") + + + try: + print("\n------------Starting Optimisation-----------------") + print("Running PyGAD solver ...") + ga = GAPyGADScheduler(config=ga_config) + optimised_schedules, __, peak_usage, __, initial_fitness = ga.solve(schedules) + print(f"Optimized schedule for server_id {server_id}: new peak usage {peak_usage}") + + + if peak_usage < initial_fitness: + try: + print("\n-------------Updating Schedules------------------") + db_cur.execute("BEGIN;") + _assign_new_schedules(optimised_schedules, db_cur=db_cur) + scheduler.snapshot_schedules(db_cur, server_id=server_id, computed_usage=peak_usage, reason='Smart Schedule Optimization') + db_cur.execute("COMMIT;") + except Exception as e: + db_cur.execute("ROLLBACK;") + print("Database changes have been rolled back due to the error.") + raise Exception(f"Error during schedule update for server_id {server_id}: {e}") + print("--------------------------------------------------\n") + else: + print(f"No improvement found for server_id {server_id}. Current peak usage: {initial_fitness}, Optimized peak usage: {peak_usage}. No schedule updates will be made.") + print("--------------------------------------------------\n") + + except Exception as e: + print(f"Error during optimization for server_id {server_id}: {e}") + sys.exit(1) diff --git a/cicada/commands/smart_schedule_rollback.py b/cicada/commands/smart_schedule_rollback.py new file mode 100644 index 0000000..4463e94 --- /dev/null +++ b/cicada/commands/smart_schedule_rollback.py @@ -0,0 +1,97 @@ +from typing import Optional +from cicada.lib import postgres, utils +from cicada.lib import scheduler + + + +def _rollback_to_previous_snapshot(db_cur, server_id): + """ + Roll back to the previous snapshot for a given server_id. If no previous snapshot exists, perform a full rollback. + """ + print(f"\n[Rolling back server {server_id}]") + snapshots = scheduler.retrieve_snapshots(db_cur, server_id) + current_snapshot = snapshots[0][0] if snapshots and len(snapshots) > 0 else None + previous_snapshot = snapshots[1][0] if snapshots and len(snapshots) > 1 else None + + db_cur.execute("BEGIN;") + try: + # Remove the current snapshot (if it exists) to prevent it from being restored in future rollbacks + if current_snapshot is not None: + scheduler.reset_schedule_backups(db_cur, snapshot_id=current_snapshot) + scheduler.remove_snapshot(db_cur, current_snapshot) + + # Restore the previous snapshot if it exists. If no previous snapshot exists, perform a full rollback instead + if previous_snapshot is not None: + scheduler.restore_previous_schedules(db_cur, server_id=server_id, snapshot_id=previous_snapshot) + else: + print("No previous snapshot found. Commencing full rollback instead...\n") + scheduler.full_rollback(db_cur, server_id=server_id) + db_cur.execute("COMMIT;") + except Exception as e: + db_cur.execute("ROLLBACK;") + print("Database changes have been rolled back due to the error.") + raise Exception(f"Error during rollback to previous snapshot for server_id {server_id}: {e}") + + +@utils.named_exception_handler("smart_schedule_rollback") +def main(server_id: Optional[int] = None, schedule_id: Optional[str] = None, dbname=None, full=False, previous=False): + """ + Roll back schedules after smart_schedule optimization. + + Args: + server_id: Optional[int] [Mutually exclusive with schedule_id] + Target server to roll back. + schedule_id: Optional[str] [Mutually exclusive with server_id] + Target schedule to roll back - can only be used with --full flag. + dbname: Optional[str] + Database name to connect to. + full: bool + If used, sets smart_interval_mask to NULL (revert to original interval_mask). + previous: bool + If used, restores to the most recent snapshot (step back one optimization). + """ + if server_id is not None and not isinstance(server_id, int): + raise TypeError(f"server_id needs to be of type int. {type(server_id)}") + if schedule_id is not None and not isinstance(schedule_id, str): + raise TypeError("schedule_id needs to be of type str") + if not(full or previous) or (full and previous): + raise ValueError("Exactly one of --full or --previous flags must be provided") + if schedule_id and not full: + raise ValueError("schedule_id can only be used with --full flag") + + db_conn = postgres.db_cicada(dbname) + db_cur = db_conn.cursor() + + try: + if full: + print("\n------------Starting Full Rollback-----------------") + db_cur.execute("BEGIN;") + try: + scheduler.full_rollback(db_cur, server_id, schedule_id) + print("Full rollback successful\n") + if not schedule_id: + server_ids = [server_id] if server_id else [server[0] for server in scheduler.get_all_server_ids(db_cur)] + for server in server_ids: + scheduler.snapshot_schedules(db_cur, server_id=server, reason='Full Rollback') + db_cur.execute("COMMIT;") + except Exception as e: + db_cur.execute("ROLLBACK;") + raise Exception(f"Error during full rollback: {e}") + + elif previous: + print("\n------------Starting Rollback to Previous Snapshot-----------------") + if not server_id: + print(f"Rolling back all servers...") + for server in scheduler.get_all_server_ids(db_cur): + _rollback_to_previous_snapshot(db_cur, server_id=server[0]) + else: + _rollback_to_previous_snapshot(db_cur, server_id=server_id) + print("--------------------------------------------------\n") + + except Exception as e: + print(f"Error during rollback: {e}") + raise + + finally: + db_cur.close() + db_conn.close() diff --git a/cicada/commands/spread_schedules.py b/cicada/commands/spread_schedules.py index 8c5d837..23aca17 100644 --- a/cicada/commands/spread_schedules.py +++ b/cicada/commands/spread_schedules.py @@ -117,6 +117,8 @@ def main(spread_details, dbname=None): output_message += " | Forced abort_running and adhoc_execute" scheduler.update_schedule_details(db_cur, new_schedule_details) + scheduler.reset_schedule_backups(db_cur, schedule_id=schedule_id) + else: output_message = ( f"'{str(current_schedule_details['schedule_id'])}' will be reassigned : " diff --git a/cicada/lib/scheduler.py b/cicada/lib/scheduler.py index f958b4e..da8599b 100644 --- a/cicada/lib/scheduler.py +++ b/cicada/lib/scheduler.py @@ -81,6 +81,7 @@ def get_schedule_details(db_cur, schedule_id): ,is_running ,abort_running ,interval_mask + ,smart_interval_mask ,first_run_date ,last_run_date ,exec_command @@ -107,12 +108,13 @@ def get_schedule_details(db_cur, schedule_id): schedule_details["is_running"] = row[7] schedule_details["abort_running"] = row[8] schedule_details["interval_mask"] = row[9] - schedule_details["first_run_date"] = row[10] - schedule_details["last_run_date"] = row[11] - schedule_details["exec_command"] = row[12] - schedule_details["parameters"] = row[13] - schedule_details["adhoc_parameters"] = row[14] - schedule_details["schedule_group_id"] = row[15] + schedule_details["smart_interval_mask"] = row[10] + schedule_details["first_run_date"] = row[11] + schedule_details["last_run_date"] = row[12] + schedule_details["exec_command"] = row[13] + schedule_details["parameters"] = row[14] + schedule_details["adhoc_parameters"] = row[15] + schedule_details["schedule_group_id"] = row[16] return schedule_details @@ -233,6 +235,92 @@ def update_schedule_details(db_cur, schedule_details): db_cur.execute(sqlquery) +def update_schedule_details_bulk(db_cur, schedule_list): + """Update multiple schedules with individual UPDATE statements in a single execute call.""" + if not schedule_list: + return + + statements = [] + params = [] + + for schedule in schedule_list: + updates = {k: v for k, v in schedule.items() if k != "schedule_id" and v is not None} + + if not updates: + continue + + set_clause = ", ".join([f"{col} = %s" for col in sorted(updates.keys())]) + statement = f"UPDATE schedules SET {set_clause} WHERE schedule_id = %s" + statements.append(statement) + + for col in sorted(updates.keys()): + params.append(updates[col]) + params.append(schedule['schedule_id']) + + if not statements: + print("No fields to update for any schedules. Bulk update skipped.") + return + + sqlquery = "; ".join(statements) + db_cur.execute(sqlquery, tuple(params)) + print(f"\nBulk updated {len(statements)} schedules") + + +def snapshot_schedules(db_cur, server_id=None, computed_usage=None, reason=None): + """Create a snapshot of specific schedules with the same snapshot_id. + + Args: + db_cur: Database cursor + schedule_ids: List of schedule_ids to snapshot + server_id: server_id for the snapshot + computed_usage: Computed usage for the snapshot + reason: Optional reason/context for the snapshot + """ + + if not server_id: + raise ValueError("server_id must be provided for snapshot") + + # Insert into snapshots table to get a new snapshot_id + sqlquery = "INSERT INTO snapshots (reason, server_id, computed_usage) VALUES (%s, %s, %s) RETURNING snapshot_id" + db_cur.execute(sqlquery, (reason, server_id, computed_usage)) + snapshot_id = db_cur.fetchone()[0] + + # Snapshot the specified schedules with the same snapshot_id + sqlquery = """ + INSERT INTO schedule_backups (schedule_id, server_id, interval_mask, smart_interval_mask, snapshot_id) + SELECT schedule_id, server_id, interval_mask, smart_interval_mask, %s + FROM schedules WHERE server_id = %s + """ + db_cur.execute(sqlquery, (snapshot_id, server_id)) + + # Clean up old snapshots (keep last 5) + min_snapshot_query = """ + SELECT snapshot_id FROM snapshots + WHERE server_id = %s + ORDER BY snapshot_id DESC + LIMIT 1 OFFSET 4 + """ + + db_cur.execute(min_snapshot_query, (server_id,)) + result = db_cur.fetchone() + min_snapshot_to_keep = result[0] if result else 0 + + cleanup_backups_query = """ + DELETE FROM schedule_backups + WHERE snapshot_id < %s + AND server_id = %s + """ + db_cur.execute(cleanup_backups_query, (min_snapshot_to_keep, server_id)) + print(f"\nCleaned up old schedule_backups for server_id {server_id} in schedule_backups table") + cleanup_snapshots_query = """ + DELETE FROM snapshots + WHERE snapshot_id < %s + AND server_id = %s + """ + db_cur.execute(cleanup_snapshots_query, (min_snapshot_to_keep, server_id)) + print(f"Cleaned up old snapshots for server_id {server_id} in snapshots table") + + def get_schedule_executable(db_cur, schedule_id): """Extract details of executable of a schedule""" sqlquery = ( @@ -288,7 +376,7 @@ def get_all_schedules(db_cur, server_id, is_async): ( /* foo */ (SELECT schedule_id, - interval_mask, + COALESCE(smart_interval_mask, interval_mask) as interval_mask, exec_command, COALESCE(adhoc_parameters, parameters, '') AS parameters, adhoc_execute, @@ -370,3 +458,166 @@ def get_all_schedule_ids(db_cur): def delete_schedule(db_cur, schedule_id): sqlquery = f"DELETE from schedules WHERE schedule_id = '{schedule_id}'" db_cur.execute(sqlquery) + + + +def get_all_server_ids(db_cur): + """Get all possible server_ids from the servers table""" + sqlquery = "SELECT DISTINCT server_id FROM schedules ORDER BY server_id" + db_cur.execute(sqlquery) + server_ids = db_cur.fetchall() + + return server_ids + +def get_all_schedule_ids_per_server(db_cur, server_id): + """Get all possible schedule_ids for each server from the schedules table""" + sqlquery = """ SELECT DISTINCT schedule_id FROM schedules WHERE server_id = %s """ + db_cur.execute(sqlquery, (server_id,)) + schedule_ids = db_cur.fetchall() + + return schedule_ids + + +def get_median_run_time(db_cur, schedule_id): + """ + Calculate the median runtime in minutes for a schedule_id from the schedule_log table. + + Zero runs => 5 mins (conservative estimate, allows local testing without data and for new schedules to be + scheduled without having to wait for historical data to be collected. + """ + + sqlquery = f""" + SELECT percentile_cont(0.5) + WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (end_time - start_time)) / 60) + AS median_minutes_taken + FROM schedule_log + WHERE schedule_id = '{schedule_id}' + """ + db_cur.execute(sqlquery) + row = db_cur.fetchone() + + try: + average_runtime_minutes = float(row[0]) + return average_runtime_minutes + except Exception: + # No runs -> assigns default runtime of 5 minutes + return 5 + + +def retrieve_snapshots(db_cur, server_id): + """ + Retrieve all snapshots in reverse chronological order. Returns None if no snapshots exist. + """ + sqlquery = """ + SELECT snapshot_id, snapshot_timestamp + FROM snapshots + WHERE server_id = %s + ORDER BY snapshot_timestamp DESC + """ + db_cur.execute(sqlquery, (server_id,)) + snapshots = db_cur.fetchall() + return snapshots if snapshots else None + + +def full_rollback(db_cur, server_id=None, schedule_id=None): + """ + Roll back schedules to original interval_mask by setting smart_interval_mask to NULL for either a server_id or an individual schedule_id. + Args: + server_id | schedule_id: Optional[int | str] [Mutually exclusive] + Target server/schedule to roll back all schedules for. If not provided, will roll back all schedules for all servers. + """ + if server_id and schedule_id: + raise ValueError("Cannot specify both server_id and schedule_id for full rollback, please specify only one to rollback all schedules for a server or an individual schedule respectively") + if server_id: + schedule_ids = [row[0] for row in get_all_schedule_ids_per_server(db_cur, server_id)] + elif schedule_id: + print(f"Rolling back schedule_id {schedule_id} to original interval_mask...") + schedule_ids = [schedule_id] + else: + print(f"Rolling back schedules for all servers to original interval_mask...") + schedule_ids =[row[1] for row in get_all_schedule_ids(db_cur)] + + print(f"Found {len(schedule_ids)} schedules to rollback for server_id ...") + print("Removing smart_interval_mask for selected schedules...") + update_all_schedules_query = """ + UPDATE schedules SET smart_interval_mask = NULL WHERE schedule_id = ANY(%s::text[]) + """ + db_cur.execute(update_all_schedules_query, (schedule_ids,)) + print(f"Schedules Updated:'{chr(10).join([f'- {sid}' for sid in schedule_ids])}") + + return + +def restore_previous_schedules(db_cur, server_id, snapshot_id): + """ + Restore schedules from snapshots. + """ + if not snapshot_id: + raise ValueError("snapshot_id is required to restore previous schedules") + + schedule_ids = get_all_schedule_ids_per_server(db_cur, server_id) + print(f"{len(schedule_ids)} schedules found for server_id {server_id}") + print("Restoring schedules from snapshot...") + sqlquery = """ + UPDATE schedules + SET smart_interval_mask = schedule_backups.smart_interval_mask + FROM schedule_backups + WHERE schedules.schedule_id = schedule_backups.schedule_id + AND schedules.server_id = %s + AND schedule_backups.snapshot_id = %s + AND schedules.interval_mask = schedule_backups.interval_mask + """ + db_cur.execute(sqlquery, (server_id, snapshot_id)) + rows_updated = db_cur.rowcount + print(f"{rows_updated} Schedules restored") + + +def get_blocklisted_schedule_ids(db_cur): + """Get a list of schedule_ids that are blocklisted from optimization""" + sqlquery = "SELECT schedule_id FROM schedule_blocklist" + db_cur.execute(sqlquery) + blocklist_schedule_ids = [row[0] for row in db_cur.fetchall()] + return blocklist_schedule_ids + + +def reset_schedule_backups(db_cur, snapshot_id=None, schedule_id=None): + """Reset schedule_backups table by deleting all entries""" + if not snapshot_id and not schedule_id: + raise ValueError("Either snapshot_id or schedule_id must be provided to reset schedule_backups") + if snapshot_id and schedule_id: + raise ValueError("Cannot specify both snapshot_id and schedule_id to reset schedule_backups") + + if schedule_id: + sqlquery_backups = "DELETE FROM schedule_backups WHERE schedule_id = %s" + db_cur.execute(sqlquery_backups, (schedule_id,)) + if snapshot_id: + sqlquery_backups = " DELETE FROM schedule_backups WHERE snapshot_id = %s" + sqlquery_snapshots = " DELETE FROM snapshots WHERE snapshot_id = %s" + db_cur.execute(sqlquery_backups, (snapshot_id,)) + db_cur.execute(sqlquery_snapshots, (snapshot_id,)) + + +def blocklist_schedule(db_cur, schedule_id, reason=None): + """Add a schedule_id to the blocklist""" + sqlquery = "INSERT INTO schedule_blocklist (schedule_id, reason) VALUES (%s, %s) ON CONFLICT DO NOTHING" + db_cur.execute(sqlquery, (schedule_id, reason)) + return + +def remove_blocklist_schedule(db_cur, schedule_id): + """Remove a schedule_id from the blocklist""" + sqlquery = "DELETE FROM schedule_blocklist WHERE schedule_id = %s" + db_cur.execute(sqlquery, (schedule_id,)) + return + +def remove_snapshot(db_cur, snapshot_id): + """Remove a snapshot_id from the snapshots table""" + sqlquery = "DELETE FROM snapshots WHERE snapshot_id = %s" + db_cur.execute(sqlquery, (snapshot_id,)) + return + + +def validate_server_id(db_cur, server_id): + """Validate that a server_id exists in the servers table""" + sqlquery = "SELECT COUNT(1) FROM servers WHERE server_id = %s" + db_cur.execute(sqlquery, (server_id,)) + row = db_cur.fetchone() + return (row[0] == 1) \ No newline at end of file diff --git a/cicada/lib/smart_scheduling/__init__.py b/cicada/lib/smart_scheduling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cicada/lib/smart_scheduling/config.py b/cicada/lib/smart_scheduling/config.py new file mode 100644 index 0000000..5f39955 --- /dev/null +++ b/cicada/lib/smart_scheduling/config.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Optional, List + +@dataclass +class GAConfig: + num_generations: int = 20 + sol_per_pop: int = 40 + num_parents_mating: int = 10 + mutation_percent_genes: int = 20 + parent_selection_type: str = "rank" + crossover_type: str = "uniform" + mutation_type: str = "random" + keep_elitism: int = 2 + random_seed: Optional[int] = None diff --git a/cicada/lib/smart_scheduling/domain.py b/cicada/lib/smart_scheduling/domain.py new file mode 100644 index 0000000..4094f9c --- /dev/null +++ b/cicada/lib/smart_scheduling/domain.py @@ -0,0 +1,98 @@ +from __future__ import annotations +import math +from typing import Optional +from croniter import croniter +import datetime +from cicada.lib.scheduler import get_median_run_time + + +class Schedule: + schedule_id: str + server_id: int + interval_mask: str + frequency_minutes: int + median_runtime_minutes: int + shifted: bool + start_time_mins: Optional[int] + blocklisted: bool + + + def __init__(self, schedule_id: str, server_id: int, interval_mask: str, smart_interval_mask: Optional[str] = None, blocklisted: bool = False, db_cur=None): + self.schedule_id = schedule_id + self.server_id = server_id + self.interval_mask = interval_mask + self.smart_interval_mask = smart_interval_mask + self.blocklisted = blocklisted + self.determine_attributes(db_cur) + + def determine_attributes(self, db_cur): + """Determine frequency and average runtime from interval_mask and scheduler module""" + + self.shifted = False + self.current_interval_mask = ( + self.smart_interval_mask + if self.smart_interval_mask is not None + else self.interval_mask + ) + self._determine_frequency() + self._determine_start_time_mins() + self._get_average_runtime(db_cur) + + def _determine_frequency(self): + """Determine frequency in minutes from interval_mask using crontier""" + schedule = croniter(self.interval_mask) + first_iter = schedule.get_next(datetime.datetime) + second_iter = schedule.get_next(datetime.datetime) + frequency = (second_iter - first_iter).total_seconds() / 60 + self.frequency_minutes = int(frequency) + + + def _get_average_runtime(self, db_cur): + """Get average runtime from scheduler module""" + self.median_runtime_minutes = math.ceil(get_median_run_time(db_cur, self.schedule_id)) + + def _determine_start_time_mins(self): + """Determine the start time in minutes from midnight from the interval_mask""" + + today = datetime.datetime.now(datetime.timezone.utc).date() + midnight = datetime.datetime.combine(today, datetime.time.min, tzinfo=datetime.timezone.utc) + + # Infrequent taps aren't bounded by their frequency but instead shift within the hour + # Basing it on the original interval mask prevents creep over multiple optimizations + # and ensures the schedule doesn't shift more than an hour from the original schedule + if self.frequency_minutes > 60: + it = croniter(self.interval_mask, midnight) + else: + it = croniter(self.current_interval_mask, midnight) + + if croniter.match(self.current_interval_mask, midnight): + first_iter = midnight + self.start_time_mins = 0 + else: + first_iter = it.get_next(datetime.datetime) + self.start_time_mins = first_iter.hour * 60 + first_iter.minute + + def is_blocklisted(self): + """Determine if the Schedule is blocklisted based on schedule_id""" + return self.blocklisted + + def frequency_is_supported(self): + """Determine if the Schedule frequency is supported for smart scheduling""" + if (self.frequency_minutes != 1440 and self.frequency_minutes > 60): return False + if (self.frequency_minutes <= 1): return False + return True + + def is_unsupported(self): + """Determine if the Schedule is unsupported for smart scheduling based on frequency or if it's blocklisted""" + return (not self.frequency_is_supported() or self.is_blocklisted() or not self.is_regular_schedule()) + + def is_regular_schedule(self): + """Check if the cron expression is a regular schedule that can be optimized by the GA """ + try: + schedule = croniter(self.interval_mask) + iters = [schedule.get_next(datetime.datetime) for _ in range(100)] + freqs = [iters[i + 1] - iters[i] for i in range(len(iters) - 1)] + if any(freq <= datetime.timedelta(minutes=1) for freq in freqs): return False + return all(f == freqs[0] for f in freqs) + except (ValueError, KeyError): + return False diff --git a/cicada/lib/smart_scheduling/evaluation.py b/cicada/lib/smart_scheduling/evaluation.py new file mode 100644 index 0000000..88e553e --- /dev/null +++ b/cicada/lib/smart_scheduling/evaluation.py @@ -0,0 +1,46 @@ +import numpy as np +from typing import Sequence +from cicada.lib.smart_scheduling.domain import Schedule + + +def evaluate_usage_and_peak(start_times: Sequence[int], schedules: Sequence[Schedule]): + """ + Returns the usage time series and peak usage for a given schedule solution + Args: + start_times: Sequence[int] : start time in minutes for each schedule + schedules: Sequence[Schedule] : list of Schedule objects + Returns: + usage: np.ndarray : usage time series + peak: float : peak usage + """ + + mins_per_day = 1440 + diff = np.zeros(mins_per_day + 1, dtype=float) + + for i, schedule in enumerate(schedules): + if not schedule.frequency_is_supported(): + continue + + freq = schedule.frequency_minutes + if start_times[i] >= freq: + raise ValueError(f"Start time should be the earliest it can be for schedule: {schedule} with start time {start_times[i]} exceeds frequency {freq}") + if freq <= 0: + raise ValueError(f"Unsupported frequency {freq} for schedule {schedule} {schedule.interval_mask} should have been labelled unsupported and caught earlier.") + + run_time = schedule.median_runtime_minutes + minute = int(start_times[i]) + + # Iterate through the day in increments of the schedule's frequency, adding the schedule's usage to the diff array for the duration of its runtime. + # We use a diff array to efficiently calculate the cumulative usage at each minute. Instead of appending the usage for each minute the + # 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[end] -= 1 + minute += freq + + # Sums up everything in the diff array to get the total usage at each minute, and finds the peak usage. + # Ignore the last element of the diff array since it's just a placeholder to handle the end minute subtraction for schedules that run until the end of the day. + usage = np.cumsum(diff[:-1]) + peak = float(np.max(usage)) if usage.size else 0.0 + return usage, peak \ No newline at end of file diff --git a/cicada/lib/smart_scheduling/ga_pygad.py b/cicada/lib/smart_scheduling/ga_pygad.py new file mode 100644 index 0000000..17d8157 --- /dev/null +++ b/cicada/lib/smart_scheduling/ga_pygad.py @@ -0,0 +1,129 @@ +from __future__ import annotations +from typing import List, Mapping, Optional, Sequence +import numpy as np +import pygad + +from cicada.lib.smart_scheduling.config import GAConfig +from cicada.lib.smart_scheduling.domain import Schedule +from cicada.lib.smart_scheduling.evaluation import evaluate_usage_and_peak + + +class GAPyGADScheduler: + """ + Genetic Algorithm Scheduler using PyGAD + Args: + config: Optional[GAConfig] : configuration for the genetic algorithm + Returns: + Schedule : optimized schedule for all schedules + + + Implementation Note: We only consider the regular schedules during fitness evaluation to aid simplicity as there are few irregular + schedules. All regular schedules are fed into the scheduler however those on the blocklist will remain unchanged + and are kept purely to ensure the fitness evaluation is accurate to the actual schedule. + + We cap the max shift of a schedule to within the hour to prevent large shifts for schedules that run daily. + """ + + def __init__(self, config: Optional[Mapping[str, object]] = None): + if config is None: + self.cfg = GAConfig() + else: + filtered_config = {key: value for key, value in config.items() if value is not None} + self.cfg = GAConfig(**filtered_config) + + + def _gene_space(self, schedules: Sequence[Schedule]) -> List[dict]: + # Build gene_space per schedule: each gene space is limited by its frequency + # Unless the schedule is unsupported (either blocklisted, irregular or has frequency greater than 60 mins), + # in which case we fix the gene space so it remains unchanged in the GA but is still included in fitness evaluation. + # Constrain schedules with frequency > 60 mins to an hour to prevent large shifts. + + gene_space = [] + mins_per_day = 1440 + + for schedule in schedules: + if schedule.is_unsupported(): + # Fix gene space to current start time (no shift allowed) + gene_space.append({"low": schedule.start_time_mins, "high": schedule.start_time_mins}) + elif schedule.frequency_minutes > 60: + # Shift within the hour, clamped to day limit + high = min(schedule.start_time_mins + 59, mins_per_day) + low = max(high - 59, 0) + gene_space.append({"low": low, "high": high}) + else: + # Shift within frequency range + gene_space.append({"low": 0, "high": schedule.frequency_minutes - 1}) + + return gene_space + + def _initial_population(self, schedules: Sequence[Schedule], gene_space: List[List[int]]) -> np.ndarray: + rng = np.random.default_rng(self.cfg.random_seed) + seed = [] + + # Add current start minutes as first solution to bias solution space towards current solution + for i, schedule in enumerate(schedules): + gs = gene_space[i] + s = int(schedule.start_time_mins) + seed.append(max(min(s, gs["high"]), gs["low"])) + pop = [seed] + + # Populate the rest of the initial population randomly within the gene space limits for each schedule + for _ in range(self.cfg.sol_per_pop - 1): + pop.append([int(rng.integers(gene_space[i]["low"], gene_space[i]["high"] + 1)) for i in range(len(schedules))]) + return np.asarray(pop, dtype=int) + + def fitness_fn(self, ga, solution, solution_idx): + _, peak = evaluate_usage_and_peak(solution, self.schedules) + return -float(peak) + + def solve(self, schedules: Sequence[Schedule]) -> tuple[Sequence[Schedule], List[int], float, np.ndarray, float]: + self.schedules = schedules + gene_space = self._gene_space(schedules) + print("Successfully initialised gene space") + + initial_population = self._initial_population(schedules, gene_space) + print("Created initial population. Current Solution Start Times:") + print(initial_population[0]) + + initial_fitness = self.fitness_fn(None, initial_population[0], 0) + print("Initial population fitness (max usage):", -initial_fitness) + + ga = pygad.GA( + num_generations=self.cfg.num_generations, + sol_per_pop=self.cfg.sol_per_pop, + num_parents_mating=self.cfg.num_parents_mating, + num_genes=len(schedules), + gene_type=int, + gene_space=gene_space, + mutation_percent_genes=self.cfg.mutation_percent_genes, + fitness_func=self.fitness_fn, + parent_selection_type=self.cfg.parent_selection_type, + keep_elitism=self.cfg.keep_elitism, + crossover_type=self.cfg.crossover_type, + mutation_type=self.cfg.mutation_type, + allow_duplicate_genes=True, + initial_population=initial_population, + random_seed=self.cfg.random_seed, + ) + ga.run() + + best_solution, best_fitness, _ = ga.best_solution() + start_times = [int(v) for v in best_solution] + peak_usage = -float(best_fitness) + + print(f"Optimised for {self.cfg.num_generations} generations. Best Solution Start Times:") + print(best_solution) + + usage, _ = evaluate_usage_and_peak(start_times, schedules) + + # Update schedule objects start_time_mins attribute based on GA solution + for i, schedule in enumerate(schedules): + if not (start_times[i] >= gene_space[i]["low"] and start_times[i] <= gene_space[i]["high"]): + raise RuntimeError(f"Start time for schedule {schedule.schedule_id} is out of gene space bounds. Start time: {start_times[i]}, Gene space: {gene_space[i]}") + if schedule.is_unsupported() and start_times[i] != schedule.start_time_mins: + raise RuntimeError(f"Unsupported schedule {schedule.schedule_id} should not have been shifted in the GA solution. {schedule.start_time_mins} != {start_times[i]}") + elif schedule.start_time_mins != start_times[i]: + schedule.shifted = True + schedule.start_time_mins = start_times[i] + + return schedules, start_times, peak_usage, usage, -initial_fitness \ No newline at end of file diff --git a/docs/genetic_algorithm_process_cycle.png b/docs/genetic_algorithm_process_cycle.png new file mode 100644 index 0000000..04dfa4b Binary files /dev/null and b/docs/genetic_algorithm_process_cycle.png differ diff --git a/docs/offspring_ga.png b/docs/offspring_ga.png new file mode 100644 index 0000000..1566901 Binary files /dev/null and b/docs/offspring_ga.png differ diff --git a/docs/smart_scheduler_technical_overview.md b/docs/smart_scheduler_technical_overview.md new file mode 100644 index 0000000..59c17c4 --- /dev/null +++ b/docs/smart_scheduler_technical_overview.md @@ -0,0 +1,246 @@ +# Technical Overview: SmartScheduling Feature (AP-2566) + +## Problem Statement + +Cicada schedules jobs across multiple servers. Without optimization, jobs naturally cluster at the same start times (e.g., many cron jobs at :00 or :30 every hour), causing resource spikes and conflicts. Jobs compete for CPU, I/O, and network bandwidth, reducing overall system efficiency and reliability. + +## Solution Overview + +**SmartScheduling** uses a Genetic Algorithm (GA) to automatically shift job start times, distributing them across the 24-hour period while respecting original cron frequencies and maintaining schedule validity. This reduces peak resource contention and improves system throughput. + +The GA evolves shift offsets for each schedule over multiple generations to find near-optimal start time distributions that minimize peak usage. + +## Architecture + +### Core Components + +1. **Domain Layer** (`domain.py`) — Represents schedules as Schedule objects + +2. **GA Configuration** (`config.py`) — Hyperparameters for optimization + +3. **Genetic Algorithm Engine** (`pygad.py`) — PyGAD-based optimizer + +4. **Fitness Evaluation** (`evaluation.py`) — Scoring logic + +5. **Command Handler** (`smart_schedule.py`) — Orchestrates optimization + +6. **Rollback System** (`smart_schedule_rollback.py`) — Recovery mechanism + +7. **Blocklisting** (`blocklist.py`) - Adding/Removing taps from Blocklist + +### Database Schema Changes + +**New Tables:** + +- **`schedule_backups`** — Interval mask of schedules at different points in time to allow for rolling back + - `schedule_id`: schedule snapshotted + - `snapshot_id`: snapshot identifier + - `server_id`: server_id of the snapshot at that point in time (used to prevent rollback to a previous version which was on a different server) + - `interval_mask`: original interval mask (used to prevent rollback to a previous version which had a different interval_mask) + - `smart_interval_mask`: smart interval mask at that point in time + +- **`snapshot_table`** - Snapshot metadata + -`snapshot_timestamp` + +- **`schedule_blocklist`** — Excludes schedules from optimization + - `schedule_id` (PK): schedule to exclude + - `reason`: free-text explanation (e.g., "manual request", "irregular cron") + - Allows selective opt-out without deleting schedule + + +## Data Flow + +### Optimization Workflow + +``` +1. Load Schedules + └─> Query database for all schedules on a server + +2. Create schedule Objects per schedule_id + └─> Calculating properties based on cron schedule + └─> Check whether it's supported (blocklist, irregular etc.) + +3. Run GA Optimization + └─> GAPyGADScheduler.solve(schedules): + ├─> Build gene_space (permissible shifts per schedule) + │ ├─> Unsupported schedules: gene_space = [0] (no shift) + │ ├─> Frequent schedules (< 60 min): gene_space = [0..frequency) + │ └─> Infrequent schedules (> 60 min): gene_space = [0..60) + ├─> Initialize population with current solution as seed + ├─> PyGAD evolves population over N generations + │ └─> Each generation: mutation, crossover, fitness evaluation + ├─> Calculate fitness (-peak_usage) for each candidate + └─> Returns best solution + +4. Evaluate Improvement + └─> Compare initial_peak_usage vs optimized_peak_usage. If improved: proceed to assignment + +5. Update Schedules + └─> both schedule table and schedule_backups in case of rollback +``` + +### Rollback Workflow + +``` +Rollback command triggered with server_id or schedule_id + +1. Query schedule_backups for affected schedules +2. Restore to previous interval_mask (or original if full=True) +3. Update schedules table +4. Update schedule_backups with new checkpoint +``` + +## Genetic Algorithm Details + +

+ +

+ + +### Gene Representation + +Each gene is a minute representing where a schedule should start within a day. +- Gene value can take any value between the min and max start time +- The gene space is limited to the smallest range it could be and then extrapolated out when it comes to evaluating the max usage over the day +- Defining unique gene spaces where each schedule has it's own gene space allows us to reduce the search space considerably. +- By tailoring our gene spaces we can allow the schedule to only traverse a couple of discrete positions, this makes our algorithm run as efficiently as possible and have a more comprehensive search of the solution space. + + +### Fitness Function + +Inverse of the peak_usage since it's a minimisation problem. Peak_usage is calculated over a single day since that covers 99% of all schedules. + - For each schedule, add it to the usage array from `start_time` to `start_time + runtime` + - Repeat for minute + - Use a difference array for efficient cumulative calculation + - Uses only the maximum usage across entire day + + +### Crossover & Mutation + +

+ +

+ + + +- **Crossover Type**: Uniform (each gene inherited from random parent per individual) +- **Mutation Type**: Random (randomly select genes and replace with random value from gene space) +- **Elitism**: Keep the best solution across generations (default: 1) + +The creation of the offsprings uses different methods to change the solutions, however they must remain within the gene limits. For more information checkout the official [PyGAD documentation](https://pypi.org/project/pygad) as it will be infinitely better than anything I can produce + +### Population Seeding + +We seed the initial population with the current solution as we want to **prioritise stability over minor gains**. +The system is already quite imprecise: + - assumes uniform CPU usage across all schedules + - rounds runtime to nearest minute + - assumes consistent runtimes from one schedule run to another + +Because of this imprecision and a natural desire not to overfit the system (e.g. we don't want a solution that minimises the peak usage unless a heavy schedule runs a minute longer than usual and then it clashes with another heavy schedule) we want to only change the schedule runs when it offers an actual advantage. Shifting schedules occassionally is needed to minimise the usage, however shifting can also cause missed schedule runs (if we e.g. change schedule 13-59/15 * * * * to 9-59/15 * * * * at 11 minutes past the hour). + +## Configuration + +The `GAConfig` dataclass controls GA behavior. + +Tuning Considerations: +- ↑ `num_generations`: Better solutions but slower +- ↑ `sol_per_pop`: Larger search space but slower +- ↑ `mutation_percent_genes`: More exploration, less exploitation + +## Schedule Validation & Filtering + +Not all schedules are suitable for GA optimization: + +### Supported Schedules +- **Frequency**: `x > 1` & `x <=1440` minutes (1 minute to 1 day) +- **Regularity**: Cron expression must be perfectly regular (same interval between every consecutive run) +- **blocklist**: Schedule is not in `schedule_blocklist` table + +### Unsupported Schedules (Skipped) +- **Irregular cron**: e.g., "0 0,12 * * *" (runs at two different times) — frequency varies +- **Too frequent**: <= 1 minute +- **Too rare**: > 1440 minutes (more than a day) +- **blocklisted**: Explicitly marked in `schedule_blocklist` table +- **Parsing errors**: Invalid cron expressions + +**Unsupported schedules remain in the fitness evaluation** but don't participate in the optimization (shift = 0 fixed), ensuring the fitness score reflects realistic daily load. + + +## Checkpointing & Rollback + +Every optimization run creates a checkpoint in `schedule_backups`: + +- **Original**: pristine pre-optimization cron (set once, never changes unless the schedule gets upserted) +- **Previous**: cron before this optimization +- **Current**: new cron after optimization + +**Rollback Options:** +- `--full`: Revert to original (wipe all optimization history) +- (default): Revert to previous (undo last optimization only) + +**Selective Rollback:** +- `--server_id`: Rollback all schedules on a server +- `--schedule_id`: Rollback a single schedule + +## Integration Points + +### CLI Entry Points + +Added to `cicada/cli.py`: +- `cicada smart_schedule [--server_id ]` — Run optimization +- `cicada smart_schedule_rollback [--server_id | --schedule_id ] [--full]` — Revert changes + +### Command-Line Parameters for `smart_schedule` + +- `--server_id`: Optimize schedules on specific server (optional; all servers if omitted) +- `--dbname`: Database name override +- GA hyperparameters can be passed via `ga_config` parameter (advanced use) + +### Database Dependencies + +- **Read**: `schedules`, `servers`, `schedule_logs`, `schedule_blocklist` +- **Write**: `schedules` (interval_mask), `schedule_backups` (checkpoints) + +## Design Decisions + +### Why a Genetic Algorithm? + +1. **NP-hard problem**: Optimal schedule assignment is combinatorially hard; GA provides good approximations in reasonable time +2. **Flexible constraints**: GA naturally handles irregular constraints (blocklists, unsupported frequencies). It allows us an easy mechanism to include these in the calculations while not changing them +3. **No gradient**: Fitness landscape is non-smooth; gradient-free methods suit this +4. **Mature libraries**: PyGAD is well-maintained and configurable +5. **Discrete Runs**: Works well with discrete times + + +### Why Seed with Current Solution? + +- Biases the GA towards an existing solution space (there are many solutions that are equally good and we don't need to explore every single one, this isn't a problem with a clear global minimum) +- Biases search toward improvements on the current baseline which avoids "thrashing" between very different schedules + +### Why Unsupported schedules Stay in Fitness? + +- Ensures fitness reflects real daily load (including irregular jobs, blocklisted, etc.) +- Allows GA to account for load from non-optimizable schedules +- Prevents GA from misoptimizing around missing jobs + +### Why No Frequency Constraints in Fitness? + +- GA gene space enforces frequency constraints (each schedule's shifts are within its frequency) +- Fitness function only evaluates peak, not constraint satisfaction +- Simpler, faster fitness evaluation without redundant checking + +## Performance Considerations + +- **Time Complexity**: O(pop_size × num_generations × num_schedules × blocks_per_day) +- **Space Complexity**: O(num_schedules × blocks_per_day) +- **Typical Runtime**: ~5-30 seconds for 100-500 schedules, 20 generations, 40 population size +- **Bottleneck**: Fitness evaluation (diff array accumulation); PyGAD overhead minimal + +**Optimization Tips:** +- blocklist irregular/infrequent schedules to reduce optimization scope +- Reduce `num_generations` or `sol_per_pop` for faster, lower-quality results (specifically for when testing) + +## Future Enhancements + + **CPU**: Add CPU to the model. Needs a separate mechanism to capture CPU levels so this is likely not worthwhile implementing diff --git a/local-dev/entrypoint.sh b/local-dev/entrypoint.sh index fb99d1f..abe3c79 100755 --- a/local-dev/entrypoint.sh +++ b/local-dev/entrypoint.sh @@ -65,6 +65,7 @@ make dev --file=${CICADA_HOME}/Makefile --always-make python=python3.8 # Register this server in Database ${CICADA_HOME}/venv/bin/cicada register_server +psql -v ON_ERROR_STOP=1 "sslmode=prefer user=${DB_POSTGRES_USER} host=${DB_POSTGRES_HOST} port=${DB_POSTGRES_PORT} dbname=${DB_POSTGRES_DB}" --file=setup/create_test_tap_setup.sql --quiet # Upsert some manual test schedules ${CICADA_HOME}/venv/bin/cicada upsert_schedule --schedule_id=missing_exec --is_enabled=1 --exec_command="death.exe" --interval_mask="* * * * *" diff --git a/setup.py b/setup.py index bfa429a..ddcfaa5 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="cicada", - version="0.9.0", + version="0.10.0", description="Lightweight, agent-based, distributed scheduler", long_description=long_description, long_description_content_type="text/markdown", @@ -24,6 +24,8 @@ "tabulate==0.9.*", "slack-sdk==3.37.*", "backoff==2.2.*", + "numpy==1.24.*", + "pygad==3.5.*" ], extras_require={ "dev": [ diff --git a/setup/MIGRATION_GUIDE.md b/setup/MIGRATION_GUIDE.md new file mode 100644 index 0000000..a805884 --- /dev/null +++ b/setup/MIGRATION_GUIDE.md @@ -0,0 +1,368 @@ +# Schema Migration Guide + +This guide explains how to add schema changes that work gracefully for both fresh installations and existing deployments. + +## Philosophy + +Cicada's migration strategy ensures: +- **Fresh installs** get the complete schema from `schema.sql` +- **Existing installs** are updated via migration scripts +- **Idempotency** — migrations can be run multiple times safely +- **Rollback awareness** — changes integrate with the existing `schedule_changes` audit trail + +## Pattern: Adding a New Column to `schedules` + +This is the most common migration scenario. Here's the approach: + +### Step 1: Update `setup/schema.sql` + +Add the column definition **within the `CREATE TABLE IF NOT EXISTS public.schedules` statement**: + +```sql +-- In the CREATE TABLE IF NOT EXISTS public.schedules section: +CREATE TABLE IF NOT EXISTS public.schedules +( + -- ... existing columns ... + my_new_column VARCHAR(255) DEFAULT 'default_value', -- Add here + CONSTRAINT schedules_pkey PRIMARY KEY (schedule_id), + -- ... rest of constraints ... +) +WITH (OIDS=FALSE); +``` + +Then, add a defensive `ALTER TABLE` with `IF NOT EXISTS` immediately after the `CREATE TABLE`: + +```sql +-- Add my_new_column if not exists (for existing installations upgrading) +ALTER TABLE public.schedules +ADD COLUMN IF NOT EXISTS my_new_column VARCHAR(255) DEFAULT 'default_value'; +``` + +**Why both?** +- The column in `CREATE TABLE` satisfies fresh installs (cleaner schema) +- The `ALTER TABLE ... IF NOT EXISTS` ensures existing installations get the column without errors + +### Step 2: Create a Versioned Migration Script (Optional but Recommended) + +If the migration is complex or you want a separate, self-contained migration file: + +Create `setup/migrate_YYYYMMDD_description.sql`: + +```sql +/** Migration: Add my_new_column to schedules + Run as cicada user on db_cicada database + Date: 2026-05-21 +**/ +START TRANSACTION; + +-- Add my_new_column to schedules if it doesn't exist +ALTER TABLE public.schedules +ADD COLUMN IF NOT EXISTS my_new_column VARCHAR(255) DEFAULT 'default_value'; + +-- If the migration adds a column with NOT NULL and no default, +-- backfill existing rows first: +-- UPDATE public.schedules SET my_new_column = 'backfill_value' +-- WHERE my_new_column IS NULL; + +COMMIT TRANSACTION; +``` + +**When to create a separate migration:** +- The change requires data backfilling +- The change is complex (e.g., adding constraints, creating indexes, altering types) +- You want an audit trail of when the migration was run +- The migration takes significant time (separate script = easier to monitor) + +**Naming convention:** `migrate_YYYYMMDD_short_description.sql` (e.g., `migrate_20260521_add_priority_column.sql`) + +### Step 3: Update Code to Handle Missing Columns + +In `cicada/lib/scheduler.py` and related modules, make code defensive: + +```python +# Instead of assuming the column exists: +# schedule = result['my_new_column'] # ❌ KeyError on old schema + +# Use getattr with defaults: +schedule = result.get('my_new_column', 'default_value') # ✅ Safe for old and new schema + +# Or use SQL COALESCE for consistent defaults: +SELECT + *, + COALESCE(my_new_column, 'default_value') as my_new_column +FROM schedules; +``` + +### Step 4: Update Tests + +Add fixtures that test **both old and new schema** versions: + +```python +# tests/conftest.py or relevant test file + +@pytest.fixture +def db_with_old_schema(pg_conn): + """Database with old schema (before migration)""" + # Run schema.sql without the new column + # Or mock a result without the column + return pg_conn + +@pytest.fixture +def db_with_new_schema(pg_conn): + """Database with new schema (after migration)""" + # Run full schema.sql with the new column + return pg_conn + +def test_code_handles_missing_column(db_with_old_schema): + # Verify code doesn't crash when column is missing + result = get_schedule_details(schedule_id, db=db_with_old_schema) + assert result['schedule_id'] == schedule_id + # Should not raise KeyError even though my_new_column is missing +``` + +## Pattern: Creating a New Table + +### Example: Adding `schedule_notifications` table + +#### Step 1: Update `schema.sql` + +```sql +-- Table: schedule_notifications +-- New table to track notification settings for schedules +CREATE TABLE IF NOT EXISTS public.schedule_notifications +( + notification_id SERIAL NOT NULL, + schedule_id VARCHAR(255) NOT NULL, + notify_on_failure SMALLINT NOT NULL DEFAULT 1, + notify_email VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT schedule_notifications_pkey PRIMARY KEY (notification_id), + CONSTRAINT schedule_notifications_schedule_fkey FOREIGN KEY (schedule_id) + REFERENCES public.schedules (schedule_id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH (OIDS=FALSE); + +CREATE INDEX IF NOT EXISTS schedule_notifications_schedule_id_idx + ON public.schedule_notifications + USING btree (schedule_id); +``` + +#### Step 2: Create Migration Script (Recommended) + +```sql +/** Migration: Add schedule_notifications table + Run as cicada user on db_cicada database + Date: 2026-05-21 +**/ +START TRANSACTION; + +CREATE TABLE IF NOT EXISTS public.schedule_notifications +( + notification_id SERIAL NOT NULL, + schedule_id VARCHAR(255) NOT NULL, + notify_on_failure SMALLINT NOT NULL DEFAULT 1, + notify_email VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT schedule_notifications_pkey PRIMARY KEY (notification_id), + CONSTRAINT schedule_notifications_schedule_fkey FOREIGN KEY (schedule_id) + REFERENCES public.schedules (schedule_id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH (OIDS=FALSE); + +CREATE INDEX IF NOT EXISTS schedule_notifications_schedule_id_idx + ON public.schedule_notifications + USING btree (schedule_id); + +COMMIT TRANSACTION; +``` + +## Pattern: Modifying an Existing Column + +### Example: Making `parameters` column larger + +#### Step 1: Update `schema.sql` + +Document the change in a comment: + +```sql +-- Modified 2026-05-21: Increased from VARCHAR(1024) to VARCHAR(4096) +parameters VARCHAR(4096), +``` + +#### Step 2: Create Migration Script + +```sql +/** Migration: Increase parameters column size + Run as cicada user on db_cicada database + Date: 2026-05-21 +**/ +START TRANSACTION; + +-- Alter the column type +ALTER TABLE public.schedules +ALTER COLUMN parameters TYPE VARCHAR(4096); + +COMMIT TRANSACTION; +``` + +**Note:** Expanding VARCHAR is always safe and fast in PostgreSQL. Contracting requires data validation first. + +## Pattern: Adding an Index + +### Example: Optimize queries on `interval_mask` + +#### Step 1: Update `schema.sql` + +```sql +-- Index: schedules_interval_mask_idx +CREATE INDEX IF NOT EXISTS schedules_interval_mask_idx + ON public.schedules + USING btree (interval_mask); +``` + +#### Step 2: Create Migration Script + +```sql +/** Migration: Add index on schedules.interval_mask + Run as cicada user on db_cicada database + Date: 2026-05-21 +**/ +START TRANSACTION; + +CREATE INDEX IF NOT EXISTS schedules_interval_mask_idx + ON public.schedules + USING btree (interval_mask); + +COMMIT TRANSACTION; +``` + +**Note:** Use `IF NOT EXISTS` so the migration is idempotent. + +## Deployment Workflow + +### For Fresh Installations +1. User runs `setup/schema.sql` → gets complete schema with all columns, tables, indexes + +### For Existing Installations +1. User deploys new code version +2. User applies migration scripts **in order** by date: + ```bash + psql -U cicada -d db_cicada -f setup/migrate_20260501_first_change.sql + psql -U cicada -d db_cicada -f setup/migrate_20260521_second_change.sql + ``` +3. Code continues running (defensive handling of optional columns/tables) + +### Deployment Documentation + +Include in your release notes: + +```markdown +## v2.0.0 - 2026-05-21 + +### Database Migrations Required + +Run migrations **in order**: +```bash +psql -U cicada -d db_cicada -f setup/migrate_20260501_new_feature.sql +psql -U cicada -d db_cicada -f setup/migrate_20260521_add_priority.sql +``` + +### Changes +- Added `priority` column to `schedules` table +- Added new `schedule_notifications` table for notification settings +- Code is backward-compatible; migrations are optional but recommended for full feature support + +### Rollback +If you encounter issues: +- Migrations can be reversed by dropping the new columns/tables (see comments in migration scripts) +- Older code versions will continue to work with the new schema (via defensive defaults) +``` + +## Testing Migrations Locally + +### Setup +```bash +# Create a test database +psql -U postgres -c "CREATE DATABASE db_cicada_test;" +psql -U postgres -d db_cicada_test -c "CREATE USER cicada WITH PASSWORD 'password';" +psql -U postgres -d db_cicada_test -c "GRANT ALL PRIVILEGES ON DATABASE db_cicada_test TO cicada;" +``` + +### Test Fresh Install +```bash +psql -U cicada -d db_cicada_test -f setup/schema.sql +``` + +### Test Migration Path +```bash +# Apply old schema, then run migrations +psql -U cicada -d db_cicada_test -f setup/schema.sql +psql -U cicada -d db_cicada_test -f setup/migrate_20260521_add_priority_column.sql + +# Verify the migration +psql -U cicada -d db_cicada_test -c "SELECT column_name FROM information_schema.columns + WHERE table_name='schedules' AND column_name='priority';" +``` + +## Best Practices + +1. **Always use `IF NOT EXISTS`** for idempotency + - `CREATE TABLE IF NOT EXISTS` + - `CREATE INDEX IF NOT EXISTS` + - `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` + +2. **Set sensible defaults** for new columns + - Avoid `NULL` unless necessary + - Use `DEFAULT` clause in schema + +3. **Wrap migrations in transactions** + ```sql + START TRANSACTION; + -- migration SQL + COMMIT TRANSACTION; + ``` + +4. **Make code defensive** + - Use `dict.get()` with defaults instead of direct key access + - Use SQL `COALESCE()` for optional columns in queries + - Catch exceptions gracefully if a table doesn't exist yet + +5. **Document changes** + - Add comments in `schema.sql` noting when columns were added/modified + - Include the date and reason in migration script headers + - Update `CLAUDE.md` if architectural changes occur + +6. **Test both paths** + - Verify fresh installs work + - Verify migrations work on existing schema + - Verify code handles missing columns gracefully + +## Rollback Guidance + +If a migration causes issues: + +### Reversing Column Additions +```sql +ALTER TABLE public.schedules +DROP COLUMN my_new_column; +``` + +### Reversing Table Creations +```sql +DROP TABLE IF EXISTS public.schedule_notifications CASCADE; +``` + +### Reversing Index Additions +```sql +DROP INDEX IF EXISTS public.schedule_notifications_schedule_id_idx; +``` + +Document these in comments within the migration file for quick reference. + +## References + +- See `setup/schema.sql` for the current complete schema +- See existing patterns like the `smart_interval_mask` column (added in line 96-98) +- See the `schedule_blocklist` table (added with `IF NOT EXISTS` pattern) diff --git a/setup/create_test_tap_setup.sql b/setup/create_test_tap_setup.sql new file mode 100644 index 0000000..d42bb88 --- /dev/null +++ b/setup/create_test_tap_setup.sql @@ -0,0 +1,524 @@ +-- Current state of schedules as of 31st Mar 2026 +START TRANSACTION; + +-- Add all servers +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (1, '4e538ad035e1', '4e538ad035e1', 'ip4_address_1', 1) ON CONFLICT (server_id) DO UPDATE SET is_enabled = 1; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (2, '4e538ad035e2', '4e538ad035e2', 'ip4_address_2', 0) ON CONFLICT DO NOTHING; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (3, '4e538ad035e3', '4e538ad035e3', 'ip4_address_3', 0) ON CONFLICT DO NOTHING; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (4, '4e538ad035e4', '4e538ad035e4', 'ip4_address_4', 0) ON CONFLICT DO NOTHING; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (5, '4e538ad035e5', '4e538ad035e5', 'ip4_address_5', 0) ON CONFLICT DO NOTHING; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (6, '4e538ad035e6', '4e538ad035e6', 'ip4_address_6', 0) ON CONFLICT DO NOTHING; +INSERT INTO public.servers (server_id, hostname, fqdn, ip4_address, is_enabled) VALUES (7, '4e538ad035e7', '4e538ad035e7', 'ip4_address_7', 0) ON CONFLICT DO NOTHING; + +COMMIT TRANSACTION; + + +START TRANSACTION; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'visit_log_based', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'mixpanel_chapter_screen', '0 12 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'verification_aggregate_collector', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'balance_valuation_snapshot', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'expose_iceberg_tables', '0 1 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'fraud_transfer_data_historical', '0 */1 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'payin_legacy_payments', '0 2 7 * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'atm_temp', '2/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_treasury_rate_gaming_rule_execution', '30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fraud_ratio', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'osquery_analytics', '2 */2 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'insight', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_rules_maintainer_ruleAttributesEntity', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_capability_stream_capability_outcome', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'amljira', '30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'snowflake_to_liquidity_planner_s3', '0 */12 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'payin_routing_full_table', '0 0 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_treasury_speed_model_predictions', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payment_intents', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'securityexecutor_reporting_sast', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'securityexecutor_reporting_ato', '12/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'liquidity', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'kafka_identityserviceemailverificationstatusupdated', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_swift_speed_model_predictions', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'aml_nms_full', '0 11 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'aggregated_public_uptime_metrics', '0 11 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'incident_bot', '0 15 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'terminate_mixpanel_taps', '0 19 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_fraud_event_log_1', '27 14 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'twcard_spend_control_persisted_values', '52 10 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mixpanel_payin_flow', '0 21 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'virality_mixpanel', '0 21 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'comparison_mixpanel', '0 22 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'archive_cicada_schedule_log', '1 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'findb', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'findb4', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mixpanel_product_usage', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mixpanel_twilio_ui', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'archive_pipelinewise_logs', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'verify_other_mixpanel_events', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_snowflake_static_objects_force', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_profileDeactivationCheck', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'mixpanel_critical_banner', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_roles_force', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'edgeauthorization', '0 */24 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'import_pipelinewise_config_force', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'feature', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'twilio_support', '0 22 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'DefectDojo', '0 0 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'purge_elt_cluster_tmp', '0 1 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'slack_users', '0 1 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'mixpanel_aml_handling', '0 1 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'mixpanel_kyc_handling', '0 1 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'archive_ap_tools_logs', '0 2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'findb2', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'swift_bic', '0 2 * * 2', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'lienzo', '0 2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'source_obf_views_force', '0 2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'blog', '0 2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_snowflake_service_accounts_force', '0 3 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'twcard_spend_control_hotlist_mongo', '0 21 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'mixpanel_assets', '0 3 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'appsec_vulns', '0 3 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'github_engineering_metrics_config', '0 2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_fraud_event_log_2', '27 20 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'receive_health_monitor', '0 6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'ci_build_metrics', '0 6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kafka_security_auditlog', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'findb5', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'profile_contact', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_ato_intelligence_oauth_session', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'group_access_management', '0 9 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'librabank', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'transfer2', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'workday', '0 1,2,4,6,10,22 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'price_decision', '15 */3 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'country_risk', '0 12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'mixpanel_verification_hub_visits', '0 12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mixpanel_duplicate_account', '0 12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'findb_full_table', '0 12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'plastic_full_table', '05 */12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'backup_states', '3 */6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'application', '0 */6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'salesforce', '0 */3 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'visit', '0 */2 * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'github_k8s_manifest', '0 */6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'aml_nms_sanctions_entries', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'aml_alert', '30 */4 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_schema_roles', '15 */3 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_countriesfeedback', '30 */2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_featurefeedback', '30 */2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fin_accounting_full_table', '0 12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'ados', '4 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'estimator2', '*/30 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mixpanel_compliance_tooling', '0 */12 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'arm', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'trxinventory', '0 */2 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'partner_hub', '0 */2 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'jira', '0 */2 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'mastercard_fraud_connector', '0 */2 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'safeguarding_timescaledb_hypertables', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'ddcase_fulltable', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'source_obf_views', '0 4-23,0-1 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'fx3', '45 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'compliance-identity', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'task', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fx', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'transfer', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'estimator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'visit_fulltable', '0 */2 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'fx2', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payin_core_large_tables', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'safeguarding_timescaledb', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payin_core_small_tables', '15 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payin_regional_tables', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fx4', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'zendesk_cshelp', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fraud_payin', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'contact_flows', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'SonarQube', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'inrgateway', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'data_obfuscation_service', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'bank_scraper', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'twcard_fraud_metadata', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'airflow_fraud_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'license', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'saved_payment_methods', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'sepa_inst_gateway_incoming', '*/45 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'twcard_fraud_risk', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'backoffice_auth', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fin_accounting', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'SonarQubeIncremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'pln_service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'smurfs_logical', '0 */1 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'prioritisation', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'account_platform', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'case_handling_db', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'fin_general_accounting_controls', '*/15 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'airflow_finance_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'ato_intelligence', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'risk_assurance', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'client_upsells', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'auto_top_up', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'airflow_platform_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'aedg', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'pull_payin', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'audit_request', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_travelhub', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'fin_business_event_archive', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'card_payin', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'aup', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'code_update', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'email_document_processor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'llm_prompt', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'identity_platform', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_twcard_fraudrisk_v2_assessment_results_lightweight', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fincrime_label', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'au_direct', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'fin_analytics', '*/15 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'partners_verification', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'verification_of_payee', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'balance_backoffice', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'partners_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'fin_da_control_findings', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'strong_customer_auth', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'regional_reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_dispute_form', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'pricing_tax', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'account_plans', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payment_data_extractor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'request_to_pay', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'regional_alert_orchestrator_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'identify_face_match', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'identity', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_spend_control_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'case_routing_db', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'transfer-repair', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'spinnaker_clouddriver', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mea_payin', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'kafka_pnccancellation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fin_issue', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_hat_svc_profile', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_activityattachmentadded', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'partner_key_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'compensation_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'europe_partners', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'data_inventory', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'swift_routing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_mitigatoruploadedfilecontext', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'jp_direct', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kafka_ato_intelligence_transfer_session', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'kafka_identityservice_emailcheckpointdetailsupdated', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_address_validationmetadata', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'inrgateway_clear', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'spinnaker_fiat', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_activityfeestatementdownloaded', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'airflow_people_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'edgeauthorization_log_based', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'ph_direct', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'link_state', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'pisp', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'stargate', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'pix', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'airflow_verification_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'compliance_reporting', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'watson_service2', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fraud_trigger', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'send_experience', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'airflow_crm_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'airflow_support_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'airflow_marketing_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'discount', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'payops_process_orchestrator_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_servicing_reconciliation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'investments_tax_report', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'wise_security_intelligence', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'npp_overlay', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'compensation_orchestrator_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'airflow_internal_audit_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'unstructured_document_evaluation', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'br_direct', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'kafka_nbb_profileremediation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'payin_deposit_details', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'twcard_transaction_notifier', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'qatf_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'airflow_core_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'mitigation_requirements_db', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_mitigatoranalyticskyc', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'fin_da_open_balance_problem', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_provisioning_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'airflow_fincrime_platform_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_virtual_agent', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kycpostprocessor_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'payops_process_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'payment_request', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'airflow_aml_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'liquidity_planner', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'wise_card_program', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'airflow_product_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_twcard_virtual_agent_assessment_results_lightweight', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'process_engine', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'route_availability', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'transfer_requirements_processor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'quote_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'pricing_volume_aggregator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'depositaccount', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'address', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'fin_da_pds_open_balance', '*/30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'regional_batch_tooling', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_verificationfileuploaded', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'bankruptcy', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'pix_dict', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'twcard_reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'airflow_treasury_k8s', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'notifications_logbased', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'kafka_accountownershipproofanalyticsrequests', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_swift_fee_model_predictions', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'kafka_treasury_cancellation_model_predictions', '*/15 * * * *', 'dummy_command', 0, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'aggregated_prometheus_metrics', '0 11 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'victorops_scraper', '0 11 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'equiniti', '0 11,19 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'superset', '0 */6 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'nbb_reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'fraud_profile_incremental', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'mixpanel_send', '0 10 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'slack_ap', '0 */6 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'cashback', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_feedbackupdated', '30 */2 * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'comparison', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'sechub_reports', '0 */2 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'smurfs', '0 */1 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_settlement', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'bank_statements', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'reconciliation', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'account_details2', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_tokenisation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_roles', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'import_pipelinewise_config', '5 2-23 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'fraud_profile', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'scam_prevention', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'amazon_pspp_reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'aml_nms', '*/15 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_snowflake_static_objects', '*/30 3-23 * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'rate_historical', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'order_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'platon_analytics_docsquality', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'bill_payment', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'investment_broker', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'atm_log_based', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'approval', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'twcard_advice', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'rate_connector_elektron', '*/30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'market_connector', '*/30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'rate_live', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'wishmaster', '*/30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'aml_new', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'guided_help', '30 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'global_currencies_integrations', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_insightsfeedback', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'verification_status_reporting', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'accrual', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'liquidity_planner_incremental', '0 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'profile', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'openbanking_gateway', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'pricing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'tax_registry', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'spinnaker_front50', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'quote', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'fin_qa_cash_reconciliation', '*/15 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'sepa_inst_gateway', '*/45 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'balance_investments', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'billing-engine', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'bin', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'verificationevidence', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_transaction', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'balance', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'fraud_device_fingerprinting', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'mm_rule_executor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'twcard_spend_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'regional_alert_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kycpostprocessor', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'netbox', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'platform-invoicing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'cicadascheduler', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'id_document_evaluation_ml_model_prediction', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'dormant_account', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'aml_new_aml_risk_mi_risk', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'aml_bot', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'balance_interest', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'fincrime_tagging', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'associateduser', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'ddcase_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'compliance_remediation_tool', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'payin_linker', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'security-consent', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'refresh_cycle', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'public_profile', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'borderless_dd', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'wise_github_bot', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'product_usage_controls_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'latam-verification-service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'bank_details_order', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_recovery_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'contactdb', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_recovery', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'aml_new_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'refresh_cycle_log_based', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'verification_work', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'support_analytics', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'pep_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'create_snowflake_service_accounts', '8/20 * * * *', 'dummy_command', 1, 0) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_order_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'notifications', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'sanction', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'account_details', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'evidence_attributes_service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'ach', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'achrisk', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'business_verification_details_collection', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'bank_details_payment', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'payment_document_review', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'acquiring_risk', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'auto_transfer', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'payinavail', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'originator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'edd_log_based', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'twcard_3ds', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'statementsdb_incremental', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'batchpayments', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'safeguarding_new', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kafka_npssurveyresultevents', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_identity_controls_attribute_updated_event', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'rate_alerts', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'facetec_sdk', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'plastic', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'workflow', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'terms', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'interac', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_provisioning', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'id_document_processor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'business_incorporation_backend', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'rate_trigger', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'citi', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'profile_identifier', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'spinnaker_orca', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'send-order', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'twcard_transaction_cashback', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'minibank', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'octopus', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_case_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'virality', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'speedcommunication', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'thirdpartypayment', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'statementsdb', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'payout_linker', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'kafka_iddocumentevaluationevents', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'manual_script_runner', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'kafka_twcard_merchant_incorrect_information_feedback', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'user_security', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kyc_liveness', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'manual_authorisation', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'twcard_balance_adjustment', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'twcard_authorisation', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'connected_accounts', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'brltrxreport', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'twcard_merchant', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fraud_ops_bot', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'kafka_capability_hub_internal_capability_outcome', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'twcard_claim_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'confirmation-of-payee', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_order', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'twcard_clearing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'virtual_agent', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'manual-linker', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'verification_ml_data_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_verificationrefundedtransfers', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'dynamic_pricing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'looker', '* * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'consumer_onboarding', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'kafka_grace_period_updated_event', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'japan-verification-service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'partners_settlement_funding', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'partners-settlement', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'feature_charge_orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'wise_account_checkout', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'payout_coordinator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'recipient_claim', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'northam_payout', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'kafka_mitigatorverificationanalytics', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'bank_details', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'giro', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'pacific_verification', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'invite', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'dispute', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'webhook_sender', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'bacs', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'publictransfer', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'directdebit', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'kafka_batch_operations_operation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'subject_consent', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'northam_verification', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'inr-orchestrator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'partner_support', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'latam_partner_integrations', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'census', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'boauthor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'pay_like_a_local', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'northam_bc', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'ddcase', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'securityexecutor_reporting_host', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'securityexecutor_reporting_sca', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'plastic_incremental', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'faq_db', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'swift_connectivity', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'bank_feed_sync', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'transaction_control', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'pep', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'safeguarding', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'gbg', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'approved_sender_accounts', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'payment_preferences', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'ext_marketing_data_connector', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'acquiring_payment', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'routing_engine', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'direct_fast', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'aml_service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'trusted-beneficiary', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'indonesia_reporting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'payin_routing', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'austrac', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'linking', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'southeast_asia_verification', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'rolegroup_management', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'referral_fraud', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'edd', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'bill_splitting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'pacific_direct_debit', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'selfservice', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'greater-china', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'payout', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fin_qa_controls', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'checkout', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'rfi_service', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'ausregionaldatareplicator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'voting', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (3, 'conversion_order', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'kafka_treasury_rule_execution_result', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'kafka_twcard_fraudrisk_rbaassessmentresults', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'documentreview', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'noc', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'moneytracker', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'fraud_state', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'galileo', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'swift_processor', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (1, 'twcard_audit', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'manual_verification_state', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'sms', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (5, 'twcard_reconciliation', '*/15 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (7, 'referral_aggregator', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (2, 'business_legitimacy_verification', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (4, 'fx_audit_log', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO public.schedules (SERVER_ID, SCHEDULE_ID, INTERVAL_MASK, EXEC_COMMAND, IS_ENABLED, IS_RUNNING) VALUES (6, 'cumulative_limit', '*/30 * * * *', 'dummy_command', 1, 1) ON CONFLICT DO NOTHING; + + +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('plastic_full_table', 'test for blocklist functionality') ON CONFLICT DO NOTHING; + +COMMIT TRANSACTION; diff --git a/setup/schema.sql b/setup/schema.sql index ffa4360..67b5bc9 100644 --- a/setup/schema.sql +++ b/setup/schema.sql @@ -19,6 +19,7 @@ BEGIN END; $BODY$; + -- Table: servers CREATE TABLE IF NOT EXISTS public.servers ( @@ -60,6 +61,7 @@ CREATE TABLE IF NOT EXISTS public.schedules is_running smallint NOT NULL DEFAULT 0, abort_running smallint NOT NULL DEFAULT 0, interval_mask character varying(32) NOT NULL, + smart_interval_mask character varying(32), first_run_date timestamp(3) without time zone NOT NULL DEFAULT '1000-01-01 00:00:00.000'::timestamp without time zone, last_run_date timestamp(3) without time zone NOT NULL DEFAULT '9999-12-31 23:59:59.999'::timestamp without time zone, exec_command character varying NOT NULL, @@ -91,6 +93,10 @@ COMMENT ON COLUMN schedules.parameters IS 'Exact string of parameters for comman COMMENT ON COLUMN schedules.adhoc_parameters IS 'If specified, will overwrite parameters for next run'; COMMENT ON COLUMN schedules.schedule_group_id IS 'Optional field to help group schedules'; +-- Add smart_interval_mask column if not exists (for existing installations upgrading to a version with smart scheduling) +ALTER TABLE public.schedules +ADD COLUMN IF NOT EXISTS smart_interval_mask character varying(32); + -- Index: schedules_adhoc_execute_idx CREATE INDEX IF NOT EXISTS schedules_adhoc_execute_idx ON public.schedules @@ -188,4 +194,76 @@ WITH ( ) ; + +-- Snapshots table stores metadata about each snapshot (timestamp, operation type) +CREATE TABLE IF NOT EXISTS public.snapshots +( + snapshot_id serial NOT NULL, + snapshot_timestamp timestamp without time zone NOT NULL DEFAULT (now())::timestamp without time zone, + server_id integer, + computed_usage character varying(255), + reason character varying(255), + CONSTRAINT snapshots_pkey PRIMARY KEY (snapshot_id) +) +WITH ( + OIDS=FALSE +); + +-- Table to store schedule snapshots for rollback +-- Keeps last 3 snapshots per schedule_id for rollback and audit trail +CREATE TABLE IF NOT EXISTS public.schedule_backups +( + schedule_id character varying(255) NOT NULL, + server_id integer NOT NULL, + interval_mask character varying(32) NOT NULL, + smart_interval_mask character varying(32), + snapshot_id integer NOT NULL, + CONSTRAINT schedule_backups_pkey PRIMARY KEY (schedule_id, snapshot_id), + CONSTRAINT schedule_backups_snapshot_fkey FOREIGN KEY (snapshot_id) + REFERENCES snapshots (snapshot_id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + + + + +CREATE TABLE IF NOT EXISTS public.schedule_blocklist +( + schedule_id character varying(255) NOT NULL, + timestamp timestamp without time zone NOT NULL DEFAULT (now())::timestamp without time zone, + reason character varying(255), + CONSTRAINT schedule_blocklist_pkey PRIMARY KEY (schedule_id) +) +WITH ( + OIDS=FALSE +); + +-- Add in cicada schedules used for running (admin schedules) +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('source_obf_views_force', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('source_obf_views', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_snowflake_static_objects_force', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_snowflake_static_objects', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_schema_roles', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_snowflake_service_accounts_force', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_snowflake_service_accounts', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_roles', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('create_roles_force', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('import_pipelinewise_config_force', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('import_pipelinewise_config', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('archive_ap_tools_logs', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('archive_cicada_schedule_log', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('archive_pipelinewise_logs', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('expose_iceberg_tables', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('terminate_mixpanel_taps', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('backup_states', 'Admin Schedules') ON CONFLICT DO NOTHING; +INSERT INTO public.schedule_blocklist (SCHEDULE_ID, REASON) VALUES ('purge_elt_cluster_tmp', 'Admin Schedules') ON CONFLICT DO NOTHING; + + + + + + COMMIT TRANSACTION; diff --git a/tests/test_functional_cli_entrypoint.py b/tests/test_functional_cli_entrypoint.py index 82147b8..d8f5fae 100644 --- a/tests/test_functional_cli_entrypoint.py +++ b/tests/test_functional_cli_entrypoint.py @@ -21,9 +21,9 @@ def test_cicada_help(): positional arguments: command register_server , list_server_schedules , exec_server_schedules - , show_schedule , upsert_schedule , exec_schedule , - spread_schedules , archive_schedule_log , ping_slack , - list_schedule_ids , delete_schedule , version + , smart_schedule , show_schedule , upsert_schedule , + exec_schedule , spread_schedules , archive_schedule_log , + ping_slack , list_schedule_ids , delete_schedule , version optional arguments: -h, --help show this help message and exit @@ -41,9 +41,9 @@ def test_bad_command(): positional arguments: command register_server , list_server_schedules , exec_server_schedules - , show_schedule , upsert_schedule , exec_schedule , - spread_schedules , archive_schedule_log , ping_slack , - list_schedule_ids , delete_schedule , version + , smart_schedule , show_schedule , upsert_schedule , + exec_schedule , spread_schedules , archive_schedule_log , + ping_slack , list_schedule_ids , delete_schedule , version optional arguments: -h, --help show this help message and exit @@ -331,3 +331,70 @@ def test_list_schedule_ids(): -h, --help show this help message and exit """ assert actual == expected + + + +def test_smart_schedule_help(): + """test_smart_schedule_help""" + actual = subprocess.run(["cicada", "smart_schedule", "-h"], check=True, stdout=subprocess.PIPE).stdout.decode("utf-8") + + assert "optimise" in actual.lower() + assert "rollback" in actual.lower() + assert "blocklist" in actual.lower() + + +def test_smart_schedule_optimise_help(): + """test_smart_schedule optimise subcommand help""" + actual = subprocess.run(["cicada", "smart_schedule", "optimise", "-h"], check=True, stdout=subprocess.PIPE).stdout.decode("utf-8") + expected_snippet = """usage: smart_schedule optimise [-h] [--server_id SERVER_ID]""" + + assert expected_snippet in actual + + +def test_smart_schedule_rollback_help(): + """test_smart_schedule rollback subcommand help""" + actual = subprocess.run(["cicada", "smart_schedule", "rollback", "-h"], check=True, stdout=subprocess.PIPE).stdout.decode( + "utf-8" + ) + + expected_snippet = """usage: smart_schedule [-h] (--full | --previous)""" + assert expected_snippet in actual + +def test_smart_schedule_rollback_missing_flags(): + """test_smart_schedule rollback requires either --full or --previous""" + actual = subprocess.run(["cicada", "smart_schedule", "rollback"], check=False, stderr=subprocess.PIPE).stderr.decode("utf-8") + expected_snippet = """error: one of the arguments --full --previous is required""" + + assert expected_snippet in actual + + +def test_smart_schedule_rollback_mutually_exclusive(): + """test_smart_schedule rollback --full and --previous are mutually exclusive""" + actual = subprocess.run( + ["cicada", "smart_schedule", "rollback", "--full", "--previous", "--server_id", "1"], + check=False, + stderr=subprocess.PIPE + ).stderr.decode("utf-8") + expected_snippet = "smart_schedule: error: argument --previous: not allowed with argument --full" + + assert expected_snippet in actual + + +def test_smart_schedule_blocklist_help(): + """test_smart_schedule blocklist subcommand help""" + actual = subprocess.run(["cicada", "smart_schedule", "blocklist", "-h"], check=True, stdout=subprocess.PIPE).stdout.decode( + "utf-8" + ) + + assert "--schedule_id SCHEDULE_ID" in actual + assert "--remove" in actual + + +def test_smart_schedule_blocklist_missing_schedule_id(): + """test_smart_schedule blocklist requires --schedule_id""" + actual = subprocess.run(["cicada", "smart_schedule", "blocklist"], check=False, stderr=subprocess.PIPE).stderr.decode("utf-8") + + expected_snippet = """error: the following arguments are required: --schedule_id""" + assert expected_snippet in actual + + diff --git a/tests/test_smart_scheduling.py b/tests/test_smart_scheduling.py new file mode 100644 index 0000000..d365c0a --- /dev/null +++ b/tests/test_smart_scheduling.py @@ -0,0 +1,1560 @@ +"""Tests for smart scheduling and rollback functionality""" + +import croniter +import pytest +import os +import datetime +from unittest.mock import Mock, MagicMock, patch, call +import numpy as np +import psycopg2 + +from cicada.lib.smart_scheduling.domain import Schedule +from cicada.lib.smart_scheduling.config import GAConfig +from cicada.lib.smart_scheduling.evaluation import evaluate_usage_and_peak +import cicada.commands.smart_schedule as smart_schedule +from cicada.lib.smart_scheduling.ga_pygad import GAPyGADScheduler +from cicada.lib import scheduler + + +@pytest.fixture(scope="session", autouse=True) +def get_env_vars(): + """get_env_vars""" + + pytest.cicada_home = os.environ.get("CICADA_HOME") + + pytest.db_host = os.environ.get("DB_POSTGRES_HOST") + pytest.db_port = os.environ.get("DB_POSTGRES_PORT") + pytest.db_user = os.environ.get("DB_POSTGRES_USER") + pytest.db_pass = os.environ.get("DB_POSTGRES_PASS") + + +@pytest.fixture() +def db_setup(get_env_vars): + """db_setup""" + pytest.db_test = f"pytest_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S_%f')}" + + # Create the test_db + pg_conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database="postgres", + ) + pg_conn.autocommit = True + pg_cur = pg_conn.cursor() + pg_cur.execute(f"CREATE DATABASE {pytest.db_test}") + pg_cur.close() + pg_conn.close() + + # Create test_db structure + test_conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database=pytest.db_test, + ) + test_conn.autocommit = True + test_cur = test_conn.cursor() + test_cur.execute(open(f"{pytest.cicada_home}/setup/schema.sql", "r", encoding="utf-8").read()) + test_cur.close() + test_conn.close() + + yield + + # Cleanup: terminate all connections and drop test database + pg_conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database="postgres", + ) + pg_conn.autocommit = True + pg_cur = pg_conn.cursor() + # Terminate all connections to the test database + pg_cur.execute( + f""" + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{pytest.db_test}' + AND pid <> pg_backend_pid() + """ + ) + pg_cur.close() + pg_conn.close() + + # Now drop the database + pg_conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database="postgres", + ) + pg_conn.autocommit = True + pg_cur = pg_conn.cursor() + pg_cur.execute(f"DROP DATABASE {pytest.db_test}") + pg_cur.close() + pg_conn.close() + + +def query_test_db(query): + """Run a SQL query in a postgres database""" + rows = [] + conn = None + try: + conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database=pytest.db_test, + ) + conn.set_session(readonly=False, autocommit=True) + + cur = conn.cursor() + cur.execute(query) + + if cur.rowcount > 0 and cur.description: + rows = cur.fetchall() + + cur.close() + finally: + if conn: + conn.close() + return rows + + +def get_db_cursor(): + """Get a cursor to the test database""" + conn = psycopg2.connect( + host=pytest.db_host, + port=pytest.db_port, + user=pytest.db_user, + password=pytest.db_pass, + database=pytest.db_test, + ) + conn.set_session(readonly=False, autocommit=True) + return conn, conn.cursor() + + +class TestEvaluateUsageAndPeak: + """Tests for evaluate_usage_and_peak function""" + + def test_evaluate_single_schedule_no_overlap(self, db_setup): + """Test usage evaluation with a single schedule that doesn't overlap""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": 1, + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + start_blocks = [0] + usage, peak = evaluate_usage_and_peak(start_blocks, [test_schedule]) + + assert usage.shape == (1440,) + assert peak == 1 + for i in range(24): + mins = i * 60 + assert (usage[mins : mins + 5] == 1).all() + assert (usage[mins + 5 : (i + 1) * 60] == 0).all() + finally: + db_cur.close() + db_conn.close() + + def test_evaluate_multiple_schedules_no_overlap(self, db_setup): + """Test evaluation with multiple schedules that don't overlap""" + db_conn, db_cur = get_db_cursor() + try: + schedule1_details = { + "schedule_id": 1, + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + schedule1 = Schedule( + schedule_id=schedule1_details['schedule_id'], + server_id=schedule1_details['server_id'], + interval_mask=schedule1_details['interval_mask'], + smart_interval_mask=schedule1_details.get('smart_interval_mask'), + blocklisted=schedule1_details.get('blocklisted'), + db_cur=db_cur + ) + schedule1.frequency_minutes = 60 + schedule1.median_runtime_minutes = 5 + + schedule2_details = { + "schedule_id": 2, + "server_id": 1, + "interval_mask": "30 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + schedule2 = Schedule( + schedule_id=schedule2_details['schedule_id'], + server_id=schedule2_details['server_id'], + interval_mask=schedule2_details['interval_mask'], + smart_interval_mask=schedule2_details.get('smart_interval_mask'), + blocklisted=schedule2_details.get('blocklisted'), + db_cur=db_cur + ) + schedule2.frequency_minutes = 60 + schedule2.median_runtime_minutes = 5 + + start_blocks = [0, 30] + usage, peak = evaluate_usage_and_peak(start_blocks, [schedule1, schedule2]) + + assert (usage[0:5] == 1).all() + assert (usage[6:30] == 0.0).all() + assert (usage[30:35] == 1).all() + assert (usage[35:60] == 0.0).all() + assert peak == 1 + finally: + db_cur.close() + db_conn.close() + + def test_evaluate_overlapping_schedules(self, db_setup): + """Test evaluation with overlapping schedules""" + db_conn, db_cur = get_db_cursor() + try: + schedule1_details = { + "schedule_id": 1, + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + schedule1 = Schedule( + schedule_id=schedule1_details['schedule_id'], + server_id=schedule1_details['server_id'], + interval_mask=schedule1_details['interval_mask'], + smart_interval_mask=schedule1_details.get('smart_interval_mask'), + blocklisted=schedule1_details.get('blocklisted'), + db_cur=db_cur + ) + schedule1.frequency_minutes = 60 + schedule1.median_runtime_minutes = 10 + + schedule2_details = { + "schedule_id": 2, + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + schedule2 = Schedule( + schedule_id=schedule2_details['schedule_id'], + server_id=schedule2_details['server_id'], + interval_mask=schedule2_details['interval_mask'], + smart_interval_mask=schedule2_details.get('smart_interval_mask'), + blocklisted=schedule2_details.get('blocklisted'), + db_cur=db_cur + ) + schedule2.frequency_minutes = 60 + schedule2.median_runtime_minutes = 5 + + start_blocks = [0, 0] + usage, peak = evaluate_usage_and_peak(start_blocks, [schedule1, schedule2]) + + assert peak == 2 + assert usage[0] == 2 + assert usage[5] == 1 + finally: + db_cur.close() + db_conn.close() + + def test_evaluate_wrapping_around_day(self, db_setup): + """Test that schedules wrapping around midnight work correctly""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": 1, + "server_id": 1, + "interval_mask": "0 0 * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + test_schedule.frequency_minutes = 60 + test_schedule.median_runtime_minutes = 5 + start_blocks = [1430] # (1430 mins = 23:50) + + # Should throw an assertion error that the start block is too late for the frequency of the schedule + with pytest.raises(ValueError, match=r"Start time should be the earliest it can be for schedule: .* with start time 1430 exceeds frequency 60"): + evaluate_usage_and_peak(start_blocks, [test_schedule]) + finally: + db_cur.close() + db_conn.close() + + + +class TestScheduleDomain: + """Tests for Schedule domain object""" + + def test_schedule_initialization(self, db_setup): + """Test Schedule object initialization""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 5, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.schedule_id == "test-id-1" + assert test_schedule.server_id == 5 + assert test_schedule.interval_mask == "0 * * * *" + assert test_schedule.shifted == False + assert test_schedule.start_time_mins == 0 + assert test_schedule.median_runtime_minutes == 5 + finally: + db_cur.close() + db_conn.close() + + + def test_schedule_dataclass_fields_initialized(self, db_setup): + """Test that all dataclass fields are properly initialized, including defaults""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 5, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + # Verify all fields exist and have correct default values + assert hasattr(test_schedule, 'shifted') + assert hasattr(test_schedule, 'median_runtime_minutes') + assert hasattr(test_schedule, 'start_time_mins') + assert hasattr(test_schedule, 'blocklisted') + assert hasattr(test_schedule, 'frequency_minutes') + + # Verify default values + assert test_schedule.shifted is False + assert test_schedule.median_runtime_minutes == 5 # Will be updated by _get_average_runtime + assert test_schedule.start_time_mins == 0 + assert test_schedule.blocklisted is False + + # Accessing any of these should NOT raise AttributeError + try: + _ = test_schedule.shifted + _ = test_schedule.median_runtime_minutes + _ = test_schedule.start_time_mins + _ = test_schedule.blocklisted + except AttributeError as e: + pytest.fail(f"AttributeError raised when accessing dataclass field: {e}") + finally: + db_cur.close() + db_conn.close() + + def test_schedule_frequency_hourly(self, db_setup): + """Test frequency determination for hourly cron""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 * * * *", # Every hour + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.frequency_minutes == 60 + finally: + db_cur.close() + db_conn.close() + + def test_schedule_frequency_daily(self, db_setup): + """Test frequency determination for daily cron""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 0 * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.frequency_minutes == 1440 + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_unsupported_irregular_cron(self, db_setup): + """Test that schedules with irregular cron expressions are marked as unsupported""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0-15 */9 * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.is_unsupported() + assert not test_schedule.frequency_is_supported() + assert not test_schedule.is_regular_schedule() + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_unsupported_low_frequency(self, db_setup): + """Test that schedules with unsupported low frequencies are marked as unsupported""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 0 * * 0", # Weekly + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.is_unsupported() + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_regular_schedule_hourly(self, db_setup): + """Test that hourly schedules are recognized as regular""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.is_regular_schedule() + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_regular_schedule_every_15_mins(self, db_setup): + """Test that every-15-minute schedules are recognized as regular""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "*/15 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.is_regular_schedule() + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_regular_schedule_daily(self, db_setup): + """Test that daily schedules are recognized as regular""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 0 * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert test_schedule.is_regular_schedule() + finally: + db_cur.close() + db_conn.close() + + def test_schedule_45_min_schedule_is_supported(self, db_setup): + """Test that 45-minute frequency schedules are recognized as supported""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "*/45 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert not test_schedule.is_unsupported() + # Fails due to cronitor issue -> means any */45 gets missed out of the smart scheduling + finally: + db_cur.close() + db_conn.close() + + def test_schedule_is_irregular_schedule_weekdays(self, db_setup): + """Test that weekday-only schedules are marked as irregular""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-id-1", + "server_id": 1, + "interval_mask": "0 9 * * 1-5", # Weekdays only + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + + assert not test_schedule.is_regular_schedule() + finally: + db_cur.close() + db_conn.close() + + +class TestGAPyGADScheduler: + """Tests for GAPyGADScheduler""" + + def test_custom_config(self): + """Test GAConfig with custom values""" + config = GAConfig( + num_generations=50, + sol_per_pop=100, + random_seed=42, + ) + assert config.num_generations == 50 + assert config.sol_per_pop == 100 + assert config.random_seed == 42 + assert config.num_parents_mating == 10 + assert config.mutation_percent_genes == 20 + assert config.parent_selection_type == "rank" + assert config.crossover_type == "uniform" + assert config.mutation_type == "random" + + def test_scheduler_uses_default_config_when_optional_config_is_missing(self): + ga_scheduler = GAPyGADScheduler() + + assert ga_scheduler.cfg == GAConfig() + assert ga_scheduler.cfg.num_generations == 20 + + def test_scheduler_initialization_custom_config(self): + """Test scheduler initialization with custom config""" + config = {"num_generations": 30} + ga_scheduler = GAPyGADScheduler(config) + + assert ga_scheduler.cfg.num_generations == 30 + + def test_scheduler_initialization_filters_none_values(self): + """Test that None values are filtered out when initializing config""" + config = {"num_generations": None} + ga_scheduler = GAPyGADScheduler(config) + + assert ga_scheduler.cfg.num_generations == 20 + + +class TestSchedulerDatabaseFunctions: + """Tests for scheduler database functions (rollback/snapshot)""" + + def test_get_blocklisted_schedule_ids_empty(self, db_setup): + """Test retrieving blocklisted schedule IDs when none exist""" + db_conn, db_cur = get_db_cursor() + try: + # Initially should have the 18 admin schedules + result = scheduler.get_blocklisted_schedule_ids(db_cur) + assert len(result) == 18 + finally: + db_cur.close() + db_conn.close() + + def test_blocklist_schedule(self, db_setup): + """Test blocklisting a schedule""" + db_conn, db_cur = get_db_cursor() + try: + # First register a server and create a schedule + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'G')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, exec_command) + VALUES ('test-sched-1', 1, '0 * * * *', 'echo test')""" + ) + + # Blocklist the schedule + scheduler.blocklist_schedule(db_cur, "test-sched-1", reason="Testing") + + # Verify it's blocklisted + result = scheduler.get_blocklisted_schedule_ids(db_cur) + assert len(result) >= 1 + assert "test-sched-1" in result + finally: + db_cur.close() + db_conn.close() + + def test_snapshot_schedules_basic(self, db_setup): + """Test snapshotting schedules""" + db_conn, db_cur = get_db_cursor() + query_test_db("DELETE FROM schedules") + + try: + # Register a server and create schedules + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'G')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-sched-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + + # Snapshot the schedule + scheduler.snapshot_schedules(db_cur, server_id=1, reason="Test optimization") + + # Verify snapshot was created + snapshots = query_test_db("SELECT snapshot_id FROM snapshots WHERE reason = 'Test optimization'") + assert len(snapshots) > 0 + finally: + db_cur.close() + db_conn.close() + + def test_full_rollback_with_schedule_id(self, db_setup): + """Test full rollback for a specific schedule""" + db_conn, db_cur = get_db_cursor() + try: + # Register a server and create a schedule with smart_interval_mask set + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'B')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-sched-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + + # Perform full rollback + scheduler.full_rollback(db_cur, schedule_id="test-sched-1") + + # Verify smart_interval_mask is set to NULL + result = query_test_db( + "SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'test-sched-1'" + ) + assert result[0][0] is None + finally: + db_cur.close() + db_conn.close() + + def test_restore_previous_schedules_requires_snapshot_id(self): + """Test that restore_previous_schedules requires snapshot_id""" + db_cur = Mock() + + with pytest.raises(TypeError): + scheduler.restore_previous_schedules(db_cur) + + +class TestEndToEndSmartScheduling: + """Integration tests for end-to-end smart scheduling workflow""" + + def test_create_schedules_from_details(self, db_setup): + """Test creating multiple Schedule objects from details""" + db_conn, db_cur = get_db_cursor() + try: + schedules_data = [ + { + "schedule_id": "sched-1", + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + }, + { + "schedule_id": "sched-2", + "server_id": 1, + "interval_mask": "*/30 * * * *", + "smart_interval_mask": None, + "blocklisted": False + }, + ] + + schedules = [Schedule( + schedule_id=data['schedule_id'], + server_id=data['server_id'], + interval_mask=data['interval_mask'], + smart_interval_mask=data.get('smart_interval_mask'), + blocklisted=data.get('blocklisted'), + db_cur=db_cur + ) for data in schedules_data] + + assert len(schedules) == 2 + assert schedules[0].schedule_id == "sched-1" + assert schedules[1].schedule_id == "sched-2" + finally: + db_cur.close() + db_conn.close() + + def test_snapshot_schedules(self, db_setup): + """Test the snapshot_schedules function""" + db_conn, db_cur = get_db_cursor() + try: + 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, smart_interval_mask, exec_command) + VALUES ('sched-1', 1, '0 * * * *', '0 * * * *', 'echo test')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('sched-2', 1, '*/30 * * * *', '*/30 * * * *', 'echo test')""" + ) + + scheduler.snapshot_schedules(db_cur, server_id=1, reason="Test optimization") + + # Verify that snapshots were created + snapshots = query_test_db("SELECT snapshot_id FROM snapshots WHERE reason = 'Test optimization'") + assert len(snapshots) > 0 + finally: + db_cur.close() + db_conn.close() + + def test_retrieve_snapshots(self, db_setup): + """Test retrieving schedule snapshots""" + db_conn, db_cur = get_db_cursor() + try: + 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, smart_interval_mask, exec_command) + VALUES ('test-sched', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + query_test_db( + """INSERT INTO snapshots (snapshot_id, server_id, reason, snapshot_timestamp) + VALUES (1, 1, 'GA optimization', NOW())""" + ) + + snapshots = scheduler.retrieve_snapshots(db_cur, 1) + + assert len(snapshots) > 0 + finally: + db_cur.close() + db_conn.close() + + def test_multiple_overlapping_schedules_evaluation(self, db_setup): + """Test evaluating usage for multiple overlapping schedules""" + db_conn, db_cur = get_db_cursor() + try: + # Create 3 schedules with different patterns + schedules = [] + for i in range(3): + schedule_data = { + "schedule_id": f"sched-{i}", + "server_id": 1, + "interval_mask": "0 * * * *" if i == 0 else f"*/{15 * (i + 1)} * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_data['schedule_id'], + server_id=schedule_data['server_id'], + interval_mask=schedule_data['interval_mask'], + smart_interval_mask=schedule_data.get('smart_interval_mask'), + blocklisted=schedule_data.get('blocklisted'), + db_cur=db_cur + ) + test_schedule.frequency_minutes = 60 + test_schedule.median_runtime_minutes = 5 + schedules.append(test_schedule) + + # Stagger start times to create overlaps + start_blocks = [0, 10, 20] + usage, peak = evaluate_usage_and_peak(start_blocks, schedules) + + assert peak > 0.3 # Should have some overlapping usage + assert usage.shape == (1440,) + finally: + db_cur.close() + db_conn.close() + + +class TestSmartSchedulingCommand: + """Tests for the smart scheduling command""" + + def test_smart_scheduling_frequency_unchanged_hourly_schedule(self, db_setup): + """Test that the frequency of the schedule remains unchanged after smart scheduling""" + db_conn, db_cur = get_db_cursor() + try: + hourly_schedule_details = { + "schedule_id": "test-schedule-1", + "server_id": 1, + "interval_mask": "0 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + hourly_schedule = Schedule( + schedule_id=hourly_schedule_details['schedule_id'], + server_id=hourly_schedule_details['server_id'], + interval_mask=hourly_schedule_details['interval_mask'], + smart_interval_mask=hourly_schedule_details.get('smart_interval_mask'), + blocklisted=hourly_schedule_details.get('blocklisted'), + db_cur=db_cur + ) + hourly_schedule.shifted = True + hourly_schedule.start_time_mins = 15 + + smart_schedule._update_schedule_cron(hourly_schedule) + assert hourly_schedule.smart_interval_mask == "15 * * * *" + assert hourly_schedule.interval_mask == "0 * * * *" + assert hourly_schedule.frequency_minutes == 60 + + hourly_schedule.determine_attributes(db_cur) + assert hourly_schedule.is_regular_schedule() + assert hourly_schedule.frequency_minutes == 60 + finally: + db_cur.close() + db_conn.close() + + def test_smart_scheduling_frequency_unchanged_fifteen_min_schedule(self, db_setup): + """Test that the frequency of the schedule remains unchanged after smart scheduling""" + db_conn, db_cur = get_db_cursor() + try: + fifteen_min_schedule_details = { + "schedule_id": "test-schedule-2", + "server_id": 1, + "interval_mask": "*/15 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + fifteen_min_schedule = Schedule( + schedule_id=fifteen_min_schedule_details['schedule_id'], + server_id=fifteen_min_schedule_details['server_id'], + interval_mask=fifteen_min_schedule_details['interval_mask'], + smart_interval_mask=fifteen_min_schedule_details.get('smart_interval_mask'), + blocklisted=fifteen_min_schedule_details.get('blocklisted'), + db_cur=db_cur + ) + fifteen_min_schedule.shifted = True + fifteen_min_schedule.start_time_mins = 3 + + smart_schedule._update_schedule_cron(fifteen_min_schedule) + assert fifteen_min_schedule.smart_interval_mask == "3-59/15 * * * *" + assert fifteen_min_schedule.frequency_minutes == 15 + + fifteen_min_schedule.determine_attributes(db_cur) + assert fifteen_min_schedule.is_regular_schedule() + assert fifteen_min_schedule.frequency_minutes == 15 + finally: + db_cur.close() + db_conn.close() + + def test_gene_space_constraints(self, db_setup): + """Test that the gene space constraints are respected when updating schedule crons""" + db_conn, db_cur = get_db_cursor() + try: + schedule_details = { + "schedule_id": "test-schedule-3", + "server_id": 1, + "interval_mask": "*/45 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + test_schedule.frequency_minutes = 45 + test_schedule.shifted = True + test_schedule.start_time_mins = 50 # Shift greater than frequency + + with pytest.raises(ValueError): + smart_schedule._update_schedule_cron(test_schedule) + finally: + db_cur.close() + db_conn.close() + + def test_smart_scheduling_gene_space_constraints_30_min(self, db_setup): + """Test that the gene space constraints don't create invalid cron expressions""" + db_conn, db_cur = get_db_cursor() + try: + ga_scheduler = GAPyGADScheduler() + + schedule_details = { + "schedule_id": "test-schedule-1", + "server_id": 1, + "interval_mask": "*/30 * * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + gene_space = ga_scheduler._gene_space([test_schedule]) + + test_schedule.shifted = True + test_schedule.start_time_mins = gene_space[0]["high"] + smart_schedule._update_schedule_cron(test_schedule) + assert test_schedule.smart_interval_mask == "29-59/30 * * * *" + assert croniter.croniter.is_valid(test_schedule.smart_interval_mask) + assert test_schedule.frequency_minutes == 30 + + test_schedule.start_time_mins = gene_space[0]["low"] + 1 + smart_schedule._update_schedule_cron(test_schedule) + assert test_schedule.smart_interval_mask == "1-59/30 * * * *" + assert croniter.croniter.is_valid(test_schedule.smart_interval_mask) + assert test_schedule.frequency_minutes == 30 + finally: + db_cur.close() + db_conn.close() + + def test_smart_scheduling_gene_space_constraints_daily(self, db_setup): + """Test that the gene space constraints don't create invalid cron expressions""" + db_conn, db_cur = get_db_cursor() + try: + ga_scheduler = GAPyGADScheduler() + + schedule_details = { + "schedule_id": "test-schedule-4", + "server_id": 1, + "interval_mask": "30 8 * * *", + "smart_interval_mask": None, + "blocklisted": False + } + test_schedule = Schedule( + schedule_id=schedule_details['schedule_id'], + server_id=schedule_details['server_id'], + interval_mask=schedule_details['interval_mask'], + smart_interval_mask=schedule_details.get('smart_interval_mask'), + blocklisted=schedule_details.get('blocklisted'), + db_cur=db_cur + ) + gene_space = ga_scheduler._gene_space([test_schedule]) + + test_schedule.shifted = True + test_schedule.start_time_mins = gene_space[0]["high"] + smart_schedule._update_schedule_cron(test_schedule) + assert test_schedule.smart_interval_mask == "29 9 * * *" + assert croniter.croniter.is_valid(test_schedule.smart_interval_mask) + assert test_schedule.frequency_minutes == 1440 + + test_schedule.start_time_mins = gene_space[0]["low"] + smart_schedule._update_schedule_cron(test_schedule) + assert test_schedule.smart_interval_mask == "30 8 * * *" + assert croniter.croniter.is_valid(test_schedule.smart_interval_mask) + assert test_schedule.frequency_minutes == 1440 + finally: + db_cur.close() + db_conn.close() + + def test_get_schedules_per_server_no_schedules_single_server(self, db_setup): + """Test that _get_schedules_per_server raises ValueError when no schedules exist for a server""" + try: + # Create two servers (omit server_id to use auto-increment) + query_test_db( + """INSERT INTO servers (hostname, fqdn, ip4_address) + VALUES ('test-server-1', 'test-server-1.local', '127.0.0.1'), + ('test-server-2', 'test-server-2.local', '127.0.0.2')""" + ) + + # Add a schedule only to server 1 + query_test_db( + """INSERT INTO schedules + (schedule_id, server_id, interval_mask, exec_command) + VALUES ('schedule-1', 1, '0 * * * *', 'echo test')""" + ) + + # Get a fresh cursor after data insertion + db_conn, db_cur = get_db_cursor() + + # Attempt to get schedules for server 2 without any schedules + with pytest.raises(ValueError, match="No schedules found for server_id 2"): + smart_schedule._get_schedules_per_server(server_id=2, db_cur=db_cur) + + db_cur.close() + db_conn.close() + except Exception as e: + raise e + + def test_main_no_schedules_single_server(self, db_setup, capsys): + """Test that main() handles servers without schedules gracefully (single server)""" + try: + # Create two servers + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server-1', 'test-server-1.local', '127.0.0.1'), + (2, 'test-server-2', 'test-server-2.local', '127.0.0.2')""" + ) + + # Add a schedule only to server 1 + query_test_db( + """INSERT INTO schedules + (schedule_id, server_id, interval_mask, exec_command) + VALUES ('schedule-1', 1, '0 * * * *', 'echo test')""" + ) + + # Call main with server_id 2 (should return early without error) + smart_schedule.main(server_id=2, dbname=pytest.db_test) + + # Verify that ValueError message was printed + captured = capsys.readouterr() + assert "No schedules found for server_id 2" in captured.out + except Exception as e: + raise e + + +class TestScheduleSnapshots: + """Tests for schedule snapshots functionality""" + + def test_snapshot_schedules(self, db_setup): + """Test snapshotting schedules and automatic schedule backup creation""" + db_conn, db_cur = get_db_cursor() + try: + # Create server and schedules first + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'C')""" + ) + schedule_ids = ["test-schedule-1", "test-schedule-2"] + for sched_id in schedule_ids: + query_test_db( + f"""INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('{sched_id}', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + + # Snapshot the schedules + scheduler.snapshot_schedules(db_cur, server_id = 1, reason="Test optimization") + + # Verify snapshot was created + snapshot_result = query_test_db("SELECT snapshot_id, server_id FROM snapshots WHERE reason = 'Test optimization'") + assert len(snapshot_result) > 0 + snapshot_id = snapshot_result[0][0] + server_id = snapshot_result[0][1] + assert server_id == 1 + + # Verify schedule backups exist for this snapshot + schedule_backups_result = query_test_db("SELECT schedule_id FROM schedule_backups WHERE snapshot_id = %s" % snapshot_id) + assert len(schedule_backups_result) == len(schedule_ids) + backup_schedule_ids = [row[0] for row in schedule_backups_result] + for schedule_id in schedule_ids: + assert schedule_id in backup_schedule_ids + + finally: + db_cur.close() + db_conn.close() + + + def test_full_rollback_by_server_id(self, db_setup): + """Test full rollback for a server""" + db_conn, db_cur = get_db_cursor() + try: + # Register a server and create a schedule with smart_interval_mask set + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'D')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-schedule-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + scheduler.full_rollback(db_cur, server_id=1) + assert query_test_db("SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'test-schedule-1'")[0][0] is None + assert query_test_db("SELECT interval_mask FROM schedules WHERE schedule_id = 'test-schedule-1'")[0][0] == "0 * * * *" + + + finally: + db_cur.close() + db_conn.close() + + + def test_full_rollback_by_schedule_id(self, db_setup): + """Test full rollback for a specific schedule""" + db_conn, db_cur = get_db_cursor() + try: + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'E')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-schedule-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + + # Perform full rollback + scheduler.full_rollback(db_cur, schedule_id="test-schedule-1") + + # Verify smart_interval_mask is set to NULL + assert query_test_db("SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'test-schedule-1'")[0][0] is None + assert query_test_db("SELECT interval_mask FROM schedules WHERE schedule_id = 'test-schedule-1'")[0][0] == "0 * * * *" + + finally: + db_cur.close() + db_conn.close() + + def test_restore_previous_schedules(self, db_setup): + """Test rollback to previous for a specific schedule""" + db_conn, db_cur = get_db_cursor() + try: + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'F')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-schedule-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + scheduler.snapshot_schedules(db_cur, server_id=1, reason="Test optimization") + assert query_test_db("SELECT smart_interval_mask FROM schedule_backups WHERE schedule_id = 'test-schedule-1'")[0][0] == "30 * * * *" + assert query_test_db("SELECT COUNT(*) FROM schedule_backups WHERE schedule_id = 'test-schedule-1'")[0][0] == 1 + + query_test_db("UPDATE schedules SET smart_interval_mask = '45 * * * *' WHERE schedule_id = 'test-schedule-1'") + + scheduler.snapshot_schedules(db_cur, server_id=1, reason="Test optimization") + assert query_test_db("SELECT server_id FROM snapshots ORDER BY snapshot_id DESC LIMIT 1")[0][0] == 1 + assert query_test_db("SELECT smart_interval_mask FROM schedule_backups WHERE schedule_id = 'test-schedule-1' ORDER BY snapshot_id DESC LIMIT 1")[0][0] == "45 * * * *" + assert query_test_db("SELECT COUNT(*) FROM schedule_backups WHERE schedule_id = 'test-schedule-1'")[0][0] == 2 + + # Perform rollback to previous snapshot + prev_snapshot_id = query_test_db("SELECT snapshot_id FROM snapshots ORDER BY snapshot_timestamp DESC LIMIT 1 OFFSET 1")[0][0] + scheduler.restore_previous_schedules(db_cur, snapshot_id=prev_snapshot_id, server_id=1) + assert query_test_db("SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'test-schedule-1'")[0][0] == "30 * * * *" + + finally: + db_cur.close() + db_conn.close() + + + def test_snapshot_cleanup(self, db_setup): + """Test that snapshot limits are enforced and old snapshots are deleted""" + db_conn, db_cur = get_db_cursor() + try: + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', 'H')""" + ) + query_test_db( + """INSERT INTO schedules (schedule_id, server_id, interval_mask, smart_interval_mask, exec_command) + VALUES ('test-schedule-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + # Create more than 5 snapshots to trigger deletion of old snapshots + for i in range(7): + scheduler.snapshot_schedules(db_cur, server_id=1) + + # Verify that only the 5 most recent snapshots remain + snapshot_count = query_test_db("SELECT COUNT(*) FROM snapshots WHERE server_id = 1")[0][0] + assert snapshot_count == 5 + oldest_snapshot_id = query_test_db("SELECT snapshot_id FROM snapshots WHERE server_id = 1 ORDER BY snapshot_timestamp ASC LIMIT 1")[0][0] + assert oldest_snapshot_id == 3 + oldest_snapshot_id = query_test_db("SELECT snapshot_id FROM schedule_backups WHERE server_id = 1 ORDER BY snapshot_id ASC LIMIT 1")[0][0] + assert oldest_snapshot_id == 3 + + finally: + db_cur.close() + db_conn.close() + + +class TestOptimiseWithCustomDbConnection: + """Tests for optimise() function with custom db connection""" + + def test_optimise_with_custom_db_connection_single_server(self, db_setup): + """Test optimise() function using a custom db connection for a single server""" + 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) + 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')""" + ) + + # Call optimise with custom db_cur + ga_config = {"random_seed": 1, "mutation_type": None, "num_generations": 2, "sol_per_pop": 5, "num_parents_mating": 2} + smart_schedule.optimise(db_cur=db_cur, server_id=1, ga_config=ga_config) + + # Verify that the schedules were processed (no errors should occur) + schedules = query_test_db("SELECT schedule_id FROM schedules WHERE server_id = 1") + smart_interval_masks = query_test_db("SELECT smart_interval_mask FROM schedules WHERE server_id = 1") + snapshots = query_test_db("SELECT snapshot_id FROM snapshots WHERE server_id = 1") + schedule_backups = query_test_db("SELECT schedule_id, interval_mask, smart_interval_mask FROM schedule_backups WHERE server_id = 1") + assert all(mask is not None for mask in smart_interval_masks) + assert query_test_db("""SELECT count(*) FROM schedules + LEFT JOIN schedule_backups ON schedules.schedule_id = schedule_backups.schedule_id + WHERE schedules.interval_mask != schedule_backups.interval_mask + OR schedules.smart_interval_mask != schedule_backups.smart_interval_mask""")[0][0] == 0 + + + assert len(schedules) == 5 + assert len(snapshots) == 1 + assert len(schedule_backups) == 5 + finally: + db_cur.close() + db_conn.close() + + + def test_optimise_with_custom_db_connection_multiple_servers(self, db_setup): + """Test optimise() function with custom db connection for multiple servers""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create multiple servers and schedules + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'server-1', 'server-1.local', '192.168.1.1'), + (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') + """ + ) + + # Call optimise with custom db_cur + ga_config = {"random_seed": 1, "mutation_type": None, "num_generations": 2, "sol_per_pop": 5, "num_parents_mating": 2} + + results_1 = query_test_db("SELECT schedule_id, smart_interval_mask FROM schedules WHERE server_id = 1") + results_2 = query_test_db("SELECT schedule_id, smart_interval_mask FROM schedules WHERE server_id = 2") + if results_1: + schedules_1, smart_interval_masks_1 = zip(*results_1) + if results_2: + schedules_2, smart_interval_masks_2 = zip(*results_2) + smart_schedule.optimise(db_cur=db_cur, ga_config=ga_config) + + results_1 = query_test_db("SELECT schedule_id, smart_interval_mask FROM schedules WHERE server_id = 1") + results_2 = query_test_db("SELECT schedule_id, smart_interval_mask FROM schedules WHERE server_id = 2") + if results_1: + schedules_1, smart_interval_masks_1 = zip(*results_1) + if results_2: + schedules_2, smart_interval_masks_2 = zip(*results_2) + snapshots_1 = query_test_db("SELECT snapshot_id FROM snapshots WHERE server_id = 1") + snapshots_2 = query_test_db("SELECT snapshot_id FROM snapshots WHERE server_id = 2") + assert len(schedules_1) == 5 and len(schedules_2) == 5 + assert len(snapshots_1) == 1 and len(snapshots_2) == 1 + assert query_test_db("SELECT count(*) FROM schedule_backups WHERE server_id = 1")[0][0] == 5 + assert query_test_db("SELECT count(*) FROM schedule_backups WHERE server_id = 2")[0][0] == 5 + assert all(mask is not None for mask in smart_interval_masks_1) + assert all(mask is not None for mask in smart_interval_masks_2) + + assert query_test_db("""SELECT count(*) FROM schedules + LEFT JOIN schedule_backups ON schedules.schedule_id = schedule_backups.schedule_id + WHERE schedules.interval_mask != schedule_backups.interval_mask + OR schedules.smart_interval_mask != schedule_backups.smart_interval_mask""")[0][0] == 0 + finally: + db_cur.close() + db_conn.close() + + def test_optimise_invalid_server_id_with_custom_connection(self, db_setup): + """Test optimise() raises error for invalid server_id with custom db connection""" + db_conn, db_cur = get_db_cursor() + try: + # Attempt to optimize for non-existent server + with pytest.raises(ValueError, match="Server with server_id=999 does not exist"): + smart_schedule.optimise(db_cur=db_cur, server_id=999, ga_config=None) + finally: + db_cur.close() + db_conn.close() + + def test_optimise_no_schedules_with_custom_connection(self, db_setup, capsys): + """Test optimise() handles server with no schedules gracefully using custom db connection""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create server with no schedules + query_test_db( + """INSERT INTO servers (server_id, hostname, fqdn, ip4_address) + VALUES (1, 'test-server', 'test-server.local', '192.168.1.1')""" + ) + + # Call optimise - should return early with message + smart_schedule.optimise(db_cur=db_cur, server_id=1, ga_config=None) + + # Verify error message was printed + captured = capsys.readouterr() + assert "No schedules found for server_id 1" in captured.out + finally: + db_cur.close() + db_conn.close() + + +class TestUpdateScheduleDetailsBulk: + """Tests for update_schedule_details_bulk function""" + + def test_update_schedule_details_bulk_single_schedule(self, db_setup): + """Test bulk update of a single schedule""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create server and schedule + 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, smart_interval_mask, exec_command) + VALUES ('sched-1', 1, '0 * * * *', NULL, 'echo test')""" + ) + + # Bulk update: set smart_interval_mask + schedule_list = [ + { + "schedule_id": "sched-1", + "smart_interval_mask": "30 * * * *", + } + ] + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=schedule_list) + + # Verify update + result = query_test_db("SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'sched-1'") + assert result[0][0] == "30 * * * *" + finally: + db_cur.close() + db_conn.close() + + def test_update_schedule_details_bulk_multiple_schedules(self, db_setup): + """Test bulk update of multiple 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, smart_interval_mask, exec_command) + VALUES ('sched-1', 1, '0 * * * *', NULL, 'echo test1'), + ('sched-2', 1, '15 * * * *', NULL, 'echo test2'), + ('sched-3', 1, '30 * * * *', NULL, 'echo test3')""" + ) + + # Bulk update: set smart_interval_mask for all schedules + schedule_list = [ + {"schedule_id": "sched-1", "smart_interval_mask": "10 * * * *"}, + {"schedule_id": "sched-2", "smart_interval_mask": "25 * * * *"}, + {"schedule_id": "sched-3", "smart_interval_mask": "40 * * * *"}, + ] + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=schedule_list) + + # Verify updates + result = query_test_db( + "SELECT schedule_id, smart_interval_mask FROM schedules WHERE server_id = 1 ORDER BY schedule_id" + ) + assert len(result) == 3 + assert result[0] == ("sched-1", "10 * * * *") + assert result[1] == ("sched-2", "25 * * * *") + assert result[2] == ("sched-3", "40 * * * *") + finally: + db_cur.close() + db_conn.close() + + def test_update_schedule_details_bulk_with_null_values(self, db_setup): + """Test that NULL values in schedule_list are skipped""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create server and schedule + 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, smart_interval_mask, exec_command) + VALUES ('sched-1', 1, '0 * * * *', '30 * * * *', 'echo test')""" + ) + + # Bulk update: set smart_interval_mask to something new, but include None values + schedule_list = [ + { + "schedule_id": "sched-1", + "smart_interval_mask": "45 * * * *", + "parameters": None, # This should be ignored + } + ] + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=schedule_list) + + # Verify that only smart_interval_mask was updated + result = query_test_db("SELECT smart_interval_mask FROM schedules WHERE schedule_id = 'sched-1'") + assert result[0][0] == "45 * * * *" + finally: + db_cur.close() + db_conn.close() + + def test_update_schedule_details_bulk_empty_list(self, db_setup): + """Test bulk update with empty schedule list""" + db_conn, db_cur = get_db_cursor() + try: + # Should return early without error + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=[]) + # No assertion needed - test passes if no exception is raised + finally: + db_cur.close() + db_conn.close() + + + def test_update_schedule_details_bulk_multiple_fields(self, db_setup): + """Test bulk update of multiple fields for each schedule""" + db_conn, db_cur = get_db_cursor() + try: + # Setup: Create server and schedule + 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, smart_interval_mask, exec_command) + VALUES ('sched-1', 1, '0 * * * *', NULL, 'echo test'), ('sched-2', 1, '15 * * * *', NULL, 'echo test')""" + ) + + # Bulk update: update multiple fields + schedule_list = [ + { + "schedule_id": "sched-1", + "smart_interval_mask": "15 * * * *", + "interval_mask": "15 * * * *", + }, + { + "schedule_id": "sched-2", + "smart_interval_mask": "5 * * * *", + "interval_mask": "5 * * * *" + } + ] + scheduler.update_schedule_details_bulk(db_cur=db_cur, schedule_list=schedule_list) + + # Verify updates + result = query_test_db( + "SELECT smart_interval_mask, interval_mask FROM schedules WHERE schedule_id = 'sched-1'" + ) + assert result[0][0] == "15 * * * *" + assert result[0][1] == "15 * * * *" + result = query_test_db( + "SELECT smart_interval_mask, interval_mask, is_enabled FROM schedules WHERE schedule_id = 'sched-2'" + ) + assert result[0][0] == "5 * * * *" + assert result[0][1] == "5 * * * *" + finally: + db_cur.close() + db_conn.close() \ No newline at end of file