Skip to content

Commit e3df64a

Browse files
committed
test_mail_stress: Add high-concurrency stress test for SMTP.
Add a test which sends 10,000 messages using a large number of concurrent SMTP clients.
1 parent 38cfc20 commit e3df64a

3 files changed

Lines changed: 188 additions & 6 deletions

File tree

.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ jobs:
119119
- name: Run unit tests
120120
run: |
121121
IGNORE_LIBBFD_MEMORY_LEAK_BUGS=1 scripts/run_tests.sh test_unit
122+
- name: Run mail stress tests
123+
run: |
124+
# Different failure modes/bottlenecks are tested with low debug vs high
125+
# This test with low BBS debug is also very quick (compared to w/ high debug)
126+
tests/test -ttest_mail_stress -dddddddddd -D -x
127+
# This test is very intensive so for efficiency, we don't run it
128+
# in the bottleneck test paths and instead run it at the end here
129+
NO_VALGRIND=1 scripts/run_tests.sh test_mail_stress
122130
debian-12:
123131
runs-on: ubuntu-24.04
124132
name: Debian 12

scripts/run_tests.sh

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set -e
44

55
# $1 = optional, specific test to run
66
# ENV[ASAN_RUN] = 1, to run tests for a build compiled with Address Sanitizer
7+
# ENV[NO_VALGRIND] = 1, to not run test(s) under valgrind
78
TEST=$1
89

