Add Idleness Http Endpoint (#1847)

Implement `GET / _idle` request
This commit is contained in:
Dmitry Bushev 2021-07-12 16:53:44 +03:00 committed by GitHub
parent 399fa5edfe
commit b3badf1b80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 411 additions and 5 deletions

View File

@ -1,5 +1,10 @@
# Enso Next # Enso Next
## Tooling
- Implemented an HTTP endponint returning the time that the language server has
spent idle ([#1847](https://github.com/enso-org/enso/pull/1847)).
# Enso 0.2.13 (2021-07-09) # Enso 0.2.13 (2021-07-09)
## Interpreter/Runtime ## Interpreter/Runtime

View File

@ -45,3 +45,5 @@ The protocol messages are broken up into documents as follows:
The messages and types pertaining to the project manager component. The messages and types pertaining to the project manager component.
- [**Language Server Message Specification:**](./protocol-language-server.md) - [**Language Server Message Specification:**](./protocol-language-server.md)
The messages and types pertaining to the language server component. The messages and types pertaining to the language server component.
- [**Language Server Http Endpoints Specification**](./language-server-http-endoints.md)
Specification of the Language Server Http endpoints.

View File

@ -0,0 +1,140 @@
---
layout: developer-doc
title: Language Server HTTP Endpoints
category: language-server
tags: [language-server, protocol, specification]
order: 6
---
# HTTP Endpoints
Language server exposes a number of HTTP endpoints on the same socket as the
JSONRPC protocol.
<!-- MarkdownTOC levels="2" autolink="true" indent=" " -->
- [`/_health`](#---health-)
- [`/_health/readiness`](#---health-readiness-)
- [`/_health/liveness`](#---health-liveness-)
- [`/_idle`](#---idle-)
<!-- /MarkdownTOC -->
## `/_health`
HTTP endpoint that provides basic health checking capabilities.
### `GET | HEAD`
Returns `200 OK` when the server is started and `500 Internal Server Error`
otherwise.
#### Request
```text
> GET /_health HTTP/1.1
> Host: localhost:63597
> User-Agent: curl/7.77.0
> Accept: */*
```
#### Response
```text
< HTTP/1.1 200 OK
< Server: akka-http/10.2.0-RC1
< Date: Fri, 09 Jul 2021 15:16:16 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 2
<
OK
```
## `/_health/readiness`
The server readiness probe.
### `GET | HEAD`
Returns `200 OK` when the server is initialized and `500 Internal Server Error`
otherwise.
#### Request
```text
> GET /_health/readiness HTTP/1.1
> Host: localhost:63597
> User-Agent: curl/7.77.0
> Accept: */*
```
#### Response
```text
< HTTP/1.1 200 OK
< Server: akka-http/10.2.0-RC1
< Date: Fri, 09 Jul 2021 15:30:53 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 2
<
OK
```
## `/_health/liveness`
The server liveness probe.
### `GET | HEAD`
Checks if all the server subsystems are functioning and returns `200 OK` or
`500 Internal Server Error` otherwise.
#### Request
```text
> GET /_health/liveness HTTP/1.1
> Host: localhost:60339
> User-Agent: curl/7.77.0
> Accept: */*
```
#### Response
```text
< HTTP/1.1 200 OK
< Server: akka-http/10.2.0-RC1
< Date: Fri, 09 Jul 2021 15:35:43 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 2
<
OK
```
## `/_idle`
The server idleness probe.
### `GET`
Return the amount of time the language server is idle.
#### Request
```text
> GET /_idle HTTP/1.1
> Host: localhost:60339
> User-Agent: curl/7.77.0
> Accept: */*
```
#### Response
```text
< HTTP/1.1 200 OK
< Server: akka-http/10.2.0-RC1
< Date: Fri, 09 Jul 2021 15:44:51 GMT
< Content-Type: application/json
< Content-Length: 21
<
{"idle_time_sec":58}
```

View File

@ -2,6 +2,7 @@ package org.enso.languageserver.boot
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.time.Clock
import akka.actor.ActorSystem import akka.actor.ActorSystem
import org.enso.jsonrpc.JsonRpcServer import org.enso.jsonrpc.JsonRpcServer
@ -21,7 +22,11 @@ import org.enso.languageserver.filemanager.{
} }
import org.enso.languageserver.http.server.BinaryWebSocketServer import org.enso.languageserver.http.server.BinaryWebSocketServer
import org.enso.languageserver.io._ import org.enso.languageserver.io._
import org.enso.languageserver.monitoring.HealthCheckEndpoint import org.enso.languageserver.monitoring.{
HealthCheckEndpoint,
IdlenessEndpoint,
IdlenessMonitor
}
import org.enso.languageserver.protocol.binary.{ import org.enso.languageserver.protocol.binary.{
BinaryConnectionControllerFactory, BinaryConnectionControllerFactory,
InboundMessageDecoder InboundMessageDecoder
@ -60,6 +65,8 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
logLevel logLevel
) )
private val utcClock = Clock.systemUTC()
val directoriesConfig = ProjectDirectoriesConfig(serverConfig.contentRootPath) val directoriesConfig = ProjectDirectoriesConfig(serverConfig.contentRootPath)
private val contentRoot = ContentRootWithFile( private val contentRoot = ContentRootWithFile(
ContentRoot.Project(serverConfig.contentRootUuid), ContentRoot.Project(serverConfig.contentRootUuid),
@ -105,6 +112,9 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
val versionsRepo = new SqlVersionsRepo(sqlDatabase)(system.dispatcher) val versionsRepo = new SqlVersionsRepo(sqlDatabase)(system.dispatcher)
log.trace("Created SQL repos: [{}. {}].", suggestionsRepo, versionsRepo) log.trace("Created SQL repos: [{}. {}].", suggestionsRepo, versionsRepo)
val idlenessMonitor =
system.actorOf(IdlenessMonitor.props(utcClock))
lazy val sessionRouter = lazy val sessionRouter =
system.actorOf(SessionRouter.props(), "session-router") system.actorOf(SessionRouter.props(), "session-router")
@ -272,6 +282,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
stdErrController, stdErrController,
stdInController, stdInController,
runtimeConnector, runtimeConnector,
idlenessMonitor,
languageServerConfig languageServerConfig
) )
log.trace( log.trace(
@ -296,13 +307,16 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
serverConfig.computeExecutionContext serverConfig.computeExecutionContext
) )
val idlenessEndpoint =
new IdlenessEndpoint(idlenessMonitor)
val jsonRpcServer = val jsonRpcServer =
new JsonRpcServer( new JsonRpcServer(
JsonRpc.protocol, JsonRpc.protocol,
jsonRpcControllerFactory, jsonRpcControllerFactory,
JsonRpcServer JsonRpcServer
.Config(outgoingBufferSize = 10000, lazyMessageTimeout = 10.seconds), .Config(outgoingBufferSize = 10000, lazyMessageTimeout = 10.seconds),
List(healthCheckEndpoint) List(healthCheckEndpoint, idlenessEndpoint)
) )
log.trace("Created JSON RPC Server [{}].", jsonRpcServer) log.trace("Created JSON RPC Server [{}].", jsonRpcServer)

View File

@ -0,0 +1,64 @@
package org.enso.languageserver.monitoring
import akka.actor.ActorRef
import akka.http.scaladsl.model.{
ContentTypes,
HttpEntity,
MessageEntity,
StatusCodes
}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.pattern.ask
import akka.util.Timeout
import com.typesafe.scalalogging.LazyLogging
import org.enso.jsonrpc._
import scala.concurrent.duration._
import scala.util.{Failure, Success}
/** HTTP endpoint that provides idleness capabilities.
*
* @param idlenessMonitor an actor monitoring the server idle time
*/
class IdlenessEndpoint(
idlenessMonitor: ActorRef
) extends Endpoint
with LazyLogging {
implicit private val timeout: Timeout = Timeout(10.seconds)
/** @inheritdoc */
override def route: Route =
idlenessProbe
private val idlenessProbe =
path("_idle") {
get {
checkIdleness()
}
}
private def checkIdleness(): Route = {
val future = idlenessMonitor ? MonitoringProtocol.GetIdleTime
onComplete(future) {
case Failure(_) =>
complete(StatusCodes.InternalServerError)
case Success(r: MonitoringProtocol.IdleTime) =>
complete(IdlenessEndpoint.toHttpEntity(r))
case Success(r) =>
logger.error("Unexpected response from idleness monitor: [{}]", r)
complete(StatusCodes.InternalServerError)
}
}
}
object IdlenessEndpoint {
private def toJsonText(t: MonitoringProtocol.IdleTime): String =
s"""{"idle_time_sec":${t.idleTimeSeconds}}"""
def toHttpEntity(t: MonitoringProtocol.IdleTime): MessageEntity =
HttpEntity(ContentTypes.`application/json`, toJsonText(t))
}

View File

@ -0,0 +1,35 @@
package org.enso.languageserver.monitoring
import java.time.{Clock, Duration, Instant}
import akka.actor.{Actor, Props}
import org.enso.languageserver.util.UnhandledLogging
/** An actor that monitors the server time spent idle.
*
* @param clock the system clock
*/
class IdlenessMonitor(clock: Clock) extends Actor with UnhandledLogging {
override def receive: Receive = initialized(clock.instant())
private def initialized(lastActiveTime: Instant): Receive = {
case MonitoringProtocol.ResetIdleTime =>
context.become(initialized(clock.instant()))
case MonitoringProtocol.GetIdleTime =>
val idleTime = Duration.between(lastActiveTime, clock.instant())
sender() ! MonitoringProtocol.IdleTime(idleTime.toSeconds)
}
}
object IdlenessMonitor {
/** Creates a configuration object used to create an idleness monitor.
*
* @return a configuration object
*/
def props(clock: Clock): Props = Props(new IdlenessMonitor(clock))
}

View File

@ -26,4 +26,16 @@ object MonitoringProtocol {
*/ */
case object OK extends ReadinessResponse case object OK extends ReadinessResponse
/** A request to reset the idle time. */
case object ResetIdleTime
/** A request to get the server idle time. */
case object GetIdleTime
/** A response containing the idle time.
*
* @param idleTimeSeconds the idle time in seconds.
*/
case class IdleTime(idleTimeSeconds: Long)
} }

View File

@ -1,5 +1,7 @@
package org.enso.languageserver.protocol.json package org.enso.languageserver.protocol.json
import java.util.UUID
import akka.actor.{Actor, ActorRef, Cancellable, Props, Stash, Status} import akka.actor.{Actor, ActorRef, Cancellable, Props, Stash, Status}
import akka.pattern.pipe import akka.pattern.pipe
import akka.util.Timeout import akka.util.Timeout
@ -15,6 +17,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
import org.enso.languageserver.capability.CapabilityProtocol import org.enso.languageserver.capability.CapabilityProtocol
import org.enso.languageserver.data.Config import org.enso.languageserver.data.Config
import org.enso.languageserver.event.{ import org.enso.languageserver.event.{
InitializedEvent,
JsonSessionInitialized, JsonSessionInitialized,
JsonSessionTerminated JsonSessionTerminated
} }
@ -24,6 +27,7 @@ import org.enso.languageserver.io.InputOutputApi._
import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput} import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput}
import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol} import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol}
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping} import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
import org.enso.languageserver.monitoring.MonitoringProtocol
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.requesthandler._ import org.enso.languageserver.requesthandler._
import org.enso.languageserver.requesthandler.capability._ import org.enso.languageserver.requesthandler.capability._
@ -63,7 +67,6 @@ import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.util.UnhandledLogging import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo
import java.util.UUID
import scala.concurrent.duration._ import scala.concurrent.duration._
/** An actor handling communications between a single client and the language /** An actor handling communications between a single client and the language
@ -77,6 +80,7 @@ import scala.concurrent.duration._
* @param contentRootManager manages the available content roots * @param contentRootManager manages the available content roots
* @param contextRegistry a router that dispatches execution context requests * @param contextRegistry a router that dispatches execution context requests
* @param suggestionsHandler a reference to the suggestions requests handler * @param suggestionsHandler a reference to the suggestions requests handler
* @param idlenessMonitor a reference to the idleness monitor actor
* @param requestTimeout a request timeout * @param requestTimeout a request timeout
*/ */
class JsonConnectionController( class JsonConnectionController(
@ -92,6 +96,7 @@ class JsonConnectionController(
val stdErrController: ActorRef, val stdErrController: ActorRef,
val stdInController: ActorRef, val stdInController: ActorRef,
val runtimeConnector: ActorRef, val runtimeConnector: ActorRef,
val idlenessMonitor: ActorRef,
val languageServerConfig: Config, val languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds requestTimeout: FiniteDuration = 10.seconds
) extends Actor ) extends Actor
@ -157,6 +162,9 @@ class JsonConnectionController(
logger.info("RPC session initialized for client [{}].", clientId) logger.info("RPC session initialized for client [{}].", clientId)
val session = JsonSession(clientId, self) val session = JsonSession(clientId, self)
context.system.eventStream.publish(JsonSessionInitialized(session)) context.system.eventStream.publish(JsonSessionInitialized(session))
context.system.eventStream.publish(
InitializedEvent.InitializationFinished
)
val cancellable = context.system.scheduler.scheduleOnce( val cancellable = context.system.scheduler.scheduleOnce(
requestTimeout, requestTimeout,
@ -179,6 +187,7 @@ class JsonConnectionController(
case Status.Failure(ex) => case Status.Failure(ex) =>
logger.error("Failed to initialize the resources. {}", ex.getMessage) logger.error("Failed to initialize the resources. {}", ex.getMessage)
receiver ! ResponseError(Some(request.id), ResourcesInitializationError) receiver ! ResponseError(Some(request.id), ResourcesInitializationError)
context.system.eventStream.publish(InitializedEvent.InitializationFailed)
context.become(connected(webActor)) context.become(connected(webActor))
case _ => stash() case _ => stash()
@ -356,6 +365,7 @@ class JsonConnectionController(
} }
case req @ Request(method, _, _) if requestHandlers.contains(method) => case req @ Request(method, _, _) if requestHandlers.contains(method) =>
refreshIdleTime(method)
val handler = context.actorOf( val handler = context.actorOf(
requestHandlers(method), requestHandlers(method),
s"request-handler-$method-${UUID.randomUUID()}" s"request-handler-$method-${UUID.randomUUID()}"
@ -363,6 +373,15 @@ class JsonConnectionController(
handler.forward(req) handler.forward(req)
} }
private def refreshIdleTime(method: Method): Unit = {
method match {
case InitialPing | Ping =>
// ignore
case _ =>
idlenessMonitor ! MonitoringProtocol.ResetIdleTime
}
}
private def createRequestHandlers( private def createRequestHandlers(
rpcSession: JsonSession rpcSession: JsonSession
): Map[Method, Props] = { ): Map[Method, Props] = {
@ -473,6 +492,7 @@ object JsonConnectionController {
stdErrController: ActorRef, stdErrController: ActorRef,
stdInController: ActorRef, stdInController: ActorRef,
runtimeConnector: ActorRef, runtimeConnector: ActorRef,
idlenessMonitor: ActorRef,
languageServerConfig: Config, languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds requestTimeout: FiniteDuration = 10.seconds
): Props = ): Props =
@ -490,6 +510,7 @@ object JsonConnectionController {
stdErrController, stdErrController,
stdInController, stdInController,
runtimeConnector, runtimeConnector,
idlenessMonitor,
languageServerConfig, languageServerConfig,
requestTimeout requestTimeout
) )

View File

@ -26,6 +26,7 @@ class JsonConnectionControllerFactory(
stdErrController: ActorRef, stdErrController: ActorRef,
stdInController: ActorRef, stdInController: ActorRef,
runtimeConnector: ActorRef, runtimeConnector: ActorRef,
idlenessMonitor: ActorRef,
config: Config config: Config
)(implicit system: ActorSystem) )(implicit system: ActorSystem)
extends ClientControllerFactory { extends ClientControllerFactory {
@ -50,6 +51,7 @@ class JsonConnectionControllerFactory(
stdErrController, stdErrController,
stdInController, stdInController,
runtimeConnector, runtimeConnector,
idlenessMonitor,
config config
) )
) )

View File

@ -0,0 +1,27 @@
package org.enso.languageserver
import java.time.{Clock, Instant, ZoneId, ZoneOffset}
/** Test clock which time flow can be controlled.
*
* @param instant the initial point in time
*/
case class TestClock(var instant: Instant = Instant.EPOCH) extends Clock {
private val UTC = ZoneOffset.UTC
/** @inheritdoc */
override def getZone: ZoneId = UTC
/** @inheritdoc */
override def withZone(zone: ZoneId): Clock =
TestClock(instant)
/** Move time forward by the specified amount of seconds.
*
* @param seconds the amount of seconds
*/
def moveTimeForward(seconds: Long): Unit = {
instant = instant.plusSeconds(seconds)
}
}

View File

@ -0,0 +1,75 @@
package org.enso.languageserver.monitoring
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import org.enso.languageserver.TestClock
import org.enso.testkit.FlakySpec
import org.scalatest.flatspec.AnyFlatSpecLike
import org.scalatest.matchers.should.Matchers
import scala.concurrent.duration._
class IdlenessEndpointSpec
extends AnyFlatSpecLike
with Matchers
with FlakySpec
with ScalatestRouteTest
with Directives {
implicit val timeout = RouteTestTimeout(25.seconds)
"An idleness probe" should "reply with server idle time" in withEndpoint {
(_, _, endpoint) =>
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":0}"""
}
}
it should "count idle time" in withEndpoint { (clock, _, endpoint) =>
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":0}"""
}
val idleTimeSeconds = 1L
clock.moveTimeForward(idleTimeSeconds)
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":$idleTimeSeconds}"""
}
}
it should "reset idle time" in withEndpoint { (clock, monitor, endpoint) =>
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":0}"""
}
val idleTimeSeconds = 1L
clock.moveTimeForward(idleTimeSeconds)
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":$idleTimeSeconds}"""
}
monitor ! MonitoringProtocol.ResetIdleTime
Get("/_idle") ~> endpoint.route ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual s"""{"idle_time_sec":0}"""
}
}
def withEndpoint(
test: (TestClock, ActorRef, IdlenessEndpoint) => Any
): Unit = {
val clock = TestClock()
val idlenessMonitor = system.actorOf(IdlenessMonitor.props(clock))
val endpoint = new IdlenessEndpoint(idlenessMonitor)
test(clock, idlenessMonitor, endpoint)
}
}

