Skip to content

Commit d6d22e9

Browse files
author
Qiming Yuan
committed
Optimize upload memory usage.
1 parent a9c9a1c commit d6d22e9

File tree

3 files changed

+144
-59
lines changed

3 files changed

+144
-59
lines changed

Dropbox.Api.Tests/DropboxApiTests.cs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ namespace Dropbox.Api.Tests
2424
[TestClass]
2525
public class DropboxApiTests
2626
{
27+
/// <summary>
28+
/// The user access token.
29+
/// </summary>
30+
public static string UserAccessToken;
31+
2732
/// <summary>
2833
/// The Dropbox client.
2934
/// </summary>
@@ -39,11 +44,12 @@ public class DropboxApiTests
3944
/// </summary>
4045
public static DropboxAppClient AppClient;
4146

47+
4248
[ClassInitialize]
4349
public static void Initialize(TestContext context)
4450
{
45-
var userToken = context.Properties["userAccessToken"].ToString();
46-
Client = new DropboxClient(userToken);
51+
UserAccessToken = context.Properties["userAccessToken"].ToString();
52+
Client = new DropboxClient(UserAccessToken);
4753

4854
var teamToken = context.Properties["teamAccessToken"].ToString();
4955
TeamClient = new DropboxTeamClient(teamToken);
@@ -123,6 +129,42 @@ public async Task TestUpload()
123129
Assert.AreEqual("abc", content);
124130
}
125131

132+
/// <summary>
133+
/// Test upload with retry.
134+
/// </summary>
135+
/// <returns>The <see cref="Task"/></returns>
136+
[TestMethod]
137+
public async Task TestUploadRetry()
138+
{
139+
var count = 0;
140+
141+
var mockHandler = new MockHttpMessageHandler((r, s) =>
142+
{
143+
if (count++ < 2)
144+
{
145+
var error = new HttpResponseMessage(HttpStatusCode.InternalServerError)
146+
{
147+
Content = new StringContent("Error")
148+
};
149+
150+
return Task.FromResult(error);
151+
}
152+
153+
return s(r);
154+
});
155+
156+
var mockClient = new HttpClient(mockHandler);
157+
var client = new DropboxClient(
158+
UserAccessToken,
159+
new DropboxClientConfig { HttpClient = mockClient, MaxRetriesOnError = 10 });
160+
161+
var response = await client.Files.UploadAsync("/Foo.txt", body: GetStream("abc"));
162+
var downloadResponse = await Client.Files.DownloadAsync("/Foo.txt");
163+
var content = await downloadResponse.GetContentAsStringAsync();
164+
Assert.AreEqual("abc", content);
165+
}
166+
167+
126168
/// <summary>
127169
/// Test upload.
128170
/// </summary>
@@ -154,7 +196,7 @@ public async Task TestRateLimit()
154196

155197
mockResponse.Headers.Add("X-Dropbox-Request-Id", "123");
156198

157-
var mockHandler = new MockHttpMessageHandler(mockResponse);
199+
var mockHandler = new MockHttpMessageHandler((r, s) => Task.FromResult(mockResponse));
158200
var mockClient = new HttpClient(mockHandler);
159201
var client = new DropboxClient("dummy", new DropboxClientConfig { HttpClient = mockClient });
160202
try
@@ -275,5 +317,44 @@ private static MemoryStream GetStream(string content)
275317
var buffer = Encoding.UTF8.GetBytes(content);
276318
return new MemoryStream(buffer);
277319
}
320+
321+
/// Test User-Agent header is set with default values.
322+
/// </summary>
323+
/// <returns>The <see cref="Task"/></returns>
324+
[TestMethod]
325+
public async Task TestUserAgentDefault()
326+
{
327+
HttpRequestMessage lastRequest = null;
328+
var mockHandler = new MockHttpMessageHandler((r, s) =>
329+
{
330+
lastRequest = r;
331+
return s(r);
332+
});
333+
334+
var mockClient = new HttpClient(mockHandler);
335+
var client = new DropboxClient(UserAccessToken, new DropboxClientConfig { HttpClient = mockClient });
336+
await client.Users.GetCurrentAccountAsync();
337+
Assert.IsTrue(lastRequest.Headers.UserAgent.ToString().Contains("OfficialDropboxDotNetSDKv2"));
338+
}
339+
340+
/// Test User-Agent header is populated with user supplied value in DropboxClientConfig.
341+
/// </summary>
342+
/// <returns>The <see cref="Task"/></returns>
343+
[TestMethod]
344+
public async Task TestUserAgentUserSupplied()
345+
{
346+
HttpRequestMessage lastRequest = null;
347+
var mockHandler = new MockHttpMessageHandler((r, s) =>
348+
{
349+
lastRequest = r;
350+
return s(r);
351+
});
352+
353+
var mockClient = new HttpClient(mockHandler);
354+
var userAgent = "UserAgentTest";
355+
var client = new DropboxClient(UserAccessToken, new DropboxClientConfig { HttpClient = mockClient, UserAgent = userAgent });
356+
await client.Users.GetCurrentAccountAsync();
357+
Assert.IsTrue(lastRequest.Headers.UserAgent.ToString().Contains(userAgent));
358+
}
278359
}
279360
}

