github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/vent/service/consumer.go (about) 1 package service 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "sync" 8 "time" 9 10 "github.com/hyperledger/burrow/encoding" 11 "github.com/hyperledger/burrow/logging" 12 "github.com/hyperledger/burrow/logging/structure" 13 "github.com/hyperledger/burrow/rpc/lib/jsonrpc" 14 "github.com/hyperledger/burrow/rpc/rpcevents" 15 "github.com/hyperledger/burrow/rpc/web3/ethclient" 16 "github.com/hyperledger/burrow/vent/chain" 17 "github.com/hyperledger/burrow/vent/chain/burrow" 18 "github.com/hyperledger/burrow/vent/chain/ethereum" 19 "github.com/hyperledger/burrow/vent/config" 20 "github.com/hyperledger/burrow/vent/sqldb" 21 "github.com/hyperledger/burrow/vent/sqlsol" 22 "github.com/hyperledger/burrow/vent/types" 23 "github.com/pkg/errors" 24 "google.golang.org/grpc/connectivity" 25 ) 26 27 // Consumer contains basic configuration for consumer to run 28 type Consumer struct { 29 Config *config.VentConfig 30 Logger *logging.Logger 31 DB *sqldb.SQLDB 32 Chain chain.Chain 33 // external events channel used for when vent is leveraged as a library 34 EventsChannel chan types.EventData 35 Done chan struct{} 36 shutdownOnce sync.Once 37 LastProcessedHeight uint64 38 } 39 40 // NewConsumer constructs a new consumer configuration. 41 // The event channel will be passed a collection of rows generated from all of the events in a single block 42 // It will be closed by the consumer when it is finished 43 func NewConsumer(cfg *config.VentConfig, log *logging.Logger, eventChannel chan types.EventData) *Consumer { 44 cfg.BlockConsumerConfig.Complete() 45 return &Consumer{ 46 Config: cfg, 47 Logger: log, 48 EventsChannel: eventChannel, 49 Done: make(chan struct{}), 50 } 51 } 52 53 // Run connects to a grpc service and subscribes to log events, 54 // then gets tables structures, maps them & parse event data. 55 // Store data in SQL event tables, it runs forever 56 func (c *Consumer) Run(projection *sqlsol.Projection, stream bool) error { 57 var err error 58 59 c.Logger.InfoMsg("Connecting to Burrow gRPC server") 60 61 c.Chain, err = c.connectToChain() 62 if err != nil { 63 return errors.Wrapf(err, "Error connecting to Burrow gRPC server at %s", c.Config.ChainAddress) 64 } 65 defer c.Chain.Close() 66 defer close(c.EventsChannel) 67 68 abiProvider, err := NewAbiProvider(c.Config.AbiFileOrDirs, c.Chain, c.Logger) 69 if err != nil { 70 return errors.Wrapf(err, "Error loading ABIs") 71 } 72 73 if len(projection.Spec) == 0 { 74 c.Logger.InfoMsg("No events specifications found") 75 return nil 76 } 77 78 c.Logger.InfoMsg("Connecting to SQL database") 79 80 connection := types.SQLConnection{ 81 DBAdapter: c.Config.DBAdapter, 82 DBURL: c.Config.DBURL, 83 DBSchema: c.Config.DBSchema, 84 Log: c.Logger, 85 } 86 87 c.DB, err = sqldb.NewSQLDB(connection) 88 if err != nil { 89 return fmt.Errorf("error connecting to SQL database: %v", err) 90 } 91 defer c.DB.Close() 92 93 err = c.DB.Init(c.Chain.GetChainID(), c.Chain.GetVersion()) 94 if err != nil { 95 return fmt.Errorf("could not clean tables after ChainID change: %v", err) 96 } 97 98 c.Logger.InfoMsg("Synchronizing config and database projection structures") 99 100 err = c.DB.SynchronizeDB(c.Chain.GetChainID(), projection.Tables) 101 if err != nil { 102 return errors.Wrap(err, "Error trying to synchronize database") 103 } 104 105 // doneCh is used for sending a "done" signal from each goroutine to the main thread 106 // eventCh is used for sending received events to the main thread to be stored in the db 107 errCh := make(chan error, 1) 108 eventCh := make(chan types.EventData) 109 110 go func() { 111 defer func() { 112 c.Shutdown() 113 }() 114 go c.announceEvery(c.Done) 115 116 c.Logger.InfoMsg("Getting last processed block number from SQL log table") 117 118 fromBlock, err := c.DB.LastBlockHeight(c.Chain.GetChainID()) 119 if err != nil { 120 errCh <- errors.Wrapf(err, "Error trying to get last processed block number") 121 return 122 } 123 124 startingBlock := fromBlock 125 // Start the block after the last one successfully committed - apart from if this is the first block 126 // We include block 0 because it is where we currently place dump/restored transactions 127 if startingBlock > 0 { 128 startingBlock++ 129 } 130 131 // Allows us skip historical/checkpointed state 132 if startingBlock < c.Config.MinimumHeight { 133 startingBlock = c.Config.MinimumHeight 134 } 135 136 // setup block range to get needed blocks server side 137 var end *rpcevents.Bound 138 if stream { 139 end = rpcevents.StreamBound() 140 } else { 141 end = rpcevents.LatestBound() 142 } 143 144 request := &rpcevents.BlocksRequest{ 145 BlockRange: rpcevents.NewBlockRange(rpcevents.AbsoluteBound(startingBlock), end), 146 } 147 148 c.Logger.TraceMsg("Waiting for blocks...") 149 150 // gets blocks in given range based on last processed block taken from database 151 consumer := NewBlockConsumer(c.Chain.GetChainID(), projection, c.Config.SpecOpt, abiProvider.GetEventAbi, 152 eventCh, c.Done, c.Logger) 153 154 err = c.Chain.ConsumeBlocks(context.Background(), request.BlockRange, consumer) 155 156 if err != nil { 157 if err == io.EOF { 158 c.Logger.InfoMsg("EOF stream received...") 159 } else { 160 if finished(c.Done) { 161 c.Logger.TraceMsg("GRPC connection closed") 162 } else { 163 errCh <- errors.Wrapf(err, "Error receiving blocks") 164 return 165 } 166 } 167 } 168 }() 169 170 for { 171 select { 172 // Process block events 173 case blk := <-eventCh: 174 c.LastProcessedHeight = blk.BlockHeight 175 err := c.commitBlock(projection, blk) 176 if err != nil { 177 c.Logger.InfoMsg("error committing block", "err", err) 178 return err 179 } 180 181 // Await completion 182 case <-c.Done: 183 select { 184 185 // Select possible error 186 case err := <-errCh: 187 c.Logger.InfoMsg("finished with error", "err", err) 188 return err 189 190 // Or fallback to success 191 default: 192 c.Logger.InfoMsg("finished successfully") 193 return nil 194 } 195 } 196 } 197 } 198 199 func (c *Consumer) commitBlock(projection *sqlsol.Projection, blockEvents types.EventData) error { 200 // upsert rows in specific SQL event tables and update block number 201 if err := c.DB.SetBlock(c.Chain.GetChainID(), projection.Tables, blockEvents); err != nil { 202 return fmt.Errorf("error upserting rows in database: %v", err) 203 } 204 205 // send to the external events channel in a non-blocking manner 206 select { 207 case c.EventsChannel <- blockEvents: 208 default: 209 } 210 return nil 211 } 212 213 // Health returns the health status for the consumer 214 func (c *Consumer) Health() error { 215 if finished(c.Done) { 216 return errors.New("closing service") 217 } 218 219 // check db status 220 if c.DB == nil { 221 return errors.New("database disconnected") 222 } 223 224 if err := c.DB.Ping(); err != nil { 225 return errors.New("database unavailable") 226 } 227 228 // check grpc connection status 229 if c.Chain == nil { 230 return errors.New("grpc disconnected") 231 } 232 233 if grpcState := c.Chain.Connectivity(); grpcState != connectivity.Ready { 234 return errors.New("grpc connection not ready") 235 } 236 237 return nil 238 } 239 240 // Shutdown gracefully shuts down the events consumer 241 func (c *Consumer) Shutdown() { 242 c.shutdownOnce.Do(func() { 243 c.Logger.InfoMsg("Shutting down vent consumer...") 244 close(c.Done) 245 err := c.Chain.Close() 246 if err != nil { 247 c.Logger.InfoMsg("Could not close Chain connection", structure.ErrorKey, err) 248 } 249 }) 250 } 251 252 func (c *Consumer) StatusMessage(ctx context.Context) []interface{} { 253 return c.Chain.StatusMessage(context.Background(), c.LastProcessedHeight) 254 } 255 256 func (c *Consumer) announceEvery(doneCh <-chan struct{}) { 257 if c.Config.AnnounceEvery != 0 { 258 ticker := time.NewTicker(c.Config.AnnounceEvery) 259 for { 260 select { 261 case <-ticker.C: 262 c.Logger.InfoMsg("Announcement", c.StatusMessage(context.Background())...) 263 case <-doneCh: 264 ticker.Stop() 265 return 266 } 267 } 268 } 269 } 270 271 func finished(doneCh chan struct{}) bool { 272 select { 273 case <-doneCh: 274 return true 275 default: 276 return false 277 } 278 } 279 280 func (c *Consumer) connectToChain() (chain.Chain, error) { 281 filter := &chain.Filter{ 282 Addresses: c.Config.WatchAddresses, 283 } 284 c.Logger.InfoMsg("Attempting to detect chain type", "chain_address", c.Config.ChainAddress) 285 burrowChain, burrowErr := dialBurrow(c.Config.ChainAddress, filter) 286 if burrowErr == nil { 287 return burrowChain, nil 288 } 289 ethChain, ethErr := dialEthereum(c.Config.ChainAddress, filter, &c.Config.BlockConsumerConfig, c.Logger) 290 if ethErr != nil { 291 return nil, fmt.Errorf("could not connect to either Burrow or Ethereum chain, "+ 292 "Burrow error: %v, Ethereum error: %v", burrowErr, ethErr) 293 } 294 return ethChain, nil 295 } 296 297 func dialBurrow(chainAddress string, filter *chain.Filter) (*burrow.Chain, error) { 298 conn, err := encoding.GRPCDial(chainAddress) 299 if err != nil { 300 return nil, err 301 } 302 return burrow.New(conn, filter) 303 } 304 305 func dialEthereum(chainAddress string, filter *chain.Filter, consumerConfig *chain.BlockConsumerConfig, 306 logger *logging.Logger) (*ethereum.Chain, error) { 307 client := ethclient.NewEthClient(jsonrpc.NewClient(chainAddress)) 308 return ethereum.New(client, filter, consumerConfig, logger) 309 }