github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/cmd/burrow/commands/vent.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/signal"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/hyperledger/burrow/config/source"
    15  	"github.com/hyperledger/burrow/crypto"
    16  	"github.com/hyperledger/burrow/execution/evm/abi"
    17  	"github.com/hyperledger/burrow/logging/logconfig"
    18  	"github.com/hyperledger/burrow/vent/config"
    19  	"github.com/hyperledger/burrow/vent/service"
    20  	"github.com/hyperledger/burrow/vent/sqldb"
    21  	"github.com/hyperledger/burrow/vent/sqlsol"
    22  	"github.com/hyperledger/burrow/vent/types"
    23  	cli "github.com/jawher/mow.cli"
    24  )
    25  
    26  type LogLevel string
    27  
    28  const (
    29  	LogLevelNone  LogLevel = "none"
    30  	LogLevelInfo  LogLevel = "info"
    31  	LogLevelTrace LogLevel = "trace"
    32  )
    33  
    34  func logConfig(level LogLevel) *logconfig.LoggingConfig {
    35  	logConf := logconfig.New()
    36  	switch level {
    37  	case LogLevelNone:
    38  		return logConf.None()
    39  	case LogLevelTrace:
    40  		return logConf.WithTrace()
    41  	default:
    42  		return logConf
    43  	}
    44  }
    45  
    46  // Vent consumes EVM events and commits to a DB
    47  func Vent(output Output) func(cmd *cli.Cmd) {
    48  	return func(cmd *cli.Cmd) {
    49  
    50  		cmd.Command("start", "Start the Vent consumer service",
    51  			func(cmd *cli.Cmd) {
    52  				cfg := config.DefaultVentConfig()
    53  
    54  				dbOpts := sqlDBOpts(cmd, cfg)
    55  				grpcAddrOpt := cmd.StringOpt("chain-addr", cfg.ChainAddress, "Address to connect to the Hyperledger Burrow gRPC server")
    56  				httpAddrOpt := cmd.StringOpt("http-addr", cfg.HTTPListenAddress, "Address to bind the HTTP server")
    57  				logLevelOpt := cmd.StringOpt("log-level", string(LogLevelInfo), "Logging level (none, info, trace)")
    58  				watchAddressesOpt := cmd.StringsOpt("watch", nil, "Add contract address to global watch filter")
    59  				minimumHeightOpt := cmd.IntOpt("minimum-height", 0, "Only process block greater than or equal to height passed")
    60  				maxRetriesOpt := cmd.IntOpt("max-retries", int(cfg.BlockConsumerConfig.MaxRetries), "Maximum number of retries when consuming blocks")
    61  				maxRequestRateOpt := cmd.StringOpt("max-request-rate", "", "Maximum request rate given as (number of requests)/(time base), e.g. 1000/24h for 1000 requests per day")
    62  				backoffDurationOpt := cmd.StringOpt("backoff", "",
    63  					"The minimum duration to wait before asking for new blocks - increases exponentially when errors occur. Values like 200ms, 1s, 2m")
    64  				batchSizeOpt := cmd.IntOpt("batch-size", int(cfg.BlockConsumerConfig.MaxBlockBatchSize),
    65  					"The maximum number of blocks from which to request events in a single call - will reduce logarithmically to 1 when errors occur.")
    66  				abiFileOpt := cmd.StringsOpt("abi", cfg.AbiFileOrDirs, "EVM Contract ABI file or folder")
    67  				specFileOrDirOpt := cmd.StringsOpt("spec", cfg.SpecFileOrDirs, "SQLSol specification file or folder")
    68  				dbBlockOpt := cmd.BoolOpt("blocks", false, "Create block tables and persist related data")
    69  				dbTxOpt := cmd.BoolOpt("txs", false, "Create tx tables and persist related data")
    70  
    71  				announceEveryOpt := cmd.StringOpt("announce-every", "5s", "Announce vent status every period as a Go duration, e.g. 1ms, 3s, 1h")
    72  
    73  				cmd.Before = func() {
    74  					var err error
    75  					// Rather annoying boilerplate here... but there is no way to pass mow.cli a pointer for it to fill you value
    76  					cfg.DBAdapter = *dbOpts.adapter
    77  					cfg.DBURL = *dbOpts.url
    78  					cfg.DBSchema = *dbOpts.schema
    79  					cfg.ChainAddress = *grpcAddrOpt
    80  					cfg.HTTPListenAddress = *httpAddrOpt
    81  					cfg.WatchAddresses = make([]crypto.Address, len(*watchAddressesOpt))
    82  					cfg.MinimumHeight = uint64(*minimumHeightOpt)
    83  					cfg.BlockConsumerConfig.MaxRequests, cfg.BlockConsumerConfig.TimeBase, err = parseRequestRate(*maxRequestRateOpt)
    84  					if err != nil {
    85  						output.Fatalf("Could not parse max request rate: %w", err)
    86  					}
    87  					cfg.BlockConsumerConfig.MaxRetries = uint64(*maxRetriesOpt)
    88  					cfg.BlockConsumerConfig.BaseBackoffDuration, err = parseDuration(*backoffDurationOpt)
    89  					if err != nil {
    90  						output.Fatalf("could not parse backoff duration: %w", err)
    91  					}
    92  					cfg.BlockConsumerConfig.MaxBlockBatchSize = uint64(*batchSizeOpt)
    93  					for i, wa := range *watchAddressesOpt {
    94  						cfg.WatchAddresses[i], err = crypto.AddressFromHexString(wa)
    95  						if err != nil {
    96  							output.Fatalf("could not parse watch address: %w", err)
    97  						}
    98  					}
    99  					cfg.AbiFileOrDirs = *abiFileOpt
   100  					cfg.SpecFileOrDirs = *specFileOrDirOpt
   101  					if *dbBlockOpt {
   102  						cfg.SpecOpt |= sqlsol.Block
   103  					}
   104  					if *dbTxOpt {
   105  						cfg.SpecOpt |= sqlsol.Tx
   106  					}
   107  
   108  					cfg.AnnounceEvery, err = parseDuration(*announceEveryOpt)
   109  					if err != nil {
   110  						output.Fatalf("could not parse announce-every duration %s: %v", *announceEveryOpt, err)
   111  					}
   112  				}
   113  
   114  				cmd.Spec = "--spec=<spec file or dir>... [--abi=<abi file or dir>...] " +
   115  					"[--watch=<contract address>...] [--minimum-height=<lowest height from which to read>] " +
   116  					"[--max-retries=<max block request retries>] [--backoff=<minimum backoff duration>] " +
   117  					"[--max-request-rate=<requests / time base>] [--batch-size=<minimum block batch size>] " +
   118  					"[--db-adapter] [--db-url] [--db-schema] [--blocks] [--txs] [--chain-addr] [--http-addr] " +
   119  					"[--log-level] [--announce-every=<duration>]"
   120  
   121  				cmd.Action = func() {
   122  					logger, err := logConfig(LogLevel(*logLevelOpt)).Logger()
   123  					if err != nil {
   124  						output.Fatalf("failed to load logger: %v", err)
   125  					}
   126  
   127  					logger = logger.With("service", "vent")
   128  					consumer := service.NewConsumer(cfg, logger, make(chan types.EventData))
   129  					if err != nil {
   130  						output.Fatalf("Could not create Vent Consumer: %v", err)
   131  					}
   132  					server := service.NewServer(cfg, logger, consumer)
   133  
   134  					projection, err := sqlsol.SpecLoader(cfg.SpecFileOrDirs, cfg.SpecOpt)
   135  					if err != nil {
   136  						output.Fatalf("Spec loader error: %v", err)
   137  					}
   138  
   139  					var wg sync.WaitGroup
   140  
   141  					// setup channel for termination signals
   142  					ch := make(chan os.Signal)
   143  
   144  					signal.Notify(ch, syscall.SIGTERM)
   145  					signal.Notify(ch, syscall.SIGINT)
   146  
   147  					// start the events consumer
   148  					wg.Add(1)
   149  
   150  					go func() {
   151  						if err := consumer.Run(projection, true); err != nil {
   152  							output.Fatalf("Consumer execution error: %v", err)
   153  						}
   154  
   155  						wg.Done()
   156  					}()
   157  
   158  					// start the http server
   159  					wg.Add(1)
   160  
   161  					go func() {
   162  						server.Run()
   163  						wg.Done()
   164  					}()
   165  
   166  					// wait for a termination signal from the OS and
   167  					// gracefully shutdown the events consumer and the http server
   168  					go func() {
   169  						<-ch
   170  						consumer.Shutdown()
   171  						server.Shutdown()
   172  					}()
   173  
   174  					// wait until the events consumer and the http server are done
   175  					wg.Wait()
   176  				}
   177  			})
   178  
   179  		cmd.Command("schema", "Print JSONSchema for spec file format to validate table specs",
   180  			func(cmd *cli.Cmd) {
   181  				cmd.Action = func() {
   182  					output.Printf(source.JSONString(types.ProjectionSpecSchema()))
   183  				}
   184  			})
   185  
   186  		cmd.Command("spec", "Generate SQLSOL specification from ABIs",
   187  			func(cmd *cli.Cmd) {
   188  				abiFileOpt := cmd.StringsOpt("abi", nil, "EVM Contract ABI file or folder")
   189  				dest := cmd.StringArg("SPEC", "", "Write resulting spec to this json file")
   190  
   191  				cmd.Action = func() {
   192  					abiSpec, err := abi.LoadPath(*abiFileOpt...)
   193  					if err != nil {
   194  						output.Fatalf("ABI loader error: %v", err)
   195  					}
   196  
   197  					spec, err := sqlsol.GenerateSpecFromAbis(abiSpec)
   198  					if err != nil {
   199  						output.Fatalf("error generating spec: %s\n", err)
   200  					}
   201  
   202  					err = ioutil.WriteFile(*dest, []byte(source.JSONString(spec)), 0644)
   203  					if err != nil {
   204  						output.Fatalf("error writing file: %v\n", err)
   205  					}
   206  				}
   207  			})
   208  
   209  		cmd.Command("restore", "Restore the mapped tables from the _vent_log table",
   210  			func(cmd *cli.Cmd) {
   211  				const timeLayout = "2006-01-02 15:04:05"
   212  
   213  				dbOpts := sqlDBOpts(cmd, config.DefaultVentConfig())
   214  				timeOpt := cmd.StringOpt("t time", "", fmt.Sprintf("restore time up to which all "+
   215  					"log entries will be applied to restore DB, in the format '%s'- restores all log entries if omitted",
   216  					timeLayout))
   217  				prefixOpt := cmd.StringOpt("p prefix", "", "")
   218  
   219  				cmd.Spec = "[--db-adapter] [--db-url] [--db-schema] [--time=<date/time to up to which to restore>] " +
   220  					"[--prefix=<destination table prefix>]"
   221  
   222  				var restoreTime time.Time
   223  
   224  				cmd.Before = func() {
   225  					if *timeOpt != "" {
   226  						var err error
   227  						restoreTime, err = time.Parse(timeLayout, *timeOpt)
   228  						if err != nil {
   229  							output.Fatalf("Could not parse restore time, should be in the format '%s': %v",
   230  								timeLayout, err)
   231  						}
   232  					}
   233  				}
   234  
   235  				cmd.Action = func() {
   236  					log, err := logconfig.New().Logger()
   237  					if err != nil {
   238  						output.Fatalf("failed to load logger: %v", err)
   239  					}
   240  					db, err := sqldb.NewSQLDB(types.SQLConnection{
   241  						DBAdapter: *dbOpts.adapter,
   242  						DBURL:     *dbOpts.url,
   243  						DBSchema:  *dbOpts.schema,
   244  						Log:       log.With("service", "vent"),
   245  					})
   246  					if err != nil {
   247  						output.Fatalf("Could not connect to SQL DB: %v", err)
   248  					}
   249  
   250  					if restoreTime.IsZero() {
   251  						output.Logf("Restoring DB to state from log")
   252  					} else {
   253  						output.Logf("Restoring DB to state from log as of %v", restoreTime)
   254  					}
   255  
   256  					if *prefixOpt == "" {
   257  						output.Logf("Restoring DB in-place by overwriting any existing tables")
   258  					} else {
   259  						output.Logf("Restoring DB to destination tables with prefix '%s'", *prefixOpt)
   260  					}
   261  
   262  					err = db.RestoreDB(restoreTime, *prefixOpt)
   263  					if err != nil {
   264  						output.Fatalf("Error restoring DB: %v", err)
   265  					}
   266  					output.Logf("Successfully restored DB")
   267  				}
   268  			})
   269  	}
   270  }
   271  
   272  func parseDuration(duration string) (time.Duration, error) {
   273  	if duration == "" {
   274  		return 0, nil
   275  	}
   276  	return time.ParseDuration(duration)
   277  }
   278  
   279  func parseRequestRate(rate string) (int, time.Duration, error) {
   280  	if rate == "" {
   281  		return 0, 0, nil
   282  	}
   283  	ratio := strings.Split(rate, "/")
   284  	if len(ratio) != 2 {
   285  		return 0, 0, fmt.Errorf("expected a ratio string separated by a '/' but got %s", rate)
   286  	}
   287  	requests, err := strconv.ParseInt(ratio[0], 10, 0)
   288  	if err != nil {
   289  		return 0, 0, fmt.Errorf("could not parse max requests as base 10 integer: %w", err)
   290  	}
   291  	timeBase, err := time.ParseDuration(ratio[1])
   292  	if err != nil {
   293  		return 0, 0, fmt.Errorf("could not parse time base: %w", err)
   294  	}
   295  	return int(requests), timeBase, nil
   296  }
   297  
   298  type dbOpts struct {
   299  	adapter *string
   300  	url     *string
   301  	schema  *string
   302  }
   303  
   304  func sqlDBOpts(cmd *cli.Cmd, cfg *config.VentConfig) dbOpts {
   305  	return dbOpts{
   306  		adapter: cmd.StringOpt("db-adapter", cfg.DBAdapter, "Database adapter, 'postgres' or 'sqlite' (if built with the sqlite tag) are supported"),
   307  		url:     cmd.StringOpt("db-url", cfg.DBURL, "PostgreSQL database URL or SQLite db file path"),
   308  		schema:  cmd.StringOpt("db-schema", cfg.DBSchema, "PostgreSQL database schema (empty for SQLite)"),
   309  	}
   310  }