Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/862 add spanner module #865

Closed

Conversation

TwanVB
Copy link

@TwanVB TwanVB commented Apr 3, 2023

What does this PR do?

Add spanner (emulator) module:

  • Created a new module based on the template
  • Chose to use rest for internal setup of the container as to not create dependency issues when a client under test would depend upon a different version of the google spanner client Google.Cloud.Spanner.Data

Why is it important?

Other developers in need of testing locally or in CI processes against a spanner will need less code and time to set up their testcontainer. The testcontainers module set will be bigger, increasing the maturity of the suite.

Related issues

@netlify
Copy link

netlify bot commented Apr 3, 2023

Deploy Preview for testcontainers-dotnet ready!

Name Link
🔨 Latest commit 37e0c92
🔍 Latest deploy log https://app.netlify.com/sites/testcontainers-dotnet/deploys/645d7032107066000820d189
😎 Deploy Preview https://deploy-preview-865--testcontainers-dotnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

Copy link
Collaborator

@HofmeisterAn HofmeisterAn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Twan, thank you for submitting the pull request for the Testcontainers Spanner module. I appreciate the effort you've put into it so far.

However, I have some suggestions that I believe could help improve the module implementation. Specifically, I noticed that there are some parts of the implementation that are not aligned with the other modules.

Additionally, I am wondering if the code that creates instances, databases, etc. (in StartAsync(CancellationToken)) is necessary for a successful container start (running the Spanner service). It seems like we are including tasks that should be the responsibility of the Google.Cloud.Spanner.* NuGet dependencies. This relates to #865 (comment).

Could you please provide some clarification on whether this code is necessary, or if it can be removed to make the module implementation more streamlined and consistent with other Testcontainers modules?

src/Testcontainers.Spanner/.editorconfig Show resolved Hide resolved
src/Testcontainers.Spanner/SpannerBuilder.cs Outdated Show resolved Hide resolved
src/Testcontainers.Spanner/SpannerContainer.cs Outdated Show resolved Hide resolved
Comment on lines 52 to 33
{
await base.StartAsync(ct);

Environment.SetEnvironmentVariable(EnvironmentVariableEmulatorHost, $"{Hostname}:{GrpcPort}");
#pragma warning disable S5332
// warning S5332: Using http protocol is insecure. Use https instead.
// This doesn't apply here, as it is a testing setup only localhost connection encryption is not required.
_webClient.BaseAddress = new Uri($"http://{Hostname}:{RestPort}");
#pragma warning restore S5332

await CreateSpannerInstance(ct);
await CreateDatabase(ct);
}

private async Task CreateSpannerInstance(CancellationToken ct = default)
{
var resource = new
{
instanceId = _configuration.InstanceId,
instance = new
{
config = "emulator-config",
displayName = _configuration.InstanceId,
}
};
string requestUri = $"v1/projects/{_configuration.ProjectId}/instances";
string resourceDescription = "instance";

await CreateResource(requestUri, resource, resourceDescription, ct);
}

private async Task CreateDatabase(CancellationToken ct = default)
{
var resource = new
{
createStatement = $"CREATE DATABASE `{_configuration.DatabaseId}`",
};


string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases";
string resourceDescription = "database";

await CreateResource(requestUri, resource, resourceDescription, ct);
}

public async Task ExecuteDdlAsync(params string[] ddl)
{
string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases/{_configuration.DatabaseId}/ddl";
var request = new { statements = ddl };

var response = await _webClient.PatchAsJsonAsync(requestUri, request)
.ValidateAsync("update ddl");

var operation = await response.Content.ReadFromJsonAsync<SpannerOperationDto>() ??
throw new InvalidOperationException(
"Executing ddl resulted in a response with a body deserializing as SpannerOperation to null");

await AwaitOperationAsync(operation);

if (operation.Error != null)
{
throw new InvalidOperationException($"Failed to execute ddl operation with error {operation.Error}");
}
}

