Skip to content

Commit db380dd

Browse files
authored
Fix SUB_DISABLE reconnect retransmission
1 parent 5ed173c commit db380dd

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

src/spock_apply.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ static void spock_apply_worker_attach(void);
264264
static void spock_apply_worker_detach(void);
265265

266266
static bool should_log_exception(bool failed);
267+
static void clear_transient_exception_state_on_connection_loss(void);
267268

268269
static ApplyReplayEntry * apply_replay_entry_create(int r, char *buf);
269270
static void apply_replay_entry_free(ApplyReplayEntry * entry);
@@ -361,6 +362,47 @@ should_log_exception(bool failed)
361362
return false;
362363
}
363364

365+
/*
366+
* Forget the in-flight transaction marker after a transport failure on the
367+
* normal apply path.
368+
*
369+
* handle_begin() records commit_lsn in shared memory before any changes are
370+
* applied. That marker normally lets a restarted worker recognize a
371+
* transaction which failed during apply and replay it under the configured
372+
* exception policy. A provider disconnect is different: PostgreSQL aborts
373+
* the local transaction and the replication origin is deliberately left at
374+
* the last durable commit so the provider can send the transaction again.
375+
*
376+
* Leaving commit_lsn behind makes the replacement worker misclassify that
377+
* retransmitted transaction as an apply failure. In SUB_DISABLE mode it then
378+
* performs a read-only exception replay and disables the subscription instead
379+
* of applying the transaction. Clear the marker only for a first-pass
380+
* transport failure. Genuine apply failures and failures during exception
381+
* replay retain their state.
382+
*/
383+
static void
384+
clear_transient_exception_state_on_connection_loss(void)
385+
{
386+
SpockExceptionLog *exception_log;
387+
388+
if (!in_remote_transaction ||
389+
MyApplyWorker == NULL ||
390+
MyApplyWorker->use_try_block ||
391+
xact_had_exception ||
392+
exception_log_ptr == NULL ||
393+
my_exception_log_index < 0)
394+
return;
395+
396+
exception_log = &exception_log_ptr[my_exception_log_index];
397+
exception_log->commit_lsn = InvalidXLogRecPtr;
398+
exception_log->local_tuple = NULL;
399+
exception_log->initial_error_message[0] = '\0';
400+
exception_log->failed_action = 0;
401+
402+
elog(DEBUG1, "SPOCK %s: cleared transient exception state after provider connection loss",
403+
MySubscription->name);
404+
}
405+
364406
/*
365407
* Check if given relation is in process of being synchronized.
366408
*
@@ -3692,6 +3734,7 @@ apply_work(PGconn *streamConn)
36923734
edata->sqlerrcode == ERRCODE_ADMIN_SHUTDOWN ||
36933735
(applyconn != NULL && PQstatus(applyconn) == CONNECTION_BAD))
36943736
{
3737+
clear_transient_exception_state_on_connection_loss();
36953738
elog(LOG, "SPOCK %s: connection error during apply, exiting via rethrow: %s",
36963739
MySubscription->name, edata->message);
36973740
PG_RE_THROW();

tests/tap/schedule

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ test: 044_apply_change_logging
5757

5858
# Regression tests
5959
test: 103_manager_worker_dboid_race
60+
test: 105_sub_disable_retransmit_after_disconnect
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
use lib '.';
5+
use SpockTest qw(
6+
create_cluster destroy_cluster
7+
get_test_config scalar_query psql_or_bail
8+
wait_for_sub_status
9+
);
10+
11+
# A connection-class error after BEGIN aborts the local transaction and makes
12+
# the provider retransmit it. The exception marker recorded by handle_begin()
13+
# must not make the replacement worker treat that valid retransmission as
14+
# exception replay under SUB_DISABLE.
15+
16+
create_cluster(2, 'Create 2-node reconnect retransmission test cluster');
17+
18+
my $config = get_test_config();
19+
my $p1 = $config->{node_ports}->[0];
20+
my $p2 = $config->{node_ports}->[1];
21+
my $conn = "host=$config->{host} dbname=$config->{db_name} port=$p1 " .
22+
"user=$config->{db_user} password=$config->{db_password}";
23+
my $subscriber_log = "$config->{log_dir}/00${p2}.log";
24+
25+
psql_or_bail(2, "ALTER SYSTEM SET spock.exception_behaviour = sub_disable");
26+
psql_or_bail(2, "SELECT pg_reload_conf()");
27+
28+
psql_or_bail(1, "CREATE TABLE reconnect_retransmit (id int PRIMARY KEY, val text)");
29+
psql_or_bail(2, "CREATE TABLE reconnect_retransmit (id int PRIMARY KEY, val text)");
30+
31+
# Sequence increments are non-transactional, so SQLSTATE 08006 is raised only
32+
# on the first apply attempt. ENABLE REPLICA restricts the trigger to apply.
33+
psql_or_bail(2, q{
34+
CREATE SEQUENCE reconnect_fail_once;
35+
CREATE FUNCTION reconnect_fail_once() RETURNS trigger LANGUAGE plpgsql AS $$
36+
BEGIN
37+
IF nextval('reconnect_fail_once') = 1 THEN
38+
RAISE EXCEPTION 'injected provider connection loss'
39+
USING ERRCODE = '08006';
40+
END IF;
41+
RETURN NEW;
42+
END $$;
43+
CREATE TRIGGER reconnect_fail_once_trg
44+
BEFORE INSERT ON reconnect_retransmit
45+
FOR EACH ROW EXECUTE FUNCTION reconnect_fail_once();
46+
ALTER TABLE reconnect_retransmit
47+
ENABLE REPLICA TRIGGER reconnect_fail_once_trg;
48+
});
49+
50+
psql_or_bail(2,
51+
"SELECT spock.sub_create('sub_n1_n2', '$conn', " .
52+
"ARRAY['default', 'default_insert_only', 'ddl_sql'], false, false)");
53+
ok(wait_for_sub_status(2, 'sub_n1_n2', 'replicating', 30),
54+
'subscription starts in replicating state');
55+
56+
my $log_offset = -s $subscriber_log // 0;
57+
psql_or_bail(1, "INSERT INTO reconnect_retransmit VALUES (1, 'must survive reconnect')");
58+
59+
my $applied = 0;
60+
for (1 .. 60) {
61+
$applied = scalar_query(2,
62+
"SELECT count(*) FROM reconnect_retransmit WHERE id = 1");
63+
last if defined $applied && $applied eq '1';
64+
sleep(1);
65+
}
66+
is($applied, '1', 'retransmitted transaction is applied after worker restart');
67+
68+
is(scalar_query(2,
69+
"SELECT sub_enabled FROM spock.subscription WHERE sub_name = 'sub_n1_n2'"),
70+
't', 'SUB_DISABLE subscription remains enabled');
71+
ok(wait_for_sub_status(2, 'sub_n1_n2', 'replicating', 30),
72+
'subscription returns to replicating state');
73+
is(scalar_query(2, "SELECT last_value FROM reconnect_fail_once"),
74+
'2', 'replica trigger proves the transaction was attempted twice');
75+
76+
my $new_log = '';
77+
if (open(my $lf, '<', $subscriber_log)) {
78+
seek($lf, $log_offset, 0);
79+
local $/;
80+
$new_log = <$lf> // '';
81+
close($lf);
82+
}
83+
84+
like($new_log,
85+
qr/cleared transient exception state after provider connection loss/,
86+
'connection-loss path clears transient exception state');
87+
unlike($new_log,
88+
qr/Transaction failed, subscription will be disabled/,
89+
'retransmission does not enter SUB_DISABLE exception replay');
90+
91+
destroy_cluster('Destroy reconnect retransmission test cluster');
92+
done_testing();

0 commit comments

Comments
 (0)