Skip to content

Commit 9dcff36

Browse files
authored
Merge pull request #99 from fly-apps/add_repl_slot_admin
Add admin api to view Replication slots
2 parents 559cfcc + 06fcd85 commit 9dcff36

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

pkg/commands/admin.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package commands
33
import (
44
"encoding/json"
55
"fmt"
6+
"github.com/fly-examples/postgres-ha/pkg/flypg"
7+
"github.com/pkg/errors"
68
"io"
79
"net/http"
810
"os/exec"
@@ -174,3 +176,115 @@ func handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
174176

175177
render.JSON(w, resp, http.StatusOK)
176178
}
179+
180+
func handleReplicationStats(w http.ResponseWriter, r *http.Request) {
181+
conn, close, err := localConnection(r.Context())
182+
if err != nil {
183+
render.Err(w, err)
184+
return
185+
}
186+
defer close()
187+
188+
stats, err := admin.ResolveReplicationLag(r.Context(), conn)
189+
if err != nil {
190+
render.Err(w, err)
191+
return
192+
}
193+
194+
resp := &Response{Result: stats}
195+
196+
render.JSON(w, resp, http.StatusOK)
197+
}
198+
199+
func handleStolonDBUid(w http.ResponseWriter, r *http.Request) {
200+
env, err := util.BuildEnv()
201+
if err != nil {
202+
render.Err(w, err)
203+
}
204+
205+
data, err := stolon.FetchClusterData(env)
206+
if err != nil {
207+
render.Err(w, err)
208+
}
209+
210+
node, err := flypg.NewNode()
211+
if err != nil {
212+
render.Err(w, err)
213+
}
214+
215+
for _, db := range data.DBs {
216+
if db.Spec.KeeperUID == node.KeeperUID {
217+
resp := &Response{Result: db.UID}
218+
render.JSON(w, resp, http.StatusOK)
219+
return
220+
}
221+
}
222+
223+
render.Err(w, errors.New("can't find db"))
224+
}
225+
226+
func handleEnableReadonly(w http.ResponseWriter, r *http.Request) {
227+
conn, close, err := localConnection(r.Context())
228+
if err != nil {
229+
render.Err(w, err)
230+
return
231+
}
232+
defer close()
233+
234+
err = admin.SetReadonly(r.Context(), conn, true)
235+
if err != nil {
236+
render.Err(w, err)
237+
}
238+
239+
args := []string{"root", "pkill", "haproxy"}
240+
241+
cmd := exec.Command("gosu", args...)
242+
243+
if err := cmd.Run(); err != nil {
244+
render.Err(w, err)
245+
return
246+
}
247+
248+
if cmd.ProcessState.ExitCode() != 0 {
249+
err := fmt.Errorf(cmd.ProcessState.String())
250+
render.Err(w, err)
251+
return
252+
}
253+
254+
resp := &Response{Result: true}
255+
256+
render.JSON(w, resp, http.StatusOK)
257+
}
258+
259+
func handleDisableReadonly(w http.ResponseWriter, r *http.Request) {
260+
conn, close, err := localConnection(r.Context())
261+
if err != nil {
262+
render.Err(w, err)
263+
return
264+
}
265+
defer close()
266+
267+
err = admin.SetReadonly(r.Context(), conn, false)
268+
if err != nil {
269+
render.Err(w, err)
270+
}
271+
272+
args := []string{"root", "pkill", "haproxy"}
273+
274+
cmd := exec.Command("gosu", args...)
275+
276+
if err := cmd.Run(); err != nil {
277+
render.Err(w, err)
278+
return
279+
}
280+
281+
if cmd.ProcessState.ExitCode() != 0 {
282+
err := fmt.Errorf(cmd.ProcessState.String())
283+
render.Err(w, err)
284+
return
285+
}
286+
287+
resp := &Response{Result: true}
288+
289+
render.JSON(w, resp, http.StatusOK)
290+
}

pkg/commands/handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ func Handler() http.Handler {
3232
r.Get("/failover/trigger", handleFailoverTrigger)
3333
r.Get("/restart", handleRestart)
3434
r.Get("/settings/view", handleViewSettings)
35+
r.Get("/replicationstats", handleReplicationStats)
36+
r.Post("/readonly/enable", handleEnableReadonly)
37+
r.Post("/readonly/disable", handleDisableReadonly)
38+
r.Get("/dbuid", handleStolonDBUid)
3539
r.Post("/settings/update", handleUpdateSettings)
3640
})
3741

pkg/flypg/admin/admin.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"crypto/md5"
77
"fmt"
8+
"github.com/pkg/errors"
89
"os"
910
"strings"
1011

@@ -252,6 +253,59 @@ func ResolveRole(ctx context.Context, pg *pgx.Conn) (string, error) {
252253
return "leader", nil
253254
}
254255

256+
type ReplicationStat struct {
257+
Name string
258+
Diff int
259+
}
260+
261+
func ResolveReplicationLag(ctx context.Context, pg *pgx.Conn) ([]*ReplicationStat, error) {
262+
sql := "select application_name, pg_current_wal_lsn() - flush_lsn as diff from pg_stat_replication;"
263+
264+
rows, err := pg.Query(ctx, sql)
265+
if err != nil {
266+
return nil, err
267+
}
268+
defer rows.Close()
269+
var stats []*ReplicationStat
270+
for rows.Next() {
271+
var s ReplicationStat
272+
if err := rows.Scan(&s.Name, &s.Diff); err != nil {
273+
return nil, err
274+
}
275+
stats = append(stats, &s)
276+
}
277+
return stats, nil
278+
}
279+
280+
func SetReadonly(ctx context.Context, pg *pgx.Conn, enable bool) error {
281+
role, err := ResolveRole(ctx, pg)
282+
if err != nil {
283+
return err
284+
}
285+
if role != "leader" {
286+
return errors.New("can't set non primary to read-only")
287+
}
288+
289+
databases, err := ListDatabases(ctx, pg)
290+
if err != nil {
291+
return err
292+
}
293+
294+
for _, db := range databases {
295+
// exclude administrative dbs
296+
if db.Name == "postgres" {
297+
continue
298+
}
299+
300+
sql := fmt.Sprintf("ALTER DATABASE %s SET default_transaction_read_only=%v;", db.Name, enable)
301+
if _, err = pg.Exec(ctx, sql); err != nil {
302+
return fmt.Errorf("failed to alter readonly state on db %s: %s", db.Name, err)
303+
}
304+
}
305+
306+
return nil
307+
}
308+
255309
func ResolveSettings(ctx context.Context, pg *pgx.Conn, list []string) (*flypg.Settings, error) {
256310
node, err := flypg.NewNode()
257311
if err != nil {

0 commit comments

Comments
 (0)