Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,37 @@ fn is_lock_error(err: &redb::DatabaseError) -> bool {
}

impl BazelCache {
/// Eagerly create tables so that subsequent read transactions can open them.
/// Write-transaction `open_table` creates the table if it doesn't exist;
/// read-transaction `open_table` returns `TableDoesNotExist` if it was never created.
fn ensure_tables_exist(db: &Database) -> Result<(), CacheError> {
let txn = db.begin_write()?;
{
txn.open_table(CLASSPATH_TABLE)?;
txn.open_table(BUILD_HASH_TABLE)?;
}
txn.commit()?;
Ok(())
}

/// 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
///
/// After opening, eagerly creates tables so read transactions always succeed.
pub fn open(cache_dir: &Path) -> Result<Self, CacheError> {
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 }),
Ok(db) => {
Self::ensure_tables_exist(&db)?;
return Ok(Self { db });
}
Err(ref e) if is_lock_error(e) => {
log::warn!(
"Cache database locked, retrying in 500ms: {}",
Expand All @@ -109,7 +127,10 @@ impl BazelCache {
// 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 }),
Ok(db) => {
Self::ensure_tables_exist(&db)?;
return Ok(Self { db });
}
Err(ref e) if is_lock_error(e) => {
log::warn!(
"Cache database still locked after retry, recreating: {}",
Expand All @@ -122,13 +143,19 @@ impl BazelCache {
// Stage 3: delete and recreate
let _ = std::fs::remove_file(&db_path);
let db = Database::create(&db_path)?;
Self::ensure_tables_exist(&db)?;
Ok(Self { db })
}

/// Get a cached classpath for a target
/// Get a cached classpath for a target.
/// Returns None if the table doesn't exist yet (fresh database).
pub fn get_classpath(&self, label: &str) -> Result<Option<String>, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(CLASSPATH_TABLE)?;
let table = match txn.open_table(CLASSPATH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => return Err(CacheError::TableError(e)),
};
if let Some(value) = table.get(label)? {
Ok(Some(value.value().to_string()))
} else {
Expand All @@ -147,10 +174,15 @@ impl BazelCache {
Ok(())
}

/// Get a cached BUILD file hash
/// Get a cached BUILD file hash.
/// Returns None if the table doesn't exist yet (fresh database).
pub fn get_build_hash(&self, path: &str) -> Result<Option<String>, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(BUILD_HASH_TABLE)?;
let table = match txn.open_table(BUILD_HASH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => return Err(CacheError::TableError(e)),
};
if let Some(value) = table.get(path)? {
Ok(Some(value.value().to_string()))
} else {
Expand Down Expand Up @@ -182,10 +214,15 @@ impl BazelCache {
Ok(())
}

/// Load all cached classpaths (bulk load for IDE restart)
/// Load all cached classpaths (bulk load for IDE restart).
/// Returns an empty Vec if the table doesn't exist yet (fresh database).
pub fn load_all_classpaths(&self) -> Result<Vec<(String, String)>, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(CLASSPATH_TABLE)?;
let table = match txn.open_table(CLASSPATH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
Err(e) => return Err(CacheError::TableError(e)),
};
let mut result = Vec::new();
for entry in table.iter()? {
let (key, value) = entry?;
Expand Down Expand Up @@ -221,7 +258,11 @@ impl BazelCache {

fn find_corrupted_entries(&self) -> Result<Vec<String>, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(CLASSPATH_TABLE)?;
let table = match txn.open_table(CLASSPATH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
Err(e) => return Err(CacheError::TableError(e)),
};
let mut corrupted = Vec::new();
for entry in table.iter()? {
match entry {
Expand All @@ -243,13 +284,21 @@ impl BazelCache {

pub fn count_classpath_entries(&self) -> Result<usize, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(CLASSPATH_TABLE)?;
let table = match txn.open_table(CLASSPATH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(0),
Err(e) => return Err(CacheError::TableError(e)),
};
Ok(table.len()? as usize)
}

pub fn list_build_hash_keys(&self) -> Result<Vec<String>, CacheError> {
let txn = self.db.begin_read()?;
let table = txn.open_table(BUILD_HASH_TABLE)?;
let table = match txn.open_table(BUILD_HASH_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
Err(e) => return Err(CacheError::TableError(e)),
};
let mut keys = Vec::new();
for entry in table.iter()? {
match entry {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.ILog;
Expand All @@ -19,6 +21,8 @@

public class BazelClasspathContainer implements IClasspathContainer {
private static final ILog LOG = Platform.getLog(BazelClasspathContainer.class);
private static final Set<String> WARNED_MISSING_PATHS = ConcurrentHashMap.newKeySet();
private static final java.util.concurrent.ConcurrentHashMap<String, Boolean> FILE_EXISTS_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
public static final IPath CONTAINER_PATH = Path.fromPortableString("com.bazel.jdt.BAZEL_CONTAINER");
private static final String DESCRIPTION = "Bazel Dependencies";

Expand Down Expand Up @@ -72,15 +76,19 @@ private IClasspathEntry parseEntry(String raw) {
switch (type) {
case "LIB":
IPath jarPath = Path.fromPortableString(path);
if (!jarPath.toFile().exists()) {
LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt",
"Skipping non-existent JAR: " + path));
if (!fileExists(path, jarPath)) {
if (WARNED_MISSING_PATHS.add(path)) {
LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt",
"Skipping non-existent JAR: " + path));
}
return null;
}
IPath srcPath = sourcePath != null ? Path.fromPortableString(sourcePath) : null;
if (srcPath != null && !srcPath.toFile().exists()) {
LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt",
"Source attachment path does not exist, ignoring: " + sourcePath));
if (srcPath != null && !fileExists(sourcePath, srcPath)) {
if (WARNED_MISSING_PATHS.add(sourcePath)) {
LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt",
"Source attachment path does not exist, ignoring: " + sourcePath));
}
srcPath = null;
}
IAccessRule[] accessRules = parseAccessRules(accessRulesStr);
Expand Down Expand Up @@ -169,6 +177,15 @@ private static String extractPackageName(String targetLabel) {
return LabelUtils.extractPackageName(targetLabel);
}

private static boolean fileExists(String pathKey, IPath ipath) {
return FILE_EXISTS_CACHE.computeIfAbsent(pathKey, k -> ipath.toFile().exists());
}

static void resetWarnings() {
WARNED_MISSING_PATHS.clear();
FILE_EXISTS_CACHE.clear();
}

@Override
public IClasspathEntry[] getClasspathEntries() {
return entries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,31 @@ public class BazelClasspathContainerInitializer extends ClasspathContainerInitia

private static final ILog LOG = Platform.getLog(BazelClasspathContainerInitializer.class);
private static final Set<String> INITIALIZING = ConcurrentHashMap.newKeySet();
private static volatile boolean IMPORT_IN_PROGRESS = false;

public static void setImportInProgress(boolean inProgress) {
IMPORT_IN_PROGRESS = inProgress;
}

@Override
public void initialize(IPath containerPath, IJavaProject project) throws CoreException {
if (!BazelClasspathContainer.CONTAINER_PATH.equals(containerPath)) {
return;
}
String projectName = project.getProject().getName();

if (IMPORT_IN_PROGRESS) {
LOG.log(new Status(IStatus.INFO, "com.bazel.jdt",
"Skipping container resolve during import for project " + projectName));
JavaCore.setClasspathContainer(
BazelClasspathContainer.CONTAINER_PATH,
new IJavaProject[]{project},
new IClasspathContainer[]{BazelClasspathContainer.EMPTY},
null
);
return;
}

if (!INITIALIZING.add(projectName)) {
LOG.log(new Status(IStatus.INFO, "com.bazel.jdt",
"Skipping recursive initialize for project " + projectName));
Expand All @@ -42,33 +60,31 @@ public void initialize(IPath containerPath, IJavaProject project) throws CoreExc

private void doInitialize(IJavaProject project) throws CoreException {
BazelBridge bridge = BazelBridge.getInstance();
if (!bridge.isInitialized()) {
recoverFromCache(project, bridge);
if (tryRecoverFromCache(project, bridge)) {
return;
}
List<String> targetLabels = TargetProjectMapping.readTargets(project.getProject());
if (targetLabels.isEmpty()) {
LOG.log(new Status(IStatus.INFO, "com.bazel.jdt",
"No persisted target labels for project '" + project.getProject().getName()
+ "' - setting empty container (importer will configure)"));
JavaCore.setClasspathContainer(
BazelClasspathContainer.CONTAINER_PATH,
new IJavaProject[]{project},
new IClasspathContainer[]{BazelClasspathContainer.EMPTY},
null
);
return;
} else {
BazelClasspathManager.setMergedClasspathContainer(project.getProject());
if (bridge.isInitialized()) {
List<String> targetLabels = TargetProjectMapping.readTargets(project.getProject());
if (!targetLabels.isEmpty()) {
BazelClasspathManager.setMergedClasspathContainer(project.getProject());
return;
}
}
LOG.log(new Status(IStatus.INFO, "com.bazel.jdt",
"No persisted target labels for project '" + project.getProject().getName()
+ "' - setting empty container (importer will configure)"));
JavaCore.setClasspathContainer(
BazelClasspathContainer.CONTAINER_PATH,
new IJavaProject[]{project},
new IClasspathContainer[]{BazelClasspathContainer.EMPTY},
null
);
}

private void recoverFromCache(IJavaProject project, BazelBridge bridge) {
private boolean tryRecoverFromCache(IJavaProject project, BazelBridge bridge) {
List<String> targetLabels = TargetProjectMapping.readTargets(project.getProject());
if (targetLabels.isEmpty()) {
LOG.info("No persisted targets for " + project.getProject().getName()
+ " — skipping container initialization until import runs");
return;
return false;
}
java.util.ArrayList<String> allEntries = new java.util.ArrayList<>();
for (String label : targetLabels) {
Expand All @@ -78,24 +94,31 @@ private void recoverFromCache(IJavaProject project, BazelBridge bridge) {
}
}
if (allEntries.isEmpty()) {
LOG.info("No cached classpath for " + project.getProject().getName()
+ " — skipping until import provides entries");
return;
return false;
}
try {
BazelClasspathContainer container = new BazelClasspathContainer(
allEntries.toArray(new String[0]), Collections.emptyList(),
bridge.getDependencyResolutionMode(),
project.getProject().getName());
if (container.getClasspathEntries().length == 0) {
LOG.info("All cached classpath entries for " + project.getProject().getName()
+ " reference stale artifacts — skipping cache recovery");
return false;
}
JavaCore.setClasspathContainer(
BazelClasspathContainer.CONTAINER_PATH,
new IJavaProject[]{project},
new IClasspathContainer[]{container},
null
);
LOG.info("Recovered classpath from file cache for " + project.getProject().getName()
+ " (" + container.getClasspathEntries().length + " entries)");
return true;
} catch (Exception e) {
LOG.warn("Failed to apply cached classpath for " + project.getProject().getName()
+ ": " + e.getMessage());
return false;
}
}

Expand Down
Loading
Loading