refactor(Spanner): Spanner.Data depends on MUX types directly#15535
refactor(Spanner): Spanner.Data depends on MUX types directly#15535amanda-tarafa wants to merge 4 commits intogoogleapis:mainfrom
Conversation
Note that Spanner.Data already used MUX only, but it did so through the session pool API interface.
Summary of ChangesHello, 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 Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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()); }; |
There was a problem hiding this comment.
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()); };| 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; |
There was a problem hiding this comment.
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; | ||
| } | ||
|
|
||
|
|
| using Google.Protobuf; | ||
| using Google.Protobuf.WellKnownTypes; | ||
| using System; | ||
| using System.CodeDom; |
| /// </summary> | ||
| public SpannerTransactionCreationOptions WithIsDetached(bool isDetached) => | ||
| /// <summary> | ||
| /// Returns a new instance identical to this one except for the value of <see cref="IsDetached"/>. |
There was a problem hiding this comment.
NIT: Indentation is missing here.
| try | ||
| { | ||
| _sessionPool = await Builder.AcquireSessionPoolAsync().ConfigureAwait(false); | ||
| _managedSession = await Builder.MaybeAcquireSessionAsync(requireSession).ConfigureAwait(false); |
There was a problem hiding this comment.
NIT: White space.
| namespace Google.Cloud.Spanner.Data | ||
| { | ||
| /// <summary> | ||
| /// EphemeralTransaction acquires and releases PooledSessions from the provided SessionPool on an as-needed basis. |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
NIT: doc xml is empty for require session.
| /// <summary> | ||
| /// The Spanner client used by this managed session. | ||
| /// </summary> | ||
| public SpannerClient Client => _options.Client; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
NIT: SpannerClinet -> SpannerClient
| try | ||
| { | ||
| _sessionPool = await Builder.AcquireSessionPoolAsync().ConfigureAwait(false); | ||
| _managedSession = await Builder.MaybeAcquireSessionAsync(requireSession).ConfigureAwait(false); |
There was a problem hiding this comment.
NIT: Extra space.
|
|
||
| /// <inheritdoc /> | ||
| public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsyncImpl(GetTransactionEnlister(), cancellationToken); | ||
| public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsyncImpl(GetTransactionEnlister(), requireSession: false); |
There was a problem hiding this comment.
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.
No description provided.