Aplikasi nyata hampir selalu membaca jauh lebih banyak daripada menulis: satu transaksi kasir INSERT bisa berpasangan dengan puluhan SELECT — cek stok, tampilan katalog, laporan penjualan, dashboard. Primary yang juga menanggung semua beban baca itu adalah pemborosan kapasitas yang bisa dihindari: standby yang Anda bangun di chapter Streaming Replication sudah berisi salinan data yang hampir selalu segar, dan PostgreSQL bisa menjalankannya sebagai read replica — server yang melayani query baca penuh sambil terus me-replay WAL. Chapter ini memakai replica untuk pembacaan, membuktikan dua batas pentingnya (menolak tulisan, dan tidak selalu segar), lalu membahas cara aplikasi menutup celah konsistensi itu.

Standby Siap Melayani Baca Sejak Awal #

Kemampuan standby menerima koneksi dan menjalankan SELECT sambil recovery berjalan disebut hot standby, dan sejak PostgreSQL 10 status ini aktif secara bawaan lewat parameter hot_standby = on — tidak ada konfigurasi tambahan yang perlu Anda kerjakan. Kita bangun kembali pasangan demo (primary port 54361, replica port 54362, direktori /home/z/my-project/tmp/rp_read), lalu membaca parameter mode bacanya langsung dari replica:

#!/usr/bin/env bash
# Menyiapkan pasangan primary-read replica untuk demo chapter Read Replica.
# Primary di port 54361, standby (read replica) di port 54362.
# Skrip berikutnya (baca-saja.sh dan konsistensi-akhir.sh) melanjutkan
# pasangan server ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read
PRI=$BASE/pgdata_pusat     # primary, port 54361
STD=$BASE/pgdata_cabang    # read replica, port 54362
SOCK=$BASE/sock

# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$STD" -m immediate stop >/dev/null 2>&1 || true
rm -rf "$BASE"
mkdir -p "$SOCK"

# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() {  # port database sql nilai_yang_diharapkan
  local port="$1" db="$2" sql="$3" harapan="$4" i hasil
  for i in $(seq 1 150); do
    hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
            -c "$sql" 2>/dev/null) || true
    if [ "$hasil" = "$harapan" ]; then return 0; fi
    sleep 0.2
  done
  echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
  return 1
}

echo "== Langkah 1: initdb dan jalankan primary di port 54361 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54361 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54361 postgres "SELECT 1;" "1"
echo "primary siap"

echo "== Langkah 2: data awal di primary =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "CREATE TABLE pelanggan (id int PRIMARY KEY, nama text, kota text, poin int);" \
  -c "INSERT INTO pelanggan VALUES (1, 'Budi', 'Jakarta', 120), (2, 'Siti', 'Bandung', 85), (3, 'Ani', 'Surabaya', 200), (4, 'Rudi', 'Medan', 60);"

echo "== Langkah 3: pg_basebackup -R, lalu jalankan replica di port 54362 =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54361 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54362 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "4"
echo "replica menyala dan konsisten"

echo "== Langkah 4: parameter mode baca di replica =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SHOW hot_standby;" \
  -c "SHOW hot_standby_feedback;" \
  -c "SHOW max_standby_streaming_delay;"

echo "== Langkah 5: replica melayani pembacaan =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT id, nama, kota, poin FROM pelanggan ORDER BY poin DESC, id;"

Output yang Dihasilkan:

text
== Langkah 1: initdb dan jalankan primary di port 54361 ==
primary siap
== Langkah 2: data awal di primary ==
CREATE DATABASE
CREATE TABLE
INSERT 0 4
== Langkah 3: pg_basebackup -R, lalu jalankan replica di port 54362 ==
replica menyala dan konsisten
== Langkah 4: parameter mode baca di replica ==
 hot_standby 
-------------
 on
(1 row)

 hot_standby_feedback 
----------------------
 off
(1 row)

 max_standby_streaming_delay 
-----------------------------
 30s
(1 row)

== Langkah 5: replica melayani pembacaan ==
 id | nama |   kota   | poin 
----+------+----------+------
  3 | Ani  | Surabaya |  200
  1 | Budi | Jakarta  |  120
  2 | Siti | Bandung  |   85
  4 | Rudi | Medan    |   60
(4 rows)

Tiga parameter di Langkah 4 adalah papan kendali sebuah replica. hot_standby = on menegaskan mode baca aktif tanpa kita atur apa pun. hot_standby_feedback = off berarti replica tidak melaporkan query terpanjangnya ke primary — konsekuensinya dibahas di bagian konflik recovery di bawah. max_standby_streaming_delay = 30s adalah anggaran kesabaran replay: berapa lama replay boleh menunda diri demi query yang sedang berjalan sebelum akhirnya membatalkan query itu. Langkah 5 menutup dengan bukti sederhana: SELECT dengan ORDER BY berjalan di replica dan mengembalikan data yang sama dengan primary.

Replica Menolak Tulisan #

Batas pertama replica sudah Anda saksikan di chapter Streaming Replication : standby menjalankan recovery terus-menerus sehingga server menolak semua perintah tulisan. Skrip berikut mengulang pembuktian itu pada pasangan yang baru kita bangun — kelakuannya identik, dan pesan errornya layak dihafal karena sering muncul di log aplikasi yang salah alamat koneksi:

bash baca-saja.sh
#!/usr/bin/env bash
# Read replica hanya melayani SELECT: INSERT di sana harus ditolak server.
# Skrip ini sengaja berakhir dengan exit code 3 (galat SQL dari psql),
# memakai pasangan server dari siapkan-replica.sh (replica di port 54362).
exec psql -X -v ON_ERROR_STOP=1 -h 127.0.0.1 -p 54362 -U postgres -d toko <<'SQL'
INSERT INTO pelanggan VALUES (5, 'Wati', 'Semarang', 90);
SQL

Output yang Dihasilkan:

text
ERROR:  cannot execute INSERT in a read-only transaction

Pesan cannot execute INSERT in a read-only transaction adalah jawaban server, bukan kegagalan jaringan. Satu-satunya jalan replica menerima tulisan adalah berhenti menjadi replica — lewat promosi yang dibahas di chapter Konsep Failover . Untuk menulis sekarang, aplikasi harus ke primary; di sinilah kebutuhan routing muncul, yang kita bahas setelah urusan konsistensi.

Konsistensi Eventual dan Jebakan Read-Your-Own-Writes #

Batas kedua lebih halus dan lebih berbahaya: replica membaca keadaan tertinggal. Replikasi asinkron memberi Anda eventual consistency — perubahan dijamin sampai, tetapi tidak dijamin sudah sampai sekarang. Masalah klasiknya bernama read-your-own-writes: pengguna menyimpan profil, halaman tersimpan, lalu halaman berikutnya membaca dari replica — dan profilnya seolah hilang, padahal cuma belum sampai.

Di jaringan lokal jeda ini mikroskopis, sehingga sulit disaksikan. Untuk itu skrip berikut memakai recovery_min_apply_delay (parameter asli untuk delayed standby) supaya replica tertinggal 4 detik secara sengaja — cukup lama untuk melihat dunia dari mata sebuah replica yang tertinggal:

#!/usr/bin/env bash
# Konsistensi eventual dan masalah read-your-own-writes di read replica.
# Replica di-restart dengan recovery_min_apply_delay=4s agar jeda replay
# cukup panjang untuk disaksikan: baris yang baru ditulis di primary
# tidak langsung terbaca di replica, lalu muncul setelah jeda terlewati.
# Skrip ini melanjutkan pasangan server dari siapkan-replica.sh.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read
STD=$BASE/pgdata_cabang
SOCK=$BASE/sock

# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() {  # port database sql nilai_yang_diharapkan
  local port="$1" db="$2" sql="$3" harapan="$4" i hasil
  for i in $(seq 1 150); do
    hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
            -c "$sql" 2>/dev/null) || true
    if [ "$hasil" = "$harapan" ]; then return 0; fi
    sleep 0.2
  done
  echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
  return 1
}

echo "== Langkah 1: restart replica dengan jeda replay 4 detik =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54362 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang -c recovery_min_apply_delay=4s" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "4"
echo "replica kembali melayani baca"

echo "== Langkah 2: tulis satu pelanggan baru di primary =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "INSERT INTO pelanggan VALUES (5, 'Wati', 'Semarang', 90);"
echo "lomba pencarian pelanggan Wati:"

echo "== Langkah 3: baca SEGERA dari replica — Wati belum ada =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT count(*) AS baris_replica, count(*) FILTER (WHERE nama = 'Wati') AS ketemu_wati FROM pelanggan;"

echo "== Langkah 4: baca yang sama dari primary — Wati sudah ada =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "SELECT count(*) AS baris_primary, count(*) FILTER (WHERE nama = 'Wati') AS ketemu_wati FROM pelanggan;"

