github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dprocedures/dolt_gc.go (about) 1 // Copyright 2022 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package dprocedures 16 17 import ( 18 "errors" 19 "fmt" 20 "os" 21 "time" 22 23 "github.com/cenkalti/backoff/v4" 24 "github.com/dolthub/go-mysql-server/sql" 25 26 "github.com/dolthub/dolt/go/cmd/dolt/cli" 27 "github.com/dolthub/dolt/go/libraries/doltcore/branch_control" 28 "github.com/dolthub/dolt/go/libraries/doltcore/dconfig" 29 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 30 ) 31 32 const ( 33 cmdFailure = 1 34 cmdSuccess = 0 35 ) 36 37 func init() { 38 if os.Getenv(dconfig.EnvDisableGcProcedure) != "" { 39 DoltGCFeatureFlag = false 40 } 41 } 42 43 var DoltGCFeatureFlag = true 44 45 // doltGC is the stored procedure to run online garbage collection on a database. 46 func doltGC(ctx *sql.Context, args ...string) (sql.RowIter, error) { 47 if !DoltGCFeatureFlag { 48 return nil, errors.New("DOLT_GC() stored procedure disabled") 49 } 50 res, err := doDoltGC(ctx, args) 51 if err != nil { 52 return nil, err 53 } 54 return rowToIter(int64(res)), nil 55 } 56 57 var ErrServerPerformedGC = errors.New("this connection was established when this server performed an online garbage collection. this connection can no longer be used. please reconnect.") 58 59 func doDoltGC(ctx *sql.Context, args []string) (int, error) { 60 dbName := ctx.GetCurrentDatabase() 61 62 if len(dbName) == 0 { 63 return cmdFailure, fmt.Errorf("Empty database name.") 64 } 65 if err := branch_control.CheckAccess(ctx, branch_control.Permissions_Write); err != nil { 66 return cmdFailure, err 67 } 68 69 apr, err := cli.CreateGCArgParser().Parse(args) 70 if err != nil { 71 return cmdFailure, err 72 } 73 74 if apr.NArg() != 0 { 75 return cmdFailure, InvalidArgErr 76 } 77 78 dSess := dsess.DSessFromSess(ctx.Session) 79 ddb, ok := dSess.GetDoltDB(ctx, dbName) 80 if !ok { 81 return cmdFailure, fmt.Errorf("Could not load database %s", dbName) 82 } 83 84 if apr.Contains(cli.ShallowFlag) { 85 err = ddb.ShallowGC(ctx) 86 if err != nil { 87 return cmdFailure, err 88 } 89 } else { 90 // Currently, if this server is involved in cluster 91 // replication, a full GC is only safe to run on the primary. 92 // We assert that we are the primary here before we begin, and 93 // we assert again that we are the primary at the same epoch as 94 // we establish the safepoint. 95 96 origepoch := -1 97 if _, role, ok := sql.SystemVariables.GetGlobal(dsess.DoltClusterRoleVariable); ok { 98 // TODO: magic constant... 99 if role.(string) != "primary" { 100 return cmdFailure, fmt.Errorf("cannot run a full dolt_gc() while cluster replication is enabled and role is %s; must be the primary", role.(string)) 101 } 102 _, epoch, ok := sql.SystemVariables.GetGlobal(dsess.DoltClusterRoleEpochVariable) 103 if !ok { 104 return cmdFailure, fmt.Errorf("internal error: cannot run a full dolt_gc(); cluster replication is enabled but could not read %s", dsess.DoltClusterRoleEpochVariable) 105 } 106 origepoch = epoch.(int) 107 } 108 109 // TODO: If we got a callback at the beginning and an 110 // (allowed-to-block) callback at the end, we could more 111 // gracefully tear things down. 112 err = ddb.GC(ctx, func() error { 113 if origepoch != -1 { 114 // Here we need to sanity check role and epoch. 115 if _, role, ok := sql.SystemVariables.GetGlobal(dsess.DoltClusterRoleVariable); ok { 116 if role.(string) != "primary" { 117 return fmt.Errorf("dolt_gc failed: when we began we were a primary in a cluster, but now our role is %s", role.(string)) 118 } 119 _, epoch, ok := sql.SystemVariables.GetGlobal(dsess.DoltClusterRoleEpochVariable) 120 if !ok { 121 return fmt.Errorf("dolt_gc failed: when we began we were a primary in a cluster, but we can no longer read the cluster role epoch.") 122 } 123 if origepoch != epoch.(int) { 124 return fmt.Errorf("dolt_gc failed: when we began we were primary in the cluster at epoch %d, but now we are at epoch %d. for gc to safely finalize, our role and epoch must not change throughout the gc.", origepoch, epoch.(int)) 125 } 126 } else { 127 return fmt.Errorf("dolt_gc failed: when we began we were a primary in a cluster, but we can no longer read the cluster role.") 128 } 129 } 130 131 killed := make(map[uint32]struct{}) 132 processes := ctx.ProcessList.Processes() 133 for _, p := range processes { 134 if p.Connection != ctx.Session.ID() { 135 // Kill any inflight query. 136 ctx.ProcessList.Kill(p.Connection) 137 // Tear down the connection itself. 138 ctx.KillConnection(p.Connection) 139 killed[p.Connection] = struct{}{} 140 } 141 } 142 143 // Look in processes until the connections are actually gone. 144 params := backoff.NewExponentialBackOff() 145 params.InitialInterval = 1 * time.Millisecond 146 params.MaxInterval = 25 * time.Millisecond 147 params.MaxElapsedTime = 3 * time.Second 148 err := backoff.Retry(func() error { 149 processes := ctx.ProcessList.Processes() 150 for _, p := range processes { 151 if _, ok := killed[p.Connection]; ok { 152 return errors.New("unable to establish safepoint.") 153 } 154 } 155 return nil 156 }, params) 157 if err != nil { 158 return err 159 } 160 ctx.Session.SetTransaction(nil) 161 dsess.DSessFromSess(ctx.Session).SetValidateErr(ErrServerPerformedGC) 162 return nil 163 }) 164 if err != nil { 165 return cmdFailure, err 166 } 167 } 168 169 return cmdSuccess, nil 170 }