Merge branch 'fix-webui-error-handle' into fix/dark-mode-graph-text-colors
This commit is contained in:
commit
659f2d5944
4 changed files with 177 additions and 16 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
||||||
|
|
||||||
__version__ = "1.4.8.1"
|
__version__ = "1.4.8.2"
|
||||||
__author__ = "Zirui Guo"
|
__author__ = "Zirui Guo"
|
||||||
__url__ = "https://github.com/HKUDS/LightRAG"
|
__url__ = "https://github.com/HKUDS/LightRAG"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__api_version__ = "0222"
|
__api_version__ = "0223"
|
||||||
|
|
|
||||||
|
|
@ -2179,7 +2179,12 @@ async def extract_entities(
|
||||||
|
|
||||||
async def _process_with_semaphore(chunk):
|
async def _process_with_semaphore(chunk):
|
||||||
async with semaphore:
|
async with semaphore:
|
||||||
return await _process_single_content(chunk)
|
try:
|
||||||
|
return await _process_single_content(chunk)
|
||||||
|
except Exception as e:
|
||||||
|
chunk_id = chunk[0] # Extract chunk_id from chunk[0]
|
||||||
|
prefixed_exception = create_prefixed_exception(e, chunk_id)
|
||||||
|
raise prefixed_exception from e
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for c in ordered_chunks:
|
for c in ordered_chunks:
|
||||||
|
|
@ -2217,7 +2222,7 @@ async def extract_entities(
|
||||||
await asyncio.wait(pending)
|
await asyncio.wait(pending)
|
||||||
|
|
||||||
# Add progress prefix to the exception message
|
# Add progress prefix to the exception message
|
||||||
progress_prefix = f"Chunks[{processed_chunks+1}/{total_chunks}]"
|
progress_prefix = f"C[{processed_chunks+1}/{total_chunks}]"
|
||||||
|
|
||||||
# Re-raise the original exception with a prefix
|
# Re-raise the original exception with a prefix
|
||||||
prefixed_exception = create_prefixed_exception(first_exception, progress_prefix)
|
prefixed_exception = create_prefixed_exception(first_exception, progress_prefix)
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,26 @@ export default function DocumentManager() {
|
||||||
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([])
|
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([])
|
||||||
const isSelectionMode = selectedDocIds.length > 0
|
const isSelectionMode = selectedDocIds.length > 0
|
||||||
|
|
||||||
|
// Add refs to track previous pipelineBusy state and current interval
|
||||||
|
const prevPipelineBusyRef = useRef<boolean | undefined>(undefined);
|
||||||
|
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
|
// Add retry mechanism state
|
||||||
|
const [retryState, setRetryState] = useState({
|
||||||
|
count: 0,
|
||||||
|
lastError: null as Error | null,
|
||||||
|
isBackingOff: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add circuit breaker state
|
||||||
|
const [circuitBreakerState, setCircuitBreakerState] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
failureCount: 0,
|
||||||
|
lastFailureTime: null as number | null,
|
||||||
|
nextRetryTime: null as number | null
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Handle checkbox change for individual documents
|
// Handle checkbox change for individual documents
|
||||||
const handleDocumentSelect = useCallback((docId: string, checked: boolean) => {
|
const handleDocumentSelect = useCallback((docId: string, checked: boolean) => {
|
||||||
setSelectedDocIds(prev => {
|
setSelectedDocIds(prev => {
|
||||||
|
|
@ -519,6 +539,98 @@ export default function DocumentManager() {
|
||||||
setDocs(response.pagination.total_count > 0 ? legacyDocs : null);
|
setDocs(response.pagination.total_count > 0 ? legacyDocs : null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Utility function to create timeout wrapper for API calls
|
||||||
|
const withTimeout = useCallback((
|
||||||
|
promise: Promise<any>,
|
||||||
|
timeoutMs: number = 30000,
|
||||||
|
errorMsg: string = 'Request timeout'
|
||||||
|
): Promise<any> => {
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error(errorMsg)), timeoutMs)
|
||||||
|
});
|
||||||
|
return Promise.race([promise, timeoutPromise]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// Enhanced error classification
|
||||||
|
const classifyError = useCallback((error: any) => {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return { type: 'cancelled', shouldRetry: false, shouldShowToast: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message === 'Request timeout') {
|
||||||
|
return { type: 'timeout', shouldRetry: true, shouldShowToast: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message?.includes('Network Error') || error.code === 'NETWORK_ERROR') {
|
||||||
|
return { type: 'network', shouldRetry: true, shouldShowToast: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status >= 500) {
|
||||||
|
return { type: 'server', shouldRetry: true, shouldShowToast: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status >= 400 && error.status < 500) {
|
||||||
|
return { type: 'client', shouldRetry: false, shouldShowToast: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'unknown', shouldRetry: true, shouldShowToast: true };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Circuit breaker utility functions
|
||||||
|
const isCircuitBreakerOpen = useCallback(() => {
|
||||||
|
if (!circuitBreakerState.isOpen) return false;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (circuitBreakerState.nextRetryTime && now >= circuitBreakerState.nextRetryTime) {
|
||||||
|
// Reset circuit breaker to half-open state
|
||||||
|
setCircuitBreakerState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isOpen: false,
|
||||||
|
failureCount: Math.max(0, prev.failureCount - 1)
|
||||||
|
}));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [circuitBreakerState]);
|
||||||
|
|
||||||
|
const recordFailure = useCallback((error: Error) => {
|
||||||
|
const now = Date.now();
|
||||||
|
setCircuitBreakerState(prev => {
|
||||||
|
const newFailureCount = prev.failureCount + 1;
|
||||||
|
const shouldOpen = newFailureCount >= 3; // Open after 3 failures
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen: shouldOpen,
|
||||||
|
failureCount: newFailureCount,
|
||||||
|
lastFailureTime: now,
|
||||||
|
nextRetryTime: shouldOpen ? now + (Math.pow(2, newFailureCount) * 1000) : null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setRetryState(prev => ({
|
||||||
|
count: prev.count + 1,
|
||||||
|
lastError: error,
|
||||||
|
isBackingOff: true
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const recordSuccess = useCallback(() => {
|
||||||
|
setCircuitBreakerState({
|
||||||
|
isOpen: false,
|
||||||
|
failureCount: 0,
|
||||||
|
lastFailureTime: null,
|
||||||
|
nextRetryTime: null
|
||||||
|
});
|
||||||
|
|
||||||
|
setRetryState({
|
||||||
|
count: 0,
|
||||||
|
lastError: null,
|
||||||
|
isBackingOff: false
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Intelligent refresh function: handles all boundary cases
|
// Intelligent refresh function: handles all boundary cases
|
||||||
const handleIntelligentRefresh = useCallback(async (
|
const handleIntelligentRefresh = useCallback(async (
|
||||||
targetPage?: number, // Optional target page, defaults to current page
|
targetPage?: number, // Optional target page, defaults to current page
|
||||||
|
|
@ -540,7 +652,12 @@ export default function DocumentManager() {
|
||||||
sort_direction: sortDirection
|
sort_direction: sortDirection
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await getDocumentsPaginated(request);
|
// Use timeout wrapper for the API call
|
||||||
|
const response = await withTimeout(
|
||||||
|
getDocumentsPaginated(request),
|
||||||
|
30000, // 30 second timeout
|
||||||
|
'Document fetch timeout'
|
||||||
|
);
|
||||||
|
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
|
@ -556,7 +673,11 @@ export default function DocumentManager() {
|
||||||
page: lastPage
|
page: lastPage
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastPageResponse = await getDocumentsPaginated(lastPageRequest);
|
const lastPageResponse = await withTimeout(
|
||||||
|
getDocumentsPaginated(lastPageRequest),
|
||||||
|
30000,
|
||||||
|
'Document fetch timeout'
|
||||||
|
);
|
||||||
|
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
|
@ -575,14 +696,22 @@ export default function DocumentManager() {
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
|
const errorClassification = classifyError(err);
|
||||||
|
|
||||||
|
if (errorClassification.shouldShowToast) {
|
||||||
|
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorClassification.shouldRetry) {
|
||||||
|
recordFailure(err as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [statusFilter, pagination.page, pagination.page_size, sortField, sortDirection, t, updateComponentState]);
|
}, [statusFilter, pagination.page, pagination.page_size, sortField, sortDirection, t, updateComponentState, withTimeout, classifyError, recordFailure]);
|
||||||
|
|
||||||
// New paginated data fetching function
|
// New paginated data fetching function
|
||||||
const fetchPaginatedDocuments = useCallback(async (
|
const fetchPaginatedDocuments = useCallback(async (
|
||||||
|
|
@ -602,10 +731,6 @@ export default function DocumentManager() {
|
||||||
await fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
|
await fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
|
||||||
}, [fetchPaginatedDocuments, pagination.page, pagination.page_size, statusFilter]);
|
}, [fetchPaginatedDocuments, pagination.page, pagination.page_size, statusFilter]);
|
||||||
|
|
||||||
// Add refs to track previous pipelineBusy state and current interval
|
|
||||||
const prevPipelineBusyRef = useRef<boolean | undefined>(undefined);
|
|
||||||
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
|
|
||||||
// Function to clear current polling interval
|
// Function to clear current polling interval
|
||||||
const clearPollingInterval = useCallback(() => {
|
const clearPollingInterval = useCallback(() => {
|
||||||
if (pollingIntervalRef.current) {
|
if (pollingIntervalRef.current) {
|
||||||
|
|
@ -620,18 +745,49 @@ export default function DocumentManager() {
|
||||||
|
|
||||||
pollingIntervalRef.current = setInterval(async () => {
|
pollingIntervalRef.current = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
|
// Check circuit breaker before making request
|
||||||
|
if (isCircuitBreakerOpen()) {
|
||||||
|
return; // Skip this polling cycle
|
||||||
|
}
|
||||||
|
|
||||||
// Only perform fetch if component is still mounted
|
// Only perform fetch if component is still mounted
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
await fetchDocuments()
|
await fetchDocuments();
|
||||||
|
recordSuccess(); // Record successful operation
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Only show error if component is still mounted
|
// Only handle error if component is still mounted
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
const errorClassification = classifyError(err);
|
||||||
|
|
||||||
|
// Always reset isRefreshing state on error
|
||||||
|
setIsRefreshing(false);
|
||||||
|
|
||||||
|
if (errorClassification.shouldShowToast) {
|
||||||
|
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorClassification.shouldRetry) {
|
||||||
|
recordFailure(err as Error);
|
||||||
|
|
||||||
|
// Implement exponential backoff for retries
|
||||||
|
const backoffDelay = Math.min(Math.pow(2, retryState.count) * 1000, 30000); // Max 30s
|
||||||
|
|
||||||
|
if (retryState.count < 3) { // Max 3 retries
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setRetryState(prev => ({ ...prev, isBackingOff: false }));
|
||||||
|
}
|
||||||
|
}, backoffDelay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-retryable errors, stop polling
|
||||||
|
clearPollingInterval();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}, [fetchDocuments, t, clearPollingInterval]);
|
}, [fetchDocuments, t, clearPollingInterval, isCircuitBreakerOpen, recordSuccess, recordFailure, classifyError, retryState.count]);
|
||||||
|
|
||||||
const scanDocuments = useCallback(async () => {
|
const scanDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue