fix(v2-scheduler): avoid restart loops when scheduler is disabled
This commit is contained in:
@ -59,6 +59,8 @@ DAILY_ORCHESTRATION_INTERVAL_SECONDS=86400
|
|||||||
# Future optional scheduler loop settings (not enabled in base v2 runtime)
|
# Future optional scheduler loop settings (not enabled in base v2 runtime)
|
||||||
SCHEDULER_ENABLED=0
|
SCHEDULER_ENABLED=0
|
||||||
SCHEDULER_INTERVAL_SECONDS=900
|
SCHEDULER_INTERVAL_SECONDS=900
|
||||||
|
# When scheduler is disabled but container is started, keep it idle (avoid restart loops)
|
||||||
|
SCHEDULER_DISABLED_SLEEP_SECONDS=300
|
||||||
|
|
||||||
# API safeguards (read-only API is optional)
|
# API safeguards (read-only API is optional)
|
||||||
API_THROTTLE_ANON=100/hour
|
API_THROTTLE_ANON=100/hour
|
||||||
|
|||||||
@ -282,6 +282,7 @@ Notes:
|
|||||||
- image: `registry.younerd.org/hoopscout/scheduler:${APP_IMAGE_TAG:-latest}`
|
- image: `registry.younerd.org/hoopscout/scheduler:${APP_IMAGE_TAG:-latest}`
|
||||||
- command: `/app/scripts/scheduler.sh`
|
- command: `/app/scripts/scheduler.sh`
|
||||||
- interval: `DAILY_ORCHESTRATION_INTERVAL_SECONDS`
|
- interval: `DAILY_ORCHESTRATION_INTERVAL_SECONDS`
|
||||||
|
- disabled idle interval: `SCHEDULER_DISABLED_SLEEP_SECONDS`
|
||||||
|
|
||||||
### Scheduler entrypoint/runtime expectations
|
### Scheduler entrypoint/runtime expectations
|
||||||
|
|
||||||
@ -290,6 +291,7 @@ Notes:
|
|||||||
- scheduler is disabled unless:
|
- scheduler is disabled unless:
|
||||||
- compose `scheduler` profile is started
|
- compose `scheduler` profile is started
|
||||||
- `SCHEDULER_ENABLED=1`
|
- `SCHEDULER_ENABLED=1`
|
||||||
|
- if scheduler service is started while disabled (`SCHEDULER_ENABLED=0`), it does not exit; it enters idle sleep mode to avoid restart loops with `restart: unless-stopped`
|
||||||
- this keeps default runtime simple while supporting daily automation
|
- this keeps default runtime simple while supporting daily automation
|
||||||
|
|
||||||
### LBA extractor assumptions and limitations (MVP)
|
### LBA extractor assumptions and limitations (MVP)
|
||||||
|
|||||||
@ -75,6 +75,10 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
SCHEDULER_ENABLED: ${SCHEDULER_ENABLED:-0}
|
||||||
|
SCHEDULER_DISABLED_SLEEP_SECONDS: ${SCHEDULER_DISABLED_SLEEP_SECONDS:-300}
|
||||||
|
DAILY_ORCHESTRATION_INTERVAL_SECONDS: ${DAILY_ORCHESTRATION_INTERVAL_SECONDS:-86400}
|
||||||
command: /app/scripts/scheduler.sh
|
command: /app/scripts/scheduler.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@ -2,8 +2,16 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ "${SCHEDULER_ENABLED:-0}" != "1" ]; then
|
if [ "${SCHEDULER_ENABLED:-0}" != "1" ]; then
|
||||||
echo "Scheduler disabled (SCHEDULER_ENABLED=${SCHEDULER_ENABLED:-0}). Exiting."
|
DISABLED_SLEEP="${SCHEDULER_DISABLED_SLEEP_SECONDS:-300}"
|
||||||
exit 0
|
if [ "${DISABLED_SLEEP}" -lt 30 ]; then
|
||||||
|
echo "SCHEDULER_DISABLED_SLEEP_SECONDS must be >= 30"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Scheduler disabled (SCHEDULER_ENABLED=${SCHEDULER_ENABLED:-0}). Entering idle mode with ${DISABLED_SLEEP}s sleep."
|
||||||
|
while true; do
|
||||||
|
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] Scheduler disabled; sleeping for ${DISABLED_SLEEP}s."
|
||||||
|
sleep "${DISABLED_SLEEP}"
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
INTERVAL="${DAILY_ORCHESTRATION_INTERVAL_SECONDS:-${SCHEDULER_INTERVAL_SECONDS:-86400}}"
|
INTERVAL="${DAILY_ORCHESTRATION_INTERVAL_SECONDS:-${SCHEDULER_INTERVAL_SECONDS:-86400}}"
|
||||||
|
|||||||
43
tests/test_scheduler_operational_safety.py
Normal file
43
tests/test_scheduler_operational_safety.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_disabled_mode_stays_alive_without_exit_loop():
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["SCHEDULER_ENABLED"] = "0"
|
||||||
|
env["SCHEDULER_DISABLED_SLEEP_SECONDS"] = "30"
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["sh", "scripts/scheduler.sh"],
|
||||||
|
cwd=_repo_root(),
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
time.sleep(1.0)
|
||||||
|
assert process.poll() is None
|
||||||
|
finally:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_compose_service_is_profile_gated():
|
||||||
|
compose_text = (_repo_root() / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
assert 'profiles: ["scheduler"]' in compose_text
|
||||||
|
assert "restart: unless-stopped" in compose_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_script_declares_idle_disabled_behavior():
|
||||||
|
scheduler_script = (_repo_root() / "scripts/scheduler.sh").read_text(encoding="utf-8")
|
||||||
|
assert "Entering idle mode" in scheduler_script
|
||||||
|
assert "SCHEDULER_DISABLED_SLEEP_SECONDS" in scheduler_script
|
||||||
Reference in New Issue
Block a user