Skip to content

Commit 13c5105

Browse files
Add client-side async API for replication (IAsyncReplicator) (#1182)
Co-authored-by: Paul Irwin <paulirwin@gmail.com>
1 parent 034c009 commit 13c5105

12 files changed

Lines changed: 903 additions & 87 deletions

Directory.Build.targets

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<PropertyGroup Condition=" $(TargetFramework.StartsWith('net8.')) Or $(TargetFramework.StartsWith('net9.')) Or $(TargetFramework.StartsWith('net10.')) ">
5353

5454
<DefineConstants>$(DefineConstants);FEATURE_ASPNETCORE_TESTHOST</DefineConstants>
55+
<DefineConstants>$(DefineConstants);FEATURE_CANCELLATIONTOKENSOURCE_CANCELASYNC</DefineConstants>
56+
<DefineConstants>$(DefineConstants);FEATURE_HTTPCONTENT_READASSTREAM</DefineConstants>
57+
<DefineConstants>$(DefineConstants);FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN</DefineConstants>
5558

5659
</PropertyGroup>
5760

src/Lucene.Net.Replicator/Http/HttpClientBase.cs

Lines changed: 216 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Lucene.Net.Diagnostics;
21
using Lucene.Net.Support;
32
using System;
43
using System.IO;
@@ -7,6 +6,9 @@
76
using System.Net.Http;
87
using System.Threading;
98
using System.Threading.Tasks;
9+
// ReSharper disable VirtualMemberNeverOverridden.Global
10+
// ReSharper disable MemberCanBePrivate.Global
11+
1012
#nullable enable
1113

1214
namespace Lucene.Net.Replicator.Http
@@ -173,20 +175,38 @@ protected virtual void ThrowKnownError(HttpResponseMessage response)
173175
}
174176

175177
/// <summary>
176-
/// <b>Internal:</b> Execute a request and return its result.
178+
/// <b>Internal:</b> Execute a POST request with custom HttpContent and return its result.
177179
/// The <paramref name="parameters"/> argument is treated as: name1,value1,name2,value2,...
178180
/// </summary>
179181
protected virtual HttpResponseMessage ExecutePost(string request, HttpContent content, params string[]? parameters)
180182
{
181183
EnsureOpen();
182184

183-
var req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters));
184-
185-
req.Content = content;
185+
var req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters))
186+
{
187+
Content = content
188+
};
186189

187190
return Execute(req);
188191
}
189192

193+
/// <summary>
194+
/// <b>Internal:</b> Execute a POST request asynchronously with custom HttpContent.
195+
/// The <paramref name="parameters"/> argument is treated as: name1,value1,name2,value2,...
196+
/// </summary>
197+
protected virtual async Task<HttpResponseMessage> ExecutePostAsync(string request, HttpContent content, CancellationToken cancellationToken, params string[]? parameters)
198+
{
199+
EnsureOpen();
200+
201+
var req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters))
202+
{
203+
Content = content
204+
};
205+
206+
return await ExecuteAsync(req, cancellationToken).ConfigureAwait(false);
207+
}
208+
209+
190210
/// <summary>
191211
/// <b>Internal:</b> Execute a request and return its result.
192212
/// The <paramref name="parameters"/> argument is treated as: name1,value1,name2,value2,...
@@ -200,6 +220,16 @@ protected virtual HttpResponseMessage ExecuteGet(string request, params string[]
200220
return Execute(req);
201221
}
202222

223+
/// <summary>
224+
/// Execute a GET request asynchronously with an array of parameters.
225+
/// </summary>
226+
protected virtual async Task<HttpResponseMessage> ExecuteGetAsync(string action, string[]? parameters, CancellationToken cancellationToken)
227+
{
228+
EnsureOpen();
229+
var req = new HttpRequestMessage(HttpMethod.Get, QueryString(action, parameters));
230+
return await ExecuteAsync(req, cancellationToken).ConfigureAwait(false);
231+
}
232+
203233
private HttpResponseMessage Execute(HttpRequestMessage request)
204234
{
205235
//.NET Note: Bridging from Async to Sync, this is not ideal and we could consider changing the interface to be Async or provide Async overloads
@@ -209,6 +239,16 @@ private HttpResponseMessage Execute(HttpRequestMessage request)
209239
return response;
210240
}
211241

