github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/db/bulkconnect.go (about)

     1  package db
     2  
     3  import (
     4  	"blockbook/bchain"
     5  	"time"
     6  
     7  	"github.com/golang/glog"
     8  	"github.com/tecbot/gorocksdb"
     9  )
    10  
    11  // bulk connect
    12  // in bulk mode the data are cached and stored to db in batches
    13  // it speeds up the import in two ways:
    14  // 1) balances and txAddresses are modified several times during the import, there is a chance that the modifications are done before write to DB
    15  // 2) rocksdb seems to handle better fewer larger batches than continuous stream of smaller batches
    16  
    17  type bulkAddresses struct {
    18  	bi        BlockInfo
    19  	addresses map[string][]outpoint
    20  }
    21  
    22  // BulkConnect is used to connect blocks in bulk, faster but if interrupted inconsistent way
    23  type BulkConnect struct {
    24  	d                  *RocksDB
    25  	isUTXO             bool
    26  	bulkAddresses      []bulkAddresses
    27  	bulkAddressesCount int
    28  	txAddressesMap     map[string]*TxAddresses
    29  	balances           map[string]*AddrBalance
    30  	height             uint32
    31  }
    32  
    33  const (
    34  	maxBulkAddresses      = 200000
    35  	maxBulkTxAddresses    = 500000
    36  	partialStoreAddresses = maxBulkTxAddresses / 10
    37  	maxBulkBalances       = 800000
    38  	partialStoreBalances  = maxBulkBalances / 10
    39  )
    40  
    41  // InitBulkConnect initializes bulk connect and switches DB to inconsistent state
    42  func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) {
    43  	bc := &BulkConnect{
    44  		d:              d,
    45  		isUTXO:         d.chainParser.IsUTXOChain(),
    46  		txAddressesMap: make(map[string]*TxAddresses),
    47  		balances:       make(map[string]*AddrBalance),
    48  	}
    49  	if err := d.SetInconsistentState(true); err != nil {
    50  		return nil, err
    51  	}
    52  	glog.Info("rocksdb: bulk connect init, db set to inconsistent state")
    53  	return bc, nil
    54  }
    55  
    56  func (b *BulkConnect) storeTxAddresses(wb *gorocksdb.WriteBatch, all bool) (int, int, error) {
    57  	var txm map[string]*TxAddresses
    58  	var sp int
    59  	if all {
    60  		txm = b.txAddressesMap
    61  		b.txAddressesMap = make(map[string]*TxAddresses)
    62  	} else {
    63  		txm = make(map[string]*TxAddresses)
    64  		for k, a := range b.txAddressesMap {
    65  			// store all completely spent transactions, they will not be modified again
    66  			r := true
    67  			for _, o := range a.Outputs {
    68  				if o.Spent == false {
    69  					r = false
    70  					break
    71  				}
    72  			}
    73  			if r {
    74  				txm[k] = a
    75  				delete(b.txAddressesMap, k)
    76  			}
    77  		}
    78  		sp = len(txm)
    79  		// store some other random transactions if necessary
    80  		if len(txm) < partialStoreAddresses {
    81  			for k, a := range b.txAddressesMap {
    82  				txm[k] = a
    83  				delete(b.txAddressesMap, k)
    84  				if len(txm) >= partialStoreAddresses {
    85  					break
    86  				}
    87  			}
    88  		}
    89  	}
    90  	if err := b.d.storeTxAddresses(wb, txm); err != nil {
    91  		return 0, 0, err
    92  	}
    93  	return len(txm), sp, nil
    94  }
    95  
    96  func (b *BulkConnect) parallelStoreTxAddresses(c chan error, all bool) {
    97  	defer close(c)
    98  	start := time.Now()
    99  	wb := gorocksdb.NewWriteBatch()
   100  	defer wb.Destroy()
   101  	count, sp, err := b.storeTxAddresses(wb, all)
   102  	if err != nil {
   103  		c <- err
   104  		return
   105  	}
   106  	if err := b.d.db.Write(b.d.wo, wb); err != nil {
   107  		c <- err
   108  		return
   109  	}
   110  	glog.Info("rocksdb: height ", b.height, ", stored ", count, " (", sp, " spent) txAddresses, ", len(b.txAddressesMap), " remaining, done in ", time.Since(start))
   111  	c <- nil
   112  }
   113  
   114  func (b *BulkConnect) storeBalances(wb *gorocksdb.WriteBatch, all bool) (int, error) {
   115  	var bal map[string]*AddrBalance
   116  	if all {
   117  		bal = b.balances
   118  		b.balances = make(map[string]*AddrBalance)
   119  	} else {
   120  		bal = make(map[string]*AddrBalance)
   121  		// store some random balances
   122  		for k, a := range b.balances {
   123  			bal[k] = a
   124  			delete(b.balances, k)
   125  			if len(bal) >= partialStoreBalances {
   126  				break
   127  			}
   128  		}
   129  	}
   130  	if err := b.d.storeBalances(wb, bal); err != nil {
   131  		return 0, err
   132  	}
   133  	return len(bal), nil
   134  }
   135  
   136  func (b *BulkConnect) parallelStoreBalances(c chan error, all bool) {
   137  	defer close(c)
   138  	start := time.Now()
   139  	wb := gorocksdb.NewWriteBatch()
   140  	defer wb.Destroy()
   141  	count, err := b.storeBalances(wb, all)
   142  	if err != nil {
   143  		c <- err
   144  		return
   145  	}
   146  	if err := b.d.db.Write(b.d.wo, wb); err != nil {
   147  		c <- err
   148  		return
   149  	}
   150  	glog.Info("rocksdb: height ", b.height, ", stored ", count, " balances, ", len(b.balances), " remaining, done in ", time.Since(start))
   151  	c <- nil
   152  }
   153  
   154  func (b *BulkConnect) storeBulkAddresses(wb *gorocksdb.WriteBatch) error {
   155  	for _, ba := range b.bulkAddresses {
   156  		if err := b.d.storeAddresses(wb, ba.bi.Height, ba.addresses); err != nil {
   157  			return err
   158  		}
   159  		if err := b.d.writeHeight(wb, ba.bi.Height, &ba.bi, opInsert); err != nil {
   160  			return err
   161  		}
   162  	}
   163  	b.bulkAddressesCount = 0
   164  	b.bulkAddresses = b.bulkAddresses[:0]
   165  	return nil
   166  }
   167  
   168  // ConnectBlock connects block in bulk mode
   169  func (b *BulkConnect) ConnectBlock(block *bchain.Block, storeBlockTxs bool) error {
   170  	b.height = block.Height
   171  	if !b.isUTXO {
   172  		return b.d.ConnectBlock(block)
   173  	}
   174  	addresses := make(map[string][]outpoint)
   175  	if err := b.d.processAddressesUTXO(block, addresses, b.txAddressesMap, b.balances); err != nil {
   176  		return err
   177  	}
   178  	var storeAddressesChan, storeBalancesChan chan error
   179  	var sa bool
   180  	if len(b.txAddressesMap) > maxBulkTxAddresses || len(b.balances) > maxBulkBalances {
   181  		sa = true
   182  		if len(b.txAddressesMap)+partialStoreAddresses > maxBulkTxAddresses {
   183  			storeAddressesChan = make(chan error)
   184  			go b.parallelStoreTxAddresses(storeAddressesChan, false)
   185  		}
   186  		if len(b.balances)+partialStoreBalances > maxBulkBalances {
   187  			storeBalancesChan = make(chan error)
   188  			go b.parallelStoreBalances(storeBalancesChan, false)
   189  		}
   190  	}
   191  	b.bulkAddresses = append(b.bulkAddresses, bulkAddresses{
   192  		bi: BlockInfo{
   193  			Hash:   block.Hash,
   194  			Time:   block.Time,
   195  			Txs:    uint32(len(block.Txs)),
   196  			Size:   uint32(block.Size),
   197  			Height: block.Height,
   198  		},
   199  		addresses: addresses,
   200  	})
   201  	b.bulkAddressesCount += len(addresses)
   202  	// open WriteBatch only if going to write
   203  	if sa || b.bulkAddressesCount > maxBulkAddresses || storeBlockTxs {
   204  		start := time.Now()
   205  		wb := gorocksdb.NewWriteBatch()
   206  		defer wb.Destroy()
   207  		bac := b.bulkAddressesCount
   208  		if sa || b.bulkAddressesCount > maxBulkAddresses {
   209  			if err := b.storeBulkAddresses(wb); err != nil {
   210  				return err
   211  			}
   212  		}
   213  		if storeBlockTxs {
   214  			if err := b.d.storeAndCleanupBlockTxs(wb, block); err != nil {
   215  				return err
   216  			}
   217  		}
   218  		if err := b.d.db.Write(b.d.wo, wb); err != nil {
   219  			return err
   220  		}
   221  		if bac > b.bulkAddressesCount {
   222  			glog.Info("rocksdb: height ", b.height, ", stored ", bac, " addresses, done in ", time.Since(start))
   223  		}
   224  	}
   225  	if storeAddressesChan != nil {
   226  		if err := <-storeAddressesChan; err != nil {
   227  			return err
   228  		}
   229  	}
   230  	if storeBalancesChan != nil {
   231  		if err := <-storeBalancesChan; err != nil {
   232  			return err
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  // Close flushes the cached data and switches DB from inconsistent state open
   239  // after Close, the BulkConnect cannot be used
   240  func (b *BulkConnect) Close() error {
   241  	glog.Info("rocksdb: bulk connect closing")
   242  	start := time.Now()
   243  	storeAddressesChan := make(chan error)
   244  	go b.parallelStoreTxAddresses(storeAddressesChan, true)
   245  	storeBalancesChan := make(chan error)
   246  	go b.parallelStoreBalances(storeBalancesChan, true)
   247  	wb := gorocksdb.NewWriteBatch()
   248  	defer wb.Destroy()
   249  	bac := b.bulkAddressesCount
   250  	if err := b.storeBulkAddresses(wb); err != nil {
   251  		return err
   252  	}
   253  	if err := b.d.db.Write(b.d.wo, wb); err != nil {
   254  		return err
   255  	}
   256  	glog.Info("rocksdb: height ", b.height, ", stored ", bac, " addresses, done in ", time.Since(start))
   257  	if err := <-storeAddressesChan; err != nil {
   258  		return err
   259  	}
   260  	if err := <-storeBalancesChan; err != nil {
   261  		return err
   262  	}
   263  	if err := b.d.SetInconsistentState(false); err != nil {
   264  		return err
   265  	}
   266  	glog.Info("rocksdb: bulk connect closed, db set to open state")
   267  	b.d = nil
   268  	return nil
   269  }