Skip to content

Commit b83d5ca

Browse files
markhannumclaude
andcommitted
sqllogfill: auto-disable when master cannot service log requests
When all of the master's sql engines are busy and its dispatch queue is full, the master rejects the logfill's comdb2_transaction_logs() request. Previously the logfill thread would just disconnect, sleep, and hammer the already-saturated master again indefinitely -- and because send_rep_all_req/ send_rep_log_req are suppressed while sql_logfill is active, the gap could not close via normal replication either. Mirror the existing auth-failure autodisable precedent: count consecutive failed log requests and set gbl_sql_logfill_auto_disabled once they reach gbl_sql_logfill_request_fail_autodisable_threshold (default 5; a value <= 0 disables the behavior). Once disabled, sql_logfill_active() returns false and the db falls back to the traditional fill-request path. The master's reject is delivered as CDB2ERR_REJECTED, but that code is retryable: the cdb2 client retries it internally and -- for our CDB2_DIRECT_CPU handle with min_retries==1 -- surfaces it as CDB2ERR_CONNECT_ERROR (or CDB2ERR_TRAN_IO_ERROR), not CDB2ERR_REJECTED, so we key off the codes actually returned. Those same codes are also what an unreachable master produces: cdb2_open() does not connect for a DIRECT_CPU handle (and "set transaction blocksql" is a client-side set), so the first real round-trip -- the "select 1" ping in connect_to_master -- is where an unreachable master surfaces, and it is a counting site. We deliberately do not distinguish "reachable but saturated" from "unreachable": both mean logfill cannot get logs from the master, and falling back to normal replication is the right response either way. Spurious trips are avoided not by inspecting the error but by (1) returning early WITHOUT counting when there is no elected master (thedb->master unset or ".invalid"), which covers ordinary elections and outages, and (2) resetting the counters the moment we reach the master and make progress (a successful fetch, or confirming there is no gap), so only a sustained run of failures against a still-elected master reaches the threshold. Auto-disable now parks the logfill threads instead of terminating them: clearing gbl_sql_logfill_auto_disabled (no longer READONLY) resumes them (the resuming thread logs a message), and parking resets the counters so a resume starts with a fresh threshold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Mark Hannum <mhannum@bloomberg.net>
1 parent 6765b49 commit b83d5ca

4 files changed

Lines changed: 103 additions & 5 deletions

File tree

db/db_tunables.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ extern int gbl_sql_logfill_dedicated_apply_thread;
249249
extern int gbl_sql_logfill_lookahead_records;
250250
extern int gbl_sql_logfill_next_timeout;
251251
extern int gbl_sql_logfill_autodisable_threshold;
252+
extern int gbl_sql_logfill_request_fail_autodisable_threshold;
252253
extern int gbl_sql_logfill_auto_disabled;
253254
extern int gbl_dedup_rep_all_reqs;
254255
extern int gbl_apply_queue_memory;

