Each Patroni member gets a share of its own RAM in its own config.yml:
shared_buffers a quarter (15% beside a control plane or agent),
effective_cache_size, maintenance_work_mem and work_mem scaled to it, all
clamped. It is read again on every server init --patroni, so a machine
given more memory gets the use of it. A single-machine PostgreSQL init
installs is sized the same way, as a shared machine, instead of a fixed
96MB.
The step itself went in with 85025df by accident, in an early form that
restarted PostgreSQL before Patroni had taken the reload, leaving the old
shared_buffers with a restart pending. It now waits for Patroni to report
the restart pending, restarts with restart_pending (replicas first, the
leader last), and restarts a member it finds pending from an earlier run.
Applied to the PoC: pg01 978MB, ctl02 (control plane beside it) 587MB,
both from 128MB; streaming, lag 0, gateways unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
108 lines
2.6 KiB
Go
108 lines
2.6 KiB
Go
package pgha
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MemberState is one member as Patroni reports it.
|
|
type MemberState struct {
|
|
Name string `json:"name"`
|
|
// Role is "leader", "replica" or "sync_standby".
|
|
Role string `json:"role"`
|
|
// State is "running" for a leader, "streaming" for a replica keeping up,
|
|
// and anything else for one that is not.
|
|
State string `json:"state"`
|
|
Host string `json:"host"`
|
|
Timeline int `json:"timeline"`
|
|
// PendingRestart is a parameter changed that PostgreSQL takes only on
|
|
// restart.
|
|
PendingRestart bool `json:"pending_restart"`
|
|
// Lag is how far behind the leader a replica is, in bytes; -1 when
|
|
// Patroni does not know, which is itself worth an alert.
|
|
Lag int64 `json:"-"`
|
|
RawLag json.RawMessage `json:"lag"`
|
|
}
|
|
|
|
// Cluster is the cluster's state, as any member's API reports it.
|
|
type Cluster struct {
|
|
Members []MemberState `json:"members"`
|
|
}
|
|
|
|
// Leader is the leading member's name, or "".
|
|
func (c Cluster) Leader() string {
|
|
for _, m := range c.Members {
|
|
if m.Role == "leader" || m.Role == "standby_leader" {
|
|
return m.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Complete reports whether n members are up: one leading, the rest streaming.
|
|
func (c Cluster) Complete(n int) bool {
|
|
if c.Leader() == "" || len(c.Members) < n {
|
|
return false
|
|
}
|
|
streaming := 0
|
|
for _, m := range c.Members {
|
|
if m.Role != "leader" && m.State == "streaming" {
|
|
streaming++
|
|
}
|
|
}
|
|
return streaming >= n-1
|
|
}
|
|
|
|
func (c Cluster) String() string {
|
|
if len(c.Members) == 0 {
|
|
return "empty"
|
|
}
|
|
var parts []string
|
|
for _, m := range c.Members {
|
|
parts = append(parts, m.Name+" "+m.Role+"/"+m.State)
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// HTTPStatus asks each host's Patroni in turn and returns the first answer:
|
|
// any member describes the whole cluster.
|
|
func HTTPStatus(ctx context.Context, hosts []string) (Cluster, error) {
|
|
var last error
|
|
for _, h := range hosts {
|
|
b, err := restJSON(ctx, h, http.MethodGet, "/cluster", nil, "", 3*time.Second)
|
|
if err != nil {
|
|
last = err
|
|
continue
|
|
}
|
|
var c Cluster
|
|
if err := json.Unmarshal(b, &c); err != nil {
|
|
last = fmt.Errorf("%s /cluster: %w", h, err)
|
|
continue
|
|
}
|
|
for i := range c.Members {
|
|
c.Members[i].Lag = lagOf(c.Members[i].RawLag)
|
|
}
|
|
return c, nil
|
|
}
|
|
if last == nil {
|
|
last = fmt.Errorf("no hosts to ask")
|
|
}
|
|
return Cluster{}, fmt.Errorf("no member's Patroni answered: %w", last)
|
|
}
|
|
|
|
// lagOf reads Patroni's lag: a number of bytes, or "unknown".
|
|
func lagOf(raw json.RawMessage) int64 {
|
|
if len(raw) == 0 {
|
|
return 0
|
|
}
|
|
var n int64
|
|
if json.Unmarshal(raw, &n) == nil {
|
|
return n
|
|
}
|
|
return -1
|
|
}
|