View File

@ -1,5 +1,8 @@
package org.enso.languageserver.websocket.json package org.enso.languageserver.websocket.json
import java.nio.file.Files
import java.util.UUID
import akka.testkit.TestProbe import akka.testkit.TestProbe
import io.circe.literal._ import io.circe.literal._
import io.circe.parser.parse import io.circe.parser.parse
@ -18,6 +21,7 @@ import org.enso.languageserver.effect.ZioExec
import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager._ import org.enso.languageserver.filemanager._
import org.enso.languageserver.io._ import org.enso.languageserver.io._
import org.enso.languageserver.monitoring.IdlenessMonitor
import org.enso.languageserver.protocol.json.{ import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory, JsonConnectionControllerFactory,
JsonRpc JsonRpc
@ -26,6 +30,7 @@ import org.enso.languageserver.refactoring.ProjectNameChangedEvent
import org.enso.languageserver.runtime.{ContextRegistry, RuntimeFailureMapper} import org.enso.languageserver.runtime.{ContextRegistry, RuntimeFailureMapper}
import org.enso.languageserver.search.SuggestionsHandler import org.enso.languageserver.search.SuggestionsHandler
import org.enso.languageserver.session.SessionRouter import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.TestClock
import org.enso.languageserver.text.BufferRegistry import org.enso.languageserver.text.BufferRegistry
import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.data.TypeGraph
import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api
@ -34,8 +39,6 @@ import org.enso.testkit.EitherValue
import org.enso.text.Sha3_224VersionCalculator import org.enso.text.Sha3_224VersionCalculator
import org.scalatest.OptionValues import org.scalatest.OptionValues
import java.nio.file.Files
import java.util.UUID
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -56,6 +59,7 @@ class BaseServerTest
val config = mkConfig val config = mkConfig
val runtimeConnectorProbe = TestProbe() val runtimeConnectorProbe = TestProbe()
val versionCalculator = Sha3_224VersionCalculator val versionCalculator = Sha3_224VersionCalculator
val clock = TestClock()
val typeGraph: TypeGraph = { val typeGraph: TypeGraph = {
val graph = TypeGraph("Any") val graph = TypeGraph("Any")
@ -150,6 +154,10 @@ class BaseServerTest
) )
) )
val idlenessMonitor = system.actorOf(
IdlenessMonitor.props(clock)
)
val contextRegistry = val contextRegistry =
system.actorOf( system.actorOf(
ContextRegistry.props( ContextRegistry.props(
@ -209,6 +217,7 @@ class BaseServerTest
stdErrController, stdErrController,
stdInController, stdInController,
runtimeConnectorProbe.ref, runtimeConnectorProbe.ref,
idlenessMonitor,
config config
) )
} }