Skip to content

Commit 8cd8930

Browse files
committed
net_nntp: Add log for expired articles and allow interrupting expiration.
* Add articles to expire log when removed from history. * Treat articles no longer in the spool as "expired" when rebuilding history. * Abort expiration if unload is pending. * Run expiration in the background by default, so that net_nntp can be unloaded while expiration is running. The tests still run in the foreground for better synchronization (and since we're not expiring large numbers of articles in the tests).
1 parent 2bff540 commit 8cd8930

7 files changed

Lines changed: 130 additions & 12 deletions

File tree

nets/net_nntp.c

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ static int nntps_port = DEFAULT_NNTPS_PORT;
9090
static int nnsp_port = DEFAULT_NNSP_PORT;
9191

9292
static int nntp_enabled = 1, nntps_enabled = 1, nnsp_enabled = 1;
93-
int nntp_unloading = 0; /* Used extern by nntp_feed_nntp.c */
93+
int nntp_unloading = 0; /* Used extern by nntp_feed_nntp.c and nntp_history.c */
9494
void *thismodule; /* Used extern by nntp_suck.c */
9595

9696
static bbs_rwlock_t nntp_lock;
@@ -2153,17 +2153,52 @@ static int cli_delarticle(struct bbs_cli_args *a)
21532153
return 0;
21542154
}
21552155

2156+
static pthread_t expire_thread;
2157+
static int expire_done = 0;
2158+
2159+
static void *do_expiration(void *varg)
2160+
{
2161+
char *group = varg;
2162+
int res = history_expire(group); /* OK if group is NULL */
2163+
free_if(group);
2164+
if (res < 0) {
2165+
bbs_warning("Failed to expire articles\n");
2166+
} else {
2167+
bbs_verb(5, "Expired %d news article%s\n", res, ESS(res));
2168+
}
2169+
expire_done = 1;
2170+
return NULL;
2171+
}
2172+
21562173
static int cli_expire(struct bbs_cli_args *a)
21572174
{
21582175
const char *group = a->argv[2];
2159-
int res;
2176+
char *varg = group ? strdup(group) : NULL;
2177+
if (expire_thread) {
2178+
if (!expire_done) {
2179+
bbs_dprintf(a->fdout, "Expiration already in progress\n");
2180+
return -1;
2181+
}
2182+
bbs_pthread_join(expire_thread, NULL);
2183+
}
2184+
if (bbs_pthread_create(&expire_thread, NULL, do_expiration, varg)) {
2185+
free_if(varg);
2186+
bbs_dprintf(a->fdout, "Failed to expire articles\n");
2187+
return -1;
2188+
}
2189+
bbs_dprintf(a->fdout, "Started expiration task\n");
2190+
return 0;
2191+
}
21602192

2161-
res = history_expire(group); /* OK if group is NULL */
2193+
static int cli_fgexpire(struct bbs_cli_args *a)
2194+
{
2195+
const char *group = a->argv[2];
2196+
int res = history_expire(group); /* OK if group is NULL */
21622197
if (res < 0) {
21632198
bbs_dprintf(a->fdout, "Failed to expire articles\n");
21642199
return -1;
21652200
}
2166-
bbs_dprintf(a->fdout, "Expired %d article%s\n", res, ESS(res));
2201+
bbs_dprintf(a->fdout, "Expired %d news article%s\n", res, ESS(res));
21672202
return 0;
21682203
}
21692204

