Skip to content

Commit

Permalink
fix: replace infinite retries with exponential backoff strategy in fi…
Browse files Browse the repository at this point in the history
…le representations (#835)
  • Loading branch information
mwwoda authored Jul 20, 2022
1 parent 841452c commit f2a5713
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 7 deletions.
6 changes: 6 additions & 0 deletions Box.V2.Test/Box.V2.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
<None Update="Fixtures\BoxFileRequest\GetFileRequest200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures\BoxFiles\PollRepresentationPending200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures\BoxFiles\GetRepresentationContentPending200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures\BoxFiles\CreateFileSharedLink200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
106 changes: 105 additions & 1 deletion Box.V2.Test/BoxFilesManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Box.V2.Exceptions;
Expand Down Expand Up @@ -1201,5 +1200,110 @@ public async Task DownloadZip_ValidResponse()
Assert.AreNotEqual(fs.Length, 0);
}
}


[TestMethod]
[ExpectedException(typeof(BoxCodingException))]
public async Task GetRepresentationContentAsync_ShouldThrowException_IfTooManyRetriesAndHandleRetryTrue()
{
Handler.SetupSequence(h => h.ExecuteAsync<BoxFile>(It.IsAny<IBoxRequest>()))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
StatusCode = System.Net.HttpStatusCode.Accepted,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}));

var repRequest = new BoxRepresentationRequest
{
FileId = "11111",
XRepHints = $"[jpg?dimensions=320x320]",
HandleRetry = true
};

var result = await _filesManager.GetRepresentationContentAsync(repRequest);
}

[TestMethod]
[ExpectedException(typeof(BoxCodingException))]
public async Task GetRepresentationContentAsync_ShouldThrowException_IfTooManyRetries()
{
Handler.Setup(h => h.ExecuteAsync<BoxFile>(It.IsAny<IBoxRequest>()))
.Returns(Task.FromResult<IBoxResponse<BoxFile>>(new BoxResponse<BoxFile>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/GetRepresentationContentPending200.json")
}));

Handler.SetupSequence(h => h.ExecuteAsync<BoxRepresentation>(It.IsAny<IBoxRequest>()))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}))
.Returns(Task.FromResult<IBoxResponse<BoxRepresentation>>(new BoxResponse<BoxRepresentation>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxFiles/PollRepresentationPending200.json")
}));

var repRequest = new BoxRepresentationRequest
{
FileId = "11111",
XRepHints = $"[jpg?dimensions=320x320]",
};

var result = await _filesManager.GetRepresentationContentAsync(repRequest);
}
}
}
2 changes: 2 additions & 0 deletions Box.V2.Test/BoxResourceManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Box.V2.Converter;
using Box.V2.Request;
using Box.V2.Services;
using Box.V2.Test.Helpers;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -49,6 +50,7 @@ protected BoxResourceManagerTest()
Config.SetupGet(x => x.SignRequestsEndpointUri).Returns(SignRequestUri);
Config.SetupGet(x => x.SignRequestsEndpointWithPathUri).Returns(SignRequestWithPathUri);
Config.SetupGet(x => x.FileRequestsEndpointWithPathUri).Returns(FileRequestsWithPathUri);
Config.SetupGet(x => x.RetryStrategy).Returns(new InstantRetryStrategy());

AuthRepository = new AuthRepository(Config.Object, Service, Converter, new OAuthSession("fakeAccessToken", "fakeRefreshToken", 3600, "bearer"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"type": "file",
"id": "11111",
"etag": "0",
"representations": {
"entries": [
{
"representation": "jpg",
"properties": {
"dimensions": "320x320",
"paged": "false",
"thumb": "false"
},
"info": {
"url": "https://api.box.com/2.0/internal_files/11111/versions/22222/representations/jpg_320x320"
},
"status": {
"state": "pending"
},
"content": {
"url_template": "https://dl.boxcloud.com/api/2.0/internal_files/11111/versions/22222/representations/jpg_320x320/content/{+asset_path}"
}
}
]
}
}

17 changes: 17 additions & 0 deletions Box.V2.Test/Fixtures/BoxFiles/PollRepresentationPending200.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"representation": "jpg",
"properties": {
"dimensions": "320x320",
"paged": "false",
"thumb": "false"
},
"info": {
"url": "https://api.box.com/2.0/internal_files/11111/versions/22222/representations/jpg_320x320"
},
"status": {
"state": "pending"
},
"content": {
"url_template": "https://dl.boxcloud.com/api/2.0/internal_files/11111/versions/22222/representations/jpg_320x320/content/{+asset_path}"
}
}
6 changes: 6 additions & 0 deletions Box.V2/Config/BoxConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public BoxConfig(BoxConfigBuilder builder)
AcceptEncoding = builder.AcceptEncoding;
WebProxy = builder.WebProxy;
Timeout = builder.Timeout;
RetryStrategy = builder.RetryStrategy;
}

