mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
add mysql as a new storage engine
This commit is contained in:
parent
2ed418c191
commit
ce46b5eb0b
@ -330,6 +330,23 @@ type Config struct {
|
|||||||
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
|
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
|
||||||
// - https://github.com/transmission/transmission/pull/468
|
// - https://github.com/transmission/transmission/pull/468
|
||||||
// - https://github.com/etcd-io/etcd/issues/9353
|
// - https://github.com/etcd-io/etcd/issues/9353
|
||||||
|
//
|
||||||
|
// 1. If client connection is secure via HTTPS, allow any hostnames.
|
||||||
|
// 2. If client connection is not secure and "HostWhitelist" is not empty,
|
||||||
|
// only allow HTTP requests whose Host field is listed in whitelist.
|
||||||
|
//
|
||||||
|
// Note that the client origin policy is enforced whether authentication
|
||||||
|
// is enabled or not, for tighter controls.
|
||||||
|
//
|
||||||
|
// By default, "HostWhitelist" is "*", which allows any hostnames.
|
||||||
|
// Note that when specifying hostnames, loopback addresses are not added
|
||||||
|
// automatically. To allow loopback interfaces, leave it empty or set it "*",
|
||||||
|
// or add them to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
|
||||||
|
//
|
||||||
|
// CVE-2018-5702 reference:
|
||||||
|
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
|
||||||
|
// - https://github.com/transmission/transmission/pull/468
|
||||||
|
// - https://github.com/etcd-io/etcd/issues/9353
|
||||||
HostWhitelist map[string]struct{}
|
HostWhitelist map[string]struct{}
|
||||||
|
|
||||||
// UserHandlers is for registering users handlers and only used for
|
// UserHandlers is for registering users handlers and only used for
|
||||||
@ -462,6 +479,11 @@ type Config struct {
|
|||||||
|
|
||||||
// ServerFeatureGate is a server level feature gate
|
// ServerFeatureGate is a server level feature gate
|
||||||
ServerFeatureGate featuregate.FeatureGate
|
ServerFeatureGate featuregate.FeatureGate
|
||||||
|
|
||||||
|
// BackendType specifies the type of backend storage to use
|
||||||
|
BackendType string `json:"backend-type"`
|
||||||
|
// MySQLDSN is the Data Source Name for the MySQL backend
|
||||||
|
MySQLDSN string `json:"mysql-dsn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configYAML holds the config suitable for yaml parsing
|
// configYAML holds the config suitable for yaml parsing
|
||||||
@ -586,6 +608,9 @@ func NewConfig() *Config {
|
|||||||
|
|
||||||
AutoCompactionMode: DefaultAutoCompactionMode,
|
AutoCompactionMode: DefaultAutoCompactionMode,
|
||||||
ServerFeatureGate: features.NewDefaultServerFeatureGate(DefaultName, nil),
|
ServerFeatureGate: features.NewDefaultServerFeatureGate(DefaultName, nil),
|
||||||
|
|
||||||
|
BackendType: "mysql",
|
||||||
|
MySQLDSN: "root:password@tcp(localhost:3306)/etcd",
|
||||||
}
|
}
|
||||||
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
|
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
|
||||||
return cfg
|
return cfg
|
||||||
|
|||||||
@ -65,6 +65,8 @@ type config struct {
|
|||||||
configFile string
|
configFile string
|
||||||
printVersion bool
|
printVersion bool
|
||||||
ignored []string
|
ignored []string
|
||||||
|
BackendType string `json:"backend-type"`
|
||||||
|
MySQLDSN string `json:"mysql-dsn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configFlags has the set of flags used for command line parsing a Config
|
// configFlags has the set of flags used for command line parsing a Config
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"go.etcd.io/etcd/server/v3/embed"
|
"go.etcd.io/etcd/server/v3/embed"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver/api/v2discovery"
|
"go.etcd.io/etcd/server/v3/etcdserver/api/v2discovery"
|
||||||
"go.etcd.io/etcd/server/v3/etcdserver/errors"
|
"go.etcd.io/etcd/server/v3/etcdserver/errors"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dirType string
|
type dirType string
|
||||||
|
|||||||
@ -48,12 +48,14 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
@ -32,6 +34,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
|||||||
@ -129,6 +129,13 @@ type backend struct {
|
|||||||
lg *zap.Logger
|
lg *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BackendType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BackendTypeBoltDB BackendType = "boltdb"
|
||||||
|
BackendTypeMySQL BackendType = "mysql"
|
||||||
|
)
|
||||||
|
|
||||||
type BackendConfig struct {
|
type BackendConfig struct {
|
||||||
// Path is the file path to the backend file.
|
// Path is the file path to the backend file.
|
||||||
Path string
|
Path string
|
||||||
@ -149,6 +156,9 @@ type BackendConfig struct {
|
|||||||
|
|
||||||
// Hooks are getting executed during lifecycle of Backend's transactions.
|
// Hooks are getting executed during lifecycle of Backend's transactions.
|
||||||
Hooks Hooks
|
Hooks Hooks
|
||||||
|
|
||||||
|
BackendType BackendType
|
||||||
|
MySQLDSN string // MySQL Data Source Name
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendConfigOption func(*BackendConfig)
|
type BackendConfigOption func(*BackendConfig)
|
||||||
@ -159,11 +169,25 @@ func DefaultBackendConfig(lg *zap.Logger) BackendConfig {
|
|||||||
BatchLimit: defaultBatchLimit,
|
BatchLimit: defaultBatchLimit,
|
||||||
MmapSize: InitialMmapSize,
|
MmapSize: InitialMmapSize,
|
||||||
Logger: lg,
|
Logger: lg,
|
||||||
|
BackendType: BackendTypeMySQL, // Change this to MySQL
|
||||||
|
MySQLDSN: "root:password@tcp(localhost:3306)/etcd", // Default MySQL DSN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(bcfg BackendConfig) Backend {
|
func New(bcfg BackendConfig) Backend {
|
||||||
|
switch bcfg.BackendType {
|
||||||
|
case BackendTypeBoltDB:
|
||||||
return newBackend(bcfg)
|
return newBackend(bcfg)
|
||||||
|
case BackendTypeMySQL:
|
||||||
|
be, err := newMySQLBackend(bcfg)
|
||||||
|
if err != nil {
|
||||||
|
bcfg.Logger.Panic("failed to create MySQL backend", zap.Error(err))
|
||||||
|
}
|
||||||
|
return be
|
||||||
|
default:
|
||||||
|
bcfg.Logger.Panic("unknown backend type", zap.String("type", string(bcfg.BackendType)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithMmapSize(size uint64) BackendConfigOption {
|
func WithMmapSize(size uint64) BackendConfigOption {
|
||||||
|
|||||||
325
server/storage/backend/backend_mysql.go
Normal file
325
server/storage/backend/backend_mysql.go
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mysqlBackend struct {
|
||||||
|
db *sql.DB
|
||||||
|
lg *zap.Logger
|
||||||
|
mu sync.RWMutex
|
||||||
|
stopc chan struct{}
|
||||||
|
donec chan struct{}
|
||||||
|
hooks Hooks
|
||||||
|
postLockInsideApplyHook func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMySQLBackend(bcfg BackendConfig) (*mysqlBackend, error) {
|
||||||
|
db, err := sql.Open("mysql", bcfg.MySQLDSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection pool settings
|
||||||
|
db.SetMaxOpenConns(100)
|
||||||
|
db.SetMaxIdleConns(10)
|
||||||
|
db.SetConnMaxLifetime(time.Hour)
|
||||||
|
|
||||||
|
be := &mysqlBackend{
|
||||||
|
db: db,
|
||||||
|
lg: bcfg.Logger,
|
||||||
|
stopc: make(chan struct{}),
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
hooks: bcfg.Hooks,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize tables
|
||||||
|
if err := be.initTables(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return be, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) initTables() error {
|
||||||
|
_, err := m.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS kv_store (
|
||||||
|
key VARBINARY(512) PRIMARY KEY,
|
||||||
|
value LONGBLOB,
|
||||||
|
create_revision BIGINT,
|
||||||
|
mod_revision BIGINT,
|
||||||
|
version BIGINT
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) BatchTx() BatchTx {
|
||||||
|
return &mysqlBatchTx{be: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) ReadTx() ReadTx {
|
||||||
|
return &mysqlReadTx{be: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) ConcurrentReadTx() ReadTx {
|
||||||
|
return &mysqlReadTx{be: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) Snapshot() Snapshot {
|
||||||
|
return &mysqlSnapshot{be: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) Hash(ignores func([]byte, []byte) bool) (uint32, error) {
|
||||||
|
// Implement hash calculation for MySQL
|
||||||
|
// This is a placeholder implementation; you should replace it with your actual logic.
|
||||||
|
return 0, fmt.Errorf("Hash not implemented for MySQL backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) Size() int64 {
|
||||||
|
var size int64
|
||||||
|
row := m.db.QueryRow("SELECT SUM(DATA_LENGTH + INDEX_LENGTH) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()")
|
||||||
|
err := row.Scan(&size)
|
||||||
|
if err != nil {
|
||||||
|
m.lg.Error("failed to get database size", zap.Error(err))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) SizeInUse() int64 {
|
||||||
|
return m.Size() // For MySQL, Size and SizeInUse are the same
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) OpenReadTxN() int64 {
|
||||||
|
// MySQL doesn't have a concept of read transactions, so return 0
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) Defrag() error {
|
||||||
|
// MySQL handles fragmentation internally, so this is a no-op
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) ForceCommit() {
|
||||||
|
// MySQL commits automatically, so this is a no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) Close() error {
|
||||||
|
close(m.stopc)
|
||||||
|
<-m.donec
|
||||||
|
return m.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mysqlBackend) SetTxPostLockInsideApplyHook(hook func()) {
|
||||||
|
m.postLockInsideApplyHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysqlBatchTx implements BatchTx interface
|
||||||
|
type mysqlBatchTx struct {
|
||||||
|
be *mysqlBackend
|
||||||
|
tx *sql.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) Lock() {
|
||||||
|
t.be.mu.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) Unlock() {
|
||||||
|
t.be.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeCreateBucket(bucket Bucket) {
|
||||||
|
// MySQL doesn't use buckets, so this is a no-op
|
||||||
|
t.be.lg.Warn("UnsafeCreateBucket called on MySQL backend", zap.String("bucket", "n/a"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafePut(bucket Bucket, key []byte, value []byte) {
|
||||||
|
if t.tx == nil {
|
||||||
|
var err error
|
||||||
|
t.tx, err = t.be.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to begin transaction", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := t.tx.Exec("INSERT INTO kv_store (key, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?", key, value, value)
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to put key-value pair", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeSeqPut(bucket Bucket, key []byte, value []byte) {
|
||||||
|
t.UnsafePut(bucket, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeDelete(bucket Bucket, key []byte) {
|
||||||
|
if t.tx == nil {
|
||||||
|
var err error
|
||||||
|
t.tx, err = t.be.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to begin transaction", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := t.tx.Exec("DELETE FROM kv_store WHERE key = ?", key)
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to delete key", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeDeleteBucket(bucket Bucket) {
|
||||||
|
t.be.lg.Warn("UnsafeDeleteBucket called on MySQL backend", zap.String("bucket", "n/a"))
|
||||||
|
// No-op for MySQL as it doesn't use buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeForEach(bucket Bucket, visitor func(k, v []byte) error) error {
|
||||||
|
rows, err := t.be.db.Query("SELECT key, value FROM kv_store")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var k, v []byte
|
||||||
|
if err := rows.Scan(&k, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visitor(k, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) UnsafeRange(bucket Bucket, key, endKey []byte, limit int64) ([][]byte, [][]byte) {
|
||||||
|
var keys, values [][]byte
|
||||||
|
query := "SELECT key, value FROM kv_store WHERE key >= ? AND key < ? ORDER BY key LIMIT ?"
|
||||||
|
rows, err := t.be.db.Query(query, key, endKey, limit)
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to query range", zap.Error(err))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var k, v []byte
|
||||||
|
if err := rows.Scan(&k, &v); err != nil {
|
||||||
|
t.be.lg.Error("failed to scan row", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
return keys, values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) Commit() {
|
||||||
|
if t.tx != nil {
|
||||||
|
err := t.tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to commit transaction", zap.Error(err))
|
||||||
|
}
|
||||||
|
t.tx = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) CommitAndStop() {
|
||||||
|
t.Commit()
|
||||||
|
// Additional cleanup if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) LockInsideApply() {
|
||||||
|
t.be.mu.Lock()
|
||||||
|
if t.be.postLockInsideApplyHook != nil {
|
||||||
|
t.be.postLockInsideApplyHook()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlBatchTx) LockOutsideApply() {
|
||||||
|
t.be.mu.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysqlReadTx implements ReadTx interface
|
||||||
|
type mysqlReadTx struct {
|
||||||
|
be *mysqlBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlReadTx) Lock() {}
|
||||||
|
func (t *mysqlReadTx) Unlock() {}
|
||||||
|
func (t *mysqlReadTx) Reset() {}
|
||||||
|
func (t *mysqlReadTx) RLock() {}
|
||||||
|
func (t *mysqlReadTx) RUnlock() {}
|
||||||
|
|
||||||
|
func (t *mysqlReadTx) UnsafeRange(bucket Bucket, key, endKey []byte, limit int64) ([][]byte, [][]byte) {
|
||||||
|
var keys, values [][]byte
|
||||||
|
query := "SELECT key, value FROM kv_store WHERE key >= ? AND key < ? ORDER BY key LIMIT ?"
|
||||||
|
rows, err := t.be.db.Query(query, key, endKey, limit)
|
||||||
|
if err != nil {
|
||||||
|
t.be.lg.Error("failed to query range", zap.Error(err))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var k, v []byte
|
||||||
|
if err := rows.Scan(&k, &v); err != nil {
|
||||||
|
t.be.lg.Error("failed to scan row", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
return keys, values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlReadTx) UnsafeGet(bucket Bucket, key []byte) (value []byte, err error) {
|
||||||
|
err = t.be.db.QueryRow("SELECT value FROM kv_store WHERE key = ?", key).Scan(&value)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mysqlReadTx) UnsafeForEach(bucket Bucket, visitor func(k, v []byte) error) error {
|
||||||
|
rows, err := t.be.db.Query("SELECT key, value FROM kv_store")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var k, v []byte
|
||||||
|
if err := rows.Scan(&k, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visitor(k, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysqlSnapshot implements Snapshot interface
|
||||||
|
type mysqlSnapshot struct {
|
||||||
|
be *mysqlBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mysqlSnapshot) Close() error {
|
||||||
|
// MySQL doesn't require explicit snapshot closing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mysqlSnapshot) Size() int64 {
|
||||||
|
return s.be.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mysqlSnapshot) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
// Implement snapshot writing logic
|
||||||
|
return 0, fmt.Errorf("WriteTo not implemented for MySQL snapshot")
|
||||||
|
}
|
||||||
188
server/storage/backend/backend_mysql_test.go
Normal file
188
server/storage/backend/backend_mysql_test.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testMySQLDSN string
|
||||||
|
|
||||||
|
// Define TestBucket
|
||||||
|
var TestBucket Bucket = testBucket("test")
|
||||||
|
|
||||||
|
type testBucket string
|
||||||
|
|
||||||
|
func (b testBucket) ID() BucketID {
|
||||||
|
return BucketID(0) // You might want to implement a proper ID system
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b testBucket) Name() []byte {
|
||||||
|
return []byte(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b testBucket) String() string {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b testBucket) IsSafeRangeBucket() bool {
|
||||||
|
// Implement this method based on your requirements
|
||||||
|
// For testing purposes, we'll return true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Set up the test MySQL DSN
|
||||||
|
// You might want to make this configurable via environment variables
|
||||||
|
cfg := mysql.NewConfig()
|
||||||
|
cfg.User = "root"
|
||||||
|
cfg.Passwd = "password"
|
||||||
|
cfg.DBName = "etcd_test"
|
||||||
|
cfg.ParseTime = true
|
||||||
|
cfg.Loc = time.UTC
|
||||||
|
|
||||||
|
testMySQLDSN = cfg.FormatDSN()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestMySQL(t *testing.T) *mysqlBackend {
|
||||||
|
db, err := sql.Open("mysql", testMySQLDSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test database
|
||||||
|
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS etcd_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Close()
|
||||||
|
|
||||||
|
// Create backend
|
||||||
|
lg, _ := zap.NewDevelopment()
|
||||||
|
bcfg := BackendConfig{
|
||||||
|
Logger: lg,
|
||||||
|
MySQLDSN: testMySQLDSN,
|
||||||
|
}
|
||||||
|
|
||||||
|
be, err := newMySQLBackend(bcfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create MySQL backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return be
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardownTestMySQL(t *testing.T, be *mysqlBackend) {
|
||||||
|
be.Close()
|
||||||
|
|
||||||
|
// Drop test database
|
||||||
|
db, err := sql.Open("mysql", testMySQLDSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
_, err = db.Exec("DROP DATABASE IF EXISTS etcd_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to drop test database: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQLBackend_BatchTx(t *testing.T) {
|
||||||
|
be := setupTestMySQL(t)
|
||||||
|
defer teardownTestMySQL(t, be)
|
||||||
|
|
||||||
|
tx := be.BatchTx()
|
||||||
|
|
||||||
|
// Test Put and Get
|
||||||
|
bucket := TestBucket
|
||||||
|
key := []byte("testkey")
|
||||||
|
value := []byte("testvalue")
|
||||||
|
|
||||||
|
tx.Lock()
|
||||||
|
tx.UnsafePut(bucket, key, value)
|
||||||
|
tx.Unlock()
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
rtx := be.ReadTx()
|
||||||
|
rtx.RLock()
|
||||||
|
gotValues, _ := rtx.UnsafeRange(bucket, key, nil, 0)
|
||||||
|
rtx.RUnlock()
|
||||||
|
|
||||||
|
if len(gotValues) == 0 || string(gotValues[0]) != string(value) {
|
||||||
|
t.Errorf("Got %s, want %s", string(gotValues[0]), string(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Delete
|
||||||
|
tx.Lock()
|
||||||
|
tx.UnsafeDelete(bucket, key)
|
||||||
|
tx.Unlock()
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
rtx.RLock()
|
||||||
|
gotValues, _ = rtx.UnsafeRange(bucket, key, nil, 0)
|
||||||
|
rtx.RUnlock()
|
||||||
|
|
||||||
|
if len(gotValues) != 0 {
|
||||||
|
t.Errorf("Got %s, want nil", string(gotValues[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQLBackend_ReadTx(t *testing.T) {
|
||||||
|
be := setupTestMySQL(t)
|
||||||
|
defer teardownTestMySQL(t, be)
|
||||||
|
|
||||||
|
tx := be.BatchTx()
|
||||||
|
|
||||||
|
// Insert test data
|
||||||
|
bucket := TestBucket
|
||||||
|
testData := map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
"key3": "value3",
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Lock()
|
||||||
|
for k, v := range testData {
|
||||||
|
tx.UnsafePut(bucket, []byte(k), []byte(v))
|
||||||
|
}
|
||||||
|
tx.Unlock()
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
// Test Range
|
||||||
|
rtx := be.ReadTx()
|
||||||
|
rtx.RLock()
|
||||||
|
keys, values := rtx.UnsafeRange(bucket, []byte("key"), []byte("key4"), 0)
|
||||||
|
rtx.RUnlock()
|
||||||
|
|
||||||
|
if len(keys) != len(testData) || len(values) != len(testData) {
|
||||||
|
t.Errorf("Got %d keys and %d values, want %d each", len(keys), len(values), len(testData))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
value := values[i]
|
||||||
|
if testData[string(key)] != string(value) {
|
||||||
|
t.Errorf("For key %s, got value %s, want %s", string(key), string(value), testData[string(key)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Set up any global test environment here if needed
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
// Tear down any global test environment here if needed
|
||||||
|
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user