-
Notifications
You must be signed in to change notification settings - Fork 396
feat: Multiplex session v1 implementation #15322
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
base: main
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @purva9413, 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 delivers the foundational implementation for multiplexed sessions in the Google Cloud Spanner client library. The primary goal is to optimize resource usage and enhance performance by enabling multiple transactions to operate concurrently over a single session. This involves the introduction of new abstractions for managing these sessions and their associated transactions, along with updates to the session pooling infrastructure and a thorough suite of tests to ensure stability and correctness. 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. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this 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 major new feature: multiplexed sessions. The implementation adds several new classes like ManagedSession, ManagedTransaction, and ManagedSessionOptions, and updates the SessionPoolManager to handle these new session types. The changes also include extensive refactoring of existing tests to use the new ManagedTransaction instead of PooledSession, and adds new integration and unit tests for the multiplexed session functionality.
Overall, the implementation is comprehensive and well-tested. I've identified a critical thread-safety issue in SessionPoolManager that needs to be addressed. Additionally, there are several opportunities for code cleanup and simplification, such as removing unused variables, commented-out code, and duplicate test cases, as well as improving documentation by filling in TODOs and empty XML comments. My detailed feedback is in the comments below.
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SessionPoolManager.cs
Outdated
Show resolved
Hide resolved
...ogle.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/V1/ManagedSessionTests.cs
Outdated
Show resolved
Hide resolved
...ogle.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/V1/ManagedSessionTests.cs
Outdated
Show resolved
Hide resolved
...ogle.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.IntegrationTests/V1/ManagedSessionTests.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/V1/DirectedReadTests.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSessionOptions.cs
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedTransaction.cs
Show resolved
Hide resolved
| public partial class ManagedTransaction | ||
| { | ||
| // All these fields are derived from the ManagedSession | ||
| private readonly Session _multiplexSession; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is more expressive on which type of session (multiplex vs pooled) from the backend is expected in the ManagedSession. Plus it is a private variable.
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedTransaction.cs
Outdated
Show resolved
Hide resolved
amanda-tarafa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patial review for now.
| /// TODO: Add summary for mux sessions | ||
| /// </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 :)
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
| /// <summary> | ||
| /// The name of the session. This is never null. | ||
| /// </summary> | ||
| internal SessionName SessionName => Session.SessionName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be null now, before we acquire the first session?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the SessionBuilder.BuildAsync() is the only way for the customer to create a ManagedSession. And we do not return prematurely in the sense we only return a ManagedSession object once the underlying Session of the ManagedSession is created. Does that make sense?
| public DatabaseName DatabaseName { get; } | ||
|
|
||
| /// <summary> | ||
| /// The database role of the managed session | ||
| /// </summary> | ||
| public string DatabaseRole { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arent these two on the ManagedSessionOptions already, and/or could we put them there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to keep ManagedSessionOptions as optional configurable parameters for the ManagedSession (Similar to the SessionPoolOptions). The DatabaseName and DatabaseRole are required parameters for creating a multiplex session in Spanner (so by relation also for creating a ManagedSession), and are set on the SessionBuilder initially when the customer wants to build a ManagedSession.
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
| public async Task<ManagedTransaction> CreateManagedTransactionWithOptions(TransactionOptions transactionOptions, bool singleUseTransaction) | ||
| { | ||
| await MaybeRefreshWithTimePeriodCheck().ConfigureAwait(false); | ||
| return new ManagedTransaction(this, transactionId: null, transactionOptions, singleUseTransaction, readTimestamp: null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to pass this or could we just pass _session?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a couple of things needed from the ManagedSession object when we create the ManagedTransaction. These are set in the constructor of the ManagedTransaction through the this ManagedSession object:
- The client
- The underlying session
- Configurable options from the
ManagedSession.Optionslike the RPC timeouts.
apis/Google.Cloud.Spanner.V1/Google.Cloud.Spanner.V1/ManagedSession.cs
Outdated
Show resolved
Hide resolved
6de231e to
d60cf55
Compare
chore: Remove Task.Run for creating the session
d60cf55 to
6ffc5a5
Compare
| try | ||
| { | ||
| // Non-blocking, fast check if the session is still "fresh" enough | ||
| if (_session != null && !SessionHasExpired(HardRefreshIntervalInDays)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SessionHasExpired treats the null case as expired. The null check shouldn't be necessary.
| { | ||
| // Clear the task after completion (success or failure) | ||
| // to allow the next refresh to run. | ||
| _ = Interlocked.Exchange(ref _sessionCreationTask, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: I don't think you need an interlocked exchange here since it's already executing within the context of a lock. But It should be fine, if we want to be defensive.
| /// If this is true then <see cref="TransactionOptions"/> will be <see cref="TransactionOptions.ModeOneofCase.ReadOnly"/> | ||
| /// and <see cref="TransactionId"/> will be null. | ||
| /// </summary> | ||
| internal bool SingleUseTransaction { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: How do you feel aboutIsSingleUseTransaction to make it clear it's a boolean. Otherwise I see that name and think it might be a transaction object.
| transactionId is null || TransactionOptions.ModeCase != ModeOneofCase.None, | ||
| nameof(transactionOptions), | ||
| "No transaction options were specified for the given transasaction ID."); | ||
| $"No transaction options were specified for the given transasaction ID {transactionId is null}, {transactionId?.ToBase64()}, {TransactionOptions.ModeCase}."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message would be hard to interpret without looking at the code, could you update it to something more like this:
$"No transaction options were specified. TransactionId: {transactionId?.ToBase64() ?? "null"}, Mode: {TransactionOptions.ModeCase}."
| /// <param name="callSettings">If not null, applies overrides to this RPC call.</param> | ||
| /// <returns>A task representing the asynchronous operation. When the task completes, the result is the response from the RPC.</returns> | ||
| internal Task<PartitionResponse> PartitionReadOrQueryAsync(PartitionReadOrQueryRequest request, CallSettings callSettings) | ||
| internal async Task<PartitionResponse> PartitionReadOrQueryAsync(PartitionReadOrQueryRequest request, CallSettings callSettings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add the async modifier? I think it's marginally more efficient the original way. Same question other places like RollBackAsync
| var client = await _clientFactory.Invoke(options, SpannerSettings).ConfigureAwait(false); | ||
| var managedSessionBuilder = new SessionBuilder(dbName, client) | ||
| { | ||
| Options = ManagedSessionOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ManagedSessionOptions could be null here depending on the static constructor used to create the SessionPoolmanager do we need another null check here?
| public string DatabaseRole { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The client used for all operations in this managed session. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a note here similar to DataBasename saying it is required.
| /// <summary> | ||
| /// Returns a ManagedTransaction for the same ManagedSession as this one with the given transaction related values. | ||
| /// </summary> | ||
| public async Task<ManagedTransaction> CreateManagedTransaction(ByteString transactionId, TransactionOptions transactionOptions, bool singleUseTransaction, Timestamp readTimestamp = null, CancellationToken cancellationToken = default) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The names need to be updated to CreateMangedTransactionAsync for all methods.
| /// <summary> | ||
| /// Returns a ManagedTransaction for the same ManagedSession as this one with the given transaction mode. | ||
| /// </summary> | ||
| public async Task<ManagedTransaction> CreateManagedTransaction(ByteString transactionId, ModeOneofCase transactionMode, Timestamp readTimestamp = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we take bool singleUseTransaction here also?
| return false; | ||
| } | ||
|
|
||
| private async Task CreateOrRefreshSessionsAsync(CancellationToken cancellationToken, bool needsRefresh = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a high level comment. The current code works, but how do we feel about using SemaphoreSlim instead? It provides a natural way to deal with async locking without the need to manage the lifecycle of a _sessionCreationTask.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High level it'd be like this:
private readonly SemaphoreSlim _sessionLock = new(1, 1);
private async Task TriggerRefresh(double refreshInterval, CancellationToken ct)
{
await _sessionLock.WaitAsync(ct);
try
{
if (SessionHasExpired(refreshInterval))
{
_session = await Client.CreateSessionAsync(...);
}
}
finally
{
_sessionLock.Release();
}
}
No description provided.