From b960661807db19950ae718580bd3fccb99ee0be0 Mon Sep 17 00:00:00 2001 From: Abdul Rabbani Date: Tue, 29 Mar 2022 16:45:26 -0400 Subject: [PATCH] Refactor: Decouple knownGaps and Indexer This commit decouples knownGaps and Indexer. All knownGaps logic is in its own file. This makes testing and maintainability easier. We have also removed all efforts to check the `lastProcessedblock` - This is because we won't ever run into this issue (hyptothetically), because geth won't let it happen. --- .gitignore | 1 + cmd/geth/config.go | 26 +- cmd/geth/main.go | 1 + cmd/geth/usage.go | 1 + cmd/utils/flags.go | 5 + statediff/config.go | 6 +- statediff/docs/KnownGaps.md | 1 - statediff/docs/database.md | 21 ++ statediff/docs/diagrams/KnownGapsProcess.png | Bin 53660 -> 33340 bytes statediff/indexer/constructor.go | 41 ++- statediff/indexer/database/dump/indexer.go | 24 -- statediff/indexer/database/file/indexer.go | 21 -- statediff/indexer/database/file/writer.go | 17 -- statediff/indexer/database/sql/indexer.go | 92 ------- .../database/sql/indexer_shared_test.go | 46 ---- .../indexer/database/sql/postgres/database.go | 1 - statediff/indexer/database/sql/writer.go | 13 - statediff/indexer/interfaces/interfaces.go | 4 - statediff/known_gaps.go | 243 ++++++++++++++++++ statediff/known_gaps_test.go | 218 ++++++++++++++++ statediff/service.go | 158 ++---------- statediff/service_public_test.go | 242 ----------------- statediff/statediffing_test_file.sql | 0 23 files changed, 540 insertions(+), 642 deletions(-) create mode 100644 statediff/docs/database.md create mode 100644 statediff/known_gaps.go create mode 100644 statediff/known_gaps_test.go delete mode 100644 statediff/service_public_test.go delete mode 100644 statediff/statediffing_test_file.sql diff --git a/.gitignore b/.gitignore index 6c7f48b51..f35ae8220 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ related-repositories/hive/** related-repositories/ipld-eth-db/** statediff/indexer/database/sql/statediffing_test_file.sql statediff/statediffing_test_file.sql +statediff/known_gaps.sql diff --git a/cmd/geth/config.go b/cmd/geth/config.go index b2340e938..21edd9a32 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -177,7 +177,6 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { if ctx.GlobalBool(utils.StateDiffFlag.Name) { var indexerConfig interfaces.Config - var fileConfig interfaces.Config var clientName, nodeID string if ctx.GlobalIsSet(utils.StateDiffWritingFlag.Name) { clientName = ctx.GlobalString(utils.StateDiffDBClientNameFlag.Name) @@ -192,15 +191,6 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { if err != nil { utils.Fatalf("%v", err) } - - if dbType != shared.FILE { - fileConfig = file.Config{ - FilePath: ctx.GlobalString(utils.StateDiffFilePath.Name), - } - } else { - fileConfig = nil - } - switch dbType { case shared.FILE: indexerConfig = file.Config{ @@ -262,14 +252,14 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } } p := statediff.Config{ - IndexerConfig: indexerConfig, - FileConfig: fileConfig, - ID: nodeID, - ClientName: clientName, - Context: context.Background(), - EnableWriteLoop: ctx.GlobalBool(utils.StateDiffWritingFlag.Name), - NumWorkers: ctx.GlobalUint(utils.StateDiffWorkersFlag.Name), - WaitForSync: ctx.GlobalBool(utils.StateDiffWaitForSync.Name), + IndexerConfig: indexerConfig, + KnownGapsFilePath: ctx.GlobalString(utils.StateDiffKnownGapsFilePath.Name), + ID: nodeID, + ClientName: clientName, + Context: context.Background(), + EnableWriteLoop: ctx.GlobalBool(utils.StateDiffWritingFlag.Name), + NumWorkers: ctx.GlobalUint(utils.StateDiffWorkersFlag.Name), + WaitForSync: ctx.GlobalBool(utils.StateDiffWaitForSync.Name), } utils.RegisterStateDiffService(stack, eth, &cfg.Eth, p, backend) } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 754c028ba..f931d3ffa 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -176,6 +176,7 @@ var ( utils.StateDiffWritingFlag, utils.StateDiffWorkersFlag, utils.StateDiffFilePath, + utils.StateDiffKnownGapsFilePath, utils.StateDiffWaitForSync, configFileFlag, } diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index b8fca81cc..c8338ac5f 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -246,6 +246,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.StateDiffWritingFlag, utils.StateDiffWorkersFlag, utils.StateDiffFilePath, + utils.StateDiffKnownGapsFilePath, utils.StateDiffWaitForSync, }, }, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 8afff991a..92f8cfc93 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -867,6 +867,11 @@ var ( Name: "statediff.file.path", Usage: "Full path (including filename) to write statediff data out to when operating in file mode", } + StateDiffKnownGapsFilePath = cli.StringFlag{ + Name: "statediff.knowngapsfile.path", + Usage: "Full path (including filename) to write knownGaps statements when the DB is unavailable.", + Value: "./known_gaps.sql", + } StateDiffDBClientNameFlag = cli.StringFlag{ Name: "statediff.db.clientname", Usage: "Client name to use when writing state diffs to database", diff --git a/statediff/config.go b/statediff/config.go index 535c46485..f20f3267e 100644 --- a/statediff/config.go +++ b/statediff/config.go @@ -26,10 +26,10 @@ import ( // Config contains instantiation parameters for the state diffing service type Config struct { - // The configuration used for the primary stateDiff Indexer + // The configuration used for the stateDiff Indexer IndexerConfig interfaces.Config - // The configuration used for the file stateDiff Indexer. This indexer is used if the primary indexer fails and is not a FILE indexer. - FileConfig interfaces.Config + // The filepath to write knownGaps insert statements if we can't connect to the DB. + KnownGapsFilePath string // A unique ID used for this service ID string // Name for the client this service is running diff --git a/statediff/docs/KnownGaps.md b/statediff/docs/KnownGaps.md index 51f069402..72e712f47 100644 --- a/statediff/docs/KnownGaps.md +++ b/statediff/docs/KnownGaps.md @@ -10,7 +10,6 @@ The known gaps table is updated when the following events occur: 1. At start up we check the latest block from the `eth.headers_cid` table. We compare the first block that we are processing with the latest block from the DB. If they are not one unit of expectedDifference away from each other, add the gap between the two blocks. 2. If there is any error in processing a block (db connection, deadlock, etc), add that block to the knownErrorBlocks slice, when the next block is successfully written, write this slice into the DB. -3. If the last processed block is not one unit of expectedDifference away from the current block being processed. This can be due to any unknown or unhandled errors in geth. # Glossary diff --git a/statediff/docs/database.md b/statediff/docs/database.md new file mode 100644 index 000000000..847bc8fa2 --- /dev/null +++ b/statediff/docs/database.md @@ -0,0 +1,21 @@ +# Overview + +This document will go through some notes on the database component of the statediff service. + +# Components + +- Indexer: The indexer creates IPLD and DB models to insert to the Postgres DB. It performs the insert utilizing and atomic function. +- Builder: The builder constructs the statediff object that needs to be inserted. +- Known Gaps: Captures any gaps that might have occured and either writes them to the DB, local sql file, to prometeus, or a local error. + +# Making Code Changes + +## Adding a New Function to the Indexer + +If you want to implement a new feature for adding data to the database. Keep the following in mind: + +1. You need to handle `sql`, `file`, and `dump`. + 1. `sql` - Contains the code needed to write directly to the `sql` db. + 2. `file` - Contains all the code required to write the SQL statements to a file. + 3. `dump` - Contains all the code for outputting events to the console. +2. You will have to add it to the `interfaces.StateDiffIndexer` interface. diff --git a/statediff/docs/diagrams/KnownGapsProcess.png b/statediff/docs/diagrams/KnownGapsProcess.png index 1da3fa68d18f40123d7151618b02fdbcfd4c002b..40ebaa80aad2fb03b6ff80ca364273e23039a7b9 100644 GIT binary patch literal 33340 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 literal 53660 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 Kk`?cbKK~D~S!BEb diff --git a/statediff/indexer/constructor.go b/statediff/indexer/constructor.go index f9d8920ce..1a4f64001 100644 --- a/statediff/indexer/constructor.go +++ b/statediff/indexer/constructor.go @@ -32,29 +32,22 @@ import ( ) // NewStateDiffIndexer creates and returns an implementation of the StateDiffIndexer interface. -// You can specify the specific -func NewStateDiffIndexer(ctx context.Context, chainConfig *params.ChainConfig, nodeInfo node.Info, config interfaces.Config, specificIndexer shared.DBType) (interfaces.StateDiffIndexer, error) { - var indexerToCreate shared.DBType - if specificIndexer == "" { - indexerToCreate = config.Type() - } else { - indexerToCreate = specificIndexer - } - log.Info("Indexer to create is", "indexer", indexerToCreate) - switch indexerToCreate { +func NewStateDiffIndexer(ctx context.Context, chainConfig *params.ChainConfig, nodeInfo node.Info, config interfaces.Config) (sql.Database, interfaces.StateDiffIndexer, error) { + switch config.Type() { case shared.FILE: - log.Info("Creating a statediff indexer in SQL file writing mode") + log.Info("Starting statediff service in SQL file writing mode") fc, ok := config.(file.Config) if !ok { - return nil, fmt.Errorf("file config is not the correct type: got %T, expected %T", config, file.Config{}) + return nil, nil, fmt.Errorf("file config is not the correct type: got %T, expected %T", config, file.Config{}) } fc.NodeInfo = nodeInfo - return file.NewStateDiffIndexer(ctx, chainConfig, fc) + ind, err := file.NewStateDiffIndexer(ctx, chainConfig, fc) + return nil, ind, err case shared.POSTGRES: - log.Info("Creating a statediff service in Postgres writing mode") + log.Info("Starting statediff service in Postgres writing mode") pgc, ok := config.(postgres.Config) if !ok { - return nil, fmt.Errorf("postgres config is not the correct type: got %T, expected %T", config, postgres.Config{}) + return nil, nil, fmt.Errorf("postgres config is not the correct type: got %T, expected %T", config, postgres.Config{}) } var err error var driver sql.Driver @@ -62,25 +55,27 @@ func NewStateDiffIndexer(ctx context.Context, chainConfig *params.ChainConfig, n case postgres.PGX: driver, err = postgres.NewPGXDriver(ctx, pgc, nodeInfo) if err != nil { - return nil, err + return nil, nil, err } case postgres.SQLX: driver, err = postgres.NewSQLXDriver(ctx, pgc, nodeInfo) if err != nil { - return nil, err + return nil, nil, err } default: - return nil, fmt.Errorf("unrecognized Postgres driver type: %s", pgc.Driver) + return nil, nil, fmt.Errorf("unrecognized Postgres driver type: %s", pgc.Driver) } - return sql.NewStateDiffIndexer(ctx, chainConfig, postgres.NewPostgresDB(driver)) + db := postgres.NewPostgresDB(driver) + ind, err := sql.NewStateDiffIndexer(ctx, chainConfig, db) + return db, ind, err case shared.DUMP: - log.Info("Creating statediff indexer in data dump mode") + log.Info("Starting statediff service in data dump mode") dumpc, ok := config.(dump.Config) if !ok { - return nil, fmt.Errorf("dump config is not the correct type: got %T, expected %T", config, dump.Config{}) + return nil, nil, fmt.Errorf("dump config is not the correct type: got %T, expected %T", config, dump.Config{}) } - return dump.NewStateDiffIndexer(chainConfig, dumpc), nil + return nil, dump.NewStateDiffIndexer(chainConfig, dumpc), nil default: - return nil, fmt.Errorf("unrecognized database type: %s", indexerToCreate) + return nil, nil, fmt.Errorf("unrecognized database type: %s", config.Type()) } } diff --git a/statediff/indexer/database/dump/indexer.go b/statediff/indexer/database/dump/indexer.go index 921fafed9..e450f941a 100644 --- a/statediff/indexer/database/dump/indexer.go +++ b/statediff/indexer/database/dump/indexer.go @@ -496,27 +496,3 @@ func (sdi *StateDiffIndexer) PushCodeAndCodeHash(batch interfaces.Batch, codeAnd func (sdi *StateDiffIndexer) Close() error { return sdi.dump.Close() } - -// Only needed to satisfy use cases -func (sdi *StateDiffIndexer) FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64, index interfaces.StateDiffIndexer) error { - log.Error("We can't find gaps in write mode!") - return fmt.Errorf("We can't find gaps in write mode!") -} - -// Written but not tested. Unsure if there is a real use case for this anywhere. -func (sdi *StateDiffIndexer) PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64, index interfaces.StateDiffIndexer) error { - log.Info("Dumping known gaps") - if startingBlockNumber.Cmp(endingBlockNumber) != -1 { - return fmt.Errorf("Starting Block %d, is greater than ending block %d", startingBlockNumber, endingBlockNumber) - } - knownGap := models.KnownGapsModel{ - StartingBlockNumber: startingBlockNumber.String(), - EndingBlockNumber: endingBlockNumber.String(), - CheckedOut: checkedOut, - ProcessingKey: processingKey, - } - if _, err := fmt.Fprintf(sdi.dump, "%+v\r\n", knownGap); err != nil { - return err - } - return nil -} diff --git a/statediff/indexer/database/file/indexer.go b/statediff/indexer/database/file/indexer.go index f11bf8f28..870c1f259 100644 --- a/statediff/indexer/database/file/indexer.go +++ b/statediff/indexer/database/file/indexer.go @@ -478,24 +478,3 @@ func (sdi *StateDiffIndexer) PushCodeAndCodeHash(batch interfaces.Batch, codeAnd func (sdi *StateDiffIndexer) Close() error { return sdi.fileWriter.Close() } - -func (sdi *StateDiffIndexer) FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64, indexer interfaces.StateDiffIndexer) error { - log.Error("We can't find gaps in write mode!") - return fmt.Errorf("We can't find gaps in write mode!") -} - -func (sdi *StateDiffIndexer) PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64, indexer interfaces.StateDiffIndexer) error { - log.Info("Writing Gaps to file") - if startingBlockNumber.Cmp(endingBlockNumber) != -1 { - return fmt.Errorf("Starting Block %d, is greater than ending block %d", startingBlockNumber, endingBlockNumber) - } - knownGap := models.KnownGapsModel{ - StartingBlockNumber: startingBlockNumber.String(), - EndingBlockNumber: endingBlockNumber.String(), - CheckedOut: checkedOut, - ProcessingKey: processingKey, - } - - sdi.fileWriter.upsertKnownGaps(knownGap) - return nil -} diff --git a/statediff/indexer/database/file/writer.go b/statediff/indexer/database/file/writer.go index 8010ec13d..48de0853d 100644 --- a/statediff/indexer/database/file/writer.go +++ b/statediff/indexer/database/file/writer.go @@ -155,14 +155,6 @@ const ( storageInsert = "INSERT INTO eth.storage_cids (header_id, state_path, storage_leaf_key, cid, storage_path, " + "node_type, diff, mh_key) VALUES ('%s', '\\x%x', '%s', '%s', '\\x%x', %d, %t, '%s');\n" - // INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) - //VALUES ($1, $2, $3, $4) - // ON CONFLICT (starting_block_number) DO UPDATE SET (ending_block_number, processing_key) = ($2, $4) - // WHERE eth.known_gaps.ending_block_number <= $2 - knownGapsInsert = "INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) " + - "VALUES ('%s', '%s', %t, %d) " + - "ON CONFLICT (starting_block_number) DO UPDATE SET (ending_block_number, processing_key) = ('%s', %d) " + - "WHERE eth.known_gaps.ending_block_number <= '%s';\n" ) func (sqw *SQLWriter) upsertNode(node nodeinfo.Info) { @@ -261,12 +253,3 @@ func (sqw *SQLWriter) upsertStorageCID(storageCID models.StorageNodeModel) { sqw.stmts <- []byte(fmt.Sprintf(storageInsert, storageCID.HeaderID, storageCID.StatePath, storageKey, storageCID.CID, storageCID.Path, storageCID.NodeType, true, storageCID.MhKey)) } - -func (sqw *SQLWriter) upsertKnownGaps(knownGaps models.KnownGapsModel) { - sqw.stmts <- []byte(fmt.Sprintf(knownGapsInsert, knownGaps.StartingBlockNumber, knownGaps.EndingBlockNumber, knownGaps.CheckedOut, knownGaps.ProcessingKey, - knownGaps.EndingBlockNumber, knownGaps.ProcessingKey, knownGaps.EndingBlockNumber)) - //knownGapsInsert = "INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) " + - // "VALUES ('%s', '%s', %t, %d) " + - // "ON CONFLICT (starting_block_number) DO UPDATE SET (ending_block_number, processing_key) = ('%s', %d) " + - // "WHERE eth.known_gaps.ending_block_number <= '%s';\n" -} diff --git a/statediff/indexer/database/sql/indexer.go b/statediff/indexer/database/sql/indexer.go index 8fbde648e..4f2b52434 100644 --- a/statediff/indexer/database/sql/indexer.go +++ b/statediff/indexer/database/sql/indexer.go @@ -555,95 +555,3 @@ func (sdi *StateDiffIndexer) Close() error { } // Update the known gaps table with the gap information. -func (sdi *StateDiffIndexer) PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64, fileIndexer interfaces.StateDiffIndexer) error { - if startingBlockNumber.Cmp(endingBlockNumber) != -1 { - return fmt.Errorf("Starting Block %d, is greater than ending block %d", startingBlockNumber, endingBlockNumber) - } - knownGap := models.KnownGapsModel{ - StartingBlockNumber: startingBlockNumber.String(), - EndingBlockNumber: endingBlockNumber.String(), - CheckedOut: checkedOut, - ProcessingKey: processingKey, - } - log.Info("Writing known gaps to the DB") - if err := sdi.dbWriter.upsertKnownGaps(knownGap); err != nil { - log.Warn("Error writing knownGaps to DB, writing them to file instead") - fileIndexer.PushKnownGaps(startingBlockNumber, endingBlockNumber, checkedOut, processingKey, nil) - return err - } - return nil -} - -// This is a simple wrapper function which will run QueryRow on the DB -func (sdi *StateDiffIndexer) QueryDb(queryString string) (string, error) { - var ret string - err := sdi.dbWriter.db.QueryRow(context.Background(), queryString).Scan(&ret) - if err != nil { - log.Error(fmt.Sprint("Can't properly query the DB for query: ", queryString)) - return "", err - } - return ret, nil -} - -// This function is a simple wrapper which will call QueryDb but the return value will be -// a big int instead of a string -func (sdi *StateDiffIndexer) QueryDbToBigInt(queryString string) (*big.Int, error) { - ret := new(big.Int) - res, err := sdi.QueryDb(queryString) - if err != nil { - return ret, err - } - ret, ok := ret.SetString(res, 10) - if !ok { - log.Error(fmt.Sprint("Can't turn the res ", res, "into a bigInt")) - return ret, fmt.Errorf("Can't turn %s into a bigInt", res) - } - return ret, nil -} - -// Users provide the latestBlockInDb and the latestBlockOnChain -// as well as the expected difference. This function does some simple math. -// The expected difference for the time being is going to be 1, but as we run -// More geth nodes, the expected difference might fluctuate. -func isGap(latestBlockInDb *big.Int, latestBlockOnChain *big.Int, expectedDifference *big.Int) bool { - latestBlock := big.NewInt(0) - if latestBlock.Sub(latestBlockOnChain, expectedDifference).Cmp(latestBlockInDb) != 0 { - log.Warn("We found a gap", "latestBlockInDb", latestBlockInDb, "latestBlockOnChain", latestBlockOnChain, "expectedDifference", expectedDifference) - return true - } - return false - -} - -// This function will check for Gaps and update the DB if gaps are found. -// The processingKey will currently be set to 0, but as we start to leverage horizontal scaling -// It might be a useful parameter to update depending on the geth node. -// TODO: -// REmove the return value -// Write to file if err in writing to DB -func (sdi *StateDiffIndexer) FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64, fileIndexer interfaces.StateDiffIndexer) error { - dbQueryString := "SELECT MAX(block_number) FROM eth.header_cids" - latestBlockInDb, err := sdi.QueryDbToBigInt(dbQueryString) - if err != nil { - return err - } - - gapExists := isGap(latestBlockInDb, latestBlockOnChain, expectedDifference) - if gapExists { - startBlock := big.NewInt(0) - endBlock := big.NewInt(0) - startBlock.Add(latestBlockInDb, expectedDifference) - endBlock.Sub(latestBlockOnChain, expectedDifference) - - log.Warn("Found Gaps starting at", "startBlock", startBlock, "endingBlock", endBlock) - err := sdi.PushKnownGaps(startBlock, endBlock, false, processingKey, fileIndexer) - if err != nil { - log.Error("We were unable to write the following gap to the DB", "start Block", startBlock, "endBlock", endBlock, "error", err) - // Write to file SQL file instead!!! - // If write to SQL file fails, write to disk. Handle this within the write to SQL file function! - return err - } - } - - return nil -} diff --git a/statediff/indexer/database/sql/indexer_shared_test.go b/statediff/indexer/database/sql/indexer_shared_test.go index e7c7e2f9f..1d11a5f8e 100644 --- a/statediff/indexer/database/sql/indexer_shared_test.go +++ b/statediff/indexer/database/sql/indexer_shared_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "math/big" "os" "testing" @@ -181,48 +180,3 @@ func tearDown(t *testing.T) { err := ind.Close() require.NoError(t, err) } -func TestKnownGapsUpsert(t *testing.T) { - testKnownGapsUpsert(t) - -} -func testKnownGapsUpsert(t *testing.T) { - gapDifference := big.NewInt(10) // Set a difference between latestBlock in DB and on Chain - expectedDifference := big.NewInt(1) // Set what the expected difference between latestBlock in DB and on Chain should be - - stateDiff, err := setupDb(t) - if err != nil { - t.Fatal(err) - } - - fileInd := setupFile(t) - - // Get the latest block from the DB - latestBlockInDb, err := stateDiff.QueryDbToBigInt("SELECT MAX(block_number) FROM eth.header_cids") - if err != nil { - t.Fatal("Can't find a block in the eth.header_cids table.. Please put one there") - } - - // Add the gapDifference for testing purposes - latestBlockOnChain := big.NewInt(0) - latestBlockOnChain.Add(latestBlockInDb, gapDifference) - - t.Log("The latest block on the chain is: ", latestBlockOnChain) - t.Log("The latest block on the DB is: ", latestBlockInDb) - - gapUpsertErr := stateDiff.FindAndUpdateGaps(latestBlockOnChain, expectedDifference, 0, fileInd) - require.NoError(t, gapUpsertErr) - - // Calculate what the start and end block should be in known_gaps - // And check to make sure it is properly inserted - startBlock := big.NewInt(0) - endBlock := big.NewInt(0) - startBlock.Add(latestBlockInDb, expectedDifference) - endBlock.Sub(latestBlockOnChain, expectedDifference) - - queryString := fmt.Sprintf("SELECT starting_block_number from eth.known_gaps WHERE starting_block_number = %d AND ending_block_number = %d", startBlock, endBlock) - - _, queryErr := stateDiff.QueryDb(queryString) // Figure out the string. - require.NoError(t, queryErr) - t.Logf("Updated Known Gaps table starting from, %d, and ending at, %d", startBlock, endBlock) - -} diff --git a/statediff/indexer/database/sql/postgres/database.go b/statediff/indexer/database/sql/postgres/database.go index 1564ad8b5..ab0658d4e 100644 --- a/statediff/indexer/database/sql/postgres/database.go +++ b/statediff/indexer/database/sql/postgres/database.go @@ -106,5 +106,4 @@ func (db *DB) InsertKnownGapsStm() string { return `INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) VALUES ($1, $2, $3, $4) ON CONFLICT (starting_block_number) DO UPDATE SET (ending_block_number, processing_key) = ($2, $4) WHERE eth.known_gaps.ending_block_number <= $2` - //return `INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) VALUES (1, 2, true, 1)` } diff --git a/statediff/indexer/database/sql/writer.go b/statediff/indexer/database/sql/writer.go index 34ea5bbfc..3f1dfc0b5 100644 --- a/statediff/indexer/database/sql/writer.go +++ b/statediff/indexer/database/sql/writer.go @@ -17,7 +17,6 @@ package sql import ( - "context" "fmt" "github.com/ethereum/go-ethereum/common" @@ -183,15 +182,3 @@ func (w *Writer) upsertStorageCID(tx Tx, storageCID models.StorageNodeModel) err } return nil } - -// Upserts known gaps to the DB. -// INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) VALUES ($1, $2, $3, $4) -func (w *Writer) upsertKnownGaps(knownGaps models.KnownGapsModel) error { - _, err := w.db.Exec(context.Background(), w.db.InsertKnownGapsStm(), - knownGaps.StartingBlockNumber, knownGaps.EndingBlockNumber, knownGaps.CheckedOut, knownGaps.ProcessingKey) - if err != nil { - return fmt.Errorf("error upserting known_gaps entry: %v", err) - } - - return nil -} diff --git a/statediff/indexer/interfaces/interfaces.go b/statediff/indexer/interfaces/interfaces.go index 09778be50..8f951230d 100644 --- a/statediff/indexer/interfaces/interfaces.go +++ b/statediff/indexer/interfaces/interfaces.go @@ -32,10 +32,6 @@ type StateDiffIndexer interface { PushStateNode(tx Batch, stateNode sdtypes.StateNode, headerID string) error PushCodeAndCodeHash(tx Batch, codeAndCodeHash sdtypes.CodeAndCodeHash) error ReportDBMetrics(delay time.Duration, quit <-chan bool) - FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64, indexer StateDiffIndexer) error - // The indexer at the end allows us to pass one indexer to another. - // We use then for the SQL indexer, we pass it the file indexer so it can write to file if writing to the DB fails. - PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64, indexer StateDiffIndexer) error io.Closer } diff --git a/statediff/known_gaps.go b/statediff/known_gaps.go new file mode 100644 index 000000000..f94b59262 --- /dev/null +++ b/statediff/known_gaps.go @@ -0,0 +1,243 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff + +import ( + "context" + "fmt" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/statediff/indexer/database/sql" + "github.com/ethereum/go-ethereum/statediff/indexer/models" +) + +var ( + knownGapsInsert = "INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) " + + "VALUES ('%s', '%s', %t, %d) " + + "ON CONFLICT (starting_block_number) DO UPDATE SET (ending_block_number, processing_key) = ('%s', %d) " + + "WHERE eth.known_gaps.ending_block_number <= '%s';\n" + dbQueryString = "SELECT MAX(block_number) FROM eth.header_cids" + defaultWriteFilePath = "./known_gaps.sql" +) + +type KnownGaps interface { + PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64) error + FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64) error +} + +type KnownGapsState struct { + // Should we check for gaps by looking at the DB and comparing the latest block with head + checkForGaps bool + // Arbitrary processingKey that can be used down the line to differentiate different geth nodes. + processingKey int64 + // This number indicates the expected difference between blocks. + // Currently, this is 1 since the geth node processes each block. But down the road this can be used in + // Tandom with the processingKey to differentiate block processing logic. + expectedDifference *big.Int + // Indicates if Geth is in an error state + // This is used to indicate the right time to upserts + errorState bool + // This array keeps track of errorBlocks as they occur. + // When the errorState is false again, we can process these blocks. + // Do we need a list, can we have /KnownStartErrorBlock and knownEndErrorBlock ints instead? + knownErrorBlocks []*big.Int + // The filepath to write SQL statements if we can't connect to the DB. + writeFilePath string + // DB object to use for reading and writing to the DB + db sql.Database +} + +func NewKnownGapsState(checkForGaps bool, processingKey int64, expectedDifference *big.Int, + errorState bool, writeFilePath string, db sql.Database) *KnownGapsState { + + return &KnownGapsState{ + checkForGaps: checkForGaps, + processingKey: processingKey, + expectedDifference: expectedDifference, + errorState: errorState, + writeFilePath: writeFilePath, + db: db, + } + +} + +func MinMax(array []*big.Int) (*big.Int, *big.Int) { + var max *big.Int = array[0] + var min *big.Int = array[0] + for _, value := range array { + if max.Cmp(value) == -1 { + max = value + } + if min.Cmp(value) == 1 { + min = value + } + } + return min, max +} + +// This function actually performs the write of the known gaps. It will try to do the following, it only goes to the next step if a failure occurs. +// 1. Write to the DB directly. +// 2. Write to sql file locally. +// 3. Write to prometheus directly. +// 4. Logs and error. +func (kg *KnownGapsState) PushKnownGaps(startingBlockNumber *big.Int, endingBlockNumber *big.Int, checkedOut bool, processingKey int64) error { + if startingBlockNumber.Cmp(endingBlockNumber) != -1 { + return fmt.Errorf("Starting Block %d, is greater than ending block %d", startingBlockNumber, endingBlockNumber) + } + knownGap := models.KnownGapsModel{ + StartingBlockNumber: startingBlockNumber.String(), + EndingBlockNumber: endingBlockNumber.String(), + CheckedOut: checkedOut, + ProcessingKey: processingKey, + } + log.Info("Writing known gaps to the DB") + + var writeErr error + if kg.db != nil { + dbErr := kg.upsertKnownGaps(knownGap) + if dbErr != nil { + log.Warn("Error writing knownGaps to DB, writing them to file instead") + writeErr = kg.upsertKnownGapsFile(knownGap) + } + } else { + writeErr = kg.upsertKnownGapsFile(knownGap) + + } + if writeErr != nil { + log.Info("Unsuccessful when writing to a file", "Error", writeErr) + return writeErr + } + return nil +} + +// This is a simple wrapper function to write gaps from a knownErrorBlocks array. +func (kg *KnownGapsState) captureErrorBlocks(knownErrorBlocks []*big.Int) { + startErrorBlock, endErrorBlock := MinMax(knownErrorBlocks) + + log.Warn("The following Gaps were found", "knownErrorBlocks", knownErrorBlocks) + log.Warn("Updating known Gaps table", "startErrorBlock", startErrorBlock, "endErrorBlock", endErrorBlock, "processingKey", kg.processingKey) + kg.PushKnownGaps(startErrorBlock, endErrorBlock, false, kg.processingKey) + +} + +// Users provide the latestBlockInDb and the latestBlockOnChain +// as well as the expected difference. This function does some simple math. +// The expected difference for the time being is going to be 1, but as we run +// More geth nodes, the expected difference might fluctuate. +func isGap(latestBlockInDb *big.Int, latestBlockOnChain *big.Int, expectedDifference *big.Int) bool { + latestBlock := big.NewInt(0) + if latestBlock.Sub(latestBlockOnChain, expectedDifference).Cmp(latestBlockInDb) != 0 { + log.Warn("We found a gap", "latestBlockInDb", latestBlockInDb, "latestBlockOnChain", latestBlockOnChain, "expectedDifference", expectedDifference) + return true + } + return false + +} + +// This function will check for Gaps and update the DB if gaps are found. +// The processingKey will currently be set to 0, but as we start to leverage horizontal scaling +// It might be a useful parameter to update depending on the geth node. +// TODO: +// REmove the return value +// Write to file if err in writing to DB +func (kg *KnownGapsState) FindAndUpdateGaps(latestBlockOnChain *big.Int, expectedDifference *big.Int, processingKey int64) error { + // Make this global + latestBlockInDb, err := kg.QueryDbToBigInt(dbQueryString) + if err != nil { + return err + } + + gapExists := isGap(latestBlockInDb, latestBlockOnChain, expectedDifference) + if gapExists { + startBlock := big.NewInt(0) + endBlock := big.NewInt(0) + startBlock.Add(latestBlockInDb, expectedDifference) + endBlock.Sub(latestBlockOnChain, expectedDifference) + + log.Warn("Found Gaps starting at", "startBlock", startBlock, "endingBlock", endBlock) + err := kg.PushKnownGaps(startBlock, endBlock, false, processingKey) + if err != nil { + log.Error("We were unable to write the following gap to the DB", "start Block", startBlock, "endBlock", endBlock, "error", err) + return err + } + } + + return nil +} + +// Upserts known gaps to the DB. +// INSERT INTO eth.known_gaps (starting_block_number, ending_block_number, checked_out, processing_key) VALUES ($1, $2, $3, $4) +func (kg *KnownGapsState) upsertKnownGaps(knownGaps models.KnownGapsModel) error { + _, err := kg.db.Exec(context.Background(), kg.db.InsertKnownGapsStm(), + knownGaps.StartingBlockNumber, knownGaps.EndingBlockNumber, knownGaps.CheckedOut, knownGaps.ProcessingKey) + if err != nil { + return fmt.Errorf("error upserting known_gaps entry: %v", err) + } + log.Info("Successfully Wrote gaps to the DB", "startBlock", knownGaps.StartingBlockNumber, "endBlock", knownGaps.EndingBlockNumber) + return nil +} + +func (kg *KnownGapsState) upsertKnownGapsFile(knownGaps models.KnownGapsModel) error { + insertStmt := []byte(fmt.Sprintf(knownGapsInsert, knownGaps.StartingBlockNumber, knownGaps.EndingBlockNumber, knownGaps.CheckedOut, knownGaps.ProcessingKey, + knownGaps.EndingBlockNumber, knownGaps.ProcessingKey, knownGaps.EndingBlockNumber)) + log.Info("Trying to write file") + if kg.writeFilePath == "" { + kg.writeFilePath = defaultWriteFilePath + } + f, err := os.OpenFile(kg.writeFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Info("Unable to open a file for writing") + return err + } + defer f.Close() + + if _, err = f.Write(insertStmt); err != nil { + log.Info("Unable to open write insert statement to file") + return err + } + log.Info("Wrote the gaps to a local SQL file") + return nil +} + +// This is a simple wrapper function which will run QueryRow on the DB +func (kg *KnownGapsState) QueryDb(queryString string) (string, error) { + var ret string + err := kg.db.QueryRow(context.Background(), queryString).Scan(&ret) + if err != nil { + log.Error(fmt.Sprint("Can't properly query the DB for query: ", queryString)) + return "", err + } + return ret, nil +} + +// This function is a simple wrapper which will call QueryDb but the return value will be +// a big int instead of a string +func (kg *KnownGapsState) QueryDbToBigInt(queryString string) (*big.Int, error) { + ret := new(big.Int) + res, err := kg.QueryDb(queryString) + if err != nil { + return ret, err + } + ret, ok := ret.SetString(res, 10) + if !ok { + log.Error(fmt.Sprint("Can't turn the res ", res, "into a bigInt")) + return ret, fmt.Errorf("Can't turn %s into a bigInt", res) + } + return ret, nil +} diff --git a/statediff/known_gaps_test.go b/statediff/known_gaps_test.go new file mode 100644 index 000000000..d7be1339c --- /dev/null +++ b/statediff/known_gaps_test.go @@ -0,0 +1,218 @@ +package statediff + +import ( + "context" + "fmt" + "io/ioutil" + "math/big" + "os" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/statediff/indexer/database/sql" + "github.com/ethereum/go-ethereum/statediff/indexer/database/sql/postgres" + "github.com/stretchr/testify/require" +) + +var ( + chainConf = params.MainnetChainConfig + knownGapsFilePath = "./known_gaps.sql" +) + +type gapValues struct { + lastProcessedBlock int64 + currentBlock int64 + knownErrorBlocksStart int64 + knownErrorBlocksEnd int64 + expectedDif int64 + processingKey int64 +} + +// Add clean db +// Test for failures when they are expected, when we go from smaller block to larger block +// We should no longer see the smaller block in DB +func TestKnownGaps(t *testing.T) { + + tests := []gapValues{ + // Known Gaps + {knownErrorBlocksStart: 115, knownErrorBlocksEnd: 120, expectedDif: 1, processingKey: 1}, + /// Same tests as above with a new expected DIF + {knownErrorBlocksStart: 1150, knownErrorBlocksEnd: 1200, expectedDif: 2, processingKey: 2}, + // Test update when block number is larger!! + {knownErrorBlocksStart: 1150, knownErrorBlocksEnd: 1204, expectedDif: 2, processingKey: 2}, + // Update when processing key is different! + {knownErrorBlocksStart: 1150, knownErrorBlocksEnd: 1204, expectedDif: 2, processingKey: 10}, + } + + testWriteToDb(t, tests, true) + testWriteToFile(t, tests, true) + //testFindAndUpdateGaps(t, true) +} + +// test writing blocks to the DB +func testWriteToDb(t *testing.T, tests []gapValues, wipeDbBeforeStart bool) { + t.Log("Starting Write to DB test") + db := setupDb(t) + + // Clear Table first, this is needed because we updated an entry to have a larger endblock number + // so we can't find the original start and endblock pair. + if wipeDbBeforeStart { + t.Log("Cleaning up eth.known_gaps table") + db.Exec(context.Background(), "DELETE FROM eth.known_gaps") + } + + for _, tc := range tests { + // Create an array with knownGaps based on user inputs + knownGaps := KnownGapsState{ + processingKey: tc.processingKey, + expectedDifference: big.NewInt(tc.expectedDif), + db: db, + } + service := &Service{ + KnownGaps: knownGaps, + } + knownErrorBlocks := (make([]*big.Int, 0)) + knownErrorBlocks = createKnownErrorBlocks(knownErrorBlocks, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) + service.KnownGaps.knownErrorBlocks = knownErrorBlocks + // Upsert + testCaptureErrorBlocks(t, service) + // Validate that the upsert was done correctly. + validateUpsert(t, service, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) + } + tearDown(t, db) + +} + +// test writing blocks to file and then inserting them to DB +func testWriteToFile(t *testing.T, tests []gapValues, wipeDbBeforeStart bool) { + t.Log("Starting write to file test") + db := setupDb(t) + // Clear Table first, this is needed because we updated an entry to have a larger endblock number + // so we can't find the original start and endblock pair. + if wipeDbBeforeStart { + t.Log("Cleaning up eth.known_gaps table") + db.Exec(context.Background(), "DELETE FROM eth.known_gaps") + } + if _, err := os.Stat(knownGapsFilePath); err == nil { + err := os.Remove(knownGapsFilePath) + if err != nil { + t.Fatal("Can't delete local file") + } + } + tearDown(t, db) + for _, tc := range tests { + knownGaps := KnownGapsState{ + processingKey: tc.processingKey, + expectedDifference: big.NewInt(tc.expectedDif), + writeFilePath: knownGapsFilePath, + db: nil, // Only set to nil to be verbose that we can't use it + } + service := &Service{ + KnownGaps: knownGaps, + } + knownErrorBlocks := (make([]*big.Int, 0)) + knownErrorBlocks = createKnownErrorBlocks(knownErrorBlocks, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) + service.KnownGaps.knownErrorBlocks = knownErrorBlocks + + testCaptureErrorBlocks(t, service) + + file, ioErr := ioutil.ReadFile(knownGapsFilePath) + require.NoError(t, ioErr) + + requests := strings.Split(string(file), ";") + + newDb := setupDb(t) + service.KnownGaps.db = newDb + for _, request := range requests { + _, err := newDb.Exec(context.Background(), request) + require.NoError(t, err) + } + // Validate that the upsert was done correctly. + validateUpsert(t, service, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) + tearDown(t, newDb) + } +} + +// Find a gap, if no gaps exist, it will create an arbitrary one +func testFindAndUpdateGaps(t *testing.T, wipeDbBeforeStart bool) { + db := setupDb(t) + + if wipeDbBeforeStart { + db.Exec(context.Background(), "DELETE FROM eth.known_gaps") + } + knownGaps := KnownGapsState{ + processingKey: 1, + expectedDifference: big.NewInt(1), + db: db, + } + service := &Service{ + KnownGaps: knownGaps, + } + + latestBlockInDb, err := service.KnownGaps.QueryDbToBigInt("SELECT MAX(block_number) FROM eth.header_cids") + if err != nil { + t.Skip("Can't find a block in the eth.header_cids table.. Please put one there") + } + + // Add the gapDifference for testing purposes + gapDifference := big.NewInt(10) // Set a difference between latestBlock in DB and on Chain + expectedDifference := big.NewInt(1) // Set what the expected difference between latestBlock in DB and on Chain should be + + latestBlockOnChain := big.NewInt(0) + latestBlockOnChain.Add(latestBlockInDb, gapDifference) + + t.Log("The latest block on the chain is: ", latestBlockOnChain) + t.Log("The latest block on the DB is: ", latestBlockInDb) + + gapUpsertErr := service.KnownGaps.FindAndUpdateGaps(latestBlockOnChain, expectedDifference, 0) + require.NoError(t, gapUpsertErr) + + startBlock := big.NewInt(0) + endBlock := big.NewInt(0) + + startBlock.Add(latestBlockInDb, gapDifference) + endBlock.Sub(latestBlockOnChain, gapDifference) + validateUpsert(t, service, startBlock.Int64(), endBlock.Int64()) + +} + +// test capturing missed blocks +func testCaptureErrorBlocks(t *testing.T, service *Service) { + service.KnownGaps.captureErrorBlocks(service.KnownGaps.knownErrorBlocks) +} + +// Helper function to create an array of gaps given a start and end block +func createKnownErrorBlocks(knownErrorBlocks []*big.Int, knownErrorBlocksStart int64, knownErrorBlocksEnd int64) []*big.Int { + for i := knownErrorBlocksStart; i <= knownErrorBlocksEnd; i++ { + knownErrorBlocks = append(knownErrorBlocks, big.NewInt(i)) + } + return knownErrorBlocks +} + +// Make sure the upsert was performed correctly +func validateUpsert(t *testing.T, service *Service, startingBlock int64, endingBlock int64) { + t.Logf("Starting to query blocks: %d - %d", startingBlock, endingBlock) + queryString := fmt.Sprintf("SELECT starting_block_number from eth.known_gaps WHERE starting_block_number = %d AND ending_block_number = %d", startingBlock, endingBlock) + + _, queryErr := service.KnownGaps.QueryDb(queryString) // Figure out the string. + t.Logf("Updated Known Gaps table starting from, %d, and ending at, %d", startingBlock, endingBlock) + require.NoError(t, queryErr) +} + +// Create a DB object to use +func setupDb(t *testing.T) sql.Database { + db, err := postgres.SetupSQLXDB() + if err != nil { + t.Error("Can't create a DB connection....") + t.Fatal(err) + } + // stateDiff, err := sql.NewStateDiffIndexer(context.Background(), chainConf, db) + return db +} + +// Teardown the DB +func tearDown(t *testing.T, db sql.Database) { + t.Log("Starting tearDown") + db.Close() +} diff --git a/statediff/service.go b/statediff/service.go index f68490fe5..5bacd9ad7 100644 --- a/statediff/service.go +++ b/statediff/service.go @@ -18,7 +18,6 @@ package statediff import ( "bytes" - "fmt" "math/big" "strconv" "strings" @@ -42,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" ind "github.com/ethereum/go-ethereum/statediff/indexer" + "github.com/ethereum/go-ethereum/statediff/indexer/database/sql" "github.com/ethereum/go-ethereum/statediff/indexer/interfaces" nodeinfo "github.com/ethereum/go-ethereum/statediff/indexer/node" "github.com/ethereum/go-ethereum/statediff/indexer/shared" @@ -140,29 +140,6 @@ type Service struct { } // This structure keeps track of the knownGaps at any given moment in time -type KnownGapsState struct { - // Should we check for gaps by looking at the DB and comparing the latest block with head - checkForGaps bool - // Arbitrary processingKey that can be used down the line to differentiate different geth nodes. - processingKey int64 - // This number indicates the expected difference between blocks. - // Currently, this is 1 since the geth node processes each block. But down the road this can be used in - // Tandom with the processingKey to differentiate block processing logic. - expectedDifference *big.Int - // Indicates if Geth is in an error state - // This is used to indicate the right time to upserts - errorState bool - // This array keeps track of errorBlocks as they occur. - // When the errorState is false again, we can process these blocks. - // Do we need a list, can we have /KnownStartErrorBlock and knownEndErrorBlock ints instead? - knownErrorBlocks []*big.Int - // The last processed block keeps track of the last processed block. - // Its used to make sure we didn't skip over any block! - lastProcessedBlock *big.Int - // This fileIndexer is used to write the knownGaps to file - // If we can't properly write to DB - fileIndexer interfaces.StateDiffIndexer -} // BlockCache caches the last block for safe access from different service loops type BlockCache struct { @@ -183,7 +160,7 @@ func NewBlockCache(max uint) BlockCache { func New(stack *node.Node, ethServ *eth.Ethereum, cfg *ethconfig.Config, params Config, backend ethapi.Backend) error { blockChain := ethServ.BlockChain() var indexer interfaces.StateDiffIndexer - var fileIndexer interfaces.StateDiffIndexer + var db sql.Database quitCh := make(chan bool) if params.IndexerConfig != nil { info := nodeinfo.Info{ @@ -194,31 +171,13 @@ func New(stack *node.Node, ethServ *eth.Ethereum, cfg *ethconfig.Config, params ClientName: params.ClientName, } var err error - indexer, err = ind.NewStateDiffIndexer(params.Context, blockChain.Config(), info, params.IndexerConfig, "") + db, indexer, err = ind.NewStateDiffIndexer(params.Context, blockChain.Config(), info, params.IndexerConfig) if err != nil { - log.Error("Error creating indexer", "indexer: ", params.IndexerConfig.Type(), "error: ", err) return err } - if params.FileConfig != nil { - fileIndexer, err = ind.NewStateDiffIndexer(params.Context, blockChain.Config(), info, params.FileConfig, shared.FILE) - if err != nil { - log.Error("Error creating file indexer", "error: ", err) - return err - } - } else { - fileIndexer = indexer - } - //fileIndexer, fileErr = file.NewStateDiffIndexer(params.Context, blockChain.Config(), info) indexer.ReportDBMetrics(10*time.Second, quitCh) } - var checkForGaps bool - if params.IndexerConfig.Type() == shared.POSTGRES { - checkForGaps = true - } else { - log.Info("We are not going to check for gaps on start up since we are not connected to Postgres!") - checkForGaps = false - } workers := params.NumWorkers if workers == 0 { workers = 1 @@ -226,11 +185,17 @@ func New(stack *node.Node, ethServ *eth.Ethereum, cfg *ethconfig.Config, params // If we ever have multiple processingKeys we can update them here // along with the expectedDifference knownGaps := &KnownGapsState{ - checkForGaps: checkForGaps, processingKey: 0, expectedDifference: big.NewInt(1), errorState: false, - fileIndexer: fileIndexer, + writeFilePath: params.KnownGapsFilePath, + db: db, + } + if params.IndexerConfig.Type() == shared.POSTGRES { + knownGaps.checkForGaps = true + } else { + log.Info("We are not going to check for gaps on start up since we are not connected to Postgres!") + knownGaps.checkForGaps = false } sds := &Service{ Mutex: sync.Mutex{}, @@ -348,75 +313,6 @@ func (sds *Service) writeGenesisStateDiff(currBlock *types.Block, workerId uint) statediffMetrics.lastStatediffHeight.Update(genesisBlockNumber) } -// This function will capture any missed blocks that were not captured in sds.KnownGaps.knownErrorBlocks. -// It is invoked when the sds.KnownGaps.lastProcessed block is not one unit -// away from sds.KnownGaps.expectedDifference -// Essentially, if geth ever misses blocks but doesn't output an error, we are covered. -func (sds *Service) captureMissedBlocks(currentBlock *big.Int, knownErrorBlocks []*big.Int, lastProcessedBlock *big.Int) { - // last processed: 110 - // current block: 125 - log.Debug("current block", "block number: ", currentBlock) - log.Debug("knownErrorBlocks", "knownErrorBlocks: ", knownErrorBlocks) - log.Debug("last processed block", "block number: ", lastProcessedBlock) - log.Debug("expected difference", "sds.KnownGaps.expectedDifference: ", sds.KnownGaps.expectedDifference) - - if len(knownErrorBlocks) > 0 { - // 115 - startErrorBlock := new(big.Int).Set(knownErrorBlocks[0]) - // 120 - endErrorBlock := new(big.Int).Set(knownErrorBlocks[len(knownErrorBlocks)-1]) - - // 111 - expectedStartErrorBlock := big.NewInt(0).Add(lastProcessedBlock, sds.KnownGaps.expectedDifference) - // 124 - expectedEndErrorBlock := big.NewInt(0).Sub(currentBlock, sds.KnownGaps.expectedDifference) - - if (expectedStartErrorBlock.Cmp(startErrorBlock) != 0) && - (expectedEndErrorBlock.Cmp(endErrorBlock) != 0) { - log.Info("All Gaps already captured in knownErrorBlocks") - } - - if expectedEndErrorBlock.Cmp(endErrorBlock) == 1 { - log.Warn("There are gaps in the knownErrorBlocks list", "knownErrorBlocks", knownErrorBlocks) - log.Warn("But there are gaps that were also not added there.") - log.Warn("Last Block in knownErrorBlocks", "endErrorBlock", endErrorBlock) - log.Warn("Last processed Block", "lastProcessedBlock", lastProcessedBlock) - log.Warn("Current Block", "currentBlock", currentBlock) - //120 + 1 == 121 - startBlock := big.NewInt(0).Add(endErrorBlock, sds.KnownGaps.expectedDifference) - // 121 to 124 - log.Warn(fmt.Sprintf("Adding the following block range to known_gaps table: %d - %d", startBlock, expectedEndErrorBlock)) - sds.indexer.PushKnownGaps(startBlock, expectedEndErrorBlock, false, sds.KnownGaps.processingKey, sds.KnownGaps.fileIndexer) - } - - if expectedStartErrorBlock.Cmp(startErrorBlock) == -1 { - log.Warn("There are gaps in the knownErrorBlocks list", "knownErrorBlocks", knownErrorBlocks) - log.Warn("But there are gaps that were also not added there.") - log.Warn("First Block in knownErrorBlocks", "startErrorBlock", startErrorBlock) - log.Warn("Last processed Block", "lastProcessedBlock", lastProcessedBlock) - // 115 - 1 == 114 - endBlock := big.NewInt(0).Sub(startErrorBlock, sds.KnownGaps.expectedDifference) - // 111 to 114 - log.Warn(fmt.Sprintf("Adding the following block range to known_gaps table: %d - %d", expectedStartErrorBlock, endBlock)) - sds.indexer.PushKnownGaps(expectedStartErrorBlock, endBlock, false, sds.KnownGaps.processingKey, sds.KnownGaps.fileIndexer) - } - - log.Warn("The following Gaps were found", "knownErrorBlocks", knownErrorBlocks) - log.Warn(fmt.Sprint("Updating known Gaps table from ", startErrorBlock, " to ", endErrorBlock, " with processing key, ", sds.KnownGaps.processingKey)) - sds.indexer.PushKnownGaps(startErrorBlock, endErrorBlock, false, sds.KnownGaps.processingKey, sds.KnownGaps.fileIndexer) - - } else { - log.Warn("We missed blocks without any errors.") - // 110 + 1 == 111 - startBlock := big.NewInt(0).Add(lastProcessedBlock, sds.KnownGaps.expectedDifference) - // 125 - 1 == 124 - endBlock := big.NewInt(0).Sub(currentBlock, sds.KnownGaps.expectedDifference) - log.Warn("Missed blocks starting from", "startBlock", startBlock) - log.Warn("Missed blocks ending at", "endBlock", endBlock) - sds.indexer.PushKnownGaps(startBlock, endBlock, false, sds.KnownGaps.processingKey, sds.KnownGaps.fileIndexer) - } -} - func (sds *Service) writeLoopWorker(params workerParams) { defer params.wg.Done() for { @@ -440,7 +336,7 @@ func (sds *Service) writeLoopWorker(params workerParams) { // Check and update the gaps table. if sds.KnownGaps.checkForGaps && !sds.KnownGaps.errorState { log.Info("Checking for Gaps at", "current block", currentBlock.Number()) - go sds.indexer.FindAndUpdateGaps(currentBlock.Number(), sds.KnownGaps.expectedDifference, sds.KnownGaps.processingKey, sds.KnownGaps.fileIndexer) + go sds.KnownGaps.FindAndUpdateGaps(currentBlock.Number(), sds.KnownGaps.expectedDifference, sds.KnownGaps.processingKey) sds.KnownGaps.checkForGaps = false } @@ -449,34 +345,22 @@ func (sds *Service) writeLoopWorker(params workerParams) { if err != nil { log.Error("statediff.Service.WriteLoop: processing error", "block height", currentBlock.Number().Uint64(), "error", err.Error(), "worker", params.id) sds.KnownGaps.errorState = true + log.Warn("Updating the following block to knownErrorBlocks to be inserted into knownGaps table", "blockNumber", currentBlock.Number()) sds.KnownGaps.knownErrorBlocks = append(sds.KnownGaps.knownErrorBlocks, currentBlock.Number()) - log.Warn("Updating the following block to knownErrorBlocks to be inserted into knownGaps table", "block number", currentBlock.Number()) // Write object to startdiff continue } sds.KnownGaps.errorState = false - // Understand what the last block that should have been processed is - previousExpectedBlock := big.NewInt(0).Sub(currentBlock.Number(), sds.KnownGaps.expectedDifference) - // If we last block which should have been processed is not - // the actual lastProcessedBlock, add it to known gaps table. - if sds.KnownGaps.lastProcessedBlock != nil { - // Can't combine the two if statements because you can't compare a nil object... - if previousExpectedBlock.Cmp(sds.KnownGaps.lastProcessedBlock) != 0 { - // We must pass in parameters by VALUE not reference. - // If we pass them in my reference, the references can change before the computation is complete! - staticKnownErrorBlocks := make([]*big.Int, len(sds.KnownGaps.knownErrorBlocks)) - copy(staticKnownErrorBlocks, sds.KnownGaps.knownErrorBlocks) - staticLastProcessedBlock := new(big.Int).Set(sds.KnownGaps.lastProcessedBlock) + if sds.KnownGaps.knownErrorBlocks != nil { + // We must pass in parameters by VALUE not reference. + // If we pass them in my reference, the references can change before the computation is complete! + staticKnownErrorBlocks := make([]*big.Int, len(sds.KnownGaps.knownErrorBlocks)) + copy(staticKnownErrorBlocks, sds.KnownGaps.knownErrorBlocks) + sds.KnownGaps.knownErrorBlocks = nil - log.Debug("Starting capturedMissedBlocks") - log.Debug("previousExpectedBlock is", "previous expected block: ", previousExpectedBlock) - log.Debug("sds.KnownGaps.lastProcessedBlock is", "sds.KnownGaps.lastProcessedBlock: ", sds.KnownGaps.lastProcessedBlock) - - go sds.captureMissedBlocks(currentBlock.Number(), staticKnownErrorBlocks, staticLastProcessedBlock) - sds.KnownGaps.knownErrorBlocks = nil - } + log.Debug("Starting capturedMissedBlocks") + go sds.KnownGaps.captureErrorBlocks(staticKnownErrorBlocks) } - sds.KnownGaps.lastProcessedBlock = currentBlock.Number() // TODO: how to handle with concurrent workers statediffMetrics.lastStatediffHeight.Update(int64(currentBlock.Number().Uint64())) diff --git a/statediff/service_public_test.go b/statediff/service_public_test.go deleted file mode 100644 index ed2c5d3ec..000000000 --- a/statediff/service_public_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package statediff - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "math/big" - "os" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/statediff/indexer/database/file" - "github.com/ethereum/go-ethereum/statediff/indexer/database/sql" - "github.com/ethereum/go-ethereum/statediff/indexer/database/sql/postgres" - "github.com/ethereum/go-ethereum/statediff/indexer/interfaces" - "github.com/ethereum/go-ethereum/statediff/indexer/mocks" - "github.com/stretchr/testify/require" -) - -var ( - db sql.Database - err error - indexer interfaces.StateDiffIndexer - chainConf = params.MainnetChainConfig -) - -type gapValues struct { - lastProcessedBlock int64 - currentBlock int64 - knownErrorBlocksStart int64 - knownErrorBlocksEnd int64 - expectedDif int64 - processingKey int64 -} - -// Add clean db -// Test for failures when they are expected, when we go from smaller block to larger block -// We should no longer see the smaller block in DB -func TestKnownGaps(t *testing.T) { - - tests := []gapValues{ - // Unprocessed gaps before and after knownErrorBlock - {lastProcessedBlock: 110, knownErrorBlocksStart: 115, knownErrorBlocksEnd: 120, currentBlock: 125, expectedDif: 1, processingKey: 1}, - // No knownErrorBlocks - {lastProcessedBlock: 130, knownErrorBlocksStart: 0, knownErrorBlocksEnd: 0, currentBlock: 140, expectedDif: 1, processingKey: 1}, - // No gaps before or after knownErrorBlocks - {lastProcessedBlock: 150, knownErrorBlocksStart: 151, knownErrorBlocksEnd: 159, currentBlock: 160, expectedDif: 1, processingKey: 1}, - // gaps before knownErrorBlocks but not after - {lastProcessedBlock: 170, knownErrorBlocksStart: 180, knownErrorBlocksEnd: 189, currentBlock: 190, expectedDif: 1, processingKey: 1}, - // gaps after knownErrorBlocks but not before - {lastProcessedBlock: 200, knownErrorBlocksStart: 201, knownErrorBlocksEnd: 205, currentBlock: 210, expectedDif: 1, processingKey: 1}, - /// Same tests as above with a new expected DIF - // Unprocessed gaps before and after knownErrorBlock - {lastProcessedBlock: 1100, knownErrorBlocksStart: 1150, knownErrorBlocksEnd: 1200, currentBlock: 1250, expectedDif: 2, processingKey: 2}, - // No knownErrorBlocks - {lastProcessedBlock: 1300, knownErrorBlocksStart: 0, knownErrorBlocksEnd: 0, currentBlock: 1400, expectedDif: 2, processingKey: 2}, - // No gaps before or after knownErrorBlocks - {lastProcessedBlock: 1500, knownErrorBlocksStart: 1502, knownErrorBlocksEnd: 1598, currentBlock: 1600, expectedDif: 2, processingKey: 2}, - // gaps before knownErrorBlocks but not after - {lastProcessedBlock: 1700, knownErrorBlocksStart: 1800, knownErrorBlocksEnd: 1898, currentBlock: 1900, expectedDif: 2, processingKey: 2}, - // gaps after knownErrorBlocks but not before - {lastProcessedBlock: 2000, knownErrorBlocksStart: 2002, knownErrorBlocksEnd: 2050, currentBlock: 2100, expectedDif: 2, processingKey: 2}, - // Test update when block number is larger!! - {lastProcessedBlock: 2000, knownErrorBlocksStart: 2002, knownErrorBlocksEnd: 2052, currentBlock: 2100, expectedDif: 2, processingKey: 2}, - // Update when processing key is different! - {lastProcessedBlock: 2000, knownErrorBlocksStart: 2002, knownErrorBlocksEnd: 2052, currentBlock: 2100, expectedDif: 2, processingKey: 10}, - } - - testWriteToDb(t, tests, true) - testWriteToFile(t, tests, true) -} - -// test writing blocks to the DB -func testWriteToDb(t *testing.T, tests []gapValues, wipeDbBeforeStart bool) { - stateDiff, db, err := setupDb(t) - require.NoError(t, err) - - // Clear Table first, this is needed because we updated an entry to have a larger endblock number - // so we can't find the original start and endblock pair. - if wipeDbBeforeStart { - db.Exec(context.Background(), "DELETE FROM eth.known_gaps") - } - - for _, tc := range tests { - // Create an array with knownGaps based on user inputs - checkGaps := tc.knownErrorBlocksStart != 0 && tc.knownErrorBlocksEnd != 0 - knownErrorBlocks := (make([]*big.Int, 0)) - if checkGaps { - knownErrorBlocks = createKnownErrorBlocks(knownErrorBlocks, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) - } - // Upsert - testCaptureMissedBlocks(t, tc.lastProcessedBlock, tc.currentBlock, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd, - tc.expectedDif, tc.processingKey, stateDiff, knownErrorBlocks, nil) - // Validate that the upsert was done correctly. - callValidateUpsert(t, checkGaps, stateDiff, tc.lastProcessedBlock, tc.currentBlock, tc.expectedDif, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) - } - tearDown(t, stateDiff) - -} - -// test writing blocks to file and then inserting them to DB -func testWriteToFile(t *testing.T, tests []gapValues, wipeDbBeforeStart bool) { - stateDiff, db, err := setupDb(t) - require.NoError(t, err) - - // Clear Table first, this is needed because we updated an entry to have a larger endblock number - // so we can't find the original start and endblock pair. - if wipeDbBeforeStart { - db.Exec(context.Background(), "DELETE FROM eth.known_gaps") - } - - tearDown(t, stateDiff) - for _, tc := range tests { - // Reuse processing key from expecteDiff - checkGaps := tc.knownErrorBlocksStart != 0 && tc.knownErrorBlocksEnd != 0 - knownErrorBlocks := (make([]*big.Int, 0)) - if checkGaps { - knownErrorBlocks = createKnownErrorBlocks(knownErrorBlocks, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) - } - - fileInd := setupFile(t) - testCaptureMissedBlocks(t, tc.lastProcessedBlock, tc.currentBlock, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd, - tc.expectedDif, tc.processingKey, stateDiff, knownErrorBlocks, fileInd) - fileInd.Close() - - newStateDiff, db, _ := setupDb(t) - - file, ioErr := ioutil.ReadFile(file.TestConfig.FilePath) - require.NoError(t, ioErr) - - requests := strings.Split(string(file), ";") - - // Skip the first two enteries - for _, request := range requests[2:] { - _, err := db.Exec(context.Background(), request) - require.NoError(t, err) - } - callValidateUpsert(t, checkGaps, newStateDiff, tc.lastProcessedBlock, tc.currentBlock, tc.expectedDif, tc.knownErrorBlocksStart, tc.knownErrorBlocksEnd) - } -} - -// test capturing missed blocks -func testCaptureMissedBlocks(t *testing.T, lastBlockProcessed int64, currentBlockNum int64, knownErrorBlocksStart int64, knownErrorBlocksEnd int64, expectedDif int64, processingKey int64, - stateDiff *sql.StateDiffIndexer, knownErrorBlocks []*big.Int, fileInd interfaces.StateDiffIndexer) { - - lastProcessedBlock := big.NewInt(lastBlockProcessed) - currentBlock := big.NewInt(currentBlockNum) - - knownGaps := KnownGapsState{ - processingKey: processingKey, - expectedDifference: big.NewInt(expectedDif), - fileIndexer: fileInd, - } - service := &Service{ - KnownGaps: knownGaps, - indexer: stateDiff, - } - service.captureMissedBlocks(currentBlock, knownErrorBlocks, lastProcessedBlock) -} - -// Helper function to create an array of gaps given a start and end block -func createKnownErrorBlocks(knownErrorBlocks []*big.Int, knownErrorBlocksStart int64, knownErrorBlocksEnd int64) []*big.Int { - for i := knownErrorBlocksStart; i <= knownErrorBlocksEnd; i++ { - knownErrorBlocks = append(knownErrorBlocks, big.NewInt(i)) - } - return knownErrorBlocks -} - -// This function will call the validateUpsert function based on various conditions. -func callValidateUpsert(t *testing.T, checkGaps bool, stateDiff *sql.StateDiffIndexer, - lastBlockProcessed int64, currentBlockNum int64, expectedDif int64, knownErrorBlocksStart int64, knownErrorBlocksEnd int64) { - // If there are gaps in knownErrorBlocks array - if checkGaps { - - // If there are no unexpected gaps before or after the entries in the knownErrorBlocks array - // Only handle the knownErrorBlocks Array - if lastBlockProcessed+expectedDif == knownErrorBlocksStart && knownErrorBlocksEnd+expectedDif == currentBlockNum { - validateUpsert(t, stateDiff, knownErrorBlocksStart, knownErrorBlocksEnd) - - // If there are gaps after knownErrorBlocks array, process them - } else if lastBlockProcessed+expectedDif == knownErrorBlocksStart { - validateUpsert(t, stateDiff, knownErrorBlocksStart, knownErrorBlocksEnd) - validateUpsert(t, stateDiff, knownErrorBlocksEnd+expectedDif, currentBlockNum-expectedDif) - - // If there are gaps before knownErrorBlocks array, process them - } else if knownErrorBlocksEnd+expectedDif == currentBlockNum { - validateUpsert(t, stateDiff, lastBlockProcessed+expectedDif, knownErrorBlocksStart-expectedDif) - validateUpsert(t, stateDiff, knownErrorBlocksStart, knownErrorBlocksEnd) - - // if there are gaps before, after, and within the knownErrorBlocks array,handle all the errors. - } else { - validateUpsert(t, stateDiff, lastBlockProcessed+expectedDif, knownErrorBlocksStart-expectedDif) - validateUpsert(t, stateDiff, knownErrorBlocksStart, knownErrorBlocksEnd) - validateUpsert(t, stateDiff, knownErrorBlocksEnd+expectedDif, currentBlockNum-expectedDif) - - } - } else { - validateUpsert(t, stateDiff, lastBlockProcessed+expectedDif, currentBlockNum-expectedDif) - } - -} - -// Make sure the upsert was performed correctly -func validateUpsert(t *testing.T, stateDiff *sql.StateDiffIndexer, startingBlock int64, endingBlock int64) { - t.Logf("Starting to query blocks: %d - %d", startingBlock, endingBlock) - queryString := fmt.Sprintf("SELECT starting_block_number from eth.known_gaps WHERE starting_block_number = %d AND ending_block_number = %d", startingBlock, endingBlock) - - _, queryErr := stateDiff.QueryDb(queryString) // Figure out the string. - t.Logf("Updated Known Gaps table starting from, %d, and ending at, %d", startingBlock, endingBlock) - require.NoError(t, queryErr) -} - -// Create a DB object to use -func setupDb(t *testing.T) (*sql.StateDiffIndexer, sql.Database, error) { - db, err = postgres.SetupSQLXDB() - if err != nil { - t.Fatal(err) - } - stateDiff, err := sql.NewStateDiffIndexer(context.Background(), chainConf, db) - return stateDiff, db, err -} - -// Create a file statediff indexer. -func setupFile(t *testing.T) interfaces.StateDiffIndexer { - if _, err := os.Stat(file.TestConfig.FilePath); !errors.Is(err, os.ErrNotExist) { - err := os.Remove(file.TestConfig.FilePath) - require.NoError(t, err) - } - indexer, err = file.NewStateDiffIndexer(context.Background(), mocks.TestConfig, file.TestConfig) - require.NoError(t, err) - return indexer -} - -// Teardown the DB -func tearDown(t *testing.T, stateDiff *sql.StateDiffIndexer) { - t.Log("Starting tearDown") - sql.TearDownDB(t, db) - err := stateDiff.Close() - require.NoError(t, err) -} diff --git a/statediff/statediffing_test_file.sql b/statediff/statediffing_test_file.sql deleted file mode 100644 index e69de29bb..000000000