Files
coder/coderd/database/pglocks.go
Steven Masley 854044e811 chore: fix concurrent CommitQuota transactions for unrelated users/orgs (#15261)
The failure condition being fixed is `w1` and `w2` could belong
to different users, organizations, and templates and still cause a
serializable failure if run concurrently. This is because the old query 
did a `seq scan` on the `workspace_builds` table. Since that is the 
table being updated, we really want to prevent that.

So before this would fail for any 2 workspaces. Now it only fails if
`w1` and `w2` are owned by the same user and organization.
2024-11-01 11:05:49 -04:00

120 lines
3.0 KiB
Go

package database
import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"time"
"github.com/jmoiron/sqlx"
"github.com/coder/coder/v2/coderd/util/slice"
)
// PGLock docs see: https://www.postgresql.org/docs/current/view-pg-locks.html#VIEW-PG-LOCKS
type PGLock struct {
// LockType see: https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-LOCK-TABLE
LockType *string `db:"locktype"`
Database *string `db:"database"` // oid
Relation *string `db:"relation"` // oid
RelationName *string `db:"relation_name"`
Page *int `db:"page"`
Tuple *int `db:"tuple"`
VirtualXID *string `db:"virtualxid"`
TransactionID *string `db:"transactionid"` // xid
ClassID *string `db:"classid"` // oid
ObjID *string `db:"objid"` // oid
ObjSubID *int `db:"objsubid"`
VirtualTransaction *string `db:"virtualtransaction"`
PID int `db:"pid"`
Mode *string `db:"mode"`
Granted bool `db:"granted"`
FastPath *bool `db:"fastpath"`
WaitStart *time.Time `db:"waitstart"`
}
func (l PGLock) Equal(b PGLock) bool {
// Lazy, but hope this works
return reflect.DeepEqual(l, b)
}
func (l PGLock) String() string {
granted := "granted"
if !l.Granted {
granted = "waiting"
}
var details string
switch safeString(l.LockType) {
case "relation":
details = ""
case "page":
details = fmt.Sprintf("page=%d", *l.Page)
case "tuple":
details = fmt.Sprintf("page=%d tuple=%d", *l.Page, *l.Tuple)
case "virtualxid":
details = "waiting to acquire virtual tx id lock"
default:
details = "???"
}
return fmt.Sprintf("%d-%5s [%s] %s/%s/%s: %s",
l.PID,
safeString(l.TransactionID),
granted,
safeString(l.RelationName),
safeString(l.LockType),
safeString(l.Mode),
details,
)
}
// PGLocks returns a list of all locks in the database currently in use.
func (q *sqlQuerier) PGLocks(ctx context.Context) (PGLocks, error) {
rows, err := q.sdb.QueryContext(ctx, `
SELECT
relation::regclass AS relation_name,
*
FROM pg_locks;
`)
if err != nil {
return nil, err
}
defer rows.Close()
var locks []PGLock
err = sqlx.StructScan(rows, &locks)
if err != nil {
return nil, err
}
return locks, err
}
type PGLocks []PGLock
func (l PGLocks) String() string {
// Try to group things together by relation name.
sort.Slice(l, func(i, j int) bool {
return safeString(l[i].RelationName) < safeString(l[j].RelationName)
})
var out strings.Builder
for i, lock := range l {
if i != 0 {
_, _ = out.WriteString("\n")
}
_, _ = out.WriteString(lock.String())
}
return out.String()
}
// Difference returns the difference between two sets of locks.
// This is helpful to determine what changed between the two sets.
func (l PGLocks) Difference(to PGLocks) (new PGLocks, removed PGLocks) {
return slice.SymmetricDifferenceFunc(l, to, func(a, b PGLock) bool {
return a.Equal(b)
})
}