Skip to content

Commit f7acbb3

Browse files
Pigbibicodex
andcommitted
fix: harden runtime scheduling and notifications
Co-Authored-By: Codex <noreply@openai.com>
1 parent fb638ea commit f7acbb3

15 files changed

Lines changed: 1306 additions & 29 deletions

.github/workflows/execution-report-heartbeat.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
name: Execution Report Heartbeat
22

3-
# Schedule disabled; trade/error notifications own unattended alerting.
43
on:
54
workflow_dispatch:
65
inputs:
@@ -17,6 +16,8 @@ on:
1716
options:
1817
- "true"
1918
- "false"
19+
schedule:
20+
- cron: "20 22 * * *"
2021

2122
env:
2223
GCP_PROJECT_ID: firstradequant
@@ -41,8 +42,14 @@ jobs:
4142
RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT: ${{ inputs.fail_workflow_on_alert || vars.RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT || 'true' }}
4243
RUNTIME_HEARTBEAT_ACCEPT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_ACCEPT_STAGES }}
4344
RUNTIME_HEARTBEAT_REJECT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_REJECT_STAGES }}
45+
RUNTIME_HEARTBEAT_MARKET_AWARE: ${{ vars.RUNTIME_HEARTBEAT_MARKET_AWARE || 'true' }}
46+
RUNTIME_HEARTBEAT_MARKET_CALENDAR: ${{ vars.FIRSTRADE_MARKET_CALENDAR }}
47+
RUNTIME_HEARTBEAT_MARKET_TIMEZONE: ${{ vars.FIRSTRADE_MARKET_TIMEZONE }}
4448
RUNTIME_TARGET_ENABLED: ${{ vars.RUNTIME_TARGET_ENABLED }}
4549
RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}
50+
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
51+
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
52+
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
4653
FIRSTRADE_GCS_STATE_BUCKET: ${{ vars.FIRSTRADE_GCS_STATE_BUCKET }}
4754
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
4855
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
@@ -53,6 +60,19 @@ jobs:
5360
uses: actions/checkout@v6
5461

5562
- name: Authenticate to Google Cloud
63+
id: gcp_auth_primary
64+
continue-on-error: true
65+
uses: google-github-actions/auth@v3
66+
with:
67+
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
68+
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
69+
70+
- name: Wait before Google Cloud authentication retry
71+
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
72+
run: sleep 10
73+
74+
- name: Retry Google Cloud authentication
75+
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
5676
uses: google-github-actions/auth@v3
5777
with:
5878
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
@@ -61,5 +81,11 @@ jobs:
6181
- name: Set up gcloud
6282
uses: google-github-actions/setup-gcloud@v3
6383

84+
- name: Install market calendar
85+
continue-on-error: true
86+
run: >-
87+
python -m pip install --disable-pip-version-check
88+
--retries 3 --timeout 30 "pandas-market-calendars==5.4.0"
89+
6490
- name: Check recent execution report
6591
run: python scripts/execution_report_heartbeat.py

.github/workflows/runtime-guard.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ jobs:
6363
uses: actions/checkout@v6
6464

6565
- name: Authenticate to Google Cloud
66+
id: gcp_auth_primary
67+
continue-on-error: true
68+
uses: google-github-actions/auth@v3
69+
with:
70+
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
71+
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
72+
73+
- name: Wait before Google Cloud authentication retry
74+
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
75+
run: sleep 10
76+
77+
- name: Retry Google Cloud authentication
78+
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
6679
uses: google-github-actions/auth@v3
6780
with:
6881
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}

.github/workflows/sync-cloud-run-env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
QSL_ENABLE_CLOUD_RUN_AUTOMATION: ${{ vars.QSL_ENABLE_CLOUD_RUN_AUTOMATION }}
4848
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
4949
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
50+
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
5051
CLOUD_SCHEDULER_LOCATION: ${{ vars.CLOUD_SCHEDULER_LOCATION }}
5152
CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }}
5253
CLOUD_SCHEDULER_PROBE_TIME: ${{ vars.CLOUD_SCHEDULER_PROBE_TIME }}

application/rebalance_service.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,19 @@ def publish_log(text: str) -> None:
242242
except TypeError:
243243
log_message(text)
244244

245+
delivery_sent = True
246+
247+
def send_and_capture(text: str) -> bool | None:
248+
nonlocal delivery_sent
249+
outcome = sender(text)
250+
delivery_sent = outcome is not False
251+
return outcome
252+
245253
NotificationPublisher(
246254
log_message=publish_log,
247-
send_message=sender,
255+
send_message=send_and_capture,
248256
).publish(RenderedNotification(detailed_text=message, compact_text=message))
249-
return True
257+
return delivery_sent
250258

251259

252260
def _should_publish_cycle_notification(result: Mapping[str, Any]) -> bool:
@@ -733,6 +741,8 @@ def log_message(message: str) -> None:
733741
settings=settings,
734742
notification_sender=notification_sender,
735743
)
744+
if not result["notification_sent"]:
745+
result["notification_error"] = "delivery_not_acknowledged"
736746
except Exception as exc:
737747
result["notification_sent"] = False
738748
result["notification_error"] = f"{type(exc).__name__}: {exc}"

