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
16 changes: 15 additions & 1 deletion .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -919,16 +919,28 @@ jobs:
continue
fi

service_url="$(gcloud run services describe "${cloud_run_service}" \
--project="${GCP_PROJECT_ID}" \
--region="${CLOUD_RUN_REGION}" \
--format='value(status.url)' 2>/dev/null || true)"
if [ -z "${service_url}" ]; then
echo "Unable to resolve Cloud Run service URL for ${cloud_run_service}; cannot sync scheduler URI." >&2
exit 1
fi

for suffix in scheduler probe-scheduler precheck-scheduler; do
case "${suffix}" in
scheduler)
schedule_time="${main_time}"
scheduler_path="/run"

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 Update heartbeat matching before switching jobs to /run

When the scheduler sync rewrites the main job to ${service_url}/run, the scheduler-aware heartbeat no longer recognizes that job: scripts/execution_report_heartbeat.py only treats scheduler URIs with path / as strategy-run jobs and then includes a service when no matching job is found. For less-frequent targets such as monthly services, this makes every configured service look due on every heartbeat run, causing false missing-report alerts/failures even on days the main scheduler did not run. Update the heartbeat matcher to accept the new /run path as part of this route migration.

Useful? React with 👍 / 👎.

;;
probe-scheduler)
schedule_time="${probe_time}"
scheduler_path="/probe"
;;
precheck-scheduler)
schedule_time="${precheck_time}"
scheduler_path="/dry-run"
;;
esac

Expand Down Expand Up @@ -968,10 +980,12 @@ jobs:
PY
)"

echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule} and timezone to ${market_timezone}."
scheduler_uri="${service_url}${scheduler_path}"
echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule}, timezone to ${market_timezone}, and URI to ${scheduler_uri}."
gcloud scheduler jobs update http "${job_name}" \
--project="${GCP_PROJECT_ID}" \
--location="${scheduler_location}" \
--uri="${scheduler_uri}" \
--schedule="${desired_schedule}" \
--time-zone="${market_timezone}" \
--quiet
Expand Down
29 changes: 13 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ def _handle_request(
*,
dry_run_only_override: bool | None = None,
response_body: str = "OK",
dry_run_label: str = "precheck",
dry_run_label: str = "strategy dry-run",
):
if request.method == "GET":
if dry_run_only_override is None:
Expand All @@ -960,14 +960,15 @@ def _handle_request(
signals=strategy_plugin_signals,
error=strategy_plugin_error,
)
execution_window = "dry_run" if dry_run_only_override else "execution"
lock_acquired = STRATEGY_RUN_LOCK.acquire(blocking=False)
try:
log_runtime_event(
log_context,
"strategy_cycle_received",
message="Received strategy precheck request" if dry_run_only_override else "Received strategy execution request",
message="Received strategy dry-run request" if dry_run_only_override else "Received strategy execution request",
http_method=request.method,
execution_window="precheck" if dry_run_only_override else "execution",
execution_window=execution_window,
)
if not lock_acquired:
log_runtime_event(
Expand All @@ -991,7 +992,7 @@ def _handle_request(
log_context,
"market_closed",
message="Market closed; skip strategy execution",
execution_window="precheck" if dry_run_only_override else "execution",
execution_window=execution_window,
market=MARKET,
market_calendar=MARKET_CALENDAR,
market_timezone=MARKET_TIMEZONE,
Expand All @@ -1005,8 +1006,8 @@ def _handle_request(
log_runtime_event(
log_context,
"strategy_cycle_started",
message="Starting strategy precheck" if dry_run_only_override else "Starting strategy execution",
execution_window="precheck" if dry_run_only_override else "execution",
message="Starting strategy dry-run" if dry_run_only_override else "Starting strategy execution",
execution_window=execution_window,
)
if dry_run_only_override is None:
publish_strategy_plugin_alerts(strategy_plugin_signals, report=report)
Expand Down Expand Up @@ -1047,7 +1048,7 @@ def _handle_request(
log_context,
"strategy_signal_snapshot",
message="Strategy signal snapshot",
execution_window="precheck" if dry_run_only_override else "execution",
execution_window=execution_window,
**signal_snapshot,
)
report_summary = _build_cycle_report_summary(
Expand Down Expand Up @@ -1085,8 +1086,8 @@ def _handle_request(
log_runtime_event(
log_context,
"strategy_cycle_completed",
message="Strategy precheck completed" if dry_run_only_override else "Strategy execution completed",
execution_window="precheck" if dry_run_only_override else "execution",
message="Strategy dry-run completed" if dry_run_only_override else "Strategy execution completed",
execution_window=execution_window,
result=cycle_result.result,
)
return (response_body if dry_run_only_override else cycle_result.result), 200
Expand Down Expand Up @@ -1259,22 +1260,18 @@ def _handle_probe(*, response_body: str = "Probe OK"):
print(f"failed to persist execution report: {persist_exc}", flush=True)


@app.route("/", methods=["POST", "GET"])
@app.route("/run", methods=["POST", "GET"])
def handle_request():
return _route_with_runtime_error_fallback(_handle_request)


@app.route("/precheck", methods=["POST", "GET"])
@app.route("/dry-run", methods=["POST", "GET"])
def handle_precheck():
response_body = "Dry Run OK" if request.path == "/dry-run" else "Precheck OK"
dry_run_label = "strategy dry-run" if request.path == "/dry-run" else "precheck"
def handle_dry_run():
return _route_with_runtime_error_fallback(
_handle_request,
dry_run_only_override=True,
response_body=response_body,
dry_run_label=dry_run_label,
response_body="Dry Run OK",
dry_run_label="strategy dry-run",
)


Expand Down
77 changes: 24 additions & 53 deletions tests/test_request_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ def route_methods(strategy_module):

def test_cloud_run_route_contracts_are_registered(strategy_module):
assert route_methods(strategy_module) == {
"/": ["GET", "POST"],
"/run": ["GET", "POST"],
"/precheck": ["GET", "POST"],
"/dry-run": ["GET", "POST"],
"/probe": ["GET", "POST"],
"/health": ["GET"],
Expand All @@ -36,7 +34,7 @@ def fail_if_called():

monkeypatch.setattr(strategy_module, "run_strategy_core", fail_if_called)

with strategy_module.app.test_request_context("/", method="GET"):
with strategy_module.app.test_request_context("/run", method="GET"):
body, status = strategy_module.handle_request()

assert status == 200
Expand All @@ -53,7 +51,7 @@ def fake_run_strategy_core(**_kwargs):
monkeypatch.setattr(strategy_module, "is_market_open_today", lambda **_kwargs: True)
monkeypatch.setattr(strategy_module, "run_strategy_core", fake_run_strategy_core)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 200
Expand Down Expand Up @@ -83,7 +81,7 @@ def fake_dispatch(signals, **kwargs):
monkeypatch.setattr(strategy_module, "dispatch_strategy_plugin_alerts", fake_dispatch)
monkeypatch.setattr(strategy_module, "run_strategy_core", lambda **_kwargs: "OK - executed")

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 200
Expand All @@ -95,7 +93,7 @@ def fake_dispatch(signals, **kwargs):
assert observed["alerts"][0][1]["state_settings"] is not None


def test_handle_precheck_post_uses_dry_run_override(strategy_module, monkeypatch):
def test_handle_dry_run_post_uses_dry_run_override(strategy_module, monkeypatch):
observed = {"called": False, "dry_run_only_override": None, "events": []}

monkeypatch.setattr(strategy_module, "build_request_log_context", lambda: types.SimpleNamespace(run_id="run-001"))
Expand All @@ -106,35 +104,6 @@ def test_handle_precheck_post_uses_dry_run_override(strategy_module, monkeypatch
monkeypatch.setattr(strategy_module, "load_strategy_plugin_signals", lambda: ((), None))
monkeypatch.setattr(strategy_module, "attach_strategy_plugin_report", lambda *args, **kwargs: None)

def fake_run_strategy_core(**kwargs):
observed["called"] = True
observed["dry_run_only_override"] = kwargs.get("dry_run_only_override")
return "OK - precheck"

monkeypatch.setattr(strategy_module, "run_strategy_core", fake_run_strategy_core)

with strategy_module.app.test_request_context("/precheck", method="POST"):
body, status = strategy_module.handle_precheck()

assert status == 200
assert body == "Precheck OK"
assert observed["called"] is True
assert observed["dry_run_only_override"] is True
assert observed["events"][0][0] == "strategy_cycle_received"
assert observed["events"][0][1]["execution_window"] == "precheck"


def test_handle_dry_run_alias_post_uses_dry_run_override(strategy_module, monkeypatch):
observed = {"called": False, "dry_run_only_override": None}

monkeypatch.setattr(strategy_module, "build_request_log_context", lambda: types.SimpleNamespace(run_id="run-001"))
monkeypatch.setattr(strategy_module, "build_execution_report", lambda log_context, **_kwargs: {"status": "pending"})
monkeypatch.setattr(strategy_module, "persist_execution_report", lambda report, **_kwargs: "/tmp/runtime-report.json")
monkeypatch.setattr(strategy_module, "emit_runtime_log", lambda *args, **kwargs: None)
monkeypatch.setattr(strategy_module, "is_market_open_today", lambda **_kwargs: True)
monkeypatch.setattr(strategy_module, "load_strategy_plugin_signals", lambda: ((), None))
monkeypatch.setattr(strategy_module, "attach_strategy_plugin_report", lambda *args, **kwargs: None)

def fake_run_strategy_core(**kwargs):
observed["called"] = True
observed["dry_run_only_override"] = kwargs.get("dry_run_only_override")
Expand All @@ -143,15 +112,17 @@ def fake_run_strategy_core(**kwargs):
monkeypatch.setattr(strategy_module, "run_strategy_core", fake_run_strategy_core)

with strategy_module.app.test_request_context("/dry-run", method="POST"):
body, status = strategy_module.handle_precheck()
body, status = strategy_module.handle_dry_run()

assert status == 200
assert body == "Dry Run OK"
assert observed["called"] is True
assert observed["dry_run_only_override"] is True
assert observed["events"][0][0] == "strategy_cycle_received"
assert observed["events"][0][1]["execution_window"] == "dry_run"


def test_precheck_composer_wires_dry_run_override_to_order_execution(strategy_module, monkeypatch):
def test_dry_run_composer_wires_dry_run_override_to_order_execution(strategy_module, monkeypatch):
observed = {}

class FakeBrokerAdapters:
Expand All @@ -172,7 +143,7 @@ def fake_build_broker_adapters(*, dry_run_only_override=None):
assert observed["dry_run_only_override"] is True


def test_handle_precheck_ignores_paper_liquidate_only(strategy_module, monkeypatch):
def test_handle_dry_run_ignores_paper_liquidate_only(strategy_module, monkeypatch):
observed = {"called": False}

monkeypatch.setattr(strategy_module, "build_request_log_context", lambda: types.SimpleNamespace(run_id="run-001"))
Expand All @@ -187,28 +158,28 @@ def test_handle_precheck_ignores_paper_liquidate_only(strategy_module, monkeypat
def fake_run_strategy_core(**kwargs):
observed["called"] = True
assert kwargs.get("dry_run_only_override") is True
return "OK - precheck"
return "OK - dry-run"

monkeypatch.setattr(strategy_module, "run_strategy_core", fake_run_strategy_core)

with strategy_module.app.test_request_context("/precheck", method="POST"):
body, status = strategy_module.handle_precheck()
with strategy_module.app.test_request_context("/dry-run", method="POST"):
body, status = strategy_module.handle_dry_run()

assert status == 200
assert body == "Precheck OK"
assert body == "Dry Run OK"
assert observed["called"] is True


def test_handle_precheck_get_does_not_execute(strategy_module, monkeypatch):
def test_handle_dry_run_get_does_not_execute(strategy_module, monkeypatch):
observed = {"called": False}

monkeypatch.setattr(strategy_module, "run_strategy_core", lambda **_kwargs: observed.__setitem__("called", True))

with strategy_module.app.test_request_context("/precheck", method="GET"):
body, status = strategy_module.handle_precheck()
with strategy_module.app.test_request_context("/dry-run", method="GET"):
body, status = strategy_module.handle_dry_run()

assert status == 200
assert body == "Precheck OK - use POST to run precheck"
assert body == "Dry Run OK - use POST to run strategy dry-run"
assert observed["called"] is False


Expand Down Expand Up @@ -402,7 +373,7 @@ def fail_if_called():

strategy_module.STRATEGY_RUN_LOCK.acquire()
try:
with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()
finally:
strategy_module.STRATEGY_RUN_LOCK.release()
Expand Down Expand Up @@ -455,7 +426,7 @@ def test_handle_request_persists_machine_readable_report(strategy_module, monkey
lambda report: observed.setdefault("report", dict(report)) or "/tmp/runtime-report.json",
)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 200
Expand Down Expand Up @@ -518,7 +489,7 @@ def fake_run_strategy_core(**_kwargs):
lambda report: observed.setdefault("report", dict(report)) or "/tmp/runtime-report.json",
)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 200
Expand Down Expand Up @@ -612,7 +583,7 @@ def fail_if_called():
lambda report: observed.setdefault("report", dict(report)) or "/tmp/runtime-report.json",
)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 200
Expand All @@ -638,7 +609,7 @@ def test_handle_request_error_persists_machine_readable_report(strategy_module,
)
monkeypatch.setattr(strategy_module, "send_tg_message", lambda message: observed["messages"].append(message))

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 500
Expand Down Expand Up @@ -718,7 +689,7 @@ def fake_post(_url, *, json, timeout):
monkeypatch.setenv("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS", "plugin-chat")
monkeypatch.setattr(strategy_module.requests, "post", fake_post)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 500
Expand Down Expand Up @@ -746,7 +717,7 @@ def fake_post(_url, *, json, timeout):
monkeypatch.setattr(strategy_module, "TG_CHAT_ID", "chat-1")
monkeypatch.setattr(strategy_module.requests, "post", fake_post)

with strategy_module.app.test_request_context("/", method="POST"):
with strategy_module.app.test_request_context("/run", method="POST"):
body, status = strategy_module.handle_request()

assert status == 500
Expand Down