Skip to content

Commit a33cebf

Browse files
committed
database/sql: add examples for opening and testing a DB pool
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>
1 parent 79257c4 commit a33cebf

3 files changed

Lines changed: 298 additions & 23 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package sql_test
6+
7+
import (
8+
"context"
9+
"database/sql"
10+
"flag"
11+
"log"
12+
"os"
13+
"os/signal"
14+
"time"
15+
)
16+
17+
var pool *sql.DB // Database connection pool.
18+
19+
func Example_openDBCLI() {
20+
id := flag.Int64("id", 0, "person ID to find")
21+
dsn := flag.String("dsn", os.Getenv("DSN"), "connection data source name")
22+
flag.Parse()
23+
24+
if len(*dsn) == 0 {
25+
log.Fatal("missing dsn flag")
26+
}
27+
if *id == 0 {
28+
log.Fatal("missing person ID")
29+
}
30+
var err error
31+
32+
// Opening a driver typically will not attempt to connect to the database.
33+
pool, err = sql.Open("driver-name", *dsn)
34+
if err != nil {
35+
// This will not be a connection error, but a DSN parse error or
36+
// another initialization error.
37+
log.Fatal("unable to use data source name", err)
38+
}
39+
defer pool.Close()
40+
41+
pool.SetConnMaxLifetime(0)
42+
pool.SetMaxIdleConns(3)
43+
pool.SetMaxOpenConns(3)
44+
45+
ctx, stop := context.WithCancel(context.Background())
46+
defer stop()
47+
48+
appSignal := make(chan os.Signal, 3)
49+
signal.Notify(appSignal, os.Interrupt)
50+
51+
go func() {
52+
select {
53+
case <-appSignal:
54+
stop()
55+
}
56+
}()
57+
58+
Ping(ctx)
59+
60+
Query(ctx, *id)
61+
}
62+
63+
// Ping the database to verify DSN provided by the user is valid and the
64+
// server accessible. If the ping fails exit the program with an error.
65+
func Ping(ctx context.Context) {
66+
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
67+
defer cancel()
68+
69+
if err := pool.PingContext(ctx); err != nil {
70+
log.Fatalf("unable to connect to database: %v", err)
71+
}
72+
}
73+
74+
// Query the database for the information requested and prints the results.
75+
// If the query fails exit the program with an error.
76+
func Query(ctx context.Context, id int64) {
77+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
78+
defer cancel()
79+
80+
var name string
81+
err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&name)
82+
if err != nil {
83+
log.Fatal("unable to execute search query", err)
84+
}
85+
log.Println("name=", name)
86+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package sql_test
6+
7+
import (
8+
"context"
9+
"database/sql"
10+
"encoding/json"
11+
"fmt"
12+
"io"
13+
"log"
14+
"net/http"
15+
"time"
16+
)
17+
18+
func Example_openDBService() {
19+
// Opening a driver typically will not attempt to connect to the database.
20+
db, err := sql.Open("driver-name", "database=test1")
21+
if err != nil {
22+
// This will not be a connection error, but a DSN parse error or
23+
// another initialization error.
24+
log.Fatal(err)
25+
}
26+
db.SetConnMaxLifetime(0)
27+
db.SetMaxIdleConns(50)
28+
db.SetMaxOpenConns(50)
29+
30+
s := &Service{db: db}
31+
32+
http.ListenAndServe(":8080", s)
33+
}
34+
35+
type Service struct {
36+
db *sql.DB
37+
}
38+
39+
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
40+
db := s.db
41+
switch r.URL.Path {
42+
default:
43+
http.Error(w, "not found", http.StatusNotFound)
44+
return
45+
case "/healthz":
46+
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
47+
defer cancel()
48+
49+
err := s.db.PingContext(ctx)
50+
if err != nil {
51+
http.Error(w, fmt.Sprintf("db down: %v", err), http.StatusFailedDependency)
52+
return
53+
}
54+
w.WriteHeader(http.StatusOK)
55+
return
56+
case "/quick-action":
57+
// This is a short SELECT. Use the request context as the base of
58+
// the context timeout.
59+
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
60+
defer cancel()
61+
62+
id := 5
63+
org := 10
64+
var name string
65+
err := db.QueryRowContext(ctx, `
66+
select
67+
p.name
68+
from
69+
people as p
70+
join organization as o on p.organization = o.id
71+
where
72+
p.id = :id
73+
and o.id = :org
74+
;`,
75+
sql.Named("id", id),
76+
sql.Named("org", org),
77+
).Scan(&name)
78+
if err != nil {
79+
if err == sql.ErrNoRows {
80+
http.Error(w, "not found", http.StatusNotFound)
81+
return
82+
}
83+
http.Error(w, err.Error(), http.StatusInternalServerError)
84+
return
85+
}
86+
io.WriteString(w, name)
87+
return
88+
case "/long-action":
89+
// This is a long SELECT. Use the request context as the base of
90+
// the context timeout, but give it some time to finish. If
91+
// the client cancels before the query is done the query will also
92+
// be canceled.
93+
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
94+
defer cancel()
95+
96+
var names []string
97+
rows, err := db.QueryContext(ctx, "select p.name from people as p where p.active = true;")
98+
if err != nil {
99+
http.Error(w, err.Error(), http.StatusInternalServerError)
100+
return
101+
}
102+
103+
for rows.Next() {
104+
var name string
105+
err = rows.Scan(&name)
106+
if err != nil {
107+
break
108+
}
109+
names = append(names, name)
110+
}
111+
// Check for errors during rows "Close".
112+
// This may be more important if multiple statements are executed
113+
// in a single batch and rows were written as well as read.
114+
if closeErr := rows.Close(); closeErr != nil {
115+
http.Error(w, closeErr.Error(), http.StatusInternalServerError)
116+
return
117+
}
118+
119+
// Check for row scan error.
120+
if err != nil {
121+
http.Error(w, err.Error(), http.StatusInternalServerError)
122+
return
123+
}
124+
125+
// Check for errors during row iteration.
126+
if err = rows.Err(); err != nil {
127+
http.Error(w, err.Error(), http.StatusInternalServerError)
128+
return
129+
}
130+
131+
json.NewEncoder(w).Encode(names)
132+
return
133+
case "/async-action":
134+
// This action has side effects that we want to preserve
135+
// even if the client cancels the HTTP request part way through.
136+
// For this we do not use the http request context as a base for
137+
// the timeout.
138+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
139+
defer cancel()
140+
141+
var orderRef = "ABC123"
142+
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
143+
_, err = tx.ExecContext(ctx, "stored_proc_name", orderRef)
144+
145+
if err != nil {
146+
tx.Rollback()
147+
http.Error(w, err.Error(), http.StatusInternalServerError)
148+
return
149+
}
150+
err = tx.Commit()
151+
if err != nil {
152+
http.Error(w, "action in unknown state, check state before attempting again", http.StatusInternalServerError)
153+
return
154+
}
155+
w.WriteHeader(http.StatusOK)
156+
return
157+
}
158+
}

0 commit comments

Comments
 (0)