notifications/telegram.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,18 +553,29 @@ def build_sender(token: str | None, chat_id: str | None, *, requests_module=None
553553
if requests_module is None:
554554
import requests as requests_module
555555

556-
def send_tg_message(message: str) -> None:
556+
def send_tg_message(message: str) -> bool:
557557
if not token or not chat_id:
558-
return
558+
return False
559559
url = f"https://api.telegram.org/bot{token}/sendMessage"
560560
try:
561-
requests_module.post(
561+
response = requests_module.post(
562562
url,
563563
json={"chat_id": chat_id, "text": _break_telegram_market_symbol_auto_links(message)},
564564
timeout=15,
565565
)
566+
status_code = int(getattr(response, "status_code", 200) or 200)
567+
if status_code < 200 or status_code >= 300:
568+
print(f"Telegram send failed: HTTP {status_code}", flush=True)
569+
return False
570+
load_payload = getattr(response, "json", None)
571+
payload = load_payload() if callable(load_payload) else None
572+
if isinstance(payload, Mapping) and payload.get("ok") is False:
573+
print("Telegram send failed: negative API acknowledgement", flush=True)
574+
return False
566575
except Exception as exc:
567576
print(f"Telegram send failed: {type(exc).__name__}", flush=True)
577+
return False
578+
return True
568579

569580
return send_tg_message
570581

scripts/cloud_run_runtime_guard.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"URL_ERROR",
2424
"URL_UNREACHABLE",
2525
)
26+
SCHEDULER_CLOUD_RUN_DEDUP_SECONDS = 120
2627

2728

2829
def _split_values(raw: str | None) -> list[str]:
@@ -135,6 +136,41 @@ def _scheduler_entry_since(
135136
return max(matches) if matches else fallback
136137

137138

139+
def _is_duplicate_scheduler_failure(
140+
entry: dict[str, Any],
141+
cloud_run_failures_by_service: dict[str, list[dict[str, Any]]],
142+
) -> bool:
143+
scheduler_timestamp = _parse_timestamp(entry.get("timestamp"))
144+
job_name = _entry_job_name(entry)
145+
if scheduler_timestamp is None or not job_name:
146+
return False
147+
148+
tolerance = dt.timedelta(seconds=SCHEDULER_CLOUD_RUN_DEDUP_SECONDS)
149+
for service, failures in cloud_run_failures_by_service.items():
150+
if not any(alias and alias in job_name for alias in _service_job_aliases(service)):
151+
continue
152+
for failure in failures:
153+
cloud_run_timestamp = _parse_timestamp(failure.get("timestamp"))
154+
if (
155+
cloud_run_timestamp is not None
156+
and abs(scheduler_timestamp - cloud_run_timestamp) <= tolerance
157+
):
158+
return True
159+
return False
160+
161+
162+
def _services_without_success(
163+
services: list[str],
164+
success_count_by_service: dict[str, int],
165+
queried_services: set[str],
166+
) -> list[str]:
167+
return [
168+
service
169+
for service in services
170+
if service in queried_services and success_count_by_service.get(service, 0) == 0
171+
]
172+
173+
138174
def _run_gcloud(args: list[str]) -> subprocess.CompletedProcess[str]:
139175
return subprocess.run(args, text=True, capture_output=True, check=False)
140176

@@ -505,6 +541,9 @@ def main() -> int:
505541
issues: list[str] = []
506542
details: list[str] = []
507543
success_count = 0
544+
success_count_by_service: dict[str, int] = {}
545+
queried_services: set[str] = set()
546+
cloud_run_failures_by_service: dict[str, list[dict[str, Any]]] = {}
508547
service_since_by_name: dict[str, dt.datetime] = {}
509548

510549
try:
@@ -527,16 +566,26 @@ def main() -> int:
527566
except RuntimeError as exc:
528567
issues.append(f"Cloud Run log query failed for {service}: {exc}")
529568
continue
569+
queried_services.add(service)
530570
failures = [entry for entry in entries if _is_failure(entry)]
531-
success_count += sum(1 for entry in entries if _is_success(entry))
571+
cloud_run_failures_by_service[service] = failures
572+
service_success_count = sum(1 for entry in entries if _is_success(entry))
573+
success_count_by_service[service] = service_success_count
574+
success_count += service_success_count
532575
if failures:
533576
issues.append(f"{len(failures)} Cloud Run failure log(s) for {service}")
534577
details.extend(_summarize(entry) for entry in failures[:5])
535578

536-
if services and require_success and success_count == 0:
537-
issues.append(
538-
f"no successful Cloud Run request found for {', '.join(services)} in the last {lookback_minutes} minutes"
539-
)
579+
if services and require_success:
580+
for service in _services_without_success(
581+
services,
582+
success_count_by_service,
583+
queried_services,
584+
):
585+
issues.append(
586+
f"no successful Cloud Run request found for {service} "
587+
f"in the last {lookback_minutes} minutes"
588+
)
540589

541590
if check_scheduler and scheduler_pattern:
542591
log_filter = f'resource.type="cloud_scheduler_job" AND timestamp >= "{since_text}"'
@@ -557,6 +606,11 @@ def main() -> int:
557606
entry_since = _scheduler_entry_since(entry, service_since_by_name, since)
558607
if entry_timestamp and entry_timestamp < entry_since:
559608
continue
609+
if _is_duplicate_scheduler_failure(
610+
entry,
611+
cloud_run_failures_by_service,
612+
):
613+
continue
560614
failures.append(entry)
561615
if failures:
562616
issues.append(f"{len(failures)} Cloud Scheduler failure log(s)")

0 commit comments

Comments
 (0)