Skip to content

Commit c5a4fe5

Browse files
committed
sql_cacher: fix full-caching reload concurrency
Only affects full-caching, as follows: - Use switchable reader/writer locking to serialize full-cache reload writers while allowing row-mode reads. - Protect reload-version and state transitions through fetch and decode. - Return previous-generation values only during active full reloads. - Preserve startup versions and clear state on failure. - Retain table locking as a compatibility option. - Validate lock scopes and protect initial column metadata. - Fix an expire= parsing bug in the cache_table modparam. - Fix an infinite loop when cache-entry lock initialization fails. - Document mixed-generation reads and failure behavior. - Add parser, loader, version, and multi-process locking regression tests.
1 parent cf4df8b commit c5a4fe5

6 files changed

Lines changed: 485 additions & 87 deletions

File tree

‎atomic.h‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
#undef NO_ATOMIC_OPS
4444

4545
typedef _Atomic(unsigned long) atomic_t;
46-
typedef _Atomic(_Bool) atomic_bool_t;
4746
#else /* HAVE_STDATOMIC */
4847

4948
/************************* i386 & x86_64 ARCH ****************************/
@@ -73,7 +72,6 @@ typedef _Atomic(_Bool) atomic_bool_t;
7372
* not some alias that contains the same information.
7473
*/
7574
typedef struct { volatile unsigned int counter; } atomic_t;
76-
typedef atomic_t atomic_bool_t;
7775

7876
/*! \brief
7977
* atomic_add - add integer to atomic variable
@@ -143,7 +141,6 @@ static __inline__ void atomic_dec(atomic_t *v)
143141
* not some alias that contains the same information.
144142
*/
145143
typedef struct { volatile unsigned long counter; } atomic_t;
146-
typedef atomic_t atomic_bool_t;
147144