@@ -4815,6 +4850,7 @@ static struct bbs_cli_entry cli_commands_nntp[] = {
48154850
BBS_CLI_COMMAND(cli_setstatus, "news setstatus", 4, "Edit posting status for a newsgroup", "news setstatus <group> <y/n/m>"),
48164851
BBS_CLI_COMMAND(cli_delarticle, "news delarticle", 4, "Delete an article", "news delarticle <group> <article number>"),
48174852
BBS_CLI_COMMAND(cli_expire, "news expire", 2, "Remove expired articles from the spool (optionally just for one group)", "news expire <group>"),
4853+
BBS_CLI_COMMAND(cli_fgexpire, "news fgexpire", 2, "Remove expired articles from the spool (optionally just for one group), in foreground", "news expire <group>"),
48184854
BBS_CLI_COMMAND(cli_feedflush, "news feedflush", 2, "Flush queued articles for feed(s)", "news feedflush [<site>]"),
48194855
BBS_CLI_COMMAND(cli_feedstats, "news feedstats", 2, "Show outgoing feed stats", "news feedstats [<site>]"),
48204856
};
@@ -5135,6 +5171,9 @@ static void cleanup_subsystems(void)
51355171
{
51365172
active_cleanup();
51375173
spool_cleanup();
5174+
if (expire_thread) {
5175+
bbs_pthread_join(expire_thread, NULL);
5176+
}
51385177
history_cleanup();
51395178
}
51405179

