mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-23 06:20:48 +02:00
* shell: volume.fsck reads the filer from one store snapshot when it can volume.fsck collects the filer's chunk references by walking directory by directory, so each directory is seen at a different time. A file renamed from a directory not yet listed into one already listed is never seen, its chunks look like orphans, and -reallyDeleteFromVolume deletes live data. Checkpoint rotation and write-to-tmp-then-rename hit this. Add an optional filer store capability, EntryScanner, that visits every entry below a directory from one consistent snapshot. The SQL stores implement it with one REPEATABLE READ read-only transaction read in primary-key pages, so a concurrent rename is seen under exactly one of its names. Key-value rows are skipped by their dirhash, a bucket directory whose table is missing fails the scan (from the snapshot it cannot be told from one dropped a moment ago), and hard links are resolved in the same transaction. SQLite offers it only in WAL mode, on its own connection. TraverseBfsMetadata serves the snapshot when asked via request metadata sw-traverse-mode=snapshot and reports the mode in a response header; without the metadata it walks as before, so peer bootstrap and admin export are unchanged. Snapshot responses carry no entry_ts_ns, whose per-entry lookup would cost more than the scan. volume.fsck requests the snapshot and falls back to the walk on an older filer or a store that cannot scan. * filer, shell: fail a scan that cannot start, not fall back to the walk A transient BeginTx failure and a preliminary GetEntry error were treated like a missing capability or missing root, so fsck would take the directory walk - the very path that misses renamed-away entries. Only a genuinely unsupported scan or a confirmed-absent root may fall back. * filer: only snapshot-listed bucket names keep a missing table fatal A cached-only name, whose bucket may have been deleted between copying the cache and opening the snapshot, is skipped when its table is gone. * filer: roll back to a savepoint before skipping a cached-only table On PostgreSQL a failed statement aborts the snapshot transaction, so ignoring a missing-table error without the savepoint still left every later cached-only table unreadable. --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
//go:build (linux || darwin || windows) && sqlite
|
|
|
|
// limited GOOS due to modernc.org/libc/unistd
|
|
|
|
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer/abstract_sql"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer/mysql"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func init() {
|
|
filer.Stores = append(filer.Stores, &SqliteStore{})
|
|
}
|
|
|
|
type SqliteStore struct {
|
|
abstract_sql.AbstractSqlStore
|
|
}
|
|
|
|
func (store *SqliteStore) GetName() string {
|
|
return "sqlite"
|
|
}
|
|
|
|
// sqliteSqlGen is the MySQL generator with SQLite's own page cursor: SQLite
|
|
// seeks a row-value comparison on the primary key but scans the OR form.
|
|
type sqliteSqlGen struct {
|
|
*mysql.SqlGenMysql
|
|
}
|
|
|
|
func (gen sqliteSqlGen) GetSqlScanPage(tableName string, dirhash int64, name string, limit int) (string, []interface{}) {
|
|
return fmt.Sprintf("SELECT `dirhash`, `name`, `directory`, `meta` FROM `%s` WHERE (`dirhash`, `name`) > (?, ?) ORDER BY `dirhash` ASC, `name` ASC LIMIT ?", tableName),
|
|
[]interface{}{dirhash, name, limit}
|
|
}
|
|
|
|
// ScanEntries offers the snapshot scan only in WAL mode: with a rollback
|
|
// journal a long read transaction holds a shared lock and writers fail after
|
|
// the busy timeout.
|
|
func (store *SqliteStore) ScanEntries(ctx context.Context, dirPath util.FullPath, fn func(ctx context.Context, entry *filer.Entry) error) error {
|
|
var mode string
|
|
if err := store.DB.QueryRowContext(ctx, "PRAGMA journal_mode").Scan(&mode); err != nil {
|
|
return fmt.Errorf("%w: journal_mode: %v", filer.ErrUnsupportedEntryScan, err)
|
|
}
|
|
if !strings.EqualFold(mode, "wal") {
|
|
return fmt.Errorf("%w: sqlite journal_mode is %q, the scan needs wal", filer.ErrUnsupportedEntryScan, mode)
|
|
}
|
|
return store.AbstractSqlStore.ScanEntries(ctx, dirPath, fn)
|
|
}
|
|
|
|
func (store *SqliteStore) Initialize(configuration util.Configuration, prefix string) (err error) {
|
|
dbFile := configuration.GetString(prefix + "dbFile")
|
|
createTable := `CREATE TABLE IF NOT EXISTS "%s" (
|
|
dirhash BIGINT,
|
|
name VARCHAR(1000),
|
|
directory TEXT,
|
|
meta BLOB,
|
|
PRIMARY KEY (dirhash, name)
|
|
) WITHOUT ROWID;`
|
|
upsertQuery := `INSERT INTO "%s"(dirhash,name,directory,meta)VALUES(?,?,?,?)
|
|
ON CONFLICT(dirhash,name) DO UPDATE SET
|
|
directory=excluded.directory,
|
|
meta=excluded.meta;
|
|
`
|
|
return store.initialize(
|
|
dbFile,
|
|
createTable,
|
|
upsertQuery,
|
|
)
|
|
}
|
|
|
|
// sqliteDSN keeps the store's two pools on one database and lets a writer that
|
|
// meets the other pool's reader wait for it instead of failing outright. A bare
|
|
// :memory: is private to each connection, so it has to be named and shared.
|
|
func sqliteDSN(dbFile string) string {
|
|
dsn := dbFile
|
|
if dsn == ":memory:" {
|
|
dsn = fmt.Sprintf("file:seaweedfs%d?mode=memory&cache=shared", time.Now().UnixNano())
|
|
}
|
|
separator := "?"
|
|
if strings.Contains(dsn, "?") {
|
|
separator = "&"
|
|
}
|
|
return dsn + separator + "_pragma=busy_timeout(10000)"
|
|
}
|
|
|
|
func (store *SqliteStore) initialize(dbFile, createTable, upsertQuery string) (err error) {
|
|
|
|
store.SupportBucketTable = true
|
|
store.SqlGenerator = sqliteSqlGen{&mysql.SqlGenMysql{
|
|
CreateTableSqlTemplate: createTable,
|
|
DropTableSqlTemplate: "drop table if exists `%s`",
|
|
UpsertQueryTemplate: upsertQuery,
|
|
}}
|
|
|
|
dsn := sqliteDSN(dbFile)
|
|
|
|
db, dbErr := sql.Open("sqlite", dsn)
|
|
if dbErr != nil {
|
|
if db != nil {
|
|
db.Close()
|
|
}
|
|
return fmt.Errorf("can not connect to %s error:%v", dbFile, dbErr)
|
|
}
|
|
|
|
if err = db.Ping(); err != nil {
|
|
return fmt.Errorf("connect to %s error:%v", dbFile, err)
|
|
}
|
|
|
|
if err = store.UseConnectionPools(db, func() (*sql.DB, error) {
|
|
return sql.Open("sqlite", dsn)
|
|
}, 1, 1, 0); err != nil {
|
|
return err
|
|
}
|
|
// The main pool is one connection, which a long scan must not hold.
|
|
if store.ScanDB, err = sql.Open("sqlite", dsn); err != nil {
|
|
return fmt.Errorf("open scan connection to %s: %v", dbFile, err)
|
|
}
|
|
store.ScanDB.SetMaxOpenConns(1)
|
|
|
|
if err = store.CreateTable(context.Background(), abstract_sql.DEFAULT_TABLE); err != nil {
|
|
return fmt.Errorf("init table %s: %v", abstract_sql.DEFAULT_TABLE, err)
|
|
}
|
|
|
|
return nil
|
|
}
|