Add server idle timeout

This commit adds the ability for sbt to automatically shut itself down
if it has been idle for some duration of time. The motivation is that
if the user may not realize they have an sbt server running in the
background that is using resources. We don't want to be too aggressive
with the idle timeout because that can reduce the efficacy of the thin
client. A value of one week is chosen so that users can enjoy a long
weekend and when they return to their computer, they won't have to
restart sbt. If they haven't used the server in at least a week, it
seems prudent to just kill it.
This commit is contained in:
Ethan Atkins 2020-06-23 18:58:02 -07:00
parent f8e06def74
commit ea823f1051
5 changed files with 44 additions and 8 deletions

View File

@ -16,6 +16,7 @@ import sbt.internal.server.ServerHandler
import sbt.internal.util.{ AttributeKey, Terminal }
import sbt.librarymanagement.ModuleID
import sbt.util.Level
import scala.concurrent.duration.FiniteDuration
object BasicKeys {
val historyPath = AttributeKey[Option[File]](
@ -83,6 +84,13 @@ object BasicKeys {
10000
)
val serverIdleTimeout =
AttributeKey[Option[FiniteDuration]](
"serverIdleTimeOut",
"If set to a defined value, sbt server will exit if it goes at least the specified duration without receiving any commands.",
10000
)
// Unlike other BasicKeys, this is not used directly as a setting key,
// and severLog / logLevel is used instead.
private[sbt] val serverLogLevel =

View File

@ -378,6 +378,7 @@ object Defaults extends BuildCommon {
interactionService :== CommandLineUIService,
autoStartServer := true,
serverHost := "127.0.0.1",
serverIdleTimeout := Some(new FiniteDuration(7, TimeUnit.DAYS)),
serverPort := 5000 + (Hash
.toHex(Hash(appConfiguration.value.baseDirectory.toString))
.## % 1000),

View File

@ -95,6 +95,7 @@ object Keys {
val serverHost = SettingKey(BasicKeys.serverHost)
val serverAuthentication = SettingKey(BasicKeys.serverAuthentication)
val serverConnectionType = SettingKey(BasicKeys.serverConnectionType)
val serverIdleTimeout = SettingKey(BasicKeys.serverIdleTimeout)
val windowsServerSecurityLevel = SettingKey(BasicKeys.windowsServerSecurityLevel)
val fullServerHandlers = SettingKey(BasicKeys.fullServerHandlers)
val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.")

View File

@ -24,6 +24,7 @@ import Keys.{
templateResolverInfos,
autoStartServer,
serverHost,
serverIdleTimeout,
serverLog,
serverPort,
serverAuthentication,
@ -52,6 +53,7 @@ import sbt.util.{ Show, Level }
import sjsonnew.JsonFormat
import language.experimental.macros
import scala.concurrent.duration.FiniteDuration
sealed trait ProjectDefinition[PR <: ProjectReference] {
@ -515,6 +517,7 @@ object Project extends ProjectExtra {
val startSvr: Option[Boolean] = get(autoStartServer)
val host: Option[String] = get(serverHost)
val port: Option[Int] = get(serverPort)
val timeout: Option[Option[FiniteDuration]] = get(serverIdleTimeout)
val authentication: Option[Set[ServerAuthentication]] = get(serverAuthentication)
val connectionType: Option[ConnectionType] = get(serverConnectionType)
val srvLogLevel: Option[Level.Value] = (logLevel in (ref, serverLog)).get(structure.data)
@ -534,6 +537,7 @@ object Project extends ProjectExtra {
.setCond(serverHost.key, host)
.setCond(serverAuthentication.key, authentication)
.setCond(serverConnectionType.key, connectionType)
.setCond(serverIdleTimeout.key, timeout)
.put(historyPath.key, history)
.put(templateResolverInfos.key, trs)
.setCond(shellPrompt.key, prompt)

View File

@ -71,14 +71,36 @@ private[sbt] final class CommandExchange {
state: Option[State],
logger: Logger
): Exec = {
@tailrec def impl(deadline: Option[Deadline]): Exec = {
val idleDeadline = state.flatMap { s =>
lastState.set(s)
s.get(BasicKeys.serverIdleTimeout) match {
case Some(Some(d)) => Some(d.fromNow)
case _ => None
}
}
@tailrec def impl(gcDeadline: Option[Deadline], idleDeadline: Option[Deadline]): Exec = {
state.foreach(s => prompt(ConsolePromptEvent(s)))
def poll: Option[Exec] =
def poll: Option[Exec] = {
val deadline = gcDeadline.toSeq ++ idleDeadline match {
case s @ Seq(_, _) => Some(s.min)
case s => s.headOption
}
Option(deadline match {
case Some(d: Deadline) =>
commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS)
commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS) match {
case null if idleDeadline.fold(false)(_.isOverdue) =>
state.foreach { s =>
s.get(BasicKeys.serverIdleTimeout) match {
case Some(Some(d)) => s.log.info(s"sbt idle timeout of $d expired")
case _ =>
}
}
Exec("exit", Some(CommandSource(ConsoleChannel.defaultName)))
case x => x
}
case _ => commandQueue.take
})
}
poll match {
case Some(exec) if exec.source.fold(true)(s => channels.exists(_.name == s.channelName)) =>
exec.commandLine match {
@ -92,24 +114,24 @@ private[sbt] final class CommandExchange {
} match {
case Some(c) if c.isAttached =>
c.shutdown(false)
impl(deadline)
impl(gcDeadline, idleDeadline)
case _ => exec
}
case _ => exec
}
case None =>
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
val newDeadline = if (gcDeadline.fold(false)(_.isOverdue())) {
GCUtil.forceGcWithInterval(interval, logger)
None
} else deadline
impl(newDeadline)
} else gcDeadline
impl(newDeadline, idleDeadline)
}
}
// Do not manually run GC until the user has been idling for at least the min gc interval.
impl(interval match {
case d: FiniteDuration => Some(d.fromNow)
case _ => None
})
}, idleDeadline)
}
private def addConsoleChannel(): Unit =