decred.org/dcrdex@v1.0.5/server/db/driver/pg/epochs.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package pg
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"database/sql/driver"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/dex/candles"
    16  	"decred.org/dcrdex/dex/order"
    17  	"decred.org/dcrdex/server/db"
    18  	"decred.org/dcrdex/server/db/driver/pg/internal"
    19  	"github.com/lib/pq"
    20  )
    21  
    22  // In a table, a []order.OrderID is stored as a BYTEA[]. The orderIDs type
    23  // defines the Value and Scan methods for such an OrderID slice using
    24  // pq.ByteaArray and copying of OrderId data to/from []byte.
    25  type orderIDs []order.OrderID
    26  
    27  // Value implements the sql/driver.Valuer interface.
    28  func (oids orderIDs) Value() (driver.Value, error) {
    29  	if oids == nil {
    30  		return nil, nil
    31  	}
    32  	if len(oids) == 0 {
    33  		return "{}", nil
    34  	}
    35  
    36  	ba := make(pq.ByteaArray, 0, len(oids))
    37  	for i := range oids {
    38  		ba = append(ba, oids[i][:])
    39  	}
    40  	return ba.Value()
    41  }
    42  
    43  // Scan implements the sql.Scanner interface.
    44  func (oids *orderIDs) Scan(src any) error {
    45  	var ba pq.ByteaArray
    46  	err := ba.Scan(src)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	n := len(ba)
    52  	*oids = make([]order.OrderID, n)
    53  	for i := range ba {
    54  		copy((*oids)[i][:], ba[i])
    55  	}
    56  	return nil
    57  }
    58  
    59  // InsertEpoch stores the results of a newly-processed epoch. TODO: test.
    60  func (a *Archiver) InsertEpoch(ed *db.EpochResults) error {
    61  	marketSchema, err := a.marketSchema(ed.MktBase, ed.MktQuote)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	epochsTableName := fullEpochsTableName(a.dbName, marketSchema)
    67  	stmt := fmt.Sprintf(internal.InsertEpoch, epochsTableName)
    68  
    69  	_, err = a.db.Exec(stmt, ed.Idx, ed.Dur, ed.MatchTime, ed.CSum, ed.Seed,
    70  		orderIDs(ed.OrdersRevealed), orderIDs(ed.OrdersMissed))
    71  	if err != nil {
    72  		a.fatalBackendErr(err)
    73  		return err
    74  	}
    75  
    76  	epochReportsTableName := fullEpochReportsTableName(a.dbName, marketSchema)
    77  	stmt = fmt.Sprintf(internal.InsertEpochReport, epochReportsTableName)
    78  	epochEnd := (ed.Idx + 1) * ed.Dur
    79  	_, err = a.db.Exec(stmt, epochEnd, ed.Dur, ed.MatchVolume, ed.QuoteVolume, ed.BookBuys, ed.BookBuys5, ed.BookBuys25,
    80  		ed.BookSells, ed.BookSells5, ed.BookSells25, ed.HighRate, ed.LowRate, ed.StartRate, ed.EndRate)
    81  	if err != nil {
    82  		a.fatalBackendErr(err)
    83  	}
    84  
    85  	return err
    86  }
    87  
    88  // LastEpochRate gets the EndRate of the last EpochResults inserted for the
    89  // market. If the database is empty, no error and a rate of zero are returned.
    90  func (a *Archiver) LastEpochRate(base, quote uint32) (rate uint64, err error) {
    91  	marketSchema, err := a.marketSchema(base, quote)
    92  	if err != nil {
    93  		return 0, err
    94  	}
    95  
    96  	epochReportsTableName := fullEpochReportsTableName(a.dbName, marketSchema)
    97  	stmt := fmt.Sprintf(internal.SelectLastEpochRate, epochReportsTableName)
    98  	if err = a.db.QueryRowContext(a.ctx, stmt).Scan(&rate); err != nil && !errors.Is(sql.ErrNoRows, err) {
    99  		return 0, err
   100  	}
   101  	return rate, nil
   102  }
   103  
   104  // LoadEpochStats reads all market epoch history from the database, updating the
   105  // provided caches along the way.
   106  func (a *Archiver) LoadEpochStats(base, quote uint32, caches []*candles.Cache) error {
   107  	marketSchema, err := a.marketSchema(base, quote)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	epochReportsTableName := fullEpochReportsTableName(a.dbName, marketSchema)
   112  
   113  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   114  	defer cancel()
   115  
   116  	// First. load stored candles from the candles table. Establish a start
   117  	// stamp for scanning epoch reports for partial candles.
   118  	var oldestNeeded uint64 = math.MaxUint64
   119  	sinceCaches := make(map[uint64]*candles.Cache, 0) // maps oldest end stamp
   120  	now := uint64(time.Now().UnixMilli())
   121  	for _, cache := range caches {
   122  		if err = a.loadCandles(base, quote, cache, candles.CacheSize); err != nil {
   123  			return fmt.Errorf("loadCandles: %w", err)
   124  		}
   125  
   126  		var since uint64
   127  		if len(cache.Candles) > 0 {
   128  			// If we have candles, set our since value to the next expected
   129  			// epoch stamp.
   130  			idx := cache.Last().EndStamp / cache.BinSize
   131  			since = (idx + 1) * cache.BinSize
   132  		} else {
   133  			since = now - (cache.BinSize * candles.CacheSize)
   134  			since = since - since%cache.BinSize // truncate to first end stamp of the epoch
   135  		}
   136  		if since < oldestNeeded {
   137  			oldestNeeded = since
   138  		}
   139  		sinceCaches[since] = cache
   140  	}
   141  
   142  	tstart := time.Now()
   143  	defer func() { log.Debugf("select epoch candles in: %v", time.Since(tstart)) }()
   144  
   145  	stmt := fmt.Sprintf(internal.SelectEpochCandles, epochReportsTableName)
   146  	rows, err := a.db.QueryContext(ctx, stmt, oldestNeeded) // +1 because candles aren't stored until the end stamp is surpassed.
   147  	if err != nil {
   148  		return fmt.Errorf("SelectEpochCandles: %w", err)
   149  	}
   150  
   151  	defer rows.Close()
   152  
   153  	var endStamp, epochDur, matchVol, quoteVol, highRate, lowRate, startRate, endRate fastUint64
   154  	for rows.Next() {
   155  		err = rows.Scan(&endStamp, &epochDur, &matchVol, &quoteVol, &highRate, &lowRate, &startRate, &endRate)
   156  		if err != nil {
   157  			return fmt.Errorf("Scan: %w", err)
   158  		}
   159  		candle := &candles.Candle{
   160  			StartStamp:  uint64(endStamp - epochDur),
   161  			EndStamp:    uint64(endStamp),
   162  			MatchVolume: uint64(matchVol),
   163  			QuoteVolume: uint64(quoteVol),
   164  			HighRate:    uint64(highRate),
   165  			LowRate:     uint64(lowRate),
   166  			StartRate:   uint64(startRate),
   167  			EndRate:     uint64(endRate),
   168  		}
   169  		for since, cache := range sinceCaches {
   170  			if uint64(endStamp) > since {
   171  				cache.Add(candle)
   172  			}
   173  		}
   174  	}
   175  
   176  	return rows.Err()
   177  }
   178  
   179  // LastCandleEndStamp pulls the last stored candles end stamp for a market and
   180  // candle duration.
   181  func (a *Archiver) LastCandleEndStamp(base, quote uint32, candleDur uint64) (uint64, error) {
   182  	marketSchema, err := a.marketSchema(base, quote)
   183  	if err != nil {
   184  		return 0, err
   185  	}
   186  
   187  	tableName := fullCandlesTableName(a.dbName, marketSchema, candleDur)
   188  
   189  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   190  	defer cancel()
   191  
   192  	stmt := fmt.Sprintf(internal.SelectLastEndStamp, tableName)
   193  	row := a.db.QueryRowContext(ctx, stmt)
   194  	var endStamp fastUint64
   195  	if err = row.Scan(&endStamp); err != nil {
   196  		if errors.Is(err, sql.ErrNoRows) {
   197  			return 0, nil
   198  		}
   199  		return 0, err
   200  	}
   201  	return uint64(endStamp), nil
   202  }
   203  
   204  // InsertCandles inserts new candles for a market and candle duration.
   205  func (a *Archiver) InsertCandles(base, quote uint32, candleDur uint64, cs []*candles.Candle) error {
   206  	marketSchema, err := a.marketSchema(base, quote)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	tableName := fullCandlesTableName(a.dbName, marketSchema, candleDur)
   211  	stmt := fmt.Sprintf(internal.InsertCandle, tableName)
   212  
   213  	insert := func(c *candles.Candle) error {
   214  		ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   215  		defer cancel()
   216  
   217  		_, err = a.db.ExecContext(ctx, stmt,
   218  			c.EndStamp, c.MatchVolume, c.QuoteVolume, c.HighRate, c.LowRate, c.StartRate, c.EndRate,
   219  		)
   220  		if err != nil {
   221  			a.fatalBackendErr(err)
   222  			return err
   223  		}
   224  		return nil
   225  	}
   226  
   227  	for _, c := range cs {
   228  		if err = insert(c); err != nil {
   229  			return err
   230  		}
   231  	}
   232  	return nil
   233  }
   234  
   235  // loadCandles loads the last n candles of a specified duration and market into
   236  // the provided cache.
   237  func (a *Archiver) loadCandles(base, quote uint32, cache *candles.Cache, n uint64) error {
   238  	marketSchema, err := a.marketSchema(base, quote)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	candleDur := cache.BinSize
   244  
   245  	tableName := fullCandlesTableName(a.dbName, marketSchema, candleDur)
   246  	stmt := fmt.Sprintf(internal.SelectCandles, tableName)
   247  
   248  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   249  	defer cancel()
   250  
   251  	rows, err := a.db.QueryContext(ctx, stmt, n)
   252  	if err != nil {
   253  		return fmt.Errorf("QueryContext: %w", err)
   254  	}
   255  	defer rows.Close()
   256  
   257  	var endStamp, matchVol, quoteVol, highRate, lowRate, startRate, endRate fastUint64
   258  	for rows.Next() {
   259  		err = rows.Scan(&endStamp, &matchVol, &quoteVol, &highRate, &lowRate, &startRate, &endRate)
   260  		if err != nil {
   261  			return fmt.Errorf("Scan: %w", err)
   262  		}
   263  		cache.Add(&candles.Candle{
   264  			StartStamp:  uint64(endStamp) - candleDur,
   265  			EndStamp:    uint64(endStamp),
   266  			MatchVolume: uint64(matchVol),
   267  			QuoteVolume: uint64(quoteVol),
   268  			HighRate:    uint64(highRate),
   269  			LowRate:     uint64(lowRate),
   270  			StartRate:   uint64(startRate),
   271  			EndRate:     uint64(endRate),
   272  		})
   273  	}
   274  
   275  	if err = rows.Err(); err != nil {
   276  		return err
   277  	}
   278  
   279  	return nil
   280  }