Files
llmgw/internal/pgha/status.go
T
Ilker ManapandClaude Opus 5.5 42d3b7ea22 The store's PostgreSQL sized to its machine's memory
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>
2026-10-08 21:48:53 +02:00

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
}