From eefe74ed93f054a6e48010198898654dec0e442a Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 31 Aug 2023 15:06:58 +0100 Subject: [PATCH] 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 --- .../json/JsonConnectionController.scala | 1 + .../runtime/ContextRegistry.scala | 23 +++++- .../search/SuggestionsHandler.scala | 52 +++++++++++- .../InvalidateModulesIndexCommand.java | 66 +++++++++++++++ .../instrument/command/CommandFactory.scala | 4 +- .../command/InvalidateModulesIndexCmd.scala | 35 -------- .../execution/JobControlPlane.scala | 7 ++ .../execution/JobExecutionEngine.scala | 8 ++ .../RuntimeSuggestionUpdatesTest.scala | 82 +++++++++++++++++++ .../enso/compiler/SerializationManager.scala | 3 +- .../org/enso/jsonrpc/MessageHandler.scala | 6 +- 11 files changed, 244 insertions(+), 43 deletions(-) create mode 100644 engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/command/InvalidateModulesIndexCommand.java delete mode 100644 engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/InvalidateModulesIndexCmd.scala diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index e50100ca0b..025de33a04 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -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) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index 682f6bb69e..5759046c6a 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -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( diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala index debc687e04..437bd85d79 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala @@ -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]]. diff --git a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/command/InvalidateModulesIndexCommand.java b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/command/InvalidateModulesIndexCommand.java new file mode 100644 index 0000000000..e30714100f --- /dev/null +++ b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/command/InvalidateModulesIndexCommand.java @@ -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 maybeRequestId) { + super(maybeRequestId); + } + + @Override + public Future 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); + } +} diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala index 2c345887ac..ec1487d484 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala @@ -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) diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/InvalidateModulesIndexCmd.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/InvalidateModulesIndexCmd.scala deleted file mode 100644 index 4af9138c94..0000000000 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/InvalidateModulesIndexCmd.scala +++ /dev/null @@ -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()) - } - } - -} diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobControlPlane.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobControlPlane.scala index 59c3efcd10..55fb88fd5f 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobControlPlane.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobControlPlane.scala @@ -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] diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala index 8113cc8d79..a3d6eb4738 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala @@ -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() diff --git a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeSuggestionUpdatesTest.scala b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeSuggestionUpdatesTest.scala index a5b0080b79..bb35f738f4 100644 --- a/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeSuggestionUpdatesTest.scala +++ b/engine/runtime-with-instruments/src/test/scala/org/enso/interpreter/test/instrument/RuntimeSuggestionUpdatesTest.scala @@ -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!") + } } diff --git a/engine/runtime/src/main/scala/org/enso/compiler/SerializationManager.scala b/engine/runtime/src/main/scala/org/enso/compiler/SerializationManager.scala index 5310c7f22d..453ed7cee7 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/SerializationManager.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/SerializationManager.scala @@ -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)) { diff --git a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala index 196df7a031..3b455d3a24 100644 --- a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala +++ b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala @@ -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 = {