Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize suggestions database only once #8116

Merged
merged 14 commits into from
Oct 21, 2023
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't init assume that isInitialized is false at the beginning of the execution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented it in a higher-level initialization components

}
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for InstanceHolder when the only method and INSTANCE field of the InitializationComponentInitialized class deal with holding and initializing the instance.

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 {
4e6 marked this conversation as resolved.
Show resolved Hide resolved

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
Loading