/// <summary>
Expand Down Expand Up @@ -292,6 +293,11 @@ public Uri BoxAuthTokenApiUri
/// Timeout for the connection
/// </summary>
public TimeSpan? Timeout { get; private set; }

/// <summary>
/// Retry strategy for failed requests
/// </summary>
public IRetryStrategy RetryStrategy { get; private set; } = new ExponentialBackoff();
}

public enum CompressionType
Expand Down
17 changes: 17 additions & 0 deletions Box.V2/Config/BoxConfigBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net;
using Box.V2.Utility;

namespace Box.V2.Config
{
Expand Down Expand Up @@ -251,6 +252,17 @@ public BoxConfigBuilder SetEnterpriseId(string enterpriseId)
return this;
}

/// <summary>
/// Sets retry strategy.
/// </summary>
/// <param name="enterpriseId">Retry strategy.</param>
/// <returns>this BoxConfigBuilder object for chaining</returns>
public BoxConfigBuilder SetRetryStrategy(IRetryStrategy retryStrategy)
{
RetryStrategy = retryStrategy;
return this;
}

public string ClientId { get; private set; }
public string ClientSecret { get; private set; }
public string EnterpriseId { get; private set; }
Expand Down Expand Up @@ -298,6 +310,11 @@ public Uri BoxAuthTokenApiUri
/// </summary>
public TimeSpan? Timeout { get; private set; }

/// <summary>
/// Retry strategy for failed requests
/// </summary>
public IRetryStrategy RetryStrategy { get; private set; } = new ExponentialBackoff();

private Uri EnsureEndsWithSlash(Uri uri)
{
return uri.ToString().EndsWith("/") ? uri : new Uri($"{uri}{"/"}");
Expand Down
5 changes: 5 additions & 0 deletions Box.V2/Config/IBoxConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using Box.V2.Utility;

namespace Box.V2.Config
{
Expand Down Expand Up @@ -143,5 +144,9 @@ public interface IBoxConfig
/// Timeout for the connection
/// </summary>
TimeSpan? Timeout { get; }
/// <summary>
/// Retry strategy for failed requests
/// </summary>
IRetryStrategy RetryStrategy { get; }
}
}
31 changes: 25 additions & 6 deletions Box.V2/Managers/BoxFilesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Box.V2.Extensions;
using Box.V2.Models;
using Box.V2.Models.Request;
using Box.V2.Request;
using Box.V2.Services;
using Box.V2.Utility;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -1300,11 +1301,14 @@ public async Task<BoxRepresentationCollection<BoxRepresentation>> GetRepresentat

IBoxResponse<BoxFile> response = await ToResponseAsync<BoxFile>(request).ConfigureAwait(false);

var retryCounter = 1;

while (response.StatusCode == HttpStatusCode.Accepted && representationRequest.HandleRetry)
{
const int RepresentationRequestRetryTime = 3000;
await Task.Delay(RepresentationRequestRetryTime);
response = await ToResponseAsync<BoxFile>(request).ConfigureAwait(false);
response = await CallWithRetryCheck(() => ToResponseAsync<BoxFile>(request),
$"Could not get valid File Representation status after {retryCounter} retries.",
retryCounter++)
.ConfigureAwait(false);
}

return response.ResponseObject.Representations;
Expand Down Expand Up @@ -1396,7 +1400,7 @@ private async Task<BoxZip> CreateZip(BoxZipRequest zipRequest)
return response.ResponseObject;
}

private async Task<string> PollRepresentationInfo(string infoUrl)
private async Task<string> PollRepresentationInfo(string infoUrl, int retryCounter = 1)
{
var infoRequest = new BoxRequest(new Uri(infoUrl));
IBoxResponse<BoxRepresentation> infoResponse = await ToResponseAsync<BoxRepresentation>(infoRequest).ConfigureAwait(false);
Expand All @@ -1410,12 +1414,27 @@ private async Task<string> PollRepresentationInfo(string infoUrl)
throw new BoxCodingException("Representation had error status");
case "none":
case "pending":
await Task.Delay(1000);
return await PollRepresentationInfo(infoUrl).ConfigureAwait(false);
return await CallWithRetryCheck(() => PollRepresentationInfo(infoUrl, ++retryCounter),
$"Could not get valid Representation status after {retryCounter} retries.",
retryCounter)
.ConfigureAwait(false);
default:
throw new BoxCodingException("Representation has unknown status");
}
}

private async Task<T> CallWithRetryCheck<T>(Func<Task<T>> action, string errorMessage, int retryCounter = 1) where T : class
{
if (retryCounter <= HttpRequestHandler.RetryLimit)
{
await Task.Delay(_config.RetryStrategy.GetRetryTimeout(retryCounter));
return await action().ConfigureAwait(false);
}
else
{
throw new BoxCodingException(errorMessage);
}
}
}

internal static class UploadUsingSessionInternal
Expand Down

0 comments on commit f2a5713

Please sign in to comment.