vitess.io/vitess@v0.16.2/go/vt/schemamanager/local_controller.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package schemamanager 18 19 import ( 20 "fmt" 21 "os" 22 "path" 23 "strings" 24 "time" 25 26 "context" 27 28 "vitess.io/vitess/go/vt/log" 29 ) 30 31 // LocalController listens to the specified schema change dir and applies schema changes. 32 // schema change dir lay out 33 // 34 // | 35 // |----keyspace_01 36 // |----input 37 // |---- create_test_table.sql 38 // |---- alter_test_table_02.sql 39 // |---- ... 40 // |----complete // contains completed schema changes in yyyy/MM/dd 41 // |----2015 42 // |----01 43 // |----01 44 // |--- create_table_table_02.sql 45 // |----log // contains detailed execution information about schema changes 46 // |----2015 47 // |----01 48 // |----01 49 // |--- create_table_table_02.sql 50 // |----error // contains failed schema changes 51 // |----2015 52 // |----01 53 // |----01 54 // |--- create_table_table_03.sql 55 // 56 // Schema Change Files: ${keyspace}/input/*.sql 57 // Error Files: ${keyspace}/error/${YYYY}/${MM}/${DD}/*.sql 58 // Log Files: ${keyspace}/log/${YYYY}/${MM}/${DD}/*.sql 59 // Complete Files: ${keyspace}/complete/${YYYY}/${MM}/${DD}/*.sql 60 type LocalController struct { 61 schemaChangeDir string 62 keyspace string 63 sqlPath string 64 sqlFilename string 65 errorDir string 66 logDir string 67 completeDir string 68 } 69 70 // NewLocalController creates a new LocalController instance. 71 func NewLocalController(schemaChangeDir string) *LocalController { 72 return &LocalController{ 73 schemaChangeDir: schemaChangeDir, 74 } 75 } 76 77 // Open goes through the schema change dir and find a keyspace with a pending 78 // schema change. 79 func (controller *LocalController) Open(ctx context.Context) error { 80 // find all keyspace directories. 81 fileInfos, err := os.ReadDir(controller.schemaChangeDir) 82 if err != nil { 83 return err 84 } 85 for _, fileinfo := range fileInfos { 86 if !fileinfo.IsDir() { 87 continue 88 } 89 dirpath := path.Join(controller.schemaChangeDir, fileinfo.Name()) 90 schemaChanges, err := os.ReadDir(path.Join(dirpath, "input")) 91 if err != nil { 92 log.Warningf("there is no input dir in %s", dirpath) 93 continue 94 } 95 // found a schema change 96 if len(schemaChanges) > 0 { 97 controller.keyspace = fileinfo.Name() 98 controller.sqlFilename = schemaChanges[0].Name() 99 controller.sqlPath = path.Join(dirpath, "input", schemaChanges[0].Name()) 100 101 currentTime := time.Now() 102 datePart := fmt.Sprintf( 103 "%d/%d/%d", 104 currentTime.Year(), 105 currentTime.Month(), 106 currentTime.Day()) 107 108 controller.errorDir = path.Join(dirpath, "error", datePart) 109 controller.completeDir = path.Join(dirpath, "complete", datePart) 110 controller.logDir = path.Join(dirpath, "log", datePart) 111 // the remaining schema changes will be picked by the next runs 112 break 113 } 114 } 115 return nil 116 } 117 118 // Read reads schema changes. 119 func (controller *LocalController) Read(ctx context.Context) ([]string, error) { 120 if controller.keyspace == "" || controller.sqlPath == "" { 121 return []string{}, nil 122 } 123 data, err := os.ReadFile(controller.sqlPath) 124 if err != nil { 125 return nil, err 126 } 127 return strings.Split(string(data), ";"), nil 128 } 129 130 // Keyspace returns current keyspace that is ready for applying schema change. 131 func (controller *LocalController) Keyspace() string { 132 return controller.keyspace 133 } 134 135 // Close reset keyspace, sqlPath, errorDir, logDir and completeDir. 136 func (controller *LocalController) Close() { 137 controller.keyspace = "" 138 controller.sqlPath = "" 139 controller.sqlFilename = "" 140 controller.errorDir = "" 141 controller.logDir = "" 142 controller.completeDir = "" 143 } 144 145 // OnReadSuccess is no-op 146 func (controller *LocalController) OnReadSuccess(ctx context.Context) error { 147 return nil 148 } 149 150 // OnReadFail is no-op 151 func (controller *LocalController) OnReadFail(ctx context.Context, err error) error { 152 log.Errorf("failed to read file: %s, error: %v", controller.sqlPath, err) 153 return nil 154 } 155 156 // OnValidationSuccess is no-op 157 func (controller *LocalController) OnValidationSuccess(ctx context.Context) error { 158 return nil 159 } 160 161 // OnValidationFail is no-op 162 func (controller *LocalController) OnValidationFail(ctx context.Context, err error) error { 163 return controller.moveToErrorDir(ctx) 164 } 165 166 // OnExecutorComplete is no-op 167 func (controller *LocalController) OnExecutorComplete(ctx context.Context, result *ExecuteResult) error { 168 if len(result.FailedShards) > 0 || result.ExecutorErr != "" { 169 return controller.moveToErrorDir(ctx) 170 } 171 if err := os.MkdirAll(controller.completeDir, os.ModePerm); err != nil { 172 return err 173 } 174 if err := os.MkdirAll(controller.logDir, os.ModePerm); err != nil { 175 return err 176 } 177 178 if err := controller.writeToLogDir(ctx, result); err != nil { 179 return err 180 } 181 182 return os.Rename( 183 controller.sqlPath, 184 path.Join(controller.completeDir, controller.sqlFilename)) 185 } 186 187 func (controller *LocalController) moveToErrorDir(ctx context.Context) error { 188 if err := os.MkdirAll(controller.errorDir, os.ModePerm); err != nil { 189 return err 190 } 191 return os.Rename( 192 controller.sqlPath, 193 path.Join(controller.errorDir, controller.sqlFilename)) 194 } 195 196 func (controller *LocalController) writeToLogDir(ctx context.Context, result *ExecuteResult) error { 197 logFile, err := os.Create(path.Join(controller.logDir, controller.sqlFilename)) 198 if err != nil { 199 return err 200 } 201 defer logFile.Close() 202 203 logFile.WriteString(fmt.Sprintf("-- new file: %s\n", controller.sqlPath)) 204 for _, sql := range result.Sqls { 205 logFile.WriteString(sql) 206 logFile.WriteString(";\n") 207 } 208 rowsReturned := uint64(0) 209 rowsAffected := uint64(0) 210 for _, queryResult := range result.SuccessShards { 211 rowsReturned += uint64(len(queryResult.Result.Rows)) 212 rowsAffected += queryResult.Result.RowsAffected 213 } 214 logFile.WriteString(fmt.Sprintf("-- Rows returned: %d\n", rowsReturned)) 215 logFile.WriteString(fmt.Sprintf("-- Rows affected: %d\n", rowsAffected)) 216 logFile.WriteString("-- \n") 217 logFile.WriteString(fmt.Sprintf("-- ran in %fs\n", result.TotalTimeSpent.Seconds())) 218 logFile.WriteString("-- Execution succeeded\n") 219 return nil 220 } 221 222 var _ Controller = (*LocalController)(nil) 223 224 func init() { 225 RegisterControllerFactory( 226 "local", 227 func(params map[string]string) (Controller, error) { 228 schemaChangeDir, ok := params[SchemaChangeDirName] 229 if !ok { 230 return nil, fmt.Errorf("unable to construct a LocalController instance because param: %s is missing in params: %v", SchemaChangeDirName, params) 231 } 232 return NewLocalController(schemaChangeDir), nil 233 }, 234 ) 235 }