github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/scrape/scrape_consolidate.go (about) 1 package scrapePkg 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 12 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base" 13 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" 14 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/file" 15 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/index" 16 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger" 17 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/notify" 18 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types" 19 ) 20 21 const asciiAppearanceSize = 59 22 23 // Consolidate calls into the block scraper to (a) call Blaze and (b) consolidate if applicable 24 func (bm *BlazeManager) Consolidate(ctx context.Context, blocks []base.Blknum) error { 25 var err error 26 chain := bm.chain 27 28 backup := file.BackupFile{} 29 stageFn, _ := file.LatestFileInFolder(bm.StageFolder()) // it may not exist... 30 if file.FileExists(stageFn) { 31 backup, err = file.MakeBackup(filepath.Join(config.PathToCache(chain), "tmp"), stageFn) 32 if err != nil { 33 return errors.New("Could not create backup file: " + err.Error()) 34 } 35 defer func() { 36 backup.Restore() 37 }() 38 } 39 40 // After this point if we fail the backup file will replace the original file, so 41 // we can safely remove these the stage file (and ripe files) and it will get replaced 42 43 // Some counters... 44 nAppsFound := 0 45 nAddrsFound := 0 46 47 // Load the stage into the map... 48 exists := file.FileExists(stageFn) // order matters 49 appMap, chunkRange, nAppearances := bm.AsciiFileToAppearanceMap(stageFn) 50 if !exists { 51 // Brand new stage. 52 chunkRange = base.FileRange{First: bm.meta.Finalized + 1, Last: blocks[0]} 53 } 54 55 // For each block... 56 nChunks := 0 57 for _, block := range blocks { 58 if ctx.Err() != nil { 59 // This means the context got cancelled, i.e. we got a SIGINT. 60 return nil 61 } 62 fn := fmt.Sprintf("%09d.txt", block) 63 if file.FileExists(filepath.Join(bm.UnripeFolder(), fn)) { 64 continue // skip unripe files 65 } 66 67 ripeFn := filepath.Join(bm.RipeFolder(), fn) 68 if !file.FileExists(ripeFn) { 69 if bm.hasNoAddresses(block) { 70 // this is okay -- see comment at the function 71 } else { 72 msg := fmt.Sprintf("ripe file not found for block %d", block) + spaces 73 if !bm.AllowMissing() { 74 _ = cleanEphemeralIndexFolders(chain) 75 return errors.New(msg) 76 } else { 77 logger.Warn(msg) 78 } 79 } 80 } 81 82 // Read in the ripe file, add it to the appMap and... 83 thisMap, _, thisCount := bm.AsciiFileToAppearanceMap(ripeFn) 84 nAppearances += thisCount 85 nAppsFound += thisCount 86 nAddrsFound += len(thisMap) 87 for addr, apps := range thisMap { 88 appMap[addr] = append(appMap[addr], apps...) 89 } 90 chunkRange.Last = block 91 92 // ...decide if we need to consolidate... 93 isSnap := bm.IsSnap(chunkRange.Last) // Have we hit a snap point? 94 isOvertop := nAppearances >= int(bm.PerChunk()) // Does this block overtop a chunk? 95 if isSnap || isOvertop { 96 // Make a chunk - i.e., consolidate 97 chunkPath := filepath.Join(config.PathToIndex(chain), "finalized", chunkRange.String()+".bin") 98 publisher := base.ZeroAddr 99 var chunk index.Chunk 100 if report, err := chunk.Write(chain, publisher, chunkPath, appMap, nAppearances); err != nil { 101 // Remove file if it exists, because it might not be correct 102 _ = os.Remove(index.ToIndexPath(chunkPath)) 103 return NewCriticalError(err) 104 } else if report == nil { 105 logger.Fatal("should not happen ==> write chunk returned empty report") 106 } else { 107 report.Snapped = isSnap 108 report.FileSize = file.FileSize(chunkPath) 109 report.Report() 110 } 111 if err = bm.opts.NotifyChunkWritten(chunk, chunkPath); err != nil { 112 return err 113 } 114 115 // reset for next chunk 116 bm.meta, _ = bm.opts.Conn.GetMetaData(bm.IsTestMode()) 117 appMap = make(map[string][]types.AppRecord, 0) 118 chunkRange.First = chunkRange.Last + 1 119 chunkRange.Last = chunkRange.Last + 1 120 nAppearances = 0 121 nChunks++ 122 } 123 } 124 125 var newRange base.FileRange 126 if len(appMap) > 0 { // are there any appearances in this block range? 127 newRange = base.FileRange{First: bm.meta.Finalized + 1, Last: 0} 128 129 // We need an array because we're going to write it back to disc 130 appearances := make([]string, 0, nAppearances) 131 for addr, apps := range appMap { 132 for _, app := range apps { 133 if ctx.Err() != nil { 134 // This means the context got cancelled, i.e. we got a SIGINT. 135 return nil 136 } 137 record := fmt.Sprintf("%s\t%09d\t%05d", addr, app.BlockNumber, app.TransactionIndex) 138 appearances = append(appearances, record) 139 newRange.Last = base.Max(newRange.Last, base.Blknum(app.BlockNumber)) 140 } 141 } 142 143 // The stage needs to be sorted because the end user queries it and we want the search to be fast 144 sort.Strings(appearances) 145 146 stageFn = filepath.Join(bm.StageFolder(), fmt.Sprintf("%s.txt", newRange)) 147 if err := file.LinesToAsciiFile(stageFn, appearances); err != nil { 148 os.Remove(stageFn) 149 return err 150 } 151 } 152 153 // Let the user know what happened... 154 nAppsNow := int(file.FileSize(stageFn) / asciiAppearanceSize) 155 bm.report(len(blocks), int(bm.PerChunk()), nChunks, nAppsNow, nAppsFound, nAddrsFound) 156 157 if bm.opts.Notify { 158 if err := Notify(notify.Notification[string]{ 159 Msg: notify.MessageStageUpdated, 160 Meta: bm.meta, 161 Payload: newRange.String(), 162 }); err != nil { 163 return err 164 } 165 } 166 167 // Commit the change by deleting the backup file. 168 backup.Clear() 169 170 return nil 171 } 172 173 // AsciiFileToAppearanceMap reads the appearances from the stage file and returns them as a map 174 func (bm *BlazeManager) AsciiFileToAppearanceMap(fn string) (map[string][]types.AppRecord, base.FileRange, int) { 175 appearances := file.AsciiFileToLines(fn) 176 os.Remove(fn) // It's okay to remove this. If it fails, we'll just start over. 177 178 appMap := make(map[string][]types.AppRecord, len(appearances)) 179 fileRange := base.FileRange{First: base.NOPOSN, Last: 0} 180 181 if len(appearances) == 0 { 182 return appMap, base.FileRange{First: 0, Last: 0}, 0 183 } 184 185 nAdded := 0 186 for _, line := range appearances { 187 parts := strings.Split(line, "\t") 188 if len(parts) == 3 { // shouldn't be needed, but just in case... 189 addr := strings.ToLower(parts[0]) 190 bn := base.MustParseBlknum(strings.TrimLeft(parts[1], "0")) 191 txid := base.MustParseTxnum(strings.TrimLeft(parts[2], "0")) 192 // See #3252 193 if addr == base.SentinalAddr.Hex() && txid == types.MisconfigReward { 194 continue 195 } 196 fileRange.First = base.Min(fileRange.First, bn) 197 fileRange.Last = base.Max(fileRange.Last, bn) 198 appMap[addr] = append(appMap[addr], types.AppRecord{ 199 BlockNumber: uint32(bn), 200 TransactionIndex: uint32(txid), 201 }) 202 nAdded++ 203 } 204 } 205 206 return appMap, fileRange, nAdded 207 } 208 209 // hasNoAddresses returns true if (a) the miner is zero, (b) there are no transactions, uncles, or withdrawals. 210 // (It is truly a block with no addresses -- for example block 15537860 on mainnet.) 211 func (bm *BlazeManager) hasNoAddresses(bn base.Blknum) bool { 212 if block, err := bm.opts.Conn.GetBlockHeaderByNumber(bn); err != nil { 213 return false 214 } else { 215 return base.IsPrecompile(block.Miner.Hex()) && 216 len(block.Transactions) == 0 && 217 len(block.Uncles) == 0 && 218 len(block.Withdrawals) == 0 219 } 220 }