Syntrex Claude Sonnet 4.6 commited on
Commit
7b21bf1
·
1 Parent(s): dd66ccf

Add 40s timeout to baseline build branches to prevent props page hang

Browse files

Wraps 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>

Files changed (2) hide show
  1. data/shared_baseline.py +104 -35
  2. 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
- patch_bundle = build_shared_baseline_bundle(
1735
- batter_names=tuple(sorted(missing_hitter_names)),
1736
- pitcher_names=tuple(sorted(missing_pitcher_names)),
1737
- )
1738
- if persist_runtime_refresh:
1739
- _queue_shared_baseline_bundle_persist(
1740
- patch_bundle,
1741
- source_status="runtime_request_patch",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1742
  )
1743
- merged_bundle = _merge_shared_baseline_bundles(snapshot_bundle, patch_bundle)
1744
- if snapshot_stale:
1745
- merged_bundle["background_refresh_queued"] = queue_shared_baseline_refresh(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1746
  batter_names=snapshot_batter_names,
1747
  pitcher_names=snapshot_pitcher_names,
1748
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  return _annotate_request_coverage(
1750
- merged_bundle,
1751
  requested_hitter_names=snapshot_batter_names,
1752
  requested_pitcher_names=snapshot_pitcher_names,
1753
- coverage_mode="request_completed_patch",
1754
- background_refresh_queued=bool(merged_bundle.get("background_refresh_queued")),
1755
  )
1756
-
1757
- runtime_bundle = build_shared_baseline_bundle(
1758
- batter_names=snapshot_batter_names,
1759
- pitcher_names=snapshot_pitcher_names,
1760
- )
1761
- runtime_bundle["snapshot_source_status"] = "runtime_fallback"
1762
- runtime_bundle["runtime_fallback_used"] = True
1763
- runtime_bundle["request_patch_used"] = False
1764
- if persist_runtime_refresh:
1765
- _queue_shared_baseline_bundle_persist(
1766
- runtime_bundle,
1767
- source_status="runtime_refreshed",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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