Aaron Brown commited on
Commit
890f5f3
·
1 Parent(s): fb68239

Robust app startup: handle missing openenv/runtime gracefully

Browse files

app.py now catches import and initialization errors at every level:
- If ManagedSnapshotRuntime fails, runs without managed snapshots
- If openenv is missing, creates standalone FastAPI with equivalent
endpoints (/health, /reset, /step, /state)
- If create_app() fails entirely, creates minimal health endpoint
so HF Spaces can at least show diagnostic info

Files changed (1) hide show
  1. src/open_range/server/app.py +122 -22
src/open_range/server/app.py CHANGED
@@ -1,45 +1,145 @@
1
- """FastAPI application wired through the OpenEnv app factory."""
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
 
 
 
 
5
  from fastapi import FastAPI
6
- from openenv.core.env_server import create_app as create_openenv_app
7
 
8
- from open_range.server.console import console_router
9
- from open_range.server.environment import RangeEnvironment
10
- from open_range.server.models import RangeAction, RangeObservation
11
- from open_range.server.runtime import ManagedSnapshotRuntime
12
 
13
 
14
  def create_app() -> FastAPI:
15
- """Create the OpenRange app using the standard OpenEnv contract."""
16
- runtime = ManagedSnapshotRuntime.from_env()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def env_factory() -> RangeEnvironment:
19
  return RangeEnvironment(runtime=runtime)
20
 
21
- app = create_openenv_app(
22
- env_factory,
23
- RangeAction,
24
- RangeObservation,
25
- env_name="open_range",
26
- )
27
- app.state.env = env_factory()
28
- app.state.runtime = runtime
29
- app.add_event_handler("startup", runtime.start)
30
- app.add_event_handler("shutdown", runtime.stop)
31
- app.include_router(console_router)
32
- return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  def main() -> None:
36
  """Run the installed package entrypoint via uvicorn."""
37
  import uvicorn
38
-
39
  uvicorn.run("open_range.server.app:app", host="0.0.0.0", port=8000)
40
 
41
 
42
- app = create_app()
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  if __name__ == "__main__":
45
  main()
 
1
+ """FastAPI application for OpenRange.
2
+
3
+ Uses the OpenEnv app factory when openenv is installed, otherwise
4
+ creates a standalone FastAPI app with equivalent endpoints.
5
+ """
6
 
7
  from __future__ import annotations
8
 
9
+ import logging
10
+ import sys
11
+ import traceback
12
+
13
  from fastapi import FastAPI
 
14
 
15
+ logger = logging.getLogger(__name__)
 
 
 
16
 
17
 
18
  def create_app() -> FastAPI:
19
+ """Create the OpenRange app.
20
+
21
+ Tries the OpenEnv factory first; falls back to a standalone
22
+ FastAPI app if openenv is not installed or if the runtime
23
+ fails to initialise (e.g. missing manifest on HF Spaces).
24
+ """
25
+ from open_range.server.environment import RangeEnvironment
26
+ from open_range.server.models import RangeAction, RangeObservation
27
+
28
+ # Try to create the managed runtime (snapshot pool, validator, etc.)
29
+ runtime = None
30
+ try:
31
+ from open_range.server.runtime import ManagedSnapshotRuntime
32
+ runtime = ManagedSnapshotRuntime.from_env()
33
+ except Exception:
34
+ logger.warning(
35
+ "ManagedSnapshotRuntime.from_env() failed — running without managed snapshots:\n%s",
36
+ traceback.format_exc(),
37
+ )
38
 
39
  def env_factory() -> RangeEnvironment:
40
  return RangeEnvironment(runtime=runtime)
41
 
42
+ # Try OpenEnv factory first
43
+ try:
44
+ from openenv.core.env_server import create_app as create_openenv_app
45
+ fastapp = create_openenv_app(
46
+ env_factory,
47
+ RangeAction,
48
+ RangeObservation,
49
+ env_name="open_range",
50
+ )
51
+ except Exception:
52
+ logger.warning(
53
+ "OpenEnv create_app failed — creating standalone FastAPI:\n%s",
54
+ traceback.format_exc(),
55
+ )
56
+ fastapp = _create_standalone_app(env_factory)
57
+
58
+ fastapp.state.env = env_factory()
59
+ if runtime is not None:
60
+ fastapp.state.runtime = runtime
61
+ fastapp.add_event_handler("startup", runtime.start)
62
+ fastapp.add_event_handler("shutdown", runtime.stop)
63
+
64
+ try:
65
+ from open_range.server.console import console_router
66
+ fastapp.include_router(console_router)
67
+ except Exception:
68
+ pass # Console router is optional
69
+
70
+ return fastapp
71
+
72
+
73
+ def _create_standalone_app(
74
+ env_factory: object,
75
+ ) -> FastAPI:
76
+ """Standalone FastAPI app with OpenEnv-compatible endpoints.
77
+
78
+ Used when the openenv package is not available.
79
+ """
80
+ from open_range.server.models import RangeAction, RangeObservation
81
+
82
+ fastapp = FastAPI(title="OpenRange", version="0.1.0")
83
+ _env_holder: dict = {}
84
+
85
+ def _get_env():
86
+ if "env" not in _env_holder:
87
+ _env_holder["env"] = env_factory() # type: ignore[operator]
88
+ return _env_holder["env"]
89
+
90
+ @fastapp.get("/health")
91
+ def health():
92
+ return {"status": "healthy"}
93
+
94
+ @fastapp.get("/metadata")
95
+ def metadata():
96
+ env = _get_env()
97
+ return env.get_metadata()
98
+
99
+ @fastapp.post("/reset")
100
+ def reset(seed: int | None = None, episode_id: str | None = None):
101
+ env = _get_env()
102
+ obs = env.reset(seed=seed, episode_id=episode_id)
103
+ return {"observation": obs.model_dump()}
104
+
105
+ @fastapp.post("/step")
106
+ def step(action: RangeAction):
107
+ env = _get_env()
108
+ obs = env.step(action)
109
+ return {
110
+ "observation": obs.model_dump(),
111
+ "reward": obs.reward,
112
+ "done": obs.done,
113
+ }
114
+
115
+ @fastapp.get("/state")
116
+ def state():
117
+ env = _get_env()
118
+ return env.state.model_dump()
119
+
120
+ return fastapp
121
 
122
 
123
  def main() -> None:
124
  """Run the installed package entrypoint via uvicorn."""
125
  import uvicorn
 
126
  uvicorn.run("open_range.server.app:app", host="0.0.0.0", port=8000)
127
 
128
 
129
+ # Module-level app creation with error reporting
130
+ try:
131
+ app = create_app()
132
+ except Exception:
133
+ # If create_app fails entirely, print the error and create a minimal
134
+ # health-only app so HF Spaces doesn't show "no logs".
135
+ traceback.print_exc()
136
+ print("[app.py] FATAL: create_app() failed. Creating minimal health endpoint.", file=sys.stderr)
137
+ app = FastAPI(title="OpenRange (degraded)")
138
+
139
+ @app.get("/health")
140
+ def _health():
141
+ return {"status": "degraded", "error": "App failed to initialize"}
142
+
143
 
144
  if __name__ == "__main__":
145
  main()