148145
/*! \brief
149146
* atomic_set - set atomic variable

‎modules/sql_cacher/README.md‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ when appropriate keys are requested.
3535
For on demand caching, the stored values have a configurable expire period after
3636
which they are permanently removed unless an MI reload function is called for a
3737
specific key. In the case of full caching the data is automatically reloaded at
38-
a configurable interval. Consequently if the data in the SQL database changes
39-
and a MI reload function is called, the old data remains in cache only
40-
until it expires.
38+
a configurable interval. Old full-caching rows may remain physically stored
39+
until their expiry time, but reload versions control whether they can be read.
40+
41+
Full caching allows reads while an automatic or MI full-table reload writes
42+
rows into the cache. During the reload, a lookup may return either the new row
43+
or its value from the immediately preceding reload. Once the reload finishes,
44+
rows not loaded by that reload are no longer returned, even if their cache
45+
entries have not expired. Use `full_caching_lock_scope=table` to make reads
46+
wait for the reload instead.
47+
48+
A reload updates the cache in place. If it fails after writing some rows, those
49+
rows remain available; rows still carrying the preceding version are no longer
50+
returned when the failed reload ends. Neither locking mode provides rollback.
4151

4252

4353
### Dependencies
@@ -91,14 +101,20 @@ If not present, all the columns from the table will be cached
91101
- *expire* : expire period for the values stored
92102
in the cache for the on demand caching type in seconds
93103
If not present, default value is "1 hour"
94-
- *full_caching_lock_scope* : whether to lock reads while loading the table
95-
into the cache on a row or table basis. With `table`, loading large tables
96-
can block `$sql_cached_value` reads and stall SIP processing, but ensures
97-
full-table consistency. `row` allows reads while other rows are being written.
98-
- row
99-
- table
100-
101-
*If not present, default value is "row".*
104+
- *full_caching_lock_scope* : controls reads during full caching reloads
105+
(`on_demand=0`):
106+
- `row` (default): reads continue while rows are being loaded. CacheDB
107+
provides synchronization for individual values. Different lookups,
108+
including rows returned by `sql_cache_dump()`, may observe a mix
109+
of the new and previous reloads; this is not a table snapshot.
110+
- `table`: reads wait while the table is being loaded into the cache.
111+
112+
Reloads of the same full-caching entry run one at a time within an
113+
OpenSIPS instance, including `sql_cacher:reload` calls that specify a `key`.
114+
In `row` mode, changing a cached SQL column from an integer type to a
115+
string type, or vice versa, requires restarting OpenSIPS before reloading
116+
the cache.
117+
`full_caching_lock_scope` has no effect on on-demand caching.
102118

103119

104120
The parameters must be given in the exact order specified above.

‎modules/sql_cacher/sql_cacher.c‎

Lines changed: 109 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,46 @@
3737
#include "sql_cacher.h"
3838
#include "../../lib/csv.h"
3939

40-
/*
41-
* cache_lock conditional compilation of atomic variable support
42-
* so we don't need to acquire a lock for reads
43-
* if the cache_table param full_caching_lock_scope is set to row
44-
* if atomic ops are not supported then the param has no effect
45-
*/
46-
#ifndef NO_ATOMIC_OPS
40+
/* Row-mode writers serialize with each other, but run alongside readers.
41+
* Upgrade to exclusive access only while changing the reload version, state
42+
* or column layout. Readers keep this state stable through fetch + decode.
43+
* This also works on platforms without C11 atomics. */
4744
#define lock_cache_writes(cache_lock) \
4845
do { \
49-
atomic_store(&cache_lock.is_writing, 1); \
50-
lock_start_write(cache_lock.ref_lock); \
46+
if ((cache_lock).scope == CACHE_LOCK_ROW) \
47+
lock_start_sw_read((cache_lock).ref_lock); \
48+
else \
49+
lock_start_write((cache_lock).ref_lock); \
5150
} while (0)
5251
#define unlock_cache_writes(cache_lock) \
5352
do { \
54-
atomic_store(&cache_lock.is_writing, 0); \
55-
lock_stop_write(cache_lock.ref_lock); \
53+
if ((cache_lock).scope == CACHE_LOCK_ROW) \
54+
lock_stop_sw_read((cache_lock).ref_lock); \
55+
else \
56+
lock_stop_write((cache_lock).ref_lock); \
5657
} while (0)
57-
#define acquire_read_lock(cache_lock) \
58+
#define lock_cache_state(cache_lock, old) \
5859
do { \
59-
if (cache_lock.scope == CACHE_LOCK_TABLE) { \
60-
lock_start_read(cache_lock.ref_lock); \
61-
} \
60+
if ((cache_lock).scope == CACHE_LOCK_ROW) \
61+
lock_switch_write((cache_lock).ref_lock, old); \
6262
} while (0)
63-
#define release_read_lock(cache_lock) \
63+
#define unlock_cache_state(cache_lock, old) \
6464
do { \
65-
if (cache_lock.scope == CACHE_LOCK_TABLE) { \
66-
lock_stop_read(cache_lock.ref_lock); \
67-
} \
65+
if ((cache_lock).scope == CACHE_LOCK_ROW) \
66+
lock_switch_read((cache_lock).ref_lock, old); \
6867
} while (0)
69-
#define is_cache_writing(cache_lock) atomic_load(&cache_lock.is_writing)
70-
#define init_cache_lock ((cache_lock_t){NULL, CACHE_LOCK_ROW, 0})
71-
#else
72-
#define lock_cache_writes(cache_lock) lock_start_write(cache_lock.ref_lock)
73-
#define unlock_cache_writes(cache_lock) lock_stop_write(cache_lock.ref_lock)
74-
#define acquire_read_lock(cache_lock) lock_start_read(cache_lock.ref_lock)
75-
#define release_read_lock(cache_lock) lock_stop_read(cache_lock.ref_lock)
76-
#define is_cache_writing(cache_lock) 0
77-
#define init_cache_lock ((cache_lock_t){NULL, CACHE_LOCK_ROW})
78-
#endif
68+
#define acquire_read_lock(cache_lock) lock_start_read((cache_lock).ref_lock)
69+
#define release_read_lock(cache_lock) lock_stop_read((cache_lock).ref_lock)
70+
71+
static void finish_cache_reload(cache_lock_t *cache_lock)
72+
{
73+
int old_w_flag = 0;
7974

80-
/* end of cache_lock conditional compilation */
75+
lock_cache_state(*cache_lock, old_w_flag);
76+
cache_lock->is_writing = 0;
77+
unlock_cache_state(*cache_lock, old_w_flag);
78+
unlock_cache_writes(*cache_lock);
79+
}
8180

8281
static int mod_init(void);
8382
static void destroy(void);
@@ -239,7 +238,9 @@ static int parse_cache_entry(unsigned int type, void *val)
239238
new_entry->nr_ints = 0;
240239
new_entry->nr_strs = 0;
241240
new_entry->column_types = 0;
242-
new_entry->cache_lock = init_cache_lock;
241+
new_entry->cache_lock.ref_lock = NULL;
242+
new_entry->cache_lock.scope = CACHE_LOCK_ROW;
243+
new_entry->cache_lock.is_writing = 0;
243244

