github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/init/handle_init_download.go (about)

     1  // Copyright 2021 The TrueBlocks Authors. All rights reserved.
     2  // Use of this source code is governed by a license that can
     3  // be found in the LICENSE file.
     4  
     5  package initPkg
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"runtime"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/colors"
    17  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/index"
    19  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    20  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/manifest"
    21  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/progress"
    22  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    23  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/walk"
    24  )
    25  
    26  var m sync.Mutex
    27  
    28  // TODO: So we can capture both the blooms and the index portions in one summary. Once we move to single stream, this can go local
    29  var nProcessed int
    30  var nStarted int
    31  
    32  // downloadAndReportProgress Downloads the chunks and reports progress to the progressChannel
    33  func (opts *InitOptions) downloadAndReportProgress(chunks []types.ChunkRecord, chunkType walk.CacheType, nTotal int) ([]types.ChunkRecord, bool) {
    34  	chain := opts.Globals.Chain
    35  	sleep := base.Max(.0125, opts.Sleep)
    36  	successCount := 0
    37  
    38  	failed := []types.ChunkRecord{}
    39  	cancelled := false
    40  
    41  	// Establish a channel to listen for progress messages
    42  	progressChannel := progress.MakeChan()
    43  	defer close(progressChannel)
    44  
    45  	// If we make this too big, the pinning service chokes
    46  	poolSize := runtime.NumCPU() * 2
    47  
    48  	// Start the go routine that downloads the chunks. This sends messages through the progressChannel
    49  	go index.DownloadChunks(chain, chunks, chunkType, poolSize, progressChannel)
    50  
    51  	for event := range progressChannel {
    52  		chunk, ok := event.Payload.(*types.ChunkRecord)
    53  		var rng string
    54  		if ok {
    55  			rng = chunk.Range
    56  		}
    57  
    58  		if event.Event == progress.Cancelled {
    59  			cancelled = true
    60  			break
    61  		}
    62  
    63  		if event.Event == progress.AllDone {
    64  			msg := fmt.Sprintf("%sCompleted initializing %s files.%s", colors.BrightWhite, chunkType, colors.Off)
    65  			logger.Info(msg, strings.Repeat(" ", 60))
    66  			break
    67  		}
    68  
    69  		// TODO: is this a performance issue?
    70  		m.Lock() // To conflict progress printing
    71  		switch event.Event {
    72  		case progress.Error:
    73  			logger.Error(event.Error)
    74  			if ok {
    75  				failed = append(failed, *chunk)
    76  				if errors.Is(event.Error, index.ErrWriteToDiscError) {
    77  					sleep = base.Min(4, sleep*1.2)
    78  					successCount = 0
    79  				}
    80  			}
    81  
    82  		case progress.Start:
    83  			nStarted++
    84  			if nStarted <= 50 { // we don't need too many of these
    85  				logger.Info("Started download of", event.Message, "[", nStarted, " of ", nTotal, "]")
    86  			}
    87  			if nStarted == poolSize*3 {
    88  				msg := fmt.Sprintf("%sPlease wait...%s", colors.BrightWhite, colors.Off)
    89  				logger.Info(msg)
    90  			}
    91  
    92  		case progress.Update:
    93  			msg := fmt.Sprintf("%s%s%s", colors.Yellow, event.Message, colors.Off)
    94  			logger.Info(msg, spaces)
    95  
    96  		case progress.Finished:
    97  			nProcessed++
    98  			col := colors.Yellow
    99  			if event.Message == "bloom" {
   100  				col = colors.Magenta
   101  			}
   102  			successCount++
   103  			pct := float64(nProcessed) / float64(nTotal) * 100
   104  			sleepStr := ""
   105  			if sleep > .25 {
   106  				sleepStr = fmt.Sprintf(" [sleep: %0.2fms]", sleep*1000)
   107  			}
   108  			msg := fmt.Sprintf("Finished download of %s%s%s %s%s%s (% 4d of %4d %0.1f%%%s) chain: %s", col, event.Message, colors.Off, col, rng, colors.Off, nProcessed, nTotal, pct, sleepStr, opts.Globals.Chain)
   109  			logger.Info(msg, spaces)
   110  			if successCount%10 == 0 {
   111  				sleep = base.Max(.0125, sleep/1.2)
   112  				successCount = 0
   113  			}
   114  
   115  		default:
   116  			logger.Info(event.Message, rng, spaces)
   117  		}
   118  		m.Unlock()
   119  
   120  		if sleep > 0 {
   121  			ms := time.Duration(sleep*1000) * time.Millisecond
   122  			time.Sleep(ms)
   123  		}
   124  	}
   125  
   126  	return failed, cancelled
   127  }
   128  
   129  // retry retries downloading any `failedChunks`. It repeats `nTimes` times by calling `downloadChunks` function.
   130  //
   131  // Returns number of chunks that we were unable to fetch. This function is simple because:
   132  //  1. it will never get a new failing chunk (it only feeds in the list of known, failed chunks)
   133  //  2. The maximum number of failing chunks we can get equals the length of `failedChunks`.
   134  //
   135  // TODO: Instead of storing failed attempts in an array and retrying them after processing the entire list in the manifest,
   136  // TODO: we want to re-process failed downloads on the stop. In that way, we can do progressive backoff per chunk (as opposed
   137  // TODO: to globally). We want to back-off on single chunks instead of every chunk. The backoff routine carries an 'attempts'
   138  // TODO: value and we wait after each failure 2^nAttempts (double the wait each time it fails). Max 10 tries or something.
   139  func retry(failedChunks []types.ChunkRecord, nTimes int, downloadChunksFunc func(chunks []types.ChunkRecord) (failed []types.ChunkRecord, cancelled bool)) int {
   140  	count := 0
   141  
   142  	chunksToRetry := failedChunks
   143  	cancelled := false
   144  
   145  	for {
   146  		if len(chunksToRetry) == 0 {
   147  			break
   148  		}
   149  
   150  		if count >= nTimes {
   151  			break
   152  		}
   153  
   154  		logger.Warn(colors.Yellow, "Retrying", len(chunksToRetry), "downloads", colors.Off)
   155  		if chunksToRetry, cancelled = downloadChunksFunc(chunksToRetry); cancelled {
   156  			break
   157  		}
   158  
   159  		count++
   160  	}
   161  
   162  	return len(chunksToRetry)
   163  }
   164  
   165  // updateLocalManifest updates the local manifest with the one downloaded but may add existing chunks if they are later...
   166  func (opts *InitOptions) updateLocalManifest(existing, remote *manifest.Manifest) error {
   167  	chain := opts.Globals.Chain
   168  
   169  	copy := *remote
   170  
   171  	if existing != nil {
   172  		lastExisting := base.RangeFromRangeString(existing.Chunks[len(existing.Chunks)-1].Range)
   173  		lastRemote := base.RangeFromRangeString(remote.Chunks[len(remote.Chunks)-1].Range)
   174  		if !lastExisting.LaterThan(lastRemote) {
   175  			for _, ch := range existing.Chunks {
   176  				rng := base.RangeFromRangeString(ch.Range)
   177  				if rng.LaterThan(lastRemote) {
   178  					copy.Chunks = append(copy.Chunks, ch)
   179  				}
   180  			}
   181  		}
   182  	}
   183  
   184  	return copy.SaveManifest(chain, config.PathToManifest(chain))
   185  }
   186  
   187  var spaces = strings.Repeat(" ", 55)