From cbb076ddf1ccb7cb1e8ddc5d5147d2e0256b91d2 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 27 Jul 2026 13:02:55 +0800 Subject: [PATCH] Blocked by card to only highlight if any query is blocked longer than 10 sec (#48292) ## Context As per PR title - we're currently showing a "danger" state for the blocked by metric card as long as there's at least query that's blocked. This may come off as too noisy in a real database scenario hence opting to fine tune this behaviour a little ## Changes involved We'll now only show the "danger" state for the blocked by metric card if any of the blocked queries are blocked for longer than 10 seconds image Otherwise will just be a default card image ## Summary by CodeRabbit - **New Features** - Added clearer blocked-query monitoring, including the longest-blocked process and duration. - Added interactive selection for the longest-blocked process. - **Bug Fixes** - Improved activity-duration tracking across active and idle-in-transaction states. - Blocked-query warnings now reflect duration thresholds rather than query count alone. - Prevented negative duration values in blocked-query metrics. --- .../DatabaseConnections.constants.ts | 2 +- .../DatabaseConnections.utils.ts | 13 +++++ .../DatabaseConnections/Overview.tsx | 52 +++++++++++++++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.constants.ts b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.constants.ts index ab07cd26b47..f54c683b4af 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.constants.ts +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.constants.ts @@ -5,7 +5,7 @@ // - Shorter threshold as it indicates a lock export const WARN_DURATION_ACTIVE_QUERY = 30 // seconds export const WARN_DURATION_IDLE_TXN = 10 // seconds - +export const WARN_DURATION_BLOCKED = 10 // seconds export const WARN_TOP_BLOCKER = 3 // If the query is blocking more than 3 queries export const QUERY_STATE_TOOLTIP = { diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts index 39316c92365..81457f80a89 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts @@ -25,3 +25,16 @@ export const getBadgeVariant = (activity: DatabaseActivity) => { if (state === 'idle in transaction' || state === 'idle in transaction (aborted)') return 'warning' return 'default' } + +// Resolves the timestamp that duration should be measured from for a given state - +// query_start while actively running, transaction_start while idle in transaction. +export const getActivityStart = (activity: DatabaseActivity) => { + if (activity.state === 'active') return activity.query_start + if ( + activity.state === 'idle in transaction' || + activity.state === 'idle in transaction (aborted)' + ) { + return activity.transaction_start + } + return null +} diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx index c7c64e60576..d8224a1ffa7 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx @@ -11,9 +11,11 @@ import { import { WARN_DURATION_ACTIVE_QUERY, + WARN_DURATION_BLOCKED, WARN_DURATION_IDLE_TXN, WARN_TOP_BLOCKER, } from './DatabaseConnections.constants' +import { getActivityStart } from './DatabaseConnections.utils' import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { useDatabaseActivityQuery, type DatabaseActivity } from '@/data/database/activity-query' @@ -42,7 +44,23 @@ export const Overview = ({ live }: OverviewProps) => { { refetchOnWindowFocus: live, refetchInterval: live ? 3000 : false } ) const activeQueries = (data ?? []).filter((x) => x.state === 'active') + const blockedQueries = (data ?? []).filter((x) => x.blocked_by.length > 0) + const warnBlockedQueries = blockedQueries.some((activity) => { + const start = getActivityStart(activity) + if (!start) return false + return dayjs().utc().diff(dayjs(start).utc(), 'second') >= WARN_DURATION_BLOCKED + }) + const longestBlockedQuery = blockedQueries.reduce<{ + activity: DatabaseActivity + duration: number + } | null>((longest, activity) => { + const start = getActivityStart(activity) + if (!start) return longest + const duration = Math.max(dayjs().utc().diff(dayjs(start).utc(), 'second'), 0) + return longest === null || duration > longest.duration ? { activity, duration } : longest + }, null) + const idleInTransactionQueries = (data ?? []).filter((x) => { const isIdleInTransaction = x.state === 'idle in transaction' || x.state === 'idle in transaction (aborted)' @@ -53,7 +71,7 @@ export const Overview = ({ live }: OverviewProps) => { const longestRunningQuery = (data ?? []) .filter((x) => LONG_RUNNING_STATES.includes(x.state)) .reduce<{ activity: DatabaseActivity; duration: number } | null>((longest, activity) => { - const start = activity.state === 'active' ? activity.query_start : activity.transaction_start + const start = getActivityStart(activity) if (!start) return longest const duration = Math.max(dayjs().utc().diff(dayjs(start).utc(), 'second'), 0) return longest === null || duration > longest.duration ? { activity, duration } : longest @@ -182,11 +200,11 @@ export const Overview = ({ live }: OverviewProps) => {

@@ -204,8 +222,32 @@ export const Overview = ({ live }: OverviewProps) => { - - {blockedQueries.length} + + {blockedQueries.length} + {warnBlockedQueries && longestBlockedQuery && ( + <> + ยท + setSelectedPid(longestBlockedQuery.activity.pid)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setSelectedPid(longestBlockedQuery.activity.pid) + } + }} + > + PID {longestBlockedQuery.activity.pid} blocked for{' '} + + {formatDuration(longestBlockedQuery.duration * 1000, 0)} + + + + )}