Dropbox.Api.Tests/MockHttpMessageHandler.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,27 @@
66

77
namespace Dropbox.Api.Tests
88
{
9+
using System;
910
using System.Net.Http;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213

13-
public class MockHttpMessageHandler : HttpMessageHandler
14+
public class MockHttpMessageHandler : HttpClientHandler
1415
{
16+
public delegate Task<HttpResponseMessage> Sender(HttpRequestMessage message);
17+
1518
/// <summary>
16-
/// The fake response.
19+
/// The mock handler.
1720
/// </summary>
18-
private readonly HttpResponseMessage response;
19-
21+
Func<HttpRequestMessage, Sender, Task<HttpResponseMessage>> handler;
22+
2023
/// <summary>
2124
/// Initializes a new instance of the <see cref="MockHttpMessageHandler"/> class.
2225
/// </summary>
2326
/// <param name="response">The mock response.</param>
24-
public MockHttpMessageHandler(HttpResponseMessage response)
27+
public MockHttpMessageHandler(Func<HttpRequestMessage, Sender, Task<HttpResponseMessage>> handler)
2528
{
26-
this.response = response;
29+
this.handler = handler;
2730
}
2831

2932
/// <summary>
@@ -34,7 +37,7 @@ public MockHttpMessageHandler(HttpResponseMessage response)
3437
/// <returns>The response.</returns>
3538
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
3639
{
37-
return Task.FromResult(this.response);
40+
return this.handler(request, r => base.SendAsync(r, cancellationToken));
3841
}
3942
}
4043
}