242+
// LUCENENET specific - async counterpart to Execute used by ExecuteGetAsync/ExecutePostAsync.
243+
// Uses ResponseHeadersRead so callers can stream large response bodies (e.g. replication files)
244+
// rather than buffering them into memory, and verifies status (matching sync Execute).
245+
private async Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage request, CancellationToken cancellationToken)
246+
{
247+
var response = await httpc.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
248+
VerifyStatus(response);
249+
return response;
250+
}
251+
212252
private string QueryString(string request, params string[]? parameters)
213253
{
214254
return parameters is null || parameters.Length == 0
@@ -255,7 +295,11 @@ public virtual Stream GetResponseStream(HttpResponseMessage response) // LUCENEN
255295
/// <exception cref="IOException"></exception>
256296
public virtual Stream GetResponseStream(HttpResponseMessage response, bool consume) // LUCENENET: This was ResponseInputStream in Lucene
257297
{
298+
#if FEATURE_HTTPCONTENT_READASSTREAM
299+
Stream result = response.Content.ReadAsStream();
300+
#else
258301
Stream result = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult();
302+
#endif
259303

260304
if (consume)
261305
{
@@ -265,6 +309,70 @@ public virtual Stream GetResponseStream(HttpResponseMessage response, bool consu
265309
return result;
266310
}
267311

312+
/// <summary>
313+
/// Internal utility: input stream of the provided response.
314+
/// The returned stream takes ownership of the response and will dispose it
315+
/// when the stream is disposed.
316+
/// </summary>
317+
/// <exception cref="IOException"></exception>
318+
protected virtual Stream GetResponseStreamWithOwnership(HttpResponseMessage response)
319+
{
320+
#if FEATURE_HTTPCONTENT_READASSTREAM
321+
Stream result = response.Content.ReadAsStream();
322+
#else
323+
Stream result = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult();
324+
#endif
325+
return new ResponseOwningStream(result, response);
326+
}
327+
328+
/// <summary>
329+
/// Internal utility: input stream of the provided response asynchronously.
330+
/// </summary>
331+
/// <exception cref="IOException"></exception>
332+
public virtual async Task<Stream> GetResponseStreamAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
333+
{
334+
#if FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN
335+
Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
336+
#else
337+
Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
338+
#endif
339+
return result;
340+
}
341+
342+
/// <summary>
343+
/// Internal utility: input stream of the provided response asynchronously, which optionally
344+
/// consumes the response's resources when the input stream is exhausted.
345+
/// </summary>
346+
/// <exception cref="IOException"></exception>
347+
// ReSharper disable once UnusedMember.Global - public API
348+
public virtual async Task<Stream> GetResponseStreamAsync(HttpResponseMessage response, bool consume, CancellationToken cancellationToken = default)
349+
{
350+
#if FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN
351+
Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
352+
#else
353+
Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
354+
#endif
355+
if (consume)
356+
result = new ConsumingStream(result);
357+
return result;
358+
}
359+
360+
/// <summary>
361+
/// Internal utility: input stream of the provided response asynchronously.
362+
/// The returned stream takes ownership of the response and will dispose it
363+
/// when the stream is disposed.
364+
/// </summary>
365+
/// <exception cref="IOException"></exception>
366+
protected virtual async Task<Stream> GetResponseStreamWithOwnershipAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
367+
{
368+
#if FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN
369+
Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
370+
#else
371+
Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
372+
#endif
373+
return new ResponseOwningStream(result, response);
374+
}
375+
268376
/// <summary>
269377
/// Returns <c>true</c> if this instance was <see cref="Dispose(bool)"/>ed, otherwise
270378
/// returns <c>false</c>. Note that if you override <see cref="Dispose(bool)"/>, you must call
@@ -310,11 +418,56 @@ protected virtual T DoAction<T>(HttpResponseMessage response, bool consume, Func
310418
}
311419
}
312420
}
313-
if (Debugging.AssertsEnabled) Debugging.Assert(th != null); // extra safety - if we get here, it means the Func<T> failed
421+
422+
// if (Debugging.AssertsEnabled) Debugging.Assert(th != null); // LUCENENET: Removed assertion because it'll never be null here, ensured by NRT
314423
Util.IOUtils.ReThrow(th);
315424
return default!; // silly, if we're here, IOUtils.reThrow always throws an exception
316425
}
317426

