Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion scripts/execution_report_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,47 @@ def _list_scheduler_jobs(*, project: str | None) -> list[dict[str, Any]]:
return payload if isinstance(payload, list) else []


def _describe_scheduler_job(job_name: str, *, project: str | None) -> dict[str, Any] | None:
command = [
"gcloud",
"scheduler",
"jobs",
"describe",
job_name,
"--location",
_scheduler_location(),
"--format=json",
]
if project:
command.extend(["--project", project])
result = _run_gcloud(command)
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip()
if "not_found" in detail.lower() or "not found" in detail.lower():
return None
raise RuntimeError(detail or f"gcloud scheduler jobs describe failed for {job_name}")
if not result.stdout.strip():
return None
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise RuntimeError(f"gcloud scheduler jobs describe returned invalid JSON for {job_name}: {exc}") from exc
return payload if isinstance(payload, dict) else None


def _describe_scheduler_jobs_for_services(
services: list[str],
*,
project: str | None,
) -> list[dict[str, Any]]:
jobs = []
for service in services:
job = _describe_scheduler_job(f"{service}-scheduler", project=project)
if job:
jobs.append(job)
return jobs


def _scheduler_job_targets_strategy_run(job: dict[str, Any], service: str) -> bool:
if str(job.get("state") or "").strip().upper() not in {"", "ENABLED"}:
return False
Expand Down Expand Up @@ -401,7 +442,14 @@ def _filter_scheduler_due_services(
since: dt.datetime,
now: dt.datetime,
) -> list[str]:
jobs = _list_scheduler_jobs(project=project)
try:
jobs = _list_scheduler_jobs(project=project)
except RuntimeError as exc:
print(
f"Unable to list Cloud Scheduler jobs: {exc}; trying named scheduler job lookup.",
file=sys.stderr,
)
jobs = _describe_scheduler_jobs_for_services(services, project=project)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve window fallback when named lookup finds nothing

When jobs.list is denied and none of the ${service}-scheduler describes return a matching job (for example, a service with a differently named scheduler or before the scheduler job exists), this leaves jobs empty and the existing not service_jobs branch marks every service as due. _resolve_required_services then reports scheduler_checked=True, so main() skips the existing day-of-month fallback and monthly heartbeats can query/fail outside their expected window instead of skipping as they did when the list error was treated as unresolved. Consider treating an empty named-lookup result as unresolved so the previous fallback logic still applies.

Useful? React with 👍 / 👎.

due_services = []
for service in services:
service_jobs = [
Expand Down
32 changes: 32 additions & 0 deletions tests/test_execution_report_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,38 @@ def test_scheduler_aware_required_services_include_monthly_service_when_due(monk
assert required == ["svc-monthly"]


def test_scheduler_aware_required_services_fall_back_to_named_scheduler_describe(monkeypatch):
_clear_runtime_env(monkeypatch)
monkeypatch.setenv("CLOUD_RUN_SERVICE", "svc-monthly")
monkeypatch.setattr(
heartbeat,
"_list_scheduler_jobs",
lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("cloudscheduler.jobs.list denied")),
)
monkeypatch.setattr(
heartbeat,
"_describe_scheduler_job",
lambda job_name, **_kwargs: {
"state": "ENABLED",
"schedule": "45 15 1-7 * *",
"timeZone": "America/New_York",
"httpTarget": {"uri": "https://svc-monthly.example.run.app/"},
}
if job_name == "svc-monthly-scheduler"
else None,
)

required, skip_reason, scheduler_checked = heartbeat._resolve_required_services(
project="project-1",
since=dt.datetime(2026, 6, 10, 0, 0, tzinfo=dt.timezone.utc),
now=dt.datetime(2026, 6, 10, 2, 0, tzinfo=dt.timezone.utc),
)

assert required == []
assert skip_reason and "no configured Cloud Scheduler main job was due" in skip_reason
assert scheduler_checked is True


def test_main_skips_when_no_scheduler_main_job_is_due(monkeypatch, capsys):
_clear_runtime_env(monkeypatch)
monkeypatch.setenv("GCP_PROJECT_ID", "longbridgequant")
Expand Down