|
|
- package mempool
-
- import (
- "bytes"
- "container/list"
- "sync"
- "sync/atomic"
-
- "github.com/tendermint/go-clist"
- . "github.com/tendermint/go-common"
- "github.com/tendermint/tendermint/proxy"
- "github.com/tendermint/tendermint/types"
- tmsp "github.com/tendermint/tmsp/types"
- )
-
- /*
-
- The mempool pushes new txs onto the proxyAppCtx.
- It gets a stream of (req, res) tuples from the proxy.
- The memool stores good txs in a concurrent linked-list.
-
- Multiple concurrent go-routines can traverse this linked-list
- safely by calling .NextWait() on each element.
-
- So we have several go-routines:
- 1. Consensus calling Update() and Reap() synchronously
- 2. Many mempool reactor's peer routines calling AppendTx()
- 3. Many mempool reactor's peer routines traversing the txs linked list
- 4. Another goroutine calling GarbageCollectTxs() periodically
-
- To manage these goroutines, there are three methods of locking.
- 1. Mutations to the linked-list is protected by an internal mtx (CList is goroutine-safe)
- 2. Mutations to the linked-list elements are atomic
- 3. AppendTx() calls can be paused upon Update() and Reap(), protected by .proxyMtx
-
- Garbage collection of old elements from mempool.txs is handlde via
- the DetachPrev() call, which makes old elements not reachable by
- peer broadcastTxRoutine() automatically garbage collected.
-
- */
-
- const cacheSize = 100000
-
- type Mempool struct {
- proxyMtx sync.Mutex
- proxyAppCtx proxy.AppContext
- txs *clist.CList // concurrent linked-list of good txs
- counter int64 // simple incrementing counter
- height int // the last block Update()'d to
- expected *clist.CElement // pointer to .txs for next response
-
- // Keep a cache of already-seen txs.
- // This reduces the pressure on the proxyApp.
- cacheMap map[string]struct{}
- cacheList *list.List
- }
-
- func NewMempool(proxyAppCtx proxy.AppContext) *Mempool {
- mempool := &Mempool{
- proxyAppCtx: proxyAppCtx,
- txs: clist.New(),
- counter: 0,
- height: 0,
- expected: nil,
-
- cacheMap: make(map[string]struct{}, cacheSize),
- cacheList: list.New(),
- }
- proxyAppCtx.SetResponseCallback(mempool.resCb)
- return mempool
- }
-
- // Return the first element of mem.txs for peer goroutines to call .NextWait() on.
- // Blocks until txs has elements.
- func (mem *Mempool) TxsFrontWait() *clist.CElement {
- return mem.txs.FrontWait()
- }
-
- // Try a new transaction in the mempool.
- // Potentially blocking if we're blocking on Update() or Reap().
- func (mem *Mempool) AppendTx(tx types.Tx) (err error) {
- mem.proxyMtx.Lock()
- defer mem.proxyMtx.Unlock()
-
- // CACHE
- if _, exists := mem.cacheMap[string(tx)]; exists {
- return nil
- }
- if mem.cacheList.Len() >= cacheSize {
- popped := mem.cacheList.Front()
- poppedTx := popped.Value.(types.Tx)
- delete(mem.cacheMap, string(poppedTx))
- mem.cacheList.Remove(popped)
- }
- mem.cacheMap[string(tx)] = struct{}{}
- mem.cacheList.PushBack(tx)
- // END CACHE
-
- if err = mem.proxyAppCtx.Error(); err != nil {
- return err
- }
- mem.proxyAppCtx.AppendTxAsync(tx)
- return nil
- }
-
- // TMSP callback function
- // CONTRACT: No other goroutines mutate mem.expected concurrently.
- func (mem *Mempool) resCb(req tmsp.Request, res tmsp.Response) {
- switch res := res.(type) {
- case tmsp.ResponseAppendTx:
- reqAppendTx := req.(tmsp.RequestAppendTx)
- if mem.expected == nil { // Normal operation
- if res.RetCode == tmsp.RetCodeOK {
- mem.counter++
- memTx := &mempoolTx{
- counter: mem.counter,
- height: int64(mem.height),
- tx: reqAppendTx.TxBytes,
- }
- mem.txs.PushBack(memTx)
- } else {
- // ignore bad transaction
- // TODO: handle other retcodes
- }
- } else { // During Update()
- // TODO Log sane warning if mem.expected is nil.
- memTx := mem.expected.Value.(*mempoolTx)
- if !bytes.Equal(reqAppendTx.TxBytes, memTx.tx) {
- PanicSanity("Unexpected tx response from proxy")
- }
- if res.RetCode == tmsp.RetCodeOK {
- // Good, nothing to do.
- } else {
- // TODO: handle other retcodes
- // Tx became invalidated due to newly committed block.
- // NOTE: Concurrent traversal of mem.txs via CElement.Next() still works.
- mem.txs.Remove(mem.expected)
- mem.expected.DetachPrev()
- }
- mem.expected = mem.expected.Next()
- }
- default:
- // ignore other messages
- }
- }
-
- // Get the valid transactions run so far, and the hash of
- // the application state that results from those transactions.
- func (mem *Mempool) Reap() ([]types.Tx, []byte, error) {
- mem.proxyMtx.Lock()
- defer mem.proxyMtx.Unlock()
-
- // First, get the hash of txs run so far
- hash, err := mem.proxyAppCtx.GetHashSync()
- if err != nil {
- return nil, nil, err
- }
-
- // And collect all the transactions.
- txs := mem.collectTxs()
-
- return txs, hash, nil
- }
-
- func (mem *Mempool) collectTxs() []types.Tx {
- txs := make([]types.Tx, 0, mem.txs.Len())
- for e := mem.txs.Front(); e != nil; e = e.Next() {
- memTx := e.Value.(*mempoolTx)
- txs = append(txs, memTx.tx)
- }
- return txs
- }
-
- // "block" is the new block that was committed.
- // Txs that are present in "block" are discarded from mempool.
- // NOTE: this should be called *after* block is committed by consensus.
- // CONTRACT: block is valid and next in sequence.
- func (mem *Mempool) Update(block *types.Block) error {
- mem.proxyMtx.Lock()
- defer mem.proxyMtx.Unlock()
-
- // Rollback mempool synchronously
- // TODO: test that proxyAppCtx's state matches the block's
- err := mem.proxyAppCtx.RollbackSync()
- if err != nil {
- return err
- }
-
- // First, create a lookup map of txns in new block.
- blockTxsMap := make(map[string]struct{})
- for _, tx := range block.Data.Txs {
- blockTxsMap[string(tx)] = struct{}{}
- }
-
- // Remove transactions that are already in block.
- // Return the remaining potentially good txs.
- goodTxs := mem.filterTxs(block.Height, blockTxsMap)
-
- // Set height and expected
- mem.height = block.Height
- mem.expected = mem.txs.Front()
-
- // Push good txs to proxyAppCtx
- // NOTE: resCb() may be called concurrently.
- for _, tx := range goodTxs {
- mem.proxyAppCtx.AppendTxAsync(tx)
- if err := mem.proxyAppCtx.Error(); err != nil {
- return err
- }
- }
-
- // NOTE: Even though we return immediately without e.g.
- // calling mem.proxyAppCtx.FlushSync(),
- // New mempool txs will still have to wait until
- // all goodTxs are re-processed.
- // So we could make synchronous calls here to proxyAppCtx.
-
- return nil
- }
-
- func (mem *Mempool) filterTxs(height int, blockTxsMap map[string]struct{}) []types.Tx {
- goodTxs := make([]types.Tx, 0, mem.txs.Len())
- for e := mem.txs.Front(); e != nil; e = e.Next() {
- memTx := e.Value.(*mempoolTx)
- if _, ok := blockTxsMap[string(memTx.tx)]; ok {
- // Remove the tx since already in block.
- mem.txs.Remove(e)
- e.DetachPrev()
- continue
- }
- // Good tx!
- atomic.StoreInt64(&memTx.height, int64(height))
- goodTxs = append(goodTxs, memTx.tx)
- }
- return goodTxs
- }
-
- //--------------------------------------------------------------------------------
-
- // A transaction that successfully ran
- type mempoolTx struct {
- counter int64 // a simple incrementing counter
- height int64 // height that this tx had been validated in
- tx types.Tx //
- }
-
- func (memTx *mempoolTx) Height() int {
- return int(atomic.LoadInt64(&memTx.height))
- }
|