code.vegaprotocol.io/vega@v0.79.0/blockexplorer/store/migrator.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package store
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/georgysavva/scany/pgxscan"
    24  	"github.com/jackc/pgx/v4/pgxpool"
    25  )
    26  
    27  // Migrator is a data migration agent that will migrate tx_results data
    28  // that is required for providing data for the block explorer APIs.
    29  // The tx_results table is populated by Tendermint and the number of records
    30  // can be very large. This causes data migration difficult when there is a need
    31  // to add additional information to tx_results in order to fulfill requirements
    32  // for the block explorer APIs.
    33  // The migration agent will allow migrations to run in the background without
    34  // blocking upgrades with slow database migration scripts.
    35  type Migrator struct {
    36  	pool   *pgxpool.Pool
    37  	config Config
    38  }
    39  
    40  // NewMigrator creates a new data migration agent.
    41  func NewMigrator(pool *pgxpool.Pool, config Config) *Migrator {
    42  	return &Migrator{
    43  		pool:   pool,
    44  		config: config,
    45  	}
    46  }
    47  
    48  func (m *Migrator) checkCanMigrate(ctx context.Context) bool {
    49  	// We only want to migrate if we have a tx_results_old table
    50  	sql := `select table_name from information_schema.tables where table_name = 'tx_results_old'`
    51  	var tableName string
    52  	if err := m.pool.QueryRow(ctx, sql).Scan(&tableName); err != nil {
    53  		return false
    54  	}
    55  	return true
    56  }
    57  
    58  func (m *Migrator) cleanupOldData(ctx context.Context) error {
    59  	// we want to drop the old table if it exists
    60  	sql := `drop table if exists tx_results_old`
    61  	if _, err := m.pool.Exec(ctx, sql); err != nil {
    62  		return fmt.Errorf("could not drop old table: %w", err)
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // Migrate will run the data migration.
    69  func (m *Migrator) Migrate(ctx context.Context) error {
    70  	if !m.config.MigrateData {
    71  		return nil
    72  	}
    73  
    74  	if !m.checkCanMigrate(ctx) {
    75  		return nil
    76  	}
    77  
    78  	// create indexes on the tables that we will be querying
    79  	if err := m.createIndexes(ctx); err != nil {
    80  		return err
    81  	}
    82  
    83  	// get a list of dates that we need to migrate
    84  	migrateDates, err := m.getMigrationDates(ctx)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	// loop through each date and migrate the data for the date
    90  	for _, d := range migrateDates {
    91  		// we're going to make use of temporary tables which are only visible to the session that created them
    92  		// we therefore have to use a single connection and make sure that the same connection is used for the
    93  		// migration process.
    94  		conn, err := m.pool.Acquire(ctx)
    95  		if err != nil {
    96  			return fmt.Errorf("could not acquire connection: %w", err)
    97  		}
    98  		// if we error, we want to stop the migration rather than continue as we do
    99  		if err := m.doMigration(ctx, conn, d); err != nil {
   100  			return fmt.Errorf("could not migrate data for date %s: %w", d.Format("2006-01-02"), err)
   101  		}
   102  		// make sure we release the connection back to the pool when we're done
   103  		conn.Release()
   104  
   105  		// we want to pause so as not to hog all the processing and prevent block explorer from processing blocks
   106  		time.Sleep(m.config.MigratePauseInterval)
   107  	}
   108  
   109  	if err := m.cleanupOldData(ctx); err != nil {
   110  		return fmt.Errorf("could not drop redundant migration data: %w", err)
   111  	}
   112  
   113  	return nil
   114  }
   115  
   116  func (m *Migrator) getMigrationDates(ctx context.Context) ([]time.Time, error) {
   117  	sql := `create table if not exists migration_dates(
   118  		migration_date date primary key,
   119  		migrated bool default (false)
   120  	)`
   121  
   122  	if _, err := m.pool.Exec(ctx, sql); err != nil {
   123  		return nil, fmt.Errorf("could not create migration_dates table: %w", err)
   124  	}
   125  
   126  	// now let's populate the data we need, only new dates that aren't in the table will be added
   127  	sql = `insert into migration_dates(migration_date)
   128  		select distinct created_at::date
   129  		from blocks
   130  		on conflict do nothing`
   131  
   132  	if _, err := m.pool.Exec(ctx, sql); err != nil {
   133  		return nil, fmt.Errorf("could not populate migration_dates table: %w", err)
   134  	}
   135  
   136  	// now retrieve the dates that we need to migrate in reverse order because we want to migrate the latest
   137  	// data first
   138  	sql = `select migration_date from migration_dates where migrated = false order by migration_date desc`
   139  
   140  	var migrationDates []struct {
   141  		MigrationDate time.Time
   142  	}
   143  
   144  	if err := pgxscan.Select(ctx, m.pool, &migrationDates, sql); err != nil {
   145  		return nil, fmt.Errorf("could not retrieve migration dates: %w", err)
   146  	}
   147  
   148  	dates := make([]time.Time, len(migrationDates))
   149  
   150  	for i, d := range migrationDates {
   151  		dates[i] = d.MigrationDate
   152  	}
   153  
   154  	return dates, nil
   155  }
   156  
   157  func (m *Migrator) doMigration(ctx context.Context, conn *pgxpool.Conn, date time.Time) error {
   158  	startDate := date
   159  	endDate := date.AddDate(0, 0, 1)
   160  
   161  	tx, err := conn.Begin(ctx)
   162  	if err != nil {
   163  		return fmt.Errorf("could not start transaction: %w", err)
   164  	}
   165  
   166  	// pre-migration cleanup
   167  	cleanupSQL := []string{
   168  		`drop table if exists blocks_temp`,
   169  		`drop table if exists tx_results_temp`,
   170  	}
   171  
   172  	for _, sql := range cleanupSQL {
   173  		if _, err := tx.Exec(ctx, sql); err != nil {
   174  			tx.Rollback(ctx)
   175  			return fmt.Errorf("could not cleanup temporary tables: %w", err)
   176  		}
   177  	}
   178  
   179  	type migrationQuery struct {
   180  		SQL  string
   181  		args []any
   182  	}
   183  
   184  	// create a temporary table for the blocks that need to be migrated for the given date
   185  	migrateSQL := []migrationQuery{
   186  		{
   187  			// just get the blocks we need to update for the date
   188  			SQL:  `select * into blocks_temp from blocks where created_at >= $1 and created_at < $2`,
   189  			args: []any{startDate, endDate},
   190  		},
   191  		{
   192  			// and the tx_results for the date
   193  			SQL:  `select * into tx_results_temp from tx_results_old where created_at >= $1 and created_at < $2`,
   194  			args: []any{startDate, endDate},
   195  		},
   196  		{
   197  			// create an index on the temporary blocks table
   198  			SQL:  `create index idx_blocks_temp_rowid on blocks_temp(rowid)`,
   199  			args: []any{},
   200  		},
   201  		{
   202  			// create an index on the temporary tx_results table
   203  			SQL:  `create index idx_tx_results_temp_block_id on tx_results_temp(block_id)`,
   204  			args: []any{},
   205  		},
   206  		{
   207  			// update the tx_results_temp table with the block height for the date
   208  			SQL: `update tx_results_temp t
   209  				set block_height = b.height
   210  			from blocks_temp b
   211  			where t.block_id = b.rowid`,
   212  			args: []any{},
   213  		},
   214  	}
   215  
   216  	// moving a full day can cause a lock on the database and slow things down for the block explorer, so lets move things a block of X
   217  	// hours at a time, configurable in the settings, and default to 1 hour
   218  	migrationMoveSQL := make([]migrationQuery, 0)
   219  
   220  	moveStart := startDate
   221  	for {
   222  		if moveStart.Equal(endDate) || moveStart.After(endDate) {
   223  			break
   224  		}
   225  
   226  		moveEnd := moveStart.Add(m.config.MigrateBlockDuration)
   227  		migrationMoveSQL = append(migrationMoveSQL, migrationQuery{
   228  			SQL: `insert into tx_results(rowid, block_id, index, created_at, tx_hash, tx_result, submitter, cmd_type, block_height)
   229  			select rowid, block_id, index, created_at, tx_hash, tx_result, submitter, cmd_type, block_height
   230  			from tx_results_temp
   231  			where created_at >= $1 and created_at < $2
   232  			on conflict do nothing`,
   233  			args: []any{moveStart, moveEnd},
   234  		})
   235  
   236  		moveStart = moveStart.Add(m.config.MigrateBlockDuration)
   237  	}
   238  
   239  	// Once all the chunks have been moved, then we should record the migration date as completed so it won't be done again
   240  	migrationMoveSQL = append(migrationMoveSQL, migrationQuery{
   241  		SQL:  `update migration_dates set migrated = true where migration_date = $1`,
   242  		args: []any{date},
   243  	})
   244  
   245  	// finally we want to do the cleanup between migration dates
   246  	migrationCleanupSQL := []migrationQuery{
   247  		// now drop the temporary tables
   248  		{
   249  			SQL:  `drop table if exists blocks_temp`,
   250  			args: []any{},
   251  		},
   252  		{
   253  			SQL:  `drop table if exists tx_results_temp`,
   254  			args: []any{},
   255  		},
   256  	}
   257  
   258  	migrateSQL = append(migrateSQL, migrationMoveSQL...)
   259  	migrateSQL = append(migrateSQL, migrationCleanupSQL...)
   260  
   261  	for _, query := range migrateSQL {
   262  		if _, err := tx.Exec(ctx, query.SQL, query.args...); err != nil {
   263  			tx.Rollback(ctx)
   264  			return fmt.Errorf("could not migrate data for date %s: %w", date.Format("2006-01-02"), err)
   265  		}
   266  	}
   267  
   268  	if err := tx.Commit(ctx); err != nil {
   269  		tx.Rollback(ctx)
   270  		return fmt.Errorf("could not commit migration for date %s: %w", date.Format("2006-01-02"), err)
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  func (m *Migrator) createIndexes(ctx context.Context) error {
   277  	sql := `create index if not exists idx_tx_results_old_created_at on tx_results_old(created_at)`
   278  	// this index creation could take some time, but we don't know how long it should take so we don't want to timeout
   279  	if _, err := m.pool.Exec(ctx, sql); err != nil {
   280  		return fmt.Errorf("could not create created_at index for tx_results_old: %w", err)
   281  	}
   282  
   283  	sql = `create index if not exists idx_blocks_created_at on blocks(created_at)`
   284  	// this index creation could take some time, but we don't know how long it should take so we don't want to timeout
   285  	if _, err := m.pool.Exec(ctx, sql); err != nil {
   286  		return fmt.Errorf("could not create created_at index for blocks: %w", err)
   287  	}
   288  
   289  	return nil
   290  }