Refactor inactive IP cleanup to use pagination

This should avoid dead many dead tuples when processing batches of
sessions to cleanup
This commit is contained in:
Quentin Gliech
2026-01-23 18:52:33 +01:00
parent b4025acc80
commit 270236cb4a
13 changed files with 177 additions and 108 deletions

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT oauth2_session_id\n FROM oauth2_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND last_active_at < $1\n LIMIT $2\n ),\n updated AS (\n UPDATE oauth2_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE oauth2_sessions.oauth2_session_id = to_update.oauth2_session_id\n RETURNING 1\n )\n SELECT COUNT(*) AS \"count!\" FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Int8"
]
},
"nullable": [
null
]
},
"hash": "4b1f70e97a155bf2a352d7f9353c535ea0cc9d3eaf2b35933477c18d8c8bcb2e"
}

View File

@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT user_session_id, last_active_at\n FROM user_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND ($1::timestamptz IS NULL OR last_active_at >= $1)\n AND last_active_at < $2\n ORDER BY last_active_at ASC\n LIMIT $3\n FOR UPDATE\n ),\n updated AS (\n UPDATE user_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE user_sessions.user_session_id = to_update.user_session_id\n RETURNING user_sessions.last_active_at\n )\n SELECT COUNT(*) AS \"count!\", MAX(last_active_at) AS last_active_at FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "last_active_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Timestamptz",
"Int8"
]
},
"nullable": [
null,
null
]
},
"hash": "535225206622b9190ccf42f7d66268818dc84c37b168ab45e582e0a727796a06"
}

View File

@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT oauth2_session_id, last_active_at\n FROM oauth2_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND ($1::timestamptz IS NULL OR last_active_at >= $1)\n AND last_active_at < $2\n ORDER BY last_active_at ASC\n LIMIT $3\n FOR UPDATE\n ),\n updated AS (\n UPDATE oauth2_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE oauth2_sessions.oauth2_session_id = to_update.oauth2_session_id\n RETURNING oauth2_sessions.last_active_at\n )\n SELECT COUNT(*) AS \"count!\", MAX(last_active_at) AS last_active_at FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "last_active_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Timestamptz",
"Int8"
]
},
"nullable": [
null,
null
]
},
"hash": "7b06e6f21c69056b526538f06f06268efd13d7af3cecb452168d514a379fec30"
}

View File

@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT compat_session_id, last_active_at\n FROM compat_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND ($1::timestamptz IS NULL OR last_active_at >= $1)\n AND last_active_at < $2\n ORDER BY last_active_at ASC\n LIMIT $3\n FOR UPDATE\n ),\n updated AS (\n UPDATE compat_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE compat_sessions.compat_session_id = to_update.compat_session_id\n RETURNING compat_sessions.last_active_at\n )\n SELECT COUNT(*) AS \"count!\", MAX(last_active_at) AS last_active_at FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "last_active_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Timestamptz",
"Int8"
]
},
"nullable": [
null,
null
]
},
"hash": "926cb81dc7931890a02c5a372aef79832e5d0748dad18ab44c6671f3196d6f60"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT user_session_id\n FROM user_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND last_active_at < $1\n LIMIT $2\n ),\n updated AS (\n UPDATE user_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE user_sessions.user_session_id = to_update.user_session_id\n RETURNING 1\n )\n SELECT COUNT(*) AS \"count!\" FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Int8"
]
},
"nullable": [
null
]
},
"hash": "bf3f6a2ac0f3371e32603489744b94dc8713a2fb2c48f8e437ed8ee2dc64f70c"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH to_update AS (\n SELECT compat_session_id\n FROM compat_sessions\n WHERE last_active_ip IS NOT NULL\n AND last_active_at IS NOT NULL\n AND last_active_at < $1\n LIMIT $2\n ),\n updated AS (\n UPDATE compat_sessions\n SET last_active_ip = NULL\n FROM to_update\n WHERE compat_sessions.compat_session_id = to_update.compat_session_id\n RETURNING 1\n )\n SELECT COUNT(*) AS \"count!\" FROM updated\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Timestamptz",
"Int8"
]
},
"nullable": [
null
]
},
"hash": "d01cb56a7b231393a0725637920afe41b64d198729237dd2cef060992ebd0233"
}

View File

