Skip to content

Commit

Permalink
database/sql: add examples for opening and testing a DB pool
Browse files Browse the repository at this point in the history
Show two larger application examples. One example that
could be used in a CLI, the other in a long running
service. These demonstarates different strategies for
handling DB.Ping errors in context.

Fixes #23738

Change-Id: Id01213caf1f47917239a7506b01d30e37db74d31
Reviewed-on: https://go-review.googlesource.com/c/101216
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
  • Loading branch information
kardianos committed Nov 16, 2018
1 parent 8f4bc46 commit 8d33508
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 23 deletions.
86 changes: 86 additions & 0 deletions src/database/sql/example_cli_test.go
@@ -0,0 +1,86 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sql_test

import (
"context"
"database/sql"
"flag"
"log"
"os"
"os/signal"
"time"
)

var pool *sql.DB // Database connection pool.

func Example_openDBCLI() {
id := flag.Int64("id", 0, "person ID to find")
dsn := flag.String("dsn", os.Getenv("DSN"), "connection data source name")
flag.Parse()

if len(*dsn) == 0 {
log.Fatal("missing dsn flag")
}
if *id == 0 {
log.Fatal("missing person ID")
}
var err error

// Opening a driver typically will not attempt to connect to the database.
pool, err = sql.Open("driver-name", *dsn)
if err != nil {
// This will not be a connection error, but a DSN parse error or
// another initialization error.
log.Fatal("unable to use data source name", err)
}
defer pool.Close()

pool.SetConnMaxLifetime(0)
pool.SetMaxIdleConns(3)
pool.SetMaxOpenConns(3)

ctx, stop := context.WithCancel(context.Background())
defer stop()

appSignal := make(chan os.Signal, 3)
signal.Notify(appSignal, os.Interrupt)

go func() {
select {
case <-appSignal:
stop()
}
}()

Ping(ctx)

Query(ctx, *id)
}

// Ping the database to verify DSN provided by the user is valid and the
// server accessible. If the ping fails exit the program with an error.
func Ping(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()

if err := pool.PingContext(ctx); err != nil {
log.Fatalf("unable to connect to database: %v", err)
}
}

// Query the database for the information requested and prints the results.
// If the query fails exit the program with an error.
func Query(ctx context.Context, id int64) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

var name string
err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&name)
if err != nil {
log.Fatal("unable to execute search query", err)
}
log.Println("name=", name)
}
158 changes: 158 additions & 0 deletions src/database/sql/example_service_test.go
@@ -0,0 +1,158 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sql_test

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)

func Example_openDBService() {
// Opening a driver typically will not attempt to connect to the database.
db, err := sql.Open("driver-name", "database=test1")
if err != nil {
// This will not be a connection error, but a DSN parse error or
// another initialization error.
log.Fatal(err)
}
db.SetConnMaxLifetime(0)
db.SetMaxIdleConns(50)
db.SetMaxOpenConns(50)

s := &Service{db: db}

http.ListenAndServe(":8080", s)
}

type Service struct {
db *sql.DB
}

func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
db := s.db
switch r.URL.Path {
default:
http.Error(w, "not found", http.StatusNotFound)
return
case "/healthz":
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
defer cancel()

err := s.db.PingContext(ctx)
if err != nil {
http.Error(w, fmt.Sprintf("db down: %v", err), http.StatusFailedDependency)
return
}
w.WriteHeader(http.StatusOK)
return
case "/quick-action":
// This is a short SELECT. Use the request context as the base of
// the context timeout.
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()

id := 5
org := 10
var name string
err := db.QueryRowContext(ctx, `
select
p.name
from
people as p
join organization as o on p.organization = o.id
where
p.id = :id
and o.id = :org
;`,
sql.Named("id", id),
sql.Named("org", org),
).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, name)
return
case "/long-action":
// This is a long SELECT. Use the request context as the base of
// the context timeout, but give it some time to finish. If
// the client cancels before the query is done the query will also
// be canceled.
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()

var names []string
rows, err := db.QueryContext(ctx, "select p.name from people as p where p.active = true;")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
break
}
names = append(names, name)
}
// Check for errors during rows "Close".
// This may be more important if multiple statements are executed
// in a single batch and rows were written as well as read.
if closeErr := rows.Close(); closeErr != nil {
http.Error(w, closeErr.Error(), http.StatusInternalServerError)
return
}

// Check for row scan error.
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Check for errors during row iteration.
if err = rows.Err(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(names)
return
case "/async-action":
// This action has side effects that we want to preserve
// even if the client cancels the HTTP request part way through.
// For this we do not use the http request context as a base for
// the timeout.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var orderRef = "ABC123"
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
_, err = tx.ExecContext(ctx, "stored_proc_name", orderRef)

if err != nil {
tx.Rollback()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = tx.Commit()
if err != nil {
http.Error(w, "action in unknown state, check state before attempting again", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
return
}
}

0 comments on commit 8d33508

Please sign in to comment.