diff --git a/test/advisory_lock_class.sh b/test/advisory_lock_class.sh index b619c96f5..9ff5f49de 100755 --- a/test/advisory_lock_class.sh +++ b/test/advisory_lock_class.sh @@ -204,4 +204,40 @@ check "a duplicate key is still rejected" \ "$(grep -qiE 'duplicate key|unique constraint' <<<"$dup" && echo rejected || echo "NOT rejected: $(head -1 <<<"$dup")")" \ "rejected" +# ---- the row-identity lock (class 104) and its two settings (#1341) ---------------- +# +# An UPDATE or DELETE serialises on the row it changes through an advisory lock in +# class 104 (PGCOLUMNAR_LOCKCLASS_ROW_IDENTITY). pgcolumnar.enable_row_update_lock +# turns that off for a session; pgcolumnar.row_lock_buckets is part of the lock tag, +# so it is fixed at server start. Neither was set or read by any test. +psql_run "CREATE TABLE rl (id int, v int) USING pgcolumnar;" +psql_run "INSERT INTO rl SELECT g, g FROM generate_series(1, 100) g;" +# row_locks_while_updating SETTING -> "|", read +# in one query once the holder's UPDATE is running. WAITED FOR, NOT SLEPT ON (#1368 +# review): read after a fixed sleep, a holder that had not reached its UPDATE yet made +# the off arm read 0 on a build that ignores the setting, because nothing was locked. +# The second field proves the UPDATE was running when the first was read. +row_locks_while_updating() { + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -Atq \ + -c "BEGIN; SET LOCAL pgcolumnar.enable_row_update_lock = $1; UPDATE rl SET v = v + 1 WHERE id = 7; SELECT pg_sleep(3); COMMIT;" \ + >/dev/null 2>&1 & + local holder=$! got="" i + for i in $(seq 1 100); do + got="$(q "SELECT count(*) FILTER (WHERE locktype = 'advisory' AND objsubid = 104) || '|' || + count(*) FILTER (WHERE locktype = 'relation' AND relation = 'rl'::regclass AND mode = 'RowExclusiveLock') + FROM pg_locks WHERE pid <> pg_backend_pid()")" + [ "${got#*|}" = 1 ] && break + sleep 0.1 + done + echo "$got" + wait "$holder" +} +_rl_on="$(row_locks_while_updating on)" +check "an UPDATE holds a row-identity lock (class 104) while enable_row_update_lock is on" \ + "$([ "${_rl_on%|*}" -ge 1 ] 2>/dev/null && echo "held|${_rl_on#*|}" || echo "none|${_rl_on#*|}")" "held|1" +check "and holds none with enable_row_update_lock off" "$(row_locks_while_updating off)" "0|1" +check "row_lock_buckets is fixed at server start, so a session cannot change it (55P02)" \ + "$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At -v VERBOSITY=sqlstate -c "SET pgcolumnar.row_lock_buckets = 7;" 2>&1 | sed -n 's/^.*ERROR:[[:space:]]*\([0-9A-Z]\{5\}\).*$/\1/p')|$(q "SELECT context || '|' || setting FROM pg_settings WHERE name = 'pgcolumnar.row_lock_buckets'")" \ + "55P02|postmaster|1024" + pgc_summary diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 54025d17e..1008be473 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -5379,6 +5379,7 @@ regardless. | `test_the_lock_an_insert_takes_is_not_in_a_sql_reachable_class` | the lock is enabled, the insert's locks are visible, none is SQL-reachable, and the discovering session leaves nothing held | | `test_a_user_advisory_lock_on_that_tag_does_not_block_an_insert` | a user session holding every addressable tag in class 2 does not block a columnar insert | | `test_a_duplicate_key_is_still_rejected` | the lock still does its job, so removing the collision by removing the lock does not pass | +| `test_the_row_identity_lock_and_its_settings` | an UPDATE holds a class-104 row-identity lock with `enable_row_update_lock` on and none with it off; `row_lock_buckets` refuses `SET` (55P02) because it is fixed at server start (#1341) | ## 69. test_index_am_support.py: the index methods the page claims must work `docs/features.md` said "`CREATE INDEX` builds btree and hash indexes over a diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 88fe8cb8d..f5fe02222 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -633,4 +633,7 @@ guard_tests 429 # test_ttl_expire.py, by collection over the cluster files on this tree. # cluster_tests 534 -> 538 for #1341's test_import_export_privilege.py, the twin of # import_export_privilege.sh: four tests, by collection over the cluster files on this tree. -cluster_tests 538 +# +# cluster_tests 538 -> 539 for #1341's test_the_row_identity_lock_and_its_settings in +# test_advisory_lock_class.py, by collection over the cluster files on this tree. +cluster_tests 539 diff --git a/test/pytest/test_advisory_lock_class.py b/test/pytest/test_advisory_lock_class.py index 3ed5b2f14..ddbe11dd0 100644 --- a/test/pytest/test_advisory_lock_class.py +++ b/test/pytest/test_advisory_lock_class.py @@ -239,3 +239,44 @@ def test_a_duplicate_key_is_still_rejected(pgc_conn, expect): got = ("rejected" if "duplicate key" in msg or "unique constraint" in msg else f"NOT rejected: {str(exc).splitlines()[0]}") expect.text(got, "rejected", "a duplicate key is still rejected") + + +def test_the_row_identity_lock_and_its_settings(pgc_cluster, pgc_conn, expect): + """An UPDATE serialises on its row through an advisory lock in class 104; the + session setting enable_row_update_lock turns that off, and row_lock_buckets is + part of the lock tag, so it is fixed at server start (#1341).""" + with pgc_conn.cursor() as cur: + cur.execute("CREATE TABLE rl (id int, v int) USING pgcolumnar") + cur.execute("INSERT INTO rl SELECT g, g FROM generate_series(1, 100) g") + cur.execute("SELECT current_schema()") + schema = cur.fetchone()[0] + + def row_locks_while_updating(setting): + holder = psycopg.connect(pgc_cluster.dsn(), autocommit=False) + try: + with holder.cursor() as hc: + hc.execute(f'SET search_path TO "{schema}"') + hc.execute(f"SET LOCAL pgcolumnar.enable_row_update_lock = {setting}") + hc.execute("UPDATE rl SET v = v + 1 WHERE id = 7") + with pgc_conn.cursor() as cur: + cur.execute("SELECT count(*) FROM pg_locks WHERE locktype = 'advisory'" + " AND objsubid = 104 AND pid <> pg_backend_pid()") + return cur.fetchone()[0] + finally: + holder.rollback() + holder.close() + + on = row_locks_while_updating("on") + expect.text("held" if on >= 1 else "none", "held", + "an UPDATE holds a row-identity lock (class 104) while enable_row_update_lock is on") + expect.num(row_locks_while_updating("off"), 0, "and holds none with enable_row_update_lock off") + with pgc_conn.cursor() as cur: + try: + cur.execute("SET pgcolumnar.row_lock_buckets = 7") + refused = "ok" + except psycopg.Error as e: + refused = e.sqlstate + cur.execute("SELECT context || '|' || setting FROM pg_settings WHERE name = 'pgcolumnar.row_lock_buckets'") + ctx = cur.fetchone()[0] + expect.text(f"{refused}|{ctx}", "55P02|postmaster|1024", + "row_lock_buckets is fixed at server start, so a session cannot change it (55P02)")