# 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;" } 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) ocf_log debug \ "PostgreSQL healthy: role=standby (unpromoted)" 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. ;; *) 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. # # A PostgreSQL data directory without standby.signal would start # writable/primary, so never start it from this action. # if ! pg_command ' test -f "$PGDATA/standby.signal" ' then ocf_exit_reason \ "Refusing start: PGDATA is not configured as a standby (standby.signal missing)" return "${OCF_ERR_GENERIC}" 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}" ;; *) 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 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 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}" }