Skip to content

Commit

Permalink
improvement: Simplify resolving presentation compiler
Browse files Browse the repository at this point in the history
Previously, we would have a lot of fallbacks that would be used if mtags was not avaiable. Now, each version of the compiler has a stable version of the compiler being released so there is no longer a reason to handle it via fallback. Instead we want to be explicit about what we are resolving.

It seems that previous way was flaky and while I can't reproduce the issues people had, I think simplifying it would help out at lest to get the exact failure of what is going on.
  • Loading branch information
tgodzik committed Mar 27, 2024
1 parent 00072f6 commit 8c7f800
Showing 1 changed file with 33 additions and 125 deletions.
158 changes: 33 additions & 125 deletions metals/src/main/scala/scala/meta/internal/metals/MtagsResolver.scala
Expand Up @@ -5,18 +5,13 @@ import java.util.concurrent.ConcurrentHashMap
import scala.concurrent.duration._
import scala.util.control.NonFatal

import scala.meta.internal.jdk.CollectionConverters._
import scala.meta.internal.metals.BuildInfo
import scala.meta.internal.semver.SemVer

import coursierapi.error.SimpleResolutionError
import org.jsoup.Jsoup

trait MtagsResolver {

/**
* Try and resolve mtags module for a given version of Scala.
* Can contain a bunch of fallbacks in case of non stable versions.
* @return information to use and load the presentation compiler implementation
*/
def resolve(scalaVersion: String): Option[MtagsBinaries]
Expand Down Expand Up @@ -93,7 +88,18 @@ object MtagsResolver {
removedScalaVersions.contains(version)

def resolve(scalaVersion: String): Option[MtagsBinaries] = {
resolve(scalaVersion, original = None)
if (hasStablePresentationCompiler(scalaVersion))
resolve(
scalaVersion,
original = None,
resolveType = ResolveType.StablePC,
)
else
resolve(
scalaVersion,
original = None,
resolveType = ResolveType.Regular,
)
}

private object ResolveType extends Enumeration {
Expand All @@ -103,17 +109,15 @@ object MtagsResolver {
/**
* Resolving order is following:
* 1. Built-in mtags matching scala version
* 2. Mtags matching exact scala version
* 3. Stable presentation compiler matching exact scala version
* 4. If scala version is a nightly, try to find latest supported snapshot
* 2. If presentation compiler is available we resolve it otherwise we resolve mtags.
*/
private def resolve(
scalaVersion: String,
original: Option[String],
resolveType: ResolveType.Value = ResolveType.Regular,
resolveType: ResolveType.Value,
): Option[MtagsBinaries] = {

def fetch(tries: Int = 0): State = logResolution {
def fetch(fetchResolveType: ResolveType.Value, tries: Int = 5): State =
try {
val metalsVersion = removedScalaVersions.getOrElse(
scalaVersion,
Expand All @@ -124,7 +128,7 @@ object MtagsResolver {
s"$scalaVersion is no longer supported in the current Metals versions, using the last known supported version $metalsVersion"
)
}
val jars = resolveType match {
val jars = fetchResolveType match {
case ResolveType.StablePC =>
Embedded.downloadScala3PresentationCompiler(scalaVersion)
case _ => Embedded.downloadMtags(scalaVersion, metalsVersion)
Expand All @@ -134,16 +138,17 @@ object MtagsResolver {
MtagsBinaries.Artifacts(
scalaVersion,
jars,
resolveType == ResolveType.StablePC,
fetchResolveType == ResolveType.StablePC,
)
)
} catch {
case NonFatal(_) if tries > 0 =>
fetch(fetchResolveType, tries - 1)
case NonFatal(e) =>
State.Failure(System.currentTimeMillis(), tries, e)
State.Failure(System.currentTimeMillis(), e)
}
}

def shouldResolveAgain(failure: State.Failure): Boolean = {
failure.tries < State.maxTriesInARow ||
(System
.currentTimeMillis() - failure.lastTryMillis) > 5.minutes.toMillis
}
Expand All @@ -159,8 +164,7 @@ object MtagsResolver {
s"Resolved latest nightly mtags version: $scalaVersion"
}
scribe.debug(msg)
case _: State.Failure
if !hasStablePresentationCompiler(scalaVersion) =>
case fail: State.Failure =>
val errorMsg = resolveType match {
case ResolveType.Regular =>
s"Failed to resolve mtags for $scalaVersion"
Expand All @@ -169,7 +173,7 @@ object MtagsResolver {
case ResolveType.Nightly =>
s"Failed to resolve latest nightly mtags version: $scalaVersion"
}
scribe.info(errorMsg)
scribe.error(errorMsg, fail.exception)
case _ =>
}
state
Expand All @@ -186,126 +190,30 @@ object MtagsResolver {
original.getOrElse(scalaVersion),
(_, value) => {
value match {
case null => fetch()
case null => logResolution(fetch(resolveType))
case succ: State.Success => succ
case failure: State.Failure if shouldResolveAgain(failure) =>
logResolution(fetch(resolveType))
case failure: State.Failure =>
if (shouldResolveAgain(failure))
fetch(failure.tries + 1)
else {
failure
}
failure
}
},
)

def logError(e: Throwable): Unit = {
val msg = s"Failed to fetch mtags for ${scalaVersion}"
e match {
case _: SimpleResolutionError =>
// no need to log traces for coursier error
// all explanation is in message
scribe.error(msg + "\n" + e.getMessage())
case _ =>
scribe.error(msg, e)
}
}

computed match {
case State.Success(v) =>
Some(v)
// Fallback to Stable PC version
case _: State.Failure
if resolveType != ResolveType.StablePC &&
hasStablePresentationCompiler(scalaVersion) =>
resolve(
scalaVersion,
None,
ResolveType.StablePC,
)
// Try to download latest supported snapshot
case _: State.Failure
if resolveType != ResolveType.Nightly &&
scalaVersion.contains("NIGHTLY") ||
scalaVersion.contains("nonbootstrapped") =>
findLatestSnapshot(scalaVersion) match {
case None => None
case Some(latestSnapshot) =>
scribe.warn(s"Using latest stable version $latestSnapshot")
resolve(
latestSnapshot,
Some(scalaVersion),
ResolveType.Nightly,
)
}
case failure: State.Failure =>
logError(failure.exception)
None
case _ => None
case State.Success(v) => Some(v)
case _: State.Failure => None
}
}
}

/**
* Nightlies version are able to work with artifacts compiled within the
* same RC version.
*
* For example 3.2.2-RC1-bin-20221009-2052fc2-NIGHTLY presentation compiler
* will work with classfiles compiled with 3.2.2-RC1-bin-20220910-ac6cd1c-NIGHTLY
*
* @param exactVersion version we failed to find and looking for an alternative for
* @return latest supported nightly version by thise version of metals
*/
private def findLatestSnapshot(exactVersion: String): Option[String] = try {

val metalsVersion = BuildInfo.metalsVersion

// strip timestamp to get only 3.2.2-RC1
val rcVersion = SemVer.Version
.fromString(exactVersion)
.copy(nightlyDate = None)
.toString()

val url =
s"https://oss.sonatype.org/content/repositories/snapshots/org/scalameta/"

val allScalametaArtifacts = Jsoup.connect(url).get

// find all the nightlies for current RC
val lastNightlies = allScalametaArtifacts
.select("a")
.asScala
.filter { a =>
val name = a.text()
name.contains("NIGHTLY") && name.contains(rcVersion)
}

// find last supported Scala version for this metals version
lastNightlies.reverseIterator
.find { nightlyLink =>
val link = nightlyLink.attr("href")

val mtagsPage = Jsoup.connect(link).get

mtagsPage
.select("a")
.asScala
.find(_.text() == metalsVersion + "/")
.isDefined
}
.map(_.text().stripPrefix("mtags_").stripSuffix("/"))

} catch {
case NonFatal(t) =>
scribe.error("Could not check latest nightlies", t)
None
}

sealed trait State
object State {
val maxTriesInARow: Int = 2
case class Success(v: MtagsBinaries.Artifacts) extends State
case class Failure(lastTryMillis: Long, tries: Int, exception: Throwable)
extends State
case class Failure(
lastTryMillis: Long,
exception: Throwable,
) extends State
}
}

Expand Down

0 comments on commit 8c7f800

Please sign in to comment.