-
Notifications
You must be signed in to change notification settings - Fork 53
Resolve spock.progress's LSN column per endpoint #593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,17 @@ RETURNS int[] LANGUAGE sql IMMUTABLE AS $$ | |
| SELECT (spock.version_to_array(v))[1:2]; | ||
| $$; | ||
|
|
||
| -- spock.progress's apply-progress column is remote_lsn on Spock 5.x and | ||
| -- remote_commit_lsn on 6.x; resolve it per endpoint. The IN list keeps %I safe. | ||
| CREATE OR REPLACE FUNCTION spock.progress_commit_lsn_column(p_dsn text) | ||
| RETURNS text LANGUAGE sql AS $$ | ||
| SELECT * FROM dblink(p_dsn, | ||
| $q$SELECT attname::text FROM pg_attribute | ||
| WHERE attrelid = 'spock.progress'::regclass | ||
| AND attname IN ('remote_commit_lsn', 'remote_lsn') | ||
| ORDER BY attname LIMIT 1$q$) AS t(attname text); | ||
|
Comment on lines
+99
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file="samples/Z0DAN/zodan.sql"
printf '%s\n' '--- target function and callers ---'
sed -n '80,115p;1875,1935p;2490,2530p;2590,2630p' "$file"
printf '%s\n' '--- function definition and privilege statements ---'
rg -n -C 5 'CREATE OR REPLACE FUNCTION|CREATE FUNCTION|GRANT .*zodan|REVOKE .*zodan|find.*progress|progress.*column|dblink' "$file"
printf '%s\n' '--- relevant function call sites ---'
rg -n -C 8 'remote_commit_lsn|remote_lsn|%I|progress_sql' "$file"
printf '%s\n' '--- SQL function bodies that contain the helper ---'
python3 - <<'PY'
from pathlib import Path
p = Path("samples/Z0DAN/zodan.sql")
s = p.read_text()
for needle in ("remote_commit_lsn", "remote_lsn"):
print(f"\n--- {needle} ---")
start = 0
while True:
i = s.find(needle, start)
if i < 0:
break
line = s.count("\n", 0, i) + 1
print("line", line)
start = i + 1
PYRepository: pgEdge/spock Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
file="samples/Z0DAN/zodan.sql"
printf '%s\n' '--- helper and nearby privilege statements ---'
sed -n '90,110p' "$file"
rg -n -C 3 'GRANT|REVOKE|progress_commit_lsn_column' --glob '*.sql' .
printf '%s\n' '--- source progress exception scope ---'
sed -n '1848,1970p' "$file"
printf '%s\n' '--- first slot-advancement scope ---'
sed -n '2460,2555p' "$file"
printf '%s\n' '--- second slot-advancement scope ---'
sed -n '2570,2660p' "$file"
printf '%s\n' '--- declarations and callers ---'
rg -n -C 4 'CREATE OR REPLACE (FUNCTION|PROCEDURE) spock\.(add_node|progress_commit_lsn_column|.*slot.*advance)|progress_commit_lsn_column\(' "$file"
printf '%s\n' '--- local PostgreSQL tools, if available ---'
command -v psql || true
command -v postgres || true
command -v initdb || trueRepository: pgEdge/spock Length of output: 30057 🌐 Web query:
💡 Result: In PostgreSQL, the behavior of the format function and SQL-language functions regarding NULLs and empty sets is as follows: 1. PostgreSQL format Function and %I Specifier: The format function uses format specifiers to construct strings [1][2]. When the %I specifier is used, the argument is treated as an SQL identifier (equivalent to quote_ident) [1][3]. If a NULL value is passed to a %I specifier, it results in an error [1][4]. This is distinct from other specifiers: %s treats NULL as an empty string, and %L treats NULL as the literal string "NULL" [1][2]. 2. SQL-Language Function Return Behavior: When an SQL-language function is defined to return a scalar type (not a set), it executes the last query in its body and returns the first row of that result [5][6]. If the last query returns no rows, the function returns a NULL value [5][7]. It does not return "no rows" (an empty set) because the function signature mandates that a single value be returned [8]. To achieve an empty result set, the function must be declared to return SETOF [5][8] or TABLE(...) [8]. If a scalar function executes a query that yields no rows, it effectively produces a single result containing NULL, rather than producing an empty result set [5][8]. Citations:
Handle a missing progress column explicitly. If remote 🤖 Prompt for AI Agents |
||
| $$; | ||
|
|
||
| -- ============================================================================ | ||
| -- Procedure: check_spock_version_compatibility | ||
| -- Purpose: Verify all nodes have the same Spock version before adding a node | ||
|
|
@@ -1884,12 +1895,12 @@ BEGIN | |
| v_prev_statement_timeout text; | ||
| BEGIN | ||
| progress_sql := format( | ||
| 'SELECT p.remote_commit_lsn ' | ||
| 'SELECT p.%I ' | ||
| 'FROM spock.progress p ' | ||
| 'JOIN spock.node n ON n.node_id = p.remote_node_id ' | ||
| 'WHERE p.node_id = (SELECT node_id FROM spock.node_info()) ' | ||
| ' AND n.node_name = %L', | ||
| rec.node_name); | ||
| spock.progress_commit_lsn_column(src_dsn), rec.node_name); | ||
|
Comment on lines
+1898
to
+1903
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- helper references ---'
rg -n -C 8 'progress_commit_lsn_column|progress_sql|transient|retry|statement_timeout|dblink' samples/Z0DAN/zodan.sql
printf '%s\n' '--- target ranges ---'
sed -n '70,120p;1870,1945p;2485,2535p;2585,2635p' samples/Z0DAN/zodan.sql
printf '%s\n' '--- privilege declarations ---'
rg -n -C 6 'GRANT|REVOKE|CREATE FUNCTION.*progress_commit_lsn_column|progress_commit_lsn_column' samples/Z0DAN/zodan.sqlRepository: pgEdge/spock Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- helper definition ---'
sed -n '88,108p' samples/Z0DAN/zodan.sql
printf '%s\n' '--- complete wait block ---'
sed -n '1880,1950p' samples/Z0DAN/zodan.sql
printf '%s\n' '--- slot advancement block ---'
sed -n '2478,2532p' samples/Z0DAN/zodan.sql
printf '%s\n' '--- declarations and ACL-related statements ---'
rg -n -i -C 5 'CREATE (OR REPLACE )?(FUNCTION|PROCEDURE).*progress|GRANT|REVOKE|SECURITY DEFINER|ALTER FUNCTION' samples/Z0DAN/zodan.sql
printf '%s\n' '--- all helper call sites ---'
rg -n -C 2 'spock\.progress_commit_lsn_column' samples/Z0DAN/zodan.sqlRepository: pgEdge/spock Length of output: 10054 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
path = Path("samples/Z0DAN/zodan.sql")
lines = path.read_text().splitlines()
helper = next(i for i, line in enumerate(lines) if "spock.progress_commit_lsn_column(p_dsn)" in line)
call_lines = [i for i, line in enumerate(lines) if "spock.progress_commit_lsn_column(" in line and "p_dsn" not in line]
source_call = next(i for i in call_lines if i > helper)
timeout_line = next(i for i in range(source_call, len(lines)) if "set_config('statement_timeout', '5s', true)" in lines[i])
loop_line = next(i for i in range(source_call, len(lines)) if lines[i].strip() == "LOOP")
exception_line = next(i for i in range(timeout_line, len(lines)) if lines[i].strip() == "EXCEPTION")
wait_started_line = next(i for i in range(source_call - 30, source_call) if "wait_started" in lines[i])
print({
"helper_definition_line": helper + 1,
"source_helper_call_line": source_call + 1,
"wait_started_line": wait_started_line + 1,
"loop_line": loop_line + 1,
"timeout_line": timeout_line + 1,
"probe_exception_line": exception_line + 1,
"call_precedes_loop": source_call < loop_line,
"call_precedes_timeout": source_call < timeout_line,
"call_precedes_exception": source_call < exception_line,
"wait_started_precedes_call": wait_started_line < source_call,
})
PYRepository: pgEdge/spock Length of output: 237 🌐 Web query:
💡 Result: In PostgreSQL 17, the behavior of dblink regarding timeouts differs significantly depending on whether you are attempting to establish a connection or waiting for a query result [1][2]. Connection Establishment To set a timeout for the connection establishment phase (e.g., when the remote server is unreachable), you must use the connect_timeout parameter within the connection string itself [2]. Standard local session settings like statement_timeout do not apply to the initial connection handshake [2]. Example usage: SELECT dblink_connect('myconn', 'host=remote_host dbname=mydb connect_timeout=5'); In this example, the connection attempt will time out after 5 seconds if it cannot be established [2]. Remote Query Execution For query execution, dblink relies on the remote server's settings [3][4]. The local statement_timeout does not propagate to the remote server to control the execution time of the dblink query [4]. To enforce a timeout on the remote side, you can specify options in the connection string used by dblink [4]. Example usage: SELECT * FROM dblink('host=remote_host dbname=mydb options=''-c statement_timeout=1000''', 'SELECT long_running_query') AS t(result text); In this example, the remote query will be aborted if it exceeds 1000 milliseconds (1 second) [4]. Wait Events PostgreSQL tracks dblink-related waits using the Extension wait event type [1][5]. You can monitor these in pg_stat_activity: - DblinkConnect / DblinkGetConnect: Waiting to establish a connection to a remote server [1][5]. - DblinkGetResult: Waiting to receive query results from the remote server [1][5]. Exception Handling When a remote statement_timeout triggers, the remote side cancels the query, and the local dblink call will raise a PostgreSQL error [6]. If you are using dblink in a loop and wish to continue despite a timeout error, you must explicitly handle the QUERY_CANCELED exception within your PL/pgSQL block, as generic OTHERS exception handlers do not catch it [6]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
path = Path("samples/Z0DAN/zodan.sql")
lines = path.read_text().splitlines()
definition = next(i for i, line in enumerate(lines)
if "CREATE OR REPLACE FUNCTION spock.progress_commit_lsn_column" in line)
source_call = next(i for i, line in enumerate(lines)
if "spock.progress_commit_lsn_column(src_dsn)" in line)
wait_started = next(i for i in range(source_call - 40, source_call)
if "wait_started" in lines[i])
loop = next(i for i in range(source_call, len(lines))
if lines[i].strip() == "LOOP")
timeout = next(i for i in range(source_call, len(lines))
if "set_config('statement_timeout', '5s', true)" in lines[i])
exception = next(i for i in range(timeout, len(lines))
if lines[i].strip() == "EXCEPTION")
print(f"definition={definition + 1}")
print(f"wait_started={wait_started + 1}")
print(f"source_call={source_call + 1}")
print(f"loop={loop + 1}")
print(f"timeout={timeout + 1}")
print(f"exception={exception + 1}")
print(f"call_before_loop={source_call < loop}")
print(f"call_before_timeout={source_call < timeout}")
print(f"call_before_exception={source_call < exception}")
print(f"timer_starts_before_call={wait_started < source_call}")
PYRepository: pgEdge/spock Length of output: 341 Keep column discovery inside a bounded retry path.
🤖 Prompt for AI Agents |
||
|
|
||
| RAISE NOTICE ' - Waiting for source node % to apply % changes up to sync LSN %...', | ||
| src_node_name, rec.node_name, _catchup_lsn; | ||
|
|
@@ -1905,12 +1916,13 @@ BEGIN | |
|
|
||
| PERFORM set_config('statement_timeout', coalesce(v_prev_statement_timeout, '0'), true); | ||
| EXCEPTION | ||
| WHEN undefined_column OR undefined_table | ||
| OR undefined_function OR insufficient_privilege THEN | ||
| RAISE EXCEPTION 'Cannot read apply progress from source node % (not replication lag): % (SQLSTATE %)', | ||
| src_node_name, SQLERRM, SQLSTATE; | ||
| WHEN OTHERS THEN | ||
| -- Let user know if something wrong happens | ||
| IF verb THEN | ||
| RAISE NOTICE 'An error happened: %', SQLERRM; | ||
| END IF; | ||
| -- Transient probe failure; restore timeout and retry. | ||
| RAISE NOTICE 'An error happened: %', SQLERRM; | ||
| PERFORM set_config('statement_timeout', coalesce(v_prev_statement_timeout, '0'), true); | ||
| src_progress_lsn := NULL; | ||
| END; | ||
|
|
@@ -2497,10 +2509,12 @@ BEGIN | |
| -- Slot exists but is not active (unusual). Advance defensively. | ||
| RAISE NOTICE ' Slot % found at LSN % (inactive)', src_slot_name, current_lsn; | ||
|
|
||
| SELECT p.remote_commit_lsn INTO target_lsn | ||
| FROM spock.progress p | ||
| JOIN spock.node n ON n.node_id = p.remote_node_id | ||
| WHERE n.node_name = src_node_name; | ||
| EXECUTE format( | ||
| 'SELECT p.%I FROM spock.progress p ' | ||
| 'JOIN spock.node n ON n.node_id = p.remote_node_id ' | ||
| 'WHERE n.node_name = $1', | ||
| spock.progress_commit_lsn_column(new_node_dsn)) | ||
| INTO target_lsn USING src_node_name; | ||
|
|
||
| IF target_lsn IS NOT NULL AND target_lsn > current_lsn THEN | ||
| RAISE NOTICE ' Snapshot LSN for %: %', src_node_name, target_lsn; | ||
|
|
@@ -2593,10 +2607,12 @@ BEGIN | |
|
|
||
| -- Advance the slot to resume_lsn: the last commit from this node | ||
| -- that N1 had applied at snapshot time (stored in N3's spock.progress). | ||
| SELECT p.remote_commit_lsn INTO target_lsn | ||
| FROM spock.progress p | ||
| JOIN spock.node n ON n.node_id = p.remote_node_id | ||
| WHERE n.node_name = rec.node_name; | ||
| EXECUTE format( | ||
| 'SELECT p.%I FROM spock.progress p ' | ||
| 'JOIN spock.node n ON n.node_id = p.remote_node_id ' | ||
| 'WHERE n.node_name = $1', | ||
| spock.progress_commit_lsn_column(new_node_dsn)) | ||
| INTO target_lsn USING rec.node_name; | ||
|
|
||
| IF target_lsn IS NULL THEN | ||
| RAISE NOTICE ' WARNING: No spock.progress entry for %, falling back to pg_current_wal_lsn()', rec.node_name; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C 12 "progress_commit_lsn_column|REVOKE|GRANT.*EXECUTE|spock\.progress" samples/Z0DAN/zodan.sqlRepository: pgEdge/spock
Length of output: 8297
🏁 Script executed:
Repository: pgEdge/spock
Length of output: 50370
🏁 Script executed:
Repository: pgEdge/spock
Length of output: 23282
🌐 Web query:
site:postgresql.org/docs dblink(text, text) function privileges SQL function default PUBLIC EXECUTE💡 Result:
In PostgreSQL, the dblink(text, text) function is granted the EXECUTE privilege to PUBLIC by default upon installation [1][2]. This is consistent with the general PostgreSQL behavior where EXECUTE privilege for functions and procedures is granted to PUBLIC by default [1][2]. While standard dblink functions are typically available to all users, specific security-sensitive variants, such as dblink_connect_u, are explicitly installed with all privileges revoked from PUBLIC to prevent unauthorized escalation of privileges [3][4][5]. Users should exercise caution with functions that accept connection strings, as they can potentially expose credentials or allow unauthorized remote database access if not properly secured [3][4]. To restrict access to the dblink functions, administrators can use the REVOKE command to remove the EXECUTE privilege from PUBLIC.
Citations:
Revoke
PUBLICexecution onspock.progress_commit_lsn_column(text).The function accepts an arbitrary libpq connection string and calls
dblinkwith a fixed query. RevokeEXECUTEfromPUBLIC, then grant it only to the Z0DAN operator role.🤖 Prompt for AI Agents