From c528e3e3cf934f10ab23e603233c9b23848913a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 21 Sep 2018 13:15:09 +0300 Subject: [PATCH] cmd/faucet: cache internal state, avoid sync-trashing les --- cmd/faucet/faucet.go | 114 ++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index cfe4e45f1..e17ac36f5 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -199,6 +199,8 @@ type faucet struct { keystore *keystore.KeyStore // Keystore containing the single signer account accounts.Account // Account funding user faucet requests + head *types.Header // Current head header of the faucet + balance *big.Int // Current balance of the faucet nonce uint64 // Current pending nonce of the faucet price *big.Int // Current gas price to issue funds with @@ -324,33 +326,30 @@ func (f *faucet) apiHandler(conn *websocket.Conn) { nonce uint64 err error ) - for { - // Attempt to retrieve the stats, may error on no faucet connectivity - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - head, err = f.client.HeaderByNumber(ctx, nil) - if err == nil { - balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number) - if err == nil { - nonce, err = f.client.NonceAt(ctx, f.account.Address, nil) - } + for head == nil || balance == nil { + // Retrieve the current stats cached by the faucet + f.lock.RLock() + if f.head != nil { + head = types.CopyHeader(f.head) } - cancel() + if f.balance != nil { + balance = new(big.Int).Set(f.balance) + } + nonce = f.nonce + f.lock.RUnlock() - // If stats retrieval failed, wait a bit and retry - if err != nil { - if err = sendError(conn, errors.New("Faucet offline: "+err.Error())); err != nil { + if head == nil || balance == nil { + // Report the faucet offline until initial stats are ready + if err = sendError(conn, errors.New("Faucet offline")); err != nil { log.Warn("Failed to send faucet error to client", "err", err) return } time.Sleep(3 * time.Second) - continue } - // Initial stats reported successfully, proceed with user interaction - break } // Send over the initial stats and the latest header if err = send(conn, map[string]interface{}{ - "funds": balance.Div(balance, ether), + "funds": new(big.Int).Div(balance, ether), "funded": nonce, "peers": f.stack.Server().PeerCount(), "requests": f.reqs, @@ -520,6 +519,47 @@ func (f *faucet) apiHandler(conn *websocket.Conn) { } } +// refresh attempts to retrieve the latest header from the chain and extract the +// associated faucet balance and nonce for connectivity caching. +func (f *faucet) refresh(head *types.Header) error { + // Ensure a state update does not run for too long + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // If no header was specified, use the current chain head + var err error + if head == nil { + if head, err = f.client.HeaderByNumber(ctx, nil); err != nil { + return err + } + } + // Retrieve the balance, nonce and gas price from the current head + var ( + balance *big.Int + nonce uint64 + price *big.Int + ) + if balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number); err != nil { + return err + } + if nonce, err = f.client.NonceAt(ctx, f.account.Address, head.Number); err != nil { + return err + } + if price, err = f.client.SuggestGasPrice(ctx); err != nil { + return err + } + // Everything succeeded, update the cached stats and eject old requests + f.lock.Lock() + f.head, f.balance = head, balance + f.price, f.nonce = price, nonce + for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { + f.reqs = f.reqs[1:] + } + f.lock.Unlock() + + return nil +} + // loop keeps waiting for interesting events and pushes them out to connected // websockets. func (f *faucet) loop() { @@ -537,45 +577,27 @@ func (f *faucet) loop() { go func() { for head := range update { // New chain head arrived, query the current stats and stream to clients - var ( - balance *big.Int - nonce uint64 - price *big.Int - err error - ) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number) - if err == nil { - nonce, err = f.client.NonceAt(ctx, f.account.Address, nil) - if err == nil { - price, err = f.client.SuggestGasPrice(ctx) - } + timestamp := time.Unix(head.Time.Int64(), 0) + if time.Since(timestamp) > time.Hour { + log.Warn("Skipping faucet refresh, head too old", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp)) + continue } - cancel() - - // If querying the data failed, try for the next block - if err != nil { + if err := f.refresh(head); err != nil { log.Warn("Failed to update faucet state", "block", head.Number, "hash", head.Hash(), "err", err) continue - } else { - log.Info("Updated faucet state", "block", head.Number, "hash", head.Hash(), "balance", balance, "nonce", nonce, "price", price) } // Faucet state retrieved, update locally and send to clients - balance = new(big.Int).Div(balance, ether) - - f.lock.Lock() - f.price, f.nonce = price, nonce - for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { - f.reqs = f.reqs[1:] - } - f.lock.Unlock() - f.lock.RLock() + log.Info("Updated faucet state", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp), "balance", f.balance, "nonce", f.nonce, "price", f.price) + + balance := new(big.Int).Div(f.balance, ether) + peers := f.stack.Server().PeerCount() + for _, conn := range f.conns { if err := send(conn, map[string]interface{}{ "funds": balance, "funded": f.nonce, - "peers": f.stack.Server().PeerCount(), + "peers": peers, "requests": f.reqs, }, time.Second); err != nil { log.Warn("Failed to send stats to client", "err", err)