Resolve lock leakage issue during user cancellation handling
• Change default log level to INFO • Force enable error logging output • Add lock cleanup rollback protection • Handle LLM cache persistence errors • Fix async task exception handling
This commit is contained in:
parent
77336e50b6
commit
a9ec15e669
3 changed files with 285 additions and 79 deletions
|
|
@ -12,7 +12,7 @@ from lightrag.exceptions import PipelineNotInitializedError
|
||||||
|
|
||||||
|
|
||||||
# Define a direct print function for critical logs that must be visible in all processes
|
# Define a direct print function for critical logs that must be visible in all processes
|
||||||
def direct_log(message, enable_output: bool = False, level: str = "DEBUG"):
|
def direct_log(message, enable_output: bool = False, level: str = "INFO"):
|
||||||
"""
|
"""
|
||||||
Log a message directly to stderr to ensure visibility in all processes,
|
Log a message directly to stderr to ensure visibility in all processes,
|
||||||
including the Gunicorn master process.
|
including the Gunicorn master process.
|
||||||
|
|
@ -44,7 +44,6 @@ def direct_log(message, enable_output: bool = False, level: str = "DEBUG"):
|
||||||
}
|
}
|
||||||
message_level = level_mapping.get(level.upper(), logging.DEBUG)
|
message_level = level_mapping.get(level.upper(), logging.DEBUG)
|
||||||
|
|
||||||
# print(f"Diret_log: {level.upper()} {message_level} ? {current_level}", file=sys.stderr, flush=True)
|
|
||||||
if message_level >= current_level:
|
if message_level >= current_level:
|
||||||
print(f"{level}: {message}", file=sys.stderr, flush=True)
|
print(f"{level}: {message}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
@ -168,7 +167,7 @@ class UnifiedLock(Generic[T]):
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}': {e}",
|
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}': {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=self._enable_logging,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -199,7 +198,7 @@ class UnifiedLock(Generic[T]):
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}': {e}",
|
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}': {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=self._enable_logging,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If main lock release failed but async lock hasn't been released, try to release it
|
# If main lock release failed but async lock hasn't been released, try to release it
|
||||||
|
|
@ -223,7 +222,7 @@ class UnifiedLock(Generic[T]):
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== Lock == Process {self._pid}: Failed to release async lock after main lock failure: {inner_e}",
|
f"== Lock == Process {self._pid}: Failed to release async lock after main lock failure: {inner_e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=self._enable_logging,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
@ -247,7 +246,7 @@ class UnifiedLock(Generic[T]):
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}' (sync): {e}",
|
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}' (sync): {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=self._enable_logging,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -269,7 +268,7 @@ class UnifiedLock(Generic[T]):
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}' (sync): {e}",
|
f"== Lock == Process {self._pid}: Failed to release lock '{self._name}' (sync): {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=self._enable_logging,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -401,7 +400,7 @@ def _perform_lock_cleanup(
|
||||||
direct_log(
|
direct_log(
|
||||||
f"== {lock_type} Lock == Cleanup failed: {e}",
|
f"== {lock_type} Lock == Cleanup failed: {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=False,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
return 0, earliest_cleanup_time, last_cleanup_time
|
return 0, earliest_cleanup_time, last_cleanup_time
|
||||||
|
|
||||||
|
|
@ -689,7 +688,7 @@ class KeyedUnifiedLock:
|
||||||
direct_log(
|
direct_log(
|
||||||
f"Error during multiprocess lock cleanup: {e}",
|
f"Error during multiprocess lock cleanup: {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=False,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Cleanup async locks using generic function
|
# 2. Cleanup async locks using generic function
|
||||||
|
|
@ -718,7 +717,7 @@ class KeyedUnifiedLock:
|
||||||
direct_log(
|
direct_log(
|
||||||
f"Error during async lock cleanup: {e}",
|
f"Error during async lock cleanup: {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=False,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Get current status after cleanup
|
# 3. Get current status after cleanup
|
||||||
|
|
@ -772,7 +771,7 @@ class KeyedUnifiedLock:
|
||||||
direct_log(
|
direct_log(
|
||||||
f"Error getting keyed lock status: {e}",
|
f"Error getting keyed lock status: {e}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
enable_output=False,
|
enable_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
@ -797,32 +796,239 @@ class _KeyedLockContext:
|
||||||
if enable_logging is not None
|
if enable_logging is not None
|
||||||
else parent._default_enable_logging
|
else parent._default_enable_logging
|
||||||
)
|
)
|
||||||
self._ul: Optional[List["UnifiedLock"]] = None # set in __aenter__
|
self._ul: Optional[List[Dict[str, Any]]] = None # set in __aenter__
|
||||||
|
|
||||||
# ----- enter -----
|
# ----- enter -----
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
if self._ul is not None:
|
if self._ul is not None:
|
||||||
raise RuntimeError("KeyedUnifiedLock already acquired in current context")
|
raise RuntimeError("KeyedUnifiedLock already acquired in current context")
|
||||||
|
|
||||||
# acquire locks for all keys in the namespace
|
|
||||||
self._ul = []
|
self._ul = []
|
||||||
for key in self._keys:
|
|
||||||
lock = self._parent._get_lock_for_key(
|
try:
|
||||||
self._namespace, key, enable_logging=self._enable_logging
|
# Acquire locks for all keys in the namespace
|
||||||
)
|
for key in self._keys:
|
||||||
await lock.__aenter__()
|
lock = None
|
||||||
inc_debug_n_locks_acquired()
|
entry = None
|
||||||
self._ul.append(lock)
|
|
||||||
return self
|
try:
|
||||||
|
# 1. Get lock object (reference count is incremented here)
|
||||||
|
lock = self._parent._get_lock_for_key(
|
||||||
|
self._namespace, key, enable_logging=self._enable_logging
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Immediately create and add entry to list (critical for rollback to work)
|
||||||
|
entry = {
|
||||||
|
"key": key,
|
||||||
|
"lock": lock,
|
||||||
|
"entered": False,
|
||||||
|
"debug_inc": False,
|
||||||
|
"ref_incremented": True, # Mark that reference count has been incremented
|
||||||
|
}
|
||||||
|
self._ul.append(
|
||||||
|
entry
|
||||||
|
) # Add immediately after _get_lock_for_key for rollback to work
|
||||||
|
|
||||||
|
# 3. Try to acquire the lock
|
||||||
|
# Use try-finally to ensure state is updated atomically
|
||||||
|
lock_acquired = False
|
||||||
|
try:
|
||||||
|
await lock.__aenter__()
|
||||||
|
lock_acquired = True # Lock successfully acquired
|
||||||
|
finally:
|
||||||
|
if lock_acquired:
|
||||||
|
entry["entered"] = True
|
||||||
|
inc_debug_n_locks_acquired()
|
||||||
|
entry["debug_inc"] = True
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Lock acquisition was cancelled
|
||||||
|
# The finally block above ensures entry["entered"] is correct
|
||||||
|
direct_log(
|
||||||
|
f"Lock acquisition cancelled for key {key}",
|
||||||
|
level="WARNING",
|
||||||
|
enable_output=self._enable_logging,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Other exceptions, log and re-raise
|
||||||
|
direct_log(
|
||||||
|
f"Lock acquisition failed for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
except BaseException:
|
||||||
|
# Critical: if any exception occurs (including CancelledError) during lock acquisition,
|
||||||
|
# we must rollback all already acquired locks to prevent lock leaks
|
||||||
|
# Use shield to ensure rollback completes
|
||||||
|
await asyncio.shield(self._rollback_acquired_locks())
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _rollback_acquired_locks(self):
|
||||||
|
"""Rollback all acquired locks in case of exception during __aenter__"""
|
||||||
|
if not self._ul:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def rollback_single_entry(entry):
|
||||||
|
"""Rollback a single lock acquisition"""
|
||||||
|
key = entry["key"]
|
||||||
|
lock = entry["lock"]
|
||||||
|
debug_inc = entry["debug_inc"]
|
||||||
|
entered = entry["entered"]
|
||||||
|
ref_incremented = entry.get(
|
||||||
|
"ref_incremented", True
|
||||||
|
) # Default to True for safety
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# 1. If lock was acquired, release it
|
||||||
|
if entered:
|
||||||
|
try:
|
||||||
|
await lock.__aexit__(None, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("lock_exit", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock rollback error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Release reference count (if it was incremented)
|
||||||
|
if ref_incremented:
|
||||||
|
try:
|
||||||
|
self._parent._release_lock_for_key(self._namespace, key)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("ref_release", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock rollback reference release error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Decrement debug counter
|
||||||
|
if debug_inc:
|
||||||
|
try:
|
||||||
|
dec_debug_n_locks_acquired()
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("debug_dec", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock rollback counter decrementing error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
# Release already acquired locks in reverse order
|
||||||
|
for entry in reversed(self._ul):
|
||||||
|
# Use shield to protect each lock's rollback
|
||||||
|
try:
|
||||||
|
await asyncio.shield(rollback_single_entry(entry))
|
||||||
|
except Exception as e:
|
||||||
|
# Log but continue rolling back other locks
|
||||||
|
direct_log(
|
||||||
|
f"Lock rollback unexpected error for {entry['key']}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._ul = None
|
||||||
|
|
||||||
# ----- exit -----
|
# ----- exit -----
|
||||||
async def __aexit__(self, exc_type, exc, tb):
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
# The UnifiedLock takes care of proper release order
|
if self._ul is None:
|
||||||
for ul, key in zip(reversed(self._ul), reversed(self._keys)):
|
return
|
||||||
await ul.__aexit__(exc_type, exc, tb)
|
|
||||||
self._parent._release_lock_for_key(self._namespace, key)
|
async def release_all_locks():
|
||||||
dec_debug_n_locks_acquired()
|
"""Release all locks with comprehensive error handling, protected from cancellation"""
|
||||||
self._ul = None
|
|
||||||
|
async def release_single_entry(entry, exc_type, exc, tb):
|
||||||
|
"""Release a single lock with full protection"""
|
||||||
|
key = entry["key"]
|
||||||
|
lock = entry["lock"]
|
||||||
|
debug_inc = entry["debug_inc"]
|
||||||
|
entered = entry["entered"]
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# 1. Release the lock
|
||||||
|
if entered:
|
||||||
|
try:
|
||||||
|
await lock.__aexit__(exc_type, exc, tb)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("lock_exit", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock release error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Release reference count
|
||||||
|
try:
|
||||||
|
self._parent._release_lock_for_key(self._namespace, key)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("ref_release", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock release reference error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Decrement debug counter
|
||||||
|
if debug_inc:
|
||||||
|
try:
|
||||||
|
dec_debug_n_locks_acquired()
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(("debug_dec", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock release counter decrementing error for key {key}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
all_errors = []
|
||||||
|
|
||||||
|
# Release locks in reverse order
|
||||||
|
# This entire loop is protected by the outer shield
|
||||||
|
for entry in reversed(self._ul):
|
||||||
|
try:
|
||||||
|
errors = await release_single_entry(entry, exc_type, exc, tb)
|
||||||
|
for error_type, error in errors:
|
||||||
|
all_errors.append((entry["key"], error_type, error))
|
||||||
|
except Exception as e:
|
||||||
|
all_errors.append((entry["key"], "unexpected", e))
|
||||||
|
direct_log(
|
||||||
|
f"Lock release unexpected error for {entry['key']}: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return all_errors
|
||||||
|
|
||||||
|
# CRITICAL: Protect the entire release process with shield
|
||||||
|
# This ensures that even if cancellation occurs, all locks are released
|
||||||
|
try:
|
||||||
|
all_errors = await asyncio.shield(release_all_locks())
|
||||||
|
except Exception as e:
|
||||||
|
direct_log(
|
||||||
|
f"Critical error during __aexit__ cleanup: {e}",
|
||||||
|
level="ERROR",
|
||||||
|
enable_output=True,
|
||||||
|
)
|
||||||
|
all_errors = []
|
||||||
|
finally:
|
||||||
|
# Always clear the lock list, even if shield was cancelled
|
||||||
|
self._ul = None
|
||||||
|
|
||||||
|
# If there were release errors and no other exception, raise the first release error
|
||||||
|
if all_errors and exc_type is None:
|
||||||
|
raise all_errors[0][2] # (key, error_type, error)
|
||||||
|
|
||||||
|
|
||||||
def get_internal_lock(enable_logging: bool = False) -> UnifiedLock:
|
def get_internal_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||||
|
|
|
||||||
|
|
@ -1871,9 +1871,14 @@ class LightRAG:
|
||||||
if task and not task.done():
|
if task and not task.done():
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
# Persistent llm cache
|
# Persistent llm cache with error handling
|
||||||
if self.llm_response_cache:
|
if self.llm_response_cache:
|
||||||
await self.llm_response_cache.index_done_callback()
|
try:
|
||||||
|
await self.llm_response_cache.index_done_callback()
|
||||||
|
except Exception as persist_error:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to persist LLM cache: {persist_error}"
|
||||||
|
)
|
||||||
|
|
||||||
# Record processing end time for failed case
|
# Record processing end time for failed case
|
||||||
processing_end_time = int(time.time())
|
processing_end_time = int(time.time())
|
||||||
|
|
@ -1994,9 +1999,14 @@ class LightRAG:
|
||||||
error_msg
|
error_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
# Persistent llm cache
|
# Persistent llm cache with error handling
|
||||||
if self.llm_response_cache:
|
if self.llm_response_cache:
|
||||||
await self.llm_response_cache.index_done_callback()
|
try:
|
||||||
|
await self.llm_response_cache.index_done_callback()
|
||||||
|
except Exception as persist_error:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to persist LLM cache: {persist_error}"
|
||||||
|
)
|
||||||
|
|
||||||
# Record processing end time for failed case
|
# Record processing end time for failed case
|
||||||
processing_end_time = int(time.time())
|
processing_end_time = int(time.time())
|
||||||
|
|
|
||||||
|
|
@ -2302,9 +2302,7 @@ async def merge_nodes_and_edges(
|
||||||
return entity_data
|
return entity_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = (
|
error_msg = f"Error processing entity `{entity_name}`: {e}"
|
||||||
f"Critical error in entity processing for `{entity_name}`: {e}"
|
|
||||||
)
|
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
# Try to update pipeline status, but don't let status update failure affect main exception
|
# Try to update pipeline status, but don't let status update failure affect main exception
|
||||||
|
|
@ -2340,36 +2338,32 @@ async def merge_nodes_and_edges(
|
||||||
entity_tasks, return_when=asyncio.FIRST_EXCEPTION
|
entity_tasks, return_when=asyncio.FIRST_EXCEPTION
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if any task raised an exception and ensure all exceptions are retrieved
|
|
||||||
first_exception = None
|
first_exception = None
|
||||||
successful_results = []
|
processed_entities = []
|
||||||
|
|
||||||
for task in done:
|
for task in done:
|
||||||
try:
|
try:
|
||||||
exception = task.exception()
|
result = task.result()
|
||||||
if exception is not None:
|
except BaseException as e:
|
||||||
if first_exception is None:
|
|
||||||
first_exception = exception
|
|
||||||
else:
|
|
||||||
successful_results.append(task.result())
|
|
||||||
except Exception as e:
|
|
||||||
if first_exception is None:
|
if first_exception is None:
|
||||||
first_exception = e
|
first_exception = e
|
||||||
|
else:
|
||||||
|
processed_entities.append(result)
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
pending_results = await asyncio.gather(*pending, return_exceptions=True)
|
||||||
|
for result in pending_results:
|
||||||
|
if isinstance(result, BaseException):
|
||||||
|
if first_exception is None:
|
||||||
|
first_exception = result
|
||||||
|
else:
|
||||||
|
processed_entities.append(result)
|
||||||
|
|
||||||
# If any task failed, cancel all pending tasks and raise the first exception
|
|
||||||
if first_exception is not None:
|
if first_exception is not None:
|
||||||
# Cancel all pending tasks
|
|
||||||
for pending_task in pending:
|
|
||||||
pending_task.cancel()
|
|
||||||
# Wait for cancellation to complete
|
|
||||||
if pending:
|
|
||||||
await asyncio.wait(pending)
|
|
||||||
# Re-raise the first exception to notify the caller
|
|
||||||
raise first_exception
|
raise first_exception
|
||||||
|
|
||||||
# If all tasks completed successfully, collect results
|
|
||||||
processed_entities = [task.result() for task in entity_tasks]
|
|
||||||
|
|
||||||
# ===== Phase 2: Process all relationships concurrently =====
|
# ===== Phase 2: Process all relationships concurrently =====
|
||||||
log_message = f"Phase 2: Processing {total_relations_count} relations from {doc_id} (async: {graph_max_async})"
|
log_message = f"Phase 2: Processing {total_relations_count} relations from {doc_id} (async: {graph_max_async})"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
|
@ -2421,7 +2415,7 @@ async def merge_nodes_and_edges(
|
||||||
return edge_data, added_entities
|
return edge_data, added_entities
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Critical error in relationship processing for `{sorted_edge_key}`: {e}"
|
error_msg = f"Error processing relation `{sorted_edge_key}`: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|
||||||
# Try to update pipeline status, but don't let status update failure affect main exception
|
# Try to update pipeline status, but don't let status update failure affect main exception
|
||||||
|
|
@ -2459,40 +2453,36 @@ async def merge_nodes_and_edges(
|
||||||
edge_tasks, return_when=asyncio.FIRST_EXCEPTION
|
edge_tasks, return_when=asyncio.FIRST_EXCEPTION
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if any task raised an exception and ensure all exceptions are retrieved
|
|
||||||
first_exception = None
|
first_exception = None
|
||||||
successful_results = []
|
|
||||||
|
|
||||||
for task in done:
|
for task in done:
|
||||||
try:
|
try:
|
||||||
exception = task.exception()
|
edge_data, added_entities = task.result()
|
||||||
if exception is not None:
|
except BaseException as e:
|
||||||
if first_exception is None:
|
|
||||||
first_exception = exception
|
|
||||||
else:
|
|
||||||
successful_results.append(task.result())
|
|
||||||
except Exception as e:
|
|
||||||
if first_exception is None:
|
if first_exception is None:
|
||||||
first_exception = e
|
first_exception = e
|
||||||
|
else:
|
||||||
|
if edge_data is not None:
|
||||||
|
processed_edges.append(edge_data)
|
||||||
|
all_added_entities.extend(added_entities)
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
pending_results = await asyncio.gather(*pending, return_exceptions=True)
|
||||||
|
for result in pending_results:
|
||||||
|
if isinstance(result, BaseException):
|
||||||
|
if first_exception is None:
|
||||||
|
first_exception = result
|
||||||
|
else:
|
||||||
|
edge_data, added_entities = result
|
||||||
|
if edge_data is not None:
|
||||||
|
processed_edges.append(edge_data)
|
||||||
|
all_added_entities.extend(added_entities)
|
||||||
|
|
||||||
# If any task failed, cancel all pending tasks and raise the first exception
|
|
||||||
if first_exception is not None:
|
if first_exception is not None:
|
||||||
# Cancel all pending tasks
|
|
||||||
for pending_task in pending:
|
|
||||||
pending_task.cancel()
|
|
||||||
# Wait for cancellation to complete
|
|
||||||
if pending:
|
|
||||||
await asyncio.wait(pending)
|
|
||||||
# Re-raise the first exception to notify the caller
|
|
||||||
raise first_exception
|
raise first_exception
|
||||||
|
|
||||||
# If all tasks completed successfully, collect results
|
|
||||||
for task in edge_tasks:
|
|
||||||
edge_data, added_entities = task.result()
|
|
||||||
if edge_data is not None:
|
|
||||||
processed_edges.append(edge_data)
|
|
||||||
all_added_entities.extend(added_entities)
|
|
||||||
|
|
||||||
# ===== Phase 3: Update full_entities and full_relations storage =====
|
# ===== Phase 3: Update full_entities and full_relations storage =====
|
||||||
if full_entities_storage and full_relations_storage and doc_id:
|
if full_entities_storage and full_relations_storage and doc_id:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue