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(¤tTZ); 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 }