db/db_tunables.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,11 +1476,16 @@ REGISTER_TUNABLE("sql_logfill_lookahead_records",
14761476
REGISTER_TUNABLE("sql_logfill_next_timeout", "Max amount of time logfill blocked on transaction-logs. (Default: 10)",
14771477
TUNABLE_INTEGER, &gbl_sql_logfill_next_timeout, 0, NULL, NULL, NULL, NULL);
14781478
REGISTER_TUNABLE("sql_logfill_auto_disabled",
1479-
"Set to 1 when sql-logfill has been auto-disabled due to consecutive failures. (Default: 0)",
1480-
TUNABLE_INTEGER, (int *)&gbl_sql_logfill_auto_disabled, READONLY, NULL, NULL, NULL, NULL);
1479+
"Set to 1 when sql-logfill has been auto-disabled due to consecutive failures; clear it to 0 to "
1480+
"resume the parked sql-logfill threads. (Default: 0)",
1481+
TUNABLE_INTEGER, (int *)&gbl_sql_logfill_auto_disabled, 0, NULL, NULL, NULL, NULL);
14811482
REGISTER_TUNABLE("sql_logfill_autodisable_threshold",
14821483
"Disable sql-logfill after this many consecutive authentication failures. (Default: 5)",
14831484
TUNABLE_INTEGER, &gbl_sql_logfill_autodisable_threshold, 0, NULL, NULL, NULL, NULL);
1485+
REGISTER_TUNABLE("sql_logfill_request_fail_autodisable_threshold",
1486+
"Disable sql-logfill after this many consecutive failed log requests to a reachable master "
1487+
"(e.g. all sql engines busy and queue full, surfaced as a connect/io error). (Default: 5)",
1488+
TUNABLE_INTEGER, &gbl_sql_logfill_request_fail_autodisable_threshold, 0, NULL, NULL, NULL, NULL);
14841489
REGISTER_TUNABLE("periodic_rep_report", "Report replication status every second. (Default: on)", TUNABLE_BOOLEAN,
14851490
&gbl_periodic_rep_report, 0, NULL, NULL, NULL, NULL);
14861491
REGISTER_TUNABLE("rep_verify_always_grab_writelock", "Force every rep_verify to grab writelock.", TUNABLE_BOOLEAN,

db/sqllogfill.c

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ int gbl_sql_logfill_dedicated_apply_thread = 0;
3737
int gbl_sql_logfill_lookahead_records = 10000;
3838
int gbl_sql_logfill_next_timeout = 10;
3939
int gbl_sql_logfill_autodisable_threshold = 5;
40+
int gbl_sql_logfill_request_fail_autodisable_threshold = 5;
4041

4142
/* Flags */
4243
int gbl_sql_logfill_auto_disabled = 0;
4344
static int sql_logfill_thds_created = 0;
4445
static int sql_logfill_auth_failure_cnt = 0;
46+
static int sql_logfill_request_fail_cnt = 0;
4547

4648
struct log_record {
4749
unsigned int file;
@@ -123,6 +125,47 @@ static void increment_logfill_auth_failure_cnt(void)
123125
}
124126
}
125127

128+
/* When all of the master's sql engines are busy and its dispatch queue is full,
129+
* the master rejects the logfill request with CDB2ERR_REJECTED. That error is
130+
* retryable, so the cdb2 client retries it internally and -- for our
131+
* CDB2_DIRECT_CPU handle with min_retries==1 -- surfaces it to us as
132+
* CDB2ERR_CONNECT_ERROR (or CDB2ERR_TRAN_IO_ERROR once retries are exhausted),
133+
* NOT as CDB2ERR_REJECTED (which we still match, defensively).
134+
*
135+
* These same codes are also what a genuinely unreachable master produces: for a
136+
* CDB2_DIRECT_CPU handle cdb2_open() does not connect (and "set transaction
137+
* blocksql" is a client-side set), so the first real round-trip -- the "select
138+
* 1" ping in connect_to_master() -- is where an unreachable master surfaces, and
139+
* that is a counting site. We do NOT distinguish "reachable but saturated" from
140+
* "unreachable" here; both mean "logfill can't get logs from the master", and in
141+
* both cases parking logfill and falling back to the traditional fill-request
142+
* path (otherwise suppressed while sql_logfill is active) is the right response.
143+
*
144+
* Spurious trips are avoided by two things rather than by inspecting the error:
145+
* (1) request_logs_from_master() returns early WITHOUT counting when there is no
146+
* elected master (thedb->master is unset / ".invalid"), which covers ordinary
147+
* elections and outage windows; and (2) the counters are reset the moment we
148+
* reach the master and make progress (a successful fetch, or confirming there is
149+
* no gap), so only a *sustained* run of failures against a still-elected master
150+
* reaches the threshold. */
151+
static int logfill_request_failed(int rc)
152+
{
153+
return rc == CDB2ERR_REJECTED || rc == CDB2ERR_CONNECT_ERROR || rc == CDB2ERR_TRAN_IO_ERROR;
154+
}
155+
156+
static void increment_logfill_request_fail_cnt(void)
157+
{
158+
sql_logfill_request_fail_cnt++;
159+
if (gbl_sql_logfill_request_fail_autodisable_threshold > 0 &&
160+
sql_logfill_request_fail_cnt >= gbl_sql_logfill_request_fail_autodisable_threshold) {
161+
logmsg(LOGMSG_ERROR,
162+
"%s: auto-disabling sql logfill after %d consecutive request failures (master unavailable or at "
163+
"capacity)\n",
164+
__func__, sql_logfill_request_fail_cnt);
165+
gbl_sql_logfill_auto_disabled = 1;
166+
}
167+
}
168+
126169
/* Connect to master */
127170
static int connect_to_master(bdb_state_type *bdb_state, const char *master)
128171
{
@@ -175,6 +218,8 @@ static int connect_to_master(bdb_state_type *bdb_state, const char *master)
175218

176219
if (rc == CDB2ERR_ACCESS) {
177220
increment_logfill_auth_failure_cnt();
221+
} else if (logfill_request_failed(rc)) {
222+
increment_logfill_request_fail_cnt();
178223
}
179224
return 1;
180225
}
@@ -483,6 +528,15 @@ static int request_logs_from_master(bdb_state_type *bdb_state)
483528
if (IS_ZERO_LSN(gap_lsn)) {
484529
BDB_RELLOCK();
485530
nogap_returns++;
531+
/* We reached the master and confirmed we are caught up: the master
532+
* is servicing us, so clear the consecutive-failure counters. This
533+
* is what makes the counts "consecutive" -- without it a healthy
534+
* replicant that is almost always caught up (and so almost always
535+
* takes this path) would never reset, and unrelated transient
536+
* failures would accumulate over the process lifetime and
537+
* eventually trip a spurious auto-disable. */
538+
sql_logfill_auth_failure_cnt = 0;
539+
sql_logfill_request_fail_cnt = 0;
486540
return 0;
487541
}
488542