echo "== Langkah 5: setelah jeda 4 detik terlewati, Wati tiba di replica =="
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "5"
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT id, nama, kota, poin FROM pelanggan ORDER BY poin DESC, id;"

Output yang Dihasilkan:

text
== Langkah 1: restart replica dengan jeda replay 4 detik ==
replica kembali melayani baca
== Langkah 2: tulis satu pelanggan baru di primary ==
INSERT 0 1
lomba pencarian pelanggan Wati:
== Langkah 3: baca SEGERA dari replica — Wati belum ada ==
 baris_replica | ketemu_wati 
---------------+-------------
             4 |           0
(1 row)

== Langkah 4: baca yang sama dari primary — Wati sudah ada ==
 baris_primary | ketemu_wati 
---------------+-------------
             5 |           1
(1 row)

== Langkah 5: setelah jeda 4 detik terlewati, Wati tiba di replica ==
 id | nama |   kota   | poin 
----+------+----------+------
  3 | Ani  | Surabaya |  200
  1 | Budi | Jakarta  |  120
  5 | Wati | Semarang |   90
  2 | Siti | Bandung  |   85
  4 | Rudi | Medan    |   60
(5 rows)

Output itu adalah potret masalah yang sesungguhnya. Query di Langkah 3 dan Langkah 4 adalah query yang sama, dijalankan hampir bersamaan, terhadap data yang sama — hasilnya berbeda: replica menjawab ketemu_wati = 0, primary menjawab ketemu_wati = 1. Pengguna yang menyimpan data lalu membaca dari replica akan merasa datanya hilang. Empat detik kemudian (Langkah 5) Wati muncul — konsistensi eventual ditepati, tetapi “eventual” tidak menolong pengguna yang butuh jawaban sekarang.

Cara menutup celah ini adalah keputusan routing, dan pilihannya bisa dipetakan rapi:

StrategiCara kerjaCocok untuk
Sticky session baca-setelah-tulisKlien yang baru menulis sementara diarahkan ke primary (mis. 5-30 detik, atau sampai pg_wal_lsn_diff kecil)Aplikasi web umum dengan profil “simpan lalu tampilkan”
Rutekan baca kritikal ke primaryQuery yang butuh data terbaru (cek saldo, validasi stok) selalu ke primary, sisanya ke replicaArus campuran: mayoritas baca toleran jeda, minoritas sensitif
Ukur lalu putuskanAplikasi menunggu sampai posisi tulisannya ter-replay (membandingkan LSN yang dikembalikan pg_current_wal_lsn() dengan pg_last_wal_replay_lsn() di replica)Kebutuhan konsistensi kuat tanpa mengorbankan scale-out baca

Ketiga strategi di atas adalah routing di sisi aplikasi — paling fleksibel karena hanya aplikasi yang tahu query mana yang “kritikal”. Untuk routing di luar aplikasi, PgBouncer dapat memisahkan pool koneksi baca dan tulis (lihat chapter Connection Pooling ), sedangkan Pgpool-II menambah load balancing query tingkat lanjut beserta otomatisasi failover. Ingat pula bahwa routing berbasis LSN hanya menutup celah waktu — ia tidak mengubah kenyataan bahwa replica adalah salinan yang tertinggal, ia hanya membuat Anda tahu kapan salinan itu sudah mengejar.

Konflik Recovery: Saat Query Replica Bertabrakan dengan Replay #

Replica menjalankan dua pekerjaan sekaligus: melayani query pembaca dan me-replay WAL baru. Keduanya bisa bertabrakan, karena replaying WAL kadang butuh membersihkan baris mati yang sedang dibaca query berjalan. PostgreSQL menyebut kejadian ini recovery conflict, dan bentuknya ada beberapa:

Jenis konflikPemicuPerlakuan bawaan
Snapshot too oldQuery memegang snapshot lama; VACUUM di primary sudah membersihkan baris yang dibutuhannyaQuery dibatalkan dengan pesan canceling statement due to conflict with recovery
Buffer pinQuery memegang buffer yang perlu dibersihkan replayReplay menunggu hingga max_standby_streaming_delay
LockQuery memegang lock (mis. ACCESS SHARE) yang berlawanan dengan replay DDLReplay menunggu hingga max_standby_streaming_delay

