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 }