-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Spanner: Create new instance if existing Spanner is closed #5200
Conversation
@Override | ||
public Spanner getService() { | ||
Spanner spanner = super.getService(); | ||
if (spanner.isClosed()) { |
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 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()
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.
Yes, that is done by the method call ServiceOptions.createNewService()
on L311 (and on L326 for getRpc()
).
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.
Where is the object cached?
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'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;
}
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.
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?
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 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;
}
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've added an additional test case to ensure that the correct caching behavior is actually happening here:
Line 89 in 75d68e7
public void testDoNotCacheClosedSpannerInstance() { |
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.
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.
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.
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.
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 Report
@@ 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
Continue to review full report at Codecov.
|
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.
a651565
to
fb2a4db
Compare
*/ | ||
@Override | ||
public Spanner getService() { | ||
// Method is only overridden in order to supply additional documentation. |
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.
My preference here would be to instead add documentation to getService in ServiceOptions so we don't have to override this (same with getRpc).
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.
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.
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.
Oh, sorry, didn't realize that was part of google-cloud-core. Can we perhaps add documentation to shouldRefreshService?
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'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.
rpc = serviceRpcFactory.create((OptionsT) this); | ||
} | ||
return rpc; | ||
} | ||
|
||
/** | ||
* @param cachedService The currently cached service object |
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.
minor - rename to cachedRpc
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.
Changed.
.setCredentials(NoCredentials.getInstance()) | ||
.build(); | ||
// Getting a service twice should give the same instance. | ||
Spanner service1 = options.getService(); |
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.
for completeness and coverage, can you add in a few assertions like:
assertThat(spanner1.isClosed()).isFalse();
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.
Added assertions.
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) { |
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 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?
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 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.
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.