@@ -12,6 +12,9 @@ import (
12
12
"context"
13
13
"database/sql"
14
14
"errors"
15
+ "fmt"
16
+ "sort"
17
+ "strings"
15
18
"time"
16
19
17
20
"github.com/jmoiron/sqlx"
@@ -28,6 +31,7 @@ type Store interface {
28
31
wrapper
29
32
30
33
Ping (ctx context.Context ) (time.Duration , error )
34
+ PGLocks (ctx context.Context ) (PGLocks , error )
31
35
InTx (func (Store ) error , * TxOptions ) error
32
36
}
33
37
@@ -134,6 +138,98 @@ func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) {
134
138
return time .Since (start ), err
135
139
}
136
140
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
+
137
233
func DefaultTXOptions () * TxOptions {
138
234
return & TxOptions {
139
235
Isolation : sql .LevelDefault ,
@@ -218,3 +314,10 @@ func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) er
218
314
}
219
315
return nil
220
316
}
317
+
318
+ func safeString (s * string ) string {
319
+ if s == nil {
320
+ return "<nil>"
321
+ }
322
+ return * s
323
+ }
0 commit comments