482 lines
13 KiB
Go
482 lines
13 KiB
Go
// Copyright 2021 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sqle
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/ref"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
|
|
"github.com/dolthub/dolt/go/store/datas"
|
|
"github.com/dolthub/dolt/go/store/hash"
|
|
"github.com/dolthub/dolt/go/store/types"
|
|
)
|
|
|
|
type PushOnWriteHook struct {
|
|
out io.Writer
|
|
destDb *doltdb.DoltDB
|
|
tmpDir string
|
|
}
|
|
|
|
var _ doltdb.CommitHook = (*PushOnWriteHook)(nil)
|
|
|
|
// NewPushOnWriteHook creates a ReplicateHook, parameterizaed by the backup database
|
|
// and a local tempfile for pushing
|
|
func NewPushOnWriteHook(tmpDir string, logger io.Writer) *PushOnWriteHook {
|
|
return &PushOnWriteHook{
|
|
tmpDir: tmpDir,
|
|
out: logger,
|
|
}
|
|
}
|
|
|
|
// Execute implements CommitHook, replicates head updates to the destDb field
|
|
func (ph *PushOnWriteHook) Execute(ctx context.Context, ds datas.Dataset, srcDb *doltdb.DoltDB) (func(context.Context) error, error) {
|
|
if ph.destDb == nil {
|
|
e := fmt.Errorf("PushOnWriteHook invoked with nil destDB")
|
|
logrus.Errorf("runtime error: %v", e)
|
|
return nil, e
|
|
}
|
|
|
|
err := pushDataset(ctx, ph.destDb, srcDb, ds, ph.tmpDir)
|
|
|
|
if ph.out != nil && err != nil {
|
|
// if we can't write to the output, there's not much we can do.
|
|
_, _ = ph.out.Write([]byte(fmt.Sprintf("error pushing: %+v", err)))
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
func pushDataset(ctx context.Context, destDB, srcDB *doltdb.DoltDB, ds datas.Dataset, tmpDir string) error {
|
|
addr, ok := ds.MaybeHeadAddr()
|
|
if !ok {
|
|
// TODO: fix up hack usage.
|
|
_, err := doltdb.ExposeDatabaseFromDoltDB(destDB).Delete(ctx, ds, "")
|
|
return err
|
|
}
|
|
|
|
err := destDB.PullChunks(ctx, tmpDir, srcDB, []hash.Hash{addr}, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rf, err := ref.Parse(ds.ID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return destDB.SetHead(ctx, rf, addr)
|
|
}
|
|
|
|
func (*PushOnWriteHook) ExecuteForWorkingSets() bool {
|
|
return false
|
|
}
|
|
|
|
func (*PushOnWriteHook) ExecuteForReplicaWrite() bool {
|
|
return false
|
|
}
|
|
|
|
type PushArg struct {
|
|
ds datas.Dataset
|
|
srcDb *doltdb.DoltDB
|
|
destDb *doltdb.DoltDB
|
|
hash hash.Hash
|
|
}
|
|
|
|
type AsyncPushOnWriteHook struct {
|
|
out io.Writer
|
|
ch chan PushArg
|
|
|
|
destDb *doltdb.DoltDB
|
|
}
|
|
|
|
const (
|
|
asyncPushBufferSize = 2048
|
|
asyncPushInterval = 500 * time.Millisecond
|
|
asyncPushProcessCommit = "async_push_process_commit"
|
|
asyncPushSyncReplica = "async_push_sync_replica"
|
|
)
|
|
|
|
var _ doltdb.CommitHook = (*AsyncPushOnWriteHook)(nil)
|
|
|
|
// NewAsyncPushOnWriteHook creates a AsyncReplicateHook
|
|
func NewAsyncPushOnWriteHook(nameSuffix string, tmpDir string, logger io.Writer) (*AsyncPushOnWriteHook, RunAsyncThreads) {
|
|
ch := make(chan PushArg, asyncPushBufferSize)
|
|
runThreads := func(bThreads BackgroundThreads, ctxF func(context.Context) (*sql.Context, error)) error {
|
|
return RunAsyncReplicationThreads(bThreads, nameSuffix, ctxF, ch, tmpDir, logger)
|
|
}
|
|
return &AsyncPushOnWriteHook{ch: ch}, runThreads
|
|
}
|
|
|
|
func (*AsyncPushOnWriteHook) ExecuteForWorkingSets() bool {
|
|
return false
|
|
}
|
|
|
|
func (*AsyncPushOnWriteHook) ExecuteForReplicaWrite() bool {
|
|
return false
|
|
}
|
|
|
|
// Execute implements CommitHook, replicates head updates to the destDb field
|
|
func (ah *AsyncPushOnWriteHook) Execute(ctx context.Context, ds datas.Dataset, srcDb *doltdb.DoltDB) (func(context.Context) error, error) {
|
|
if ah.destDb == nil {
|
|
e := fmt.Errorf("AsyncPushOnWriteHook invoked with nil destDB")
|
|
logrus.Errorf("runtime error: %v", e)
|
|
return nil, e
|
|
}
|
|
|
|
addr, _ := ds.MaybeHeadAddr()
|
|
// TODO: Unconditional push here seems dangerous.
|
|
ah.ch <- PushArg{ds: ds, srcDb: srcDb, destDb: ah.destDb, hash: addr}
|
|
|
|
err := ctx.Err()
|
|
if err != nil {
|
|
_, _ = ah.out.Write([]byte(err.Error()))
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
type LogHook struct {
|
|
out io.Writer
|
|
msg []byte
|
|
}
|
|
|
|
var _ doltdb.CommitHook = (*LogHook)(nil)
|
|
|
|
// NewLogHook is a noop that logs to a writer when invoked
|
|
func NewLogHook(msg []byte, logger io.Writer) *LogHook {
|
|
return &LogHook{msg: msg, out: logger}
|
|
}
|
|
|
|
// Execute implements CommitHook, writes message to log channel
|
|
func (lh *LogHook) Execute(ctx context.Context, ds datas.Dataset, db *doltdb.DoltDB) (func(context.Context) error, error) {
|
|
if lh.out != nil {
|
|
_, _ = lh.out.Write(lh.msg)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (*LogHook) ExecuteForWorkingSets() bool {
|
|
return false
|
|
}
|
|
|
|
func (*LogHook) ExecuteForReplicaWrite() bool {
|
|
return false
|
|
}
|
|
|
|
func RunAsyncReplicationThreads(bThreads BackgroundThreads, nameSuffix string, ctxF func(context.Context) (*sql.Context, error), ch chan PushArg, tmpDir string, logger io.Writer) error {
|
|
mu := &sync.Mutex{}
|
|
var newHeads = make(map[string]PushArg, asyncPushBufferSize)
|
|
|
|
// wake is a buffered signal channel used to notify the push
|
|
// goroutine that new work is available. Size 1 ensures signals
|
|
// are not lost but the sender never blocks.
|
|
wake := make(chan struct{}, 1)
|
|
|
|
updateHead := func(p PushArg) {
|
|
mu.Lock()
|
|
newHeads[p.ds.ID()] = p
|
|
mu.Unlock()
|
|
// Non-blocking send to wake the push goroutine.
|
|
select {
|
|
case wake <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// newCtx lets first goroutine drain before the second goroutine finalizes
|
|
newCtx, stop := context.WithCancel(context.Background())
|
|
|
|
// The first goroutine amortizes commits into a map keyed by dataset id.
|
|
// When the parent context cancels, this goroutine drains and kills its
|
|
// dependent goroutine.
|
|
//
|
|
// We do not track sequential commits because push follows historical
|
|
// dependencies. This does not account for reset --force, which
|
|
// breaks historical dependence.
|
|
err := bThreads.Add(asyncPushProcessCommit+nameSuffix, func(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case p, ok := <-ch:
|
|
if !ok {
|
|
return
|
|
}
|
|
updateHead(p)
|
|
case <-ctx.Done():
|
|
stop()
|
|
return
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
getHeadsCopy := func() map[string]PushArg {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(newHeads) == 0 {
|
|
return nil
|
|
}
|
|
|
|
toRet := newHeads
|
|
newHeads = make(map[string]PushArg, asyncPushBufferSize)
|
|
|
|
return toRet
|
|
}
|
|
|
|
flush := func(newHeads map[string]PushArg, latestHeads map[string]hash.Hash) {
|
|
newHeadsCopy := getHeadsCopy()
|
|
if len(newHeadsCopy) == 0 {
|
|
return
|
|
}
|
|
for id, newCm := range newHeadsCopy {
|
|
if latest, ok := latestHeads[id]; !ok || latest != newCm.hash {
|
|
func() {
|
|
// use background context to drain after sql context is canceled
|
|
sqlCtx, err := ctxF(context.Background())
|
|
if err != nil {
|
|
logger.Write([]byte("replication failed: could not create *sql.Context: " + err.Error()))
|
|
} else {
|
|
defer sql.SessionEnd(sqlCtx.Session)
|
|
sql.SessionCommandBegin(sqlCtx.Session)
|
|
defer sql.SessionCommandEnd(sqlCtx.Session)
|
|
err := pushDataset(sqlCtx, newCm.destDb, newCm.srcDb, newCm.ds, tmpDir)
|
|
if err != nil {
|
|
logger.Write([]byte("replication failed: " + err.Error()))
|
|
}
|
|
if newCm.hash.IsEmpty() {
|
|
delete(latestHeads, id)
|
|
} else {
|
|
latestHeads[id] = newCm.hash
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
// The second goroutine pushes updates to a remote chunkstore.
|
|
// It quiesces when there is no work, waking only when the first
|
|
// goroutine signals via the wake channel.
|
|
// This goroutine waits for first goroutine to drain before closing
|
|
// the channel and exiting.
|
|
err = bThreads.Add(asyncPushSyncReplica+nameSuffix, func(ctx context.Context) {
|
|
defer close(ch)
|
|
var latestHeads = make(map[string]hash.Hash, asyncPushBufferSize)
|
|
ticker := time.NewTicker(asyncPushInterval)
|
|
canPoll := false
|
|
for {
|
|
wakeCh := wake
|
|
// Only allow polling wakeCh if ticker has fired.
|
|
if !canPoll {
|
|
wakeCh = nil
|
|
}
|
|
select {
|
|
case <-newCtx.Done():
|
|
flush(newHeads, latestHeads)
|
|
return
|
|
case <-wakeCh:
|
|
canPoll = false
|
|
ticker.Reset(asyncPushInterval)
|
|
flush(newHeads, latestHeads)
|
|
case <-ticker.C:
|
|
ticker.Stop()
|
|
canPoll = true
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DynamicPushOnWriteHook is a CommitHook that conditionally invokes either a PushOnWriteHook or an AsyncPushOnWriteHook
|
|
// based on the values of the `dolt_replicate_to_remote` and `dolt_async_replication` system variables. If
|
|
// `dolt_replicate_to_remote`
|
|
//
|
|
// Each time the Execute method is invoked, the current values of the system variables are checked. If they differ from the
|
|
// last invocation, the internal PushOnWriteHook or AsyncPushOnWriteHook is updated to reflect the new configuration.
|
|
type DynamicPushOnWriteHook struct {
|
|
mu sync.Mutex
|
|
dEnv *env.DoltEnv
|
|
tempDir string
|
|
logger io.Writer
|
|
|
|
// Values below protected with mu Mutex
|
|
remote string
|
|
async bool
|
|
// Wrappers around the two hook types. We update the fields of these structs as needed based on the current config.
|
|
syncHook PushOnWriteHook
|
|
asyncHook AsyncPushOnWriteHook
|
|
}
|
|
|
|
var _ doltdb.CommitHook = (*DynamicPushOnWriteHook)(nil)
|
|
|
|
// NewDynamicPushOnWriteHook creates a DynamicPushOnWriteHook, parameterized by the environment and a logger. The configuration
|
|
// options at this time can result in errors, for example if the provided remote does not exist. This is not the case
|
|
// when the process is up and running. If bad configuration is detected at execution time, the error is logged and replication is skipped.
|
|
func NewDynamicPushOnWriteHook(ctx context.Context, nameSuffix string, dEnv *env.DoltEnv, logger io.Writer) (*DynamicPushOnWriteHook, RunAsyncThreads, error) {
|
|
remote, async, err := getReplicationVals()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
tmpDir, err := dEnv.TempTableFilesDir()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
a, newThreads := NewAsyncPushOnWriteHook(nameSuffix, tmpDir, logger)
|
|
p := NewPushOnWriteHook(tmpDir, logger)
|
|
|
|
if remote != "" {
|
|
destDb, err := getDestinationDb(ctx, dEnv, remote)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
p.destDb = destDb
|
|
a.destDb = destDb
|
|
}
|
|
|
|
return &DynamicPushOnWriteHook{
|
|
dEnv: dEnv,
|
|
tempDir: tmpDir,
|
|
logger: logger,
|
|
remote: remote,
|
|
async: async,
|
|
syncHook: *p,
|
|
asyncHook: *a,
|
|
}, newThreads, nil
|
|
}
|
|
|
|
// getReplicationVals gets the current values of the `dolt_replicate_to_remote` and `dolt_async_replication` system
|
|
// variables.
|
|
func getReplicationVals() (string, bool, error) {
|
|
_, val, ok := sql.SystemVariables.GetGlobal(dsess.ReplicateToRemote)
|
|
if !ok {
|
|
return "", false, sql.ErrUnknownSystemVariable.New(dsess.ReplicateToRemote)
|
|
}
|
|
remoteName, ok := val.(string)
|
|
if !ok {
|
|
return "", false, sql.ErrInvalidSystemVariableValue.New(val)
|
|
}
|
|
|
|
async := false
|
|
if _, val, ok = sql.SystemVariables.GetGlobal(dsess.AsyncReplication); ok && val == dsess.SysVarTrue {
|
|
async = true
|
|
}
|
|
|
|
return remoteName, async, nil
|
|
}
|
|
|
|
// getDestinationDb gets the target doltdb for replication, based on a provided remote name.
|
|
func getDestinationDb(ctx context.Context, dEnv *env.DoltEnv, remoteName string) (*doltdb.DoltDB, error) {
|
|
remotes, err := dEnv.GetRemotes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rem, ok := remotes.Get(remoteName)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: '%s'", env.ErrRemoteNotFound, remoteName)
|
|
}
|
|
|
|
destDb, err := rem.GetRemoteDB(ctx, types.Format_DOLT, dEnv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return destDb, nil
|
|
}
|
|
|
|
func (m *DynamicPushOnWriteHook) Execute(ctx context.Context, ds datas.Dataset, db *doltdb.DoltDB) (func(context.Context) error, error) {
|
|
remoteName, async, err := getReplicationVals()
|
|
|
|
// We only need the lock if the remote configuration has changed.
|
|
hook, err := func() (doltdb.CommitHook, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.remote == remoteName && m.async == async {
|
|
// No change in config since last execution.
|
|
if m.remote == "" {
|
|
// replication disabled
|
|
return nil, nil
|
|
}
|
|
|
|
if async {
|
|
return &m.asyncHook, nil
|
|
}
|
|
return &m.syncHook, nil
|
|
}
|
|
|
|
if remoteName == "" {
|
|
// replication disabled
|
|
m.remote = ""
|
|
m.async = false
|
|
m.asyncHook.destDb = nil
|
|
m.syncHook.destDb = nil
|
|
return nil, nil
|
|
}
|
|
|
|
m.remote = remoteName
|
|
|
|
destDb, err := getDestinationDb(ctx, m.dEnv, m.remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.syncHook.destDb = destDb
|
|
m.asyncHook.destDb = destDb
|
|
|
|
if async {
|
|
return &m.asyncHook, nil
|
|
}
|
|
return &m.syncHook, nil
|
|
}()
|
|
|
|
if err != nil {
|
|
logrus.Warnf("replication hook failed: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
if hook == nil {
|
|
// replication disabled
|
|
return nil, nil
|
|
}
|
|
|
|
logrus.Debugf("replication hook invoked. pushing to '%s' (asyn=%t)", remoteName, async)
|
|
return hook.Execute(ctx, ds, db)
|
|
}
|
|
|
|
func (m *DynamicPushOnWriteHook) ExecuteForWorkingSets() bool {
|
|
return false
|
|
}
|
|
|
|
func (m *DynamicPushOnWriteHook) ExecuteForReplicaWrite() bool {
|
|
return false
|
|
}
|