Di sinilah hot_standby_feedback masuk. Dengan feedback = on, replica melaporkan snapshot terlama dari query yang sedang berjalan ke primary, dan primary menahan VACUUM atas baris yang masih mungkin dibaca replica — konflik “snapshot too old” nyaris hilang, query panjang di replica aman. Harganya dibayar di primary: dead tuple hidup lebih lama, tabel mengembang (bloat), dan seluruh cluster menanggung biaya itu selama ada satu replica dengan query panjang. Dengan feedback = off (bawaan), arahnya terbalik: primary bebas membersihkan, dan query replica yang kalah dibatalkan setelah anggaran max_standby_streaming_delay habis.

Kejujuran tentang Skala Baca #

Read replica menyelesaikan masalah yang benar-benar dimilikinya: kapasitas baca. Menambah replica ke lima buah berarti lima kali kapasitas serving SELECT (dengan catatan routing yang benar). Tetapi kapasitas tulis tidak ikut bertambah satu byte: semua tulisan tetap melalui satu primary, satu arsitektur WAL, satu antrean commit. Menambah replica justru menambah beban primary sedikit — setiap replica adalah satu walsender yang harus dikirimi WAL. Bila bottleneck Anda adalah UPDATE massal atau index write yang berat, jawabannya bukan replica melainkan pemisahan beban lain (partitioning, arsip ke gudang data, atau logical replication selektif seperti chapter sebelumnya). Replica adalah alat skala baca; tulisan tetap satu — mengingat hal ini menyelamatkan banyak arsitektur dari harapan yang salah tempat.

Mematikan Demo #

Skrip penutup menghentikan kedua server demo dan menghapus direktori datanya:

bash bereskan-demo.sh
#!/usr/bin/env bash
# Menghentikan kedua server demo read replica dan menghapus
# direktori datanya. Server utama course (socket 54329) tidak disentuh.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read

"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "replica berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

Output yang Dihasilkan:

text
replica berhenti
primary berhenti
direktori data demo dihapus

Replica yang melayani baca dan menolak tulisan membuat satu pertanyaan makin menekan: apa yang terjadi ketika primary — satu-satunya tempat menulis — benar-benar mati? Proses mengangkat standby menjadi primary baru punya nama, risiko, dan disiplin tersendiri: itu topik chapter Konsep Failover .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

baca-saja.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Read replica hanya melayani SELECT: INSERT di sana harus ditolak server.
# Skrip ini sengaja berakhir dengan exit code 3 (galat SQL dari psql),
# memakai pasangan server dari siapkan-replica.sh (replica di port 54362).
exec psql -X -v ON_ERROR_STOP=1 -h 127.0.0.1 -p 54362 -U postgres -d toko <<'SQL'
INSERT INTO pelanggan VALUES (5, 'Wati', 'Semarang', 90);
SQL

bereskan-demo.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Menghentikan kedua server demo read replica dan menghapus
# direktori datanya. Server utama course (socket 54329) tidak disentuh.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read

"$PGBIN/pg_ctl" -D "$BASE/pgdata_cabang" -m fast stop >/dev/null 2>&1 || true
echo "replica berhenti"
"$PGBIN/pg_ctl" -D "$BASE/pgdata_pusat" -m fast stop >/dev/null 2>&1 || true
echo "primary berhenti"
rm -rf "$BASE"
echo "direktori data demo dihapus"

konsistensi-akhir.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Konsistensi eventual dan masalah read-your-own-writes di read replica.
# Replica di-restart dengan recovery_min_apply_delay=4s agar jeda replay
# cukup panjang untuk disaksikan: baris yang baru ditulis di primary
# tidak langsung terbaca di replica, lalu muncul setelah jeda terlewati.
# Skrip ini melanjutkan pasangan server dari siapkan-replica.sh.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read
STD=$BASE/pgdata_cabang
SOCK=$BASE/sock

# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() {  # port database sql nilai_yang_diharapkan
  local port="$1" db="$2" sql="$3" harapan="$4" i hasil
  for i in $(seq 1 150); do
    hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
            -c "$sql" 2>/dev/null) || true
    if [ "$hasil" = "$harapan" ]; then return 0; fi
    sleep 0.2
  done
  echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
  return 1
}

echo "== Langkah 1: restart replica dengan jeda replay 4 detik =="
"$PGBIN/pg_ctl" -D "$STD" -m fast stop >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54362 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang -c recovery_min_apply_delay=4s" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "4"
echo "replica kembali melayani baca"

echo "== Langkah 2: tulis satu pelanggan baru di primary =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "INSERT INTO pelanggan VALUES (5, 'Wati', 'Semarang', 90);"
echo "lomba pencarian pelanggan Wati:"

