Skip to content

Commit 9834279

Browse files
committed
Modules: improved shared dict eviction strategy.
Previously, when a slab allocation failed in evict mode, only 16 entries were evicted with a single retry. This could still result in SharedMemoryError when the freed slab slots did not match the requested allocation size class, even though the zone had plenty of evictable entries. In practice, it might happen when the following conditions are met: - The shared zone is full - evict flag is enabled - key/value entries differ in size The allocation now retries in a loop, evicting 16 entries at a time, until the allocation succeeds or no more entries remain in the expire tree. After this change, allocation with evict enabled can only fail when: - the value is larger than the zone's usable space - the expire tree has no entries left to evict - zone metadata overhead leaves insufficient room
1 parent 7547719 commit 9834279

2 files changed

Lines changed: 62 additions & 10 deletions

File tree

nginx/ngx_js_shared_dict.c

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ static ngx_int_t ngx_js_dict_copy_value_locked(njs_vm_t *vm,
128128
ngx_js_dict_t *dict, ngx_js_dict_node_t *node, njs_value_t *retval);
129129

130130
static void ngx_js_dict_expire(ngx_js_dict_t *dict, ngx_msec_t now);
131-
static void ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count);
131+
static ngx_uint_t ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_uint_t count);
132132

133133
static njs_int_t ngx_js_dict_shared_error_name(njs_vm_t *vm,
134134
njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
@@ -1371,12 +1371,24 @@ ngx_js_dict_alloc(ngx_js_dict_t *dict, size_t n)
13711371
{
13721372
void *p;
13731373

1374-
dict->shpool->log_nomem = !dict->evict;
1374+
if (!dict->evict) {
1375+
return ngx_slab_alloc_locked(dict->shpool, n);
1376+
}
1377+
1378+
dict->shpool->log_nomem = 0;
13751379
p = ngx_slab_alloc_locked(dict->shpool, n);
1380+
1381+
while (p == NULL) {
1382+
if (ngx_js_dict_evict(dict, 16) == 0) {
1383+
break;
1384+
}
1385+
1386+
p = ngx_slab_alloc_locked(dict->shpool, n);
1387+
}
1388+
13761389
dict->shpool->log_nomem = 1;
13771390

1378-
if (p == NULL && dict->evict) {
1379-
ngx_js_dict_evict(dict, 16);
1391+
if (p == NULL) {
13801392
p = ngx_slab_alloc_locked(dict->shpool, n);
13811393
}
13821394

@@ -1784,25 +1796,28 @@ ngx_js_dict_expire(ngx_js_dict_t *dict, ngx_msec_t now)
17841796
}
17851797

17861798

1787-
static void
1788-
ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count)
1799+
static ngx_uint_t
1800+
ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_uint_t count)
17891801
{
1802+
ngx_uint_t evicted;
17901803
ngx_rbtree_t *rbtree;
17911804
ngx_rbtree_node_t *rn, *next;
17921805
ngx_js_dict_node_t *node;
17931806

17941807
rbtree = &dict->sh->rbtree_expire;
17951808

17961809
if (rbtree->root == rbtree->sentinel) {
1797-
return;
1810+
return 0;
17981811
}
17991812

1813+
evicted = 0;
1814+
18001815
for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
18011816
rn != NULL;
18021817
rn = next)
18031818
{
18041819
if (count-- == 0) {
1805-
return;
1820+
return evicted;
18061821
}
18071822

18081823
node = (ngx_js_dict_node_t *)
@@ -1815,7 +1830,11 @@ ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count)
18151830
ngx_rbtree_delete(&dict->sh->rbtree, (ngx_rbtree_node_t *) node);
18161831

18171832
ngx_js_dict_node_free(dict, node);
1833+
1834+
evicted++;
18181835
}
1836+
1837+
return evicted;
18191838
}
18201839

18211840

nginx/t/js_shared_dict_evict.t

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ http {
5151
location /no_self_evict {
5252
js_content test.no_self_evict;
5353
}
54+
55+
location /cross_slab_class {
56+
js_content test.cross_slab_class;
57+
}
5458
}
5559
}
5660
@@ -110,12 +114,38 @@ $t->write_file('test.js', <<'EOF');
110114
r.return(200, 'FILLED:' + elems);
111115
}
112116
113-
export default { stress, no_self_evict };
117+
function cross_slab_class(r) {
118+
var dict = ngx.shared.stress;
119+
var v = 'x'.repeat(16);
120+
121+
/* Calibrate: find how many tiny entries fit. */
122+
dict.clear();
123+
dict.set('probe', v);
124+
125+
var elems = 0;
126+
while (dict.has('probe')) {
127+
dict.set('s_' + elems++, v);
128+
}
129+
130+
/* Rebuild at exact capacity with tiny entries. */
131+
dict.clear();
132+
for (var i = 0; i < elems; i++) {
133+
dict.set('s_' + i, v);
134+
}
135+
136+
/* Check that the zone can evict enough entries to fit a big one. */
137+
138+
dict.set('big', 'y'.repeat(2048));
139+
140+
r.return(200, 'FILLED:' + elems);
141+
}
142+
143+
export default { stress, no_self_evict, cross_slab_class };
114144
EOF
115145

116146
$t->try_run('no js_shared_dict_zone');
117147

118-
$t->plan(6);
148+
$t->plan(7);
119149

120150
###############################################################################
121151

@@ -131,4 +161,7 @@ like($update_resp, qr/FILLED:/, 'evict update: zone filled');
131161
unlike($t->read_file('error.log'), qr/is already free/,
132162
'evict update: no double-free in error log');
133163

164+
like(http_get('/cross_slab_class'), qr/FILLED:/,
165+
'evict cross slab class: large alloc after small entries');
166+
134167
###############################################################################

0 commit comments

Comments
 (0)