nets/net_nntp/nntp.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,15 @@ int spool_article_create(struct article_groups *groups, struct article_info *art
513513

514514
int spool_article_delete_by_number(const char *groupname, int article_num);
515515

516+
/*!
517+
* \brief Whether a given article exists in a group
518+
* \param groupname
519+
* \param article_num
520+
* \retval 1 if article exists in the spool
521+
* \retval 0 if article doesn't exist in the spool
522+
*/
523+
int spool_article_exists(const char *groupname, int article_num);
524+
516525
/*!
517526
* \brief Whether any message with this Message-ID exists in any group
518527
* \param messageid Message-ID of article, if searching by message-ID

nets/net_nntp/nntp_history.c

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
/* #define DEBUG_EXPIRE */
2929

3030
extern int min_history;
31+
extern int nntp_unloading;
3132

3233
/* Each thread opens a group's overview file for reading/writing, so multiple readers can operate simultaneously */
3334
static bbs_mutex_t histlock;
@@ -150,6 +151,11 @@ void history_cleanup(void)
150151
fclose(histfp);
151152
histfp = NULL;
152153
}
154+
155+
/* Lock and unlock to ensure that all expire operations have finished */
156+
bbs_mutex_lock(&histlock);
157+
bbs_mutex_unlock(&histlock);
158+
153159
bbs_mutex_destroy(&histlock);
154160
RWLIST_WRLOCK_REMOVE_ALL(&retention_patterns, entry, free);
155161
}
@@ -261,12 +267,16 @@ static inline int should_expire_article(const char *group, time_t now, time_t ar
261267

262268
int history_expire(const char *pattern)
263269
{
264-
FILE *newfp;
270+
FILE *newfp, *expirefp;
265271
char buf[NNTP_BUFSIZ];
266272
int total_removed = 0, links_removed = 0;
267273
int line = 0;
274+
int error = 0;
268275
time_t history_cutoff, now;
269276
char template[TMPNAME_BUFSIZ];
277+
char expirelogfile[NNTP_MAX_PATH_LENGTH];
278+
279+
snprintf(expirelogfile, sizeof(expirelogfile), "%s/%s", bbs_log_dir(), "nntp_expire.log");
270280

271281
RWLIST_RDLOCK(&retention_patterns);
272282
if (!any_retention_patterns_remove_articles()) {
@@ -277,7 +287,12 @@ int history_expire(const char *pattern)
277287
RWLIST_UNLOCK(&retention_patterns); /* REQUIRE_HISTORY_FP returns, so we make sure we're unlocked there */
278288

279289
if (bbs_mutex_trylock(&histlock)) {
280-
bbs_warning("An expiration operation is already in-progress, rejecting concurrent expiration attempt\n");
290+
bbs_notice("An expiration operation is already in-progress, rejecting concurrent expiration attempt\n");
291+
return -1;
292+
}
293+
294+
if (nntp_unloading) {
295+
bbs_notice("Module unload is pending, not starting article expiration\n");
281296
return -1;
282297
}
283298

@@ -292,6 +307,16 @@ int history_expire(const char *pattern)
292307
history_cutoff = now - (86400 * min_history);
293308

294309
REQUIRE_HISTORY_FP(histfp);
310+
311+
expirefp = fopen(expirelogfile, "a");
312+
if (!expirefp) {
313+
bbs_error("Failed to append to %s: %s\n", expirelogfile, strerror(errno));
314+
fclose(newfp);
315+
unlink(template);
316+
bbs_mutex_unlock(&histlock);
317+
return -1;
318+
}
319+
295320
REWIND_HISTORY(histfp);
296321

297322
RWLIST_RDLOCK(&retention_patterns);
@@ -304,8 +329,11 @@ int history_expire(const char *pattern)
304329
char links[4 * NNTP_MAX_PATH_LENGTH] = "";
305330
char *linkspos = links;
306331
size_t linksleft = sizeof(links);
307-
char *msgid = strsep(&restofline, "\t");
332+
char *msgid;
333+
308334
line++;
335+
336+
msgid = strsep(&restofline, "\t");
309337
if (unlikely(!msgid)) {
310338
bbs_warning("History file %s corrupted (line %d)\n", history_file, line);
311339
continue;
@@ -350,7 +378,8 @@ int history_expire(const char *pattern)
350378

351379
if (should_expire_article(grp, now, arrival_time, expires)) {
352380
int res = expire_article(grp, artnum);
353-
if (!res) {
381+
/* If we successfully expired it, or the article is no longer present in the spool, treat as expired */
382+
if (!res || !spool_article_exists(grp, artnum)) {
354383
keep = 0;
355384
links_removed++;
356385
}
@@ -372,16 +401,42 @@ int history_expire(const char *pattern)
372401
/* Keep in history if article still exists in any groups, or if its age is less than min_history days (even if article may no longer be in any groups) */
373402
if (groups_kept || (min_history && (arrival_time > history_cutoff))) {
374403
fprintf(newfp, "%s\t%ld~%s~%s\t%s\n", msgid, arrival_time, expires_str, bytes_str, links);
404+
} else {
405+
/* If article is now being completely removed from history, add it to the expire log, the final vestige of this article's transient existence! */
406+
fprintf(expirefp, "%s\t%ld~%s~%s\n", msgid, arrival_time, expires_str, bytes_str);
375407
}
376408
if (!groups_kept || groups_removed) {
377409
EXPIRE_DEBUG(4, "%s %s (kept: %d, removed: %d)\n", !groups_kept && groups_removed > 0 ? "Permanently deleted" : groups_removed ? "Partially deleted" : "Keeping", msgid, groups_kept, groups_removed);
378410
}
411+
412+
/* If we are unloading, skip all further expiration checks and just copy the remainder of the original history file to the new one.
413+
* This way we can stop ~immediately if finishing expiration would take a long time. */
414+
if (nntp_unloading) {
415+
long startoffset, histsize;
416+
417+
bbs_debug(4, "Aborting remaining expiration checks\n");
418+
419+
if (!total_removed) {
420+
/* No changes have been made yet, we can just keep the original file */
421+
break;
422+
}
423+
424+
startoffset = ftell(histfp);
425+
fseek(histfp, 0, SEEK_END);
426+
histsize = ftell(histfp);
427+
fflush(newfp);
428+
if (bbs_copy_file(fileno(histfp), fileno(newfp), (int) startoffset, (int) (histsize - startoffset)) < 0) { /* Copy original headers, not including empty line */
429+
error = 1; /* If we failed to append the remainder of the file, don't discard the original history file */
430+
}
431+
break;
432+
}
379433
}
380434
RWLIST_UNLOCK(&retention_patterns);
381435

382436
/* If needed, swap in the new history file and update pointers */
437+
fclose(expirefp);
383438
fclose(newfp);
384-
if (total_removed > 0) {
439+
if (total_removed > 0 && !error) {
385440
bbs_verb(5, "Swapping in new history file (removed %d link%s, completely removed %d article%s)\n", links_removed, ESS(links_removed), total_removed, ESS(total_removed));
386441
fclose(histfp);
387442
if (rename(template, history_file)) {

nets/net_nntp/nntp_spool.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ int spool_article_delete_by_number(const char *groupname, int article_num)
8080
return tradspool_article_delete_by_number(groupname, article_num);
8181
}
8282

83+
int spool_article_exists(const char *groupname, int article_num)
84+
{
85+
return tradspool_article_exists(groupname, article_num);
86+
}
87+
8388
int spool_article_stat(struct nntp_session *nntp, const char *messageid, const char *groupname, int article_num)
8489
{
8590
return tradspool_article_stat(nntp, messageid, groupname, article_num);

nets/net_nntp/nntp_spool_trad.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,15 @@ int tradspool_article_delete_by_number(const char *groupname, int article_num)
10401040
return 0;
10411041
}
10421042

1043+
int tradspool_article_exists(const char *groupname, int article_num)
1044+
{
1045+
char artpath[NNTP_MAX_PATH_LENGTH];
1046+
if (!build_group_article_path(groupname, article_num, artpath, sizeof(artpath))) {
1047+
return 1;
1048+
}
1049+
return 0;
1050+
}
1051+
10431052
int tradspool_article_stat(struct nntp_session *nntp, const char *messageid, const char *groupname, int article_num)
10441053
{
10451054
if (messageid) {

nets/net_nntp/nntp_spool_trad.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ int tradspool_group_overview_header(struct nntp_session *nntp, const char *field
2929
int tradspool_overview_header_list(struct nntp_session *nntp, enum list_category listcat, const char *argument);
3030
int tradspool_article_create(struct article_groups *groups, struct article_info *artinfo, int srcfd, size_t len);
3131
int tradspool_article_delete_by_number(const char *groupname, int article_num);
32+
int tradspool_article_exists(const char *groupname, int article_num);
3233
int tradspool_article_stat(struct nntp_session *nntp, const char *messageid, const char *groupname, int article_num);
3334
int tradspool_article_send(struct nntp_session *nntp, enum article_part_filter filter, const char *messageid, const char *groupname, int article_num);

tests/test_nntp_transit.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ static int run(void)
266266
"Expires: Thu, 31 Dec 2099 23:59:59 +00:00\r\n"
267267
);
268268
GROUP_EXPECT(client1, "test.expires", 1, 3, 3);
269-
TEST_CLI_COMMAND("news expire");
269+
TEST_CLI_COMMAND("news fgexpire");
270270
GROUP_EXPECT(client1, "test.expires", EMPTY_LOW_WATERMARK(4), EMPTY_HIGH_WATERMARK(3), 0); /* All articles should have been removed */
271271

272272
/* Should refuse article that was deleted since it was recent enough */
@@ -278,14 +278,14 @@ static int run(void)
278278
"Expires: Thu, 31 Dec 2099 23:59:59 +00:00\r\n"
279279
);
280280
GROUP_EXPECT(client1, "test.expires2", 1, 3, 3);
281-
TEST_CLI_COMMAND("news expire");
281+
TEST_CLI_COMMAND("news fgexpire");
282282
GROUP_EXPECT(client1, "test.expires2", 1, 3, 3); /* No articles should have expired */
283283

284284
TAKETHIS_ADDITIONAL(peer1, "<expires2.4@" TEST_HOSTNAME ">", TEST_EMAIL_EXTERNAL, "test.expires2",
285285
"Expires: Fri, 31 Dec 1999 23:59:59 +00:00\r\n"
286286
);
287287
GROUP_EXPECT(client1, "test.expires2", 1, 4, 4);
288-
TEST_CLI_COMMAND("news expire");
288+
TEST_CLI_COMMAND("news fgexpire");
289289
GROUP_EXPECT(client1, "test.expires2", 1, 3, 3); /* Article 4 was deleted due to its Expires header being in the past */
290290

291291
SWRITE(client1, "ARTICLE <expires2.2@" TEST_HOSTNAME ">\r\n");

0 commit comments

Comments
 (0)