-
Notifications
You must be signed in to change notification settings - Fork 323
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initialize suggestions database only once (#8116)
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
Showing
45 changed files
with
871 additions
and
714 deletions.
There are no files selected for viewing
37 changes: 37 additions & 0 deletions
37
...ver/src/main/java/org/enso/languageserver/boot/resource/AsyncResourcesInitialization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(() -> {}); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...ge-server/src/main/java/org/enso/languageserver/boot/resource/BlockingInitialization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
...server/src/main/java/org/enso/languageserver/boot/resource/DirectoriesInitialization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...e-server/src/main/java/org/enso/languageserver/boot/resource/InitializationComponent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
19 changes: 19 additions & 0 deletions
19
...c/main/java/org/enso/languageserver/boot/resource/InitializationComponentInitialized.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...age-server/src/main/java/org/enso/languageserver/boot/resource/JsonRpcInitialization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
176 changes: 176 additions & 0 deletions
176
...nguage-server/src/main/java/org/enso/languageserver/boot/resource/RepoInitialization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 -> {}); | ||
} | ||
} |
Oops, something went wrong.