Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-11 08:39:17 -05:00
pkg-proxy/internal/database/database.go
montehurd 4827cb9d76 Batch cache hit writes
Every cached download wrote its hit in its own UPDATE. SQLite runs on
one connection, so each download was a write transaction the rest
queued behind, holding a warm npm cache near 1,000 requests a second
with the CPU two thirds idle.

Hits are now counted in memory and written in one transaction every
database.hit_flush_interval, "1s" by default, "0" for a write per hit.
A failed write keeps the hits for the next flush, and Close writes
what is pending. The same test then served about 2,200 requests a
second.

Closes #322.
2026-10-01 00:13:58 -07:00

174 lines
3.5 KiB
Go

package database
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
_ "modernc.org/sqlite"
)
const SchemaVersion = 1
const dirPermissions = 0755
// Postgres connection pool limits. database/sql keeps only two idle
// connections by default, which opens a new Postgres session for almost every
// request under load.
const (
postgresMaxOpenConns = 32
postgresMaxIdleConns = 32
postgresConnMaxIdleTime = 5 * time.Minute
postgresConnMaxLifetime = 30 * time.Minute
)
type Dialect string
const (
DialectSQLite Dialect = "sqlite"
DialectPostgres Dialect = "postgres"
)
type DB struct {
*sqlx.DB
dialect Dialect
path string
hits *hitBatch
}
func (db *DB) Dialect() Dialect {
return db.dialect
}
func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func Create(path string) (*DB, error) {
if Exists(path) {
if err := os.Remove(path); err != nil {
return nil, fmt.Errorf("removing existing database: %w", err)
}
}
db, err := Open(path)
if err != nil {
return nil, err
}
if err := db.CreateSchema(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("creating schema: %w", err)
}
return db, nil
}
func Open(path string) (*DB, error) {
if dir := filepath.Dir(path); dir != "." && dir != "/" {
if err := os.MkdirAll(dir, dirPermissions); err != nil {
return nil, fmt.Errorf("creating database directory: %w", err)
}
}
// Add busy_timeout to handle concurrent writes
sqlDB, err := sqlx.Open("sqlite", path+"?_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
// Limit connections to 1 for SQLite to serialize writes
sqlDB.SetMaxOpenConns(1)
db := &DB{DB: sqlDB, dialect: DialectSQLite, path: path}
if err := db.OptimizeForReads(); err != nil {
_ = sqlDB.Close()
return nil, fmt.Errorf("optimizing database: %w", err)
}
return db, nil
}
func OpenOrCreate(path string) (*DB, error) {
if Exists(path) {
return Open(path)
}
return Create(path)
}
func OpenPostgres(url string) (*DB, error) {
sqlDB, err := sqlx.Open("postgres", url)
if err != nil {
return nil, fmt.Errorf("opening postgres database: %w", err)
}
sqlDB.SetMaxOpenConns(postgresMaxOpenConns)
sqlDB.SetMaxIdleConns(postgresMaxIdleConns)
sqlDB.SetConnMaxIdleTime(postgresConnMaxIdleTime)
sqlDB.SetConnMaxLifetime(postgresConnMaxLifetime)
if err := sqlDB.Ping(); err != nil {
_ = sqlDB.Close()
return nil, fmt.Errorf("connecting to postgres: %w", err)
}
return &DB{DB: sqlDB, dialect: DialectPostgres}, nil
}
func OpenPostgresOrCreate(url string) (*DB, error) {
db, err := OpenPostgres(url)
if err != nil {
return nil, err
}
var exists bool
err = db.Get(&exists, "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'schema_info')")
if err != nil {
_ = db.Close()
return nil, fmt.Errorf("checking schema: %w", err)
}
if !exists {
if err := db.CreateSchema(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("creating schema: %w", err)
}
}
return db, nil
}
func (db *DB) OptimizeForBulkWrites() error {
if db.dialect == DialectPostgres {
return nil
}
_, err := db.Exec(`
PRAGMA synchronous = OFF;
PRAGMA journal_mode = WAL;
PRAGMA cache_size = -64000;
`)
return err
}
func (db *DB) OptimizeForReads() error {
if db.dialect == DialectPostgres {
return nil
}
_, err := db.Exec(`
PRAGMA synchronous = NORMAL;
PRAGMA journal_mode = WAL;
`)
return err
}
func (db *DB) Path() string {
return db.path
}
func (db *DB) Rebind(query string) string {
return db.DB.Rebind(query)
}