-
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: Re-create sessions that have been invalidated by the server #4734
Changes from all commits
41dc280
c443e82
cfe198d
3f9a82b
d4863e4
b326110
4dab5c5
97cb3cc
466dd56
c9e2030
64bbd91
27ebed5
f151231
4976f55
f74cdb9
bcd474b
8b98662
f8d8075
6df4f2b
d4717a0
054bbfc
55ec5ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,9 @@ | |
package com.google.cloud.spanner; | ||
|
||
import com.google.cloud.Timestamp; | ||
import com.google.cloud.spanner.SessionPool.PooledSession; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.base.Function; | ||
import com.google.common.util.concurrent.ListenableFuture; | ||
import io.opencensus.common.Scope; | ||
import io.opencensus.trace.Span; | ||
|
@@ -33,17 +36,33 @@ class DatabaseClientImpl implements DatabaseClient { | |
TraceUtil.exportSpans(READ_WRITE_TRANSACTION, READ_ONLY_TRANSACTION, PARTITION_DML_TRANSACTION); | ||
} | ||
|
||
private final SessionPool pool; | ||
@VisibleForTesting final SessionPool pool; | ||
|
||
DatabaseClientImpl(SessionPool pool) { | ||
this.pool = pool; | ||
} | ||
|
||
@VisibleForTesting | ||
PooledSession getReadSession() { | ||
return pool.getReadSession(); | ||
} | ||
|
||
@VisibleForTesting | ||
PooledSession getReadWriteSession() { | ||
return pool.getReadWriteSession(); | ||
} | ||
|
||
@Override | ||
public Timestamp write(Iterable<Mutation> mutations) throws SpannerException { | ||
public Timestamp write(final Iterable<Mutation> mutations) throws SpannerException { | ||
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadWriteSession().write(mutations); | ||
return runWithSessionRetry( | ||
new Function<Session, Timestamp>() { | ||
@Override | ||
public Timestamp apply(Session session) { | ||
return session.write(mutations); | ||
} | ||
}); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -53,10 +72,16 @@ public Timestamp write(Iterable<Mutation> mutations) throws SpannerException { | |
} | ||
|
||
@Override | ||
public Timestamp writeAtLeastOnce(Iterable<Mutation> mutations) throws SpannerException { | ||
public Timestamp writeAtLeastOnce(final Iterable<Mutation> mutations) throws SpannerException { | ||
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadWriteSession().writeAtLeastOnce(mutations); | ||
return runWithSessionRetry( | ||
new Function<Session, Timestamp>() { | ||
@Override | ||
public Timestamp apply(Session session) { | ||
return session.writeAtLeastOnce(mutations); | ||
} | ||
}); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -69,7 +94,7 @@ public Timestamp writeAtLeastOnce(Iterable<Mutation> mutations) throws SpannerEx | |
public ReadContext singleUse() { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().singleUse(); | ||
return getReadSession().singleUse(); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -80,7 +105,7 @@ public ReadContext singleUse() { | |
public ReadContext singleUse(TimestampBound bound) { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().singleUse(bound); | ||
return getReadSession().singleUse(bound); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -91,7 +116,7 @@ public ReadContext singleUse(TimestampBound bound) { | |
public ReadOnlyTransaction singleUseReadOnlyTransaction() { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().singleUseReadOnlyTransaction(); | ||
return getReadSession().singleUseReadOnlyTransaction(); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -102,7 +127,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction() { | |
public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().singleUseReadOnlyTransaction(bound); | ||
return getReadSession().singleUseReadOnlyTransaction(bound); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -113,7 +138,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { | |
public ReadOnlyTransaction readOnlyTransaction() { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().readOnlyTransaction(); | ||
return getReadSession().readOnlyTransaction(); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -124,7 +149,7 @@ public ReadOnlyTransaction readOnlyTransaction() { | |
public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { | ||
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadSession().readOnlyTransaction(bound); | ||
return getReadSession().readOnlyTransaction(bound); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -135,7 +160,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { | |
public TransactionRunner readWriteTransaction() { | ||
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadWriteSession().readWriteTransaction(); | ||
return getReadWriteSession().readWriteTransaction(); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
|
@@ -146,24 +171,41 @@ public TransactionRunner readWriteTransaction() { | |
public TransactionManager transactionManager() { | ||
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadWriteSession().transactionManager(); | ||
return getReadWriteSession().transactionManager(); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
} | ||
} | ||
|
||
@Override | ||
public long executePartitionedUpdate(Statement stmt) { | ||
public long executePartitionedUpdate(final Statement stmt) { | ||
Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan(); | ||
try (Scope s = tracer.withSpan(span)) { | ||
return pool.getReadWriteSession().executePartitionedUpdate(stmt); | ||
return runWithSessionRetry( | ||
new Function<Session, Long>() { | ||
@Override | ||
public Long apply(Session session) { | ||
return session.executePartitionedUpdate(stmt); | ||
} | ||
}); | ||
} catch (RuntimeException e) { | ||
TraceUtil.endSpanWithFailure(span, e); | ||
throw e; | ||
} | ||
} | ||
|
||
private <T> T runWithSessionRetry(Function<Session, T> callable) { | ||
PooledSession session = getReadWriteSession(); | ||
while (true) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this respect client side timeout values set ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The short answer: It will behave the same as now, and the behavior will depend on whether the pool has a session ready to be returned or not. The There are two possible scenarios:
Both of the above will not be considered a |
||
try { | ||
return callable.apply(session); | ||
} catch (SessionNotFoundException e) { | ||
session = pool.replaceReadWriteSession(e, session); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following up from my last comment, if the session is READ, is this correct, as replaceReadWriteSession eventually returns 'getReadWriteSession()'? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, it wasn't right, even though the method was never called with READ. But that was not correct either, as |
||
} | ||
} | ||
} | ||
|
||
ListenableFuture<Void> closeAsync() { | ||
return pool.closeAsync(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright 2019 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.google.cloud.spanner; | ||
|
||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Exception thrown by Cloud Spanner when an operation detects that the session that is being used | ||
* is no longer valid. This type of error has its own subclass as it is a condition that should | ||
* normally be hidden from the user, and the client library should try to fix this internally. | ||
*/ | ||
public class SessionNotFoundException extends SpannerException { | ||
private static final long serialVersionUID = -6395746612598975751L; | ||
|
||
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ | ||
SessionNotFoundException( | ||
DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) { | ||
super(token, ErrorCode.NOT_FOUND, false, message, cause); | ||
} | ||
} |
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.
curious whats the behavior with read sessions seeing SNF errors ?
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 SNF is detected at the first actual database request. For read-only transactions (both single use and actual read transactions), that is at the first query. The query is sent to the server at the first call to ResultSet#next(), so the SNF for a read transaction is handled here:
google-cloud-java/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
Line 129 in 1b61e28