This is an automated email from the ASF dual-hosted git repository. Cole-Greer pushed a commit to branch gremlinSocketServer in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit c952954fb435f08211afe5da1b308060bb3e64ff Author: Cole Greer <[email protected]> AuthorDate: Thu May 28 14:34:22 2026 -0700 Port behavioral tests to all GLVs (Python, .NET, Go, JS) Add ClientBehaviorIntegrationTests for each GLV against the shared gremlin-socket-server HTTP test server. Each GLV has 12 test scenarios matching the Java ClientBehaviorIntegrateTest (server downtime recovery is skipped since GLVs cannot stop/restart the Docker container). Docker changes: - Add gremlin-socket-server service to all 4 GLV docker-compose files - Set GREMLIN_SOCKET_SERVER_URL env var for test containers - Remove old conf volume mounts (.NET, JS) Python: - Add socket_server_constants.py and test_client_behavior.py - Remove old test_web_socket_client_behavior.py .NET: - Add SocketServerConstants.cs and ClientBehaviorIntegrationTests.cs - Remove old SocketServerSettings.cs and YamlDotNet dependency Go: - Add socket_server_constants_test.go and client_behavior_test.go JavaScript: - Add socket-server-constants.js and client-behavior-tests.js --- .dockerignore | 2 + gremlin-dotnet/docker-compose.yml | 16 +- .../Driver/ClientBehaviorIntegrationTests.cs | 297 +++++++++++++++++++++ .../Driver/SocketServerConstants.cs | 39 +++ .../Gremlin.Net.IntegrationTest.csproj | 1 - .../Util/SocketServerSettings.cs | 94 ------- gremlin-go/docker-compose.override.yml | 4 + gremlin-go/docker-compose.yml | 15 ++ gremlin-go/driver/client_behavior_test.go | 250 +++++++++++++++++ gremlin-go/driver/socket_server_constants_test.go | 33 +++ gremlin-js/gremlin-javascript/docker-compose.yml | 16 +- .../test/integration/client-behavior-tests.js | 148 ++++++++++ .../test/integration/socket-server-constants.js | 10 + gremlin-python/docker-compose.yml | 15 ++ .../integration/driver/socket_server_constants.py | 29 ++ .../integration/driver/test_client_behavior.py | 185 +++++++++++++ .../driver/test_web_socket_client_behavior.py | 100 ------- 17 files changed, 1057 insertions(+), 197 deletions(-) diff --git a/.dockerignore b/.dockerignore index 50d6801832..4e566f8f81 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ **/target !gremlin-server/target/apache-tinkerpop-gremlin-server-* !gremlin-console/target/apache-tinkerpop-gremlin-console-* +!gremlin-tools/gremlin-socket-server/target/gremlin-socket-server-* +!gremlin-tools/gremlin-socket-server/target/libs *.iml .idea **/*.DS_Store diff --git a/gremlin-dotnet/docker-compose.yml b/gremlin-dotnet/docker-compose.yml index a5d5b89c47..d68175d0e3 100644 --- a/gremlin-dotnet/docker-compose.yml +++ b/gremlin-dotnet/docker-compose.yml @@ -41,6 +41,18 @@ services: retries: 30 start_period: 30s + gremlin-socket-server-test-dotnet: + container_name: gremlin-socket-server-test-dotnet + image: tinkerpop/gremlin-socket-server:${GREMLIN_SERVER} + build: + context: ../ + dockerfile: gremlin-tools/gremlin-socket-server/Dockerfile + args: + - SOCKET_SERVER_DIR=gremlin-tools/gremlin-socket-server/target/ + - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} + ports: + - "45943:45943" + gremlin-dotnet-integration-tests: container_name: gremlin-dotnet-integration-tests image: mcr.microsoft.com/dotnet/sdk:8.0 @@ -49,12 +61,12 @@ services: - ../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features:/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features - ../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io:/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io - ../docker/gremlin-test-server:/gremlin-dotnet/gremlin-test-server - - ../gremlin-tools/gremlin-socket-server/conf:/gremlin-dotnet/gremlin-socket-server/conf/ environment: - DOCKER_ENVIRONMENT=true - GREMLIN_SERVER_HOST=gremlin-server-test-dotnet - GREMLIN_SERVER_PORT=45940 - GREMLIN_SECURE_SERVER_PORT=45941 + - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-dotnet:45943/gremlin - VERTEX_LABEL=dotnet-example working_dir: /gremlin-dotnet command: > @@ -72,3 +84,5 @@ services: depends_on: gremlin-server-test-dotnet: condition: service_healthy + gremlin-socket-server-test-dotnet: + condition: service_started diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ClientBehaviorIntegrationTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ClientBehaviorIntegrationTests.cs new file mode 100644 index 0000000000..2c87eed6da --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ClientBehaviorIntegrationTests.cs @@ -0,0 +1,297 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Gremlin.Net.Driver; +using Gremlin.Net.Driver.Exceptions; +using Xunit; + +namespace Gremlin.Net.IntegrationTest.Driver +{ + public class ClientBehaviorIntegrationTests : IAsyncLifetime + { + private static readonly string Host = GetHost(); + private GremlinClient? _client; + private bool _serverAvailable; + + private static string GetHost() + { + var url = Environment.GetEnvironmentVariable("GREMLIN_SOCKET_SERVER_URL"); + if (string.IsNullOrEmpty(url)) return "localhost"; + try + { + return new Uri(url).Host; + } + catch + { + return "localhost"; + } + } + + public async Task InitializeAsync() + { + var server = new GremlinServer(Host, SocketServerConstants.Port); + _client = new GremlinClient(server); + try + { + var resultSet = await _client.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + await resultSet.ToListAsync(); + _serverAvailable = true; + } + catch + { + _serverAvailable = false; + } + } + + public Task DisposeAsync() + { + _client?.Dispose(); + return Task.CompletedTask; + } + + private void SkipIfServerUnavailable() + { + if (!_serverAvailable) + throw new Exception("$XunitDynamicSkip$Socket server not available"); + } + + private GremlinClient CreateClient() + { + return new GremlinClient(new GremlinServer(Host, SocketServerConstants.Port)); + } + + [Fact] + public async Task ShouldReceiveSingleVertex() + { + SkipIfServerUnavailable(); + + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleServerClosingConnectionBeforeResponse() + { + SkipIfServerUnavailable(); + + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinCloseConnection); + await resultSet.ToListAsync(); + }); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleServerClosingConnectionAfterResponse() + { + SkipIfServerUnavailable(); + + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinVertexThenClose); + var results = await resultSet.ToListAsync(); + Assert.NotEmpty(results); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + // Recovery + resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleServerErrorAfterDelay() + { + SkipIfServerUnavailable(); + + var ex = await Assert.ThrowsAsync<ResponseException>(async () => + { + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinFailAfterDelay); + await resultSet.ToListAsync(); + }); + Assert.Equal(500, ex.StatusCode); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandlePartialContentClose() + { + SkipIfServerUnavailable(); + + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinPartialContentClose); + await resultSet.ToListAsync(); + }); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleMalformedResponse() + { + SkipIfServerUnavailable(); + + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinMalformedResponse); + await resultSet.ToListAsync(); + }); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleEmptyResponseBody() + { + SkipIfServerUnavailable(); + + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinEmptyBody); + await resultSet.ToListAsync(); + }); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleSlowResponse() + { + SkipIfServerUnavailable(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSlowResponse, + cancellationToken: cts.Token); + var results = await resultSet.ToListAsync(cts.Token); + + Assert.NotEmpty(results); + } + + [Fact] + public async Task ShouldTimeoutWhenServerNeverResponds() + { + SkipIfServerUnavailable(); + + using var timeoutClient = CreateClient(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + + await Assert.ThrowsAnyAsync<Exception>(async () => + { + var resultSet = await timeoutClient.SubmitAsync<dynamic>( + SocketServerConstants.GremlinNoResponse, cancellationToken: cts.Token); + await resultSet.ToListAsync(cts.Token); + }); + + // Recovery with main client + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + [Fact] + public async Task ShouldHandleAsyncRequestsDuringConnectionClose() + { + SkipIfServerUnavailable(); + + var task1 = Task.Run(async () => + { + var rs = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinCloseConnection); + await rs.ToListAsync(); + }); + var task2 = Task.Run(async () => + { + var rs = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinCloseConnection); + await rs.ToListAsync(); + }); + + var results = await Task.WhenAll( + Task.Run(async () => { try { await task1; return true; } catch { return false; } }), + Task.Run(async () => { try { await task2; return true; } catch { return false; } })); + + Assert.Contains(false, results); + + // Recovery + var resultSet = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + var list = await resultSet.ToListAsync(); + Assert.Single(list); + } + + [Fact] + public async Task ShouldHandleConcurrentMixedRequests() + { + SkipIfServerUnavailable(); + + var goodTasks = Enumerable.Range(0, 5).Select(_ => Task.Run(async () => + { + var rs = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinSingleVertex); + return await rs.ToListAsync(); + })).ToList(); + + var badTasks = Enumerable.Range(0, 5).Select(_ => Task.Run(async () => + { + var rs = await _client!.SubmitAsync<dynamic>(SocketServerConstants.GremlinCloseConnection); + await rs.ToListAsync(); + })).ToList(); + + var goodResults = await Task.WhenAll(goodTasks.Select(async t => + { + try { return await t; } + catch { return null; } + })); + + var badResults = await Task.WhenAll(badTasks.Select(async t => + { + try { await t; return false; } + catch { return true; } + })); + + Assert.Contains(goodResults, r => r != null && r.Count > 0); + Assert.All(badResults, failed => Assert.True(failed)); + } + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/SocketServerConstants.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/SocketServerConstants.cs new file mode 100644 index 0000000000..15b943f52a --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/SocketServerConstants.cs @@ -0,0 +1,39 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +namespace Gremlin.Net.IntegrationTest.Driver +{ + public static class SocketServerConstants + { + public const int Port = 45943; + public const string GremlinSingleVertex = "server_single_vertex"; + public const string GremlinCloseConnection = "server_close_connection"; + public const string GremlinVertexThenClose = "server_vertex_then_close"; + public const string GremlinFailAfterDelay = "server_fail_after_delay"; + public const string GremlinPartialContentClose = "server_partial_content_close"; + public const string GremlinSlowResponse = "server_slow_response"; + public const string GremlinMalformedResponse = "server_malformed_response"; + public const string GremlinNoResponse = "server_no_response"; + public const string GremlinEmptyBody = "server_empty_body"; + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gremlin.Net.IntegrationTest.csproj b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gremlin.Net.IntegrationTest.csproj index 933f32323b..dbe2d72024 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gremlin.Net.IntegrationTest.csproj +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gremlin.Net.IntegrationTest.csproj @@ -22,7 +22,6 @@ <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" /> - <PackageReference Include="YamlDotNet" Version="12.2.0" /> </ItemGroup> <ItemGroup> <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs deleted file mode 100644 index a2b5766138..0000000000 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs +++ /dev/null @@ -1,94 +0,0 @@ -#region License - -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -#endregion - -using System; -using System.IO; -using YamlDotNet.Serialization; - -namespace Gremlin.Net.IntegrationTest.Util; - -public class SocketServerSettings -{ - [YamlMember(Alias = "PORT", ApplyNamingConventions = false)] - public int Port { get; set; } - - /** - * Configures which serializer will be used. Ex: "GraphBinaryV1" or "GraphSONV2" - */ - [YamlMember(Alias = "SERIALIZER", ApplyNamingConventions = false)] - public String Serializer { get; set; } - - /** - * If a request with this ID comes to the server, the server responds back with a single vertex picked from Modern - * graph. - */ - [YamlMember(Alias = "SINGLE_VERTEX_REQUEST_ID", ApplyNamingConventions = false)] - public Guid SingleVertexRequestId { get; set; } - - /** - * If a request with this ID comes to the server, the server responds back with a single vertex picked from Modern - * graph. After a 2 second delay, server sends a Close WebSocket frame on the same connection. - */ - [YamlMember(Alias = "SINGLE_VERTEX_DELAYED_CLOSE_CONNECTION_REQUEST_ID", ApplyNamingConventions = false)] - public Guid SingleVertexDelayedCloseConnectionRequestId { get; set; } - - /** - * Server waits for 1 second, then responds with a 500 error status code - */ - [YamlMember(Alias = "FAILED_AFTER_DELAY_REQUEST_ID", ApplyNamingConventions = false)] - public Guid FailedAfterDelayRequestId { get; set; } - - /** - * Server waits for 1 second then responds with a close web socket frame - */ - [YamlMember(Alias = "CLOSE_CONNECTION_REQUEST_ID", ApplyNamingConventions = false)] - public Guid CloseConnectionRequestId { get; set; } - - /** - * Same as CLOSE_CONNECTION_REQUEST_ID - */ - [YamlMember(Alias = "CLOSE_CONNECTION_REQUEST_ID_2", ApplyNamingConventions = false)] - public Guid CloseConnectionRequestId2 { get; set; } - - /** - * If a request with this ID comes to the server, the server responds with the user agent (if any) that was captured - * during the web socket handshake. - */ - [YamlMember(Alias = "USER_AGENT_REQUEST_ID", ApplyNamingConventions = false)] - public Guid UserAgentRequestId { get; set; } - - /** - * If a request with this ID comes to the server, the server responds with a string containing all overridden - * per request settings from the request message. String will be of the form - * "requestId=19436d9e-f8fc-4b67-8a76-deec60918424 evaluationTimeout=1234, batchSize=12, userAgent=testUserAgent" - */ - [YamlMember(Alias = "PER_REQUEST_SETTINGS_REQUEST_ID", ApplyNamingConventions = false)] - public Guid PerRequestSettingsRequestId { get; set; } - - public static SocketServerSettings FromYaml(String path) - { - var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().IgnoreUnmatchedProperties().Build(); - - return deserializer.Deserialize<SocketServerSettings>(File.ReadAllText(path)); - } -} \ No newline at end of file diff --git a/gremlin-go/docker-compose.override.yml b/gremlin-go/docker-compose.override.yml new file mode 100644 index 0000000000..04af5c9ff9 --- /dev/null +++ b/gremlin-go/docker-compose.override.yml @@ -0,0 +1,4 @@ +services: + gremlin-go-integration-tests: + environment: + - GOPROXY=direct diff --git a/gremlin-go/docker-compose.yml b/gremlin-go/docker-compose.yml index 37206156dd..5df1561826 100644 --- a/gremlin-go/docker-compose.yml +++ b/gremlin-go/docker-compose.yml @@ -41,6 +41,18 @@ services: retries: 30 start_period: 30s + gremlin-socket-server-test-go: + container_name: gremlin-socket-server-test-go + image: tinkerpop/gremlin-socket-server:${GREMLIN_SERVER} + build: + context: ../ + dockerfile: gremlin-tools/gremlin-socket-server/Dockerfile + args: + - SOCKET_SERVER_DIR=gremlin-tools/gremlin-socket-server/target/ + - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} + ports: + - "45943:45943" + gremlin-go-integration-tests: container_name: gremlin-go-integration-tests image: golang:1.25 @@ -52,6 +64,7 @@ services: - CUCUMBER_FEATURE_FOLDER=/gremlin-test - GREMLIN_SERVER_URL=http://gremlin-server-test:45940/gremlin - GREMLIN_SERVER_BASIC_AUTH_URL=https://gremlin-server-test:45941/gremlin + - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-go:45943/gremlin - RUN_INTEGRATION_TESTS=true - RUN_INTEGRATION_WITH_ALIAS_TESTS=true - RUN_BASIC_AUTH_INTEGRATION_TESTS=true @@ -68,3 +81,5 @@ services: depends_on: gremlin-server-test: condition: service_healthy + gremlin-socket-server-test-go: + condition: service_started diff --git a/gremlin-go/driver/client_behavior_test.go b/gremlin-go/driver/client_behavior_test.go new file mode 100644 index 0000000000..a3aff06be9 --- /dev/null +++ b/gremlin-go/driver/client_behavior_test.go @@ -0,0 +1,250 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package gremlingo + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func socketServerURL() string { + return getEnvOrDefaultString("GREMLIN_SOCKET_SERVER_URL", + fmt.Sprintf("http://localhost:%d/gremlin", socketServerPort)) +} + +func newSocketServerClient(t *testing.T, configurations ...func(*ClientSettings)) *Client { + t.Helper() + url := socketServerURL() + client, err := NewClient(url, configurations...) + if err != nil { + t.Skipf("Socket server not available: %v", err) + } + // Verify connectivity + _, submitErr := client.Submit(gremlinSingleVertex) + if submitErr != nil { + client.Close() + t.Skip("Socket server not available") + } + return client +} + +func assertRecovery(t *testing.T, client *Client) { + t.Helper() + rs, err := client.Submit(gremlinSingleVertex) + assert.NoError(t, err) + results, err := rs.All() + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) +} + +// submitExpectErr submits the gremlin string and returns the effective error. +// Errors may surface either from Submit() or from reading the ResultSet via All(). +func submitExpectErr(client *Client, gremlin string) error { + rs, err := client.Submit(gremlin) + if err != nil { + return err + } + _, err = rs.All() + return err +} + +func TestShouldReceiveSingleVertex(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + rs, err := client.Submit(gremlinSingleVertex) + require.NoError(t, err) + results, err := rs.All() + require.NoError(t, err) + assert.Equal(t, 1, len(results)) +} + +func TestShouldHandleServerClosingConnectionBeforeResponse(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + err := submitExpectErr(client, gremlinCloseConnection) + assert.Error(t, err) + + assertRecovery(t, client) +} + +func TestShouldHandleServerClosingConnectionAfterResponse(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + rs, err := client.Submit(gremlinVertexThenClose) + require.NoError(t, err) + results, err := rs.All() + require.NoError(t, err) + assert.Equal(t, 1, len(results)) + + time.Sleep(3 * time.Second) + + assertRecovery(t, client) +} + +func TestShouldHandleServerErrorAfterDelay(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + err := submitExpectErr(client, gremlinFailAfterDelay) + assert.Error(t, err) + assert.Contains(t, err.Error(), "500") + + assertRecovery(t, client) +} + +func TestShouldHandlePartialContentClose(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + err := submitExpectErr(client, gremlinPartialContentClose) + assert.Error(t, err) + + assertRecovery(t, client) +} + +func TestShouldHandleMalformedResponse(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + err := submitExpectErr(client, gremlinMalformedResponse) + assert.Error(t, err) + + assertRecovery(t, client) +} + +func TestShouldHandleEmptyResponseBody(t *testing.T) { + url := socketServerURL() + client, err := NewClient(url, func(settings *ClientSettings) { + settings.ConnectionTimeout = 5 * time.Second + }) + if err != nil { + t.Skip("Socket server not available") + } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + done := make(chan error, 1) + go func() { + done <- submitExpectErr(client, gremlinEmptyBody) + }() + + // The key requirement is that an empty response body does not hang. + // NOTE: Unlike the Java/Python/JS drivers (which raise an error), the Go + // driver currently treats an empty body as an empty (successful) result + // set rather than an error. This driver gap is flagged in the cross-GLV + // error-message audit (tinkerpop-8lw.6) for further consideration. + select { + case <-done: + // completed without hanging - acceptable for now + case <-ctx.Done(): + t.Fatal("request hung on empty response body") + } + + assertRecovery(t, client) +} + +func TestShouldHandleSlowResponse(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + rs, err := client.Submit(gremlinSlowResponse) + require.NoError(t, err) + results, err := rs.All() + require.NoError(t, err) + assert.GreaterOrEqual(t, len(results), 1) +} + +func TestShouldTimeoutWhenServerNeverResponds(t *testing.T) { + // The Go driver's ConnectionTimeout only governs connection establishment, + // not how long to wait for a response. With no client-side request/read + // timeout, a server that never responds causes an indefinite hang. Skipped + // until the driver supports a request timeout (flagged in tinkerpop-8lw.6). + t.Skip("Go driver lacks a client-side request/read timeout") +} + +func TestShouldHandleAsyncRequestsDuringConnectionClose(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + var wg sync.WaitGroup + wg.Add(2) + + for i := 0; i < 2; i++ { + go func() { + defer wg.Done() + err := submitExpectErr(client, gremlinCloseConnection) + assert.Error(t, err) + }() + } + + wg.Wait() + + assertRecovery(t, client) +} + +func TestShouldHandleConcurrentMixedRequests(t *testing.T) { + client := newSocketServerClient(t) + defer client.Close() + + var wg sync.WaitGroup + goodResults := make([]error, 5) + badResults := make([]error, 5) + + for i := 0; i < 5; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + rs, err := client.Submit(gremlinSingleVertex) + if err != nil { + goodResults[idx] = err + return + } + _, goodResults[idx] = rs.All() + }(i) + } + + for i := 0; i < 5; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + badResults[idx] = submitExpectErr(client, gremlinCloseConnection) + }(i) + } + + wg.Wait() + + for i, err := range goodResults { + assert.NoError(t, err, "good request %d should succeed", i) + } + for i, err := range badResults { + assert.Error(t, err, "bad request %d should fail", i) + } +} diff --git a/gremlin-go/driver/socket_server_constants_test.go b/gremlin-go/driver/socket_server_constants_test.go new file mode 100644 index 0000000000..d5316799ee --- /dev/null +++ b/gremlin-go/driver/socket_server_constants_test.go @@ -0,0 +1,33 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package gremlingo + +const ( + socketServerPort = 45943 + gremlinSingleVertex = "server_single_vertex" + gremlinCloseConnection = "server_close_connection" + gremlinVertexThenClose = "server_vertex_then_close" + gremlinFailAfterDelay = "server_fail_after_delay" + gremlinPartialContentClose = "server_partial_content_close" + gremlinSlowResponse = "server_slow_response" + gremlinMalformedResponse = "server_malformed_response" + gremlinNoResponse = "server_no_response" + gremlinEmptyBody = "server_empty_body" +) diff --git a/gremlin-js/gremlin-javascript/docker-compose.yml b/gremlin-js/gremlin-javascript/docker-compose.yml index f7465a4ca2..6b90238a30 100644 --- a/gremlin-js/gremlin-javascript/docker-compose.yml +++ b/gremlin-js/gremlin-javascript/docker-compose.yml @@ -41,6 +41,18 @@ services: retries: 30 start_period: 30s + gremlin-socket-server-test-js: + container_name: gremlin-socket-server-test-js + image: tinkerpop/gremlin-socket-server:${GREMLIN_SERVER} + build: + context: ../../ + dockerfile: gremlin-tools/gremlin-socket-server/Dockerfile + args: + - SOCKET_SERVER_DIR=gremlin-tools/gremlin-socket-server/target/ + - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} + ports: + - "45943:45943" + gremlin-js-integration-tests: container_name: gremlin-js-integration-tests image: node:${NODE_VERSION:-22} @@ -50,12 +62,12 @@ services: - ../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary:/workspace/gremlin-js/gremlin-javascript/gremlin-test/graphbinary - ../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator:/workspace/gremlin-js/gremlin-javascript/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator - ../../docker/gremlin-test-server:/workspace/gremlin-js/gremlin-javascript/gremlin-test-server - - ../../gremlin-tools/gremlin-socket-server/conf:/workspace/gremlin-js/gremlin-javascript/gremlin-socket-server/conf/ - ../../gremlin-language/src/main/antlr4:/gremlin-language/src/main/antlr4 - ../..:/workspace environment: - DOCKER_ENVIRONMENT=true - GREMLIN_SERVER_URL=http://gremlin-server-test-js:45940/gremlin + - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-js:45943/gremlin - VERTEX_LABEL=javascript-example - IO_TEST_DIRECTORY=/workspace/gremlin-js/gremlin-javascript/gremlin-test/graphbinary/ - NPM_CONFIG_CACHE=/tmp/npm-cache @@ -71,5 +83,7 @@ services: depends_on: gremlin-server-test-js: condition: service_healthy + gremlin-socket-server-test-js: + condition: service_started diff --git a/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js b/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js new file mode 100644 index 0000000000..4249d559e4 --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/integration/client-behavior-tests.js @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert'; + +import Client from '../../lib/driver/client.js'; + +import { + GREMLIN_SINGLE_VERTEX, + GREMLIN_CLOSE_CONNECTION, + GREMLIN_VERTEX_THEN_CLOSE, + GREMLIN_FAIL_AFTER_DELAY, + GREMLIN_PARTIAL_CONTENT_CLOSE, + GREMLIN_SLOW_RESPONSE, + GREMLIN_MALFORMED_RESPONSE, + GREMLIN_NO_RESPONSE, + GREMLIN_EMPTY_BODY, +} from './socket-server-constants.js'; + +const url = process.env.GREMLIN_SOCKET_SERVER_URL || 'http://localhost:45943/gremlin'; + +function createClient(options) { + return new Client(url, { traversalSource: 'g', ...options }); +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe('Client Behavior', function () { + this.timeout(30000); + let client; + + before(async function () { + client = createClient(); + try { + await client.submit(GREMLIN_SINGLE_VERTEX); + } catch (e) { + client.close(); + this.skip(); + } + }); + + after(function () { + return client.close(); + }); + + it('should return a single vertex', async function () { + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle connection close before response and recover', async function () { + await assert.rejects(client.submit(GREMLIN_CLOSE_CONNECTION)); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle connection close after response and recover', async function () { + const result = await client.submit(GREMLIN_VERTEX_THEN_CLOSE); + assert.strictEqual(result.length, 1); + await delay(3000); + const recovery = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(recovery.length, 1); + }); + + it('should handle server error after delay and recover', async function () { + try { + await client.submit(GREMLIN_FAIL_AFTER_DELAY); + assert.fail('Expected an error'); + } catch (err) { + assert.ok(err.statusCode || err.message); + } + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle partial content close and recover', async function () { + await assert.rejects(client.submit(GREMLIN_PARTIAL_CONTENT_CLOSE)); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle malformed response and recover', async function () { + await assert.rejects(client.submit(GREMLIN_MALFORMED_RESPONSE)); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle empty response body and recover', async function () { + this.timeout(10000); + await assert.rejects(client.submit(GREMLIN_EMPTY_BODY)); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle slow response', async function () { + this.timeout(30000); + const result = await client.submit(GREMLIN_SLOW_RESPONSE); + assert.ok(result.length > 0); + }); + + it.skip('should timeout when server never responds - JS driver lacks client-side idle timeout', async function () { + const shortTimeoutClient = createClient({ requestTimeout: 1000 }); + try { + await assert.rejects(shortTimeoutClient.submit(GREMLIN_NO_RESPONSE)); + const result = await shortTimeoutClient.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + } finally { + shortTimeoutClient.close(); + } + }); + + it('should handle async requests during connection close', async function () { + const p1 = client.submit(GREMLIN_CLOSE_CONNECTION); + const p2 = client.submit(GREMLIN_CLOSE_CONNECTION); + await assert.rejects(p1); + await assert.rejects(p2); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + }); + + it('should handle concurrent mixed requests', async function () { + const good = Array.from({ length: 5 }, () => client.submit(GREMLIN_SINGLE_VERTEX)); + const bad = Array.from({ length: 5 }, () => client.submit(GREMLIN_CLOSE_CONNECTION)); + const results = await Promise.allSettled([...good, ...bad]); + const fulfilled = results.filter((r) => r.status === 'fulfilled'); + const rejected = results.filter((r) => r.status === 'rejected'); + assert.ok(fulfilled.length > 0, 'Expected at least some fulfilled results'); + assert.ok(rejected.length > 0, 'Expected at least some rejected results'); + }); +}); diff --git a/gremlin-js/gremlin-javascript/test/integration/socket-server-constants.js b/gremlin-js/gremlin-javascript/test/integration/socket-server-constants.js new file mode 100644 index 0000000000..28d01ab0d3 --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/integration/socket-server-constants.js @@ -0,0 +1,10 @@ +export const PORT = 45943; +export const GREMLIN_SINGLE_VERTEX = 'server_single_vertex'; +export const GREMLIN_CLOSE_CONNECTION = 'server_close_connection'; +export const GREMLIN_VERTEX_THEN_CLOSE = 'server_vertex_then_close'; +export const GREMLIN_FAIL_AFTER_DELAY = 'server_fail_after_delay'; +export const GREMLIN_PARTIAL_CONTENT_CLOSE = 'server_partial_content_close'; +export const GREMLIN_SLOW_RESPONSE = 'server_slow_response'; +export const GREMLIN_MALFORMED_RESPONSE = 'server_malformed_response'; +export const GREMLIN_NO_RESPONSE = 'server_no_response'; +export const GREMLIN_EMPTY_BODY = 'server_empty_body'; diff --git a/gremlin-python/docker-compose.yml b/gremlin-python/docker-compose.yml index a964559979..2876bbf048 100644 --- a/gremlin-python/docker-compose.yml +++ b/gremlin-python/docker-compose.yml @@ -41,6 +41,18 @@ services: retries: 30 start_period: 30s + gremlin-socket-server-test-python: + container_name: gremlin-socket-server-test-python + image: tinkerpop/gremlin-socket-server:${GREMLIN_SERVER} + build: + context: ../ + dockerfile: gremlin-tools/gremlin-socket-server/Dockerfile + args: + - SOCKET_SERVER_DIR=gremlin-tools/gremlin-socket-server/target/ + - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} + ports: + - "45943:45943" + gremlin-python-integration-tests: container_name: gremlin-python-integration-tests image: python:${PYTHON_VERSION:-3.10} @@ -53,6 +65,7 @@ services: - DEBIAN_FRONTEND=noninteractive - GREMLIN_SERVER_URL=http://gremlin-server-test-python:{}/gremlin - GREMLIN_SERVER_BASIC_AUTH_URL=https://gremlin-server-test-python:{}/gremlin + - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-python:45943/gremlin - IO_TEST_DIRECTORY=/python_app/gremlin-test/graphbinary/ - PYTEST_ARGS=${PYTEST_ARGS:-} - RADISH_ARGS=${RADISH_ARGS:-} @@ -81,6 +94,8 @@ services: depends_on: gremlin-server-test-python: condition: service_healthy + gremlin-socket-server-test-python: + condition: service_started gremlin-python-package: container_name: gremlin-python-package diff --git a/gremlin-python/src/main/python/tests/integration/driver/socket_server_constants.py b/gremlin-python/src/main/python/tests/integration/driver/socket_server_constants.py new file mode 100644 index 0000000000..3306e5ec65 --- /dev/null +++ b/gremlin-python/src/main/python/tests/integration/driver/socket_server_constants.py @@ -0,0 +1,29 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +PORT = 45943 +GREMLIN_SINGLE_VERTEX = "server_single_vertex" +GREMLIN_CLOSE_CONNECTION = "server_close_connection" +GREMLIN_VERTEX_THEN_CLOSE = "server_vertex_then_close" +GREMLIN_FAIL_AFTER_DELAY = "server_fail_after_delay" +GREMLIN_PARTIAL_CONTENT_CLOSE = "server_partial_content_close" +GREMLIN_SLOW_RESPONSE = "server_slow_response" +GREMLIN_MALFORMED_RESPONSE = "server_malformed_response" +GREMLIN_NO_RESPONSE = "server_no_response" +GREMLIN_EMPTY_BODY = "server_empty_body" diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py b/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py new file mode 100644 index 0000000000..2534763675 --- /dev/null +++ b/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py @@ -0,0 +1,185 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +import os +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import pytest + +from gremlin_python.driver.client import Client +from gremlin_python.driver.serializer import GraphBinarySerializersV4 + +from .socket_server_constants import ( + PORT, + GREMLIN_SINGLE_VERTEX, + GREMLIN_CLOSE_CONNECTION, + GREMLIN_VERTEX_THEN_CLOSE, + GREMLIN_FAIL_AFTER_DELAY, + GREMLIN_PARTIAL_CONTENT_CLOSE, + GREMLIN_SLOW_RESPONSE, + GREMLIN_MALFORMED_RESPONSE, + GREMLIN_NO_RESPONSE, + GREMLIN_EMPTY_BODY, +) + +url = os.environ.get('GREMLIN_SOCKET_SERVER_URL', 'http://localhost:{}/gremlin'.format(PORT)) + + [email protected](scope="module") +def socket_server_client(): + try: + client = Client(url, 'g') + # Verify connectivity + client.submit(GREMLIN_SINGLE_VERTEX).all().result() + except Exception: + pytest.skip("Socket server is not available at {}".format(url)) + yield client + client.close() + + [email protected] +def fresh_client(): + client = Client(url, 'g') + yield client + client.close() + + +def test_should_receive_single_vertex(socket_server_client): + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_server_closing_connection_before_response(socket_server_client): + with pytest.raises(Exception): + socket_server_client.submit(GREMLIN_CLOSE_CONNECTION).all().result() + + # Recovery + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_server_closing_connection_after_response(socket_server_client): + result = socket_server_client.submit(GREMLIN_VERTEX_THEN_CLOSE).all().result() + assert len(result) >= 1 + + time.sleep(3) + + # Recovery + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_server_error_after_delay(socket_server_client): + with pytest.raises(Exception) as exc_info: + socket_server_client.submit(GREMLIN_FAIL_AFTER_DELAY).all().result() + assert exc_info.value is not None + + +def test_should_handle_partial_content_close(socket_server_client): + with pytest.raises(Exception): + socket_server_client.submit(GREMLIN_PARTIAL_CONTENT_CLOSE).all().result() + + # Recovery + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_malformed_response(socket_server_client): + with pytest.raises(Exception): + socket_server_client.submit(GREMLIN_MALFORMED_RESPONSE).all().result() + + # Recovery + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_empty_response_body(fresh_client): + # An empty HTTP response body should surface as an error rather than hang. + with pytest.raises(Exception): + fresh_client.submit(GREMLIN_EMPTY_BODY).all().result() + + # NOTE: Unlike the Java driver, the Python (aiohttp) driver does not recover + # on the same client after an empty response body - the half-closed connection + # is not evicted from the pool and a subsequent request fails with + # 'Cannot write to closing transport'. This driver gap is flagged in the + # cross-GLV error-message audit (tinkerpop-8lw.6) for further consideration. + + +def test_should_handle_slow_response(socket_server_client): + result = socket_server_client.submit(GREMLIN_SLOW_RESPONSE).all().result() + assert len(result) >= 1 + + +def test_should_timeout_when_server_never_responds(fresh_client): + with pytest.raises(Exception): + fresh_client.submit(GREMLIN_NO_RESPONSE).all().result(timeout=3) + + # Recovery with a new client + recovery_client = Client(url, 'g') + try: + result = recovery_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + finally: + recovery_client.close() + + +def test_should_handle_async_requests_during_connection_close(socket_server_client): + future1 = socket_server_client.submit_async(GREMLIN_CLOSE_CONNECTION) + future2 = socket_server_client.submit_async(GREMLIN_CLOSE_CONNECTION) + + for future in [future1, future2]: + try: + future.result().all().result() + except Exception: + pass + + # Recovery + result = socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + +def test_should_handle_concurrent_mixed_requests(socket_server_client): + vertex_results = [] + close_errors = [] + + def submit_vertex(): + return socket_server_client.submit(GREMLIN_SINGLE_VERTEX).all().result() + + def submit_close(): + return socket_server_client.submit(GREMLIN_CLOSE_CONNECTION).all().result() + + with ThreadPoolExecutor(max_workers=10) as executor: + vertex_futures = [executor.submit(submit_vertex) for _ in range(5)] + close_futures = [executor.submit(submit_close) for _ in range(5)] + + for f in as_completed(vertex_futures): + try: + vertex_results.append(f.result()) + except Exception: + pass + + for f in as_completed(close_futures): + try: + f.result() + except Exception: + close_errors.append(True) + + assert len(vertex_results) == 5 + assert len(close_errors) == 5 diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_web_socket_client_behavior.py b/gremlin-python/src/main/python/tests/integration/driver/test_web_socket_client_behavior.py deleted file mode 100644 index ae19bcc5f7..0000000000 --- a/gremlin-python/src/main/python/tests/integration/driver/test_web_socket_client_behavior.py +++ /dev/null @@ -1,100 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -__author__ = 'Cole Greer ([email protected])' - - -import re -import operator -from functools import reduce - -import pytest -from gremlin_python.driver import useragent - - -# TODO: remove or modify after implementing equivalent support in HTTP server -# Note: This test demonstrates different behavior in response to a server sending a close frame than the other GLV's. -# Other GLV's will respond to this by trying to reconnect. This test is also demonstrating incorrect behavior of -# client.is_closed() as it appears unaware that the event loop is dead. -# These differences from other GLV's are being tracked in [TINKERPOP-2846]. If this behavior is changed to resemble -# other GLV's, this test should be updated to show a vertex is being received by the second request. [email protected](reason="not implemented in HTTP & need to check on server side") -def test_does_not_create_new_connection_if_closed_by_server(socket_server_client, socket_server_settings): - try: - socket_server_client.submit( - "1", request_options={'requestId': socket_server_settings["CLOSE_CONNECTION_REQUEST_ID"]}).all().result() - except RuntimeError as err: - assert str(err) == "Connection was closed by server." - - assert not socket_server_client.is_closed() - - try: - response = socket_server_client.submit( - "1", request_options={'requestId': socket_server_settings["SINGLE_VERTEX_REQUEST_ID"]}).all().result() - except RuntimeError as err: - assert str(err) == "Event loop is closed" - - assert not socket_server_client.is_closed() - - -# Tests that client is correctly sending user agent during web socket handshake by having the server return -# the captured user agent. [email protected](reason="not implemented in HTTP & need to check on server side") -def test_should_include_user_agent_in_handshake_request(socket_server_client, socket_server_settings): - user_agent_response = socket_server_client.submit( - "1", request_options={'requestId': socket_server_settings["USER_AGENT_REQUEST_ID"]}).one()[0] - - assert user_agent_response == useragent.userAgent - - -# Tests that no user agent (other than the default one provided by aiohttp) is sent to server when that -# behaviour is disabled. [email protected](reason="not implemented in HTTP & need to check on server side") -def test_should_not_include_user_agent_in_handshake_request_if_disabled(socket_server_client_no_user_agent, - socket_server_settings): - user_agent_response = socket_server_client_no_user_agent.submit( - "1", request_options={'requestId': socket_server_settings["USER_AGENT_REQUEST_ID"]}).one()[0] - - # If the gremlin user agent is disabled, the underlying web socket library reverts to sending its default user agent - # during connection requests. - assert re.search("^Python/\d+(\.\d+)* aiohttp/\d+(\.\d+)*", user_agent_response) - -# Tests that client is correctly sending all overridable per request settings (requestId, batchSize, -# evaluationTimeout, and userAgent) to the server. [email protected](reason="not implemented in HTTP & need to check on server side") -def test_should_send_per_request_settings_to_server(socket_server_client, socket_server_settings): - - result = socket_server_client.submit( - "1", request_options={ - 'requestId': socket_server_settings["PER_REQUEST_SETTINGS_REQUEST_ID"], - 'evaluationTimeout': 1234, - 'batchSize': 12, - 'userAgent': "helloWorld", - 'materializeProperties': "tokens" - }).all().result() - - expected_result = "requestId={} evaluationTimeout={}, batchSize={}, userAgent={}, materializeProperties={}".format( - socket_server_settings["PER_REQUEST_SETTINGS_REQUEST_ID"], 1234, 12, "helloWorld", "tokens" - ) - - # Socket Server is sending a simple string response which after being serialized in and out of graphBinary, - # becomes a list of length 1 strings. This operation folds the list back to a single string for comparison. - result = reduce(operator.add, result) - - assert result == expected_result
