66import json
77import subprocess
88import sys
9+ import jpype
910
1011from collections import defaultdict , Counter
1112from functools import singledispatchmethod , reduce , cached_property
@@ -1237,10 +1238,68 @@ def __init__(self, ontology: Union[SyncOntology, str], reasoner="HermiT"):
12371238 self .mapper = self .ontology .mapper
12381239 self .inference_types_mapping = import_and_include_axioms_generators ()
12391240 self ._owlapi_reasoner = initialize_reasoner (reasoner , self ._owlapi_ontology )
1241+ # A single-threaded Java executor shared across all instances() calls.
1242+ # Using one reusable executor ensures that reasoning tasks are serialized,
1243+ # preventing "ExtensionManager is not reentrant" errors that occur when
1244+ # two Java threads concurrently access a non-thread-safe reasoner (e.g. HermiT).
1245+ # We use a daemon-thread factory so that lingering reasoning threads
1246+ # (e.g. ones that ignored an interrupt after a timeout) do not prevent
1247+ # the JVM / Python process from exiting.
1248+ Executors = JClass ("java.util.concurrent.Executors" )
1249+ ThreadFactory = JClass ("java.util.concurrent.ThreadFactory" )
1250+
1251+ @jpype .JImplements (ThreadFactory )
1252+ class _DaemonThreadFactory :
1253+ @jpype .JOverride
1254+ def newThread (self_ , r ):
1255+ t = jpype .JClass ("java.lang.Thread" )(r )
1256+ t .setDaemon (True )
1257+ t .setName ("owlapy-reasoner-worker" )
1258+ return t
1259+
1260+ self ._reasoning_executor = Executors .newSingleThreadExecutor (_DaemonThreadFactory ())
1261+
1262+ def __del__ (self ):
1263+ """Shut down the shared Java executor and dispose the reasoner when garbage-collected."""
1264+ try :
1265+ self .close ()
1266+ except Exception :
1267+ pass
1268+
1269+ def close (self ):
1270+ """Explicitly shut down the reasoner and the shared Java executor.
1271+
1272+ Call this when done with the reasoner to release all Java resources
1273+ and allow the process to exit cleanly.
1274+ """
1275+ # 1. Dispose the OWL API reasoner (releases internal threads & caches).
1276+ if hasattr (self , "_owlapi_reasoner" ) and self ._owlapi_reasoner is not None :
1277+ try :
1278+ self ._owlapi_reasoner .dispose ()
1279+ except Exception :
1280+ pass
1281+ self ._owlapi_reasoner = None
1282+
1283+ # 2. Shut down the executor and wait briefly for its thread to finish.
1284+ if hasattr (self , "_reasoning_executor" ) and self ._reasoning_executor is not None :
1285+ self ._reasoning_executor .shutdownNow ()
1286+ try :
1287+ TimeUnit = JClass ("java.util.concurrent.TimeUnit" )
1288+ self ._reasoning_executor .awaitTermination (5 , TimeUnit .SECONDS )
1289+ except Exception :
1290+ pass
1291+ self ._reasoning_executor = None
1292+
1293+ def __enter__ (self ):
1294+ return self
1295+
1296+ def __exit__ (self , exc_type , exc_val , exc_tb ):
1297+ self .close ()
1298+ return False
12401299
12411300 def _instances (self , ce : OWLClassExpression , direct = False ) -> Set [OWLNamedIndividual ]:
12421301 """
1243- Get the instances for a given class expression using HermiT .
1302+ Get the instances for a given class expression.
12441303
12451304 Args:
12461305 ce (OWLClassExpression): The class expression in OWLAPY format.
@@ -1256,7 +1315,59 @@ def _instances(self, ce: OWLClassExpression, direct=False) -> Set[OWLNamedIndivi
12561315 return {self .mapper .map_ (ind ) for ind in flattened_instances }
12571316
12581317 def instances (self , ce : OWLClassExpression , direct : bool = False , timeout : int = 1000 ):
1259- return run_with_timeout (self ._instances , timeout , (ce , direct ))
1318+ """
1319+ Get the instances for a given class expression with a Java-level timeout.
1320+
1321+ Uses Java's ExecutorService to run the reasoning task in a separate Java thread,
1322+ enabling proper timeout and interruption of the underlying Java reasoner (e.g. HermiT,
1323+ Pellet). This overcomes the limitation of Python's threading which cannot interrupt
1324+ JPype Java calls.
1325+
1326+ Args:
1327+ ce (OWLClassExpression): The class expression in OWLAPY format.
1328+ direct (bool): Whether to get direct instances or not. Defaults to False.
1329+ timeout (int): Timeout in seconds for the reasoning task. Defaults to 1000.
1330+
1331+ Returns:
1332+ set: A set of individuals classified by the given class expression.
1333+ Returns an empty set if the timeout is exceeded.
1334+ """
1335+ mapped_ce = self .mapper .map_ (ce )
1336+
1337+ TimeUnit = JClass ("java.util.concurrent.TimeUnit" )
1338+
1339+ # Submit the reasoning task to the shared single-threaded executor.
1340+ # Using a shared executor (created once in __init__) ensures that all
1341+ # instances() calls are serialized on one Java thread, preventing the
1342+ # "ExtensionManager is not reentrant" error that arises when a new
1343+ # executor is created per call and two threads overlap during shutdown.
1344+ owlapi_reasoner = self ._owlapi_reasoner
1345+
1346+ @jpype .JImplements ("java.util.concurrent.Callable" )
1347+ class _ReasonerCallable :
1348+ @jpype .JOverride
1349+ def call (self_ ):
1350+ return owlapi_reasoner .getInstances (mapped_ce , direct )
1351+
1352+ future = self ._reasoning_executor .submit (_ReasonerCallable ())
1353+ try :
1354+ instances = future .get (timeout , TimeUnit .SECONDS )
1355+ flattened_instances = instances .getFlattened ()
1356+ return {self .mapper .map_ (ind ) for ind in flattened_instances }
1357+ except jpype .JException as e :
1358+ # java.util.concurrent.TimeoutException when reasoning exceeds the timeout
1359+ if "TimeoutException" in type (e ).__name__ :
1360+ future .cancel (True ) # Interrupt the Java reasoning thread
1361+ logger .warning (f"Reasoner timed out after { timeout } seconds for instances query on: { ce } " )
1362+ return set ()
1363+ # java.util.concurrent.ExecutionException wraps exceptions from the Callable
1364+ elif "ExecutionException" in type (e ).__name__ :
1365+ cause = e .getCause ()
1366+ raise RuntimeError (
1367+ f"Reasoning failed for class expression '{ ce } ': { cause } "
1368+ ) from None
1369+ else :
1370+ raise
12601371
12611372 def equivalent_classes (self , ce : OWLClassExpression ):
12621373 """
0 commit comments