Skip to content

Commit 11ac359

Browse files
committed
Add support for multiple uvicorn workers via -w
1 parent 177b564 commit 11ac359

3 files changed

Lines changed: 43 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Alternatively, you can dynamically fetch the compounds data on startup using a S
2424
mol-search-sparql-service -s fetch_rhea.rq -e https://sparql.rhea-db.org/sparql -p 8000
2525
```
2626

27-
Other available optionally flags include `-d` (`--daemon`) to run the server in the background and write stdout/stderr to `server.log`. Port defaults to `8010` if `-p` is omitted.
27+
Other available optional flags include `-w` (`--workers`) to deploy multiple Uvicorn worker processes (default 1) and `-d` (`--daemon`) to run the server in the background and write stdout/stderr to `server.log`. Port defaults to `8010` if `-p` is omitted.
2828

2929
With 3 columns:
3030

src/mol_search_sparql_service/main.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def main():
1616

1717
parser.add_argument('-e', '--endpoint', help='SPARQL endpoint URL (required if using -s)')
1818
parser.add_argument('-p', '--port', type=int, default=8010, help='Port to run the server on (default: 8010)')
19+
parser.add_argument('-w', '--workers', type=int, default=1, help='Number of Uvicorn workers (default: 1)')
1920
parser.add_argument('-d', '--daemon', action='store_true', help='Daemonize the server process')
2021
args = parser.parse_args()
2122

@@ -37,15 +38,24 @@ def main():
3738
resp.raise_for_status()
3839

3940
fd, temp_path = tempfile.mkstemp(suffix='.tsv')
41+
# Important: Keep the temp file around for workers by not deleting it immediately
4042
with os.fdopen(fd, 'wb') as f:
4143
f.write(resp.content)
42-
44+
45+
# Pass to environment so workers can use it
46+
os.environ['COMPOUNDS_FILE'] = temp_path
47+
os.environ['DELETE_COMPOUNDS_FILE'] = '1'
48+
4349
engine.load_and_compile(temp_path)
44-
os.remove(temp_path)
50+
# We will rely on sparql_service or OS to clean this up, or clean it up after uvicorn exits.
4551
except Exception as e:
4652
print(f"Failed to fetch data from endpoint: {e}")
4753
sys.exit(1)
4854

55+
# Store explicit file path in environment for workers
56+
if args.file:
57+
os.environ['COMPOUNDS_FILE'] = args.file
58+
4959
# Daemonize if requested
5060
if args.daemon:
5161
# Double fork to detach from terminal completely
@@ -65,12 +75,15 @@ def main():
6575
os.dup2(f.fileno(), sys.stderr.fileno())
6676

6777
# 2. Start Server
68-
print(f"Starting SPARQL endpoint on port {args.port}...")
69-
78+
print(f"Starting SPARQL endpoint on port {args.port} with {args.workers} worker(s)...")
7079
import rdflib.plugins.sparql
7180
print(f"DEBUG: CUSTOM_EVALS keys: {list(rdflib.plugins.sparql.CUSTOM_EVALS.keys())}")
7281

73-
uvicorn.run(app, host="0.0.0.0", port=args.port)
82+
# Uvicorn requires an import string when using multiple workers
83+
if args.workers > 1:
84+
uvicorn.run("mol_search_sparql_service.sparql_service:app", host="0.0.0.0", port=args.port, workers=args.workers)
85+
else:
86+
uvicorn.run(app, host="0.0.0.0", port=args.port)
7487

7588
if __name__ == "__main__":
7689
main()

src/mol_search_sparql_service/sparql_service.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,29 @@ def sparql_assistant() -> str:
200200
# Functions are already registered via decorators on 'g'
201201
)
202202

203+
# Startup event to load data in worker processes
204+
import os
205+
import atexit
206+
207+
@app.on_event("startup")
208+
async def startup_event():
209+
compounds_file = os.environ.get('COMPOUNDS_FILE')
210+
# If the engine has no datasets (wasn't loaded by main.py due to workers>1), load it now
211+
if compounds_file and not engine.datasets:
212+
print(f"Worker initializing engine from: {compounds_file}")
213+
engine.load_and_compile(compounds_file)
214+
215+
@app.on_event("shutdown")
216+
async def shutdown_event():
217+
# Attempt to cleanup temp files if this is the "main" process
218+
# (or just let the OS handle temp files).
219+
delete_file = os.environ.get('DELETE_COMPOUNDS_FILE')
220+
compounds_file = os.environ.get('COMPOUNDS_FILE')
221+
if delete_file == '1' and compounds_file and os.path.exists(compounds_file):
222+
try:
223+
os.remove(compounds_file)
224+
except:
225+
pass
226+
203227
# Mount MCP Server (FastMCP's internal app)
204228
app.mount("/mcp", mcp.sse_app())

0 commit comments

Comments
 (0)