Skip to content

Commit e40254c

Browse files
committed
Add IAsyncReplicator interface and async support in HttpReplicator (client-side async API, related to #XXXX)
1 parent 9638258 commit e40254c

4 files changed

Lines changed: 507 additions & 1 deletion

File tree

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,29 @@ protected virtual HttpResponseMessage ExecuteGet(string request, params string[]
200200
return Execute(req);
201201
}
202202

203+
/// <summary>
204+
/// Execute a GET request asynchronously with an array of parameters.
205+
/// </summary>
206+
protected Task<HttpResponseMessage> ExecuteGetAsync(string action, string[] parameters, CancellationToken cancellationToken)
207+
{
208+
var url = BuildUrl(action, parameters);
209+
return Client.GetAsync(url, cancellationToken);
210+
}
211+
212+
/// <summary>
213+
/// Execute a GET request asynchronously with up to 3 name/value parameters.
214+
/// </summary>
215+
protected Task<HttpResponseMessage> ExecuteGetAsync(
216+
string action,
217+
string param1, string value1,
218+
string param2 = null, string value2 = null,
219+
string param3 = null, string value3 = null,
220+
CancellationToken cancellationToken = default)
221+
{
222+
var url = BuildUrl(action, param1, value1, param2, value2, param3, value3);
223+
return Client.GetAsync(url, cancellationToken);
224+
}
225+
203226
private HttpResponseMessage Execute(HttpRequestMessage request)
204227
{
205228
//.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
@@ -217,6 +240,31 @@ private string QueryString(string request, params string[] parameters)
217240
.Join("&", parameters.Select(WebUtility.UrlEncode).InPairs((key, val) => string.Format("{0}={1}", key, val))));
218241
}
219242

243+
// Add this property so subclasses can access the HttpClient instance
244+
protected HttpClient Client => httpc;
245+
246+
// BuildUrl helpers (mirror the QueryString overloads)
247+
protected virtual string BuildUrl(string action, string[] parameters)
248+
{
249+
// QueryString has signature: QueryString(string request, params string[] parameters)
250+
return QueryString(action, parameters);
251+
}
252+
253+
protected virtual string BuildUrl(
254+
string action,
255+
string param1, string value1,
256+
string param2 = null, string value2 = null,
257+
string param3 = null, string value3 = null)
258+
{
259+
// Forward to QueryString which accepts params string[]
260+
if (param2 == null && param3 == null)
261+
{
262+
return QueryString(action, param1, value1);
263+
}
264+
return QueryString(action, param1, value1, param2, value2, param3, value3);
265+
}
266+
267+
220268
/// <summary>
221269
/// Internal utility: input stream of the provided response.
222270
/// </summary>
@@ -262,6 +310,37 @@ public virtual Stream GetResponseStream(HttpResponseMessage response, bool consu
262310
return result;
263311
}
264312

313+
/// <summary>
314+
/// Internal utility: input stream of the provided response asynchronously.
315+
/// </summary>
316+
/// <exception cref="IOException"></exception>
317+
public virtual async Task<Stream> GetResponseStreamAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
318+
{
319+
#if NET8_0_OR_GREATER
320+
Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
321+
#else
322+
Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
323+
#endif
324+
return result;
325+
}
326+
327+
/// <summary>
328+
/// Internal utility: input stream of the provided response asynchronously, which optionally
329+
/// consumes the response's resources when the input stream is exhausted.
330+
/// </summary>
331+
/// <exception cref="IOException"></exception>
332+
public virtual async Task<Stream> GetResponseStreamAsync(HttpResponseMessage response, bool consume, CancellationToken cancellationToken = default)
333+
{
334+
#if NET8_0_OR_GREATER
335+
Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
336+
#else
337+
Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
338+
#endif
339+
if (consume)
340+
result = new ConsumingStream(result);
341+
return result;
342+
}
343+
265344
/// <summary>
266345
/// Returns <c>true</c> if this instance was <see cref="Dispose(bool)"/>ed, otherwise
267346
/// returns <c>false</c>. Note that if you override <see cref="Dispose(bool)"/>, you must call
@@ -319,6 +398,59 @@ protected virtual T DoAction<T>(HttpResponseMessage response, bool consume, Func
319398
return default; // silly, if we're here, IOUtils.reThrow always throws an exception
320399
}
321400

