-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(bigtable): fix session creation leaks #13887
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?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -175,6 +175,18 @@ private enum PoolState { | |
| @GuardedBy("poolLock") | ||
| private final SessionCreationBudget budget; | ||
|
|
||
| // Handles that reserved a session-creation budget slot in createSession() and have not yet | ||
| // released it. A slot is released exactly once: as a success when the session reaches READY | ||
| // (onSessionReady), or as a failure when the session terminates without ever becoming READY | ||
| // (onSessionClose). Tracking the reservation on the handle -- instead of inferring it from the | ||
| // close-time prevState -- is what makes the release exactly-once: a session that goes | ||
| // STARTING -> WAIT_SERVER_CLOSE (e.g. a server GO_AWAY before the open handshake completes) | ||
| // closes with prevState == WAIT_SERVER_CLOSE, which the abnormal-close branch skips, so the | ||
| // prevState == STARTING check alone would leak the slot forever. SessionHandle uses identity | ||
| // equality, so a plain HashSet keys on the handle instance. | ||
| @GuardedBy("poolLock") | ||
| private final Set<SessionHandle> sessionsHoldingBudget = new HashSet<>(); | ||
|
|
||
| private final ClientConfigurationManager configManager; | ||
| private final ClientConfigurationManager.ListenerHandle configListenerHandle; | ||
|
|
||
|
|
@@ -455,35 +467,65 @@ private void createSession(OpenParams openParams) { | |
| // Explicit create session streams in a detached context | ||
| // We don't want to propagate the rpc deadline nor the trace context | ||
| Context prevContext = Context.ROOT.attach(); | ||
| // Tracks the handle once registered so the catch below can tell whether the reservation was | ||
| // ever bound to a handle (and therefore what cleanup is required). | ||
| SessionHandle handle = null; | ||
| try { | ||
| try (Scope ignored = io.opentelemetry.context.Context.root().makeCurrent()) { | ||
|
|
||
| // Build the metadata before registering the handle so the only step between newHandle and | ||
| // session.start (whose own failures are handled by the session's abort path once the | ||
| // listener is published) is the non-throwing sessionsHoldingBudget.add. | ||
| Metadata localMd = new Metadata(); | ||
| localMd.merge(openParams.metadata()); | ||
|
|
||
| SessionStream stream = factory.createNew(); | ||
| Session session = new SessionImpl(metrics, info, sessionNum++, stream, timer); | ||
| SessionHandle handle = sessions.newHandle(session); | ||
| handle = sessions.newHandle(session); | ||
| // Bind the budget reservation made by tryReserveSession() above to this handle so it is | ||
| // released exactly once when the session becomes READY or terminates. | ||
| sessionsHoldingBudget.add(handle); | ||
|
Comment on lines
+484
to
+487
Contributor
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 Please wrap these registration steps in a poolLock.lock();
try {
handle = sessions.newHandle(session);
// Bind the budget reservation made by tryReserveSession() above to this handle so it is
// released exactly once when the session becomes READY or terminates.
sessionsHoldingBudget.add(handle);
} finally {
poolLock.unlock();
}
Contributor
Author
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. wrong. createSession is guarded by pool lock. |
||
|
|
||
| Metadata localMd = new Metadata(); | ||
| localMd.merge(openParams.metadata()); | ||
| final SessionHandle startedHandle = handle; | ||
| session.start( | ||
| openParams.request(), | ||
| localMd, | ||
| new Listener() { | ||
| @Override | ||
| public void onReady(OpenSessionResponse msg) { | ||
| SessionPoolImpl.this.onSessionReady(handle, msg); | ||
| SessionPoolImpl.this.onSessionReady(startedHandle, msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void onGoAway(GoAwayResponse msg) { | ||
| SessionPoolImpl.this.onSessionGoAway(handle, msg); | ||
| SessionPoolImpl.this.onSessionGoAway(startedHandle, msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void onClose(SessionState prevState, Status status, Metadata trailers) { | ||
| SessionPoolImpl.this.onSessionClose(handle, prevState, status, trailers); | ||
| SessionPoolImpl.this.onSessionClose(startedHandle, prevState, status, trailers); | ||
| } | ||
| }); | ||
| } | ||
| } catch (RuntimeException | Error e) { | ||
| // A synchronous failure here (e.g. factory.createNew, the SessionImpl constructor, or | ||
| // metadata merge) means no terminal session callback will ever run for this reservation, so | ||
| // the budget slot and any partially-registered handle would leak for the life of the pool. | ||
| // Release the slot as a failure and unwind the handle ourselves. Note session.start's own | ||
| // failures do NOT land here: it publishes the listener first and routes exceptions through | ||
| // the session's abort path, which fires onClose -> onSessionClose (the normal release path). | ||
| debugTagTracer.record(TelemetryConfiguration.Level.WARN, "session_create_failed"); | ||
| logger.log(Level.WARNING, "Failed to create session, releasing reserved budget", e); | ||
| // If a handle was registered it owns the reservation via the set; otherwise the reservation | ||
| // was never bound to a handle and must be released directly. | ||
| if (handle == null || sessionsHoldingBudget.remove(handle)) { | ||
| budget.onSessionCreationFailure(); | ||
| } | ||
| if (handle != null) { | ||
| sessions.removeUnstartedHandle(handle); | ||
| } | ||
|
Comment on lines
+521
to
+526
Contributor
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 operations in this catch block modify Please wrap these state updates in a poolLock.lock();
try {
if (handle == null || sessionsHoldingBudget.remove(handle)) {
budget.onSessionCreationFailure();
}
if (handle != null) {
sessions.removeUnstartedHandle(handle);
}
} finally {
poolLock.unlock();
}
Contributor
Author
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. same as above. CreateSession is already guarded by poolLock. |
||
| // Let the pool recover instead of running permanently short a session. | ||
| maybeScheduleCreateSessionRetry(); | ||
| } finally { | ||
| Context.ROOT.detach(prevContext); | ||
| } | ||
|
|
@@ -540,7 +582,11 @@ private void onSessionReady(SessionHandle handle, OpenSessionResponse ignored) { | |
| } | ||
| handle.onSessionStarted(); | ||
|
|
||
| budget.onSessionCreationSuccess(); | ||
| // Release the reservation as a success. Guarded by the set so we release exactly once even if | ||
| // onSessionReady were ever delivered more than once. | ||
| if (sessionsHoldingBudget.remove(handle)) { | ||
| budget.onSessionCreationSuccess(); | ||
| } | ||
|
|
||
| // handle pending rpcs | ||
| tryDrainPendingRpcs(); | ||
|
|
@@ -613,6 +659,21 @@ private void onSessionClose( | |
|
|
||
| poolLock.lock(); | ||
| try { | ||
| // Release the budget reservation FIRST, before any code below that can throw. This is the | ||
| // session's terminal callback, so there is no later backstop: if the reservation is not | ||
| // released here it leaks forever. In particular handle.onSessionClosed(prevState) below can | ||
| // throw (e.g. IllegalStateException on an unexpected NEW / double-CLOSED prevState), which | ||
| // would otherwise strand the slot -- the same leak this fix exists to prevent. | ||
| // | ||
| // Release as a failure if this session still holds a slot, i.e. it terminated without ever | ||
| // reaching READY; sessions that reached READY were already removed from the set in | ||
| // onSessionReady, so this never double-releases. Keying off the reservation instead of the | ||
| // close-time prevState is also what covers the STARTING -> WAIT_SERVER_CLOSE (GO_AWAY) path | ||
| // that the abnormal-close branch below skips. | ||
| if (sessionsHoldingBudget.remove(handle)) { | ||
| budget.onSessionCreationFailure(); | ||
| } | ||
|
|
||
| logger.fine( | ||
| String.format("Removing closed session from pool %s", handle.getSession().getLogName())); | ||
|
|
||
|
|
@@ -642,9 +703,8 @@ private void onSessionClose( | |
| toBeClosed = popClosableRpcs(); | ||
| } | ||
|
|
||
| if (prevState == SessionState.STARTING) { | ||
| budget.onSessionCreationFailure(); | ||
| } | ||
| // Budget release for STARTING-phase closes is handled above via sessionsHoldingBudget, | ||
| // which also covers the STARTING -> WAIT_SERVER_CLOSE (GO_AWAY) path this branch skips. | ||
|
|
||
| // TODO: backoff creating a new session when consecutive failures > max? | ||
| if (poolSizer.handleSessionClose(StatusProto.fromStatusAndTrailers(status, trailers))) { | ||
|
|
||
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.
maybe even make it 10-30 seconds