Skip to content

Commit

Permalink
Initialize suggestions database only once (#8116)
Browse files Browse the repository at this point in the history
close #8033

Changelog:
- update: run language server initialization once
- fix: issues with async `getSuggestionDatabase` message handling in new IDE
- update: implement unique background jobs
- refactor: initialization logic to Java
- refactor: `UniqueJob` to a marker interface
  • Loading branch information
4e6 authored Oct 21, 2023
1 parent 8fc720a commit b1df8b1
Show file tree
Hide file tree
Showing 45 changed files with 871 additions and 714 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.enso.languageserver.boot.resource;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;

/** Component that initializes resources in parallel. */
public class AsyncResourcesInitialization implements InitializationComponent {

private final InitializationComponent[] resources;

/**
* Create async initialization component.
*
* @param resources the list of resources to initialize
*/
public AsyncResourcesInitialization(InitializationComponent... resources) {
this.resources = resources;
}

@Override
public boolean isInitialized() {
return Arrays.stream(resources).allMatch(InitializationComponent::isInitialized);
}

@Override
public CompletableFuture<Void> init() {
return CompletableFuture.allOf(
Arrays.stream(resources)
.map(
component ->
component.isInitialized()
? CompletableFuture.completedFuture(null)
: component.init())
.toArray(CompletableFuture<?>[]::new))
.thenRun(() -> {});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.enso.languageserver.boot.resource;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;

/** Initialization component ensuring that only one initialization sequence is running at a time. */
public final class BlockingInitialization implements InitializationComponent {

private final InitializationComponent component;
private final Semaphore lock = new Semaphore(1);

/**
* Create blocking initialization component.
*
* @param component the underlying initialization component to run
*/
public BlockingInitialization(InitializationComponent component) {
this.component = component;
}

@Override
public boolean isInitialized() {
return component.isInitialized();
}

@Override
public CompletableFuture<Void> init() {
try {
lock.acquire();
} catch (InterruptedException e) {
return CompletableFuture.failedFuture(e);
}
return component.init().whenComplete((res, err) -> lock.release());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.enso.languageserver.boot.resource;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import org.enso.languageserver.data.ProjectDirectoriesConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Directories initialization. */
public class DirectoriesInitialization implements InitializationComponent {

private final Executor executor;
private final ProjectDirectoriesConfig projectDirectoriesConfig;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private volatile boolean isInitialized = false;

/**
* Creates the directories initialization component.
*
* @param executor the executor that runs the initialization
* @param projectDirectoriesConfig the directories config
*/
public DirectoriesInitialization(
Executor executor, ProjectDirectoriesConfig projectDirectoriesConfig) {
this.executor = executor;
this.projectDirectoriesConfig = projectDirectoriesConfig;
}

@Override
public boolean isInitialized() {
return isInitialized;
}

@Override
public CompletableFuture<Void> init() {
return CompletableFuture.runAsync(
() -> {
logger.info("Initializing directories...");
projectDirectoriesConfig.createDirectories();
logger.info("Initialized directories.");
isInitialized = true;
},
executor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.enso.languageserver.boot.resource;

import java.util.concurrent.CompletableFuture;

/** A component that should be initialized. */
public interface InitializationComponent {

/** @return `true` if the component is initialized */
boolean isInitialized();

/** Initialize the component. */
CompletableFuture<Void> init();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.enso.languageserver.boot.resource;

/** Object indicating that the initialization is complete. */
public final class InitializationComponentInitialized {

private static final class InstanceHolder {
private static final InitializationComponentInitialized INSTANCE =
new InitializationComponentInitialized();
}

/**
* Get the initialized marker object.
*
* @return the instance of {@link InitializationComponentInitialized}.
*/
public static InitializationComponentInitialized getInstance() {
return InstanceHolder.INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.enso.languageserver.boot.resource;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import org.enso.jsonrpc.ProtocolFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Initialization of JSON-RPC protocol. */
public class JsonRpcInitialization implements InitializationComponent {

private final Executor executor;
private final ProtocolFactory protocolFactory;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private volatile boolean isInitialized = false;

/**
* Create an instance of JSON-RPC initialization component.
*
* @param executor the executor that runs the initialization
* @param protocolFactory the JSON-RPC protocol factory
*/
public JsonRpcInitialization(Executor executor, ProtocolFactory protocolFactory) {
this.executor = executor;
this.protocolFactory = protocolFactory;
}

@Override
public boolean isInitialized() {
return isInitialized;
}

@Override
public CompletableFuture<Void> init() {
return CompletableFuture.runAsync(
() -> {
logger.info("Initializing JSON-RPC protocol.");
protocolFactory.init();
logger.info("JSON-RPC protocol initialized.");
isInitialized = true;
},
executor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package org.enso.languageserver.boot.resource;

import akka.event.EventStream;
import java.io.IOException;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import org.apache.commons.io.FileUtils;
import org.enso.languageserver.data.ProjectDirectoriesConfig;
import org.enso.languageserver.event.InitializedEvent;
import org.enso.logger.masking.MaskedPath;
import org.enso.searcher.sql.SqlDatabase;
import org.enso.searcher.sql.SqlSuggestionsRepo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.jdk.javaapi.FutureConverters;

/** Initialization of the Language Server suggestions database. */
public class RepoInitialization implements InitializationComponent {

private static final int MAX_RETRIES = 3;
private static final long RETRY_DELAY_MILLIS = 1000;

private final Executor executor;

private final ProjectDirectoriesConfig projectDirectoriesConfig;
private final EventStream eventStream;
private final SqlDatabase sqlDatabase;
private final SqlSuggestionsRepo sqlSuggestionsRepo;

private final Logger logger = LoggerFactory.getLogger(this.getClass());

private volatile boolean isInitialized = false;

/**
* Create an instance of repo initialization component.
*
* @param executor the executor that runs the initialization
* @param projectDirectoriesConfig configuration of language server directories
* @param eventStream the events stream
* @param sqlDatabase the sql database
* @param sqlSuggestionsRepo the suggestions repo
*/
public RepoInitialization(
Executor executor,
ProjectDirectoriesConfig projectDirectoriesConfig,
EventStream eventStream,
SqlDatabase sqlDatabase,
SqlSuggestionsRepo sqlSuggestionsRepo) {
this.executor = executor;
this.projectDirectoriesConfig = projectDirectoriesConfig;
this.eventStream = eventStream;
this.sqlDatabase = sqlDatabase;
this.sqlSuggestionsRepo = sqlSuggestionsRepo;
}

@Override
public boolean isInitialized() {
return isInitialized;
}

@Override
public CompletableFuture<Void> init() {
return initSqlDatabase()
.thenComposeAsync(v -> initSuggestionsRepo(), executor)
.thenRun(() -> isInitialized = true);
}

private CompletableFuture<Void> initSqlDatabase() {
return CompletableFuture.runAsync(
() -> {
logger.info("Initializing sql database [{}]...", sqlDatabase);
sqlDatabase.open();
logger.info("Initialized sql database [{}].", sqlDatabase);
},
executor)
.whenCompleteAsync(
(res, err) -> {
if (err != null) {
logger.error("Failed to initialize sql database [{}].", sqlDatabase, err);
}
},
executor);
}

private CompletableFuture<Void> initSuggestionsRepo() {
return CompletableFuture.runAsync(
() -> logger.info("Initializing suggestions repo [{}]...", sqlDatabase), executor)
.thenComposeAsync(
v ->
doInitSuggestionsRepo().exceptionallyComposeAsync(this::recoverInitializationError),
executor)
.thenRunAsync(
() -> logger.info("Initialized Suggestions repo [{}].", sqlDatabase), executor)
.whenCompleteAsync(
(res, err) -> {
if (err != null) {
logger.error("Failed to initialize SQL suggestions repo [{}].", sqlDatabase, err);
} else {
eventStream.publish(InitializedEvent.SuggestionsRepoInitialized$.MODULE$);
}
});
}

private CompletableFuture<Void> recoverInitializationError(Throwable error) {
return CompletableFuture.runAsync(
() ->
logger.warn(
"Failed to initialize the suggestions database [{}].", sqlDatabase, error),
executor)
.thenRunAsync(sqlDatabase::close, executor)
.thenComposeAsync(v -> clearDatabaseFile(0), executor)
.thenRunAsync(sqlDatabase::open, executor)
.thenRunAsync(() -> logger.info("Retrying database initialization."), executor)
.thenComposeAsync(v -> doInitSuggestionsRepo(), executor);
}

private CompletableFuture<Void> clearDatabaseFile(int retries) {
return CompletableFuture.runAsync(
() -> {
logger.info("Clear database file. Attempt #{}.", retries + 1);
try {
Files.delete(projectDirectoriesConfig.suggestionsDatabaseFile().toPath());
} catch (IOException e) {
throw new CompletionException(e);
}
},
executor)
.exceptionallyComposeAsync(error -> recoverClearDatabaseFile(error, retries), executor);
}

private CompletableFuture<Void> recoverClearDatabaseFile(Throwable error, int retries) {
if (error instanceof CompletionException) {
return recoverClearDatabaseFile(error.getCause(), retries);
} else if (error instanceof NoSuchFileException) {
logger.warn(
"Failed to delete the database file. Attempt #{}. File does not exist [{}].",
retries + 1,
new MaskedPath(projectDirectoriesConfig.suggestionsDatabaseFile().toPath()));
return CompletableFuture.completedFuture(null);
} else if (error instanceof FileSystemException) {
logger.error(
"Failed to delete the database file. Attempt #{}. The file will be removed during the shutdown.",
retries + 1,
error);
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() ->
FileUtils.deleteQuietly(projectDirectoriesConfig.suggestionsDatabaseFile())));
return CompletableFuture.failedFuture(error);
} else if (error instanceof IOException) {
logger.error("Failed to delete the database file. Attempt #{}.", retries + 1, error);
if (retries < MAX_RETRIES) {
try {
Thread.sleep(RETRY_DELAY_MILLIS);
} catch (InterruptedException e) {
throw new CompletionException(e);
}
return clearDatabaseFile(retries + 1);
} else {
return CompletableFuture.failedFuture(error);
}
}

return CompletableFuture.completedFuture(null);
}

private CompletionStage<Void> doInitSuggestionsRepo() {
return FutureConverters.asJava(sqlSuggestionsRepo.init()).thenAccept(res -> {});
}
}
Loading

0 comments on commit b1df8b1

Please sign in to comment.