Skip to content

Commit 94f56eb

Browse files
committed
Add database sql resilience example
1 parent bdd7070 commit 94f56eb

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

examples/sql/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SQL Example
2+
3+
This example demonstrates how to use Resile with Go's standard `database/sql` package.
4+
5+
It wraps `db.ExecContext` call with retries and a circuit breaker. The example uses a mock SQL driver instead of a real database so it can run anywhere without extra setup.
6+
7+
The mock driver fails the first two attempts with a temporary error and succeeds on the third attempt. This shows Resile retrying the operation.
8+
9+
## Run
10+
```bash
11+
go run ./examples/sql
12+
```
13+
Expected output:
14+
```
15+
query succeeded after 3 attempts; rows affected: 1
16+
```
17+
## How It Works
18+
```go
19+
result, err := resile.Do(ctx, func(ctx context.Context) (sql.Result, error) {
20+
return db.ExecContext(ctx, "UPDATE users SET active = ? WHERE id = ?",
21+
true,
22+
42
23+
)
24+
},
25+
resile.WithRetry(3),
26+
resile.WithBaseDelay(100*time.Millisecond),
27+
resile.WithCircuitBreaker(breaker),
28+
)
29+
```
30+
31+
The SQL call is wrapped with `resile.Do`.
32+
The circuit breaker is included to show how you can stop calling a database when repeated failures suggest it is unhealthy.
33+
34+
## Note about SQL Writes
35+
Be careful when retrying write queries like UPDATE, INSERT, or DELETE.
36+
A retry in this case will run the same write more than once if the first attempt has reached the database but client received no response. In production applications, transactions, idempotency keys, unique constraints, or other safeguards are employed when retrying writes.
37+

examples/sql/main.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"database/sql/driver"
7+
"errors"
8+
"fmt"
9+
"github.com/cinar/resile"
10+
"github.com/cinar/resile/circuit"
11+
"sync/atomic"
12+
"time"
13+
)
14+
15+
var attempts atomic.Int32
16+
17+
// transientFailureDriver is a mock SQL driver that fails the first two calls.
18+
type transientFailureDriver struct{}
19+
type transientFailureConnection struct{}
20+
21+
func (transientFailureDriver) Open(name string) (driver.Conn, error) {
22+
return transientFailureConnection{}, nil
23+
}
24+
25+
func (transientFailureConnection) Prepare(query string) (driver.Stmt, error) {
26+
return nil, errors.New("Prepare is not implemented")
27+
}
28+
29+
func (transientFailureConnection) Begin() (driver.Tx, error) {
30+
return nil, errors.New("transactions are not implemented")
31+
}
32+
33+
func (transientFailureConnection) Close() error {
34+
return nil
35+
}
36+
37+
func (transientFailureConnection) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
38+
current := attempts.Add(1)
39+
if current < 3 {
40+
return nil, errors.New("temporary database error")
41+
}
42+
return driver.RowsAffected(1), nil
43+
}
44+
45+
func main() {
46+
sql.Register("transient-sql", transientFailureDriver{})
47+
48+
db, err := sql.Open("transient-sql", "")
49+
if err != nil {
50+
panic(err)
51+
}
52+
defer db.Close()
53+
54+
ctx := context.Background()
55+
56+
breaker := circuit.New(circuit.Config{
57+
WindowType: circuit.WindowCountBased,
58+
WindowSize: 10,
59+
MinimumCalls: 3,
60+
FailureRateThreshold: 50,
61+
ResetTimeout: time.Second,
62+
})
63+
64+
// SQL call wrapped with Resile so transient db errors can be retried
65+
result, err := resile.Do(ctx, func(ctx context.Context) (sql.Result, error) {
66+
return db.ExecContext(ctx, "UPDATE users SET active = ? WHERE id = ?", true, 42)
67+
},
68+
resile.WithRetry(3),
69+
resile.WithBaseDelay(100*time.Millisecond),
70+
resile.WithCircuitBreaker(breaker),
71+
)
72+
73+
if err != nil {
74+
fmt.Printf("query failed: %v\n", err)
75+
return
76+
}
77+
78+
rowsAffected, _ := result.RowsAffected()
79+
fmt.Printf("query succeeded after %d attempts; rows affected: %d\n",
80+
attempts.Load(), rowsAffected)
81+
}

0 commit comments

Comments
 (0)