Skip to content

refactor(Spanner): Spanner.Data depends on MUX types directly#15535

Open
amanda-tarafa wants to merge 4 commits intogoogleapis:mainfrom
amanda-tarafa:spanner-data-to-mux
Open

refactor(Spanner): Spanner.Data depends on MUX types directly#15535
amanda-tarafa wants to merge 4 commits intogoogleapis:mainfrom
amanda-tarafa:spanner-data-to-mux

Conversation

@amanda-tarafa
Copy link
Copy Markdown
Contributor

No description provided.

@amanda-tarafa amanda-tarafa requested a review from a team as a code owner March 25, 2026 21:52
@product-auto-label product-auto-label bot added the api: spanner Issues related to the Spanner API. label Mar 25, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request undertakes a significant internal refactoring of the Google.Cloud.Spanner.Data library's session and transaction management. The changes introduce a new SessionManager component that centralizes the creation and caching of underlying SpannerClient and ManagedSession objects. This architectural shift allows higher-level components like SpannerConnection and SpannerTransaction to interact more directly with these core types, reducing complexity and paving the way for future performance and feature enhancements by aligning more closely with the underlying client library's direct interaction model.

Highlights

  • New Session Management Layer: Introduced a new SessionManager class responsible for directly managing and caching SpannerClient and ManagedSession instances, streamlining client and session acquisition.
  • Direct ManagedTransaction Usage: Refactored SpannerConnection, SpannerTransaction, EphemeralTransaction, and RetriableTransaction to directly utilize ManagedTransaction instead of PooledSession, simplifying transaction lifecycle management.
  • SessionPoolManager Integration: Updated SessionPoolManager to delegate its core responsibilities to the new SessionManager, indicating a shift in the primary session management abstraction.
  • Enhanced Transaction Creation Options: Added EffectiveTimestampBound to SpannerTransactionCreationOptions for more robust and centralized timestamp bound resolution, and refined the logic for generating transaction options.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new SessionManager to centralize the management of ManagedSession and SpannerClient instances, effectively replacing the previous PooledSession and SessionPoolManager architecture. This significant refactoring impacts how transactions are acquired, used, and disposed across the codebase, including SpannerConnection, SpannerTransaction, EphemeralTransaction, and RetriableTransaction. New tests for SessionManager have been added, and existing tests and logic have been updated to reflect the new session management approach. Review comments highlighted the need to correctly handle ValueTask with Task.Run by converting it to a Task using .AsTask(), and also suggested removing an unused using statement and a line containing only whitespace for code cleanliness.

var reader = request.ExecuteReadOrQueryStreamReader(session, callSettings);
reader.StreamClosed += delegate { session.ReleaseToPool(forceDelete: false); };
var reader = request.ExecuteReadOrQueryStreamReader(managedTransaction, callSettings);
reader.StreamClosed += delegate { _ = Task.Run(() => managedTransaction.DisposeAsync()); };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

ManagedTransaction.DisposeAsync() returns a ValueTask. To correctly run this in a background task using Task.Run, you should convert it to a Task using .AsTask(). This ensures the ValueTask is properly handled and avoids potential issues with how Task.Run handles delegates returning ValueTask.

                reader.StreamClosed += delegate { _ = Task.Run(() => managedTransaction.DisposeAsync().AsTask()); };

Comment on lines 557 to 565
case DisposeBehavior.CloseResources:
_session.ReleaseToPool(forceDelete: true);
_ = Task.Run(_transaction.DisposeAsync);
break;
case DisposeBehavior.Default:
// This is a no-op for a detached session.
// We don't have to make a distinction here.
_session.ReleaseToPool(forceDelete: false);
if (!IsShared)
{
_ = Task.Run(_transaction.DisposeAsync);
}
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

ManagedTransaction.DisposeAsync() returns a ValueTask. To correctly run this in a background task using Task.Run, you should convert it to a Task using .AsTask(). Also, Task.Run expects a delegate, so you should use a lambda expression () => .... This ensures the ValueTask is properly awaited in the background thread.

                case DisposeBehavior.CloseResources:
                    _ = Task.Run(() => _transaction.DisposeAsync().AsTask());
                    break;
                case DisposeBehavior.Default:
                    if (!IsShared)
                    {
                        _ = Task.Run(() => _transaction.DisposeAsync().AsTask());
                    }
                    break;

