Skip to content

Commit

Permalink
Add Idleness Http Endpoint (#1847)
Browse files Browse the repository at this point in the history
Implement `GET / _idle` request
  • Loading branch information
4e6 authored Jul 12, 2021
1 parent 399fa5e commit b3badf1
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 5 deletions.
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 time that the language server has
spent idle ([#1847](https://github.com/enso-org/enso/pull/1847)).

# Enso 0.2.13 (2021-07-09)

## Interpreter/Runtime
Expand Down
2 changes: 2 additions & 0 deletions docs/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ The protocol messages are broken up into documents as follows:
The messages and types pertaining to the project manager component.
- [**Language Server Message Specification:**](./protocol-language-server.md)
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.
140 changes: 140 additions & 0 deletions docs/language-server/language-server-http-endpoints.md
Original file line number Diff line number Diff line change
@@ -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}
```
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)

}
Loading

0 comments on commit b3badf1

Please sign in to comment.