Skip to content

Commit 048e29d

Browse files
committed
sql/pgwire: avoid frequent updates to last_login_time
This patch adds a small LRU cache to avoid updating the estimated_last_login_time overly frequently for a single user. The cache will cause us to skip the update if the user's login time was last updated within the past 10 minutes. This requires us to update the tests to use a different user, since the existing test drops `testuser` and recreates, which means that the last_login_time will not be updated for that user as it's still in the LRU cache. Release note: None
1 parent 02b17cb commit 048e29d

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

pkg/sql/logictest/testdata/logic_test/user

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@ onlyif config local-mixed-25.2
240240
statement ok
241241
DROP user testuser
242242

243-
onlyif config local-mixed-25.2
244243
user testuser nodeidx=0 newsession
245244

245+
onlyif config local-mixed-25.2
246246
statement error pq: password authentication failed for user testuser
247247
SHOW session_user
248248

@@ -259,7 +259,6 @@ skipif config local-mixed-25.2
259259
statement ok
260260
DROP user testuser
261261

262-
skipif config local-mixed-25.2
263262
user testuser nodeidx=0 newsession
264263

265264
# The logictest suite currently doesn't work for external authentication methods
@@ -271,39 +270,39 @@ skipif config local-mixed-25.2
271270
statement error pq: user identity unknown for identity provider
272271
SHOW session_user
273272

274-
subtest end
275-
276-
subtest validate_estimated_last_login_time
277-
278273
user root
279274

280275
statement ok
281276
CREATE user IF NOT EXISTS testuser
282277

283-
user testuser
278+
subtest end
279+
280+
subtest validate_estimated_last_login_time
281+
282+
user testuser2
284283

285284
# Since the logictest framework does not connect with the user until a command
286285
# is executed, we need to run a command before checking estimated_last_login_time.
287286
query T
288287
SHOW session_user
289288
----
290-
testuser
289+
testuser2
291290

292291
user root
293292

294293
# The estimated_last_login_time is not guaranteed to be populated synchronously,
295294
# so we poll until testuser's entry was updated with the last login time.
296295
skipif config local-mixed-25.2
297296
query I retry
298-
SELECT count(*) FROM system.users WHERE estimated_last_login_time IS NOT NULL AND username = 'testuser'
297+
SELECT count(*) FROM system.users WHERE estimated_last_login_time IS NOT NULL AND username = 'testuser2'
299298
----
300299
1
301300

302301
user root
303302

304303
onlyif config local-mixed-25.2
305304
statement error pq: column "estimated_last_login_time" does not exist
306-
select estimated_last_login_time from [SHOW USERS] where username = 'testuser';
305+
select estimated_last_login_time from [SHOW USERS] where username = 'testuser2';
307306

308307
subtest end
309308

