# Retry transaksi SERIALIZABLE: dua proses membaca keadaan yang sama,
# keduanya menulis, server menolak transaksi kedua dengan 40001,
# lalu transaksi tersebut diulang dari awal dengan data segar.
import psycopg
from psycopg import errors

MAKS_PERCOBAAN = 3

# Siapkan data: dua petugas sedang bertugas jaga.
setup = psycopg.connect()
setup.execute("""
    CREATE TABLE petugas_jaga (
        id    integer PRIMARY KEY,
        nama  text NOT NULL,
        jaga  boolean NOT NULL
    )
""")
setup.execute(
    "INSERT INTO petugas_jaga VALUES (1, 'Budi', TRUE), (2, 'Siti', TRUE)")
setup.commit()
setup.close()

# Aturan bisnis: petugas boleh berhenti jaga hanya bila masih ada
# petugas lain yang jaga -- minimal satu harus tetap bertugas.

a = psycopg.connect()
b = psycopg.connect()
a.isolation_level = psycopg.IsolationLevel.SERIALIZABLE
b.isolation_level = psycopg.IsolationLevel.SERIALIZABLE

# --- Percobaan pertama: dua transaksi berjalan tumpang tindih. ---
# A membaca dulu, B membaca sebelum A menulis, keduanya lalu menulis.
lain_a = a.execute(
    "SELECT count(*) FROM petugas_jaga WHERE jaga AND id <> 1"
).fetchone()[0]
lain_b = b.execute(
    "SELECT count(*) FROM petugas_jaga WHERE jaga AND id <> 2"
).fetchone()[0]
print(f"A melihat {lain_a} petugas lain jaga -> Budi berhenti")
print(f"B melihat {lain_b} petugas lain jaga -> Siti berhenti")

a.execute("UPDATE petugas_jaga SET jaga = FALSE WHERE id = 1")
a.commit()
print("A commit sukses: Budi berhenti jaga")

# SerializationFailure bisa muncul pada pernyataan maupun pada commit,
# jadi keduanya dibungkus satu try yang sama.
try:
    b.execute("UPDATE petugas_jaga SET jaga = FALSE WHERE id = 2")
    b.commit()
    print("B commit sukses (tidak diharapkan)")
except errors.SerializationFailure as e:
    print(f"B ditolak server: sqlstate {e.sqlstate}")
    b.rollback()

# --- Retry: ulangi transaksi B dari awal dengan pembacaan segar. ---
for percobaan in range(1, MAKS_PERCOBAAN + 1):
    print(f"percobaan ulang B ke-{percobaan}")
    try:
        lain = b.execute(
            "SELECT count(*) FROM petugas_jaga WHERE jaga AND id <> 2"
        ).fetchone()[0]
        if lain == 0:
            b.rollback()
            print("  Budi sudah berhenti -> Siti tetap jaga")
            break
        b.execute("UPDATE petugas_jaga SET jaga = FALSE WHERE id = 2")
        b.commit()
        print(f"  Siti berhenti jaga ({lain} petugas lain masih jaga)")
        break
    except errors.SerializationFailure:
        b.rollback()
        print("  bentrok lagi, ulangi transaksi")

# --- Keadaan akhir setelah retry. ---
with psycopg.connect() as cek:
    for id_petugas, nama, jaga in cek.execute(
            "SELECT id, nama, jaga FROM petugas_jaga ORDER BY id").fetchall():
        print(f"{id_petugas} {nama}: {'jaga' if jaga else 'berhenti'}")

a.close()
b.close()
