When opening the project for a second time suggestion database is not sent (#7699)

close #7413

Changelog:
- update: the language server listens for the client disconnection event and invalidates the suggestions index

# Important Notes
The component browser contains suggestion entries after the refresh


https://github.com/enso-org/enso/assets/357683/bcebb8bf-e09f-4fb0-86cf-95ced58413f3
This commit is contained in:
Dmitry Bushev 2023-08-31 15:06:58 +01:00 committed by GitHub
parent 255b424b72
commit eefe74ed93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 244 additions and 43 deletions

View File

@ -305,6 +305,7 @@ class JsonConnectionController(
sender() ! ResponseError(Some(id), SessionAlreadyInitialisedError)
case MessageHandler.Disconnected =>
logger.info("Json session terminated [{}].", rpcSession.clientId)
context.system.eventStream.publish(JsonSessionTerminated(rpcSession))
context.stop(self)

View File

@ -5,7 +5,8 @@ import com.typesafe.scalalogging.LazyLogging
import org.enso.languageserver.data.{ClientId, Config}
import org.enso.languageserver.event.{
ExecutionContextCreated,
ExecutionContextDestroyed
ExecutionContextDestroyed,
JsonSessionTerminated
}
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
import org.enso.languageserver.runtime.handler._
@ -84,6 +85,8 @@ final class ContextRegistry(
.subscribe(self, classOf[Api.ExecutionUpdate])
context.system.eventStream
.subscribe(self, classOf[Api.VisualizationEvaluationFailed])
context.system.eventStream.subscribe(self, classOf[JsonSessionTerminated])
}
override def receive: Receive =
@ -346,6 +349,24 @@ final class ContextRegistry(
} else {
sender() ! AccessDenied
}
case JsonSessionTerminated(client) =>
store.contexts.getOrElse(client.clientId, Set()).foreach { contextId =>
val handler = context.actorOf(
DestroyContextHandler.props(
runtimeFailureMapper,
timeout,
runtime
)
)
store.getListener(contextId).foreach(context.stop)
handler.forward(Api.DestroyContextRequest(contextId))
context.become(
withStore(store.removeContext(client.clientId, contextId))
)
context.system.eventStream
.publish(ExecutionContextDestroyed(contextId, client.clientId))
}
}
private def getRuntimeStackItem(

View File

@ -16,7 +16,7 @@ import org.enso.languageserver.data.{
Config,
ReceivesSuggestionsDatabaseUpdates
}
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.event.{InitializedEvent, JsonSessionTerminated}
import org.enso.languageserver.filemanager.{
ContentRootManager,
FileDeletedEvent,
@ -115,6 +115,8 @@ final class SuggestionsHandler(
.subscribe(self, InitializedEvent.SuggestionsRepoInitialized.getClass)
context.system.eventStream
.subscribe(self, InitializedEvent.TruffleContextInitialized.getClass)
context.system.eventStream.subscribe(self, classOf[JsonSessionTerminated])
}
override def receive: Receive =
@ -313,6 +315,17 @@ final class SuggestionsHandler(
suggestionsRepo.currentVersion
.map(GetSuggestionsDatabaseResult(_, Seq()))
.pipeTo(sender())
if (state.shouldStartBackgroundProcessing) {
runtimeConnector ! Api.Request(Api.StartBackgroundProcessing())
context.become(
initialized(
projectName,
graph,
clients,
state.backgroundProcessingStarted()
)
)
}
case Completion(path, pos, selfType, returnType, tags, isStatic) =>
val selfTypes = selfType.toList.flatMap(ty => ty :: graph.getParents(ty))
@ -397,6 +410,14 @@ final class SuggestionsHandler(
)
)
action.pipeTo(handler)(sender())
context.become(
initialized(
projectName,
graph,
clients,
state.backgroundProcessingStopped()
)
)
case ProjectNameUpdated(name, updates) =>
updates.foreach(sessionRouter ! _)
@ -434,6 +455,29 @@ final class SuggestionsHandler(
state.suggestionLoadingComplete()
)
)
case JsonSessionTerminated(_) =>
val action = for {
_ <- suggestionsRepo.clean
} yield SearchProtocol.InvalidateModulesIndex
val handler = context.system.actorOf(
InvalidateModulesIndexHandler.props(
RuntimeFailureMapper(contentRootManager),
timeout,
runtimeConnector
)
)
action.pipeTo(handler)
context.become(
initialized(
projectName,
graph,
clients,
state.backgroundProcessingStopped()
)
)
}
/** Transition the initialization process.
@ -744,6 +788,12 @@ object SuggestionsHandler {
_shouldStartBackgroundProcessing = false
this
}
/** @return the new state with the background processing stopped. */
def backgroundProcessingStopped(): State = {
_shouldStartBackgroundProcessing = true
this
}
}
/** Creates a configuration object used to create a [[SuggestionsHandler]].

View File

@ -0,0 +1,66 @@
package org.enso.interpreter.instrument.command;
import com.oracle.truffle.api.TruffleLogger;
import java.util.UUID;
import java.util.logging.Level;
import org.enso.interpreter.instrument.execution.RuntimeContext;
import org.enso.interpreter.instrument.job.DeserializeLibrarySuggestionsJob;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.polyglot.runtime.Runtime$Api$InvalidateModulesIndexResponse;
import scala.Option;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
import scala.runtime.BoxedUnit;
/** A command that invalidates the modules index. */
public final class InvalidateModulesIndexCommand extends AsynchronousCommand {
/**
* Create a command that invalidates the modules index.
*
* @param maybeRequestId an option with request id
*/
public InvalidateModulesIndexCommand(Option<UUID> maybeRequestId) {
super(maybeRequestId);
}
@Override
public Future<BoxedUnit> executeAsynchronously(RuntimeContext ctx, ExecutionContext ec) {
return Future.apply(
() -> {
TruffleLogger logger = ctx.executionService().getLogger();
long writeCompilationLockTimestamp = ctx.locking().acquireWriteCompilationLock();
try {
ctx.jobControlPlane().abortAllJobs();
EnsoContext context = ctx.executionService().getContext();
context.getTopScope().getModules().forEach(module -> module.setIndexed(false));
ctx.jobControlPlane().stopBackgroundJobs();
context
.getPackageRepository()
.getLoadedPackages()
.foreach(
pkg -> {
ctx.jobProcessor()
.runBackground(new DeserializeLibrarySuggestionsJob(pkg.libraryName()));
return BoxedUnit.UNIT;
});
reply(new Runtime$Api$InvalidateModulesIndexResponse(), ctx);
} finally {
ctx.locking().releaseWriteCompilationLock();
logger.log(
Level.FINEST,
"Kept write compilation lock [{0}] for {1} milliseconds.",
new Object[] {
this.getClass().getSimpleName(),
System.currentTimeMillis() - writeCompilationLockTimestamp
});
}
return BoxedUnit.UNIT;
},
ec);
}
}

