Initial managed-container HA resource agent baseline
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
# Sourced by managed-container; uses the shared OCF and parameter environment.
|
||||
|
||||
pg_command()
|
||||
{
|
||||
local command="$1"
|
||||
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
sh -c "${command}"
|
||||
}
|
||||
|
||||
pgdata_path()
|
||||
{
|
||||
pg_command '
|
||||
if [ -z "$PGDATA" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "%s\n" "$PGDATA"
|
||||
'
|
||||
}
|
||||
|
||||
pg_running()
|
||||
{
|
||||
pg_command '
|
||||
BINDIR="$(pg_config --bindir)" || exit 1
|
||||
"$BINDIR/pg_ctl" status -D "$PGDATA" >/dev/null 2>&1
|
||||
'
|
||||
}
|
||||
|
||||
pg_ready()
|
||||
{
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
pg_isready -q \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}"
|
||||
}
|
||||
|
||||
pg_role()
|
||||
{
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-Atqc 'SELECT pg_is_in_recovery();'
|
||||
}
|
||||
|
||||
pg_wal_receiver_status()
|
||||
{
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-Atqc "SELECT status FROM pg_stat_wal_receiver LIMIT 1;"
|
||||
}
|
||||
|
||||
pg_streaming()
|
||||
{
|
||||
[ "$(pg_wal_receiver_status 2>/dev/null || true)" = "streaming" ]
|
||||
}
|
||||
|
||||
pg_wal_receiver_sender()
|
||||
{
|
||||
mc_runtime_exec \
|
||||
--user "${PG_OS_USER}" \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-Atqc "
|
||||
SELECT sender_host || '|' || sender_port
|
||||
FROM pg_stat_wal_receiver
|
||||
WHERE status = 'streaming'
|
||||
LIMIT 1;
|
||||
"
|
||||
}
|
||||
|
||||
mc_pg_rejoin_running_standby()
|
||||
{
|
||||
local primary_info primary_rc primary_node primary_host primary_port
|
||||
|
||||
[ -n "${OCF_RESKEY_replication_peers:-}" ] || return 1
|
||||
|
||||
primary_info="$(mc_pg_find_remote_primary 2>/dev/null)"
|
||||
primary_rc=$?
|
||||
|
||||
case "${primary_rc}" in
|
||||
0)
|
||||
primary_node="${primary_info%%|*}"
|
||||
primary_info="${primary_info#*|}"
|
||||
primary_host="${primary_info%%|*}"
|
||||
primary_port="${primary_info#*|}"
|
||||
|
||||
ocf_log warn \
|
||||
"Standby is not streaming while remote primary ${primary_node} (${primary_host}:${primary_port}) is available; forcing managed rejoin"
|
||||
|
||||
if pg_running; then
|
||||
if ! pg_command '
|
||||
BINDIR="$(pg_config --bindir)" || exit 1
|
||||
exec "$BINDIR/pg_ctl" \
|
||||
-D "$PGDATA" \
|
||||
stop -m fast -w
|
||||
'
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"Unable to stop non-streaming standby before managed rejoin"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
mc_pg_require_replication_slot || return "$?"
|
||||
|
||||
REJOIN_SOURCE_HOST="${primary_host}"
|
||||
REJOIN_SOURCE_PORT="${primary_port}"
|
||||
OCF_RESKEY_rejoin_source_host="${primary_host}"
|
||||
OCF_RESKEY_rejoin_source_port="${primary_port}"
|
||||
|
||||
if ! managed_rejoin; then
|
||||
ocf_exit_reason \
|
||||
"Managed rejoin of non-streaming standby against ${primary_node} failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
pg_clear_rejoin_pending >/dev/null 2>&1 || true
|
||||
return "${OCF_SUCCESS}"
|
||||
;;
|
||||
|
||||
1)
|
||||
# No writable peer exists. This is a normal failover-election
|
||||
# condition; leave the standby running Unpromoted and let the
|
||||
# promotion scorer decide whether it is eligible.
|
||||
return 1
|
||||
;;
|
||||
|
||||
2)
|
||||
ocf_exit_reason \
|
||||
"Multiple writable PostgreSQL peers detected while repairing non-streaming standby"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
|
||||
*)
|
||||
ocf_exit_reason \
|
||||
"Unable to determine PostgreSQL peer-primary state while repairing non-streaming standby"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
pg_rejoin_pending()
|
||||
{
|
||||
pg_command 'test -f "$PGDATA/.loxthelion-ha-rejoin-required"'
|
||||
}
|
||||
|
||||
pg_mark_rejoin_pending()
|
||||
{
|
||||
pg_command '
|
||||
umask 077
|
||||
: > "$PGDATA/.loxthelion-ha-rejoin-required"
|
||||
chmod 600 "$PGDATA/.loxthelion-ha-rejoin-required"
|
||||
'
|
||||
}
|
||||
|
||||
pg_clear_rejoin_pending()
|
||||
{
|
||||
pg_command 'rm -f "$PGDATA/.loxthelion-ha-rejoin-required"'
|
||||
}
|
||||
|
||||
mc_pg_reconcile_rejoin_marker()
|
||||
{
|
||||
local primary_info primary_rc primary_node primary_host primary_port
|
||||
local receiver_sender
|
||||
|
||||
[ -n "${OCF_RESKEY_replication_peers:-}" ] || return 1
|
||||
|
||||
primary_info="$(mc_pg_find_remote_primary 2>/dev/null)"
|
||||
primary_rc=$?
|
||||
|
||||
[ "${primary_rc}" -eq 0 ] || return 1
|
||||
|
||||
primary_node="${primary_info%%|*}"
|
||||
primary_info="${primary_info#*|}"
|
||||
primary_host="${primary_info%%|*}"
|
||||
primary_port="${primary_info#*|}"
|
||||
|
||||
receiver_sender="$(pg_wal_receiver_sender 2>/dev/null || true)"
|
||||
|
||||
if ! pg_streaming; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${receiver_sender}" != "${primary_host}|${primary_port}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! pg_clear_rejoin_pending; then
|
||||
ocf_log warn \
|
||||
"Standby is streaming from elected primary ${primary_node}, but rejoin-required marker could not be cleared"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ocf_log info \
|
||||
"Standby is verified streaming from elected primary ${primary_node} (${primary_host}:${primary_port}); cleared provisional rejoin marker"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
pg_has_standby_signal()
|
||||
{
|
||||
pg_command 'test -f "$PGDATA/standby.signal"'
|
||||
}
|
||||
|
||||
managed_monitor()
|
||||
{
|
||||
local role
|
||||
|
||||
if ! service_running; then
|
||||
ocf_log debug "User service ${SERVICE_NAME} is not active"
|
||||
return "${OCF_NOT_RUNNING}"
|
||||
fi
|
||||
|
||||
if ! container_running; then
|
||||
ocf_log warn \
|
||||
"Service ${SERVICE_NAME} is active but container ${CONTAINER_NAME} is not running"
|
||||
return "${OCF_NOT_RUNNING}"
|
||||
fi
|
||||
|
||||
if ! pg_running; then
|
||||
ocf_log debug \
|
||||
"Container ${CONTAINER_NAME} is running but PostgreSQL is stopped"
|
||||
return "${OCF_NOT_RUNNING}"
|
||||
fi
|
||||
|
||||
if ! pg_ready; then
|
||||
ocf_exit_reason \
|
||||
"PostgreSQL process exists but pg_isready failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
role="$(pg_role 2>/dev/null)" || {
|
||||
ocf_exit_reason \
|
||||
"PostgreSQL is accepting connections but role query failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
}
|
||||
|
||||
case "${role}" in
|
||||
f)
|
||||
ocf_log debug \
|
||||
"PostgreSQL healthy: role=primary (promoted)"
|
||||
return "${OCF_RUNNING_PROMOTED}"
|
||||
;;
|
||||
t)
|
||||
# A former primary may be started provisionally in recovery when
|
||||
# no writable peer exists yet, solely so Pacemaker can decide
|
||||
# which Unpromoted instance to promote. Once another node becomes
|
||||
# primary, force recovery of this provisional instance through a
|
||||
# stop/start cycle so start() can pg_rewind it against the winner.
|
||||
if pg_rejoin_pending && [ -n "${OCF_RESKEY_replication_peers:-}" ]; then
|
||||
local primary_info primary_rc
|
||||
primary_info="$(mc_pg_find_remote_primary 2>/dev/null)"
|
||||
primary_rc=$?
|
||||
|
||||
case "${primary_rc}" in
|
||||
0)
|
||||
ocf_exit_reason \
|
||||
"A remote primary is now available (${primary_info%%|*}); provisional former-primary standby requires rejoin"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
2)
|
||||
ocf_exit_reason \
|
||||
"Multiple writable PostgreSQL peers detected while monitoring provisional standby"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if pg_streaming; then
|
||||
ocf_log debug \
|
||||
"PostgreSQL healthy: role=standby (unpromoted), WAL receiver=streaming"
|
||||
return "${OCF_SUCCESS}"
|
||||
fi
|
||||
|
||||
# A standby can legitimately lose its receiver while the cluster
|
||||
# has no writable primary (for example during failover election).
|
||||
# Do not declare that condition unhealthy: promotion eligibility
|
||||
# is decided separately by the promotion-scoring evidence. If a
|
||||
# writable primary already exists, however, a non-streaming
|
||||
# standby is broken and must be repaired rather than reported OK.
|
||||
if [ -n "${OCF_RESKEY_replication_peers:-}" ]; then
|
||||
local primary_info primary_rc
|
||||
primary_info="$(mc_pg_find_remote_primary 2>/dev/null)"
|
||||
primary_rc=$?
|
||||
|
||||
case "${primary_rc}" in
|
||||
0)
|
||||
ocf_exit_reason \
|
||||
"PostgreSQL standby is in recovery but WAL receiver is not streaming while remote primary ${primary_info%%|*} is available"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
1)
|
||||
ocf_log debug \
|
||||
"PostgreSQL standby is disconnected and no remote primary is currently identifiable; leaving promotion eligibility to Pacemaker scoring"
|
||||
return "${OCF_SUCCESS}"
|
||||
;;
|
||||
2)
|
||||
ocf_exit_reason \
|
||||
"Multiple writable PostgreSQL peers detected while monitoring standby"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
*)
|
||||
ocf_exit_reason \
|
||||
"Unable to determine PostgreSQL peer-primary state while monitoring standby"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Backward-compatible behavior when native peer discovery is not
|
||||
# configured: role-aware monitoring cannot prove an upstream.
|
||||
ocf_log debug \
|
||||
"PostgreSQL healthy: role=standby (unpromoted); native peer discovery is not configured"
|
||||
return "${OCF_SUCCESS}"
|
||||
;;
|
||||
*)
|
||||
ocf_exit_reason \
|
||||
"Unexpected pg_is_in_recovery() result: '${role}'"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
esac
|
||||
|
||||
}
|
||||
|
||||
managed_start()
|
||||
{
|
||||
local i monitor_rc
|
||||
|
||||
managed_monitor
|
||||
monitor_rc=$?
|
||||
|
||||
case "${monitor_rc}" in
|
||||
"${OCF_SUCCESS}")
|
||||
ocf_log info \
|
||||
"PostgreSQL resource already running unpromoted"
|
||||
return "${OCF_SUCCESS}"
|
||||
;;
|
||||
|
||||
"${OCF_RUNNING_PROMOTED}")
|
||||
ocf_exit_reason \
|
||||
"Refusing start: PostgreSQL is already primary; promotion must occur through promote()"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
|
||||
"${OCF_NOT_RUNNING}")
|
||||
# Expected for a stopped resource. Continue.
|
||||
;;
|
||||
|
||||
*)
|
||||
# If start() is called for an already-running standby that is in
|
||||
# recovery but cannot stream from an existing primary, repair it
|
||||
# instead of simply returning the monitor failure. This catches a
|
||||
# previous partial rejoin whose standby.signal survived restart.
|
||||
if service_running && container_running && pg_running; then
|
||||
local existing_role
|
||||
existing_role="$(pg_role 2>/dev/null || true)"
|
||||
if [ "${existing_role}" = "t" ] && ! pg_streaming; then
|
||||
if mc_pg_rejoin_running_standby; then
|
||||
managed_monitor
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
ocf_exit_reason \
|
||||
"Cannot start unhealthy PostgreSQL instance (monitor rc=${monitor_rc})"
|
||||
return "${monitor_rc}"
|
||||
;;
|
||||
esac
|
||||
|
||||
ocf_log info \
|
||||
"Ensuring container service ${SERVICE_NAME} is running"
|
||||
|
||||
if ! service_running; then
|
||||
if ! mc_lifecycle_start "${SERVICE_NAME}"
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"Failed to start ${SERVICE_NAME}"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
for i in $(seq 1 20); do
|
||||
container_running && break
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if ! container_running; then
|
||||
ocf_exit_reason \
|
||||
"Container ${CONTAINER_NAME} did not become running"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
if ! pg_running; then
|
||||
#
|
||||
# OCF promotable-resource rule:
|
||||
# start() must produce an Unpromoted instance. Missing standby.signal
|
||||
# therefore never authorizes a primary start. It means either:
|
||||
# * a former primary can be rewound against an existing winner, or
|
||||
# * no winner exists yet, in which case place this PGDATA into
|
||||
# recovery and let Pacemaker decide whether to promote it.
|
||||
#
|
||||
if pg_rejoin_pending || ! pg_has_standby_signal; then
|
||||
local primary_info primary_rc primary_node primary_host primary_port
|
||||
|
||||
if [ -z "${OCF_RESKEY_replication_peers:-}" ]; then
|
||||
ocf_exit_reason \
|
||||
"Cannot safely recover PGDATA without standby.signal: replication_peers is required for automatic primary discovery"
|
||||
return "${OCF_ERR_CONFIGURED}"
|
||||
fi
|
||||
|
||||
# At this point the maintenance container is running, so resolve
|
||||
# the exact configured slot from PostgreSQL/container state before
|
||||
# any rewind or recovery configuration is written.
|
||||
mc_pg_require_replication_slot || return "$?"
|
||||
|
||||
primary_info="$(mc_pg_find_remote_primary 2>/dev/null)"
|
||||
primary_rc=$?
|
||||
|
||||
case "${primary_rc}" in
|
||||
0)
|
||||
primary_node="${primary_info%%|*}"
|
||||
primary_info="${primary_info#*|}"
|
||||
primary_host="${primary_info%%|*}"
|
||||
primary_port="${primary_info#*|}"
|
||||
|
||||
ocf_log info \
|
||||
"Existing remote primary ${primary_node} (${primary_host}:${primary_port}) found; rejoining local PGDATA before start"
|
||||
|
||||
REJOIN_SOURCE_HOST="${primary_host}"
|
||||
REJOIN_SOURCE_PORT="${primary_port}"
|
||||
OCF_RESKEY_rejoin_source_host="${primary_host}"
|
||||
OCF_RESKEY_rejoin_source_port="${primary_port}"
|
||||
|
||||
if ! managed_rejoin; then
|
||||
ocf_exit_reason \
|
||||
"Automatic rejoin against ${primary_node} failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
pg_clear_rejoin_pending >/dev/null 2>&1 || true
|
||||
managed_monitor
|
||||
return $?
|
||||
;;
|
||||
|
||||
1)
|
||||
ocf_log info \
|
||||
"No remote primary is currently identifiable; preparing local PGDATA as Unpromoted and leaving promotion exclusively to Pacemaker"
|
||||
|
||||
if ! mc_pg_write_peer_recovery; then
|
||||
ocf_exit_reason \
|
||||
"Unable to place PGDATA into native standby recovery while no primary exists"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
if ! pg_mark_rejoin_pending; then
|
||||
ocf_exit_reason \
|
||||
"Unable to mark provisional standby as requiring rejoin if another node wins promotion"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
;;
|
||||
|
||||
2)
|
||||
ocf_exit_reason \
|
||||
"Multiple writable PostgreSQL peers detected; refusing start"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
|
||||
*)
|
||||
ocf_exit_reason \
|
||||
"Unable to determine PostgreSQL peer-primary state"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ocf_log info \
|
||||
"Starting PostgreSQL as an unpromoted standby inside ${CONTAINER_NAME}"
|
||||
|
||||
if ! pg_command '
|
||||
BINDIR="$(pg_config --bindir)" || exit 1
|
||||
exec "$BINDIR/pg_ctl" \
|
||||
-D "$PGDATA" \
|
||||
-l "$PGDATA/postgresql.log" \
|
||||
start -w
|
||||
'
|
||||
then
|
||||
ocf_exit_reason "pg_ctl start failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
managed_monitor
|
||||
monitor_rc=$?
|
||||
|
||||
case "${monitor_rc}" in
|
||||
"${OCF_SUCCESS}")
|
||||
ocf_log info \
|
||||
"PostgreSQL start completed in unpromoted state"
|
||||
return "${OCF_SUCCESS}"
|
||||
;;
|
||||
|
||||
"${OCF_RUNNING_PROMOTED}")
|
||||
ocf_exit_reason \
|
||||
"PostgreSQL became primary during start; stopping it to preserve OCF role semantics"
|
||||
|
||||
pg_command '
|
||||
BINDIR="$(pg_config --bindir)" || exit 1
|
||||
exec "$BINDIR/pg_ctl" \
|
||||
-D "$PGDATA" \
|
||||
stop -m fast -w
|
||||
' >/dev/null 2>&1 || true
|
||||
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
;;
|
||||
|
||||
*)
|
||||
# standby.signal can exist while the standby is still unusable
|
||||
# (for example, its required WAL was removed after a failed
|
||||
# primary repair). If a writable peer exists, route the instance
|
||||
# through managed_rejoin(), whose guarded basebackup fallback can
|
||||
# rebuild PGDATA when rewind alone cannot establish streaming.
|
||||
if pg_running; then
|
||||
local final_role
|
||||
final_role="$(pg_role 2>/dev/null || true)"
|
||||
if [ "${final_role}" = "t" ] && ! pg_streaming; then
|
||||
if mc_pg_rejoin_running_standby; then
|
||||
managed_monitor
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return "${monitor_rc}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
managed_stop()
|
||||
{
|
||||
local i
|
||||
|
||||
if container_running && pg_running; then
|
||||
ocf_log info \
|
||||
"Stopping PostgreSQL cleanly inside ${CONTAINER_NAME}"
|
||||
|
||||
if ! pg_command '
|
||||
BINDIR="$(pg_config --bindir)" || exit 1
|
||||
exec "$BINDIR/pg_ctl" \
|
||||
-D "$PGDATA" \
|
||||
stop -m fast -w
|
||||
'
|
||||
then
|
||||
ocf_exit_reason "pg_ctl stop failed"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if service_running; then
|
||||
ocf_log info \
|
||||
"Stopping container service ${SERVICE_NAME}"
|
||||
|
||||
if ! mc_lifecycle_stop "${SERVICE_NAME}"
|
||||
then
|
||||
ocf_exit_reason \
|
||||
"Failed to stop ${SERVICE_NAME}"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
for i in $(seq 1 20); do
|
||||
if ! service_running && ! container_running; then
|
||||
return "${OCF_SUCCESS}"
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if service_running; then
|
||||
ocf_exit_reason \
|
||||
"${SERVICE_NAME} remained active after stop"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
if container_running; then
|
||||
ocf_exit_reason \
|
||||
"${CONTAINER_NAME} remained running after stop"
|
||||
return "${OCF_ERR_GENERIC}"
|
||||
fi
|
||||
|
||||
return "${OCF_SUCCESS}"
|
||||
}
|
||||
|
||||
# Conservative scoring eligibility only; this does not replace role detection,
|
||||
# fencing, the promotion action, or the existing controlled-demote WAL checks.
|
||||
# This strict predicate remains unchanged; disconnected policy is separate below.
|
||||
mc_pg_promotion_eligible()
|
||||
{
|
||||
local eligible
|
||||
|
||||
# A provisional former primary must never become promotion-eligible.
|
||||
# It may run in recovery so Pacemaker can observe it, but it must first
|
||||
# complete a real rejoin/rewind against the elected primary.
|
||||
if pg_rejoin_pending; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
eligible="$(
|
||||
mc_runtime_exec_bounded 3s --user "${PG_OS_USER}" \
|
||||
--env 'PGOPTIONS=-c statement_timeout=2000' \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql -X -v ON_ERROR_STOP=1 -U "${DB_USER}" -d "${DB_NAME}" -Atqc "
|
||||
SELECT CASE WHEN pg_is_in_recovery() THEN
|
||||
NOT pg_is_wal_replay_paused()
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM pg_stat_wal_receiver
|
||||
WHERE status = 'streaming'
|
||||
AND flushed_lsn IS NOT NULL
|
||||
AND pg_last_wal_replay_lsn() >= flushed_lsn
|
||||
)
|
||||
ELSE false END;
|
||||
" 2>/dev/null
|
||||
)" || return 1
|
||||
[ "${eligible}" = t ]
|
||||
}
|
||||
|
||||
# One observation supplies both strict connected eligibility and disconnected
|
||||
# evidence. PostgreSQL restart time deliberately invalidates prior observations,
|
||||
# including a new basebackup sharing the same PostgreSQL system identifier.
|
||||
mc_pg_standby_observation()
|
||||
{
|
||||
mc_runtime_exec_bounded 3s --user "${PG_OS_USER}" \
|
||||
--env 'PGOPTIONS=-c statement_timeout=2000' \
|
||||
"${CONTAINER_NAME}" \
|
||||
psql -X -v ON_ERROR_STOP=1 -U "${DB_USER}" -d "${DB_NAME}" -Atqc "
|
||||
WITH s AS MATERIALIZED (
|
||||
SELECT pg_is_in_recovery() AS recovery,
|
||||
pg_last_wal_replay_lsn() AS replay,
|
||||
r.status AS receiver, r.flushed_lsn AS flush
|
||||
FROM (SELECT 1) AS singleton
|
||||
LEFT JOIN pg_stat_wal_receiver r ON true
|
||||
)
|
||||
SELECT json_build_object(
|
||||
'identity', json_build_array(
|
||||
(SELECT system_identifier::text FROM pg_control_system()),
|
||||
current_setting('data_directory'),
|
||||
extract(epoch FROM pg_postmaster_start_time())::text),
|
||||
'recovery', recovery,
|
||||
'paused', CASE WHEN recovery THEN pg_is_wal_replay_paused() ELSE true END,
|
||||
'receiver', receiver, 'replay', replay::text, 'flush', flush::text,
|
||||
'strict', CASE WHEN recovery THEN
|
||||
NOT pg_is_wal_replay_paused() AND receiver = 'streaming'
|
||||
AND flush IS NOT NULL AND replay >= flush
|
||||
ELSE false END)
|
||||
FROM s;
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
mc_pg_standby_score()
|
||||
{
|
||||
local observation
|
||||
|
||||
# Hard promotion veto for provisional former-primary PGDATA. Returning
|
||||
# -INFINITY here ensures Pacemaker cannot choose this instance while the
|
||||
# rejoin-required marker exists, even though monitor may report it as a
|
||||
# healthy Unpromoted standby.
|
||||
if pg_rejoin_pending; then
|
||||
if ! mc_pg_reconcile_rejoin_marker; then
|
||||
printf '%s\n' '-INFINITY'
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
observation="$(mc_pg_standby_observation)" || return 1
|
||||
# The helper is root-owned alongside the modules; state never enters the
|
||||
# rootless container. A failed/slow helper always withdraws eligibility.
|
||||
printf '%s' "${observation}" | timeout --kill-after=1s 1s python3 \
|
||||
"${MC_APPLICATION_DIR}/promotion_state.py" \
|
||||
"${MC_PROMOTION_RESOURCE_INSTANCE:-}" \
|
||||
"${SERVICE_USER}" "${SERVICE_NAME}" "${CONTAINER_NAME}" \
|
||||
"${OCF_RESKEY_disconnected_max_age:-300}"
|
||||
}
|
||||
Reference in New Issue
Block a user