@@ -525,18 +579,28 @@ static int request_logs_from_master(bdb_state_type *bdb_state)
525579
}
526580
if (rc == CDB2ERR_ACCESS) {
527581
increment_logfill_auth_failure_cnt();
582+
} else if (logfill_request_failed(rc)) {
583+
increment_logfill_request_fail_cnt();
528584
}
529585
disconnect_from_master();
530586
return 1;
531587
}
532588

533589
sql_logfill_auth_failure_cnt = 0;
590+
sql_logfill_request_fail_cnt = 0;
534591
sql_finds++;
535592

536593
if ((rc = cdb2_next_record(hndl)) != CDB2_OK) {
537594
if (gbl_debug_sql_logfill) {
538595
logmsg(LOGMSG_USER, "%s: cdb2_next_record returned rc=%d\n", __func__, rc);
539596
}
597+
/* The master accepted the query but could not stream results (e.g.
598+
* dropped the connection under load). Count the connect/io class
599+
* of errors here too; CDB2_OK_DONE and other benign codes are not
600+
* matched by logfill_request_failed(). */
601+
if (logfill_request_failed(rc)) {
602+
increment_logfill_request_fail_cnt();
603+
}
540604
disconnect_from_master();
541605
return 0;
542606
}
@@ -641,12 +705,32 @@ static void *sql_logfill_thread(void *arg)
641705
{
642706
bdb_state_type *bdb_state = (bdb_state_type *)arg;
643707
int desired, exiting;
708+
int was_parked = 0;
644709

645710
comdb2_name_thread(__func__);
646711
logmsg(LOGMSG_USER, "%s: starting sql-logfill-thread\n", __func__);
647712
bdb_thread_event(bdb_state, BDBTHR_EVENT_START);
648713

649-
while (!db_is_exiting() && !gbl_sql_logfill_auto_disabled) {
714+
while (!db_is_exiting()) {
715+
/* When auto-disabled we park rather than exit: the traditional
716+
* fill-request path has taken over (sql_logfill_active() is now false),
717+
* and an operator can clear gbl_sql_logfill_auto_disabled to resume.
718+
* Drop any master connection and reset the failure counters so a resume
719+
* starts fresh with a full threshold's worth of attempts. */
720+
if (gbl_sql_logfill_auto_disabled) {
721+
disconnect_from_master();
722+
sql_logfill_auth_failure_cnt = 0;
723+
sql_logfill_request_fail_cnt = 0;
724+
was_parked = 1;
725+
sleep(1);
726+
continue;
727+
}
728+
729+
if (was_parked) {
730+
logmsg(LOGMSG_USER, "%s: resuming sql-logfill after auto-disable was cleared\n", __func__);
731+
was_parked = 0;
732+
}
733+
650734
if (thedb->master != gbl_myhostname) {
651735
if (request_logs_from_master(bdb_state)) {
652736
sleep(1);
@@ -687,7 +771,14 @@ static void *sql_apply_thread(void *arg)
687771
struct log_record copy = {0};
688772
logmsg(LOGMSG_USER, "%s: starting sql-apply-thread\n", __func__);
689773

690-
while (!db_is_exiting() && !gbl_sql_logfill_auto_disabled) {
774+
while (!db_is_exiting()) {
775+
776+
/* Park (rather than exit) while auto-disabled so we can resume if an
777+
* operator clears gbl_sql_logfill_auto_disabled. */
778+
if (gbl_sql_logfill_auto_disabled) {
779+
sleep(1);
780+
continue;
781+
}
691782

692783
int apply_log = 0;
693784
Pthread_mutex_lock(&sql_apply_queue_lock);

tests/tunables.test/t00_all_tunables.expected

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,11 +985,12 @@
985985
(name='sql_close_sbuf', description='sql_close_sbuf', type='BOOLEAN', value='OFF', read_only='N')
986986
(name='sql_logfill', description='Request transaction logs via sql thread. (Default: off)', type='BOOLEAN', value='OFF', read_only='Y')
987987
(name='sql_logfill_apply_thread', description='Use a dedicated thread to apply sql logfills. (Default: off)', type='BOOLEAN', value='OFF', read_only='Y')
988-
(name='sql_logfill_auto_disabled', description='Set to 1 when sql-logfill has been auto-disabled due to consecutive failures. (Default: 0)', type='INTEGER', value='0', read_only='Y')
988+
(name='sql_logfill_auto_disabled', description='Set to 1 when sql-logfill has been auto-disabled due to consecutive failures; clear it to 0 to resume the parked sql-logfill threads. (Default: 0)', type='INTEGER', value='0', read_only='N')
989989
(name='sql_logfill_autodisable_threshold', description='Disable sql-logfill after this many consecutive authentication failures. (Default: 5)', type='INTEGER', value='5', read_only='N')
990990
(name='sql_logfill_debug', description='Enable extended trace for sql logfill thread. (Default: off)', type='BOOLEAN', value='OFF', read_only='N')
991991
(name='sql_logfill_lookahead_records', description='Max lookahead records cached for dedicated apply thread. (Default: 10000)', type='INTEGER', value='10000', read_only='Y')
992992
(name='sql_logfill_next_timeout', description='Max amount of time logfill blocked on transaction-logs. (Default: 10)', type='INTEGER', value='10', read_only='N')
993+
(name='sql_logfill_request_fail_autodisable_threshold', description='Disable sql-logfill after this many consecutive failed log requests to a reachable master (e.g. all sql engines busy and queue full, surfaced as a connect/io error). (Default: 5)', type='INTEGER', value='5', read_only='N')
993994
(name='sql_logfill_stats', description='Print periodic stats from sql logfill thread. (Default: on)', type='BOOLEAN', value='ON', read_only='N')
994995
(name='sql_optimize_shadows', description='', type='BOOLEAN', value='OFF', read_only='N')
995996
(name='sql_queueing_critical_trace', description='Produce trace when SQL request queue is this deep.', type='INTEGER', value='100', read_only='N')

0 commit comments

Comments
 (0)