44import logging
55import os
66import re
7+ import shutil
8+ import time
79from pathlib import Path
810from typing import Any
911from typing import Literal
1214import chromadb
1315import git
1416from chromadb .api .collection_configuration import CreateCollectionConfiguration
17+ from chromadb .api .shared_system_client import SharedSystemClient
1518from fastmcp import Context
1619from nbconvert import MarkdownExporter
1720from nbformat import read as nbread
@@ -490,10 +493,20 @@ def __init__(self, *, data_dir: Optional[Path] = None, repos_dir: Optional[Path]
490493 if not config .server .anonymized_telemetry :
491494 os .environ ["ANONYMIZED_TELEMETRY" ] = "False"
492495
493- # Initialize ChromaDB
494- self .chroma_client = chromadb .PersistentClient (path = str (self ._vector_db_path ))
495-
496- self .collection = self .chroma_client .get_or_create_collection ("holoviz_docs" , configuration = _CROMA_CONFIGURATION )
496+ # Initialize ChromaDB with health check
497+ try :
498+ self .chroma_client = chromadb .PersistentClient (path = str (self ._vector_db_path ))
499+ self .collection = self .chroma_client .get_or_create_collection ("holoviz_docs" , configuration = _CROMA_CONFIGURATION )
500+ self .collection .count () # Probe read to verify health
501+ except (KeyboardInterrupt , SystemExit ):
502+ raise
503+ except BaseException as e :
504+ logger .error ("ChromaDB initialization failed (%s: %s). Wiping and reinitializing." , type (e ).__name__ , e )
505+ shutil .rmtree (self ._vector_db_path , ignore_errors = True )
506+ self ._vector_db_path .mkdir (parents = True , exist_ok = True )
507+ SharedSystemClient .clear_system_cache ()
508+ self .chroma_client = chromadb .PersistentClient (path = str (self ._vector_db_path ))
509+ self .collection = self .chroma_client .get_or_create_collection ("holoviz_docs" , configuration = _CROMA_CONFIGURATION )
497510
498511 # Lazy-initialized async lock for database operations to prevent corruption from concurrent access
499512 self ._db_lock : Optional [asyncio .Lock ] = None
@@ -527,12 +540,37 @@ def db_lock(self) -> asyncio.Lock:
527540 self ._db_lock = asyncio .Lock ()
528541 return self ._db_lock
529542
543+ @property
544+ def _backup_path (self ) -> Path :
545+ """Path to the backup copy of the vector database directory."""
546+ return Path (str (self ._vector_db_path ) + ".bak" )
547+
548+ async def _restore_from_backup (self , ctx : Context | None = None ) -> None :
549+ """Restore vector database from backup after a write failure."""
550+ backup_path = self ._backup_path
551+ if not backup_path .exists ():
552+ await log_warning ("No backup found to restore from." , ctx )
553+ return
554+ try :
555+ shutil .rmtree (self ._vector_db_path , ignore_errors = True )
556+ shutil .copytree (backup_path , self ._vector_db_path )
557+ SharedSystemClient .clear_system_cache ()
558+ self .chroma_client = chromadb .PersistentClient (path = str (self ._vector_db_path ))
559+ self .collection = self .chroma_client .get_or_create_collection ("holoviz_docs" , configuration = _CROMA_CONFIGURATION )
560+ await log_info ("Restored vector database from backup." , ctx )
561+ except (KeyboardInterrupt , SystemExit ):
562+ raise
563+ except BaseException as e :
564+ logger .error ("Failed to restore from backup (%s: %s). Database may be degraded." , type (e ).__name__ , e )
565+
530566 def is_indexed (self ) -> bool :
531567 """Check if documentation index exists and is valid."""
532568 try :
533569 count = self .collection .count ()
534570 return count > 0
535- except Exception :
571+ except (KeyboardInterrupt , SystemExit ):
572+ raise
573+ except BaseException :
536574 return False
537575
538576 async def ensure_indexed (self , ctx : Context | None = None ):
@@ -887,6 +925,16 @@ async def index_documentation(self, ctx: Context | None = None):
887925 # Validate for duplicate IDs and log details
888926 await self ._validate_unique_ids (all_docs )
889927
928+ # Create pre-write backup of vector database
929+ backup_path = self ._backup_path
930+ try :
931+ if self ._vector_db_path .exists ():
932+ t0 = time .monotonic ()
933+ shutil .copytree (self ._vector_db_path , backup_path , dirs_exist_ok = True )
934+ await log_info (f"Pre-write backup created at { backup_path } ({ time .monotonic () - t0 :.1f} s)" , ctx )
935+ except Exception as e :
936+ await log_warning (f"Failed to create pre-write backup: { e } . Continuing without backup." , ctx )
937+
890938 # Clear existing collection
891939 await log_info ("Clearing existing index..." , ctx )
892940
@@ -911,23 +959,30 @@ async def index_documentation(self, ctx: Context | None = None):
911959 # Add documents to ChromaDB
912960 await log_info (f"Adding { len (all_docs )} documents to index..." , ctx )
913961
914- self .collection .add (
915- documents = [doc ["content" ] for doc in all_docs ],
916- metadatas = [
917- {
918- "title" : doc ["title" ],
919- "url" : doc ["url" ],
920- "project" : doc ["project" ],
921- "source_path" : doc ["source_path" ],
922- "source_path_stem" : doc ["source_path_stem" ],
923- "source_url" : doc ["source_url" ],
924- "description" : doc ["description" ],
925- "is_reference" : doc ["is_reference" ],
926- }
927- for doc in all_docs
928- ],
929- ids = [doc ["id" ] for doc in all_docs ],
930- )
962+ try :
963+ self .collection .add (
964+ documents = [doc ["content" ] for doc in all_docs ],
965+ metadatas = [
966+ {
967+ "title" : doc ["title" ],
968+ "url" : doc ["url" ],
969+ "project" : doc ["project" ],
970+ "source_path" : doc ["source_path" ],
971+ "source_path_stem" : doc ["source_path_stem" ],
972+ "source_url" : doc ["source_url" ],
973+ "description" : doc ["description" ],
974+ "is_reference" : doc ["is_reference" ],
975+ }
976+ for doc in all_docs
977+ ],
978+ ids = [doc ["id" ] for doc in all_docs ],
979+ )
980+ except (KeyboardInterrupt , SystemExit ):
981+ raise
982+ except BaseException as e :
983+ logger .error ("ChromaDB write failed (%s: %s). Attempting restore from backup." , type (e ).__name__ , e )
984+ await self ._restore_from_backup (ctx )
985+ raise
931986
932987 await log_info (f"✅ Successfully indexed { len (all_docs )} documents" , ctx )
933988 await log_info (f"📊 Vector database stored at: { self ._vector_db_path } " , ctx )
0 commit comments