@@ -764,6 +764,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
skip_all,
fields(
db.query.text,
since = since.map(tracing::field::display),
threshold = %threshold,
limit = limit,
),
@@ -771,28 +772,33 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
)]
async fn cleanup_inactive_ips(
&mut self,
since: Option<DateTime<Utc>>,
threshold: DateTime<Utc>,
limit: usize,
) -> Result<usize, Self::Error> {
let res = sqlx::query_scalar!(
) -> Result<(usize, Option<DateTime<Utc>>), Self::Error> {
let res = sqlx::query!(
r#"
WITH to_update AS (
SELECT compat_session_id
SELECT compat_session_id, last_active_at
FROM compat_sessions
WHERE last_active_ip IS NOT NULL
AND last_active_at IS NOT NULL
AND last_active_at < $1
LIMIT $2
AND ($1::timestamptz IS NULL OR last_active_at >= $1)
AND last_active_at < $2
ORDER BY last_active_at ASC
LIMIT $3
FOR UPDATE
),
updated AS (
UPDATE compat_sessions
SET last_active_ip = NULL
FROM to_update
WHERE compat_sessions.compat_session_id = to_update.compat_session_id
RETURNING 1
RETURNING compat_sessions.last_active_at
)
SELECT COUNT(*) AS "count!" FROM updated
SELECT COUNT(*) AS "count!", MAX(last_active_at) AS last_active_at FROM updated
"#,
since,
threshold,
i64::try_from(limit).unwrap_or(i64::MAX),
)
@@ -800,6 +806,9 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
.fetch_one(&mut *self.conn)
.await?;
Ok(res.try_into().unwrap_or(usize::MAX))
Ok((
res.count.try_into().unwrap_or(usize::MAX),
res.last_active_at,
))
}
}

View File

@@ -658,6 +658,7 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
skip_all,
fields(
db.query.text,
since = since.map(tracing::field::display),
threshold = %threshold,
limit = limit,
),
@@ -665,28 +666,33 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
)]
async fn cleanup_inactive_ips(
&mut self,
since: Option<DateTime<Utc>>,
threshold: DateTime<Utc>,
limit: usize,
) -> Result<usize, Self::Error> {
let res = sqlx::query_scalar!(
) -> Result<(usize, Option<DateTime<Utc>>), Self::Error> {
let res = sqlx::query!(
r#"
WITH to_update AS (
SELECT oauth2_session_id
SELECT oauth2_session_id, last_active_at
FROM oauth2_sessions
WHERE last_active_ip IS NOT NULL
AND last_active_at IS NOT NULL
AND last_active_at < $1
LIMIT $2
AND ($1::timestamptz IS NULL OR last_active_at >= $1)
AND last_active_at < $2
ORDER BY last_active_at ASC
LIMIT $3
FOR UPDATE
),
updated AS (
UPDATE oauth2_sessions
SET last_active_ip = NULL
FROM to_update
WHERE oauth2_sessions.oauth2_session_id = to_update.oauth2_session_id
RETURNING 1
RETURNING oauth2_sessions.last_active_at
)
SELECT COUNT(*) AS "count!" FROM updated
SELECT COUNT(*) AS "count!", MAX(last_active_at) AS last_active_at FROM updated
"#,
since,
threshold,
i64::try_from(limit).unwrap_or(i64::MAX),
)
@@ -694,6 +700,9 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> {
.fetch_one(&mut *self.conn)
.await?;
Ok(res.try_into().unwrap_or(usize::MAX))
Ok((
res.count.try_into().unwrap_or(usize::MAX),
res.last_active_at,
))
}
}

View File

@@ -713,6 +713,7 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> {
skip_all,
fields(
db.query.text,
since = since.map(tracing::field::display),
threshold = %threshold,
limit = limit,
),
@@ -720,28 +721,33 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> {
)]
async fn cleanup_inactive_ips(
&mut self,
since: Option<DateTime<Utc>>,
threshold: DateTime<Utc>,
limit: usize,
) -> Result<usize, Self::Error> {
let res = sqlx::query_scalar!(
) -> Result<(usize, Option<DateTime<Utc>>), Self::Error> {
let res = sqlx::query!(
r#"
WITH to_update AS (
SELECT user_session_id
SELECT user_session_id, last_active_at
FROM user_sessions
WHERE last_active_ip IS NOT NULL
AND last_active_at IS NOT NULL
AND last_active_at < $1
LIMIT $2
AND ($1::timestamptz IS NULL OR last_active_at >= $1)
AND last_active_at < $2
ORDER BY last_active_at ASC
LIMIT $3
FOR UPDATE
),
updated AS (
UPDATE user_sessions
SET last_active_ip = NULL
FROM to_update
WHERE user_sessions.user_session_id = to_update.user_session_id
RETURNING 1
RETURNING user_sessions.last_active_at
)
SELECT COUNT(*) AS "count!" FROM updated
SELECT COUNT(*) AS "count!", MAX(last_active_at) AS last_active_at FROM updated
"#,
since,
threshold,
i64::try_from(limit).unwrap_or(i64::MAX),
)
@@ -749,6 +755,9 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> {
.fetch_one(&mut *self.conn)
.await?;
Ok(res.try_into().unwrap_or(usize::MAX))
Ok((
res.count.try_into().unwrap_or(usize::MAX),
res.last_active_at,
))
}
}