echo "== Langkah 3: baca SEGERA dari replica — Wati belum ada =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT count(*) AS baris_replica, count(*) FILTER (WHERE nama = 'Wati') AS ketemu_wati FROM pelanggan;"

echo "== Langkah 4: baca yang sama dari primary — Wati sudah ada =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "SELECT count(*) AS baris_primary, count(*) FILTER (WHERE nama = 'Wati') AS ketemu_wati FROM pelanggan;"

echo "== Langkah 5: setelah jeda 4 detik terlewati, Wati tiba di replica =="
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "5"
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT id, nama, kota, poin FROM pelanggan ORDER BY poin DESC, id;"

siapkan-replica.sh

Unduh berkas
bash
#!/usr/bin/env bash
# Menyiapkan pasangan primary-read replica untuk demo chapter Read Replica.
# Primary di port 54361, standby (read replica) di port 54362.
# Skrip berikutnya (baca-saja.sh dan konsistensi-akhir.sh) melanjutkan
# pasangan server ini.
set -eu

PGBIN=/home/z/my-project/tools/dl/pgdeb/usr/lib/postgresql/17/bin
BASE=/home/z/my-project/tmp/rp_read
PRI=$BASE/pgdata_pusat     # primary, port 54361
STD=$BASE/pgdata_cabang    # read replica, port 54362
SOCK=$BASE/sock

# Buang sisa percobaan sebelumnya bila ada.
"$PGBIN/pg_ctl" -D "$PRI" -m immediate stop >/dev/null 2>&1 || true
"$PGBIN/pg_ctl" -D "$STD" -m immediate stop >/dev/null 2>&1 || true
rm -rf "$BASE"
mkdir -p "$SOCK"

# Polling: tunggu sampai sebuah query mengembalikan nilai yang diharapkan.
tunggu() {  # port database sql nilai_yang_diharapkan
  local port="$1" db="$2" sql="$3" harapan="$4" i hasil
  for i in $(seq 1 150); do
    hasil=$(psql -X -q -t -A -h 127.0.0.1 -p "$port" -U postgres -d "$db" \
            -c "$sql" 2>/dev/null) || true
    if [ "$hasil" = "$harapan" ]; then return 0; fi
    sleep 0.2
  done
  echo "TIMEOUT: menunggu '$sql' menjadi $harapan (port $port)" >&2
  return 1
}

echo "== Langkah 1: initdb dan jalankan primary di port 54361 =="
"$PGBIN/initdb" -D "$PRI" --no-locale -E UTF8 -U postgres >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$PRI" \
  -o "-p 54361 -k $SOCK -c listen_addresses=127.0.0.1" \
  -l "$BASE/primary.log" start >/dev/null 2>&1
tunggu 54361 postgres "SELECT 1;" "1"
echo "primary siap"

echo "== Langkah 2: data awal di primary =="
psql -X -h 127.0.0.1 -p 54361 -U postgres -d postgres \
  -c "CREATE DATABASE toko;"
psql -X -h 127.0.0.1 -p 54361 -U postgres -d toko \
  -c "CREATE TABLE pelanggan (id int PRIMARY KEY, nama text, kota text, poin int);" \
  -c "INSERT INTO pelanggan VALUES (1, 'Budi', 'Jakarta', 120), (2, 'Siti', 'Bandung', 85), (3, 'Ani', 'Surabaya', 200), (4, 'Rudi', 'Medan', 60);"

echo "== Langkah 3: pg_basebackup -R, lalu jalankan replica di port 54362 =="
PGDATABASE=postgres pg_basebackup -h 127.0.0.1 -p 54361 -U postgres \
  -D "$STD" -R -X stream -c fast >/dev/null 2>&1
"$PGBIN/pg_ctl" -D "$STD" \
  -o "-p 54362 -k $SOCK -c listen_addresses=127.0.0.1 -c cluster_name=standby_cabang" \
  -l "$BASE/standby.log" start >/dev/null 2>&1
tunggu 54362 toko "SELECT count(*) FROM pelanggan;" "4"
echo "replica menyala dan konsisten"

echo "== Langkah 4: parameter mode baca di replica =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SHOW hot_standby;" \
  -c "SHOW hot_standby_feedback;" \
  -c "SHOW max_standby_streaming_delay;"

echo "== Langkah 5: replica melayani pembacaan =="
psql -X -h 127.0.0.1 -p 54362 -U postgres -d toko \
  -c "SELECT id, nama, kota, poin FROM pelanggan ORDER BY poin DESC, id;"