mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Concurrent bucket deletion across multiple filer replicas races on the per-bucket DROP TABLE. The first replica drops the table; the rest hit an undefined-table error (postgres 42P01, mysql 1051) which propagates out of DeleteFolderChildren and panics the filer. On restart the same pending DROP re-runs and the filer crash-loops. Make the drop idempotent. Same defect class in all SQL backends, so fix postgres, postgres2, mysql, mysql2, and sqlite together.
170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
package mysql
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer/abstract_sql"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const (
|
|
CONNECTION_URL_PATTERN = "%s:%s@tcp(%s:%d)/%s?collation=utf8mb4_bin"
|
|
)
|
|
|
|
func init() {
|
|
filer.Stores = append(filer.Stores, &MysqlStore{})
|
|
}
|
|
|
|
type MysqlStore struct {
|
|
abstract_sql.AbstractSqlStore
|
|
}
|
|
|
|
func (store *MysqlStore) GetName() string {
|
|
return "mysql"
|
|
}
|
|
|
|
func (store *MysqlStore) Initialize(configuration util.Configuration, prefix string) (err error) {
|
|
// Absent key keeps a pooled default; an explicit 0 disables the idle pool.
|
|
configuration.SetDefault(prefix+"connection_max_idle", 2)
|
|
// Default on so minimal configs avoid the duplicate-key roundtrip the
|
|
// inode-index KvPut would otherwise emit on every write.
|
|
configuration.SetDefault(prefix+"enableUpsert", true)
|
|
return store.initialize(
|
|
configuration.GetString(prefix+"dsn"),
|
|
configuration.GetString(prefix+"upsertQuery"),
|
|
configuration.GetBool(prefix+"enableUpsert"),
|
|
configuration.GetString(prefix+"username"),
|
|
configuration.GetString(prefix+"password"),
|
|
configuration.GetString(prefix+"hostname"),
|
|
configuration.GetInt(prefix+"port"),
|
|
configuration.GetString(prefix+"database"),
|
|
configuration.GetInt(prefix+"connection_max_idle"),
|
|
configuration.GetInt(prefix+"connection_max_open"),
|
|
configuration.GetInt(prefix+"connection_max_lifetime_seconds"),
|
|
configuration.GetBool(prefix+"interpolateParams"),
|
|
configuration.GetBool(prefix+"enable_tls"),
|
|
configuration.GetString(prefix+"ca_crt"),
|
|
configuration.GetString(prefix+"client_crt"),
|
|
configuration.GetString(prefix+"client_key"),
|
|
configuration.GetBool(prefix+"tls_insecure_skip_verify"),
|
|
configuration.GetString(prefix+"tls_server_name"),
|
|
)
|
|
}
|
|
|
|
func (store *MysqlStore) initialize(dsn string, upsertQuery string, enableUpsert bool, user, password, hostname string, port int, database string, maxIdle, maxOpen,
|
|
maxLifetimeSeconds int, interpolateParams bool, enableTls bool, caCrtDir string, clientCrtDir string, clientKeyDir string,
|
|
tlsInsecureSkipVerify bool, tlsServerName string) (err error) {
|
|
|
|
store.SupportBucketTable = false
|
|
if !enableUpsert {
|
|
upsertQuery = ""
|
|
} else if upsertQuery == "" {
|
|
upsertQuery = DefaultUpsertQuery
|
|
}
|
|
gen := &SqlGenMysql{
|
|
CreateTableSqlTemplate: "",
|
|
DropTableSqlTemplate: "DROP TABLE IF EXISTS `%s`",
|
|
UpsertQueryTemplate: upsertQuery,
|
|
}
|
|
store.SqlGenerator = gen
|
|
|
|
store.RetryableErrorCallback = func(err error) bool {
|
|
var mysqlError *mysql.MySQLError
|
|
if errors.As(err, &mysqlError) {
|
|
if mysqlError.Number == 1213 { // ER_LOCK_DEADLOCK
|
|
return true
|
|
}
|
|
if mysqlError.Number == 1205 { // ER_LOCK_WAIT_TIMEOUT
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if dsn == "" {
|
|
dsn = fmt.Sprintf(CONNECTION_URL_PATTERN, user, password, hostname, port, database)
|
|
if interpolateParams {
|
|
dsn += "&interpolateParams=true"
|
|
}
|
|
}
|
|
cfg, err := mysql.ParseDSN(dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("can not parse DSN error:%w", err)
|
|
}
|
|
|
|
if enableTls {
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: tlsInsecureSkipVerify,
|
|
ServerName: tlsServerName,
|
|
}
|
|
|
|
// When ca_crt is empty, leave RootCAs nil so Go falls back to the
|
|
// system trust store. This is the common case for managed databases
|
|
// (RDS, Aiven, ...) whose certs chain to a public CA already on the host.
|
|
if caCrtDir != "" {
|
|
rootCertPool := x509.NewCertPool()
|
|
pem, err := os.ReadFile(caCrtDir)
|
|
if err != nil {
|
|
return fmt.Errorf("read ca_crt %s: %w", caCrtDir, err)
|
|
}
|
|
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
|
return fmt.Errorf("failed to append root certificate from %s", caCrtDir)
|
|
}
|
|
tlsConfig.RootCAs = rootCertPool
|
|
}
|
|
|
|
// Only attempt to load a client keypair when at least one of the paths is
|
|
// set. If either is set, both must load successfully — silently skipping
|
|
// a typo'd path used to mask broken mTLS setups as confusing handshake
|
|
// failures.
|
|
if clientCrtDir != "" || clientKeyDir != "" {
|
|
cert, err := tls.LoadX509KeyPair(clientCrtDir, clientKeyDir)
|
|
if err != nil {
|
|
return fmt.Errorf("load mysql client keypair (crt=%s key=%s): %w", clientCrtDir, clientKeyDir, err)
|
|
}
|
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
}
|
|
|
|
// Set TLS directly on the parsed Config rather than registering a global
|
|
// "mysql-tls" entry — the global registry is process-wide and would be
|
|
// overwritten if a second MysqlStore is initialized with different TLS
|
|
// settings.
|
|
cfg.TLS = tlsConfig
|
|
}
|
|
|
|
connector, err := mysql.NewConnector(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("can not create mysql connector for %s error:%w", maskedDSN(cfg), err)
|
|
}
|
|
|
|
store.DB = sql.OpenDB(connector)
|
|
store.DB.SetMaxIdleConns(maxIdle)
|
|
store.DB.SetMaxOpenConns(maxOpen)
|
|
store.DB.SetConnMaxLifetime(time.Duration(maxLifetimeSeconds) * time.Second)
|
|
|
|
if err = store.DB.Ping(); err != nil {
|
|
return fmt.Errorf("connect to %s error:%v", maskedDSN(cfg), err)
|
|
}
|
|
|
|
ConfigureListOrdering(store.DB, gen)
|
|
|
|
return nil
|
|
}
|
|
|
|
func maskedDSN(cfg *mysql.Config) string {
|
|
if cfg.Passwd == "" {
|
|
return cfg.FormatDSN()
|
|
}
|
|
return strings.ReplaceAll(cfg.FormatDSN(), cfg.Passwd, "<ADAPTED>")
|
|
}
|