Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Idleness Http Endpoint #1847

Merged
merged 11 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Enso Next

## Tooling

- Implemented an HTTP endponint returning the Language Server idle time.
4e6 marked this conversation as resolved.
Show resolved Hide resolved
[#1847](https://github.com/enso-org/enso/pull/1847).
4e6 marked this conversation as resolved.
Show resolved Hide resolved

# Enso 0.2.13 (2021-07-09)

## Interpreter/Runtime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.enso.languageserver.boot

import java.io.File
import java.net.URI
import java.time.Clock

import akka.actor.ActorSystem
import org.enso.jsonrpc.JsonRpcServer
Expand All @@ -21,7 +22,11 @@ import org.enso.languageserver.filemanager.{
}
import org.enso.languageserver.http.server.BinaryWebSocketServer
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.{
BinaryConnectionControllerFactory,
InboundMessageDecoder
Expand Down Expand Up @@ -60,6 +65,8 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
logLevel
)

private val utcClock = Clock.systemUTC()

val directoriesConfig = ProjectDirectoriesConfig(serverConfig.contentRootPath)
private val contentRoot = ContentRootWithFile(
ContentRoot.Project(serverConfig.contentRootUuid),
Expand Down Expand Up @@ -105,6 +112,9 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
val versionsRepo = new SqlVersionsRepo(sqlDatabase)(system.dispatcher)
log.trace("Created SQL repos: [{}. {}].", suggestionsRepo, versionsRepo)

val idlenessMonitor =
system.actorOf(IdlenessMonitor.props(utcClock))

lazy val sessionRouter =
system.actorOf(SessionRouter.props(), "session-router")

Expand Down Expand Up @@ -272,6 +282,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
stdErrController,
stdInController,
runtimeConnector,
idlenessMonitor,
languageServerConfig
)
log.trace(
Expand All @@ -296,13 +307,16 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
serverConfig.computeExecutionContext
)

val idlenessEndpoint =
new IdlenessEndpoint(idlenessMonitor)

val jsonRpcServer =
new JsonRpcServer(
JsonRpc.protocol,
jsonRpcControllerFactory,
JsonRpcServer
.Config(outgoingBufferSize = 10000, lazyMessageTimeout = 10.seconds),
List(healthCheckEndpoint)
List(healthCheckEndpoint, idlenessEndpoint)
)
log.trace("Created JSON RPC Server [{}].", jsonRpcServer)

Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -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))

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,16 @@ object MonitoringProtocol {
*/
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)

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.enso.languageserver.protocol.json

import java.util.UUID

import akka.actor.{Actor, ActorRef, Cancellable, Props, Stash, Status}
import akka.pattern.pipe
import akka.util.Timeout
Expand All @@ -24,6 +26,7 @@ import org.enso.languageserver.io.InputOutputApi._
import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput}
import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol}
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.requesthandler._
import org.enso.languageserver.requesthandler.capability._
Expand Down Expand Up @@ -63,7 +66,6 @@ import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo

import java.util.UUID
import scala.concurrent.duration._

/** An actor handling communications between a single client and the language
Expand All @@ -77,6 +79,7 @@ import scala.concurrent.duration._
* @param contentRootManager manages the available content roots
* @param contextRegistry a router that dispatches execution context requests
* @param suggestionsHandler a reference to the suggestions requests handler
* @param idlenessMonitor a reference to the idleness monitor actor
* @param requestTimeout a request timeout
*/
class JsonConnectionController(
Expand All @@ -92,6 +95,7 @@ class JsonConnectionController(
val stdErrController: ActorRef,
val stdInController: ActorRef,
val runtimeConnector: ActorRef,
val idlenessMonitor: ActorRef,
val languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds
) extends Actor
Expand Down Expand Up @@ -356,13 +360,23 @@ class JsonConnectionController(
}

case req @ Request(method, _, _) if requestHandlers.contains(method) =>
refreshIdleTime(method)
val handler = context.actorOf(
requestHandlers(method),
s"request-handler-$method-${UUID.randomUUID()}"
)
handler.forward(req)
}

private def refreshIdleTime(method: Method): Unit = {
method match {
case InitialPing | Ping =>
// ignore
case _ =>
idlenessMonitor ! MonitoringProtocol.ResetIdleTime
}
}

private def createRequestHandlers(
rpcSession: JsonSession
): Map[Method, Props] = {
Expand Down Expand Up @@ -473,6 +487,7 @@ object JsonConnectionController {
stdErrController: ActorRef,
stdInController: ActorRef,
runtimeConnector: ActorRef,
idlenessMonitor: ActorRef,
languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds
): Props =
Expand All @@ -490,6 +505,7 @@ object JsonConnectionController {
stdErrController,
stdInController,
runtimeConnector,
idlenessMonitor,
languageServerConfig,
requestTimeout
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class JsonConnectionControllerFactory(
stdErrController: ActorRef,
stdInController: ActorRef,
runtimeConnector: ActorRef,
idlenessMonitor: ActorRef,
config: Config
)(implicit system: ActorSystem)
extends ClientControllerFactory {
Expand All @@ -50,6 +51,7 @@ class JsonConnectionControllerFactory(
stdErrController,
stdInController,
runtimeConnector,
idlenessMonitor,
config
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Loading