401+
/// <summary>
402+
/// Do a specific async action and validate after the action that the status is still OK,
403+
/// and if not, attempt to extract the actual server side exception. Optionally
404+
/// release the response at exit, depending on <paramref name="consume"/> parameter.
405+
/// </summary>
406+
protected virtual async Task<T> DoActionAsync<T>(HttpResponseMessage response, bool consume, Func<Task<T>> call)
407+
{
408+
Exception th = null;
409+
try
410+
{
411+
VerifyStatus(response);
412+
return await call().ConfigureAwait(false);
413+
}
414+
catch (Exception t) when (t.IsThrowable())
415+
{
416+
th = t;
417+
}
418+
finally
419+
{
420+
try
421+
{
422+
VerifyStatus(response);
423+
}
424+
finally
425+
{
426+
if (consume)
427+
{
428+
try
429+
{
430+
ConsumeQuietly(response);
431+
}
432+
catch
433+
{
434+
// ignore on purpose
435+
}
436+
}
437+
}
438+
}
439+
440+
if (Debugging.AssertsEnabled) Debugging.Assert(th != null);
441+
Util.IOUtils.ReThrow(th);
442+
return default!; // never reached, rethrow above always throws
443+
}
444+
445+
/// <summary>
446+
/// Calls the overload <see cref="DoActionAsync{T}(HttpResponseMessage, bool, Func{Task{T}})"/> passing <c>true</c> to consume.
447+
/// </summary>
448+
protected virtual Task<T> DoActionAsync<T>(HttpResponseMessage response, Func<Task<T>> call)
449+
{
450+
return DoActionAsync(response, true, call);
451+
}
452+
453+
322454
/// <summary>
323455
/// Disposes this <see cref="HttpClientBase"/>.
324456
/// When called with <code>true</code>, this disposes the underlying <see cref="HttpClient"/>.

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

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System;
33
using System.IO;
44
using System.Net.Http;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
58

