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  }