Simplify shutdown logic on client disconnect in project-manager (#11712)

* Drop soft-shutdown on last client disconnect

Suspend on Windows confuses the reconnection logic and triggers a full
shutdown. This change simply drop shutdown on last client disconnect and
expects and explicit command.

* Various cherry-picks

Minor cherry-picks from the debugging branch. Should reduce  the amount
of non-critical warnings.
This commit is contained in:
Hubert Plociniczak 2024-12-02 21:19:55 +01:00 committed by GitHub
parent 16765455c2
commit 65010dffa7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 27 additions and 41 deletions

View File

@ -166,6 +166,9 @@ final class ContextRegistry(
sender() ! AccessDenied sender() ! AccessDenied
} }
case DestroyContextResponse(_) =>
// Initiated by *this* registry. Ignore
case PushContextRequest(client, contextId, stackItem) => case PushContextRequest(client, contextId, stackItem) =>
if (store.hasContext(client.clientId, contextId)) { if (store.hasContext(client.clientId, contextId)) {
val item = getRuntimeStackItem(stackItem) val item = getRuntimeStackItem(stackItem)

View File

@ -75,9 +75,12 @@ class JsonRpcServer(
} }
.to( .to(
Sink.actorRef[MessageHandler.WebMessage]( Sink.actorRef[MessageHandler.WebMessage](
messageHandler, messageHandler, {
MessageHandler.Disconnected(port), logger.trace("JSON sink stream finished with no failure")
{ _: Throwable => MessageHandler.Disconnected(port)
},
{ e: Throwable =>
logger.trace("JSON sink stream finished with a failure", e)
MessageHandler.Disconnected(port) MessageHandler.Disconnected(port)
} }
) )
@ -100,7 +103,7 @@ class JsonRpcServer(
logger.trace(s"Sent text message ${textMessage.text}.") logger.trace(s"Sent text message ${textMessage.text}.")
} }
Flow.fromSinkAndSource(incomingMessages, outgoingMessages) Flow.fromSinkAndSourceCoupled(incomingMessages, outgoingMessages)
} }
override protected def serverRoute(port: Int): Route = { override protected def serverRoute(port: Int): Route = {

View File

@ -169,18 +169,17 @@ class LanguageServerController(
* @param connectionInfo language server connection info * @param connectionInfo language server connection info
* @param serverProcessManager an actor that manages the lifecycle of the server process * @param serverProcessManager an actor that manages the lifecycle of the server process
* @param clients list of connected clients * @param clients list of connected clients
* @param scheduledShutdown cancellable timeout of the hard shutdown event and a port number of the client that initiated it * @param lastClientPort if no clients are connected denotes the last port number of a client or a project
* @return current supervising actor state * @return current supervising actor state
*/ */
private def supervising( private def supervising(
connectionInfo: LanguageServerConnectionInfo, connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef, serverProcessManager: ActorRef,
clients: Set[UUID] = Set.empty, clients: Set[UUID] = Set.empty,
scheduledShutdown: Option[(Cancellable, Int)] = None lastClientPort: Option[Int] = None
): Receive = ): Receive =
LoggingReceive.withLabel("supervising") { LoggingReceive.withLabel("supervising") {
case StartServer(clientId, _, requestedEngineVersion, _, _) => case StartServer(clientId, _, requestedEngineVersion, _, _) =>
scheduledShutdown.foreach(_._1.cancel())
if (requestedEngineVersion != engineVersion) { if (requestedEngineVersion != engineVersion) {
sender() ! ServerBootFailed( sender() ! ServerBootFailed(
new IllegalStateException( new IllegalStateException(
@ -213,7 +212,6 @@ class LanguageServerController(
) )
} }
case Terminated(_) => case Terminated(_) =>
scheduledShutdown.foreach(_._1.cancel())
logger.debug("Bootloader for {} terminated", project) logger.debug("Bootloader for {} terminated", project)
case StopServer(clientId, _) => case StopServer(clientId, _) =>
@ -224,18 +222,16 @@ class LanguageServerController(
clientId, clientId,
Some(sender()), Some(sender()),
explicitShutdownRequested = true, explicitShutdownRequested = true,
None, lastClientPort
scheduledShutdown
) )
case ScheduledShutdown(requester) => case ScheduledShutdown(requester) =>
shutDownServer(requester) shutDownServer(requester)
case LanguageServerStatusRequest => case LanguageServerStatusRequest =>
sender() ! LanguageServerStatus(project.id, scheduledShutdown.isDefined) sender() ! LanguageServerStatus(project.id, lastClientPort.isDefined)
case ShutDownServer => case ShutDownServer =>
scheduledShutdown.foreach(_._1.cancel())
shutDownServer(None) shutDownServer(None)
case ClientDisconnected(clientId, port) => case ClientDisconnected(clientId, port) =>
@ -246,13 +242,11 @@ class LanguageServerController(
clientId, clientId,
None, None,
explicitShutdownRequested = false, explicitShutdownRequested = false,
atPort = Some(port), lastClientPort.orElse(Some(port))
scheduledShutdown
) )
case ClientConnected(clientId, clientPort) => case ClientConnected(clientId, clientPort) =>
scheduledShutdown match { lastClientPort match {
case Some((cancellable, port)) if clientPort == port => case Some(port) if clientPort == port =>
cancellable.cancel()
context.become( context.become(
supervising( supervising(
connectionInfo, connectionInfo,
@ -265,7 +259,6 @@ class LanguageServerController(
} }
case RenameProject(_, namespace, oldName, newName) => case RenameProject(_, namespace, oldName, newName) =>
scheduledShutdown.foreach(_._1.cancel())
val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort) val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
context.actorOf( context.actorOf(
ProjectRenameAction ProjectRenameAction
@ -283,7 +276,6 @@ class LanguageServerController(
) )
case ServerDied => case ServerDied =>
scheduledShutdown.foreach(_._1.cancel())
logger.error("Language server died [{}]", connectionInfo) logger.error("Language server died [{}]", connectionInfo)
context.stop(self) context.stop(self)
@ -296,36 +288,24 @@ class LanguageServerController(
clientId: UUID, clientId: UUID,
maybeRequester: Option[ActorRef], maybeRequester: Option[ActorRef],
explicitShutdownRequested: Boolean, explicitShutdownRequested: Boolean,
atPort: Option[Int], atPort: Option[Int]
shutdownTimeout: Option[(Cancellable, Int)]
): Unit = { ): Unit = {
val updatedClients = clients - clientId val updatedClients = clients - clientId
if (updatedClients.isEmpty) { if (updatedClients.isEmpty) {
if (!explicitShutdownRequested) { if (!explicitShutdownRequested) {
logger.debug("Delaying shutdown for project {}", project.id) logger.debug(
val scheduledShutdown: Option[(Cancellable, Int)] = "Last client disconnected for project [{}]. Awaiting re-connection or shutdown",
shutdownTimeout.orElse( project.id
Some(
(
context.system.scheduler.scheduleOnce(
timeoutConfig.delayedShutdownTimeout,
self,
ScheduledShutdown(maybeRequester)
),
atPort.getOrElse(0)
)
)
) )
context.become( context.become(
supervising( supervising(
connectionInfo, connectionInfo,
serverProcessManager, serverProcessManager,
Set.empty, Set.empty,
scheduledShutdown atPort
) )
) )
} else { } else {
shutdownTimeout.foreach(_._1.cancel())
shutDownServer(maybeRequester) shutDownServer(maybeRequester)
} }
} else { } else {
@ -335,7 +315,7 @@ class LanguageServerController(
connectionInfo, connectionInfo,
serverProcessManager, serverProcessManager,
updatedClients, updatedClients,
shutdownTimeout None
) )
) )
} }

View File

@ -122,7 +122,7 @@ class ProjectShutdownSpec
deleteProject(projectId)(client2, implicitly[Position]) deleteProject(projectId)(client2, implicitly[Position])
} }
"ensure language server does eventually shutdown after last client disconnects" in { "ensure language server does not shutdown after last client disconnects and can re-connect" in {
val client = new WsTestClient(address) val client = new WsTestClient(address)
val projectId = createProject("Foo")(client, implicitly[Position]) val projectId = createProject("Foo")(client, implicitly[Position])
val socket1 = openProject(projectId)(client, implicitly[Position]) val socket1 = openProject(projectId)(client, implicitly[Position])
@ -154,7 +154,7 @@ class ProjectShutdownSpec
) )
val client2 = new WsTestClient(address) val client2 = new WsTestClient(address)
val socket2 = openProject(projectId)(client2, implicitly[Position]) val socket2 = openProject(projectId)(client2, implicitly[Position])
socket2 shouldNot be(socket1) socket2 shouldBe socket1
closeProject(projectId)(client2, implicitly[Position]) closeProject(projectId)(client2, implicitly[Position])
deleteProject(projectId)(client2, implicitly[Position]) deleteProject(projectId)(client2, implicitly[Position])