return null;
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This line contains only whitespace and can be removed for code cleanliness.

using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using System;
using System.CodeDom;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The using System.CodeDom; statement is not used within this file and should be removed to maintain code cleanliness.

/// </summary>
public SpannerTransactionCreationOptions WithIsDetached(bool isDetached) =>
/// <summary>
/// Returns a new instance identical to this one except for the value of <see cref="IsDetached"/>.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: Indentation is missing here.

try
{
_sessionPool = await Builder.AcquireSessionPoolAsync().ConfigureAwait(false);
_managedSession = await Builder.MaybeAcquireSessionAsync(requireSession).ConfigureAwait(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: White space.

namespace Google.Cloud.Spanner.Data
{
/// <summary>
/// EphemeralTransaction acquires and releases PooledSessions from the provided SessionPool on an as-needed basis.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: We should update this xml doc at some point not to reference SessionPools.

_spannerSettings = GaxPreconditions.CheckNotNull(spannerSettings, nameof(spannerSettings));
_spannerSettings.VersionHeaderBuilder.AppendAssemblyVersion("gccl", typeof(SessionManager));

_clientFactory = clientFactory ?? ((options, settings) => options.CreateSpannerClientAsync(settings));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not an issue with your changes, but I think CreatespannerClientAsync should really be taking a CancelationToken and passing it forward to the BuildAsync(CancellationToken) method within. Then all the async methods here could take a cancellation token and we won't have to ignore the cancellation token in the public SpannerConnection.OpenAsync(CancellationToken).

Since we already don't honor that cancellation token and all this is not part of the public surface, I'm fine with not fixing it here & now, just wanted to point it out.

/// <param name="transactionEnlister">Enlistment delegate; may be null.</param>
/// <param name="cancellationToken">Cancellation token; may be None</param>
private Task OpenAsyncImpl(Action transactionEnlister, CancellationToken cancellationToken)
/// <param name="requireSession"></param>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: doc xml is empty for require session.

/// <summary>
/// The Spanner client used by this managed session.
/// </summary>
public SpannerClient Client => _options.Client;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As another option we could make this internal and then add another public constructor to the ManagedTransaction that takes a ManagedSession Then we won't need to expose the internals of Managed session. But we would then need to add another public constructor so I can see an argument both ways. Your call.

/// <param name="callSettings">If not null, applies overrides to this RPC call.</param>
/// <returns>A <see cref="ReliableStreamReader"/> for this request.</returns>
public ReliableStreamReader ExecuteReadOrQueryStreamReader(ManagedTransaction transaction, CallSettings callSettings) =>
transaction.ExecuteReadOrQueryStreamReader(this, callSettings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should add a null check in ExecuteReadOrQueryStreamReader and in PartitionReadOrQueryAsync:

GaxPreconditions.CheckNotNull(transaction, nameof(transaction)

return await SessionManager.AcquireSessionAsync(sessionOptions).ConfigureAwait(false);
}
// If we can't acquire a session, let's make sure we acquire (and cache) the SpannerClient.
// Acquiring a session would have used the right cached SpannerClinet or would have created a new on if there was none cached.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: SpannerClinet -> SpannerClient

try
{
_sessionPool = await Builder.AcquireSessionPoolAsync().ConfigureAwait(false);
_managedSession = await Builder.MaybeAcquireSessionAsync(requireSession).ConfigureAwait(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: Extra space.


/// <inheritdoc />
public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsyncImpl(GetTransactionEnlister(), cancellationToken);
public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsyncImpl(GetTransactionEnlister(), requireSession: false);
Copy link
Copy Markdown
Contributor

@robertvoinescu-work robertvoinescu-work Apr 8, 2026

Choose a reason for hiding this comment

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

Could you provide a bit more context on this? Is there a specific reason we are deferring the session creation? Assuming the database is already specified before a connection is opened and future operations will require _managedSession anyway couldn't we just create it eagerly? Doing so would let us drop the boolean flag and simplify the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the Spanner API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants