Skip to content

Commit e036421

Browse files
authored
fix(spanner): avoid double decrement in session counting (#13395)
1 parent 40e33f6 commit e036421

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

spanner/session.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,9 @@ func (s *session) recycle() {
393393
// s is rejected by its home session pool because it expired and the
394394
// session pool currently has enough open sessions.
395395
s.pool.mu.Unlock()
396+
// s.destroy internally calls decNumInUseLocked.
396397
s.destroy(false, true)
397-
s.pool.mu.Lock()
398+
return
398399
}
399400
s.pool.decNumInUseLocked(context.Background())
400401
s.pool.mu.Unlock()
@@ -1367,6 +1368,7 @@ func (p *sessionPool) remove(s *session, isExpire bool, wasInUse bool) bool {
13671368
}
13681369
p.mu.Lock()
13691370
defer p.mu.Unlock()
1371+
13701372
if isExpire && (p.numOpened <= p.MinOpened || s.getIdleList() == nil) {
13711373
// Don't expire session if the session is not in idle list (in use), or
13721374
// if number of open sessions is going below p.MinOpened.

spanner/session_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,3 +2134,90 @@ func getSessionsPerChannel(sp *sessionPool) map[string]int {
21342134
}
21352135
return sessionsPerChannel
21362136
}
2137+
2138+
func TestSessionRecycleAfterPoolClose_NoDoubleDecrement(t *testing.T) {
2139+
t.Parallel()
2140+
ctx := t.Context()
2141+
var buf bytes.Buffer
2142+
logger := log.New(&buf, "", 0)
2143+
2144+
_, client, teardown := setupMockedTestServerWithConfig(t, ClientConfig{
2145+
Logger: logger,
2146+
SessionPoolConfig: SessionPoolConfig{
2147+
MinOpened: 1,
2148+
MaxOpened: 1,
2149+
},
2150+
DisableNativeMetrics: true,
2151+
})
2152+
defer teardown()
2153+
2154+
sp := client.idleSessions
2155+
2156+
// Take a session
2157+
sh, err := sp.take(ctx)
2158+
if err != nil {
2159+
t.Fatalf("failed to take session: %v", err)
2160+
}
2161+
2162+
// Simulate the race condition where the pool is closed but the session is still valid.
2163+
// We cannot use client.Close() because it invalidates all sessions in the health check queue.
2164+
// Instead, we manually set the pool to invalid.
2165+
sp.mu.Lock()
2166+
sp.valid = false
2167+
sp.mu.Unlock()
2168+
2169+
sh.recycle()
2170+
2171+
// Check logs for the error message
2172+
logOutput := buf.String()
2173+
if strings.Contains(logOutput, "Number of sessions in use is negative") {
2174+
t.Errorf("Unexpected error \"Number of sessions in use is negative\" logged: %s", logOutput)
2175+
}
2176+
}
2177+
2178+
func TestSessionRecycle_AlreadyInvalidSession(t *testing.T) {
2179+
t.Parallel()
2180+
ctx := t.Context()
2181+
var buf bytes.Buffer
2182+
logger := log.New(&buf, "", 0)
2183+
2184+
_, client, teardown := setupMockedTestServerWithConfig(t, ClientConfig{
2185+
DisableNativeMetrics: true,
2186+
Logger: logger,
2187+
SessionPoolConfig: SessionPoolConfig{
2188+
MinOpened: 1,
2189+
MaxOpened: 10,
2190+
},
2191+
})
2192+
defer teardown()
2193+
2194+
sp := client.idleSessions
2195+
sh, err := sp.take(ctx)
2196+
if err != nil {
2197+
t.Fatalf("failed to take session: %v", err)
2198+
}
2199+
2200+
// Manually invalidate the session to simulate it being closed/expired by another process.
2201+
// We need to access the underlying session.
2202+
s := sh.session
2203+
2204+
// Calling destroy() on the session will invalidate it and decrement numInUse.
2205+
// We want to simulate the case where it IS already invalid when recycle is called.
2206+
// So we call destroy first.
2207+
s.destroy(false, true)
2208+
2209+
// Now s is invalid.
2210+
// And numInUse has been decremented once.
2211+
2212+
sh.recycle()
2213+
2214+
// If the bug exists (double decrement), numInUse will be decremented AGAIN.
2215+
// Since we started with 1 session in use, destroy made it 0.
2216+
// Recycle would make it -1 (which triggers the log).
2217+
2218+
// Check logs for the error message
2219+
logOutput := buf.String()
2220+
if strings.Contains(logOutput, "Number of sessions in use is negative") {
2221+
t.Errorf("Unexpected error \"Number of sessions in use is negative\" logged: %s", logOutput)
2222+
}
2223+
}

0 commit comments

Comments
 (0)