69
namespace Lucene.Net.Replicator.Http
710
{
@@ -28,7 +31,7 @@ namespace Lucene.Net.Replicator.Http
2831
/// <remarks>
2932
/// @lucene.experimental
3033
/// </remarks>
31-
public class HttpReplicator : HttpClientBase, IReplicator
34+
public class HttpReplicator : HttpClientBase, IReplicator, IAsyncReplicator
3235
{
3336
/// <summary>
3437
/// Creates a new <see cref="HttpReplicator"/> with the given host, port and path.
@@ -106,5 +109,95 @@ public virtual void Release(string sessionId)
106109
// do not remove this call: as it is still validating for us!
107110
DoAction<object>(response, () => null);
108111
}
112+
113+
#region Async methods (IAsyncReplicator)
114+
115+
/// <summary>
116+
/// Checks for updates at the remote host asynchronously.
117+
/// </summary>
118+
/// <param name="currentVersion">The current index version.</param>
119+
/// <param name="cancellationToken">Cancellation token.</param>
120+
/// <returns>
121+
/// A <see cref="SessionToken"/> if updates are available; otherwise, <c>null</c>.
122+
/// </returns>
123+
public async Task<SessionToken?> CheckForUpdateAsync(string currentVersion, CancellationToken cancellationToken = default)
124+
{
125+
string[] parameters = currentVersion != null
126+
? new[] { ReplicationService.REPLICATE_VERSION_PARAM, currentVersion }
127+
: null;
128+
129+
using var response = await ExecuteGetAsync(
130+
ReplicationService.ReplicationAction.UPDATE.ToString(),
131+
parameters,
132+
cancellationToken: cancellationToken).ConfigureAwait(false);
133+
134+
return await DoActionAsync(response, async () =>
135+
{
136+
using var inputStream = new DataInputStream(
137+
await GetResponseStreamAsync(response, cancellationToken).ConfigureAwait(false));
138+
139+
return inputStream.ReadByte() == 0 ? null : new SessionToken(inputStream);
140+
}).ConfigureAwait(false);
141+
}
142+
143+
/// <summary>
144+
/// Obtains the given file from the remote host asynchronously.
145+
/// </summary>
146+
/// <param name="sessionId">The session ID.</param>
147+
/// <param name="source">The source of the file.</param>
148+
/// <param name="fileName">The file name.</param>
149+
/// <param name="cancellationToken">Cancellation token.</param>
150+
/// <returns>A <see cref="Stream"/> of the requested file.</returns>
151+
public async Task<Stream> ObtainFileAsync(string sessionId, string source, string fileName, CancellationToken cancellationToken = default)
152+
{
153+
using var response = await ExecuteGetAsync(
154+
ReplicationService.ReplicationAction.OBTAIN.ToString(),
155+
ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId,
156+
ReplicationService.REPLICATE_SOURCE_PARAM, source,
157+
ReplicationService.REPLICATE_FILENAME_PARAM, fileName,
158+
cancellationToken: cancellationToken).ConfigureAwait(false);
159+
160+
return await DoActionAsync(response, async () =>
161+
{
162+
return await GetResponseStreamAsync(response, cancellationToken).ConfigureAwait(false);
163+
}).ConfigureAwait(false);
164+
}
165+
166+
/// <summary>
167+
/// Publishes a new <see cref="IRevision"/> asynchronously.
168+
/// Not supported in this implementation.
169+
/// </summary>
170+
/// <param name="revision">The revision to publish.</param>
171+
/// <param name="cancellationToken">Cancellation token.</param>
172+
/// <returns>A <see cref="Task"/> representing the operation.</returns>
173+
/// <exception cref="NotSupportedException">Always thrown.</exception>
174+
public Task PublishAsync(IRevision revision, CancellationToken cancellationToken = default)
175+
{
176+
throw UnsupportedOperationException.Create(
177+
"this replicator implementation does not support remote publishing of revisions");
178+
}
179+
180+
/// <summary>
181+
/// Releases the session at the remote host asynchronously.
182+
/// </summary>
183+
/// <param name="sessionId">The session ID to release.</param>
184+
/// <param name="cancellationToken">Cancellation token.</param>
185+
/// <returns>A <see cref="Task"/> representing the operation.</returns>
186+
public async Task ReleaseAsync(string sessionId, CancellationToken cancellationToken = default)
187+
{
188+
using var response = await ExecuteGetAsync(
189+
ReplicationService.ReplicationAction.RELEASE.ToString(),
190+
ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId,
191+
cancellationToken: cancellationToken).ConfigureAwait(false);
192+
193+
await DoActionAsync(response, () =>
194+
{
195+
// No actual response content needed — just verification
196+
return Task.FromResult<object?>(null);
197+
}).ConfigureAwait(false);
198+
}
199+
200+
#endregion
201+
109202
}
110203
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.IO;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Lucene.Net.Replicator
6+
{
7+
/// <summary>
8+
/// Async version of <see cref="IReplicator"/> for non-blocking replication operations.
9+
/// </summary>
10+
public interface IAsyncReplicator
11+
{
12+
/// <summary>
13+
/// Check whether the given version is up-to-date and returns a
14+
/// <see cref="SessionToken"/> which can be used for fetching the revision files.
15+
/// </summary>
16+
/// <param name="currentVersion">Current version of the index.</param>
17+
/// <param name="cancellationToken">Cancellation token.</param>
18+
Task<SessionToken?> CheckForUpdateAsync(string currentVersion, CancellationToken cancellationToken = default);
19+
20+
/// <summary>
21+
/// Returns a stream for the requested file and source.
22+
/// </summary>
23+
Task<Stream> ObtainFileAsync(string sessionId, string source, string fileName, CancellationToken cancellationToken = default);
24+
25+
/// <summary>
26+
/// Notify that the specified session is no longer needed.
27+
/// </summary>
28+
Task ReleaseAsync(string sessionId, CancellationToken cancellationToken = default);
29+
30+
/// <summary>
31+
/// Publishing revisions is not supported in HttpReplicator; throw if called.
32+
/// </summary>
33+
Task PublishAsync(IRevision revision, CancellationToken cancellationToken = default);
34+
}
35+
}

0 commit comments

Comments
 (0)