diff --git a/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs b/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs index a13441f..0f9e93a 100644 --- a/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs +++ b/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs @@ -79,11 +79,48 @@ pub enum CacheError { IoError(#[from] std::io::Error), } +fn is_lock_error(err: &redb::DatabaseError) -> bool { + matches!(err, redb::DatabaseError::DatabaseAlreadyOpen) +} + impl BazelCache { - /// Open or create the cache database + /// Open or create the cache database. + /// + /// Uses a three-stage strategy for lock conflicts: + /// 1. Try to open normally + /// 2. If locked, sleep 500ms and retry (covers transient cross-process locks) + /// 3. If still locked, delete the .redb file and create a fresh database pub fn open(cache_dir: &Path) -> Result { std::fs::create_dir_all(cache_dir)?; let db_path = cache_dir.join("bazel-jdt-cache.redb"); + + // Stage 1: first attempt + match Database::create(&db_path) { + Ok(db) => return Ok(Self { db }), + Err(ref e) if is_lock_error(e) => { + log::warn!( + "Cache database locked, retrying in 500ms: {}", + db_path.display() + ); + } + Err(e) => return Err(CacheError::DatabaseError(e)), + } + + // Stage 2: retry after 500ms + std::thread::sleep(std::time::Duration::from_millis(500)); + match Database::create(&db_path) { + Ok(db) => return Ok(Self { db }), + Err(ref e) if is_lock_error(e) => { + log::warn!( + "Cache database still locked after retry, recreating: {}", + db_path.display() + ); + } + Err(e) => return Err(CacheError::DatabaseError(e)), + } + + // Stage 3: delete and recreate + let _ = std::fs::remove_file(&db_path); let db = Database::create(&db_path)?; Ok(Self { db }) } diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs index 60f1ae9..3fc0a23 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs @@ -142,11 +142,31 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeInitialize( } }; - let state = match BazelJdtState::new( - std::path::PathBuf::from(&workspace), - &bazel, - std::path::Path::new(&cache), - ) { + let cache_path = std::path::PathBuf::from(&cache); + + // Clean up any orphaned state using the same cache_dir (e.g., from a + // ClassLoader replacement that skipped nativeShutdown). + { + let mut reg = registry().lock().unwrap_or_else(|e| e.into_inner()); + let orphan_keys: Vec = reg + .iter() + .filter(|(_, s)| s.cache_dir == cache_path) + .map(|(k, _)| *k) + .collect(); + for key in orphan_keys { + if let Some(mut orphan) = reg.remove(&key) { + log::warn!( + "Cleaning up orphaned native state (handle={}) for cache_dir {:?}", + key, + cache_path + ); + shutdown_state(&mut orphan); + } + } + } + + let state = match BazelJdtState::new(std::path::PathBuf::from(&workspace), &bazel, &cache_path) + { Ok(s) => s, Err(e) => { let _ = env.throw_new( @@ -178,26 +198,7 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeInitialize( key as jlong } -#[no_mangle] -pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeShutdown( - _env: JNIEnv, - _class: JClass, - handle: jlong, -) { - if handle <= 0 { - return; - } - let key = handle as u64; - - let mut state_box = { - let mut reg = registry().lock().unwrap_or_else(|e| e.into_inner()); - match reg.remove(&key) { - Some(b) => b, - None => return, - } - }; - - let state = &mut *state_box; +fn shutdown_state(state: &mut BazelJdtState) { state.signal_shutdown(); state.set_sync_state(SyncState::Dead); @@ -227,6 +228,28 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeShutdown( } } +#[no_mangle] +pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeShutdown( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle <= 0 { + return; + } + let key = handle as u64; + + let mut state_box = { + let mut reg = registry().lock().unwrap_or_else(|e| e.into_inner()); + match reg.remove(&key) { + Some(b) => b, + None => return, + } + }; + + shutdown_state(&mut state_box); +} + fn make_watcher_callback( registry_key: u64, ) -> Box) + Send + 'static> { diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs index 78d9489..e280cd9 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs @@ -23,6 +23,7 @@ pub enum SyncState { /// Central state for the Bazel JDT bridge pub struct BazelJdtState { pub cache: BazelCache, + pub cache_dir: PathBuf, pub graph: Mutex, pub parser: BuildFileParser, pub invoker: BazelInvoker, @@ -75,6 +76,7 @@ impl BazelJdtState { Ok(Self { cache, + cache_dir: cache_dir.to_path_buf(), graph: Mutex::new(graph), parser, invoker, diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java index ae929de..fea0252 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java @@ -43,8 +43,9 @@ public void stop(BundleContext context) throws Exception { ResourcesPlugin.getWorkspace().removeResourceChangeListener(invisibleProjectListener); invisibleProjectListener = null; } + BazelBridge.getInstance().shutdown(); LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Bazel JDT Bridge bundle stopping (bridge survives for container recovery)")); + "Bazel JDT Bridge bundle stopped (classpath recovery uses disk cache on next restart)")); } private void checkForInvisibleProjects(IResourceChangeEvent event) { diff --git a/bazel-jdt-bridge/vscode-extension/package-lock.json b/bazel-jdt-bridge/vscode-extension/package-lock.json index fa7c786..4ff7f79 100644 --- a/bazel-jdt-bridge/vscode-extension/package-lock.json +++ b/bazel-jdt-bridge/vscode-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "bazel-jdt-bridge", - "version": "0.1.0", + "version": "0.1.0-pre.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bazel-jdt-bridge", - "version": "0.1.0", + "version": "0.1.0-pre.2", "devDependencies": { "@types/mocha": "^10.0.9", "@types/node": "^18.19.130",