blob: d56f08c61192353f9b779d494726952d1c3d5257 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package postgres provides functionality for reading and writing to
// the postgres database.
package postgres
import (
"context"
"database/sql"
"time"
"golang.org/x/pkgsite/internal/database"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/poller"
)
type DB struct {
db *database.DB
bypassLicenseCheck bool
expoller *poller.Poller
cancel func()
}
// New returns a new postgres DB.
func New(db *database.DB) *DB {
return newdb(db, false)
}
// NewBypassingLicenseCheck returns a new postgres DB that bypasses license
// checks. That means all data will be inserted and returned for
// non-redistributable modules, packages and directories.
func NewBypassingLicenseCheck(db *database.DB) *DB {
return newdb(db, true)
}
// For testing.
var startPoller = true
func newdb(db *database.DB, bypass bool) *DB {
p := poller.New(
[]string(nil),
func(ctx context.Context) (any, error) {
return getExcludedPrefixes(ctx, db)
},
func(err error) {
log.Errorf(context.Background(), "getting excluded prefixes: %v", err)
})
ctx, cancel := context.WithCancel(context.Background())
if startPoller {
p.Poll(ctx) // Initialize the state.
p.Start(ctx, time.Minute)
}
return &DB{
db: db,
bypassLicenseCheck: bypass,
expoller: p,
cancel: cancel,
}
}
// Close closes a DB.
func (db *DB) Close() error {
db.cancel()
return db.db.Close()
}
// Underlying returns the *database.DB inside db.
func (db *DB) Underlying() *database.DB {
return db.db
}
// StalenessTimestamp returns the index timestamp of the oldest
// module that is newer than the index timestamp of the youngest module we have
// processed. That is, let T be the maximum index timestamp of all processed
// modules. Then this function return the minimum index timestamp of unprocessed
// modules that is no less than T, or an error that wraps derrors.NotFound if
// there is none.
//
// The name of the function is imprecise: there may be an older unprocessed
// module, if one newer than it has been processed.
//
// We use this function to compute a metric that is a lower bound on the time
// it takes to process a module since it appeared in the index.
func (db *DB) StalenessTimestamp(ctx context.Context) (time.Time, error) {
var ts time.Time
err := db.db.QueryRow(ctx, `
SELECT m.index_timestamp
FROM module_version_states m
CROSS JOIN (
-- the index timestamp of the youngest processed module
SELECT index_timestamp
FROM module_version_states
WHERE last_processed_at IS NOT NULL
ORDER BY 1 DESC
LIMIT 1
) yp
WHERE m.index_timestamp > yp.index_timestamp
AND last_processed_at IS NULL
ORDER BY m.index_timestamp ASC
LIMIT 1
`).Scan(&ts)
switch err {
case nil:
return ts, nil
case sql.ErrNoRows:
return time.Time{}, derrors.NotFound
default:
return time.Time{}, err
}
}
// NumUnprocessedModules returns the number of modules that need to be processed.
func (db *DB) NumUnprocessedModules(ctx context.Context) (total, new int, err error) {
defer derrors.Wrap(&err, "NumUnprocessedModules()")
err = db.db.QueryRow(ctx, `
SELECT COUNT(*) FROM module_version_states WHERE status = 0 OR status >= 500
`).Scan(&total)
if err != nil {
return 0, 0, err
}
err = db.db.QueryRow(ctx, `
SELECT COUNT(*) FROM module_version_states WHERE status = 0 OR status = 500
`).Scan(&new)
if err != nil {
return 0, 0, err
}
return total, new, nil
}
// UserInfo holds information about a DB user.
type UserInfo struct {
User string
NumTotal int // number of processes running as that user
NumWaiting int // number of that user's processes waiting for locks
}
// GetUserInfo returns information about a database user.
func (db *DB) GetUserInfo(ctx context.Context, user string) (_ *UserInfo, err error) {
defer derrors.Wrap(&err, "GetUserInfo(%q)", user)
ui := UserInfo{User: user}
// Count the total number of processes running as user.
err = db.db.QueryRow(ctx, `
SELECT COUNT(DISTINCT pid) FROM pg_stat_activity WHERE usename = $1
`, user).Scan(&ui.NumTotal)
if err != nil {
return nil, err
}
// Count the number of processes waiting for locks. Note that we can't add
// the number of processes where granted = true to the number where granted
// = false to get the total, because a process can hold one lock while
// waiting for another.
err = db.db.QueryRow(ctx, `
Select COUNT(DISTINCT l.pid)
FROM pg_locks l INNER JOIN pg_stat_activity a USING (pid)
WHERE a.usename = $1 AND NOT l.granted
`, user).Scan(&ui.NumWaiting)
if err != nil {
return nil, err
}
return &ui, nil
}