decred.org/dcrdex@v1.0.5/server/db/driver/pg/system.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  	"database/sql"
     8  	"database/sql/driver"
     9  	"errors"
    10  	"fmt"
    11  	"regexp"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"decred.org/dcrdex/server/db/driver/pg/internal"
    17  	pq "github.com/lib/pq" // the "postgres" sql driver
    18  )
    19  
    20  const publicSchema = "public"
    21  
    22  // connect opens a connection to a PostgreSQL database. The caller is
    23  // responsible for calling Close() on the returned db when finished using it.
    24  // The input host may be an IP address for TCP connection, or an absolute path
    25  // to a UNIX domain socket. An empty string should be provided for UNIX sockets.
    26  func connect(host, port, user, pass, dbName string) (*sql.DB, error) {
    27  	var psqlInfo string
    28  	if pass == "" {
    29  		psqlInfo = fmt.Sprintf("host=%s user=%s "+
    30  			"dbname=%s sslmode=disable",
    31  			host, user, dbName)
    32  	} else {
    33  		psqlInfo = fmt.Sprintf("host=%s user=%s "+
    34  			"password=%s dbname=%s sslmode=disable",
    35  			host, user, pass, dbName)
    36  	}
    37  
    38  	// Only add port for a TCP connection since UNIX domain sockets (specified
    39  	// by a "/" prefix) do not have a port.
    40  	if !strings.HasPrefix(host, "/") {
    41  		psqlInfo += fmt.Sprintf(" port=%s", port)
    42  	}
    43  
    44  	conn, err := pq.NewConnector(psqlInfo)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	var connector driver.Connector = pq.ConnectorWithNoticeHandler(conn, func(notice *pq.Error) {
    50  		// Notice severities:
    51  		//   DEBUG, LOG, INFO, NOTICE, WARNING, ERROR, FATAL, and PANIC.
    52  		//
    53  		// The LOG severity rank is below NOTICE for clients like us. The INFO
    54  		// rank varies, but it is neither a warning or an error.
    55  		//
    56  		// https://www.postgresql.org/docs/13/runtime-config-logging.html#RUNTIME-CONFIG-SEVERITY-LEVELS
    57  		//
    58  		// See also the client_min_messages setting, which applies to us:
    59  		// https://www.postgresql.org/docs/13/runtime-config-client.html#GUC-CLIENT-MIN-MESSAGES
    60  		// The default setting is NOTICE, which excludes DEBUG and LOG, but not
    61  		// INFO since that is sent to the client regardless of the setting.
    62  		var printer func(format string, params ...any)
    63  		switch notice.Severity {
    64  		case pq.Efatal, pq.Epanic: // error caused database session to abort
    65  			printer = log.Criticalf
    66  		// NOTE: postgresql has "ERROR" severity, but there is no pq const!
    67  		case "ERROR": // error caused current command to abort
    68  			printer = log.Errorf
    69  		case pq.Ewarning: // "likely problems"
    70  			printer = log.Warnf
    71  		// No INF level logging. Either it's a problem or just debug info.
    72  		case pq.Enotice, pq.Einfo:
    73  			printer = log.Debugf
    74  		case pq.Elog, pq.Edebug:
    75  			printer = log.Tracef
    76  		default:
    77  			printer = log.Warnf
    78  		}
    79  
    80  		// NOTICE often includes "successful completion"-coded notices that are
    81  		// quite spammy even for Debugf. Cleaner to check this down here.
    82  		// https://www.postgresql.org/docs/13/errcodes-appendix.html
    83  		if notice.Severity == pq.Enotice && notice.Code.Class() == "00" { // Class 00 - Successful Completion
    84  			printer = log.Tracef
    85  		}
    86  
    87  		// Format a string for the notice, which may not include Detail.
    88  		msg := fmt.Sprintf("pq: %s (%s) - %s: %s", notice.Severity, notice.Code,
    89  			notice.Code.Name(), notice.Message)
    90  		if notice.Detail != "" {
    91  			msg += " - " + notice.Detail
    92  		}
    93  
    94  		printer(msg)
    95  	})
    96  
    97  	db := sql.OpenDB(connector)
    98  
    99  	// Establish a connection and verify it is alive.
   100  	err = db.Ping()
   101  	return db, err
   102  }
   103  
   104  // sqlExecutor is implemented by both sql.DB and sql.Tx.
   105  type sqlExecutor interface {
   106  	Exec(query string, args ...any) (sql.Result, error)
   107  }
   108  
   109  type sqlQueryer interface {
   110  	Query(query string, args ...any) (*sql.Rows, error)
   111  	QueryRow(query string, args ...any) *sql.Row
   112  }
   113  
   114  // sqlExec executes the SQL statement string with any optional arguments, and
   115  // returns the number of rows affected.
   116  func sqlExec(db sqlExecutor, stmt string, args ...any) (int64, error) {
   117  	res, err := db.Exec(stmt, args...)
   118  	if err != nil {
   119  		return 0, err
   120  	}
   121  
   122  	var N int64
   123  	N, err = res.RowsAffected()
   124  	if err != nil {
   125  		return 0, fmt.Errorf(`error in RowsAffected: %w`, err)
   126  	}
   127  	return N, err
   128  }
   129  
   130  // sqlExecStmt executes the prepared SQL statement with any optional arguments,
   131  // and returns the number of rows affected.
   132  func sqlExecStmt(stmt *sql.Stmt, args ...any) (int64, error) {
   133  	res, err := stmt.Exec(args...)
   134  	if err != nil {
   135  		return 0, err
   136  	}
   137  
   138  	var N int64
   139  	N, err = res.RowsAffected()
   140  	if err != nil {
   141  		return 0, fmt.Errorf(`error in RowsAffected: %w`, err)
   142  	}
   143  	return N, err
   144  }
   145  
   146  // namespacedTableExists checks if the specified table exists.
   147  func namespacedTableExists(db sqlQueryer, schema, tableName string) (bool, error) {
   148  	rows, err := db.Query(`SELECT 1
   149  		FROM   pg_tables
   150  		WHERE  schemaname = $1
   151  		AND    tablename = $2;`,
   152  		schema, tableName)
   153  	if err != nil {
   154  		return false, err
   155  	}
   156  	defer rows.Close()
   157  
   158  	return rows.Next(), rows.Err()
   159  }
   160  
   161  // tableExists checks if the specified table exists.
   162  func tableExists(db sqlQueryer, tableName string) (bool, error) {
   163  	rows, err := db.Query(`select relname from pg_class where relname = $1`,
   164  		tableName)
   165  	if err != nil {
   166  		return false, err
   167  	}
   168  	defer rows.Close()
   169  
   170  	return rows.Next(), rows.Err()
   171  }
   172  
   173  func columnExists(db *sql.DB, schema, table, col string) (bool, error) {
   174  	var found bool
   175  	err := db.QueryRow(`SELECT EXISTS (
   176  			SELECT column_name
   177  			FROM information_schema.columns
   178  			WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
   179  		);`, schema, table, col).Scan(&found)
   180  	if err != nil {
   181  		return false, err
   182  	}
   183  	return found, nil
   184  }
   185  
   186  // schemaExists checks if the specified schema exists.
   187  func schemaExists(db sqlQueryer, tableName string) (bool, error) {
   188  	rows, err := db.Query(`select 1 from pg_catalog.pg_namespace where nspname = $1`,
   189  		tableName)
   190  	if err != nil {
   191  		return false, err
   192  	}
   193  	defer rows.Close()
   194  
   195  	return rows.Next(), rows.Err()
   196  }
   197  
   198  type sqlQueryExecutor interface {
   199  	sqlQueryer
   200  	sqlExecutor
   201  }
   202  
   203  func createIndexStmt(db sqlQueryExecutor, fmtStmt, indexName, fullTableName string) error {
   204  	stmt := fmt.Sprintf(fmtStmt, indexName, fullTableName)
   205  	log.Debugf("Creating index %q on the %q table.", indexName, fullTableName)
   206  	_, err := db.Exec(stmt)
   207  	if errors.Is(err, sql.ErrNoRows) {
   208  		err = nil
   209  	}
   210  	return err
   211  }
   212  
   213  // createTableStmt creates a table with the given name using the provided SQL
   214  // statement, if it does not already exist.
   215  func createTableStmt(db sqlQueryExecutor, fmtStmt, schema, tableName string) (bool, error) {
   216  	exists, err := namespacedTableExists(db, schema, tableName)
   217  	if err != nil {
   218  		return false, err
   219  	}
   220  
   221  	nameSpacedTable := schema + "." + tableName
   222  	var created bool
   223  	if !exists {
   224  		stmt := fmt.Sprintf(fmtStmt, nameSpacedTable)
   225  		log.Debugf("Creating the %q table.", nameSpacedTable)
   226  		_, err = db.Exec(stmt)
   227  		if err != nil {
   228  			return false, err
   229  		}
   230  		created = true
   231  	}
   232  
   233  	return created, nil
   234  }
   235  
   236  func dropTable(db sqlExecutor, tableName string) error {
   237  	_, err := db.Exec(fmt.Sprintf(`DROP TABLE IF EXISTS %s;`, tableName))
   238  	return err
   239  }
   240  
   241  // existsIndex checks if the specified index name exists.
   242  func existsIndex(db *sql.DB, indexName string) (exists bool, err error) {
   243  	err = db.QueryRow(internal.IndexExists, indexName, publicSchema).Scan(&exists)
   244  	if errors.Is(err, sql.ErrNoRows) {
   245  		err = nil
   246  	}
   247  	return
   248  }
   249  
   250  // isUniqueIndex checks if the given index name is defined as UNIQUE.
   251  func isUniqueIndex(db *sql.DB, indexName string) (isUnique bool, err error) {
   252  	err = db.QueryRow(internal.IndexIsUnique, indexName, publicSchema).Scan(&isUnique)
   253  	return
   254  }
   255  
   256  // parseUnit is used to separate a "unit" from pg_settings such as "8kB" into a
   257  // numeric component and a base unit string.
   258  func parseUnit(unit string) (multiple float64, baseUnit string, err error) {
   259  	// This regular expression is defined so that it will match any input.
   260  	re := regexp.MustCompile(`([-\d\.]*)\s*(.*)`)
   261  	matches := re.FindStringSubmatch(unit)
   262  	// One or more of the matched substrings may be "", but the base unit
   263  	// substring (matches[2]) will match anything.
   264  	if len(matches) != 3 {
   265  		panic("inconceivable!")
   266  	}
   267  
   268  	// The regexp eats leading spaces, but there may be trailing spaces
   269  	// remaining that should be removed.
   270  	baseUnit = strings.TrimSuffix(matches[2], " ")
   271  
   272  	// The numeric component is processed by strconv.ParseFloat except in the
   273  	// cases of an empty string or a single "-", which is interpreted as a
   274  	// negative sign.
   275  	switch matches[1] {
   276  	case "":
   277  		multiple = 1
   278  	case "-":
   279  		multiple = -1
   280  	default:
   281  		multiple, err = strconv.ParseFloat(matches[1], 64)
   282  		if err != nil {
   283  			// If the numeric part does not parse as a valid number (e.g.
   284  			// "3.2.1-"), reset the base unit and return the non-nil error.
   285  			baseUnit = ""
   286  		}
   287  	}
   288  
   289  	return
   290  }
   291  
   292  // PGSetting describes a PostgreSQL setting scanned from pg_settings.
   293  type PGSetting struct {
   294  	Name, Setting, Unit, ShortDesc, Source, SourceFile, SourceLine string
   295  }
   296  
   297  // PGSettings facilitates looking up a PGSetting based on a setting's Name.
   298  type PGSettings map[string]PGSetting
   299  
   300  // String implements the Stringer interface, generating a table of the settings
   301  // where the Setting and Unit fields are merged into a single column. The rows
   302  // of the table are sorted by the PGSettings string key (the setting's Name).
   303  // This function is not thread-safe, so do not modify PGSettings concurrently.
   304  func (pgs PGSettings) String() string {
   305  	// Sort the names.
   306  	numSettings := len(pgs)
   307  	names := make([]string, 0, numSettings)
   308  	for name := range pgs {
   309  		names = append(names, name)
   310  	}
   311  	sort.Strings(names)
   312  
   313  	// Determine max width of "Setting", "Name", and "File" entries.
   314  	fileWidth, nameWidth, settingWidth := 4, 4, 7
   315  	// Also combine Setting and Unit, in the same order as the sorted names.
   316  	fullSettings := make([]string, 0, numSettings)
   317  	for i := range names {
   318  		s, ok := pgs[names[i]]
   319  		if !ok {
   320  			log.Errorf("(PGSettings).String is not thread-safe!")
   321  			continue
   322  		}
   323  
   324  		// Combine Setting and Unit.
   325  		fullSetting := s.Setting
   326  		// See if setting is numeric. Assume non-numeric settings have no Unit.
   327  		if num1, err := strconv.ParseFloat(s.Setting, 64); err == nil {
   328  			// Combine with the unit if numeric.
   329  			if num2, unit, err := parseUnit(s.Unit); err == nil {
   330  				if unit != "" {
   331  					unit = " " + unit
   332  				}
   333  				// Combine. e.g. 10.0, "8kB" => "80 kB"
   334  				fullSetting = fmt.Sprintf("%.12g%s", num1*num2, unit)
   335  			} else {
   336  				// Mystery unit.
   337  				fullSetting += " " + s.Unit
   338  			}
   339  		}
   340  
   341  		fullSettings = append(fullSettings, fullSetting)
   342  
   343  		if len(fullSetting) > settingWidth {
   344  			settingWidth = len(fullSetting)
   345  		}
   346  
   347  		// File column width.
   348  		if len(s.SourceFile) > fileWidth {
   349  			fileWidth = len(s.SourceFile)
   350  		}
   351  		// Name column width.
   352  		if len(s.Name) > nameWidth {
   353  			nameWidth = len(s.Name)
   354  		}
   355  	}
   356  
   357  	format := "%" + strconv.Itoa(nameWidth) + "s | %" + strconv.Itoa(settingWidth) +
   358  		"s | %10.10s | %" + strconv.Itoa(fileWidth) + "s | %5s | %-48.48s\n"
   359  
   360  	// Write the headers and a horizontal bar.
   361  	out := fmt.Sprintf(format, "Name", "Setting", "Source", "File", "Line", "Description")
   362  	hBar := strings.Repeat(string([]rune{0x2550}), nameWidth+1) + string([]rune{0x256A}) +
   363  		strings.Repeat(string([]rune{0x2550}), settingWidth+2) + string([]rune{0x256A}) +
   364  		strings.Repeat(string([]rune{0x2550}), 12) + string([]rune{0x256A}) +
   365  		strings.Repeat(string([]rune{0x2550}), fileWidth+2) + string([]rune{0x256A}) +
   366  		strings.Repeat(string([]rune{0x2550}), 7) + string([]rune{0x256A}) +
   367  		strings.Repeat(string([]rune{0x2550}), 50)
   368  	out += hBar + "\n"
   369  
   370  	// Write each row.
   371  	for i := range names {
   372  		s, ok := pgs[names[i]]
   373  		if !ok {
   374  			log.Warnf("(PGSettings).String is not thread-safe!")
   375  			continue
   376  		}
   377  		out += fmt.Sprintf(format, s.Name, fullSettings[i], s.Source,
   378  			s.SourceFile, s.SourceLine, s.ShortDesc)
   379  	}
   380  	return out
   381  }
   382  
   383  // retrievePGVersion retrieves the version of the connected PostgreSQL server.
   384  func retrievePGVersion(db *sql.DB) (ver string, err error) {
   385  	err = db.QueryRow(internal.RetrievePGVersion).Scan(&ver)
   386  	return
   387  }
   388  
   389  // retrieveSysSettings retrieves the PostgreSQL settings provided a query that
   390  // returns the following columns from pg_setting in order: name, setting, unit,
   391  // short_desc, source, sourcefile, sourceline.
   392  func retrieveSysSettings(stmt string, db *sql.DB) (PGSettings, error) {
   393  	rows, err := db.Query(stmt)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	defer rows.Close()
   398  
   399  	settings := make(PGSettings)
   400  
   401  	for rows.Next() {
   402  		var name, setting, unit, shortDesc, source, sourceFile sql.NullString
   403  		var sourceLine sql.NullInt64
   404  		err = rows.Scan(&name, &setting, &unit, &shortDesc,
   405  			&source, &sourceFile, &sourceLine)
   406  		if err != nil {
   407  			return nil, err
   408  		}
   409  
   410  		// If the source is "configuration file", but the file path is empty,
   411  		// the connected postgres user does not have sufficient privileges.
   412  		var line, file string
   413  		if source.String == "configuration file" {
   414  			// Shorten the source string.
   415  			source.String = "conf file"
   416  			if sourceFile.String == "" {
   417  				file = "NO PERMISSION"
   418  			} else {
   419  				file = sourceFile.String
   420  				line = strconv.FormatInt(sourceLine.Int64, 10)
   421  			}
   422  		}
   423  
   424  		settings[name.String] = PGSetting{
   425  			Name:       name.String,
   426  			Setting:    setting.String,
   427  			Unit:       unit.String,
   428  			ShortDesc:  shortDesc.String,
   429  			Source:     source.String,
   430  			SourceFile: file,
   431  			SourceLine: line,
   432  		}
   433  	}
   434  
   435  	if err = rows.Err(); err != nil {
   436  		return nil, err
   437  	}
   438  
   439  	return settings, nil
   440  }
   441  
   442  // retrieveSysSettingsConfFile retrieves settings that are set by a
   443  // configuration file (rather than default, environment variable, etc.).
   444  func retrieveSysSettingsConfFile(db *sql.DB) (PGSettings, error) {
   445  	return retrieveSysSettings(internal.RetrieveSysSettingsConfFile, db)
   446  }
   447  
   448  // retrieveSysSettingsPerformance retrieves performance-related settings.
   449  func retrieveSysSettingsPerformance(db *sql.DB) (PGSettings, error) {
   450  	return retrieveSysSettings(internal.RetrieveSysSettingsPerformance, db)
   451  }
   452  
   453  // retrieveSysSettingsServer a key server configuration settings (config_file,
   454  // data_directory, max_connections, dynamic_shared_memory_type,
   455  // max_files_per_process, port, unix_socket_directories), which may be helpful
   456  // in debugging connectivity issues or other DB errors.
   457  func retrieveSysSettingsServer(db *sql.DB) (PGSettings, error) {
   458  	return retrieveSysSettings(internal.RetrieveSysSettingsServer, db)
   459  }
   460  
   461  // retrieveSysSettingSyncCommit retrieves the synchronous_commit setting.
   462  func retrieveSysSettingSyncCommit(db *sql.DB) (syncCommit string, err error) {
   463  	err = db.QueryRow(internal.RetrieveSyncCommitSetting).Scan(&syncCommit)
   464  	return
   465  }
   466  
   467  // setSynchronousCommit sets the synchronous_commit setting.
   468  func setSynchronousCommit(db sqlExecutor, syncCommit string) error {
   469  	_, err := db.Exec(fmt.Sprintf(`SET synchronous_commit TO %s;`, syncCommit))
   470  	return err
   471  }
   472  
   473  // checkCurrentTimeZone queries for the currently set postgres time zone.
   474  func checkCurrentTimeZone(db *sql.DB) (currentTZ string, err error) {
   475  	if err = db.QueryRow(`SHOW TIME ZONE`).Scan(&currentTZ); err != nil {
   476  		err = fmt.Errorf("unable to query current time zone: %w", err)
   477  	}
   478  	return
   479  }
   480  
   481  func (a *Archiver) checkPerfSettings(showPGConfig bool) error {
   482  	// Optionally log the PostgreSQL configuration.
   483  	if showPGConfig {
   484  		perfSettings, err := retrieveSysSettingsPerformance(a.db)
   485  		if err != nil {
   486  			return err
   487  		}
   488  		log.Infof("postgres configuration settings:\n%v", perfSettings)
   489  
   490  		servSettings, err := retrieveSysSettingsServer(a.db)
   491  		if err != nil {
   492  			return err
   493  		}
   494  		log.Infof("postgres server settings:\n%v", servSettings)
   495  	}
   496  
   497  	// Check the synchronous_commit setting.
   498  	syncCommit, err := retrieveSysSettingSyncCommit(a.db)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	if syncCommit != "off" {
   503  		log.Warnf(`PERFORMANCE ISSUE! The synchronous_commit setting is "%s". `+
   504  			`Changing it to "off".`, syncCommit)
   505  		// Turn off synchronous_commit.
   506  		if err = setSynchronousCommit(a.db, "off"); err != nil {
   507  			return fmt.Errorf("failed to set synchronous_commit: %w", err)
   508  		}
   509  		// Verify that the setting was changed.
   510  		if syncCommit, err = retrieveSysSettingSyncCommit(a.db); err != nil {
   511  			return err
   512  		}
   513  		if syncCommit != "off" {
   514  			return fmt.Errorf(`Failed to set synchronous_commit="off". ` +
   515  				`Check PostgreSQL user permissions.`)
   516  		}
   517  	}
   518  	return nil
   519  }
   520  
   521  // createSchema creates a new schema.
   522  func createSchema(db *sql.DB, schema string) (bool, error) {
   523  	exists, err := schemaExists(db, schema)
   524  	if err != nil {
   525  		return false, err
   526  	}
   527  
   528  	var created bool
   529  	if !exists {
   530  		stmt := fmt.Sprintf(internal.CreateSchema, schema)
   531  		_, err = db.Exec(stmt)
   532  		if err != nil {
   533  			return false, err
   534  		}
   535  		created = true
   536  	}
   537  
   538  	return created, err
   539  }
   540  
   541  // fullTableName creates a long-form table name of the form dbName.schema.table.
   542  func fullTableName(dbName, schema, table string) string {
   543  	return dbName + "." + schema + "." + table
   544  }