blob: 43dbb74243f82edc6eb91cfa2be29a538bf98f07 [file]
// 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
import (
"context"
"database/sql"
"fmt"
"strings"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/database"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/stdlib"
)
// UpdateSearchDocumentsImportedByCount updates imported_by_count and
// imported_by_count_updated_at.
//
// It does so by completely recalculating the imported-by counts
// from the imports_unique table.
//
// UpdateSearchDocumentsImportedByCount returns the number of rows updated.
func (db *DB) UpdateSearchDocumentsImportedByCount(ctx context.Context, batchSize int) (nUpdated int64, err error) {
defer derrors.WrapStack(&err, "UpdateSearchDocumentsImportedByCount(ctx)")
log.Infof(ctx, "updating imported-by counts, batch size = %d", batchSize)
curCounts, curModCounts, err := db.getSearchPackages(ctx)
if err != nil {
return 0, err
}
newCounts, newModCounts, err := db.computeImportedByCounts(ctx, curCounts)
if err != nil {
return 0, err
}
// Include only changed counts for packages that are in search_documents.
changedCounts := computeChangedCounts(ctx, "packages", curCounts, newCounts)
changedModCounts := computeChangedCounts(ctx, "modules", curModCounts, newModCounts)
return db.UpdateSearchDocumentsImportedByCountWithCounts(ctx, changedCounts, changedModCounts, batchSize)
}
// getSearchPackages returns the set of package paths that are in the search_documents table,
// along with their current imported-by count and imported-by-module count.
func (db *DB) getSearchPackages(ctx context.Context) (counts, modcounts map[string]int, err error) {
defer derrors.WrapStack(&err, "DB.getSearchPackages(ctx)")
defer internal.RequestState(ctx, "reading search_packages table")()
counts = map[string]int{}
modcounts = map[string]int{}
err = db.db.RunQuery(ctx, `
SELECT package_path, imported_by_count, imported_by_module_count
FROM search_documents
`, func(rows *sql.Rows) error {
var (
p string
c, mc int
)
if err := rows.Scan(&p, &c, &mc); err != nil {
return err
}
counts[p] = c
modcounts[p] = mc
return nil
})
if err != nil {
return nil, nil, err
}
return counts, modcounts, nil
}
func (db *DB) computeImportedByCounts(ctx context.Context, curCounts map[string]int) (newCounts, newModCounts map[string]int, err error) {
defer derrors.WrapStack(&err, "db.computeImportedByCounts(ctx)")
defer internal.RequestState(ctx, "computing counts")()
newCounts = map[string]int{}
// We don't want to double-count modules. so keep a set
// from to_path to from_module_path.
modSets := map[string]map[string]struct{}{}
// Get all (from_path, to_path) pairs, deduped.
// Also get the from_path's module path.
err = db.db.RunQuery(ctx, `
SELECT DISTINCT from_path, from_module_path, to_path
FROM imports_unique
`, func(rows *sql.Rows) error {
var from, fromMod, to string
if err := rows.Scan(&from, &fromMod, &to); err != nil {
return err
}
// Don't count an importer if it's not in search_documents.
if _, ok := curCounts[from]; !ok {
return nil
}
// Count an importing module even if it's the same module as the package itself.
// This lets us distinguish packages that are truly unused from those that are only used
// within their module.
m := modSets[to]
if m == nil {
m = map[string]struct{}{}
modSets[to] = m
}
m[fromMod] = struct{}{}
// Don't count an importing package if it's in the same module as what it's importing.
// Unlike with modules, there is too much opportunity to inflate the count.
// Approximate that check by seeing if from_module_path is a prefix of to_path.
// (In some cases, e.g. when to_path is in a nested module, that is not correct.)
if (fromMod == stdlib.ModulePath && stdlib.Contains(to)) || strings.HasPrefix(to+"/", fromMod+"/") {
return nil
}
newCounts[to]++
return nil
})
if err != nil {
return nil, nil, err
}
newModCounts = map[string]int{}
for to, modSet := range modSets {
newModCounts[to] = len(modSet)
}
return newCounts, newModCounts, nil
}
func computeChangedCounts(ctx context.Context, prefix string, curCounts, newCounts map[string]int) map[string]int {
// Find all counts that have changed, including those that have changed to zero
// because there are no longer any importers in imports_unique.
changedCounts := map[string]int{}
for p, cc := range curCounts {
nc := newCounts[p] // nc is 0 if not present in newCounts
if cc != nc {
changedCounts[p] = nc
}
}
pct := 0
if len(curCounts) > 0 {
pct = len(changedCounts) * 100 / len(curCounts)
}
log.Debugf(ctx, "update-imported-by-counts: %s: %d changed (%d%%)", prefix, len(changedCounts), pct)
return changedCounts
}
func (db *DB) UpdateSearchDocumentsImportedByCountWithCounts(ctx context.Context, pkgCounts, modCounts map[string]int, batchSize int) (nUpdated int64, err error) {
defer derrors.WrapStack(&err, "UpdateSearchDocumentsImportedByCountWithCounts")
defer internal.RequestState(ctx, "updating search_documents")()
total := len(pkgCounts) + len(modCounts)
for len(pkgCounts) > 0 {
var nu int64
err := db.db.Transact(ctx, sql.LevelDefault, func(tx *database.DB) error {
if err := insertImportedByCounts(ctx, tx, "package", pkgCounts, batchSize); err != nil {
return err
}
nu, err = updateImportedByCounts(ctx, tx, "package")
return err
})
if err != nil {
return nUpdated, err
}
nUpdated += nu
internal.RequestState(ctx, fmt.Sprintf("updating search_documents: %d/%d", nUpdated, total))
}
for len(modCounts) > 0 {
var nu int64
err := db.db.Transact(ctx, sql.LevelDefault, func(tx *database.DB) error {
if err := insertImportedByCounts(ctx, tx, "module", modCounts, batchSize); err != nil {
return err
}
nu, err = updateImportedByCounts(ctx, tx, "module")
return err
})
if err != nil {
return nUpdated, err
}
nUpdated += nu
internal.RequestState(ctx, fmt.Sprintf("updating search_documents: %d/%d", nUpdated, total))
}
return nUpdated, nil
}
// insertImportedByCounts creates a temporary table and inserts at most limit
// rows into it, where each row is a key and value from the counts map. The
// inserted keys are deleted from counts.
func insertImportedByCounts(ctx context.Context, db *database.DB, kind string, counts map[string]int, limit int) (err error) {
defer derrors.WrapStack(&err, "insertImportedByCounts(ctx, db, counts)")
tableName := "computed_" + kind + "_counts"
createTableQuery := `
CREATE TEMPORARY TABLE ` + tableName + ` (
package_path TEXT NOT NULL,
imported_by_count INTEGER NOT NULL
) ON COMMIT DROP;
`
if _, err := db.Exec(ctx, createTableQuery); err != nil {
return fmt.Errorf("CREATE TABLE: %v", err)
}
var values []any
i := 0
for p, c := range counts {
if i >= limit {
break
}
values = append(values, p, c)
delete(counts, p)
i++
}
columns := []string{"package_path", "imported_by_count"}
return db.BulkInsert(ctx, tableName, columns, values, "")
}
// updateImportedByCounts updates the imported_by_count or imported_by_module_count
// column in search_documents for every package in computed_[kind]_counts.
//
// Rows that don't change aren't updated.
//
// Note that if a package is never imported, its imported_by_count column will
// be the default (0) and its imported_by_count_updated_at column will never be set.
func updateImportedByCounts(ctx context.Context, db *database.DB, kind string) (int64, error) {
// Lock the entire table to avoid deadlock. Without the lock, the update can
// fail because module inserts are concurrently modifying rows of
// search_documents.
// See https://www.postgresql.org/docs/11/explicit-locking.html for what locks mean.
// See https://www.postgresql.org/docs/11/sql-lock.html for the LOCK
// statement, notably the paragraph beginning "If a transaction of this sort
// is going to change the data...".
var setStmt string
switch kind {
case "package":
setStmt = `
imported_by_count = c.imported_by_count,
imported_by_count_updated_at = CURRENT_TIMESTAMP`
case "module":
setStmt = `
imported_by_module_count = c.imported_by_count,
imported_by_module_count_updated_at = CURRENT_TIMESTAMP`
default:
return 0, fmt.Errorf("unknown kind %q", kind)
}
updateStmt := fmt.Sprintf(`
LOCK TABLE search_documents IN SHARE ROW EXCLUSIVE MODE;
UPDATE search_documents s
SET %s
FROM computed_%s_counts c
INNER JOIN paths p ON p.path = c.package_path
WHERE s.package_path_id = p.id;`, setStmt, kind)
n, err := db.Exec(ctx, updateStmt)
if err != nil {
return 0, fmt.Errorf("error updating imported-by counts (%s) for search documents: %v", kind, err)
}
return n, nil
}