diff --git a/Directory.Build.targets b/Directory.Build.targets index efbd8089f3..a8c0f7cfa5 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -27,7 +27,8 @@ - $(AlternatePublishRootDirectory)/$(TargetFramework)/$(MSBuildProjectName)/ + + $(AlternatePublishRootDirectory)/$(TargetFramework)/$(MSBuildProjectName)/ @@ -38,7 +39,8 @@ - + $(DefineConstants);FEATURE_ASPNETCORE_TESTHOST $(DefineConstants);FEATURE_UTF8_TOUTF16 @@ -46,7 +48,8 @@ - + $(DefineConstants);FEATURE_RANDOM_NEXTINT64_NEXTSINGLE $(DefineConstants);FEATURE_SPANFORMATTABLE @@ -55,22 +58,26 @@ - + $(DefineConstants);FEATURE_READONLYSET - + $(DefineConstants);FEATURE_PROCESS_KILL_ENTIREPROCESSTREE $(DefineConstants);FEATURE_STRING_CONCAT_READONLYSPAN - - + + $(DefineConstants);NETSTANDARD $(DefineConstants);FEATURE_CULTUREINFO_CURRENTCULTURE_SETTER @@ -78,9 +85,16 @@ portable - - - + + + $(DefineConstants);FEATURE_HTTPCONTENT_READASSTREAM + $(DefineConstants);FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN + + + $(DefineConstants);FEATURE_ARRAY_FILL $(DefineConstants);FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR @@ -92,22 +106,28 @@ - - + + $(DefineConstants);FEATURE_STRING_CONTAINS_STRINGCOMPARISON - - + + $(DefineConstants);FEATURE_ICONFIGURATIONROOT_PROVIDERS - - + + $(DefineConstants);FEATURE_ASSEMBLY_GETCALLINGASSEMBLY $(DefineConstants);FEATURE_FILESTREAM_LOCK @@ -116,14 +136,17 @@ - - + + $(DefineConstants);FEATURE_SERIALIZABLE_EXCEPTIONS $(DefineConstants);FEATURE_SERIALIZABLE - + @@ -132,7 +155,8 @@ - + $(DefineConstants);FEATURE_ICONFIGURATIONROOT_PROVIDERS @@ -142,7 +166,8 @@ $(DefineConstants);NETFRAMEWORK - $(DefineConstants);FEATURE_CODE_ACCESS_SECURITY $(DefineConstants);FEATURE_MEMORYMAPPEDFILESECURITY @@ -152,9 +177,12 @@ full - - - + + + $(DefineConstants);FEATURE_OPENNLP @@ -163,34 +191,45 @@ + https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg#nugetorg-symbol-package-constraints --> portable - + <_Parameter1>%(InternalsVisibleTo.Identity) - <_Parameter1 Condition=" '$(SignAssembly)' == 'true' And '$(PublicKey)' != '' ">%(InternalsVisibleTo.Identity), PublicKey=$(PublicKey) + <_Parameter1 Condition=" '$(SignAssembly)' == 'true' And '$(PublicKey)' != '' ">%(InternalsVisibleTo.Identity), + PublicKey=$(PublicKey) - + - true - $(TargetFramework) + + true + + $(TargetFramework) - $(TargetFrameworks) + + $(TargetFrameworks) none - + - @@ -201,7 +240,8 @@ - + @@ -209,17 +249,24 @@ - - + + - - + + - - + + diff --git a/src/Lucene.Net.Replicator/Http/HttpClientBase.cs b/src/Lucene.Net.Replicator/Http/HttpClientBase.cs index b6e94fb49a..04f0a0fd82 100644 --- a/src/Lucene.Net.Replicator/Http/HttpClientBase.cs +++ b/src/Lucene.Net.Replicator/Http/HttpClientBase.cs @@ -1,3 +1,4 @@ +#nullable enable using Lucene.Net.Diagnostics; using Lucene.Net.Support; using Newtonsoft.Json; @@ -73,7 +74,7 @@ public abstract class HttpClientBase : IDisposable /// The port to be used to connect on. /// The path to the replicator on the host. /// Optional, The HTTP handler stack to use for sending requests, defaults to null. - protected HttpClientBase(string host, int port, string path, HttpMessageHandler messageHandler = null) + protected HttpClientBase(string host, int port, string path, HttpMessageHandler? messageHandler = null) : this(NormalizedUrl(host, port, path), messageHandler) { } @@ -88,7 +89,7 @@ protected HttpClientBase(string host, int port, string path, HttpMessageHandler /// The full url, including with host, port and path. /// Optional, The HTTP handler stack to use for sending requests. //Note: LUCENENET Specific - protected HttpClientBase(string url, HttpMessageHandler messageHandler = null) + protected HttpClientBase(string url, HttpMessageHandler? messageHandler = null) : this(url, new HttpClient(messageHandler ?? new HttpClientHandler()) { Timeout = DEFAULT_TIMEOUT }) { } @@ -171,27 +172,51 @@ protected virtual void ThrowKnownError(HttpResponseMessage response) } /// - /// Internal: Execute a request and return its result. + /// Internal: Execute a POST request with custom HttpContent and return its result. /// The argument is treated as: name1,value1,name2,value2,... /// - protected virtual HttpResponseMessage ExecutePost(string request, object entity, params string[] parameters) + protected virtual HttpResponseMessage ExecutePost(string request, HttpContent content, params string[]? parameters) { EnsureOpen(); - //.NET Note: No headers? No ContentType?... Bad use of Http? - HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters)); + var req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters)) + { + Content = content + }; + + // Use SendAsync + GetAwaiter().GetResult() to bridge sync call + var resp = httpc.SendAsync(req, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + VerifyStatus(resp); + return resp; + } - req.Content = new StringContent(JToken.FromObject(entity, JsonSerializer.Create()) - .ToString(Formatting.None), Encoding.UTF8, "application/json"); + /// + /// Internal: Execute a POST request asynchronously with custom HttpContent. + /// The argument is treated as: name1,value1,name2,value2,... + /// + protected virtual async Task ExecutePostAsync(string request, HttpContent content, params string[]? parameters) + { + EnsureOpen(); - return Execute(req); + var req = new HttpRequestMessage(HttpMethod.Post, QueryString(request, parameters)) + { + Content = content + }; + + var resp = await httpc.SendAsync(req).ConfigureAwait(false); // Async call + VerifyStatus(resp); + return resp; } + /// /// Internal: Execute a request and return its result. /// The argument is treated as: name1,value1,name2,value2,... /// - protected virtual HttpResponseMessage ExecuteGet(string request, params string[] parameters) + protected virtual HttpResponseMessage ExecuteGet(string request, params string[]? parameters) { EnsureOpen(); @@ -200,6 +225,36 @@ protected virtual HttpResponseMessage ExecuteGet(string request, params string[] return Execute(req); } + /// + /// Execute a GET request asynchronously with an array of parameters. + /// + protected Task ExecuteGetAsync(string action, string[]? parameters, CancellationToken cancellationToken) + { + EnsureOpen(); + var url = QueryString(action, parameters); + return httpc.GetAsync(url, cancellationToken); + } + + /// + /// Execute a GET request asynchronously with up to 3 name/value parameters. + /// + protected Task ExecuteGetAsync( + string action, + string param1, string value1, + string? param2 = null, string? value2 = null, + string? param3 = null, string? value3 = null, + CancellationToken cancellationToken = default) + { + EnsureOpen(); + var url = (param2 == null && param3 == null) + ? QueryString(action, param1, value1) + : QueryString(action, + param1, value1, + param2 ?? string.Empty, value2 ?? string.Empty, + param3 ?? string.Empty, value3 ?? string.Empty); + return httpc.GetAsync(url, cancellationToken); + } + private HttpResponseMessage Execute(HttpRequestMessage request) { //.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,7 +264,7 @@ private HttpResponseMessage Execute(HttpRequestMessage request) return response; } - private string QueryString(string request, params string[] parameters) + private string QueryString(string request, params string[]? parameters) { return parameters is null ? string.Format("{0}/{1}", Url, request) @@ -256,7 +311,43 @@ public virtual Stream GetResponseStream(HttpResponseMessage response) // LUCENEN /// public virtual Stream GetResponseStream(HttpResponseMessage response, bool consume) // LUCENENET: This was ResponseInputStream in Lucene { +#if FEATURE_HTTPCONTENT_READASSTREAM + Stream result = response.Content.ReadAsStream(); +#else Stream result = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); +#endif + + if (consume) + result = new ConsumingStream(result); + return result; + } + + /// + /// Internal utility: input stream of the provided response asynchronously. + /// + /// + public virtual async Task GetResponseStreamAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + { +#if FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN + Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#else + Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#endif + return result; + } + + /// + /// Internal utility: input stream of the provided response asynchronously, which optionally + /// consumes the response's resources when the input stream is exhausted. + /// + /// + public virtual async Task GetResponseStreamAsync(HttpResponseMessage response, bool consume, CancellationToken cancellationToken = default) + { +#if FEATURE_HTTPCONTENT_READASSTREAM_CANCELLATIONTOKEN + Stream result = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#else + Stream result = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#endif if (consume) result = new ConsumingStream(result); return result; @@ -284,7 +375,7 @@ protected virtual T DoAction(HttpResponseMessage response, Func call) /// protected virtual T DoAction(HttpResponseMessage response, bool consume, Func call) { - Exception th = null; + Exception? th = null; try { return call(); @@ -315,10 +406,63 @@ protected virtual T DoAction(HttpResponseMessage response, bool consume, Func } } if (Debugging.AssertsEnabled) Debugging.Assert(th != null); // extra safety - if we get here, it means the Func failed - Util.IOUtils.ReThrow(th); - return default; // silly, if we're here, IOUtils.reThrow always throws an exception + Util.IOUtils.ReThrow(th!); + return default!; // silly, if we're here, IOUtils.reThrow always throws an exception + } + + /// + /// Do a specific async action and validate after the action that the status is still OK, + /// and if not, attempt to extract the actual server side exception. Optionally + /// release the response at exit, depending on parameter. + /// + protected virtual async Task DoActionAsync(HttpResponseMessage response, bool consume, Func> call) + { + Exception? th = null; + try + { + VerifyStatus(response); + return await call().ConfigureAwait(false); + } + catch (Exception t) when (t.IsThrowable()) + { + th = t; + } + finally + { + try + { + VerifyStatus(response); + } + finally + { + if (consume) + { + try + { + ConsumeQuietly(response); + } + catch + { + // ignore on purpose + } + } + } + } + + if (Debugging.AssertsEnabled) Debugging.Assert(th != null); + Util.IOUtils.ReThrow(th!); + return default!; // never reached, rethrow above always throws + } + + /// + /// Calls the overload passing true to consume. + /// + protected virtual Task DoActionAsync(HttpResponseMessage response, Func> call) + { + return DoActionAsync(response, true, call); } + /// /// Disposes this . /// When called with true, this disposes the underlying . diff --git a/src/Lucene.Net.Replicator/Http/HttpReplicator.cs b/src/Lucene.Net.Replicator/Http/HttpReplicator.cs index 5ae21ad06c..305f681fda 100644 --- a/src/Lucene.Net.Replicator/Http/HttpReplicator.cs +++ b/src/Lucene.Net.Replicator/Http/HttpReplicator.cs @@ -1,7 +1,11 @@ +#nullable enable using J2N.IO; using System; using System.IO; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + namespace Lucene.Net.Replicator.Http { @@ -28,14 +32,14 @@ namespace Lucene.Net.Replicator.Http /// /// @lucene.experimental /// - public class HttpReplicator : HttpClientBase, IReplicator + public class HttpReplicator : HttpClientBase, IReplicator, IAsyncReplicator { /// /// Creates a new with the given host, port and path. /// for more details. /// - public HttpReplicator(string host, int port, string path, HttpMessageHandler messageHandler = null) - : base(host, port, path, messageHandler) + public HttpReplicator(string host, int port, string path, HttpMessageHandler? messageHandler = null) + : base(host, port, path, messageHandler ?? new HttpClientHandler()) { } @@ -44,7 +48,7 @@ public HttpReplicator(string host, int port, string path, HttpMessageHandler mes /// for more details. /// //Note: LUCENENET Specific - public HttpReplicator(string url, HttpMessageHandler messageHandler = null) + public HttpReplicator(string url, HttpMessageHandler? messageHandler = null) : this(url, new HttpClient(messageHandler ?? new HttpClientHandler()) { Timeout = DEFAULT_TIMEOUT }) { } @@ -62,13 +66,13 @@ public HttpReplicator(string url, HttpClient client) /// /// Checks for updates at the remote host. /// - public virtual SessionToken CheckForUpdate(string currentVersion) + public virtual SessionToken? CheckForUpdate(string? currentVersion) { - string[] parameters = null; - if (currentVersion != null) - parameters = new[] { ReplicationService.REPLICATE_VERSION_PARAM, currentVersion }; + string[]? parameters = null; + if (!string.IsNullOrEmpty(currentVersion)) + parameters = new[] { ReplicationService.REPLICATE_VERSION_PARAM, currentVersion! }; - HttpResponseMessage response = base.ExecuteGet(ReplicationService.ReplicationAction.UPDATE.ToString(), parameters); + HttpResponseMessage response = base.ExecuteGet(ReplicationService.ReplicationAction.UPDATE.ToString(), parameters ?? Array.Empty()); return DoAction(response, () => { using DataInputStream inputStream = new DataInputStream(GetResponseStream(response)); @@ -104,7 +108,98 @@ public virtual void Release(string sessionId) { HttpResponseMessage response = ExecuteGet(ReplicationService.ReplicationAction.RELEASE.ToString(), ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId); // do not remove this call: as it is still validating for us! - DoAction(response, () => null); + DoAction(response, () => null); + } + + #region Async methods (IAsyncReplicator) + + /// + /// Checks for updates at the remote host asynchronously. + /// + /// The current index version. + /// Cancellation token. + /// + /// A if updates are available; otherwise, null. + /// + public async Task CheckForUpdateAsync(string? currentVersion, CancellationToken cancellationToken = default) + { + string[]? parameters = !string.IsNullOrEmpty(currentVersion) + ? new[] { ReplicationService.REPLICATE_VERSION_PARAM, currentVersion! } + : null; + + using var response = await ExecuteGetAsync( + ReplicationService.ReplicationAction.UPDATE.ToString(), + parameters ?? Array.Empty(), + cancellationToken: cancellationToken).ConfigureAwait(false); + + return await DoActionAsync(response, async () => + { + using var inputStream = new DataInputStream( + await GetResponseStreamAsync(response, cancellationToken).ConfigureAwait(false)); + + return inputStream.ReadByte() == 0 ? null : new SessionToken(inputStream); + }).ConfigureAwait(false); + } + + /// + /// Obtains the given file from the remote host asynchronously. + /// + /// The session ID. + /// The source of the file. + /// The file name. + /// Cancellation token. + /// A of the requested file. + public async Task ObtainFileAsync(string sessionId, string source, string fileName, CancellationToken cancellationToken = default) + { + using var response = await ExecuteGetAsync( + ReplicationService.ReplicationAction.OBTAIN.ToString(), + ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId, + ReplicationService.REPLICATE_SOURCE_PARAM, source, + ReplicationService.REPLICATE_FILENAME_PARAM, fileName, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return await DoActionAsync(response, async () => + { + return await GetResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + /// + /// Publishes a new asynchronously. + /// Not supported in this implementation. + /// + /// The revision to publish. + /// Cancellation token. + /// A representing the operation. + /// Always thrown. + public Task PublishAsync(IRevision revision, CancellationToken cancellationToken = default) + { + throw UnsupportedOperationException.Create( + "this replicator implementation does not support remote publishing of revisions"); + } + + /// + /// Releases the session at the remote host asynchronously. + /// + /// The session ID to release. + /// Cancellation token. + /// A representing the operation. + public async Task ReleaseAsync(string sessionId, CancellationToken cancellationToken = default) + { + using var response = await ExecuteGetAsync( + ReplicationService.ReplicationAction.RELEASE.ToString(), + ReplicationService.REPLICATE_SESSION_ID_PARAM, sessionId, + cancellationToken: cancellationToken).ConfigureAwait(false); + + await DoActionAsync(response, () => + { + // No actual response content needed — just verification + return Task.FromResult(null); + }).ConfigureAwait(false); } + + #endregion + } } +#nullable restore diff --git a/src/Lucene.Net.Replicator/Http/package.md b/src/Lucene.Net.Replicator/Http/package.md index fd9797b387..d7bac45817 100644 --- a/src/Lucene.Net.Replicator/Http/package.md +++ b/src/Lucene.Net.Replicator/Http/package.md @@ -3,8 +3,7 @@ uid: Lucene.Net.Replicator.Http summary: *content --- - - -Provides index files replication capabilities. \ No newline at end of file +Provides index files replication capabilities.