diff --git a/wallet/invoices.c b/wallet/invoices.c index 8a1ebac7b..d6d7f05b5 100644 --- a/wallet/invoices.c +++ b/wallet/invoices.c @@ -41,6 +41,10 @@ struct invoices { struct list_head invlist; /* Waiters waiting for invoices to be paid, expired, or deleted. */ struct list_head waiters; + /* Earliest time for some invoice to expire */ + u64 min_expiry_time; + /* Expiration timer */ + struct oneshot *expiration_timer; }; static void trigger_invoice_waiter(struct invoice_waiter *w, @@ -118,8 +122,6 @@ static bool wallet_stmt2invoice_details(sqlite3_stmt *stmt, dtl->paid_timestamp = sqlite3_column_int64(stmt, 9); } - invoice->expiration_timer = NULL; - return true; } @@ -137,83 +139,15 @@ struct invoices *invoices_new(const tal_t *ctx, list_head_init(&invs->invlist); list_head_init(&invs->waiters); + invs->expiration_timer = NULL; + return invs; } -static void install_expiration_timer(struct invoices *invoices, - struct invoice *i); -static void trigger_expiration(struct invoice *i) +/* Update expirations. */ +static void update_db_expirations(struct invoices *invoices, u64 now) { - struct invoices *invoices = i->owner; - u64 now = time_now().ts.tv_sec; sqlite3_stmt *stmt; - - assert(i->details->state == UNPAID); - - /* Timer already triggered, destroy the timer object. */ - i->expiration_timer = tal_free(i->expiration_timer); - - /* There may be discrepancies between time_mono - * (used by the timer system) and time_now (used - * by the expiry time measurements). So check that - * time_now is reached. */ - if (i->details->expiry_time <= now) { - const tal_t *tmpctx = tal_tmpctx(i); - - /* Update in-memory and db. */ - i->details->state = EXPIRED; - stmt = db_prepare(invoices->db, - "UPDATE invoices" - " SET state = ?" - " WHERE id = ?;"); - sqlite3_bind_int(stmt, 1, EXPIRED); - sqlite3_bind_int64(stmt, 2, i->id); - db_exec_prepared(invoices->db, stmt); - /* Wake up all waiters. */ - trigger_invoice_waiter_expire_or_delete(invoices, i->id, i); - - tal_free(tmpctx); - } else - install_expiration_timer(invoices, i); - -} -static void install_expiration_timer(struct invoices *invoices, - struct invoice *i) -{ - struct timerel rel; - struct timeabs expiry; - struct timeabs now = time_now(); - - assert(i->details->state == UNPAID); - assert(!i->expiration_timer); - - memset(&expiry, 0, sizeof(expiry)); - expiry.ts.tv_sec = i->details->expiry_time; - - /* now > expiry */ - if (time_after(now, expiry)) - expiry = now; - - /* rel = expiry - now */ - rel = time_between(expiry, now); - - /* The oneshot is parented on the invoice. Thus if - * the invoice is deleted, the oneshot is destroyed - * also and this removes the timer. */ - i->expiration_timer = new_reltimer(invoices->timers, - i, - rel, - &trigger_expiration, i); -} - -bool invoices_load(struct invoices *invoices) -{ - int count = 0; - u64 now = time_now().ts.tv_sec; - struct invoice *i; - sqlite3_stmt *stmt; - - /* Update expirations. */ stmt = db_prepare(invoices->db, "UPDATE invoices" " SET state = ?" @@ -223,6 +157,126 @@ bool invoices_load(struct invoices *invoices) sqlite3_bind_int(stmt, 2, UNPAID); sqlite3_bind_int64(stmt, 3, now); db_exec_prepared(invoices->db, stmt); +} + +static struct invoice *invoices_find_by_id(struct invoices *invoices, + u64 id) +{ + struct invoice *i; + + /* FIXME: Use something better than a linear scan. */ + list_for_each(&invoices->invlist, i, list) { + if (i->id == id) + return i; + } + return NULL; +} + +struct invoice_id_node { + struct list_node list; + u64 id; +}; + +static void install_expiration_timer(struct invoices *invoices); +static void trigger_expiration(struct invoices *invoices) +{ + const tal_t *tmpctx = tal_tmpctx(invoices); + struct list_head idlist; + struct invoice_id_node *idn; + u64 now = time_now().ts.tv_sec; + sqlite3_stmt *stmt; + struct invoice *i; + + /* Free current expiration timer */ + invoices->expiration_timer = tal_free(invoices->expiration_timer); + + /* Acquire all expired invoices and save them in a list */ + list_head_init(&idlist); + stmt = db_prepare(invoices->db, + "SELECT id" + " FROM invoices" + " WHERE state = ?" + " AND expiry_time <= ?;"); + sqlite3_bind_int(stmt, 1, UNPAID); + sqlite3_bind_int64(stmt, 2, now); + while (sqlite3_step(stmt) == SQLITE_ROW) { + idn = tal(tmpctx, struct invoice_id_node); + list_add_tail(&idlist, &idn->list); + idn->id = sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + + /* Expire all those invoices */ + update_db_expirations(invoices, now); + + /* Trigger expirations */ + list_for_each(&idlist, idn, list) { + /* Update in-memory structure */ + i = invoices_find_by_id(invoices, idn->id); + i->details->state = EXPIRED; + /* Trigger expiration */ + trigger_invoice_waiter_expire_or_delete(invoices, + idn->id, + i); + } + + install_expiration_timer(invoices); + + tal_free(tmpctx); +} + +static void install_expiration_timer(struct invoices *invoices) +{ + int res; + sqlite3_stmt *stmt; + struct timerel rel; + struct timeabs expiry; + struct timeabs now = time_now(); + + assert(!invoices->expiration_timer); + + /* Find unpaid invoice with nearest expiry time */ + stmt = db_prepare(invoices->db, + "SELECT MIN(expiry_time)" + " FROM invoices" + " WHERE state = ?;"); + sqlite3_bind_int(stmt, 1, UNPAID); + res = sqlite3_step(stmt); + assert(res == SQLITE_ROW); + if (sqlite3_column_type(stmt, 0) == SQLITE_NULL) { + /* Nothing to install */ + sqlite3_finalize(stmt); + return; + } else + invoices->min_expiry_time = sqlite3_column_int64(stmt, 0); + sqlite3_finalize(stmt); + + memset(&expiry, 0, sizeof(expiry)); + expiry.ts.tv_sec = invoices->min_expiry_time; + + /* now > expiry */ + if (time_after(now, expiry)) + expiry = now; + + /* rel = expiry - now */ + rel = time_between(expiry, now); + + /* Have it called at indicated timerel. */ + invoices->expiration_timer = new_reltimer(invoices->timers, + invoices, + rel, + &trigger_expiration, + invoices); +} + +bool invoices_load(struct invoices *invoices) +{ + int count = 0; + u64 now = time_now().ts.tv_sec; + struct invoice *i; + sqlite3_stmt *stmt; + + update_db_expirations(invoices, now); /* Load invoices from db. */ stmt = db_query(__func__, invoices->db, @@ -244,8 +298,6 @@ bool invoices_load(struct invoices *invoices) sqlite3_finalize(stmt); return false; } - if (i->details->state == UNPAID) - install_expiration_timer(invoices, i); list_add_tail(&invoices->invlist, &i->list); count++; } @@ -253,6 +305,8 @@ bool invoices_load(struct invoices *invoices) sqlite3_finalize(stmt); + install_expiration_timer(invoices); + return true; } @@ -321,13 +375,17 @@ const struct invoice *invoices_create(struct invoices *invoices, memcpy(&invoice->details->r, &r, sizeof(invoice->details->r)); memcpy(&invoice->details->rhash, &rhash, sizeof(invoice->details->rhash)); invoice->details->expiry_time = expiry_time; - invoice->expiration_timer = NULL; /* Add to invoices object. */ list_add_tail(&invoices->invlist, &invoice->list); /* Install expiration trigger. */ - install_expiration_timer(invoices, invoice); + if (!invoices->expiration_timer || + expiry_time < invoices->min_expiry_time) { + invoices->expiration_timer + = tal_free(invoices->expiration_timer); + install_expiration_timer(invoices); + } return invoice; } @@ -443,7 +501,6 @@ void invoices_resolve(struct invoices *invoices, invoice->details->pay_index = pay_index; invoice->details->msatoshi_received = msatoshi_received; invoice->details->paid_timestamp = paid_timestamp; - invoice->expiration_timer = tal_free(invoice->expiration_timer); /* Tell all the waiters about the paid invoice. */ trigger_invoice_waiter_resolve(invoices, invoice->id, invoice); diff --git a/wallet/wallet.h b/wallet/wallet.h index 7bd4c39e3..02f64b6c7 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -397,8 +397,6 @@ struct invoice { struct list_node list; /* Database ID */ u64 id; - /* Any expiration timer in effect */ - struct oneshot *expiration_timer; /* The owning invoices object. */ struct invoices *owner; /* Loaded details. */