244245
#define PARSE_TOKEN(_ptr1, _ptr2, field, field_name_str, field_name_len) \
245246
do { \
@@ -462,7 +463,7 @@ static int parse_cache_entry(unsigned int type, void *val)
462463
goto parse_err;
463464
}
464465
tmp = memchr(p2 + 1, spec_delimiter.s[0],
465-
parse_str.len - (p2 - parse_str.s + 1));
466+
parse_str.len - (p2 - parse_str.s + 1));
466467
str str_val;
467468
if (!tmp) /* delimiter not found, reached the end of the string to parse */
468469
str_val.len = parse_str.len - (p2 - parse_str.s + 1);
@@ -496,7 +497,10 @@ static int parse_cache_entry(unsigned int type, void *val)
496497
if (p2 - p1 == EXPIRE_STR_LEN &&
497498
!memcmp(p1, EXPIRE_STR, EXPIRE_STR_LEN)) {
498499
str str_val;
499-
str_val.len = parse_str.len - (p2 - parse_str.s + 1);
500+
tmp = memchr(p2 + 1, spec_delimiter.s[0],
501+
parse_str.len - (p2 - parse_str.s + 1));
502+
str_val.len = tmp ? tmp - p2 - 1 :
503+
parse_str.len - (p2 - parse_str.s + 1);
500504
if (str_val.len <= 0) {
501505
LM_ERR("expected value of: %.*s\n", EXPIRE_STR_LEN, EXPIRE_STR);
502506
goto parse_err;
@@ -520,12 +524,8 @@ static int parse_cache_entry(unsigned int type, void *val)
520524
}
521525
}
522526