pkg/sql/pgwire/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ go_library(
6464
"//pkg/sql/sqltelemetry",
6565
"//pkg/sql/types",
6666
"//pkg/util",
67+
"//pkg/util/cache",
6768
"//pkg/util/ctxlog",
6869
"//pkg/util/duration",
6970
"//pkg/util/envutil",

pkg/sql/pgwire/last_login_updater.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,29 @@ package pgwire
77

88
import (
99
"context"
10+
"time"
1011

1112
"github.com/cockroachdb/cockroach/pkg/security/username"
1213
"github.com/cockroachdb/cockroach/pkg/sql"
14+
"github.com/cockroachdb/cockroach/pkg/util/cache"
1315
"github.com/cockroachdb/cockroach/pkg/util/log"
1416
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
1517
"github.com/cockroachdb/cockroach/pkg/util/syncutil/singleflight"
18+
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
1619
)
1720

1821
// lastLoginUpdater handles updating the last login time for SQL users with
19-
// deduplication via singleflight to reduce concurrent updates.
22+
// deduplication via singleflight to reduce concurrent updates. It also uses
23+
// an LRU cache so that a user never is updated more than once every 10 minutes.
2024
type lastLoginUpdater struct {
2125
// group ensures that there is at most one last login time update
2226
// in-flight at any given time.
2327
group *singleflight.Group
2428
// execCfg is the executor configuration used for running update operations.
2529
execCfg *sql.ExecutorConfig
30+
// lastUpdateCache tracks the last time each user's login time was updated
31+
// to avoid redundant database updates.
32+
lastUpdateCache *cache.UnorderedCache
2633

2734
mu struct {
2835
syncutil.Mutex
@@ -32,11 +39,39 @@ type lastLoginUpdater struct {
3239
}
3340
}
3441

42+
const (
43+
// lastLoginCacheSize is the maximum number of users to track in the LRU
44+
// cache.
45+
lastLoginCacheSize = 100
46+
// lastLoginEvictionTime is how long to keep entries in the cache before
47+
// evicting them.
48+
lastLoginEvictionTime = 10 * time.Minute
49+
)
50+
3551
// newLastLoginUpdater creates a new lastLoginUpdater instance.
3652
func newLastLoginUpdater(execCfg *sql.ExecutorConfig) *lastLoginUpdater {
53+
// Create cache with LRU eviction, limiting to lastLoginCacheSize users and entries older than lastLoginEvictionTime.
54+
cacheConfig := cache.Config{
55+
Policy: cache.CacheLRU,
56+
ShouldEvict: func(size int, key, value interface{}) bool {
57+
// Evict if we have more than lastLoginCacheSize entries or if the entry
58+
// is older than lastLoginEvictionTime.
59+
if size > lastLoginCacheSize {
60+
return true
61+
}
62+
lastUpdate, ok := value.(time.Time)
63+
if !ok {
64+
// Evict invalid entries also.
65+
return true
66+
}
67+
return timeutil.Since(lastUpdate) > lastLoginEvictionTime
68+
},
69+
}
70+
3771
u := &lastLoginUpdater{
38-
group: singleflight.NewGroup("update last login time", ""),
39-
execCfg: execCfg,
72+
group: singleflight.NewGroup("update last login time", ""),
73+
execCfg: execCfg,
74+
lastUpdateCache: cache.NewUnorderedCache(cacheConfig),
4075
}
4176
u.mu.pendingUsers = make(map[username.SQLUsername]struct{})
4277
return u
@@ -51,6 +86,16 @@ func (u *lastLoginUpdater) updateLastLoginTime(ctx context.Context, dbUser usern
5186
return
5287
}
5388

89+
// Check if we recently updated this user's login time.
90+
if lastUpdate, ok := u.lastUpdateCache.Get(dbUser); ok {
91+
if lastUpdateTime, ok := lastUpdate.(time.Time); ok {
92+
// Only update if it's been more than lastLoginEvictionTime since the last update.
93+
if timeutil.Since(lastUpdateTime) < lastLoginEvictionTime {
94+
return
95+
}
96+
}
97+
}
98+
5499
// Use singleflight to ensure at most one last login time update batch
55100
// is in-flight at any given time.
56101
future, leader := u.group.DoChan(ctx, "UpdateLastLoginTime",
@@ -96,10 +141,21 @@ func (u *lastLoginUpdater) processPendingUpdates(ctx context.Context) error {
96141
u.mu.pendingUsers = make(map[username.SQLUsername]struct{})
97142
u.mu.Unlock()
98143

144+
if len(users) == 0 {
145+
return nil
146+
}
147+
99148
// Update last login time for all pending users in a single query.
100149
err := sql.UpdateLastLoginTime(ctx, u.execCfg, users)
101150
if err != nil {
102151
return err
103152
}
153+
154+
// Update the cache with the current time for all successfully updated users.
155+
now := timeutil.Now()
156+
for _, user := range users {
157+
u.lastUpdateCache.Add(user, now)
158+
}
159+
104160
return nil
105161
}

0 commit comments

Comments
 (0)