decred.org/dcrdex@v1.0.5/server/db/driver/pg/pg.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 "fmt" 10 "sync" 11 "time" 12 13 "decred.org/dcrdex/dex" 14 "decred.org/dcrdex/server/db" 15 ) 16 17 // Driver implements db.Driver. 18 type Driver struct{} 19 20 // Open creates the DB backend, returning a DEXArchivist. 21 func (d *Driver) Open(ctx context.Context, cfg any) (db.DEXArchivist, error) { 22 switch c := cfg.(type) { 23 case *Config: 24 return NewArchiver(ctx, c) 25 case Config: 26 return NewArchiver(ctx, &c) 27 default: 28 return nil, fmt.Errorf("invalid config type %t", cfg) 29 } 30 } 31 32 // UseLogger sets the package-wide logger for the registered DB Driver. 33 func (*Driver) UseLogger(logger dex.Logger) { 34 UseLogger(logger) 35 } 36 37 func init() { 38 db.Register("pg", &Driver{}) 39 } 40 41 const ( 42 defaultQueryTimeout = 20 * time.Minute 43 ) 44 45 // Config holds the Archiver's configuration. 46 type Config struct { 47 Host, Port, User, Pass, DBName string 48 ShowPGConfig bool 49 QueryTimeout time.Duration 50 51 // MarketCfg specifies all of the markets that the Archiver should prepare. 52 MarketCfg []*dex.MarketInfo 53 } 54 55 // Some frequently used long-form table names. 56 type archiverTables struct { 57 feeKeys string 58 accounts string 59 bonds string 60 prepaidBonds string 61 } 62 63 // Archiver must implement server/db.DEXArchivist. 64 // So far: OrderArchiver, AccountArchiver. 65 type Archiver struct { 66 ctx context.Context 67 queryTimeout time.Duration 68 db *sql.DB 69 dbName string 70 markets map[string]*dex.MarketInfo 71 tables archiverTables 72 73 fatalMtx sync.RWMutex 74 fatal chan struct{} 75 fatalErr error 76 } 77 78 // LastErr returns any fatal or unexpected error encountered in a recent query. 79 // This may be used to check if the database had an unrecoverable error 80 // (disconnect, etc.). 81 func (a *Archiver) LastErr() error { 82 a.fatalMtx.RLock() 83 defer a.fatalMtx.RUnlock() 84 return a.fatalErr 85 } 86 87 // Fatal returns a nil or closed channel for select use. Use LastErr to get the 88 // latest fatal error. 89 func (a *Archiver) Fatal() <-chan struct{} { 90 a.fatalMtx.RLock() 91 defer a.fatalMtx.RUnlock() 92 return a.fatal 93 } 94 95 func (a *Archiver) fatalBackendErr(err error) { 96 if err == nil { 97 return 98 } 99 a.fatalMtx.Lock() 100 if a.fatalErr == nil { 101 close(a.fatal) 102 } 103 a.fatalErr = err // consider slice and append 104 a.fatalMtx.Unlock() 105 } 106 107 // NewArchiverForRead constructs a new Archiver without creating or modifying 108 // any data structures. This should be used for read-only applications. Use 109 // Close when done with the Archiver. 110 func NewArchiverForRead(ctx context.Context, cfg *Config) (*Archiver, error) { 111 // Connect to the PostgreSQL daemon and return the *sql.DB. 112 db, err := connect(cfg.Host, cfg.Port, cfg.User, cfg.Pass, cfg.DBName) 113 if err != nil { 114 return nil, err 115 } 116 117 // Put the PostgreSQL time zone in UTC. 118 var initTZ string 119 initTZ, err = checkCurrentTimeZone(db) 120 if err != nil { 121 return nil, err 122 } 123 if initTZ != "UTC" { 124 log.Infof("Switching PostgreSQL time zone to UTC for this session.") 125 if _, err = db.Exec(`SET TIME ZONE UTC`); err != nil { 126 return nil, fmt.Errorf("Failed to set time zone to UTC: %w", err) 127 } 128 } 129 130 // Display the postgres version. 131 pgVersion, err := retrievePGVersion(db) 132 if err != nil { 133 return nil, err 134 } 135 log.Info(pgVersion) 136 137 queryTimeout := cfg.QueryTimeout 138 if queryTimeout <= 0 { 139 queryTimeout = defaultQueryTimeout 140 } 141 142 mktMap := make(map[string]*dex.MarketInfo, len(cfg.MarketCfg)) 143 for _, mkt := range cfg.MarketCfg { 144 mktMap[marketSchema(mkt.Name)] = mkt 145 } 146 147 return &Archiver{ 148 ctx: ctx, 149 db: db, 150 dbName: cfg.DBName, 151 queryTimeout: queryTimeout, 152 markets: mktMap, 153 tables: archiverTables{ 154 feeKeys: fullTableName(cfg.DBName, publicSchema, feeKeysTableName), 155 accounts: fullTableName(cfg.DBName, publicSchema, accountsTableName), 156 bonds: fullTableName(cfg.DBName, publicSchema, bondsTableName), 157 prepaidBonds: fullTableName(cfg.DBName, publicSchema, prepaidBondsTableName), 158 }, 159 fatal: make(chan struct{}), 160 }, nil 161 } 162 163 // NewArchiver constructs a new Archiver. All tables are created, including 164 // tables for markets that may have been added since last startup. Use Close 165 // when done with the Archiver. 166 func NewArchiver(ctx context.Context, cfg *Config) (*Archiver, error) { 167 archiver, err := NewArchiverForRead(ctx, cfg) 168 if err != nil { 169 return nil, err 170 } 171 172 // Check critical performance-related settings. 173 if err = archiver.checkPerfSettings(cfg.ShowPGConfig); err != nil { 174 return nil, err 175 } 176 177 // Ensure all tables required by the current market configuration are ready. 178 purgeMarkets, err := prepareTables(ctx, archiver.db, cfg.MarketCfg) 179 if err != nil { 180 return nil, err 181 } 182 for _, staleMarket := range purgeMarkets { 183 mkt := archiver.markets[staleMarket] 184 if mkt == nil { // shouldn't happen 185 return nil, fmt.Errorf("unrecognized market %v", staleMarket) 186 } 187 unbookedSells, unbookedBuys, err := archiver.FlushBook(mkt.Base, mkt.Quote) 188 if err != nil { 189 return nil, fmt.Errorf("failed to flush book for market %v: %w", staleMarket, err) 190 } 191 log.Infof("Flushed %d sell orders and %d buy orders from market %v with a changed lot size.", 192 len(unbookedSells), len(unbookedBuys), staleMarket) 193 } 194 195 return archiver, nil 196 } 197 198 // Close closes the underlying DB connection. 199 func (a *Archiver) Close() error { 200 return a.db.Close() 201 } 202 203 func (a *Archiver) marketSchema(base, quote uint32) (string, error) { 204 marketName, err := dex.MarketName(base, quote) 205 if err != nil { 206 return "", err 207 } 208 schema := marketSchema(marketName) 209 _, found := a.markets[schema] 210 if !found { 211 return "", db.ArchiveError{ 212 Code: db.ErrUnsupportedMarket, 213 Detail: fmt.Sprintf(`archiver does not support the market "%s"`, schema), 214 } 215 } 216 return schema, nil 217 }