Skip to content

Spanner: Create new instance if existing Spanner is closed - #5200

Merged
kolea2 merged 11 commits into
googleapis:masterfrom
olavloite:spanner-create-new-instance-if-close
Jul 1, 2019
Merged

Spanner: Create new instance if existing Spanner is closed#5200
kolea2 merged 11 commits into
googleapis:masterfrom
olavloite:spanner-create-new-instance-if-close

Conversation

@olavloite

Copy link
Copy Markdown
Contributor

SpannerOptions caches any Spanner instance that has been created, and hands this cached instance out to all subsequent calls to SpannerOptions.getService(). This also included closed Spanner instances. The getService() method now creates a new instance if the Spanner instance has already been closed.

@olavloite
olavloite requested a review from a teamMay 18, 2019 12:39
@googlebotgooglebot added the cla: yes This human has signed the Contributor License Agreement. label May 18, 2019
@olavloiteolavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 21, 2019
@yoshi-kokoroyoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 21, 2019
@Override
public Spanner getService() {
Spanner spanner = super.getService();
if (spanner.isClosed()) {

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.

I think that the new object needs to be cached. Is there a way to make sure that super.getService()'s object is refreshed? The same comment applies to getRpc()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes, that is done by the method call ServiceOptions.createNewService() on L311 (and on L326 for getRpc()).

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.

Where is the object cached?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's cached in the super class. The super.getService(), which is called on first line of the method, gets the cached object from the super class. If it is closed, the createNewService() method (also defined in the super class ServiceOptions) will refresh it.

The implementation of ServiceOptions.getService() looks like this:

 ...
privatetransientServiceTservice;
...
publicServiceTgetService() {
if (service == null) {
service = serviceFactory.create((OptionsT) this);
}
returnservice;
}

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.

Won't the super class will cache the closed service, hence after super.service is closed, each new call to getService() will get a new instance?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The easiest way to show what happens is by showing the code as if it was in one class (in reality it is split between the super class and sub class, the createNewService() method is in the super class). This is what is going on:

 ...
privatetransientServiceTservice;
...
@OverridepublicSpannergetService() {
Spannerspanner = super.getService();
if (spanner.isClosed()) {
spanner = createNewService();
}
returnspanner;
}
protectedServiceTcreateNewService() {
service = serviceFactory.create((OptionsT) this);
returnservice;
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've added an additional test case to ensure that the correct caching behavior is actually happening here:

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.

ugh... I reviewed the code a little closer, and debugged it. I see what you did now. it was a bit confusing.

Perhaps change this:

 @SuppressWarnings("unchecked")
public ServiceT getService() {
if (service == null) {
service = serviceFactory.create((OptionsT) this);
}
return service;
}
@SuppressWarnings("unchecked")
protected ServiceT createNewService() {
service = serviceFactory.create((OptionsT) this);
return service;
}

to

 @SuppressWarnings("unchecked")
public ServiceT getService() {
if (shouldRefreshService()) {
service = serviceFactory.create((OptionsT) this);
}
return service;
}
@SuppressWarnings("unchecked")
protected boolean shouldRefreshService() {
return service == null;
}

That would make the logic a bit less spread out and readable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That is a cleaner solution. The subclass does need access to the cached object, so I've included the cached service object to the signature of the shouldRefreshService(ServiceT) method.

@ajaaym

Copy link
Copy Markdown
Contributor

I think the design followed here is one instance of ServiceOption refer to one instance of Service, rather than treating ServiceOption as factory for Service. If service returned from serviceOptions is used after close it would throw proper error message which would direct user to use new ServiceOptions. This change would be inconsistent with the other client in this library.

@codecov

codecovBot commented Jun 14, 2019

Copy link
Copy Markdown

Codecov Report

Merging #5200 into master will decrease coverage by 4.09%.
The diff coverage is 0%.

Impacted file tree graph

@@ Coverage Diff @@## master #5200 +/- ##
============================================
- Coverage 50.82% 46.72% -4.1% - Complexity 24174 24628 +454 
============================================
Files 2271 2351 +80 Lines 229912 255993 +26081 Branches 25007 29279 +4272 ============================================
+ Hits 116845 119622 +2777 - Misses 104435 127450 +23015 - Partials 8632 8921 +289
Impacted FilesCoverage ΔComplexity Δ
...src/main/java/com/google/cloud/ServiceOptions.java40.25% <0%> (-0.36%)29 <4> (+2)
...ble/data/v2/stub/EnhancedBigtableStubSettings.java94.66% <0%> (-3.95%)17% <0%> (ø)
...a/com/google/cloud/dialogflow/v2/AgentsClient.java47.72% <0%> (-2.88%)19% <0%> (ø)
.../google/cloud/dialogflow/v2beta1/AgentsClient.java47.72% <0%> (-2.88%)19% <0%> (ø)
...om/google/cloud/bigtable/data/v2/models/Query.java66.32% <0%> (-2.05%)22% <0%> (-1%)
.../v1beta1/stub/DataLabelingServiceStubSettings.java84.42% <0%> (-1.79%)50% <0%> (+8%)
.../cloud/dialogflow/v2beta1/stub/GrpcAgentsStub.java85.64% <0%> (-1.4%)12% <0%> (ø)
...oogle/cloud/dialogflow/v2/stub/GrpcAgentsStub.java85.64% <0%> (-1.4%)12% <0%> (ø)
...com/google/cloud/dialogflow/v2/AgentsSettings.java13.72% <0%> (-1.17%)2% <0%> (ø)
...oogle/cloud/dialogflow/v2beta1/AgentsSettings.java13.72% <0%> (-1.17%)2% <0%> (ø)
... and 107 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 32de049...1ca1c92. Read the comment docs.

SpannerOptions caches any Spanner instance that has been created, and
hands this cached instance out to all subsequent calls to
SpannerOptions.getService(). This also included closed Spanner
instances. The getService() method now returns an error if the Spanner
instance has already been closed.
SpannerOptions.getService() and SpannerOptions.getRpc() should return a
new instance instead of throwing an exception if the service/rpc object
has been closed.
@olavloite
olavloiteforce-pushed the spanner-create-new-instance-if-close branch from a651565 to fb2a4dbCompareJune 15, 2019 11:04
*/
@Override
public Spanner getService() {
// Method is only overridden in order to supply additional documentation.

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.

My preference here would be to instead add documentation to getService in ServiceOptions so we don't have to override this (same with getRpc).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That's not possible as there are Service classes other than Spanner that are not closeable and do not have an isClosed() method, which means that the behavior that is described for Spanner is only applicable to Spanner. That is probably also the reason that the default implementation is to cache the Service object indefinitely without checking its validity.

One possible generic solution could be to change the ServiceOptions class to implement the behavior of this PR for all Service classes that implement a custom interface CloseableService. All other Service classes would keep the current default behavior.

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.

Oh, sorry, didn't realize that was part of google-cloud-core. Can we perhaps add documentation to shouldRefreshService?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've added documentation to the shouldRefresh... methods.
Note that we cannot/should not rely on the documentation of these methods for our users, as these methods are protected.

@olavloiteolavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 21, 2019
@yoshi-kokoroyoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 21, 2019
@kolea2kolea2 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 24, 2019
@yoshi-kokoroyoshi-kokoro removed kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Jun 24, 2019
}

/**
* @param cachedService The currently cached service object

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.

minor - rename to cachedRpc

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed.

.setCredentials(NoCredentials.getInstance())
.build();
// Getting a service twice should give the same instance.
Spanner service1 = options.getService();

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.

for completeness and coverage, can you add in a few assertions like:
assertThat(spanner1.isClosed()).isFalse();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Added assertions.

@kolea2

Copy link
Copy Markdown
Contributor

LGTM besides a few minor comments

* @param cachedService The currently cached service object
* @return true if the currently cached service object should be refreshed.
*/
protected boolean shouldRefreshService(ServiceT cachedService) {

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.

Should this and shouldRefreshRpc be static?

It looks like we're not using anything from the instance but we're allowing for that possibility in the future. If so, should we instead provide a protected getter for the cached service/rpc?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The idea is to let these two methods be overridable. The base Service and ServiceRpc classes are not closeable and therefore only does a cachedService == null check.

The Spanner class (a subclass of Service) implements AutoCloseable and SpannerOptions therefore implements a custom check in the shouldRefreshService() method that calls Spanner#isClosed() instead of the cachedService == null check.

@kolea2
kolea2 merged commit 17cb858 into googleapis:masterJul 1, 2019
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: yesThis human has signed the Contributor License Agreement.🚨 criticalP0 critical issue. Requires immediate fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@olavloite@ajaaym@kolea2@chingor13@sduskis@googlebot@yoshi-automation@yoshi-kokoro