View File

@ -56,8 +56,8 @@ object CommandFactory {
case payload: Api.SetExpressionValueNotification =>
new SetExpressionValueCmd(payload)
case payload: Api.InvalidateModulesIndexRequest =>
new InvalidateModulesIndexCmd(request.requestId, payload)
case _: Api.InvalidateModulesIndexRequest =>
new InvalidateModulesIndexCommand(request.requestId)
case _: Api.GetTypeGraphRequest =>
new GetTypeGraphCommand(request.requestId)

View File

@ -1,35 +0,0 @@
package org.enso.interpreter.instrument.command
import org.enso.interpreter.instrument.execution.RuntimeContext
import org.enso.polyglot.runtime.Runtime.Api
import scala.concurrent.{ExecutionContext, Future}
/** A command that invalidates the modules index.
*
* @param maybeRequestId an option with request id
* @param request a request for invalidation
*/
class InvalidateModulesIndexCmd(
maybeRequestId: Option[Api.RequestId],
val request: Api.InvalidateModulesIndexRequest
) extends AsynchronousCommand(maybeRequestId) {
/** Executes a request.
*
* @param ctx contains suppliers of services to perform a request
*/
override def executeAsynchronously(implicit
ctx: RuntimeContext,
ec: ExecutionContext
): Future[Unit] = {
for {
_ <- Future { ctx.jobControlPlane.abortAllJobs() }
} yield {
ctx.executionService.getContext.getTopScope.getModules
.forEach(_.setIndexed(false))
reply(Api.InvalidateModulesIndexResponse())
}
}
}

View File

@ -30,6 +30,13 @@ trait JobControlPlane {
*/
def startBackgroundJobs(): Boolean
/** Stops background jobs processing.
*
* @return `true` if the call stopped background job, `false` if they are
* already stopped.
*/
def stopBackgroundJobs(): Boolean
/** Finds the first in-progress job satisfying the `filter` condition
*/
def jobInProgress[T](filter: PartialFunction[Job[_], Option[T]]): Option[T]

View File

@ -173,6 +173,14 @@ final class JobExecutionEngine(
result
}
/** @inheritdoc */
override def stopBackgroundJobs(): Boolean =
synchronized {
val result = isBackgroundJobsStarted
isBackgroundJobsStarted = false
result
}
/** @inheritdoc */
override def stop(): Unit = {
val allJobs = runningJobsRef.get()

View File

@ -1268,4 +1268,86 @@ class RuntimeSuggestionUpdatesTest
context.consumeOut shouldEqual List("Hello World!")
}
it should "invalidate modules index on command" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Enso_Test.Test.Main"
val code =
"""from Standard.Base import all
|
|main = IO.println "Hello World!"
|""".stripMargin.linesIterator.mkString("\n")
val mainFile = context.writeMain(code)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, code))
)
context.receiveNone shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Enso_Test.Test.Main", "main"),
None,
Vector()
)
)
)
)
val updates1 = context.receiveNIgnoreExpressionUpdates(4)
updates1.length shouldEqual 4
updates1 should contain allOf (
Api.Response(requestId, Api.PushContextResponse(contextId)),
context.executionComplete(contextId),
Api.Response(Api.BackgroundJobsStartedNotification())
)
val indexedModules = updates1.collect {
case Api.Response(
None,
Api.SuggestionsDatabaseModuleUpdateNotification(moduleName, _, _, _)
) =>
moduleName
}
indexedModules should contain theSameElementsAs Seq(moduleName)
context.consumeOut shouldEqual List("Hello World!")
// clear indexes
context.send(Api.Request(requestId, Api.InvalidateModulesIndexRequest()))
context.receiveN(1) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.InvalidateModulesIndexResponse())
)
// recompute
context.send(
Api.Request(requestId, Api.RecomputeContextRequest(contextId, None, None))
)
val updates2 = context.receiveNIgnoreExpressionUpdates(4)
updates2.length shouldEqual 4
updates2 should contain allOf (
Api.Response(requestId, Api.RecomputeContextResponse(contextId)),
context.executionComplete(contextId),
Api.Response(Api.BackgroundJobsStartedNotification())
)
val indexedModules2 = updates1.collect {
case Api.Response(
None,
Api.SuggestionsDatabaseModuleUpdateNotification(moduleName, _, _, _)
) =>
moduleName
}
indexedModules2 should contain theSameElementsAs Seq(moduleName)
context.consumeOut shouldEqual List("Hello World!")
}
}

View File

@ -397,7 +397,8 @@ final class SerializationManager(
compiler.context.logSerializationManager(
debugLogLevel,
"Restored IR from cache for module [{0}] at stage [{1}].",
Array[Object](module.getName, loadedCache.compilationStage())
module.getName,
loadedCache.compilationStage()
)
if (!relinkedIrChecks.contains(false)) {

View File

@ -6,8 +6,8 @@ import org.enso.jsonrpc.Errors.InvalidParams
/** An actor responsible for passing parsed massages between the web and
* a controller actor.
* @param protocol a factory for retrieving protocol object describing supported messages and their
* serialization modes.
* @param protocolFactory a factory for retrieving protocol object describing
* supported messages and their serialization modes.
* @param controller the controller actor, handling parsed messages.
*/
class MessageHandler(protocolFactory: ProtocolFactory, controller: ActorRef)
@ -32,7 +32,7 @@ class MessageHandler(protocolFactory: ProtocolFactory, controller: ActorRef)
* response deserialization.
* @return the connected actor behavior.
*/
def established(
private def established(
webConnection: ActorRef,
awaitingResponses: Map[Id, Method]
): Receive = {