Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry mechanism for sbt #450

Merged
merged 30 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
59ea584
add retry mechnism to artifact resolve
Apr 15, 2023
1286857
future await only once
Apr 16, 2023
b6bae8b
adding retry config to sbt-coursier
Apr 16, 2023
43d18ec
retries fix compile errors
Apr 22, 2023
dd81dc1
fix compile errors
Apr 29, 2023
514630a
fix unit tests for resolving task- return resolution error in case ex…
May 3, 2023
9203c84
fix retry coursier setting errors
hagay3 May 5, 2023
b6023bf
add backoff retry and fix future behaviour
hagay3 May 5, 2023
f2d8625
add specific erros for retry
hagay3 May 6, 2023
5bfb0b9
improve retry for specific errors
hagay3 May 6, 2023
4ce4d4a
Minimize diff
alexarchambault Nov 27, 2023
8addf22
NIT
alexarchambault Nov 27, 2023
91a9183
NIT Formatting
alexarchambault Nov 27, 2023
aa0bd70
NIT No need of a match here
alexarchambault Nov 27, 2023
853fa65
NIT No need of intermediate variable
alexarchambault Nov 27, 2023
7f5b9c6
Unnecessary local method
alexarchambault Nov 27, 2023
7ae070a
Renaming
alexarchambault Nov 27, 2023
bcc10d9
Don't retry for random errors
alexarchambault Nov 27, 2023
45f4a73
NIT
alexarchambault Nov 27, 2023
065331e
Inline local method
alexarchambault Nov 27, 2023
3dc925d
Start attempt at 0
alexarchambault Nov 27, 2023
e50c181
Compute exponential back-off delay alongside recursion
alexarchambault Nov 27, 2023
1fed5f7
Keep things as Task-s
alexarchambault Nov 27, 2023
ffc6500
Inline private method
alexarchambault Nov 27, 2023
bd19b3d
Check for connection timeouts only
alexarchambault Nov 27, 2023
3b3c2c9
NIT
alexarchambault Nov 27, 2023
46431a3
Make the scheduler global, initialize it lazily
alexarchambault Nov 27, 2023
f999466
NIT
alexarchambault Nov 27, 2023
e19428b
DO NOT COMMIT Temporary re-indent things for smaller overall diff
alexarchambault Nov 27, 2023
ea6b3d6
Revert "DO NOT COMMIT Temporary re-indent things for smaller overall …
alexarchambault Nov 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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