-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmain.py
More file actions
245 lines (211 loc) · 8.33 KB
/
Copy pathmain.py
File metadata and controls
245 lines (211 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
#!/usr/bin/env python3.12
"""DeepGraph - Hierarchical ML Research Knowledge Engine."""
import os
import sys
import threading
from pathlib import Path
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent))
from compat.filelock import FileLock
from config import (
APP_NAME,
AUTO_PIPELINE_ENABLED,
AUTO_RESEARCH_ENABLED,
BACKFILL_GRAPH_ON_START,
COMPUTE_BACKENDS_ENABLED,
IDEA_WORKSPACE_DIR,
REFRESH_MERGE_CANDIDATES_ON_START,
ROOT_NODE_ID,
SCOPED_INGESTION_WORKER_ENABLED,
WEB_HOST,
WEB_PORT,
WORKSPACE_DIR,
PDF_CACHE_DIR,
)
from db.database import describe_backend, init_db
from db.evidence_graph import (
backfill_entity_resolutions,
backfill_graph_from_structured_data,
refresh_merge_candidates,
)
from db.taxonomy import seed_taxonomy, backfill_result_taxonomy
from web.app import app
_PROCESS_LOCK = None
_PROCESS_LOCK_PATH = (
Path(os.environ.get("TEMP", str(Path.home() / ".cache"))) / "deepgraph-main.lock"
if os.name == "nt"
else Path("/tmp/deepgraph-main.lock")
)
def _current_lock_owner() -> str | None:
try:
owner = _PROCESS_LOCK_PATH.read_text(encoding="utf-8").strip()
except OSError:
return None
return owner or None
def _try_acquire_process_lock() -> bool:
global _PROCESS_LOCK
if _PROCESS_LOCK is not None:
return True
lock = FileLock(str(_PROCESS_LOCK_PATH))
if not lock.try_acquire():
return False
try:
handle = getattr(lock, "_handle")
handle.seek(0)
handle.truncate()
handle.write(f"{os.getpid()}\n")
handle.flush()
except OSError:
lock.release()
return False
_PROCESS_LOCK = lock
return True
def _release_process_lock() -> None:
global _PROCESS_LOCK
if _PROCESS_LOCK is None:
return
try:
_PROCESS_LOCK.release()
finally:
_PROCESS_LOCK = None
def _serve_http() -> None:
print(f"Starting {APP_NAME} at http://{WEB_HOST}:{WEB_PORT} (root node: {ROOT_NODE_ID})", flush=True)
try:
from waitress import serve
except ImportError:
print(
"Waitress is not installed; falling back to Flask dev server. "
"Install waitress for production deployments.",
flush=True,
)
app.run(host=WEB_HOST, port=WEB_PORT, debug=False, threaded=True)
return
serve(app, host=WEB_HOST, port=WEB_PORT, threads=8)
def _run_startup_maintenance(label: str, fn) -> bool:
print(f"{label}...", flush=True)
try:
fn()
except Exception as exc:
if "database is locked" in str(exc).lower():
print(f"{label} skipped: database is locked; continuing startup.", flush=True)
return False
raise
print(f"{label} ready.", flush=True)
return True
def _start_degradable_worker(label: str, start_fn, ok_statuses: set[str]) -> bool:
"""Start a background worker that is allowed to stay down.
A worker that fails closed must not take the web process with it: the
fail-closed contract is "this worker does not run", not "nothing runs".
Refusing to serve HTTP because an optional ingestion loop declined to start
turns a scoped denial into a site outage, so a bad status or an exception
is logged and skipped rather than raised.
"""
print(f"Starting {label}...", flush=True)
try:
status = start_fn() or {}
state = status.get("status") or "unknown"
except Exception as exc: # background worker startup must never kill the site
print(
f"{label} degraded: startup raised {type(exc).__name__}: {exc}; "
"the worker stays down and startup continues.",
flush=True,
)
return False
if state not in ok_statuses:
print(
f"{label} degraded: failed closed with status '{state}'"
+ (f" (reason: {status['reason']})" if status.get("reason") else "")
+ "; the worker stays down and startup continues.",
flush=True,
)
return False
print(f"{label} ready.", flush=True)
return True
def main():
if not _try_acquire_process_lock():
owner = _current_lock_owner()
if owner:
print(f"DeepGraph main already running under pid {owner}; refusing duplicate startup.", flush=True)
else:
print("DeepGraph main already running; refusing duplicate startup.", flush=True)
return
# Ensure directories exist
WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
PDF_CACHE_DIR.mkdir(parents=True, exist_ok=True)
IDEA_WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
try:
# Initialize database
print("Initializing database...", flush=True)
init_db()
backend = describe_backend()
print("Database ready.", flush=True)
print(f"Database target: {backend['target']} ({backend['backend']})", flush=True)
# Startup maintenance is idempotent; do not let transient SQLite writers
# prevent the controller, auto-research loop, and GPU scheduler from booting.
_run_startup_maintenance("Seeding taxonomy tree", seed_taxonomy)
_run_startup_maintenance("Backfilling result taxonomy links", backfill_result_taxonomy)
_run_startup_maintenance("Backfilling entity resolution map", backfill_entity_resolutions)
# Skip heavy backfills on startup for faster boot
# These can run in the background via pipeline
print("Skipping graph/merge backfill (run in pipeline instead).", flush=True)
if AUTO_PIPELINE_ENABLED:
from orchestrator.paper_worker import start as start_paper_worker
_start_degradable_worker(
"Paper ingestion worker",
start_paper_worker,
{"started", "already_running", "already_running_elsewhere"},
)
if SCOPED_INGESTION_WORKER_ENABLED:
from orchestrator.scoped_ingestion_worker import (
start as start_scoped_ingestion_worker,
)
print("Starting scoped ingestion worker...", flush=True)
scoped_ingestion_status = start_scoped_ingestion_worker()
if scoped_ingestion_status.get("status") not in {
"started",
"already_running",
}:
raise RuntimeError(
"Scoped ingestion worker failed closed during startup: "
f"{scoped_ingestion_status.get('status') or 'unknown'}"
)
print("Scoped ingestion worker ready.", flush=True)
configured_compute = {
str(value).strip().lower()
for value in (COMPUTE_BACKENDS_ENABLED or [])
}
if AUTO_RESEARCH_ENABLED or configured_compute.intersection(
{"local_gpu", "ssh_gpu", "colab_gpu"}
):
from orchestrator.gpu_scheduler import start as start_gpu_scheduler
# Durable compute reconciliation must finish before the research
# loop or a configured asynchronous backend worker starts.
print("Starting compute scheduler and recovery...", flush=True)
scheduler_status = start_gpu_scheduler()
if scheduler_status.get("status") not in {
"started",
"already_running",
"already_running_elsewhere",
}:
raise RuntimeError(
"Compute scheduler failed closed during startup: "
f"{scheduler_status.get('status') or 'unknown'}"
)
print("Compute scheduler and recovery ready.", flush=True)
if AUTO_RESEARCH_ENABLED:
from orchestrator.auto_research import start as start_auto_research
print("Starting Auto Research worker...", flush=True)
start_auto_research()
print("Auto Research worker ready.", flush=True)
# Warm the /api/stats cache in the background so the first browser paint
# is served from a warm cache instead of a cold ~30-COUNT(*) query
# (issue #34).
from web.app import prewarm_stats_cache
print("Prewarming stats cache in background...", flush=True)
threading.Thread(target=prewarm_stats_cache, daemon=True).start()
# Start web server
_serve_http()
finally:
_release_process_lock()
if __name__ == "__main__":
main()