Dropbox.Api/DropboxRequestHandler.cs

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,6 @@ private async Task<Result> RequestJsonStringWithRetry(
238238
var maxRetries = this.options.MaxClientRetries;
239239
var r = new Random();
240240

241-
byte[] cachedBody = null;
242-
long cachedStreamStart = 0;
243-
244241
if (routeStyle == RouteStyle.Upload)
245242
{
246243
if (body == null)
@@ -254,66 +251,50 @@ private async Task<Result> RequestJsonStringWithRetry(
254251
{
255252
maxRetries = 0;
256253
}
257-
else if (maxRetries == 0)
258-
{
259-
// Do not copy the stream
260-
}
261-
else if (body is MemoryStream)
262-
{
263-
cachedStreamStart = body.Position;
264-
cachedBody = ((MemoryStream)body).ToArray();
265-
}
266-
else
267-
{
268-
cachedStreamStart = body.Position;
269-
using (var mem = new MemoryStream())
270-
{
271-
await body.CopyToAsync(mem).ConfigureAwait(false);
272-
cachedBody = mem.ToArray();
273-
}
274-
}
275254
}
276255

277-
while (true)
256+
try
278257
{
279-
try
258+
while (true)
280259
{
281-
if (cachedBody == null)
260+
try
282261
{
283262
return await this.RequestJsonString(host, routeName, auth, routeStyle, requestArg, body)
284263
.ConfigureAwait(false);
285264
}
286-
else
265+
catch (RateLimitException)
287266
{
288-
using (var mem = new MemoryStream(cachedBody, writable: false))
289-
{
290-
mem.Position = cachedStreamStart;
291-
return await this.RequestJsonString(host, routeName, auth, routeStyle, requestArg, mem)
292-
.ConfigureAwait(false);
293-
}
267+
throw;
294268
}
295-
}
296-
catch (RateLimitException)
297-
{
298-
throw;
299-
}
300-
catch (RetryException)
301-
{
302-
// dropbox maps 503 - ServiceUnavailable to be a rate limiting error.
303-
// do not count a rate limiting error as an attempt
304-
if (++attempt > maxRetries)
269+
catch (RetryException)
305270
{
306-
throw;
271+
// dropbox maps 503 - ServiceUnavailable to be a rate limiting error.
272+
// do not count a rate limiting error as an attempt
273+
if (++attempt > maxRetries)
274+
{
275+
throw;
276+
}
307277
}
308-
}
309278

310-
// use exponential backoff
311-
var backoff = TimeSpan.FromSeconds(Math.Pow(2, attempt) * r.NextDouble());
279+
// use exponential backoff
280+
var backoff = TimeSpan.FromSeconds(Math.Pow(2, attempt) * r.NextDouble());
312281
#if PORTABLE40
313-
await TaskEx.Delay(backoff);
282+
await TaskEx.Delay(backoff);
314283
#else
315-
await Task.Delay(backoff);
284+
await Task.Delay(backoff);
316285
#endif
286+
if (body != null)
287+
{
288+
body.Position = 0;
289+
}
290+
}
291+
}
292+
finally
293+
{
294+
if (body != null)
295+
{
296+
body.Dispose();
297+
}
317298
}
318299
}
319300

@@ -412,7 +393,7 @@ private async Task<Result> RequestJsonString(
412393
throw new ArgumentNullException("body");
413394
}
414395

415-
request.Content = new StreamContent(body);
396+
request.Content = new CustomStreamContent(body);
416397
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
417398
break;
418399
default:
@@ -641,6 +622,26 @@ public void Dispose()
641622
}
642623
}
643624

625+
/// <summary>
626+
/// The stream content which doesn't dispose the underlying stream. This
627+
/// is useful for retry.
628+
/// </summary>
629+
internal class CustomStreamContent : StreamContent
630+
{
631+
/// <summary>
632+
/// Initializes a new instance of the <see cref="CustomStreamContent"/> class.
633+
/// </summary>
634+
/// <param name="content">The stream content.</param>
635+
public CustomStreamContent(Stream content) : base(content)
636+
{
637+
}
638+
639+
protected override void Dispose(bool disposing)
640+
{
641+
// Do not dispose the stream.
642+
}
643+
}
644+
644645
/// <summary>
645646
/// The type of api hosts.
646647
/// </summary>
@@ -752,7 +753,7 @@ public DropboxRequestHandlerOptions(
752753

753754
this.UserAgent = userAgent == null
754755
? string.Join("/", BaseUserAgent, sdkVersion)
755-
: string.Join("/", this.UserAgent, BaseUserAgent, sdkVersion);
756+
: string.Join("/", userAgent, BaseUserAgent, sdkVersion);
756757

757758
this.HttpClient = httpClient ?? DefaultHttpClient;
758759
this.OAuth2AccessToken = oauth2AccessToken;

0 commit comments

Comments
 (0)