Use OntoEnv in a long-running service¶
A web server or daemon should treat the environment as a resource it owns for its whole lifetime: connect once at startup, share the object, close it at shutdown.
The pattern¶
from contextlib import asynccontextmanager
from fastapi import FastAPI
from ontoenv import OntoEnv
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: open the environment and make it available to handlers.
env = OntoEnv.connect("/srv/ontology-env")
app.state.ontoenv = env
try:
yield
finally:
# Shutdown: flush pending writes and release the environment lock.
env.close()
app = FastAPI(lifespan=lifespan)
@app.get("/closure/{iri:path}")
def closure(iri: str):
view, imported = app.state.ontoenv.get_closure(iri)
return {"graphs": imported, "triples": len(view)}
Do not connect per request. Reopening the environment repeats work that
connect is designed to do once, and it makes it much harder to reason
about who owns the underlying storage.
The with statement is only sugar for calling close(); it changes
nothing about how the environment behaves. Use it in scripts, not here.
Refresh sources without restarting¶
connect does not read your ontology files — that is always an explicit
call. To pick up changes while the process runs:
env.update() # rescan search directories, refresh expired remotes
env.update(force=True) # reread every known source regardless of age
env.update("https://example.org/site.ttl") # just this one source
All three follow owl:imports, so dependencies are refreshed along with the
ontologies that led to them.
Run this on a timer or from an admin endpoint. Reads happening concurrently continue to see a consistent view.
Multiple worker processes¶
A persistent environment allows either one writer or multiple read-only connections at a time. A read-only process waits while a writer holds the environment lock. For a multi-process server, prepare the environment in a writable process, close it, and then start the read-only workers:
# Provisioning: this process must finish and close before workers connect.
with OntoEnv.connect("/srv/ontology-env") as env:
env.update()
# Worker startup: several processes can hold shared read-only locks.
env = OntoEnv.open("/srv/ontology-env", read_only=True)
Read-only connections never write to the environment directory. Configuration passed while opening a read-only connection applies to that session only and is not persisted.
To update sources later, stop or drain the read-only workers, open one writable
connection, run update(), close it, and then reopen the workers. A
long-lived writer cannot update alongside read-only worker processes because
it holds the exclusive lock.
Fail fast if the environment is not there¶
connect creates a missing environment, which is usually what you want. If
deployment is supposed to have prepared the environment already and a missing
one indicates a broken deploy, say so explicitly:
env = OntoEnv.open("/srv/ontology-env", read_only=True)
open raises if the environment does not exist, and never creates, scans,
or reconciles anything.
Opening an environment compares all six entry points.
Handle recovery at startup¶
If a previous process was killed between writing a graph and committing its
index, connect raises CatalogRecoveryError. Decide up front whether
your service repairs itself or refuses to start:
from ontoenv import OntoEnv, CatalogRecoveryError
try:
env = OntoEnv.connect("/srv/ontology-env")
except CatalogRecoveryError:
log.warning("recovering ontology environment after interrupted write")
env = OntoEnv.recover("/srv/ontology-env")
Recovery reads every stored graph and rebuilds the catalog; connect
normally reads the existing catalog. See Recover an interrupted environment.
Keep memory low¶
Prefer get_* over copy_* in request handlers that do not mutate the
result. A view reads from the on-disk snapshot; a copy materializes the whole
closure in Python memory on every call.
# Good — read-only view, no materialization
view, _ = env.get_closure(iri)
rows = view.query("SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }")
# Only when the caller must mutate or export the graph
g, _ = env.copy_closure(iri)
For streaming responses, skip the graph wrapper entirely:
for s, p, o in env.iter_closure_triples(iri):
yield serialize(s, p, o)
See also
Views and copies and Performance for the numbers behind this advice.