Skip to content

Commit

Permalink
Add retry logic for upload call (#26)
Browse files Browse the repository at this point in the history
Description
===========

It happens quite often that the binary upload call fails with an error
in the 500 range. I have no real idea what the root cause is since it's
a server error. Maybe the inital `createUploadResource` resource call is
to blame. This patch adds a simple retry logic around the upload task to
check if our errors go down when we try to redo the call a few times
with a timeout in between.

The values for retryCount and retryTimeout can be passed as usual via
properties, env or directly in script.

| property              | gradle property name              | environment variable                |
| --------------------- | --------------------------------- | ----------------------------------- |
| retryCount            | `appCenter.retryCount`            | `APP_CENTER_RETRY_COUNT`            |
| retryTimeout          | `appCenter.retryTimeout`          | `APP_CENTER_RETRY_TIMEOUT`          |

The default values are `3` for the `retryCount` and `5` seconds for the
`retryTimeout`.

To test the logic I had to split the actual call into a helper method.
It would be cleaner to also move the other methods over but I treat this
change as a temp fix than a proper implemenation. Maybe I roll this back
...

Changes
=======

* ![ADD] retry logic for upload call
  • Loading branch information
Larusso authored Dec 4, 2020
1 parent 4418842 commit 0f9cfbe
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec {
}

String envNameFromProperty(String property) {
"APP_CENTER_${property.replaceAll(/([A-Z])/, "_\$1").toUpperCase()}"
"APP_CENTER_${property.replaceAll(/([A-Z.])/, "_\$1").toUpperCase()}"
}

@Unroll()
Expand Down Expand Up @@ -129,6 +129,15 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec {
"publishEnabled" | false | _ | false | PropertyLocation.script
"publishEnabled" | true | _ | null | PropertyLocation.none

"retryCount" | 1 | _ | 1 | PropertyLocation.property
"retryCount" | 2 | _ | 2 | PropertyLocation.env
"retryCount" | 4 | _ | 4 | PropertyLocation.script
"retryCount" | 3 | _ | null | PropertyLocation.none

"retryTimeout" | 1000 | _ | 1000 | PropertyLocation.property
"retryTimeout" | 2000 | _ | 2000 | PropertyLocation.env
"retryTimeout" | 4000 | _ | 4000 | PropertyLocation.script
"retryTimeout" | 5000 | _ | null | PropertyLocation.none
testValue = (expectedValue == _) ? value : expectedValue
reason = location.reason() + ((location == PropertyLocation.none) ? "" : " with '$providedValue'")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ class IntegrationSpec extends nebula.test.IntegrationSpec {
case "List":
value = "[${rawValue.collect { '"' + it + '"' }.join(", ")}]"
break
case "Long":
value = "${rawValue}L"
break
default:
value = rawValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,16 @@ class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec {
"binary" | "setBinary" | "#projectDir#/some/binary/5" | "String"
"binary" | "setBinary" | "#projectDir#/some/binary/6" | "File"
"binary" | "setBinary" | "#projectDir#/some/binary/7" | "Provider<RegularFile>"

"retryCount" | "retryCount" | 1 | "Integer"
"retryCount" | "retryCount.set" | 2 | "Integer"
"retryCount" | "retryCount.set" | 3 | "Provider<Integer>"
"retryCount" | "setRetryCount" | 4 | "Integer"

"retryTimeout" | "retryTimeout" | 1000L | "Long"
"retryTimeout" | "retryTimeout.set" | 2000L | "Long"
"retryTimeout" | "retryTimeout.set" | 3000L | "Provider<Long>"
"retryTimeout" | "setRetryTimeout" | 4000L | "Long"
value = wrapValueBasedOnType(rawValue, type)
}

Expand All @@ -367,7 +377,7 @@ class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec {
and: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
publishAppCenter.binary = "$testFile"
""".stripIndent()

and: "a future version meta file"
Expand Down
8 changes: 8 additions & 0 deletions src/main/groovy/wooga/gradle/appcenter/AppCenterConsts.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,12 @@ class AppCenterConsts {
static Boolean defaultPublishEnabled = true
static String PUBLISH_ENABLED_OPTION = "appCenter.publishEnabled"
static String PUBLISH_ENABLED_ENV_VAR = "APP_CENTER_PUBLISH_ENABLED"

static Long defaultRetryTimeout = 1000 * 5
static String RETRY_TIMEOUT_OPTION = "appCenter.retryTimeout"
static String RETRY_TIMEOUT_ENV_VAR = "APP_CENTER_RETRY_TIMEOUT"

static Integer defaultRetryCount = 3
static String RETRY_COUNT_OPTION = "appCenter.retryCount"
static String RETRY_COUNT_ENV_VAR = "APP_CENTER_RETRY_COUNT"
}
25 changes: 21 additions & 4 deletions src/main/groovy/wooga/gradle/appcenter/AppCenterPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.plugins.PublishingPlugin
import wooga.gradle.appcenter.internal.DefaultAppCenterPluginExtension

import wooga.gradle.appcenter.tasks.AppCenterUploadTask

class AppCenterPlugin implements Plugin<Project> {
Expand Down Expand Up @@ -49,13 +50,14 @@ class AppCenterPlugin implements Plugin<Project> {
t.applicationIdentifier.set(extension.applicationIdentifier)
t.apiToken.set(extension.apiToken)
t.owner.set(extension.owner)
}
})
t.retryCount.set(extension.retryCount)
t.retryTimeout.set(extension.retryTimeout)
}})

project.afterEvaluate(new Action<Project>() {
@Override
void execute(Project _) {
if(extension.isPublishEnabled().get()) {
if (extension.isPublishEnabled().get()) {
def lifecyclePublishTask = tasks.getByName(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME)
lifecyclePublishTask.dependsOn(publishAppCenter)
}
Expand All @@ -71,7 +73,7 @@ class AppCenterPlugin implements Plugin<Project> {
?: System.getenv()[AppCenterConsts.DEFAULT_DESTINATIONS_ENV_VAR]) as String

if (rawValue) {
return rawValue.split(',').collect {["name": it.trim()]}
return rawValue.split(',').collect { ["name": it.trim()] }
}

AppCenterConsts.defaultDestinations
Expand Down Expand Up @@ -101,6 +103,21 @@ class AppCenterPlugin implements Plugin<Project> {
}
AppCenterConsts.defaultPublishEnabled
}))

extension.retryTimeout.set(project.provider({
String rawRetryTimout = (project.properties[AppCenterConsts.RETRY_TIMEOUT_OPTION]
?: System.getenv()[AppCenterConsts.RETRY_TIMEOUT_ENV_VAR]) as String

(rawRetryTimout) ? Long.parseLong(rawRetryTimout) : AppCenterConsts.defaultRetryTimeout
}))

extension.retryCount.set(project.provider({
String rawRetryCount = (project.properties[AppCenterConsts.RETRY_COUNT_OPTION]
?: System.getenv()[AppCenterConsts.RETRY_COUNT_ENV_VAR]) as String

(rawRetryCount) ? Integer.parseInt(rawRetryCount) : AppCenterConsts.defaultRetryCount
}))

extension
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package wooga.gradle.appcenter


import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

Expand Down Expand Up @@ -45,4 +46,13 @@ interface AppCenterPluginExtension {
Property<Boolean> getPublishEnabled()
Property<Boolean> isPublishEnabled()
void setPublishEnabled(final boolean enabled)

Property<Long> getRetryTimeout()
void setRetryTimeout(Long value)
void retryTimeout(Long value)

Property<Integer> getRetryCount()
void setRetryCount(Integer value)
void retryCount(Integer value)

}
44 changes: 44 additions & 0 deletions src/main/groovy/wooga/gradle/appcenter/api/AppCenterRest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package wooga.gradle.appcenter.api

import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.mime.MultipartEntityBuilder
import org.apache.http.entity.mime.content.FileBody
import org.gradle.api.GradleException

import java.util.logging.Logger

class AppCenterRest {
static Logger logger = Logger.getLogger(AppCenterRest.name)

static Boolean uploadResources(HttpClient client, String apiToken, String uploadUrl, File binary, Integer retryCount = 0, Long retryTimeout = 0) {
HttpPost post = new HttpPost(uploadUrl)
FileBody ipa = new FileBody(binary)
post.setHeader("X-API-Token", apiToken)
HttpEntity content = MultipartEntityBuilder.create()
.addPart("ipa", ipa)
.build()

post.setEntity(content)
HttpResponse response = client.execute(post)

if (response.statusLine.statusCode >= 500) {
logger.warning("unable to upload to provided upload url ${uploadUrl}".toString())
logger.warning(response.statusLine.reasonPhrase)

if (retryCount > 0) {
logger.warning("wait and retry after ${(retryTimeout) / 1000} s".toString())
sleep(retryTimeout)
return uploadResources(client, apiToken, uploadUrl, binary, retryCount - 1, retryTimeout)
}
}

if (response.statusLine.statusCode != 204) {
throw new GradleException("unable to upload to provided upload url ${uploadUrl}" + response.statusLine.toString())
}

true
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package wooga.gradle.appcenter.internal


import org.gradle.api.Project
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
Expand All @@ -11,13 +12,18 @@ class DefaultAppCenterPluginExtension implements AppCenterPluginExtension {
final Property<String> applicationIdentifier
final ListProperty<Map<String, String>> defaultDestinations
final Property<Boolean> publishEnabled
final Property<Long> retryTimeout
final Property<Integer> retryCount

DefaultAppCenterPluginExtension(Project project) {
apiToken = project.objects.property(String)
owner = project.objects.property(String)
applicationIdentifier = project.objects.property(String)
defaultDestinations = project.objects.listProperty(Map)
publishEnabled = project.objects.property(Boolean)
retryTimeout = project.objects.property(Long)
retryCount = project.objects.property(Integer)

}

@Override
Expand Down Expand Up @@ -89,4 +95,24 @@ class DefaultAppCenterPluginExtension implements AppCenterPluginExtension {
void setPublishEnabled(boolean enabled) {
this.publishEnabled.set(enabled)
}

@Override
void setRetryTimeout(Long value) {
this.retryTimeout.set(value)
}

@Override
void retryTimeout(Long value) {
setRetryTimeout(value)
}

@Override
void setRetryCount(Integer value) {
retryCount.set(value)
}

@Override
void retryCount(Integer value) {
setRetryTimeout(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*

import wooga.gradle.appcenter.api.AppCenterBuildInfo
import wooga.gradle.appcenter.api.AppCenterRest
import wooga.gradle.compat.ProjectLayout

import static org.gradle.util.ConfigureUtil.configureUsing
Expand Down Expand Up @@ -102,23 +104,23 @@ class AppCenterUploadTask extends DefaultTask {
final ListProperty<Map<String, String>> destinations

void setDestinations(Iterable<String> value) {
destinations.set(value.collect {["name": it]})
destinations.set(value.collect { ["name": it] })
}

void setDestinations(String... value) {
destinations.set(value.collect {["name": it]})
destinations.set(value.collect { ["name": it] })
}

void destination(String name) {
destinations.add(["name": name])
}

void destination(Iterable<String> destinations) {
this.destinations.addAll(destinations.collect {["name": it]})
this.destinations.addAll(destinations.collect { ["name": it] })
}

void destination(String... destinations) {
this.destinations.addAll(destinations.collect {["name": it]})
this.destinations.addAll(destinations.collect { ["name": it] })
}

void destinationId(String id) {
Expand Down Expand Up @@ -166,6 +168,27 @@ class AppCenterUploadTask extends DefaultTask {
@OutputFile
final Provider<RegularFile> uploadVersionMetaData

@Internal
final Property<Long> retryTimeout

void setRetryTimeout(Long value) {
this.retryTimeout.set(value)
}

void retryTimeout(Long value) {
setRetryTimeout(value)
}

void setRetryCount(Integer value) {
retryCount.set(value)
}

void retryCount(Integer value) {
setRetryCount(value)
}

final Property<Integer> retryCount

AppCenterUploadTask() {
def projectLayout = new ProjectLayout(project)
apiToken = project.objects.property(String)
Expand All @@ -175,7 +198,8 @@ class AppCenterUploadTask extends DefaultTask {
applicationIdentifier = project.objects.property(String)
releaseNotes = project.objects.property(String)
destinations = project.objects.listProperty(Map)

retryTimeout = project.objects.property(Long)
retryCount = project.objects.property(Integer)
binary = projectLayout.fileProperty()
outputDir = projectLayout.directoryProperty()
outputDir.set(temporaryDir)
Expand Down Expand Up @@ -207,22 +231,6 @@ class AppCenterUploadTask extends DefaultTask {
jsonSlurper.parseText(response.entity.content.text) as Map
}

private static uploadResources(HttpClient client, String apiToken, String uploadUrl, File binary) {
HttpPost post = new HttpPost(uploadUrl)
FileBody ipa = new FileBody(binary)
post.setHeader("X-API-Token", apiToken)
HttpEntity content = MultipartEntityBuilder.create()
.addPart("ipa", ipa)
.build()

post.setEntity(content)
HttpResponse response = client.execute(post)

if (response.statusLine.statusCode != 204) {
throw new GradleException("unable to upload to provided upload url" + response.statusLine.toString())
}
}

private static Map commitResource(HttpClient client, String owner, String applicationIdentifier, String apiToken, String uploadId) {
// curl -X PATCH --header 'Content-Type: application/json'
// --header 'Accept: application/json'
Expand Down Expand Up @@ -304,7 +312,7 @@ class AppCenterUploadTask extends DefaultTask {
String uploadUrl = uploadResource["upload_url"]
String uploadId = uploadResource["upload_id"]

uploadResources(client, apiToken, uploadUrl, binary)
AppCenterRest.uploadResources(client, apiToken, uploadUrl, binary, retryCount.getOrElse(0), retryTimeout.getOrElse(0))

def resource = commitResource(client, owner, applicationIdentifier, apiToken, uploadId)
String finalReleaseId = resource["release_id"].toString()
Expand Down
Loading

0 comments on commit 0f9cfbe

Please sign in to comment.