523-
if (!memcmp(p1, FULL_CACHING_LOCK_SCOPE, FULL_CACHING_LOCK_SCOPE_STR_LEN)) {
524-
if (*(p1+FULL_CACHING_LOCK_SCOPE_STR_LEN) != '=') { \
525-
LM_ERR("expected: '=' after: %.*s\n", FULL_CACHING_LOCK_SCOPE_STR_LEN, FULL_CACHING_LOCK_SCOPE);
526-
goto parse_err;
527-
}
528-
527+
if (p2 - p1 == FULL_CACHING_LOCK_SCOPE_STR_LEN &&
528+
!memcmp(p1, FULL_CACHING_LOCK_SCOPE, FULL_CACHING_LOCK_SCOPE_STR_LEN)) {
529529
tmp = memchr(p2 + 1, spec_delimiter.s[0],
530530
parse_str.len - (p2 - parse_str.s + 1));
531531
if (!tmp)
@@ -543,19 +543,15 @@ static int parse_cache_entry(unsigned int type, void *val)
543543
else if (len == LOCK_SCOPE_TABLE_STR_LEN && !memcmp(p2+1, LOCK_SCOPE_TABLE, len))
544544
new_entry->cache_lock.scope = CACHE_LOCK_TABLE;
545545
else {
546-
LM_ERR("Unsupported cache lock scope '%.*s' defaulting to row\n", len, p2+1);
546+
LM_ERR("Unsupported cache lock scope '%.*s' (expected row or table)\n",
547+
len, p2 + 1);
548+
goto parse_err;
547549
}
548550

549551
if (!tmp) /* delimiter not found, reached the end of the string to parse */
550552
goto end_parsing;
551-
else {
552-
p1 = tmp + 1;
553-
p2 = memchr(p1, '=', parse_str.len - (p1 - parse_str.s));
554-
if (!p2) {
555-
LM_ERR("expected: '='\n");
556-
goto parse_err;
557-
}
558-
}
553+
LM_ERR("unexpected parameter after full_caching_lock_scope\n");
554+
goto parse_err;
559555
} else if (parse_str.len - (p1 - parse_str.s) > 0) {
560556
LM_ERR("unknown parameter: %.*s\n",
561557
(int)(parse_str.len - (p1 - parse_str.s)), p1);
@@ -1073,6 +1069,8 @@ static int load_entire_table(cache_entry_t *c_entry, db_handlers_t *db_hdls,
10731069
int i;
10741070
int reload_vers = 0;
10751071
int loaded_rec = 0;
1072+
int old_w_flag = 0;
1073+
cache_entry_t layout;
10761074

10771075
sr_add_report( sql_srg, STR2CI(c_entry->id),
10781076
CHAR_INT("starting DB data loading"), 0);
@@ -1128,32 +1126,58 @@ static int load_entire_table(cache_entry_t *c_entry, db_handlers_t *db_hdls,
11281126
pkg_free(query_cols);
11291127

11301128
lock_cache_writes(db_hdls->c_entry->cache_lock);
1131-
1132-
if (update_cache_rld_vers(db_hdls, inc_rld_vers, &reload_vers) < 0) {
1133-
unlock_cache_writes(db_hdls->c_entry->cache_lock);
1134-
goto error;
1135-
}
1129+
lock_cache_state(c_entry->cache_lock, old_w_flag);
11361130

11371131
/* anything loaded ? if not, we can do a quick exit here */
11381132
if (RES_ROW_N(sql_res) == 0)
1139-
goto done;
1133+
goto update_version;
11401134

11411135
row = RES_ROWS(sql_res);
11421136
values = ROW_VALUES(row);
1143-
if (get_column_types(c_entry, values + 1, ROW_N(row) - 1) < 0) {
1137+
layout = *c_entry;
1138+
if (get_column_types(&layout, values + 1, ROW_N(row) - 1) < 0) {
1139+
unlock_cache_state(c_entry->cache_lock, old_w_flag);
11441140
unlock_cache_writes(db_hdls->c_entry->cache_lock);
11451141
goto error;
11461142
}
1143+
/* Old and new rows coexist in row mode, so their layouts must agree. */
1144+
if (c_entry->cache_lock.scope == CACHE_LOCK_ROW &&
1145+
c_entry->nr_ints + c_entry->nr_strs != 0 &&
1146+
(layout.nr_ints != c_entry->nr_ints ||
1147+
layout.nr_strs != c_entry->nr_strs ||
1148+
layout.column_types != c_entry->column_types)) {
1149+
LM_ERR("Column types changed for cache %.*s; restart OpenSIPS "
1150+
"to load the new layout\n", c_entry->id.len, c_entry->id.s);
1151+
unlock_cache_state(c_entry->cache_lock, old_w_flag);
1152+
unlock_cache_writes(c_entry->cache_lock);
1153+
goto error;
1154+
}
1155+
c_entry->nr_ints = layout.nr_ints;
1156+
c_entry->nr_strs = layout.nr_strs;
1157+
c_entry->column_types = layout.column_types;
1158+
1159+
update_version:
1160+
if (update_cache_rld_vers(db_hdls, inc_rld_vers, &reload_vers) < 0) {
1161+
unlock_cache_state(c_entry->cache_lock, old_w_flag);
1162+
unlock_cache_writes(c_entry->cache_lock);
1163+
goto error;
1164+
}
1165+
/* Startup loads do not increment the version and must not revive rows
1166+
* invalidated by an earlier reload. Single-key reloads never set this. */
1167+
c_entry->cache_lock.is_writing = inc_rld_vers != 0 && RES_ROW_N(sql_res) != 0;
1168+
unlock_cache_state(c_entry->cache_lock, old_w_flag);
1169+
if (RES_ROW_N(sql_res) == 0)
1170+
goto done;
11471171

1148-
/* load the rows into the cahchedb */
1172+
/* load the rows into the cachedb */
11491173
do {
11501174
for (i=0; i < RES_ROW_N(sql_res); i++) {
11511175
row = RES_ROWS(sql_res) + i;
11521176
values = ROW_VALUES(row);
11531177
if (!VAL_NULL(values)) {
11541178
if (insert_in_cachedb(c_entry, db_hdls, values ,values + 1,
11551179
reload_vers, ROW_N(row) - 1) < 0) {
1156-
unlock_cache_writes(db_hdls->c_entry->cache_lock);
1180+
finish_cache_reload(&c_entry->cache_lock);
11571181
goto error;
11581182
}
11591183
loaded_rec++;
@@ -1164,7 +1188,7 @@ static int load_entire_table(cache_entry_t *c_entry, db_handlers_t *db_hdls,
11641188
if (db_hdls->db_funcs.fetch_result(db_hdls->db_con,&sql_res,fetch_nr_rows)<0) {
11651189
LM_ERR("Error fetching rows (1) from SQL DB: %s\n",
11661190
db_url_escape(&c_entry->db_url));
1167-
unlock_cache_writes(db_hdls->c_entry->cache_lock);
1191+
finish_cache_reload(&c_entry->cache_lock);
11681192
goto error;
11691193
}
11701194
} else {
@@ -1173,7 +1197,7 @@ static int load_entire_table(cache_entry_t *c_entry, db_handlers_t *db_hdls,
11731197
} while (RES_ROW_N(sql_res) > 0);
11741198

11751199
done:
1176-
unlock_cache_writes(db_hdls->c_entry->cache_lock);
1200+
finish_cache_reload(&c_entry->cache_lock);
11771201

11781202
db_hdls->db_funcs.free_result(db_hdls->db_con, sql_res);
11791203

@@ -1215,6 +1239,7 @@ static int load_key(cache_entry_t *c_entry, db_handlers_t *db_hdls, str key,
12151239
db_row_t *row;
12161240
db_val_t key_val;
12171241
str src_key, null_val;
1242+
int rc, old_w_flag = 0;
12181243

12191244
src_key.len = c_entry->id.len + key.len;
12201245
src_key.s = pkg_malloc(src_key.len);
@@ -1273,9 +1298,16 @@ static int load_key(cache_entry_t *c_entry, db_handlers_t *db_hdls, str key,
12731298
row = RES_ROWS(*sql_res);
12741299
*values = ROW_VALUES(row);
12751300

1276-
if (c_entry->nr_ints + c_entry->nr_strs == 0 &&
1277-
get_column_types(c_entry, *values, ROW_N(row)) < 0)
1278-
goto sql_error;
1301+
if (c_entry->nr_ints + c_entry->nr_strs == 0) {
1302+
/* A key reload can precede the initial full-table load. */
1303+
if (!c_entry->on_demand)
1304+
lock_cache_state(c_entry->cache_lock, old_w_flag);
1305+
rc = get_column_types(c_entry, *values, ROW_N(row));
1306+
if (!c_entry->on_demand)
1307+
unlock_cache_state(c_entry->cache_lock, old_w_flag);
1308+
if (rc < 0)
1309+
goto sql_error;
1310+
}
12791311

12801312
if (insert_in_cachedb(c_entry, db_hdls, &key_val, *values, rld_vers, ROW_N(row)) < 0)
12811313
goto sql_error;
@@ -1529,7 +1561,7 @@ static int mod_init(void)
15291561
c_entry->cache_lock.ref_lock = lock_init_rw();
15301562
if (!c_entry->cache_lock.ref_lock) {
15311563
LM_ERR("Failed to init readers-writers lock\n");
1532-
continue;
1564+
return -1;
15331565
}
15341566
if (sr_register_identifier( sql_srg, STR2CI(c_entry->id),
15351567
SR_STATUS_NO_DATA, CHAR_INT("no data loaded"), 20 ) ) {
@@ -1577,7 +1609,8 @@ static int child_init(int rank)
15771609
}
15781610

15791611
/* perform full caching load in the same process but after child_init is done */
1580-
if ((rank == 1) && ipc_send_rpc(process_no, cache_init_load, NULL) < 0) {
1612+
if (rank == 1 && db_hdls_list &&
1613+
ipc_send_rpc(process_no, cache_init_load, NULL) < 0) {
15811614
LM_ERR("Failed to RPC full caching load\n");
15821615
return -1;
15831616
}
@@ -1639,11 +1672,14 @@ static int cdb_val_decode(const pv_name_fix_t *pv_name, const str *cdb_val, int
16391672
goto error;
16401673
memcpy(&int_val, int_buf, 4);
16411674

1642-
if (reload_version != int_val) {
1643-
if (!is_cache_writing(pv_name->c_entry->cache_lock) ||
1644-
(is_cache_writing(pv_name->c_entry->cache_lock) && reload_version != int_val +1))
1645-
return 3;
1646-
}
1675+
/* The caller holds a read lock through fetch + decode. Only a full
1676+
* reload in row mode permits the immediately preceding generation. */
1677+
if (int_val != reload_version &&
1678+
(pv_name->c_entry->on_demand ||
1679+
pv_name->c_entry->cache_lock.scope != CACHE_LOCK_ROW ||
1680+
!pv_name->c_entry->cache_lock.is_writing ||
1681+
reload_version <= 0 || int_val != reload_version - 1))
1682+
return 3;
16471683

16481684
/* null integer value in db */
16491685
if (!memcmp(cdb_val->s + pv_name->col_offset, zeroes, INT_B64_ENC_LEN))
@@ -2472,3 +2508,7 @@ static int sql_cache_dump(struct sip_msg *msg, db_handlers_t *dbh,
24722508

24732509
return n == 0 ? -2 : n;
24742510
}
2511+
2512+
#ifdef UNIT_TESTS
2513+
#include "test/test_sql_cacher.h"
2514+
#endif

0 commit comments

Comments
 (0)