910
# Execute all of the tests, and if a core is dumped,
@@ -77,21 +78,25 @@ install_valgrind() {
7778
if [ "$TEST" = "" ]; then # run all tests
7879
# First, do one pass without -e, in case there's a failure, it'll be caught much more quickly
7980
tests/test -dddddddddd -DDDDDDDDDD -x || handle_failure
80-
if [ "$ASAN_RUN" != "1" ]; then
81+
if [ "$ASAN_RUN" != "1" ] && [ "$NO_VALGRIND" != "1" ]; then
8182
# If all good so far, repeat but under valgrind
8283
install_valgrind
8384
tests/test -dddddddddd -DDDDDDDDDD -ex || handle_failure
8485
fi
8586
elif [ "$ASAN_RUN" = "1" ]; then
8687
tests/test -t$TEST -ddddddddd -DDDDDDDDDD -x || handle_failure
8788
else
88-
# If we are only running a specific test, don't bother with the first pass, just run directly with the -e option (valgrind)
89-
install_valgrind
90-
tests/test -t$TEST -ddddddddd -DDDDDDDDDD -ex || handle_failure
89+
if [ "$NO_VALGRIND" = "1" ]; then
90+
tests/test -t$TEST -ddddddddd -DDDDDDDDDD -x || handle_failure
91+
else
92+
# If we are only running a specific test, don't bother with the first pass, just run directly with the -e option (valgrind)
93+
install_valgrind
94+
tests/test -t$TEST -ddddddddd -DDDDDDDDDD -ex || handle_failure
95+
fi
9196
fi
9297

93-
if [ "$ASAN_RUN" = "1" ]; then
94-
exit 0 # Remainder of script not applicable to Address Sanitizer runs
98+
if [ "$ASAN_RUN" = "1" ] || [ "$NO_VALGRIND" = "1" ]; then
99+
exit 0 # Remainder of script not applicable to Address Sanitizer or valgrind-free runs
95100
fi
96101

97102
valgrind --version

tests/test_mail_stress.c

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* LBBS -- The Lightweight Bulletin Board System
3+
*
4+
* Copyright (C) 2026, Naveen Albert
5+
*
6+
* Naveen Albert <bbs@phreaknet.org>
7+
*
8+
* This program is free software, distributed under the terms of
9+
* the GNU General Public License Version 2. See the LICENSE file
10+
* at the top of the source tree.
11+
*/
12+
13+
/*! \file
14+
*
15+
* \brief Mail Stress Tests
16+
*
17+
* \author Naveen Albert <bbs@phreaknet.org>
18+
*/
19+
20+
#include "test.h"
21+
#include "email.h"
22+
23+
#include <stdlib.h>
24+
#include <stdio.h>
25+
#include <unistd.h>
26+
#include <string.h>
27+
#include <time.h>
28+
#include <sys/stat.h>
29+
#include <pthread.h>
30+
#include <sys/socket.h> /* use SOMAXCONN */
31+
#include <netinet/in.h> /* use sockaddr_in */
32+
33+
#define TARGET_MESSAGES 10000
34+
35+
/* Much more than this and *we* (the test program) will run out of file descriptors */
36+
#if SOMAXCONN >= 512
37+
#define NUM_SMTP_THREADS 512
38+
#else
39+
#define NUM_SMTP_THREADS SOMAXCONN
40+
#endif
41+
42+
#define LISTEN_BACKLOG NUM_SMTP_THREADS
43+
#define NUM_CHILD_FDS 2048
44+
45+
extern int listen_backlog;
46+
extern int child_max_fds;
47+
48+
static int pre(void)
49+
{
50+
test_preload_module("mod_mail.so");
51+
test_preload_module("mod_mimeparse.so");
52+
test_preload_module("net_smtp.so");
53+
test_load_module("mod_smtp_delivery_local.so");
54+
test_load_module("net_imap.so");
55+
56+
TEST_ADD_CONFIG("mod_mail.conf");
57+
TEST_ADD_CONFIG("net_smtp.conf");
58+
TEST_ADD_CONFIG("net_imap.conf");
59+
60+
if (!listen_backlog) {
61+
test_load_module("mod_mail_events.so");
62+
TEST_ADD_CONFIG("mod_mail_events.conf");
63+
}
64+
65+
TEST_RESET_MKDIR(TEST_MAIL_DIR);
66+
67+
listen_backlog = LISTEN_BACKLOG; /* Default in socket.c is 64, increase to ensure all connections can get handled immediately */
68+
child_max_fds = NUM_CHILD_FDS; /* Increase max file descriptors, otherwise we'll run out if NUM_SMTP_THREADS is much more than 256 */
69+
70+
return 0;
71+
}
72+
73+
static int smtp_index[NUM_SMTP_THREADS];
74+
static pthread_t smtp_threads[NUM_SMTP_THREADS];
75+
static int test_abort = 0;
76+
static int sent_messages = 0;
77+
78+
static void *send_thread(void *varg)
79+
{
80+
int i, fd = -1, lport;
81+
struct sockaddr_in sin;
82+
socklen_t slen = sizeof(sin);
83+
int sendcount = 0; /* Since we're multithreaded, each client needs its own send count */
84+
int index = *((int*) varg);
85+
86+
/* Establish SMTP connection */
87+
fd = test_make_socket(25);
88+
if (fd < 0) {
89+
test_abort = 1;
90+
return NULL;
91+
}
92+
93+
/* Figure out what port we're using locally for this connection */
94+
if (getsockname(fd, (struct sockaddr *) &sin, &slen)) {
95+
bbs_warning("getsockname failed: %s\n", strerror(errno));
96+
goto cleanup;
97+
}
98+
99+
/* Our local port allows us to correlate the client session to the handling server thread in the BBS */
100+
lport = ntohs(sin.sin_port);
101+
102+
/* Send our share of the messages */
103+
for (i = 0; i < TARGET_MESSAGES; i++) {
104+
if (i % NUM_SMTP_THREADS == index) {
105+
char sender[64];
106+
if (test_abort) {
107+
goto cleanup; /* As soon as one thread fails, all others should stop */
108+
}
109+
snprintf(sender, sizeof(sender), "external%d@" TEST_EXTERNAL_DOMAIN, i);
110+
if (test_send_message_full(fd, &sendcount, sender, TEST_EMAIL, 0)) {
111+
test_abort = 1;
112+
bbs_warning("Failed to send message %d from %s on client %d from port %d (%d sent so far)\n", i, sender, index, lport, sent_messages);
113+
goto cleanup;
114+
}
115+
bbs_atomic_fetchadd_int(&sent_messages, +1);
116+
}
117+
}
118+
119+
cleanup:
120+
close_if(fd);
121+
return NULL;
122+
}
123+
124+
static int run(void)
125+
{
126+
int i, res = -1;
127+
128+
/* Initialization */
129+
for (i = 0; i < NUM_SMTP_THREADS; i++) {
130+
smtp_threads[i] = 0;
131+
smtp_index[i] = i;
132+
}
133+
134+
/* First, send all the test messages */
135+
for (i = 0; i < NUM_SMTP_THREADS; i++) {
136+
int pres = pthread_create(&smtp_threads[i], NULL, send_thread, &smtp_index[i]);
137+
if (pres) {
138+
bbs_error("Failed to create thread: %s\n", strerror(pres));
139+
test_abort = 1;
140+
goto cleanup;
141+
}
142+
usleep(50);
143+
}
144+
145+
/* Join sending threads */
146+
for (i = 0; i < NUM_SMTP_THREADS; i++) {
147+
if (smtp_threads[i]) {
148+
pthread_join(smtp_threads[i], NULL);
149+
smtp_threads[i] = 0;
150+
}
151+
}
152+
153+
/* Verify that the email messages were all sent properly. */
154+
DIRECTORY_EXPECT_FILE_COUNT(TEST_MAIL_DIR "/1/new", TARGET_MESSAGES);
155+
156+
res = 0;
157+
158+
cleanup:
159+
for (i = 0; i < NUM_SMTP_THREADS; i++) {
160+
if (smtp_threads[i]) {
161+
pthread_join(smtp_threads[i], NULL);
162+
smtp_threads[i] = 0;
163+
}
164+
}
165+
return res;
166+
}
167+
168+
/* Do not run this test under valgrind, due to timeouts on initial 220 and final 250 */
169+
TEST_MODULE_INFO_STANDARD_FLAGS("Mail Stress Tests", TEST_FLAG_NO_AUTOLOAD | TEST_FLAG_NO_VALGRIND);

0 commit comments

Comments
 (0)