private async Task AwaitOperationAsync(SpannerOperationDto operation)
{
int delayInSeconds = 1;
const int maxDelayInSeconds = 10;

while (!operation.Done)
{
await Task.Delay(TimeSpan.FromSeconds(delayInSeconds));
string operationName = operation.Name;
var operationResponse = await _webClient.GetAsync($"v1/{operationName}").ValidateAsync("get operation");

operation = await operationResponse.Content.ReadFromJsonAsync<SpannerOperationDto>() ??
throw new InvalidOperationException(
$"Getting operation resulted in null deserialization of body for operation name {operationName}");
if (delayInSeconds * 2 < maxDelayInSeconds)
{
delayInSeconds *= 2;
}
}
}


private async Task CreateResource(string requestUri, object resource, string resourceDescription, CancellationToken ct = default)
{
await _webClient.PostAsJsonAsync(requestUri, resource, cancellationToken: ct)
.ValidateAsync($"create {resourceDescription}", ct: ct);
}

public void Dispose() => _webClient.Dispose();

protected override async ValueTask DisposeAsyncCore()
{
_webClient.Dispose();
await base.DisposeAsyncCore();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these configurations at all necessary to start a Spanner container? Usually, we do not include client tasks into Testcontainers. Developers should use the corresponding client dependency. It looks like Spanner has a dedicated dependency to create an instance or database?

The Google.Cloud.Spanner.Admin.Instance.V1 package should be used for Cloud Spanner instance administration, such as creating or deleting instances.

If necessary at all, can we move the container configuration to WithStartupCallback, e.g. like Couchbase? We try to configure the entire container until it is ready in the builder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was inspired by what I saw in the postgresql, where you can include a script for initial setup. In our use cases it made sense to always have this available, but for m ore general purpose i can see it malkes sense to also have the emulator running without this, so I'll remove it from startup.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was inspired by what I saw in the postgresql, where you can include a script for initial setup.

But this is part (supported) by the PostgreSQL image, right? As previously mentioned, while the intention is nice, we try to avoid incorporating client-side tasks into modules. This is because it does not make sense for us to maintain vendor-specific functionalities when there is already a suitable client that can handle them. Instead, we strive to offer best practice configurations for common test use cases.

{
await Task.Delay(TimeSpan.FromSeconds(delayInSeconds));
string operationName = operation.Name;
var operationResponse = await _webClient.GetAsync($"v1/{operationName}").ValidateAsync("get operation");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the HttpWaitStrategy to wait for a specific HTTP response.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is now also used by the execute ddl, but if you prefer not to have anything in here that can also be done with a client library, we should remove that part i think.

src/Testcontainers.Spanner/Testcontainers.Spanner.csproj Outdated Show resolved Hide resolved
@@ -0,0 +1 @@
[assembly: CollectionBehavior(DisableTestParallelization = true)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we disable parallelization?

Copy link
Author

@TwanVB TwanVB Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a nasty thing in the implementation for the spanner .net client that requires an environment variable to be set in case of using the emulator: https://cloud.google.com/spanner/docs/emulator#cs
So when running multiple tests in parrallel, they override eachother environment variable.
So this environment variable is now also set in the startup, As i intended to hide this emulator specific from the client, i want to do a little investigation if there is a better place/way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more specific the detection setting in the string will make the client require this environment variable, it will not take the one from the connection string.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that is unfortunate. Is there an upstream issue? It seems weird that we are unable to connect to multiple instances. Is it possible that we could achieve it by configuring a session pool manager (googleapis/google-cloud-dotnet#4970 (comment))?

Copy link
Author

@TwanVB TwanVB May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can see from the failing tests on ubuntu only, there is a difference in how the dependency on the environment variable is treated in linux and windows, I'll need to gain a more thorough understanding of this before completing this so I'll be trying to find resources/ threads /issues or source code reference on this first.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about utilizing an xUnit.net collection fixture instead of disabling parallelization? This creates a single Cloud Spanner instance for the tests. It would not be necessary to update the environment variable anymore.

@TwanVB TwanVB force-pushed the feature/862-add-spanner-module branch from 0e4d81b to 37e0c92 Compare May 11, 2023 22:46
@HofmeisterAn
Copy link
Collaborator

Your PR hasn't seen any recent activities, I will close it. Please, do not hesitate to reopen it again if you continue to work on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Enhancement]: Add spanner (emulator) module
2 participants