Skip to content

Commit

Permalink
Retry mechanism for sbt (#450)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Hagai Ovadia <hagai.ovadia@is.com>
Co-authored-by: Alexandre Archambault <alexandre.archambault@gmail.com>
  • Loading branch information
3 people committed Nov 28, 2023
1 parent c8a9925 commit dab3f9b
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import lmcoursier.definitions.{Authentication, CacheLogger, CachePolicy, FromCou
import sbt.librarymanagement.{Resolver, UpdateConfiguration, ModuleID, CrossVersion, ModuleInfo, ModuleDescriptorConfiguration}
import xsbti.Logger

import scala.concurrent.duration.Duration
import scala.concurrent.duration.{Duration, FiniteDuration}
import java.net.URL
import java.net.URLClassLoader

Expand Down Expand Up @@ -59,4 +59,5 @@ import java.net.URLClassLoader
providedInCompile: Boolean = false, // unused, kept for binary compatibility
@since
protocolHandlerDependencies: Seq[ModuleID] = Vector.empty,
retry: Option[(FiniteDuration, Int)] = None,
)
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ class CoursierDependencyResolution(
.withExclusions(excludeDependencies),
strictOpt = conf.strict.map(ToCoursier.strict),
missingOk = conf.missingOk,
retry = conf.retry.getOrElse(ResolutionParams.defaultRetry),
)

def artifactsParams(resolutions: Map[Configuration, Resolution]): ArtifactsParams =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import lmcoursier.definitions.ToCoursier
import coursier.util.Task

import scala.collection.mutable
import scala.concurrent.duration.{DurationInt, FiniteDuration}

// private[coursier]
final case class ResolutionParams(
Expand All @@ -30,6 +31,7 @@ final case class ResolutionParams(
params: coursier.params.ResolutionParams,
strictOpt: Option[Strict],
missingOk: Boolean,
retry: (FiniteDuration, Int)
) {

lazy val allConfigExtends: Map[Configuration, Set[Configuration]] = {
Expand Down Expand Up @@ -106,4 +108,5 @@ object ResolutionParams {
) ++ sys.props
}

val defaultRetry: (FiniteDuration, Int) = (1.seconds, 3)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package lmcoursier.internal

import coursier.{Resolution, Resolve}
import coursier.cache.internal.ThreadUtil
import coursier.cache.loggers.{FallbackRefreshDisplay, ProgressBarRefreshDisplay, RefreshLogger}
import coursier.core._
import coursier.error.ResolutionError
import coursier.error.ResolutionError.CantDownloadModule
import coursier.ivy.IvyRepository
import coursier.maven.MavenRepositoryLike
import coursier.params.rule.RuleResolution
import coursier.util.Task
import sbt.util.Logger

import scala.concurrent.duration.FiniteDuration
import scala.collection.mutable

// private[coursier]
Expand Down Expand Up @@ -79,47 +84,85 @@ object ResolutionRun {
if (verbosityLevel >= 2)
log.info(initialMessage)

Resolve()
// re-using various caches from a resolution of a configuration we extend
.withInitialResolution(startingResolutionOpt)
.withDependencies(
params.dependencies.collect {
case (config, dep) if configs(config) =>
dep
}
)
.withRepositories(repositories)
.withResolutionParams(
params
.params
.addForceVersion((if (isSandboxConfig) Nil else params.interProjectDependencies.map(_.moduleVersion)): _*)
.withForceScalaVersion(params.autoScalaLibOpt.nonEmpty)
.withScalaVersionOpt(params.autoScalaLibOpt.map(_._2))
.withTypelevel(params.params.typelevel)
.withRules(rules)
)
.withCache(
params
.cache
.withLogger(
params.loggerOpt.getOrElse {
RefreshLogger.create(
if (RefreshLogger.defaultFallbackMode)
new FallbackRefreshDisplay()
else
ProgressBarRefreshDisplay.create(
if (printOptionalMessage) log.info(initialMessage),
if (printOptionalMessage || verbosityLevel >= 2)
log.info(s"Resolved ${params.projectName} dependencies")
)
)
}
)
)
.either() match {
case Left(err) if params.missingOk => Right(err.resolution)
case others => others
}
val resolveTask: Resolve[Task] = {
Resolve()
// re-using various caches from a resolution of a configuration we extend
.withInitialResolution(startingResolutionOpt)
.withDependencies(
params.dependencies.collect {
case (config, dep) if configs(config) =>
dep
}
)
.withRepositories(repositories)
.withResolutionParams(
params
.params
.addForceVersion((if (isSandboxConfig) Nil else params.interProjectDependencies.map(_.moduleVersion)): _*)
.withForceScalaVersion(params.autoScalaLibOpt.nonEmpty)
.withScalaVersionOpt(params.autoScalaLibOpt.map(_._2))
.withTypelevel(params.params.typelevel)
.withRules(rules)
)
.withCache(
params
.cache
.withLogger(
params.loggerOpt.getOrElse {
RefreshLogger.create(
if (RefreshLogger.defaultFallbackMode)
new FallbackRefreshDisplay()
else
ProgressBarRefreshDisplay.create(
if (printOptionalMessage) log.info(initialMessage),
if (printOptionalMessage || verbosityLevel >= 2)
log.info(s"Resolved ${params.projectName} dependencies")
)
)
}
)
)
}

val (period, maxAttempts) = params.retry
val finalResult: Either[ResolutionError, Resolution] = {

def retry(attempt: Int, waitOnError: FiniteDuration): Task[Either[ResolutionError, Resolution]] =
resolveTask
.io
.attempt
.flatMap {
case Left(e: ResolutionError) =>
val hasConnectionTimeouts = e.errors.exists {
case err: CantDownloadModule => err.perRepositoryErrors.exists(_.contains("Connection timed out"))
case _ => false
}
if (hasConnectionTimeouts)
if (attempt + 1 >= maxAttempts) {
log.error(s"Failed, maximum iterations ($maxAttempts) reached")
Task.point(Left(e))
}
else {
log.warn(s"Attempt ${attempt + 1} failed: $e")
Task.completeAfter(retryScheduler, waitOnError).flatMap { _ =>
retry(attempt + 1, waitOnError * 2)
}
}
else
Task.point(Left(e))
case Left(ex) =>
Task.fail(ex)
case Right(value) =>
Task.point(Right(value))
}

retry(0, period).unsafeRun()(resolveTask.cache.ec)
}

finalResult match {
case Left(err) if params.missingOk => Right(err.resolution)
case others => others
}
}

def resolutions(
Expand Down Expand Up @@ -164,4 +207,5 @@ object ResolutionRun {
}
}

private lazy val retryScheduler = ThreadUtil.fixedScheduledThreadPool(1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import lmcoursier.definitions._
import sbt.librarymanagement.{Resolver, UpdateConfiguration, ModuleID, CrossVersion, ModuleInfo, ModuleDescriptorConfiguration}
import xsbti.Logger

import scala.concurrent.duration.Duration
import scala.concurrent.duration.{Duration, FiniteDuration}
import java.io.File
import java.net.URL
import java.net.URLClassLoader
Expand Down Expand Up @@ -74,6 +74,7 @@ package object syntax {
sbtClassifiers = false,
providedInCompile = false,
protocolHandlerDependencies = Vector.empty,
retry = None
)
}

Expand Down Expand Up @@ -107,6 +108,9 @@ package object syntax {

def withUpdateConfiguration(conf: UpdateConfiguration): CoursierConfiguration =
value.withMissingOk(conf.missingOk)

def withRetry(retry: (FiniteDuration, Int)): CoursierConfiguration =
value.withRetry(Some((retry._1, retry._2)))
}

implicit class PublicationOp(value: Publication) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import sbt.{AutoPlugin, Classpaths, Compile, Setting, TaskKey, Test, settingKey,
import sbt.Keys._
import sbt.librarymanagement.DependencyBuilders.OrganizationArtifactName
import sbt.librarymanagement.{ModuleID, Resolver, URLRepository}
import scala.concurrent.duration.FiniteDuration

object SbtCoursierShared extends AutoPlugin {

Expand Down Expand Up @@ -52,6 +53,8 @@ object SbtCoursierShared extends AutoPlugin {
val coursierCache = settingKey[File]("")

val sbtCoursierVersion = Properties.version

val coursierRetry = taskKey[Option[(FiniteDuration, Int)]]("Retry for downloading dependencies")
}

import autoImport._
Expand All @@ -71,7 +74,8 @@ object SbtCoursierShared extends AutoPlugin {
coursierReorderResolvers := true,
coursierKeepPreloaded := false,
coursierLogger := None,
coursierCache := CoursierDependencyResolution.defaultCacheLocation
coursierCache := CoursierDependencyResolution.defaultCacheLocation,
coursierRetry := None
)

private val pluginIvySnapshotsBase = Resolver.SbtRepositoryRoot.stripSuffix("/") + "/ivy-snapshots"
Expand Down Expand Up @@ -178,7 +182,8 @@ object SbtCoursierShared extends AutoPlugin {
confs ++ extraSources.toSeq ++ extraDocs.toSeq
},
mavenProfiles := Set.empty,
versionReconciliation := Seq.empty
versionReconciliation := Seq.empty,
coursierRetry := None
) ++ {
if (pubSettings)
IvyXmlGeneration.generateIvyXmlSettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import coursier.util.Artifact
import sbt.librarymanagement.{GetClassifiersModule, Resolver}
import sbt.{InputKey, SettingKey, TaskKey}

import scala.concurrent.duration.Duration
import scala.concurrent.duration.{Duration, FiniteDuration}

object Keys {
val coursierParallelDownloads = SettingKey[Int]("coursier-parallel-downloads")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ object ResolutionTasks {
else
Def.task(coursierRecursiveResolvers.value.distinct)

val retrySettings = Def.task(coursierRetry.value)

Def.task {
val projectName = thisProjectRef.value.project

Expand Down Expand Up @@ -169,6 +171,7 @@ object ResolutionTasks {
.withExclusions(excludeDeps),
strictOpt = strictOpt,
missingOk = missingOk,
retry = retrySettings.value.getOrElse(ResolutionParams.defaultRetry)
),
verbosityLevel,
log
Expand Down

0 comments on commit dab3f9b

Please sign in to comment.