33from typing import TYPE_CHECKING , Any , Dict , Iterable , Optional , Tuple , Union
44
55import rdflib
6- from ontoenv import CatalogRecoveryError , OntoEnv
6+ from ontoenv import CatalogRecoveryError , OntoEnv , UnresolvedImportError
77
88from buildingmotif .database .graph_connection import _is_uuid
99
1010if TYPE_CHECKING :
1111 from buildingmotif .database .graph_connection import GraphConnection
1212
13+ # ``UnresolvedImportError`` is re-exported so callers can catch an
14+ # unresolvable ``owl:imports`` without importing ontoenv themselves -- this
15+ # module is the one place BuildingMOTIF talks to it.
16+ __all__ = [
17+ "BuildingMOTIFGraphStore" ,
18+ "OntologyEnvironment" ,
19+ "OntologyImportsNotFound" ,
20+ "UnresolvedImportError" ,
21+ ]
22+
1323
1424class OntologyImportsNotFound (Exception ):
1525 """Raised when one or more owl:imports cannot be resolved."""
@@ -43,7 +53,7 @@ def __init__(
4353 else None
4454 )
4555
46- # ontoenv >=0.6.0a8 deprecated the `init_from_store` flag in favour of
56+ # ontoenv >=0.6 deprecated the `init_from_store` flag in favour of
4757 # explicit lifecycle entry points. The two cases are not the same call:
4858 #
4959 # - persistent: `connect` *is* "create it or reuse the saved index",
@@ -64,44 +74,42 @@ def __init__(
6474 def _connect_recovering (
6575 path : str , store : Optional ["BuildingMOTIFGraphStore" ], options : Dict [str , Any ]
6676 ) -> OntoEnv :
67- """``OntoEnv.connect``, clearing a stale interrupted-mutation marker.
68-
69- **This is a workaround for an ontoenv defect; delete it once ontoenv
70- stops leaving the marker for tolerable failures.**
77+ """``OntoEnv.connect``, rebuilding the catalog after an interrupted write.
7178
7279 ontoenv writes ``.ontoenv/catalog.pending`` when it begins a batched
73- mutation and removes it only when the batch finishes cleanly
74- (``BatchScope::run``). An *unresolvable* ``owl:imports`` makes the batch
75- end in error, so the marker survives -- and every later open of that
76- cache raises ``CatalogRecoveryError``.
77-
78- That is routine here rather than exceptional: Brick declares eight
79- imports that do not resolve offline (several 404 even online), so a
80- single ``Library.from_ontology(Brick)`` permanently poisons a
81- persistent cache. An ontology with no imports leaves no marker, which
82- is how this was isolated. ontoenv exposes no recovery call, so the only
83- way through is to remove the file.
84-
85- The cost of doing so: after a genuine crash mid-write the marker is a
86- real signal, and this clears it. That is the trade being made -- one
87- warned retry, and if the retry also fails the error propagates.
80+ mutation and removes it when the batch commits; finding it at open time
81+ means a process died mid-write, and ontoenv raises
82+ ``CatalogRecoveryError`` rather than trust an index that may not
83+ describe every stored graph.
84+
85+ Under ontoenv <0.6 that was a routine condition rather than an
86+ exceptional one -- a merely *unresolvable* ``owl:imports`` also left the
87+ marker, so one ``Library.load(ontology_graph=Brick)`` permanently
88+ poisoned a persistent cache -- and with no recovery call available the
89+ only way through was to delete the marker by hand. 0.6 fixed both ends:
90+ a tolerated missing import now commits cleanly and leaves no marker, and
91+ ``OntoEnv.recover`` rebuilds the catalog properly.
92+
93+ So reaching this handler now means a genuinely interrupted write, and
94+ recovery is the supported answer to one. It is taken automatically
95+ because the alternative is a dead end: recovering a custom store
96+ requires passing that store, and ours does not exist until a
97+ ``BuildingMOTIF`` has been constructed -- which is what failed. It is
98+ logged at warning level because it rescans every stored graph and is
99+ therefore much slower than a normal open. ontoenv verifies the rebuild
100+ and only clears the marker once the replacement index is published, so
101+ a failed recovery leaves the marker in place and raises.
88102 """
89103 try :
90104 return OntoEnv .connect (path , graph_store = store , ** options )
91105 except CatalogRecoveryError :
92- marker = Path (path ) / ".ontoenv" / "catalog.pending"
93- if not marker .exists ():
94- raise
95106 logging .getLogger (__name__ ).warning (
96- "Clearing stale ontoenv recovery marker at %s. This is normally "
97- "left behind by an unresolvable owl:imports rather than by an "
98- "actual interrupted write; if this repeats, the ontology cache "
99- "at %s may genuinely be damaged and can be deleted." ,
100- marker ,
107+ "Rebuilding the ontology catalog at %s after an interrupted "
108+ "write. This rescans every stored ontology and may take a "
109+ "while." ,
101110 path ,
102111 )
103- marker .unlink ()
104- return OntoEnv .connect (path , graph_store = store , ** options )
112+ return OntoEnv .recover (path , graph_store = store , ** options )
105113
106114 def close (self ) -> None :
107115 self .env .close ()
@@ -144,20 +152,20 @@ def closure_names(self, ontology: str, recursion_depth: int = -1) -> list[str]:
144152 Callers that only need to know *what is in* the closure should use this
145153 rather than discarding :py:meth:`closure_copy`'s first return value.
146154 Measured on the Brick 1.4 closure (15 graphs, ~155k triples), three
147- reps, on ontoenv 0.6.0a9 :
155+ reps, on ontoenv 0.6.0 :
148156
149157 - ``list_closure`` -- 0.000s, 0.000s, 0.000s
150- - ``copy_closure`` -- 3.972s, 3.530s , 3.623s (materializes every time)
151- - ``get_closure`` -- 2.053s , 2.077s , 2.059s (read-only view)
158+ - ``copy_closure`` -- 4.601s, 4.081s , 3.849s (materializes every time)
159+ - ``get_closure`` -- 2.435s , 2.418s , 2.479s (read-only view)
152160
153161 All three report the same 15 names, so for a name lookup this is free
154162 where the alternatives cost seconds.
155163
156- The ``get_closure`` shape changed between releases and the numbers are
157- worth keeping for that reason: on a8 it read 8.846s, 0.000s, 0.000s --
158- one expensive eager index build, then cached. On a9 it is a flat ~2.05s
159- per call. Cheaper on first use, but no longer free on repeat, so
160- "bind once and query many times" is not the win it was on a8 .
164+ The ``get_closure`` shape changed across the 0.6 alphas and the numbers
165+ are worth keeping for that reason: on 0.6.0a8 it read 8.846s, 0.000s,
166+ 0.000s -- one expensive eager index build, then cached. Since a9 it is
167+ a flat ~2.4s per call. Cheaper on first use, but no longer free on
168+ repeat, so "bind once and query many times" is not the win it was.
161169 """
162170 return list (self .env .list_closure (ontology , recursion_depth = recursion_depth ))
163171
@@ -206,7 +214,7 @@ def ontology_names(self) -> list[str]:
206214 def knows (self , ontology : str ) -> bool :
207215 """Whether ontoenv can resolve ``ontology``.
208216
209- ontoenv >=0.6.0a8 implements the container protocol, so this is a
217+ ontoenv >=0.6 implements the container protocol, so this is a
210218 direct lookup rather than building the full name list to test one
211219 membership. It is also *broader* than ``ontology in ontology_names()``:
212220 ``in`` resolves aliases and source URLs as well as canonical names, so
@@ -225,7 +233,7 @@ def ensure_and_get_closure(
225233 """Ensure graph is registered in ontoenv, then return its import closure.
226234
227235 The returned graph is materialized and mutable. ontoenv's read-only
228- ``get_closure`` view would avoid the copy, but as of 0.6.0a9 a
236+ ``get_closure`` view would avoid the copy, but as of 0.6.0 a
229237 ``ViewGraph`` deliberately **does not subclass rdflib.Graph**, so it
230238 cannot be handed back through this signature.
231239
0 commit comments