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

fix: replace infinite retries with exponential backoff strategy in file representations #835

Merged
merged 2 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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