Skip to content

Commit

Permalink
[#7] Improve retrying
Browse files Browse the repository at this point in the history
  • Loading branch information
szpak committed Feb 5, 2020
1 parent 1e34088 commit f667d46
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 27 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ dependencies {
testImplementation("com.github.tomakehurst:wiremock:2.25.1")
testImplementation("ru.lanwen.wiremock:wiremock-junit5:1.3.1")
testImplementation("org.assertj:assertj-core:3.14.0")
testImplementation("org.mockito:mockito-junit-jupiter:3.2.4")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
}

stutter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.github.gradlenexus.publishplugin

import io.github.gradlenexus.publishplugin.internal.BasicActionRetrier
import io.github.gradlenexus.publishplugin.internal.NexusClient
import io.github.gradlenexus.publishplugin.internal.StagingRepositoryTransitioner
import org.gradle.api.model.ObjectFactory
Expand All @@ -41,7 +42,7 @@ constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository
@TaskAction
fun closeStagingRepo() {
val client = NexusClient(repository.get().nexusUrl.get(), repository.get().username.orNull, repository.get().password.orNull, clientTimeout.orNull, connectTimeout.orNull)
val repositoryTransitioner = StagingRepositoryTransitioner(client, repository.get().retrying.get())
val repositoryTransitioner = StagingRepositoryTransitioner(client, BasicActionRetrier.retryUntilRepoTransitionIsCompletedRetrier(repository.get().retrying.get()))
logger.info("Closing staging repository with id '{}'", stagingRepositoryId.get())
repositoryTransitioner.effectivelyClose(stagingRepositoryId.get())
logger.info("Repository with id '{}' effectively closed", stagingRepositoryId.get())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.gradlenexus.publishplugin

import java.lang.RuntimeException

open class NexusPublishException : RuntimeException {
constructor(message: String) : super(message)
constructor(message: String, cause: Throwable) : super(message, cause)
}

open class RepositoryTransitionException(message: String) : NexusPublishException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.github.gradlenexus.publishplugin

import io.github.gradlenexus.publishplugin.internal.BasicActionRetrier
import io.github.gradlenexus.publishplugin.internal.NexusClient
import io.github.gradlenexus.publishplugin.internal.StagingRepositoryTransitioner
import org.gradle.api.model.ObjectFactory
Expand All @@ -42,7 +43,7 @@ constructor(objects: ObjectFactory, extension: NexusPublishExtension, repository
@TaskAction
fun releaseStagingRepo() {
val client = NexusClient(repository.get().nexusUrl.get(), repository.get().username.orNull, repository.get().password.orNull, clientTimeout.orNull, connectTimeout.orNull)
val repositoryTransitioner = StagingRepositoryTransitioner(client, repository.get().retrying.get())
val repositoryTransitioner = StagingRepositoryTransitioner(client, BasicActionRetrier.retryUntilRepoTransitionIsCompletedRetrier(repository.get().retrying.get()))
logger.info("Releasing staging repository with id '{}'", stagingRepositoryId.get())
repositoryTransitioner.effectivelyRelease(stagingRepositoryId.get())
logger.info("Repository with id '{}' effectively released", stagingRepositoryId.get())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.gradlenexus.publishplugin.internal

interface ActionRetrier<R> {
fun execute(operationToExecuteWithRetrying: () -> R): R
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.gradlenexus.publishplugin.internal

import io.github.alexo.retrier.Retrier
import io.github.gradlenexus.publishplugin.RetryingConfig
import io.github.gradlenexus.publishplugin.StagingRepository

open class BasicActionRetrier<R>(retryingConfig: RetryingConfig, stopFunction: (R) -> Boolean) : ActionRetrier<R> {

@Suppress("UNCHECKED_CAST")
private val retrier: Retrier = Retrier.Builder()
.withWaitStrategy(Retrier.Strategies.waitConstantly(retryingConfig.delayBetween.get().toMillis()))
.withStopStrategy(Retrier.Strategies.stopAfter(retryingConfig.maxNumber.get()))
.withResultRetryStrategy(stopFunction as ((Any) -> Boolean))
.build()

override fun execute(operationToExecuteWithRetrying: () -> R): R {
return retrier.execute(operationToExecuteWithRetrying)
}

companion object {
fun retryUntilRepoTransitionIsCompletedRetrier(retryingConfig: RetryingConfig): BasicActionRetrier<StagingRepository> {
return BasicActionRetrier(retryingConfig) { repo: StagingRepository -> repo.transitioning }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path

class NexusClient(private val baseUrl: URI, username: String?, password: String?, timeout: Duration?, connectTimeout: Duration?) {
open class NexusClient(private val baseUrl: URI, username: String?, password: String?, timeout: Duration?, connectTimeout: Duration?) {
private val api: NexusApi

init {
Expand Down Expand Up @@ -89,14 +89,14 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String?
return response.body()?.data?.stagedRepositoryId ?: throw RuntimeException("No response body")
}

fun closeStagingRepository(stagingRepositoryId: String) {
open fun closeStagingRepository(stagingRepositoryId: String) {
val response = api.closeStagingRepo(Dto(StagingRepositoryToTransit(listOf(stagingRepositoryId), "Closed by io.github.gradle-nexus.publish-plugin Gradle plugin"))).execute()
if (!response.isSuccessful) {
throw failure("close staging repository", response)
}
}

fun releaseStagingRepository(stagingRepositoryId: String) {
open fun releaseStagingRepository(stagingRepositoryId: String) {
val response = api.releaseStagingRepo(Dto(StagingRepositoryToTransit(listOf(stagingRepositoryId), "Release by io.github.gradle-nexus.publish-plugin Gradle plugin"))).execute()
if (!response.isSuccessful) {
throw failure("release staging repository", response)
Expand All @@ -106,7 +106,7 @@ class NexusClient(private val baseUrl: URI, username: String?, password: String?
fun getStagingRepositoryUri(stagingRepositoryId: String): URI =
URI.create("${baseUrl.toString().removeSuffix("/")}/staging/deployByRepositoryId/$stagingRepositoryId")

fun getStagingRepositoryStateById(stagingRepositoryId: String): StagingRepository {
open fun getStagingRepositoryStateById(stagingRepositoryId: String): StagingRepository {
val response = api.getStagingRepoById(stagingRepositoryId).execute()
if (response.code() == 404 && response.errorBody()?.string()?.contains(stagingRepositoryId) == true) {
return StagingRepository.notFound(stagingRepositoryId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,56 +16,48 @@

package io.github.gradlenexus.publishplugin.internal

import io.github.alexo.retrier.Retrier
import io.github.alexo.retrier.Retrier.Strategies.stopAfter
import io.github.gradlenexus.publishplugin.RetryingConfig
import io.github.gradlenexus.publishplugin.RepositoryTransitionException
import io.github.gradlenexus.publishplugin.StagingRepository
import org.gradle.api.GradleException
import org.slf4j.Logger
import org.slf4j.LoggerFactory

//TODO: RetryingConfig pullutes StagingRepositoryTransitioner with Gradle - problematic with unit testing
class StagingRepositoryTransitioner(val nexusClient: NexusClient, val retryingConfig: RetryingConfig) {
class StagingRepositoryTransitioner(val nexusClient: NexusClient, val retrier: ActionRetrier<StagingRepository>) {

companion object {
private val log: Logger = LoggerFactory.getLogger(StagingRepositoryTransitioner::class.java.simpleName)
}

fun effectivelyClose(repoId: String) {
effectivelyChangeState(repoId, StagingRepository.State.CLOSED, nexusClient::closeStagingRepository)
}

//TODO: Add support for autoDrop=false
fun effectivelyRelease(repoId: String) {
effectivelyChangeState(repoId, StagingRepository.State.NOT_FOUND, nexusClient::releaseStagingRepository)
}

private fun effectivelyChangeState(repoId: String, desiredState: StagingRepository.State, transitionClientRequest: (String) -> Unit) {
transitionClientRequest.invoke(repoId)
val readStagingRepository = createRetrier().execute { getStagingRepositoryStateById(repoId) }
val readStagingRepository = retrier.execute { getStagingRepositoryStateById(repoId) }
assertRepositoryNotTransitioning(readStagingRepository)
assertRepositoryInDesiredState(readStagingRepository, desiredState)
}

@Suppress("UNCHECKED_CAST")
private fun createRetrier(): Retrier {
return Retrier.Builder()
.withWaitStrategy(Retrier.Strategies.waitConstantly(retryingConfig.delayBetween.get().toMillis()))
.withStopStrategy(stopAfter(retryingConfig.maxNumber.get()))
.withResultRetryStrategy({ repo: StagingRepository -> repo.transitioning } as ((Any) -> Boolean))
.build()
}

private fun getStagingRepositoryStateById(repoId: String): StagingRepository {
val readStagingRepository: StagingRepository = nexusClient.getStagingRepositoryStateById(repoId)
println("Read staging repository: state: ${readStagingRepository.state}, transitioning: ${readStagingRepository.transitioning}")
log.info("Read staging repository: state: ${readStagingRepository.state}, transitioning: ${readStagingRepository.transitioning}")
return readStagingRepository
}

private fun assertRepositoryNotTransitioning(repository: StagingRepository) {
if (repository.transitioning) {
//TODO: Custom exception type
throw GradleException("Staging repository is still transitioning after defined time. Consider its increament. $repository")
throw RepositoryTransitionException("Staging repository is still transitioning after defined time. Consider its increment. $repository")
}
}

private fun assertRepositoryInDesiredState(repository: StagingRepository, desiredState: StagingRepository.State) {
if (repository.state != desiredState) {
//TODO: Custom exception type
throw GradleException("Staging repository is not in desired state ($desiredState): $repository. It is unexpected. Please report it " +
throw RepositoryTransitionException("Staging repository is not in desired state ($desiredState): $repository. It is unexpected. Please report it " +
"to https://github.com/gradle-nexus/publish-plugin/issues/ with '--info' logs")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.gradlenexus.publishplugin.internal

import com.nhaarman.mockitokotlin2.anyOrNull
import io.github.gradlenexus.publishplugin.RepositoryTransitionException
import io.github.gradlenexus.publishplugin.StagingRepository
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.BDDMockito.given
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.invocation.InvocationOnMock
import org.mockito.junit.jupiter.MockitoExtension

@ExtendWith(MockitoExtension::class)
internal class StagingRepositoryTransitionerTest {

companion object {
private const val TEST_STAGING_REPO_ID = "orgexample-42"
}

@Mock
private lateinit var nexusClient: NexusClient
@Mock
private lateinit var retrier: ActionRetrier<StagingRepository>

private lateinit var transitioner: StagingRepositoryTransitioner

@BeforeEach
internal fun setUp() {
transitioner = StagingRepositoryTransitioner(nexusClient, retrier)
}

@Test
internal fun `request repository close and get its state after execution by retrier`() {
given(nexusClient.getStagingRepositoryStateById(TEST_STAGING_REPO_ID))
.willReturn(StagingRepository(TEST_STAGING_REPO_ID, StagingRepository.State.CLOSED, false))
given(retrier.execute(anyOrNull())).willAnswer(executeFunctionPassedAsFirstArgument())

transitioner.effectivelyClose(TEST_STAGING_REPO_ID)

val inOrder = inOrder(nexusClient, retrier)
inOrder.verify(nexusClient).closeStagingRepository(TEST_STAGING_REPO_ID)
inOrder.verify(nexusClient).getStagingRepositoryStateById(TEST_STAGING_REPO_ID)
}

@Test
internal fun `request release repository and get its state after execution by retrier`() {
given(nexusClient.getStagingRepositoryStateById(TEST_STAGING_REPO_ID))
.willReturn(StagingRepository(TEST_STAGING_REPO_ID, StagingRepository.State.NOT_FOUND, false))
given(retrier.execute(anyOrNull())).willAnswer(executeFunctionPassedAsFirstArgument())

transitioner.effectivelyRelease(TEST_STAGING_REPO_ID)

val inOrder = inOrder(nexusClient, retrier)
inOrder.verify(nexusClient).releaseStagingRepository(TEST_STAGING_REPO_ID)
inOrder.verify(nexusClient).getStagingRepositoryStateById(TEST_STAGING_REPO_ID)
}

@Test
internal fun `throw meaningful exception on repository still in transition on close`() {
given(nexusClient.getStagingRepositoryStateById(TEST_STAGING_REPO_ID))
.willReturn(StagingRepository(TEST_STAGING_REPO_ID, StagingRepository.State.CLOSED, true))
given(retrier.execute(anyOrNull())).willAnswer(executeFunctionPassedAsFirstArgument())

assertThatExceptionOfType(RepositoryTransitionException::class.java)
.isThrownBy { transitioner.effectivelyClose(TEST_STAGING_REPO_ID) }
.withMessageContainingAll(TEST_STAGING_REPO_ID, "transitioning=true")
}

@Test
internal fun `throw meaningful exception on repository still in wrong state on close`() {
given(nexusClient.getStagingRepositoryStateById(TEST_STAGING_REPO_ID))
.willReturn(StagingRepository(TEST_STAGING_REPO_ID, StagingRepository.State.OPEN, false))
given(retrier.execute(anyOrNull())).willAnswer(executeFunctionPassedAsFirstArgument())

assertThatExceptionOfType(RepositoryTransitionException::class.java)
.isThrownBy { transitioner.effectivelyClose(TEST_STAGING_REPO_ID) }
.withMessageContainingAll(TEST_STAGING_REPO_ID, StagingRepository.State.OPEN.toString(), StagingRepository.State.CLOSED.toString())
}

private fun executeFunctionPassedAsFirstArgument(): (InvocationOnMock) -> StagingRepository {
return { invocation: InvocationOnMock ->
val passedFunction: () -> StagingRepository = invocation.getArgument(0)
passedFunction.invoke()
}
}
}

0 comments on commit f667d46

Please sign in to comment.