Spaces:
Sleeping
Sleeping
Add 40s timeout to baseline build branches to prevent props page hang
Browse filesWraps Branch 2 (patch build) and Branch 3 (full fallback build) in
threading.Thread with join(timeout=40). On timeout: returns empty/partial
bundle immediately, queues a persist-when-done background thread so next
load hits the fast snapshot path. Props page shows an info banner and
auto-reloads ~80s later once snapshots are populated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- data/shared_baseline.py +104 -35
- visualization/props_page.py +16 -0
data/shared_baseline.py
CHANGED
|
@@ -26,6 +26,7 @@ _MAX_ROWS_PER_PLAYER = 420
|
|
| 26 |
_MIN_CURRENT_ROWS_WHEN_AVAILABLE = 20
|
| 27 |
_SNAPSHOT_VERSION = "shared_baseline_v1"
|
| 28 |
_DEFAULT_SNAPSHOT_MAX_AGE_SECONDS = 60 * 30
|
|
|
|
| 29 |
_PRIOR_SEASON_RECENCY_WEIGHTS = {
|
| 30 |
2025: 1.00,
|
| 31 |
2024: 0.85,
|
|
@@ -1731,50 +1732,118 @@ def load_or_build_shared_baseline_bundle_complete_for_request(
|
|
| 1731 |
)
|
| 1732 |
|
| 1733 |
if snapshot_has_data and (missing_hitter_names or missing_pitcher_names):
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1742 |
)
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1746 |
batter_names=snapshot_batter_names,
|
| 1747 |
pitcher_names=snapshot_pitcher_names,
|
| 1748 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1749 |
return _annotate_request_coverage(
|
| 1750 |
-
|
| 1751 |
requested_hitter_names=snapshot_batter_names,
|
| 1752 |
requested_pitcher_names=snapshot_pitcher_names,
|
| 1753 |
-
coverage_mode="
|
| 1754 |
-
background_refresh_queued=
|
| 1755 |
)
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
| 1762 |
-
|
| 1763 |
-
|
| 1764 |
-
|
| 1765 |
-
|
| 1766 |
-
|
| 1767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1768 |
)
|
| 1769 |
-
if "snapshot_status" not in runtime_bundle:
|
| 1770 |
-
runtime_bundle["snapshot_status"] = snapshot_status
|
| 1771 |
-
return _annotate_request_coverage(
|
| 1772 |
-
runtime_bundle,
|
| 1773 |
-
requested_hitter_names=snapshot_batter_names,
|
| 1774 |
-
requested_pitcher_names=snapshot_pitcher_names,
|
| 1775 |
-
coverage_mode="runtime_fallback",
|
| 1776 |
-
background_refresh_queued=False,
|
| 1777 |
-
)
|
| 1778 |
|
| 1779 |
|
| 1780 |
def build_shared_baseline_bundle(
|
|
|
|
| 26 |
_MIN_CURRENT_ROWS_WHEN_AVAILABLE = 20
|
| 27 |
_SNAPSHOT_VERSION = "shared_baseline_v1"
|
| 28 |
_DEFAULT_SNAPSHOT_MAX_AGE_SECONDS = 60 * 30
|
| 29 |
+
_FALLBACK_BUILD_TIMEOUT_SECONDS: int = 40
|
| 30 |
_PRIOR_SEASON_RECENCY_WEIGHTS = {
|
| 31 |
2025: 1.00,
|
| 32 |
2024: 0.85,
|
|
|
|
| 1732 |
)
|
| 1733 |
|
| 1734 |
if snapshot_has_data and (missing_hitter_names or missing_pitcher_names):
|
| 1735 |
+
patch_result: list[dict | None] = [None]
|
| 1736 |
+
|
| 1737 |
+
def _run_patch() -> None:
|
| 1738 |
+
try:
|
| 1739 |
+
patch_result[0] = build_shared_baseline_bundle(
|
| 1740 |
+
batter_names=tuple(sorted(missing_hitter_names)),
|
| 1741 |
+
pitcher_names=tuple(sorted(missing_pitcher_names)),
|
| 1742 |
+
)
|
| 1743 |
+
except Exception as exc:
|
| 1744 |
+
_log.warning("[shared_baseline] patch build error: %s", exc)
|
| 1745 |
+
|
| 1746 |
+
_pt = threading.Thread(target=_run_patch, daemon=True)
|
| 1747 |
+
_pt.start()
|
| 1748 |
+
_pt.join(timeout=_FALLBACK_BUILD_TIMEOUT_SECONDS)
|
| 1749 |
+
|
| 1750 |
+
if patch_result[0] is not None:
|
| 1751 |
+
patch_bundle = patch_result[0]
|
| 1752 |
+
if persist_runtime_refresh:
|
| 1753 |
+
_queue_shared_baseline_bundle_persist(patch_bundle, source_status="runtime_request_patch")
|
| 1754 |
+
merged_bundle = _merge_shared_baseline_bundles(snapshot_bundle, patch_bundle)
|
| 1755 |
+
if snapshot_stale:
|
| 1756 |
+
merged_bundle["background_refresh_queued"] = queue_shared_baseline_refresh(
|
| 1757 |
+
batter_names=snapshot_batter_names,
|
| 1758 |
+
pitcher_names=snapshot_pitcher_names,
|
| 1759 |
+
)
|
| 1760 |
+
return _annotate_request_coverage(
|
| 1761 |
+
merged_bundle,
|
| 1762 |
+
requested_hitter_names=snapshot_batter_names,
|
| 1763 |
+
requested_pitcher_names=snapshot_pitcher_names,
|
| 1764 |
+
coverage_mode="request_completed_patch",
|
| 1765 |
+
background_refresh_queued=bool(merged_bundle.get("background_refresh_queued")),
|
| 1766 |
)
|
| 1767 |
+
else:
|
| 1768 |
+
_log.warning(
|
| 1769 |
+
"[shared_baseline] patch build timed out after %ds — serving partial snapshot",
|
| 1770 |
+
_FALLBACK_BUILD_TIMEOUT_SECONDS,
|
| 1771 |
+
)
|
| 1772 |
+
if persist_runtime_refresh:
|
| 1773 |
+
def _persist_patch_when_done() -> None:
|
| 1774 |
+
_pt.join()
|
| 1775 |
+
if patch_result[0] is not None:
|
| 1776 |
+
_queue_shared_baseline_bundle_persist(patch_result[0], source_status="runtime_request_patch")
|
| 1777 |
+
threading.Thread(target=_persist_patch_when_done, daemon=True).start()
|
| 1778 |
+
snapshot_bundle["snapshot_source_status"] = "patch_build_timeout"
|
| 1779 |
+
return _annotate_request_coverage(
|
| 1780 |
+
snapshot_bundle,
|
| 1781 |
+
requested_hitter_names=snapshot_batter_names,
|
| 1782 |
+
requested_pitcher_names=snapshot_pitcher_names,
|
| 1783 |
+
coverage_mode="request_completed_patch_partial",
|
| 1784 |
+
background_refresh_queued=False,
|
| 1785 |
+
)
|
| 1786 |
+
|
| 1787 |
+
runtime_result: list[dict | None] = [None]
|
| 1788 |
+
|
| 1789 |
+
def _run_fallback() -> None:
|
| 1790 |
+
try:
|
| 1791 |
+
runtime_result[0] = build_shared_baseline_bundle(
|
| 1792 |
batter_names=snapshot_batter_names,
|
| 1793 |
pitcher_names=snapshot_pitcher_names,
|
| 1794 |
)
|
| 1795 |
+
except Exception as exc:
|
| 1796 |
+
_log.warning("[shared_baseline] runtime fallback build error: %s", exc)
|
| 1797 |
+
|
| 1798 |
+
_rt = threading.Thread(target=_run_fallback, daemon=True)
|
| 1799 |
+
_rt.start()
|
| 1800 |
+
_rt.join(timeout=_FALLBACK_BUILD_TIMEOUT_SECONDS)
|
| 1801 |
+
|
| 1802 |
+
if runtime_result[0] is not None:
|
| 1803 |
+
runtime_bundle = runtime_result[0]
|
| 1804 |
+
runtime_bundle["snapshot_source_status"] = "runtime_fallback"
|
| 1805 |
+
runtime_bundle["runtime_fallback_used"] = True
|
| 1806 |
+
runtime_bundle["request_patch_used"] = False
|
| 1807 |
+
if persist_runtime_refresh:
|
| 1808 |
+
_queue_shared_baseline_bundle_persist(runtime_bundle, source_status="runtime_refreshed")
|
| 1809 |
+
if "snapshot_status" not in runtime_bundle:
|
| 1810 |
+
runtime_bundle["snapshot_status"] = snapshot_status
|
| 1811 |
return _annotate_request_coverage(
|
| 1812 |
+
runtime_bundle,
|
| 1813 |
requested_hitter_names=snapshot_batter_names,
|
| 1814 |
requested_pitcher_names=snapshot_pitcher_names,
|
| 1815 |
+
coverage_mode="runtime_fallback",
|
| 1816 |
+
background_refresh_queued=False,
|
| 1817 |
)
|
| 1818 |
+
else:
|
| 1819 |
+
_log.warning(
|
| 1820 |
+
"[shared_baseline] full fallback build timed out after %ds — returning empty bundle, "
|
| 1821 |
+
"will persist when complete",
|
| 1822 |
+
_FALLBACK_BUILD_TIMEOUT_SECONDS,
|
| 1823 |
+
)
|
| 1824 |
+
if persist_runtime_refresh:
|
| 1825 |
+
def _persist_runtime_when_done() -> None:
|
| 1826 |
+
_rt.join()
|
| 1827 |
+
if runtime_result[0] is not None:
|
| 1828 |
+
_queue_shared_baseline_bundle_persist(runtime_result[0], source_status="runtime_refreshed")
|
| 1829 |
+
threading.Thread(target=_persist_runtime_when_done, daemon=True).start()
|
| 1830 |
+
degraded: dict[str, Any] = {
|
| 1831 |
+
"blended_batter_df": pd.DataFrame(),
|
| 1832 |
+
"blended_pitcher_df": pd.DataFrame(),
|
| 1833 |
+
"batter_baseline_meta": pd.DataFrame(),
|
| 1834 |
+
"pitcher_baseline_meta": pd.DataFrame(),
|
| 1835 |
+
"snapshot_status": snapshot_status,
|
| 1836 |
+
"snapshot_source_status": "runtime_fallback_timeout",
|
| 1837 |
+
"runtime_fallback_used": True,
|
| 1838 |
+
"request_patch_used": False,
|
| 1839 |
+
}
|
| 1840 |
+
return _annotate_request_coverage(
|
| 1841 |
+
degraded,
|
| 1842 |
+
requested_hitter_names=snapshot_batter_names,
|
| 1843 |
+
requested_pitcher_names=snapshot_pitcher_names,
|
| 1844 |
+
coverage_mode="runtime_fallback_timeout",
|
| 1845 |
+
background_refresh_queued=False,
|
| 1846 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1847 |
|
| 1848 |
|
| 1849 |
def build_shared_baseline_bundle(
|
visualization/props_page.py
CHANGED
|
@@ -679,6 +679,9 @@ def _load_props_prepared_bundle(
|
|
| 679 |
max_age_seconds=60 * 60,
|
| 680 |
persist_runtime_refresh=True,
|
| 681 |
)
|
|
|
|
|
|
|
|
|
|
| 682 |
pitcher_statcast_df = bundle.get("blended_pitcher_df", pd.DataFrame())
|
| 683 |
starter_bundle = _load_props_starter_bundle(
|
| 684 |
raw=raw,
|
|
@@ -1987,6 +1990,19 @@ def render_props(
|
|
| 1987 |
raw=raw,
|
| 1988 |
probable_starters=probable_starters,
|
| 1989 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1990 |
prepared_signature = prepared_bundle.get("signature")
|
| 1991 |
if st.session_state.get("_props_prepared_signature") != prepared_signature:
|
| 1992 |
st.session_state["_props_prepared_signature"] = prepared_signature
|
|
|
|
| 679 |
max_age_seconds=60 * 60,
|
| 680 |
persist_runtime_refresh=True,
|
| 681 |
)
|
| 682 |
+
if bundle.get("snapshot_source_status") in ("runtime_fallback_timeout", "patch_build_timeout"):
|
| 683 |
+
import time as _time
|
| 684 |
+
st.session_state["props_baseline_reload_at"] = _time.time() + 80
|
| 685 |
pitcher_statcast_df = bundle.get("blended_pitcher_df", pd.DataFrame())
|
| 686 |
starter_bundle = _load_props_starter_bundle(
|
| 687 |
raw=raw,
|
|
|
|
| 1990 |
raw=raw,
|
| 1991 |
probable_starters=probable_starters,
|
| 1992 |
)
|
| 1993 |
+
import time as _time
|
| 1994 |
+
_reload_at = st.session_state.get("props_baseline_reload_at")
|
| 1995 |
+
if _reload_at:
|
| 1996 |
+
if _time.time() < _reload_at:
|
| 1997 |
+
st.info(
|
| 1998 |
+
"📊 **Player baseline data is loading in the background.** "
|
| 1999 |
+
"Props are shown with basic line analysis. "
|
| 2000 |
+
"The page will refresh automatically with full Statcast enrichment in about a minute."
|
| 2001 |
+
)
|
| 2002 |
+
else:
|
| 2003 |
+
del st.session_state["props_baseline_reload_at"]
|
| 2004 |
+
_load_props_prepared_bundle.clear()
|
| 2005 |
+
st.rerun()
|
| 2006 |
prepared_signature = prepared_bundle.get("signature")
|
| 2007 |
if st.session_state.get("_props_prepared_signature") != prepared_signature:
|
| 2008 |
st.session_state["_props_prepared_signature"] = prepared_signature
|