Skip to content

Commit 6646d04

Browse files
committed
chore: add PGLocks query to analyze what locks are held in pg
1 parent 6ce8bfe commit 6646d04

File tree

4 files changed

+116
-0
lines changed

4 files changed

+116
-0
lines changed

coderd/database/db.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
"context"
1313
"database/sql"
1414
"errors"
15+
"fmt"
16+
"sort"
17+
"strings"
1518
"time"
1619

1720
"github.com/jmoiron/sqlx"
@@ -28,6 +31,7 @@ type Store interface {
2831
wrapper
2932

3033
Ping(ctx context.Context) (time.Duration, error)
34+
PGLocks(ctx context.Context) (PGLocks, error)
3135
InTx(func(Store) error, *TxOptions) error
3236
}
3337

@@ -134,6 +138,98 @@ func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) {
134138
return time.Since(start), err
135139
}
136140

141+
type PGLocks []PGLock
142+
143+
func (l PGLocks) String() string {
144+
// Try to group things together by relation name.
145+
sort.Slice(l, func(i, j int) bool {
146+
return safeString(l[i].RelationName) < safeString(l[j].RelationName)
147+
})
148+
149+
var out strings.Builder
150+
for i, lock := range l {
151+
if i != 0 {
152+
out.WriteString("\n")
153+
}
154+
out.WriteString(lock.String())
155+
}
156+
return out.String()
157+
}
158+
159+
// PGLock docs see: https://www.postgresql.org/docs/current/view-pg-locks.html#VIEW-PG-LOCKS
160+
type PGLock struct {
161+
// LockType see: https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-LOCK-TABLE
162+
LockType *string `db:"locktype"`
163+
Database *string `db:"database"` // oid
164+
Relation *string `db:"relation"` // oid
165+
RelationName *string `db:"relation_name"`
166+
Page *int `db:"page"`
167+
Tuple *int `db:"tuple"`
168+
VirtualXID *string `db:"virtualxid"`
169+
TransactionID *string `db:"transactionid"` // xid
170+
ClassID *string `db:"classid"` // oid
171+
ObjID *string `db:"objid"` // oid
172+
ObjSubID *int `db:"objsubid"`
173+
VirtualTransaction *string `db:"virtualtransaction"`
174+
PID int `db:"pid"`
175+
Mode *string `db:"mode"`
176+
Granted bool `db:"granted"`
177+
FastPath *bool `db:"fastpath"`
178+
WaitStart *time.Time `db:"waitstart"`
179+
}
180+
181+
func (l PGLock) String() string {
182+
granted := "granted"
183+
if !l.Granted {
184+
granted = "waiting"
185+
}
186+
var details string
187+
switch safeString(l.LockType) {
188+
case "relation":
189+
details = ""
190+
case "page":
191+
details = fmt.Sprintf("page=%d", *l.Page)
192+
case "tuple":
193+
details = fmt.Sprintf("page=%d tuple=%d", *l.Page, *l.Tuple)
194+
case "virtualxid":
195+
details = "waiting to acquire virtual tx id lock"
196+
default:
197+
details = "???"
198+
}
199+
return fmt.Sprintf("%d-%5s [%s] %s/%s/%s: %s",
200+
l.PID,
201+
safeString(l.TransactionID),
202+
granted,
203+
safeString(l.RelationName),
204+
safeString(l.LockType),
205+
safeString(l.Mode),
206+
details,
207+
)
208+
}
209+
210+
// PGLocks returns a list of all locks in the database currently in use.
211+
func (q *sqlQuerier) PGLocks(ctx context.Context) (PGLocks, error) {
212+
rows, err := q.sdb.QueryContext(ctx, `
213+
SELECT
214+
relation::regclass AS relation_name,
215+
*
216+
FROM pg_locks;
217+
`)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
defer rows.Close()
223+
224+
var locks []PGLock
225+
err = sqlx.StructScan(rows, &locks)
226+
if err != nil {
227+
return nil, err
228+
}
229+
230+
return locks, err
231+
}
232+
137233
func DefaultTXOptions() *TxOptions {
138234
return &TxOptions{
139235
Isolation: sql.LevelDefault,
@@ -218,3 +314,10 @@ func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) er
218314
}
219315
return nil
220316
}
317+
318+
func safeString(s *string) string {
319+
if s == nil {
320+
return "<nil>"
321+
}
322+
return *s
323+
}

coderd/database/dbauthz/dbauthz.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,9 @@ func prepareSQLFilter(ctx context.Context, authorizer rbac.Authorizer, action po
602602
func (q *querier) Ping(ctx context.Context) (time.Duration, error) {
603603
return q.db.Ping(ctx)
604604
}
605+
func (q *querier) PGLocks(ctx context.Context) (database.PGLocks, error) {
606+
return q.db.PGLocks(ctx)
607+
}
605608

606609
// InTx runs the given function in a transaction.
607610
func (q *querier) InTx(function func(querier database.Store) error, txOpts *database.TxOptions) error {

coderd/database/dbmem/dbmem.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,9 @@ func newUniqueConstraintError(uc database.UniqueConstraint) *pq.Error {
338338
func (*FakeQuerier) Ping(_ context.Context) (time.Duration, error) {
339339
return 0, nil
340340
}
341+
func (*FakeQuerier) PGLocks(ctx context.Context) (database.PGLocks, error) {
342+
return []database.PGLock{}, nil
343+
}
341344

342345
func (tx *fakeTx) AcquireLock(_ context.Context, id int64) error {
343346
if _, ok := tx.FakeQuerier.locks[id]; ok {

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)