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  }