Skip to content

Commit

Permalink
ask user to request timeout [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek committed Nov 22, 2023
1 parent 55e8ab6 commit 8d006d4
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ final class BspServers(
client,
newConnection,
tables.dismissedNotifications.ReconnectBsp,
tables.dismissedNotifications.RequestTimeout,
config,
details.getName(),
bspStatusOpt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ final class BloopServers(
() =>
connectToLauncher(bloopVersion, config.bloopPort, userConfiguration),
tables.dismissedNotifications.ReconnectBsp,
tables.dismissedNotifications.RequestTimeout,
config,
name,
bspStatusOpt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class BuildServerConnection private (
initialConnection: BuildServerConnection.LauncherConnection,
languageClient: LanguageClient,
reconnectNotification: DismissedNotifications#Notification,
requestTimeOutNotification: DismissedNotifications#Notification,
config: MetalsServerConfig,
workspace: AbsolutePath,
supportsWrappedSources: Boolean,
Expand All @@ -71,7 +72,12 @@ class BuildServerConnection private (
}

val requestRegistry =
new RequestRegistry(defaultMinTimeout, initialConnection.cancelables)
new RequestRegistry(
defaultMinTimeout,
initialConnection.cancelables,
languageClient,
Some(requestTimeOutNotification),
)

private val isShuttingDown = new AtomicBoolean(false)
private val onReconnection =
Expand Down Expand Up @@ -423,7 +429,7 @@ class BuildServerConnection private (
private def register[T: ClassTag](
action: MetalsBuildServer => CompletableFuture[T],
onFail: => Option[(T, String)] = None,
timeout: Timeout = Timeout.DefaultFlexTimeout,
timeout: Timeout = Timeout.NoTimeout,
): CompletableFuture[T] = {
val localCancelable = new MutableCancelable()
def runWithCanceling(
Expand Down Expand Up @@ -500,6 +506,7 @@ object BuildServerConnection {
localClient: MetalsBuildClient,
languageClient: MetalsLanguageClient,
connect: () => Future[SocketConnection],
requestTimeOutNotification: DismissedNotifications#Notification,
reconnectNotification: DismissedNotifications#Notification,
config: MetalsServerConfig,
serverName: String,
Expand Down Expand Up @@ -572,6 +579,7 @@ object BuildServerConnection {
setupServer,
connection,
languageClient,
requestTimeOutNotification,
reconnectNotification,
config,
projectRoot,
Expand All @@ -588,6 +596,7 @@ object BuildServerConnection {
languageClient,
connect,
reconnectNotification,
requestTimeOutNotification,
config,
serverName,
bspStatusOpt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class DismissedNotifications(conn: () => Connection, time: Time) {
val ReconnectScalaCli = new Notification(13)
val ScalaCliImportAuto = new Notification(14)
val BspErrors = new Notification(15)
val RequestTimeout = new Notification(16)

val all: List[Notification] = List(
Only212Navigation,
Expand Down
23 changes: 23 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,29 @@ object Messages {
}
}

object RequestTimeout {

val cancel = new MessageActionItem("Cancel")
val waitAction = new MessageActionItem("Wait")
val waitAlways = new MessageActionItem("WaitAlways")

def params(actionName: String, minutes: Int): ShowMessageRequestParams = {
val params = new ShowMessageRequestParams()
params.setMessage(
s"$actionName request is taking longer than expected (over $minutes minutes), do you want to cancel and rerun it?"
)
params.setType(MessageType.Info)
params.setActions(
List(
cancel,
waitAction,
waitAlways,
).asJava
)
params
}
}

}

object FileOutOfScalaCliBspScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ final class Ammonite(
workspace(),
),
tables.dismissedNotifications.ReconnectAmmonite,
tables.dismissedNotifications.RequestTimeout,
config,
"Ammonite",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class ScalaCli(
languageClient,
() => ScalaCli.socketConn(command, connDir),
tables.dismissedNotifications.ReconnectScalaCli,
tables.dismissedNotifications.RequestTimeout,
config(),
"Scala CLI",
supportsWrappedSources = Some(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,75 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.duration.Duration
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import scala.meta.internal.metals.Cancelable
import scala.meta.internal.metals.CancelableFuture
import scala.meta.internal.metals.MetalsEnrichments._

object FutureWithTimeout {
def apply[T](duration: Duration)(

def apply[T](
duration: Duration,
onTimeout: Duration => Future[FutureWithTimeout.OnTimeout],
)(
future: () => CompletableFuture[T]
)(implicit ex: ExecutionContext): CancelableFuture[(T, Duration)] = {
val result = Promise[(T, Duration)]()
val timeBefore: Long = System.currentTimeMillis()
val resultFuture = future()
val cancelable = Cancelable { () =>
Try(resultFuture.cancel(true))
val mappedRequest = resultFuture.asScala.map { res =>
val timeAfter = System.currentTimeMillis()
val execTime = timeAfter - timeBefore
(res, Duration(execTime, TimeUnit.MILLISECONDS))
}

mappedRequest.onComplete(result.tryComplete)

val cancelable = new Cancelable {
override def cancel(): Unit = Try(resultFuture.cancel(true))
}
val withTimeout =
Future {
val res = Await.result(resultFuture.asScala, duration)
val timeAfter = System.currentTimeMillis()
val execTime = timeAfter - timeBefore
(res, Duration(execTime, TimeUnit.MILLISECONDS))
}

withTimeout.onComplete {
case Failure(_: TimeoutException) => cancelable.cancel()
case _ =>
def createWithTimeout(duration: Duration): Future[(T, Duration)] =
mappedRequest.withTimeout(duration.length.toInt, duration.unit)

def withOnTimeOut(
withTimeout: Future[(T, Duration)],
duration: Duration,
): Future[(T, Duration)] = {
withTimeout.transformWith {
case Success(res) => Future.successful(res)
case Failure(e: TimeoutException) =>
for {
action <- onTimeout(duration)
res <- action match {
case FutureWithTimeout.Cancel =>
result.tryFailure(e)
cancelable.cancel()
Future.failed(e)
case FutureWithTimeout.Wait =>
withOnTimeOut(createWithTimeout(duration * 3), duration * 3)
case FutureWithTimeout.Dismiss =>
mappedRequest
}
} yield res
case Failure(e) => Future.failed(e)
}
}
CancelableFuture(withTimeout, cancelable)

withOnTimeOut(createWithTimeout(duration), duration)

CancelableFuture(result.future, cancelable)
}

sealed trait OnTimeout
case object Wait extends OnTimeout
case object Cancel extends OnTimeout
case object Dismiss extends OnTimeout
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,81 @@
package scala.meta.internal.metals.utils

import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import scala.meta.internal.metals.Cancelable
import scala.meta.internal.metals.CancelableFuture
import scala.meta.internal.metals.DismissedNotifications
import scala.meta.internal.metals.Messages.RequestTimeout
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.MutableCancelable

import org.eclipse.lsp4j.services.LanguageClient

class RequestRegistry(
defaultMinTimeout: Duration,
initialCancellables: List[Cancelable],
languageClient: LanguageClient,
requestTimeOutNotification: Option[DismissedNotifications#Notification] =
None,
)(implicit
ex: ExecutionContext
) {
private val timeouts: Timeouts = new Timeouts(defaultMinTimeout)
private val ongoingRequests =
new MutableCancelable().addAll(initialCancellables)

private def onTimeout(
actionName: String
)(duration: Duration): Future[FutureWithTimeout.OnTimeout] = {
languageClient
.showMessageRequest(
RequestTimeout.params(actionName, duration.toMinutes.toInt)
)
.asScala
.map {
case RequestTimeout.waitAction => FutureWithTimeout.Wait
case RequestTimeout.cancel => FutureWithTimeout.Cancel
case RequestTimeout.waitAlways =>
requestTimeOutNotification.foreach(_.dismiss(7, TimeUnit.DAYS))
FutureWithTimeout.Dismiss
case _ => FutureWithTimeout.Dismiss
}
}

def register[T](
action: () => CompletableFuture[T],
timeout: Timeout = Timeout.DefaultFlexTimeout,
timeout: Timeout = Timeout.NoTimeout,
): CancelableFuture[T] = {
val CancelableFuture(result, cancelable) = getTimeout(timeout) match {
case Some(timeoutValue) =>
FutureWithTimeout(timeoutValue)(action)
.transform {
case Success((res, time)) =>
timeouts.measured(timeout, time)
Success(res)
case Failure(e: TimeoutException) =>
timeouts.measured(timeout, timeoutValue)
Failure(e)
case Failure(e) => Failure(e)
val CancelableFuture(result, cancelable) =
timeouts.getNameAndTimeout(timeout) match {
case Some((actionName, timeoutValue))
if !requestTimeOutNotification.exists(_.isDismissed) =>
FutureWithTimeout(timeoutValue, onTimeout(actionName)(_))(action)
.transform {
case Success((res, time)) =>
timeouts.measured(timeout, time)
Success(res)
case Failure(e: TimeoutException) =>
timeouts.measured(timeout, timeoutValue)
Failure(e)
case Failure(e) => Failure(e)
}
case _ =>
val resultFuture = action()
val cancelable = Cancelable { () =>
Try(resultFuture.cancel(true))
}
case None =>
val resultFuture = action()
val cancelable = Cancelable { () =>
Try(resultFuture.cancel(true))
}
CancelableFuture(resultFuture.asScala, cancelable)
}
CancelableFuture(resultFuture.asScala, cancelable)
}

ongoingRequests.add(cancelable)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scala.concurrent.duration.Duration
sealed trait Timeout
object Timeout {
case object NoTimeout extends Timeout
case object DefaultFlexTimeout extends Timeout
case class DefaultFlexTimeout(id: String) extends Timeout
case class FlexTimeout(id: String, minTimeout: Duration) extends Timeout
}

Expand All @@ -23,32 +23,35 @@ class Timeouts(defaultMinTimeout: Duration) {
case None => Some(AvgTime.of(time))
}
timeout match {
case Timeout.DefaultFlexTimeout =>
case Timeout.DefaultFlexTimeout(_) =>
defaultFlexTimeout.getAndUpdate(addToOption(_))
case Timeout.FlexTimeout(id, _) =>
timeouts.getAndUpdate(_.updatedWith(id)(addToOption))
case _ =>
}
}

def getTimeout(timeout: Timeout): Option[Duration] = {
def getNameAndTimeout(timeout: Timeout): Option[(String, Duration)] = {
timeout match {
case Timeout.DefaultFlexTimeout =>
case Timeout.DefaultFlexTimeout(id) =>
Some(
defaultFlexTimeout.get
.map(_.avgWithMin(defaultMinTimeout))
.getOrElse(defaultMinTimeout)
)
).map((id, _))
case Timeout.FlexTimeout(id, minTimeout) =>
Some(
timeouts.get
.get(id)
.map(_.avgWithMin(minTimeout))
.getOrElse(minTimeout)
)
).map((id, _))
case Timeout.NoTimeout => None
}
}

def getTimeout(timeout: Timeout): Option[Duration] =
getNameAndTimeout(timeout).map(_._2)
}

case class AvgTime(samples: Int, totalTime: Long) {
Expand Down
Loading

0 comments on commit 8d006d4

Please sign in to comment.