Repository navigation
Expand file tree
/
Copy pathexample_test.go
More file actions
129 lines (111 loc) · 3.95 KB
/
Copy pathexample_test.go
File metadata and controls
129 lines (111 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package redlease_test
import (
"context"
"database/sql"
"log"
"time"
goredis "github.com/redis/go-redis/v9"
"github.com/nijatsdev/redlease"
)
// This example runs a periodic job on exactly one instance at a time. Several
// instances call Run with the same Config.Name; only the elected leader executes
// the job. Each write the leader makes carries its leadership term's fencing
// token, so a stale leader that has not yet noticed it lost the lock cannot
// overwrite a newer leader's state.
func Example() {
rc := goredis.NewClient(&goredis.Options{Addr: "localhost:6379"})
e, err := redlease.New(rc, redlease.Config{
Name: "report-builder",
TTL: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Run blocks until ctx is cancelled. The callback runs only while this
// instance is the leader; its context is cancelled the moment leadership
// is lost. The Fencer carries this term's fencing token — use it for every
// write to shared state.
e.Run(ctx, func(leaderCtx context.Context, f redlease.Fencer) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-leaderCtx.Done():
return
case <-ticker.C:
// Record progress under the fencing token. If a newer leader
// has taken over, applied is false: this term is stale and
// must stop working.
applied, err := f.HSet(leaderCtx, "jobs:report", "status", "running")
if err != nil {
log.Printf("write failed: %v", err)
continue
}
if !applied {
log.Printf("fenced out by a newer leader; stopping")
return
}
}
}
})
}
// This example fences a write to a resource other than Redis — here a SQL
// database. redlease enforces the fence for you only on Redis writes; for any
// other store it gives you the term's fencing token and you enforce it at the
// resource, atomically with your write. In SQL that means a conditional UPDATE
// that only applies when the row's stored fence is not newer than your token.
//
// The table is assumed to hold a fence column alongside the value:
//
// CREATE TABLE state (id text PRIMARY KEY, value text, fence bigint NOT NULL DEFAULT 0);
//
// sql.Open needs a registered driver; a real program imports one, e.g.
//
// import _ "github.com/jackc/pgx/v5/stdlib" // database/sql driver "pgx"
//
// The LeaderFunc here performs one write and returns, which ends the term (see
// [LeaderFunc]); a leader with ongoing work loops until leaderCtx is done, as
// in the package example above.
func Example_databaseFencing() {
rc := goredis.NewClient(&goredis.Options{Addr: "localhost:6379"})
db, err := sql.Open("pgx", "postgres://localhost/app")
if err != nil {
log.Fatal(err)
}
defer func() { _ = db.Close() }()
e, err := redlease.New(rc, redlease.Config{Name: "report-builder", TTL: 5 * time.Second})
if err != nil {
log.Print(err)
return
}
ctx := context.Background()
e.Run(ctx, func(leaderCtx context.Context, f redlease.Fencer) {
// Take the term's token from the Fencer and enforce it yourself in SQL.
// The UPDATE applies only when our token is at least the stored fence,
// then advances it; a stale leader's lower token matches no row.
const q = `UPDATE state
SET value = $1, fence = $2
WHERE id = $3 AND fence <= $2`
res, err := db.ExecContext(leaderCtx, q, "running", f.Token(), "report")
if err != nil {
log.Printf("write failed: %v", err)
return
}
n, err := res.RowsAffected()
if err != nil {
log.Printf("rows affected: %v", err)
return
}
if n == 0 {
// No row updated: a newer leader has advanced the fence past our
// token — we are stale and must stop working. (Zero rows is also
// what a missing row yields; this example assumes the row is
// seeded. An INSERT ... ON CONFLICT upsert with the same fence
// condition covers the first write too.)
log.Printf("fenced out by a newer leader; stopping")
return
}
})
}