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, "eVol, &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, "eVol, &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 }