427+
/// <summary>
428+
/// Do a specific async action and validate after the action that the status is still OK,
429+
/// and if not, attempt to extract the actual server side exception. Optionally
430+
/// release the response at exit, depending on <paramref name="consume"/> parameter.
431+
/// </summary>
432+
protected virtual async Task<T> DoActionAsync<T>(HttpResponseMessage response, bool consume, Func<Task<T>> call)
433+
{
434+
Exception? th /* = null */;
435+
try
436+
{
437+
return await call().ConfigureAwait(false);
438+
}
439+
catch (Exception t) when (t.IsThrowable())
440+
{
441+
th = t;
442+
}
443+
finally
444+
{
445+
try
446+
{
447+
VerifyStatus(response);
448+
}
449+
finally
450+
{
451+
if (consume)
452+
{
453+
ConsumeQuietly(response);
454+
}
455+
}
456+
}
457+
458+
// if (Debugging.AssertsEnabled) Debugging.Assert(th != null); // LUCENENET: Removed assertion because it'll never be null here, ensured by NRT
459+
Util.IOUtils.ReThrow(th);
460+
return default!; // never reached, rethrow above always throws
461+
}
462+
463+
/// <summary>
464+
/// Calls the overload <see cref="DoActionAsync{T}(HttpResponseMessage, bool, Func{Task{T}})"/> passing <c>true</c> to consume.
465+
/// </summary>
466+
protected virtual Task<T> DoActionAsync<T>(HttpResponseMessage response, Func<Task<T>> call)
467+
{
468+
return DoActionAsync(response, true, call);
469+
}
470+
318471
/// <summary>
319472
/// Disposes this <see cref="HttpClientBase"/>.
320473
/// When called with <code>true</code>, this disposes the underlying <see cref="HttpClient"/>.
@@ -350,14 +503,70 @@ private static void ConsumeQuietly(HttpResponseMessage response)
350503
}
351504
}
352505

506+
/// <summary>
507+
/// Wraps a stream and disposes the associated <see cref="HttpResponseMessage"/>
508+
/// when the stream is disposed.
509+
/// </summary>
510+
private sealed class ResponseOwningStream : Stream
511+
{
512+
private readonly Stream input;
513+
private readonly HttpResponseMessage response;
514+
private bool disposed;
515+
516+
public ResponseOwningStream(Stream input, HttpResponseMessage response)
517+
{
518+
this.input = input ?? throw new ArgumentNullException(nameof(input));
519+
this.response = response ?? throw new ArgumentNullException(nameof(response));
520+
}
521+
522+
public override bool CanRead => input.CanRead;
523+
public override bool CanSeek => input.CanSeek;
524+
public override bool CanWrite => input.CanWrite;
525+
public override long Length => input.Length;
526+
public override long Position
527+
{
528+
get => input.Position;
529+
set => input.Position = value;
530+
}
531+
532+
public override void Flush() => input.Flush();
533+
public override int Read(byte[] buffer, int offset, int count) => input.Read(buffer, offset, count);
534+
public override long Seek(long offset, SeekOrigin origin) => input.Seek(offset, origin);
535+
public override void SetLength(long value) => input.SetLength(value);
536+
public override void Write(byte[] buffer, int offset, int count) => input.Write(buffer, offset, count);
537+
538+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
539+
=> input.ReadAsync(buffer, offset, count, cancellationToken);
540+
541+
#if FEATURE_STREAM_READ_SPAN
542+
public override int Read(Span<byte> buffer) => input.Read(buffer);
543+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
544+
=> input.ReadAsync(buffer, cancellationToken);
545+
#endif
546+
547+
protected override void Dispose(bool disposing)
548+
{
549+
if (!disposed)
550+
{
551+
if (disposing)
552+
{
553+
input.Dispose();
554+
response.Dispose();
555+
}
556+
disposed = true;
557+
}
558+
base.Dispose(disposing);
559+
}
560+
}
561+
353562
/// <summary>
354563
/// Wraps a stream and consumes (flushes) and disposes automatically
355564
/// when the last call to a Read overload occurs.
356565
/// </summary>
357566
private class ConsumingStream : Stream
358567
{
359568
private readonly Stream input;
360-
private bool consumed = false;
569+
private bool consumed /* = false */;
361570

362571
public ConsumingStream(Stream input)
363572
{

0 commit comments

Comments
 (0)