This is an automated email from the ASF dual-hosted git repository. florianhockmann pushed a commit to branch TINKERPOP-3063 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit e0e52b7300443fd9ce51007750585170e68d82fe Author: Florian Hockmann <f...@florian-hockmann.de> AuthorDate: Wed Mar 13 15:16:02 2024 +0100 TINKERPOP-3063 Fix authentication for concurrency Executing multiple queries in parallel could lead to authentication failures if `MaxInProcessPerConnection` is set to a value higher than `1` as the second request could then be sent to the server while the server was still waiting for the authentication challenge response from the driver for the first query. This made it necessary to set `MaxInProcessPerConnection=1` as a workaround for such scenarios which of course means that connection pooling is less efficient. Simply sending a validation request on each connection right after establishing the connection and therefore before it can be used to submit queries from the user should fix this issue. The downside of this is of course that connection establishment takes slightly longer. I think this is an acceptable trade-off even for scenarios where authentication is not used since this also validates the connection irrespective of authentication and also because this delay only occurs once right at the start of an application. --- CHANGELOG.asciidoc | 1 + docs/src/upgrade/release-3.6.x.asciidoc | 16 +++++ .../src/Gremlin.Net/Driver/Connection.cs | 2 +- .../src/Gremlin.Net/Driver/ConnectionPool.cs | 17 +++++ .../Driver/GremlinClientAuthenticationTests.cs | 73 +++++++++++++--------- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 27fde68afd..f1622b1bad 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -24,6 +24,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima === TinkerPop 3.6.7 (NOT OFFICIALLY RELEASED YET) * Fixed a bug in Gremlin.Net for .NET 8 that led to exceptions: `InvalidOperationException: Enumeration has not started. Call MoveNext.` +* Fixed a bug in the Gremlin.Net driver that could lead to failed authentication if multiple requests are executed in parallel. * Fixed message requestId serialization in `gremlin-python`. * Improved performance of `PathRetractionStrategy` for traversals that carry many children, but don't hold many labels to propogate. * Fixed bug in bytecode translation of `g.tx().commit()` and `g.tx().rollback()` in all languages. diff --git a/docs/src/upgrade/release-3.6.x.asciidoc b/docs/src/upgrade/release-3.6.x.asciidoc index da190bdc89..ac268528d1 100644 --- a/docs/src/upgrade/release-3.6.x.asciidoc +++ b/docs/src/upgrade/release-3.6.x.asciidoc @@ -43,6 +43,22 @@ Gremlin.Net driver expected. See: link:https://issues.apache.org/jira/browse/TINKERPOP-3029[TINKERPOP-3029] +==== Gremlin.Net: Fixed a bug related to authentication for multiple requests in parallel + +Executing multiple queries in parallel could lead to authentication failures if `MaxInProcessPerConnection` is set to a +value higher than `1` as the second request could then be sent to the server while the server was still waiting for +the authentication challenge response from the driver for the first query. + +This is now fixed by simply executing a validation request on each connection right after establishing the connection. +That validation request will already handle authentication if necessary so user requests can afterwards directly be +executed in parallel without needing to authenticate again. + +This is strictly speaking a breaking change since exceptions caused by authentication failures (no/wrong credentials +configured) now occur directly when the `GremlinClient` is instantiated. +In previous versions, the exceptions would only occur when the first request is executed. + +See: link:https://issues.apache.org/jira/browse/TINKERPOP-3063[TINKERPOP-3063] + === Upgrading for Providers diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs index cabe587fcd..6e4da37b7c 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs @@ -140,7 +140,7 @@ namespace Gremlin.Net.Driver } catch (Exception e) { - if (receivedMsg!.RequestId != null) + if (receivedMsg.RequestId != null) { if(_callbackByRequestId.TryRemove(receivedMsg.RequestId.Value, out var responseHandler)) { diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionPool.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionPool.cs index e4f4a72ae2..82ecace0a7 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionPool.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionPool.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Gremlin.Net.Driver.Exceptions; +using Gremlin.Net.Driver.Messages; using Gremlin.Net.Process; using Microsoft.Extensions.Logging; using Polly; @@ -163,6 +164,7 @@ namespace Gremlin.Net.Driver try { await newConnection.ConnectAsync(_cts.Token).ConfigureAwait(false); + await ValidateConnectionAsync(newConnection).ConfigureAwait(false); } catch (Exception) { @@ -173,6 +175,21 @@ namespace Gremlin.Net.Driver return newConnection; } + /// <summary> + /// Validates the connection by executing a simple validation request. + /// </summary> + /// <remarks>This also authenticates the user if necessary and therefore ensures that user requests can directly + /// be executed.</remarks> + private async Task ValidateConnectionAsync(IConnection connection) + { + await connection.SubmitAsync<string>(ValidationRequestMessage, _cts.Token).ConfigureAwait(false); + } + + private static readonly string ValidationRequest = "''"; + + private static readonly RequestMessage ValidationRequestMessage = RequestMessage.Build(Tokens.OpsEval) + .AddArgument(Tokens.ArgsGremlin, ValidationRequest).Create(); + private IConnection GetConnectionFromPool() { var connections = _connections.Snapshot; diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs index e56e282248..e9a00d5579 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs @@ -22,13 +22,13 @@ #endregion using System; +using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using Gremlin.Net.Driver; using Gremlin.Net.Driver.Exceptions; -using Gremlin.Net.IntegrationTest.Util; using Xunit; namespace Gremlin.Net.IntegrationTest.Driver @@ -37,7 +37,6 @@ namespace Gremlin.Net.IntegrationTest.Driver { private static readonly string TestHost = ConfigProvider.Configuration["TestServerIpAddress"]; private static readonly int TestPort = Convert.ToInt32(ConfigProvider.Configuration["TestSecureServerPort"]); - private readonly RequestMessageProvider _requestMessageProvider = new RequestMessageProvider(); public static bool IgnoreCertificateValidationLiveDangerouslyWheeeeeeee( object sender, @@ -49,48 +48,41 @@ namespace Gremlin.Net.IntegrationTest.Driver } [Fact] - public async Task ShouldThrowForMissingCredentials() + public void ShouldThrowForMissingCredentials() { - ClientWebSocketOptions optionsSet = null; var webSocketConfiguration = new Action<ClientWebSocketOptions>(options => { options.RemoteCertificateValidationCallback += IgnoreCertificateValidationLiveDangerouslyWheeeeeeee; - optionsSet = options; }); var gremlinServer = new GremlinServer(TestHost, TestPort, enableSsl: true); - using (var gremlinClient = new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration)) - { - var exception = await Assert.ThrowsAsync<InvalidOperationException>( - async () => await gremlinClient.SubmitWithSingleResultAsync<string>(_requestMessageProvider - .GetDummyMessage())); + var aggregateException = Assert.Throws<AggregateException>(() => + new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration)); - Assert.Contains("authentication", exception.Message); - Assert.Contains("credentials", exception.Message); - } + var innerException = aggregateException.InnerException; + Assert.Equal(typeof(InvalidOperationException), innerException!.GetType()); + Assert.Contains("authentication", innerException.Message); + Assert.Contains("credentials", innerException.Message); } [Theory] [InlineData("unknownUser", "passwordDoesntMatter")] [InlineData("stephen", "wrongPassword")] - public async Task ShouldThrowForWrongCredentials(string username, string password) + public void ShouldThrowForWrongCredentials(string username, string password) { - ClientWebSocketOptions optionsSet = null; var webSocketConfiguration = new Action<ClientWebSocketOptions>(options => { options.RemoteCertificateValidationCallback += IgnoreCertificateValidationLiveDangerouslyWheeeeeeee; - optionsSet = options; }); var gremlinServer = new GremlinServer(TestHost, TestPort, username: username, password: password, enableSsl: true); - using (var gremlinClient = new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration)) - { - var exception = await Assert.ThrowsAsync<ResponseException>( - async () => await gremlinClient.SubmitWithSingleResultAsync<string>(_requestMessageProvider - .GetDummyMessage())); + var aggregateException = Assert.Throws<AggregateException>(() => + new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration)); - Assert.Contains("Unauthorized", exception.Message); - } + var innerException = aggregateException.InnerException; + Assert.Equal(typeof(ResponseException), innerException!.GetType()); + + Assert.Contains("Unauthorized", innerException.Message); } [Theory] @@ -98,22 +90,45 @@ namespace Gremlin.Net.IntegrationTest.Driver public async Task ScriptShouldBeEvaluatedAndResultReturnedForCorrectCredentials(string requestMsg, string expectedResponse) { - ClientWebSocketOptions optionsSet = null; var webSocketConfiguration = new Action<ClientWebSocketOptions>(options => { options.RemoteCertificateValidationCallback += IgnoreCertificateValidationLiveDangerouslyWheeeeeeee; - optionsSet = options; }); const string username = "stephen"; const string password = "password"; var gremlinServer = new GremlinServer(TestHost, TestPort, username: username, password: password, enableSsl: true); - using (var gremlinClient = new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration)) - { - var response = await gremlinClient.SubmitWithSingleResultAsync<string>(requestMsg); + using var gremlinClient = new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration); + + var response = await gremlinClient.SubmitWithSingleResultAsync<string>(requestMsg); + + Assert.Equal(expectedResponse, response); + } - Assert.Equal(expectedResponse, response); + [Fact] + public async Task ExecutingRequestsInParallelOverSameConnectionShouldWorkWithAuthentication() + { + var webSocketConfiguration = + new Action<ClientWebSocketOptions>(options => + { + options.RemoteCertificateValidationCallback += IgnoreCertificateValidationLiveDangerouslyWheeeeeeee; + }); + const string username = "stephen"; + const string password = "password"; + const int nrOfParallelRequests = 10; + var gremlinServer = new GremlinServer(TestHost, TestPort, username: username, password: password, enableSsl: true); + var connectionPoolSettings = new ConnectionPoolSettings + { PoolSize = 1, MaxInProcessPerConnection = nrOfParallelRequests }; + + using var gremlinClient = new GremlinClient(gremlinServer, webSocketConfiguration: webSocketConfiguration, + connectionPoolSettings: connectionPoolSettings); + var tasks = new List<Task>(nrOfParallelRequests); + for (var i = 0; i < nrOfParallelRequests; i++) + { + tasks.Add(gremlinClient.SubmitWithSingleResultAsync<string>("''")); } + + await Task.WhenAll(tasks); } } } \ No newline at end of file