mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-22 06:41:44 +01:00
autoclean: call list in easy stages.
listforwards on a large node can easily run out of memory. Sip, don't gulp! Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
c62e1f432f
commit
ead211e5e4
2 changed files with 132 additions and 14 deletions
|
@ -13,6 +13,7 @@ static struct clean_info *timer_cinfo;
|
||||||
static struct plugin *plugin;
|
static struct plugin *plugin;
|
||||||
/* This is NULL if it's running now. */
|
/* This is NULL if it's running now. */
|
||||||
static struct plugin_timer *cleantimer;
|
static struct plugin_timer *cleantimer;
|
||||||
|
static u64 max_entries_per_call = 10000;
|
||||||
|
|
||||||
enum subsystem_type {
|
enum subsystem_type {
|
||||||
FORWARDS,
|
FORWARDS,
|
||||||
|
@ -34,8 +35,8 @@ struct subsystem_ops {
|
||||||
/* "success" and "failure" names for JSON formatting. */
|
/* "success" and "failure" names for JSON formatting. */
|
||||||
const char *names[NUM_SUBSYSTEM_VARIANTS];
|
const char *names[NUM_SUBSYSTEM_VARIANTS];
|
||||||
|
|
||||||
/* name of "list" command */
|
/* name of system for wait and "list" */
|
||||||
const char *list_command;
|
const char *system_name;
|
||||||
|
|
||||||
/* Name of array inside "list" command return */
|
/* Name of array inside "list" command return */
|
||||||
const char *arr_name;
|
const char *arr_name;
|
||||||
|
@ -89,7 +90,7 @@ static void add_forward_del_fields(struct out_req *req,
|
||||||
|
|
||||||
static const struct subsystem_ops subsystem_ops[NUM_SUBSYSTEM_TYPES] = {
|
static const struct subsystem_ops subsystem_ops[NUM_SUBSYSTEM_TYPES] = {
|
||||||
{ {"succeededforwards", "failedforwards"},
|
{ {"succeededforwards", "failedforwards"},
|
||||||
"listforwards",
|
"forwards",
|
||||||
"forwards",
|
"forwards",
|
||||||
"\"in_channel\":true,\"in_htlc_id\":true,\"resolved_time\":true,\"received_time\":true,\"status\":true",
|
"\"in_channel\":true,\"in_htlc_id\":true,\"resolved_time\":true,\"received_time\":true,\"status\":true",
|
||||||
"delforward",
|
"delforward",
|
||||||
|
@ -97,7 +98,7 @@ static const struct subsystem_ops subsystem_ops[NUM_SUBSYSTEM_TYPES] = {
|
||||||
add_forward_del_fields,
|
add_forward_del_fields,
|
||||||
},
|
},
|
||||||
{ {"succeededpays", "failedpays"},
|
{ {"succeededpays", "failedpays"},
|
||||||
"listsendpays",
|
"sendpays",
|
||||||
"payments",
|
"payments",
|
||||||
"\"created_at\":true,\"status\":true,\"payment_hash\":true,\"groupid\":true,\"partid\":true",
|
"\"created_at\":true,\"status\":true,\"payment_hash\":true,\"groupid\":true,\"partid\":true",
|
||||||
"delpay",
|
"delpay",
|
||||||
|
@ -105,7 +106,7 @@ static const struct subsystem_ops subsystem_ops[NUM_SUBSYSTEM_TYPES] = {
|
||||||
add_sendpays_del_fields,
|
add_sendpays_del_fields,
|
||||||
},
|
},
|
||||||
{ {"paidinvoices", "expiredinvoices"},
|
{ {"paidinvoices", "expiredinvoices"},
|
||||||
"listinvoices",
|
"invoices",
|
||||||
"invoices",
|
"invoices",
|
||||||
"\"label\":true,\"status\":true,\"expires_at\":true,\"paid_at\":true",
|
"\"label\":true,\"status\":true,\"expires_at\":true,\"paid_at\":true",
|
||||||
"delinvoice",
|
"delinvoice",
|
||||||
|
@ -170,6 +171,10 @@ struct per_subsystem {
|
||||||
struct clean_info *cinfo;
|
struct clean_info *cinfo;
|
||||||
enum subsystem_type type;
|
enum subsystem_type type;
|
||||||
|
|
||||||
|
/* How far are we through the listing? */
|
||||||
|
u64 offset, max;
|
||||||
|
|
||||||
|
/* How many did we ignore? */
|
||||||
u64 num_uncleaned;
|
u64 num_uncleaned;
|
||||||
struct per_variant variants[NUM_SUBSYSTEM_VARIANTS];
|
struct per_variant variants[NUM_SUBSYSTEM_VARIANTS];
|
||||||
};
|
};
|
||||||
|
@ -202,6 +207,7 @@ static const struct subsystem_ops *get_subsystem_ops(const struct per_subsystem
|
||||||
|
|
||||||
/* Mutual recursion */
|
/* Mutual recursion */
|
||||||
static void do_clean_timer(void *unused);
|
static void do_clean_timer(void *unused);
|
||||||
|
static struct command_result *do_clean(struct clean_info *cinfo);
|
||||||
|
|
||||||
static struct clean_info *new_clean_info(const tal_t *ctx,
|
static struct clean_info *new_clean_info(const tal_t *ctx,
|
||||||
struct command *cmd)
|
struct command *cmd)
|
||||||
|
@ -214,7 +220,6 @@ static struct clean_info *new_clean_info(const tal_t *ctx,
|
||||||
struct per_subsystem *ps = &cinfo->per_subsystem[i];
|
struct per_subsystem *ps = &cinfo->per_subsystem[i];
|
||||||
ps->cinfo = cinfo;
|
ps->cinfo = cinfo;
|
||||||
ps->type = i;
|
ps->type = i;
|
||||||
ps->num_uncleaned = 0;
|
|
||||||
|
|
||||||
for (enum subsystem_variant j = 0; j < NUM_SUBSYSTEM_VARIANTS; j++) {
|
for (enum subsystem_variant j = 0; j < NUM_SUBSYSTEM_VARIANTS; j++) {
|
||||||
struct per_variant *pv = &ps->variants[j];
|
struct per_variant *pv = &ps->variants[j];
|
||||||
|
@ -294,7 +299,8 @@ static struct command_result *clean_finished_one(struct clean_info *cinfo)
|
||||||
if (--cinfo->cleanup_reqs_remaining > 0)
|
if (--cinfo->cleanup_reqs_remaining > 0)
|
||||||
return command_still_pending(cinfo->cmd);
|
return command_still_pending(cinfo->cmd);
|
||||||
|
|
||||||
return clean_finished(cinfo);
|
/* See if there are more entries we need to list. */
|
||||||
|
return do_clean(cinfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct command_result *del_done(struct command *cmd,
|
static struct command_result *del_done(struct command *cmd,
|
||||||
|
@ -508,6 +514,7 @@ static struct command_result *list_done(struct command *cmd,
|
||||||
send_outreq(plugin, req);
|
send_outreq(plugin, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subsystem->offset += max_entries_per_call;
|
||||||
return clean_finished_one(subsystem->cinfo);
|
return clean_finished_one(subsystem->cinfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,8 +523,8 @@ static struct command_result *list_failed(struct command *cmd,
|
||||||
const jsmntok_t *result,
|
const jsmntok_t *result,
|
||||||
struct per_subsystem *subsystem)
|
struct per_subsystem *subsystem)
|
||||||
{
|
{
|
||||||
plugin_err(plugin, "Failed '%s': '%.*s'",
|
plugin_err(plugin, "Failed 'list%s': '%.*s'",
|
||||||
get_subsystem_ops(subsystem)->list_command,
|
get_subsystem_ops(subsystem)->system_name,
|
||||||
json_tok_full_len(result),
|
json_tok_full_len(result),
|
||||||
json_tok_full(buf, result));
|
json_tok_full(buf, result));
|
||||||
}
|
}
|
||||||
|
@ -532,9 +539,7 @@ static struct command_result *do_clean(struct clean_info *cinfo)
|
||||||
const char *filter;
|
const char *filter;
|
||||||
const struct subsystem_ops *ops = get_subsystem_ops(ps);
|
const struct subsystem_ops *ops = get_subsystem_ops(ps);
|
||||||
|
|
||||||
ps->num_uncleaned = 0;
|
|
||||||
for (size_t j = 0; j < NUM_SUBSYSTEM_VARIANTS; j++) {
|
for (size_t j = 0; j < NUM_SUBSYSTEM_VARIANTS; j++) {
|
||||||
ps->variants[j].num_cleaned = 0;
|
|
||||||
if (ps->variants[j].age)
|
if (ps->variants[j].age)
|
||||||
have_variant = true;
|
have_variant = true;
|
||||||
}
|
}
|
||||||
|
@ -543,13 +548,24 @@ static struct command_result *do_clean(struct clean_info *cinfo)
|
||||||
if (!have_variant)
|
if (!have_variant)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* Don't bother if we're past the end already. */
|
||||||
|
if (ps->offset >= ps->max)
|
||||||
|
continue;
|
||||||
|
|
||||||
filter = tal_fmt(tmpctx, "{\"%s\":[{%s}]}",
|
filter = tal_fmt(tmpctx, "{\"%s\":[{%s}]}",
|
||||||
ops->arr_name, ops->list_filter);
|
ops->arr_name, ops->list_filter);
|
||||||
req = jsonrpc_request_with_filter_start(plugin, NULL,
|
req = jsonrpc_request_with_filter_start(plugin, NULL,
|
||||||
ops->list_command,
|
tal_fmt(tmpctx,
|
||||||
|
"list%s",
|
||||||
|
ops->system_name),
|
||||||
filter,
|
filter,
|
||||||
list_done, list_failed,
|
list_done, list_failed,
|
||||||
ps);
|
ps);
|
||||||
|
/* Don't overwhelm lightningd or us if there are millions of
|
||||||
|
* entries! */
|
||||||
|
json_add_string(req->js, "index", "created");
|
||||||
|
json_add_u64(req->js, "start", ps->offset);
|
||||||
|
json_add_u64(req->js, "limit", max_entries_per_call);
|
||||||
send_outreq(plugin, req);
|
send_outreq(plugin, req);
|
||||||
cinfo->cleanup_reqs_remaining++;
|
cinfo->cleanup_reqs_remaining++;
|
||||||
}
|
}
|
||||||
|
@ -559,12 +575,77 @@ static struct command_result *do_clean(struct clean_info *cinfo)
|
||||||
return clean_finished(cinfo);
|
return clean_finished(cinfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static struct command_result *wait_done(struct command *cmd,
|
||||||
|
const char *buf,
|
||||||
|
const jsmntok_t *result,
|
||||||
|
struct per_subsystem *ps)
|
||||||
|
{
|
||||||
|
const char *err;
|
||||||
|
|
||||||
|
err = json_scan(tmpctx, buf, result, "{created:%}",
|
||||||
|
JSON_SCAN(json_to_u64, &ps->max));
|
||||||
|
if (err)
|
||||||
|
plugin_err(plugin, "Failed parsing wait response: (%s): '%.*s'",
|
||||||
|
err,
|
||||||
|
json_tok_full_len(result),
|
||||||
|
json_tok_full(buf, result));
|
||||||
|
|
||||||
|
/* We do three of these, make sure they're all complete. */
|
||||||
|
assert(ps->cinfo->cleanup_reqs_remaining != 0);
|
||||||
|
if (--ps->cinfo->cleanup_reqs_remaining > 0)
|
||||||
|
return command_still_pending(ps->cinfo->cmd);
|
||||||
|
|
||||||
|
return do_clean(ps->cinfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct command_result *wait_failed(struct command *cmd,
|
||||||
|
const char *buf,
|
||||||
|
const jsmntok_t *result,
|
||||||
|
struct per_subsystem *subsystem)
|
||||||
|
{
|
||||||
|
plugin_err(plugin, "Failed wait '%s': '%.*s'",
|
||||||
|
get_subsystem_ops(subsystem)->system_name,
|
||||||
|
json_tok_full_len(result),
|
||||||
|
json_tok_full(buf, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct command_result *start_clean(struct clean_info *cinfo)
|
||||||
|
{
|
||||||
|
cinfo->cleanup_reqs_remaining = 0;
|
||||||
|
|
||||||
|
/* We have to get max indexes first. */
|
||||||
|
for (size_t i = 0; i < NUM_SUBSYSTEM_TYPES; i++) {
|
||||||
|
struct per_subsystem *ps = &cinfo->per_subsystem[i];
|
||||||
|
const struct subsystem_ops *ops = get_subsystem_ops(ps);
|
||||||
|
struct out_req *req;
|
||||||
|
|
||||||
|
/* Reset counters while we're here */
|
||||||
|
ps->num_uncleaned = 0;
|
||||||
|
for (enum subsystem_variant j = 0; j < NUM_SUBSYSTEM_VARIANTS; j++) {
|
||||||
|
struct per_variant *pv = &ps->variants[j];
|
||||||
|
pv->num_cleaned = 0;
|
||||||
|
}
|
||||||
|
ps->offset = 0;
|
||||||
|
|
||||||
|
req = jsonrpc_request_start(plugin, NULL,
|
||||||
|
"wait",
|
||||||
|
wait_done, wait_failed, ps);
|
||||||
|
json_add_string(req->js, "subsystem", ops->system_name);
|
||||||
|
json_add_string(req->js, "indexname", "created");
|
||||||
|
json_add_u64(req->js, "nextvalue", 0);
|
||||||
|
send_outreq(plugin, req);
|
||||||
|
cinfo->cleanup_reqs_remaining++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command_still_pending(cinfo->cmd);
|
||||||
|
}
|
||||||
|
|
||||||
/* Needs a different signature than do_clean */
|
/* Needs a different signature than do_clean */
|
||||||
static void do_clean_timer(void *unused)
|
static void do_clean_timer(void *unused)
|
||||||
{
|
{
|
||||||
assert(timer_cinfo->cleanup_reqs_remaining == 0);
|
assert(timer_cinfo->cleanup_reqs_remaining == 0);
|
||||||
cleantimer = NULL;
|
cleantimer = NULL;
|
||||||
do_clean(timer_cinfo);
|
start_clean(timer_cinfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct command_result *param_subsystem(struct command *cmd,
|
static struct command_result *param_subsystem(struct command *cmd,
|
||||||
|
@ -650,7 +731,7 @@ static struct command_result *json_autoclean_once(struct command *cmd,
|
||||||
cinfo = new_clean_info(cmd, cmd);
|
cinfo = new_clean_info(cmd, cmd);
|
||||||
get_per_variant(cinfo, sv)->age = *age;
|
get_per_variant(cinfo, sv)->age = *age;
|
||||||
|
|
||||||
return do_clean(cinfo);
|
return start_clean(cinfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void memleak_mark_timer_cinfo(struct plugin *plugin,
|
static void memleak_mark_timer_cinfo(struct plugin *plugin,
|
||||||
|
@ -768,5 +849,10 @@ int main(int argc, char *argv[])
|
||||||
"How old do expired invoices have to be before deletion (0 = never)",
|
"How old do expired invoices have to be before deletion (0 = never)",
|
||||||
u64_option, u64_jsonfmt_unless_zero,
|
u64_option, u64_jsonfmt_unless_zero,
|
||||||
&timer_cinfo->per_subsystem[INVOICES].variants[FAILURE].age),
|
&timer_cinfo->per_subsystem[INVOICES].variants[FAILURE].age),
|
||||||
|
plugin_option_dev_dynamic("dev-autoclean-max-batch",
|
||||||
|
"int",
|
||||||
|
"Maximum cleans to do at a time",
|
||||||
|
u64_option, u64_jsonfmt,
|
||||||
|
&max_entries_per_call),
|
||||||
NULL);
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4352,3 +4352,35 @@ def test_plugin_startdir_lol(node_factory):
|
||||||
"""Though we fail to start many of them, we don't crash!"""
|
"""Though we fail to start many of them, we don't crash!"""
|
||||||
l1 = node_factory.get_node(broken_log='.*')
|
l1 = node_factory.get_node(broken_log='.*')
|
||||||
l1.rpc.plugin_startdir(os.path.join(os.getcwd(), 'tests/plugins'))
|
l1.rpc.plugin_startdir(os.path.join(os.getcwd(), 'tests/plugins'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_autoclean_batch(node_factory):
|
||||||
|
l1 = node_factory.get_node(1)
|
||||||
|
|
||||||
|
# Many expired invoices
|
||||||
|
for i in range(100):
|
||||||
|
l1.rpc.invoice(amount_msat=12300, label=f'inv1{i}', description='description', expiry=1)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
l1.rpc.setconfig('dev-autoclean-max-batch', 2)
|
||||||
|
|
||||||
|
# Test manual clean
|
||||||
|
ret = l1.rpc.autoclean_once('expiredinvoices', 1)
|
||||||
|
assert ret == {'autoclean': {'expiredinvoices': {'cleaned': 100, 'uncleaned': 0}}}
|
||||||
|
|
||||||
|
for i in range(100):
|
||||||
|
l1.rpc.invoice(amount_msat=12300, label=f'inv2{i}', description='description', expiry=1)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Test cycle clean
|
||||||
|
assert (l1.rpc.autoclean_status('expiredinvoices')
|
||||||
|
== {'autoclean': {'expiredinvoices': {'enabled': False, 'cleaned': 100}}})
|
||||||
|
|
||||||
|
l1.rpc.setconfig('autoclean-expiredinvoices-age', 2)
|
||||||
|
assert (l1.rpc.autoclean_status('expiredinvoices')
|
||||||
|
== {'autoclean': {'expiredinvoices': {'enabled': True, 'cleaned': 100, 'age': 2}}})
|
||||||
|
|
||||||
|
l1.rpc.setconfig('autoclean-cycle', 5)
|
||||||
|
wait_for(lambda: l1.rpc.autoclean_status('expiredinvoices')
|
||||||
|
== {'autoclean': {'expiredinvoices': {'enabled': True, 'cleaned': 200, 'age': 2}}})
|
||||||
|
|
Loading…
Add table
Reference in a new issue