Skip to content

Commit

Permalink
Make staging repository operations retry on failure
Browse files Browse the repository at this point in the history
  • Loading branch information
MaciejG604 committed Sep 27, 2023
1 parent 26f4f9c commit 491991e
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 19 deletions.
10 changes: 5 additions & 5 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,25 @@ object Deps {
}

class Publish(val crossScalaVersion: String) extends CrossScalaModule with Published {
def ivyDeps = super.ivyDeps() ++ Seq(
override def ivyDeps = super.ivyDeps() ++ Seq(
Deps.coursierCache,
Deps.coursierCore,
Deps.collectionCompat,
Deps.jsoniterCore,
Deps.sttp
)
def compileIvyDeps = super.compileIvyDeps() ++ Seq(
override def compileIvyDeps = super.compileIvyDeps() ++ Seq(
Deps.jsoniterMacros
)
def javacOptions = super.javacOptions() ++ Seq(
override def javacOptions = super.javacOptions() ++ Seq(
"--release",
"8"
)
object test extends Tests {
def ivyDeps = super.ivyDeps() ++ Seq(
override def ivyDeps = super.ivyDeps() ++ Seq(
Deps.utest
)
def testFramework = "utest.runner.Framework"
override def testFramework = "utest.runner.Framework"
}
}

Expand Down
14 changes: 7 additions & 7 deletions publish/src/coursier/publish/sonatype/HttpClientUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,18 @@ private[sonatype] final case class HttpClientUtil(
}
}

def create(url: String, post: Option[Array[Byte]] = None, isJson: Boolean = false): Unit = {

def create(
url: String,
post: Option[Array[Byte]] = None,
isJson: Boolean = false
): Response[Array[Byte]] = {
if (verbosity >= 1)
Console.err.println(s"Getting $url")
val resp = request(url, post, isJson).send(backend)
if (verbosity >= 1)
Console.err.println(s"Done: $url")
Console.err.println(s"Got HTTP ${resp.code.code} from $url")

if (resp.code.code != 201)
throw new Exception(
s"Failed to get $url (http status: ${resp.code.code}, response: ${Try(new String(resp.body, StandardCharsets.UTF_8)).getOrElse("")})"
)
resp
}

def get[T: JsonValueCodec](
Expand Down
31 changes: 24 additions & 7 deletions publish/src/coursier/publish/sonatype/SonatypeApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import coursier.publish.sonatype.logger.SonatypeLogger
import coursier.util.Task
import sttp.client3._

import scala.annotation.tailrec
import scala.concurrent.duration.Duration
import scala.util.Try
import scala.util.control.NonFatal

final case class SonatypeApi(
backend: SttpBackend[Identity, Any],
base: String,
authentication: Option[Authentication],
verbosity: Int,
retryOnTimeout: Int = 3
retryOnTimeout: Int = 3,
stagingRepoRetries: Int = 3
) {

// vaguely inspired by https://github.com/lihaoyi/mill/blob/7b4ced648ecd9b79b3a16d67552f0bb69f4dd543/scalalib/src/mill/scalalib/publish/SonatypeHttpApi.scala
Expand Down Expand Up @@ -125,12 +128,26 @@ final case class SonatypeApi(
profile: Profile,
repositoryId: String,
description: String
): Unit =
clientUtil.create(
s"${profile.uri}/$action",
post = Some(postBody(writeToArray(StagedRepositoryRequest(description, repositoryId)))),
isJson = true
)
): Unit = {
val url = s"${profile.uri}/$action"
val body = postBody(writeToArray(StagedRepositoryRequest(description, repositoryId)))
@tailrec
def sendRequest(attempt: Int): Unit = {
val resp = clientUtil.create(url, post = Some(body), isJson = true)

if (!resp.code.isSuccess) {
if (attempt >= stagingRepoRetries)
throw new Exception(
s"Failed to get $url (http status: ${resp.code.code}, response: ${Try(new String(resp.body, StandardCharsets.UTF_8)).getOrElse("")})"
)

Thread.sleep(1000 * attempt)
sendRequest(attempt + 1)
}
}

sendRequest(1)
}

def sendCloseStagingRepositoryRequest(
profile: Profile,
Expand Down
63 changes: 63 additions & 0 deletions publish/test/src/coursier/publish/sonatype/SonatypeTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package src.coursier.publish.sonatype

import coursier.publish.sonatype.SonatypeApi
import sttp.client3.testing.SttpBackendStub

import utest._

object SonatypeTests extends TestSuite {
val tests = Tests {
test("Retry sonatype repository actions") {
var count = 0
val mockBackend = SttpBackendStub.synchronous
.whenRequestMatches { _ => count += 1; count < 6 }
.thenRespondServerError()
.whenRequestMatches(_ => count >= 6)
.thenRespondOk()

{
val sonatypeApi20 = SonatypeApi(
mockBackend,
base = "https://oss.sonatype.org",
authentication = None,
verbosity = 0,
retryOnTimeout = 1,
stagingRepoRetries = 20
)

sonatypeApi20.sendPromoteStagingRepositoryRequest(
SonatypeApi.Profile("id", "name", "uri"),
"repo",
"description"
)

assert(count == 6)
}

count = 0

{
val sonatypeApi3 = SonatypeApi(
mockBackend,
base = "https://oss.sonatype.org",
authentication = None,
verbosity = 0,
retryOnTimeout = 1
)

try sonatypeApi3.sendPromoteStagingRepositoryRequest(
SonatypeApi.Profile("id", "name", "uri"),
"repo",
"description"
)
catch {
case e: Exception
if e.getMessage == "Failed to get uri/promote (http status: 500, response: Internal server error)" =>
case _: Throwable => assert(false)
}

assert(count == 3)
}
}
}
}

0 comments on commit 491991e

Please sign in to comment.