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

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

Merged

Conversation

olavloite
Copy link

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 team as a code owner May 18, 2019 12:39
@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label May 18, 2019
@olavloite olavloite requested review from kolea2 and snehashah16 May 18, 2019 12:39
@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 21, 2019
@yoshi-kokoro yoshi-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
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
Author

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
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
Author

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:

  ...
  private transient ServiceT service;
  ...
  public ServiceT getService() {
    if (service == null) {
      service = serviceFactory.create((OptionsT) this);
    }
    return service;
  }

Copy link
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
Author

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:

  ...
  private transient ServiceT service;
  ...
  @Override
  public Spanner getService() {
    Spanner spanner = super.getService();
    if (spanner.isClosed()) {
      spanner = createNewService();
    }
    return spanner;
  }

  protected ServiceT createNewService() {
    service = serviceFactory.create((OptionsT) this);
    return service;
  }

Copy link
Author

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
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
Author

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
Contributor

ajaaym commented May 24, 2019

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
Copy link

codecov bot commented Jun 14, 2019

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 Files Coverage Δ Complexity Δ
...src/main/java/com/google/cloud/ServiceOptions.java 40.25% <0%> (-0.36%) 29 <4> (+2)
...ble/data/v2/stub/EnhancedBigtableStubSettings.java 94.66% <0%> (-3.95%) 17% <0%> (ø)
...a/com/google/cloud/dialogflow/v2/AgentsClient.java 47.72% <0%> (-2.88%) 19% <0%> (ø)
.../google/cloud/dialogflow/v2beta1/AgentsClient.java 47.72% <0%> (-2.88%) 19% <0%> (ø)
...om/google/cloud/bigtable/data/v2/models/Query.java 66.32% <0%> (-2.05%) 22% <0%> (-1%)
.../v1beta1/stub/DataLabelingServiceStubSettings.java 84.42% <0%> (-1.79%) 50% <0%> (+8%)
.../cloud/dialogflow/v2beta1/stub/GrpcAgentsStub.java 85.64% <0%> (-1.4%) 12% <0%> (ø)
...oogle/cloud/dialogflow/v2/stub/GrpcAgentsStub.java 85.64% <0%> (-1.4%) 12% <0%> (ø)
...com/google/cloud/dialogflow/v2/AgentsSettings.java 13.72% <0%> (-1.17%) 2% <0%> (ø)
...oogle/cloud/dialogflow/v2beta1/AgentsSettings.java 13.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 olavloite force-pushed the spanner-create-new-instance-if-close branch from a651565 to fb2a4db Compare June 15, 2019 11:04
*/
@Override
public Spanner getService() {
// Method is only overridden in order to supply additional documentation.
Copy link
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
Author

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
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
Author

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.

@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 21, 2019
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 21, 2019
@kolea2 kolea2 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Jun 24, 2019
@yoshi-kokoro yoshi-kokoro removed kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Jun 24, 2019
rpc = serviceRpcFactory.create((OptionsT) this);
}
return rpc;
}

/**
* @param cachedService The currently cached service object
Copy link
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
Author

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
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
Author

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
Contributor

kolea2 commented Jun 25, 2019

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
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
Author

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:master Jul 1, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement. 🚨 This issue needs some love.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants