-// unfinished load testing utility for Citadel Server
+// Load testing utility for Citadel Server
//
-// Copyright (c) 1987-2023 by the citadel.org team
+// Copyright (c) 1987-2024 by the citadel.org team
//
// This program is open source software. Use, duplication, or disclosure
// is subject to the terms of the GNU General Public License, version 3.
#include <limits.h>
#include <sys/socket.h>
#include <sys/un.h>
+#include <pthread.h>
#include "../server/citadel_defs.h"
#include "../server/server.h"
#include "../server/citadel_dirs.h"
#include <libcitadel.h>
+char ctdldir[PATH_MAX]=CTDLDIR;
+
char *words[] = {
"lorem","ipsum","dolor","sit","amet","consectetuer","adipiscing","elit","integer","in","mi","a","mauris",
"ornare","sagittis","suspendisse","potenti","suspendisse","dapibus","dignissim","dolor","nam",
"tempor","tincidunt","phasellus","justo","purus","pharetra","ut","ultricies","nec","consequat","vel",
"nisi","fusce","vitae","velit","at","libero","sollicitudin","sodales","aenean","mi","libero","ultrices",
"id","suscipit","vitae","dapibus","eu","metus","aenean","vestibulum","nibh","ac","massa","vivamus",
- "vestibulum","libero","vitae","purus","in","hac","habitasse","platea","dictumst","curabitur",
+ "vestibulum","libero","vitae","purus","in","hac","maga","habitasse","platea","dictumst","curabitur",
"blandit","nunc","non","arcu","ut","nec","nibh","morbi","quis","leo","vel","magna","commodo","rhoncus",
"donec","congue","leo","eu","lacus","pellentesque","at","erat","id","mi","consequat","congue","praesent",
"a","nisl","ut","diam","interdum","molestie","fusce","suscipit","rhoncus","sem","donec","pretium",
};
int nwords = sizeof(words) / sizeof(char *);
-int serv_sock = (-1);
int uds_connectsock(char *sockpath) {
int s;
s = socket(AF_UNIX, SOCK_STREAM, 0);
if (s < 0) {
- fprintf(stderr, "sendcommand: Can't create socket: %s\n", strerror(errno));
- exit(3);
+ return(-1);
}
if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
- fprintf(stderr, "sendcommand: can't connect: %s\n", strerror(errno));
close(s);
- exit(3);
+ return(-1);
}
- return s;
+ return(s);
}
// input binary data from socket
-void serv_read(char *buf, int bytes) {
+void serv_read(int serv_sock, char *buf, int bytes) {
int len, rlen;
len = 0;
// send binary to server
-void serv_write(char *buf, int nbytes) {
+void serv_write(int serv_sock, char *buf, int nbytes) {
int bytes_written = 0;
int retval;
while (bytes_written < nbytes) {
// input string from socket - implemented in terms of serv_read()
-void serv_gets(char *buf) {
+void serv_gets(int serv_sock, char *buf) {
int i;
// Read one character at a time.
for (i = 0;; i++) {
- serv_read(&buf[i], 1);
+ serv_read(serv_sock, &buf[i], 1);
if (buf[i] == '\n' || i == (SIZ-1))
break;
}
// If we got a long line, discard characters until the newline.
if (i == (SIZ-1)) {
while (buf[i] != '\n') {
- serv_read(&buf[i], 1);
+ serv_read(serv_sock, &buf[i], 1);
}
}
// send line to server - implemented in terms of serv_write()
-void serv_puts(char *buf) {
- serv_write(buf, strlen(buf));
- serv_write("\n", 1);
+void serv_puts(int serv_sock, char *buf) {
+ serv_write(serv_sock, buf, strlen(buf));
+ serv_write(serv_sock, "\n", 1);
}
-
char *random_rooms[] = {
"Load Testing Test Room One",
"Load Test 2: Electric Boogaloo",
"Five Guys Load Testing and Fries"
};
int nrooms = sizeof(random_rooms) / sizeof(char *);
-
char *test_user = "Load Test User";
+char test_pass[16];
+time_t time_started;
+volatile int ops_completed;
-void perform_random_thing(void) {
+
+// These are our randomized load test operations: an even mix of changing rooms, posting messages, and deleting messages.
+void perform_random_thing(int serv_sock) {
int op = random() % 3;
char buf[SIZ];
int i;
// Random operation 0 : change rooms
if (op == 0) {
snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[random() % nrooms]);
- serv_puts(buf);
- serv_gets(buf);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
}
// Random operation 1 : post a message
if (op == 1) {
- serv_puts("ENT0 1");
- serv_gets(buf);
+ serv_puts(serv_sock, "ENT0 1");
+ serv_gets(serv_sock, buf);
if (buf[0] == '4') {
- bigness = random() % 10;
+ bigness = random() % 500;
strcpy(buf, "");
for (i=0; i<bigness; ++i) {
strcat(buf, words[random() % nwords]);
if ( (i != 0) && ((i % 10) == 0) ) {
- serv_puts(buf);
+ serv_puts(serv_sock, buf);
strcpy(buf, "");
}
else {
strcat(buf, " ");
}
}
- serv_puts(buf);
- serv_puts("000");
+ serv_puts(serv_sock, buf);
+ serv_puts(serv_sock, "000");
}
}
selected_msg = 0;
do {
- serv_puts("MSGS ALL");
- serv_gets(buf);
+ serv_puts(serv_sock, "MSGS ALL");
+ serv_gets(serv_sock, buf);
if (buf[0] == '1') {
- while (serv_gets(buf), strcmp(buf, "000")) {
+ while (serv_gets(serv_sock, buf), strcmp(buf, "000")) {
++total_msgs;
if ((random() % total_msgs) == 0) {
selected_msg = atol(buf);
}
}
snprintf(buf, sizeof buf, "DELE %ld", selected_msg);
- serv_puts(buf);
- serv_gets(buf);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
} while ( (buf[0] != '2') && (total_msgs > 0));
}
-
}
+#define ROW_OFFSET 8
-void do_stuff(void) {
- int i;
+// This is the main loop. We log in as the load test user, and then perform random operations until stopped.
+void *loadtest(void *pointer_to_thread_id) {
char buf[SIZ];
+ int serv_sock;
- printf("Creating test user '%s'\n", test_user);
- snprintf(buf, sizeof buf, "CREU %s", test_user);
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
- snprintf(buf, sizeof buf, "ASUP %s|640k_enough_ne1|0|||6|", test_user);
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
- snprintf(buf, sizeof buf, "USER %s", test_user);
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
- snprintf(buf, sizeof buf, "PASS 640k_enough_ne1");
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
+ int thread_id = *(int *)pointer_to_thread_id;
- for (i=0; i<nrooms; ++i) {
- printf("Creating test room '%s'\n", random_rooms[i]);
- snprintf(buf, sizeof buf, "CRE8 1|%s|", random_rooms[i]);
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
+ serv_sock = uds_connectsock(file_citadel_socket);
+
+ if (serv_sock < 0) {
+ printf("\033[8;0H\033[31mWarning: some threads failed to connect to Citadel Server.\033[0m");
+ fflush(stdout);
+ pthread_exit(NULL);
}
+ serv_gets(serv_sock, buf);
+ snprintf(buf, sizeof buf, "USER %s", test_user);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+ snprintf(buf, sizeof buf, "PASS %s", test_pass);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[0]);
- serv_puts(buf);
- serv_gets(buf);
- printf("%s\n", buf);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+
+ // Find a nice spot on the screen to show the operation count for this thread.
+ int row = ROW_OFFSET + (thread_id % 20);
+ int col = (thread_id / 20) * 10;
+ long ops = 0;
+ printf("\033[%d;%dH\033[33m 0\033[0m", row, col);
+ fflush(stdout);
while(1) {
- perform_random_thing();
+ perform_random_thing(serv_sock);
+ printf("\033[%d;%dH\033[32m%8ld\033[0m", row, col, ++ops);
+ ++ops_completed; // this is declared "volatile" so we don't need to lock it
+ if (thread_id == 0) {
+ printf("\033[2;55H\033[44m\033[33m\033[1m%d ops/sec \033[0m", (ops_completed / (time(NULL) - time_started)));
+ }
+ fflush(stdout);
}
}
+// Create (or replace) the account used for load testing, then create the rooms in which we will load test.
+void setup_accounts(int serv_sock) {
+ int i;
+ char buf[SIZ];
+
+ snprintf(buf, sizeof buf, "CREU %s", test_user);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+ snprintf(buf, sizeof buf, "ASUP %s|%s|0|||6|", test_user, test_pass);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+ snprintf(buf, sizeof buf, "USER %s", test_user);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+ snprintf(buf, sizeof buf, "PASS %s", test_pass);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+
+ for (i=0; i<nrooms; ++i) {
+ snprintf(buf, sizeof buf, "CRE8 1|%s|", random_rooms[i]);
+ serv_puts(serv_sock, buf);
+ serv_gets(serv_sock, buf);
+ }
+}
+
// Main loop. Do things and have fun.
int main(int argc, char **argv) {
- int a;
- char buf[SIZ];
- char ctdldir[PATH_MAX]=CTDLDIR;
+ int i;
+ int nthreads = 10;
+ int row, col;
+
+ fprintf(stderr, "\033[2J\033[H\033[44m\033[1m"
+ "╔════════════════════════════════════════════════════════════════════════╗\n"
+ "║ Load testing utility for Citadel ║\n"
+ "║ Copyright (c) 2023-2024 by citadel.org et al. ║\n"
+ "║ This program is open source software. Use, duplication, or disclosure ║\n"
+ "║ is subject to the terms of the GNU General Public license v3. ║\n"
+ "╚════════════════════════════════════════════════════════════════════════╝\033[0m\n"
+ );
// Parse command line
- while ((a = getopt(argc, argv, "h:")) != EOF) {
- switch (a) {
+ while ((i = getopt(argc, argv, "h:n:")) != EOF) {
+ switch (i) {
case 'h':
strncpy(ctdldir, optarg, sizeof ctdldir);
break;
+ case 'n':
+ nthreads = atoi(optarg);
+ break;
default:
- fprintf(stderr, "sendcommand: usage: %s [-h server_dir]\n", argv[0]);
+ fprintf(stderr, "loadtest: usage: %s [-h server_dir] [-n number_of_threads]\n", argv[0]);
return(1);
}
}
- fprintf(stderr, "sendcommand: started (pid=%d) connecting to Citadel server with data directory %s\n",
- (int) getpid(),
- ctdldir
- );
- fflush(stderr);
-
if (chdir(ctdldir) != 0) {
- fprintf(stderr, "sendcommand: %s: %s\n", ctdldir, strerror(errno));
+ fprintf(stderr, "loadtest: %s: %s\n", ctdldir, strerror(errno));
exit(errno);
}
- serv_sock = uds_connectsock(file_citadel_admin_socket);
- serv_gets(buf);
- fprintf(stderr, "%s\n", buf);
+ // Generate a random password for load test user. No one needs this password except us.
+ srand(time(NULL)+getpid());
+ for (i=0; i<sizeof(test_pass)-1; ++i) {
+ test_pass[i] = (rand() % 74) + 48;
+ }
+ test_pass[sizeof(test_pass)] = 0;
+
+ // paint the screen
+ for (i=0; i<nthreads; ++i) {
+ row = ROW_OFFSET + (i % 20);
+ col = (i / 20) * 10;
+ printf("\033[%d;%dH\033[31m -\033[0m", row, col);
+ fflush(stdout);
+ }
- do_stuff();
+ // start connecting
+ int serv_sock = uds_connectsock(file_citadel_admin_socket);
+ if (serv_sock < 0) {
+ fprintf(stderr, "loadtest: cannot connect to Citadel Server\n");
+ exit(1);
+ }
+ char buf[SIZ];
+ serv_gets(serv_sock, buf);
+ setup_accounts(serv_sock);
close(serv_sock);
+ size_t * threadId = calloc(nthreads, sizeof(size_t));
+ for (size_t i = 0; i < nthreads; ++i) {
+ threadId[i] = i;
+ }
+
+ time_started = time(NULL);
+ ops_completed = 0;
+
+ for (i=1; i<nthreads; ++i) {
+
+ pthread_t thread;
+ pthread_attr_t attr;
+ int ret = 0;
+
+ ret = pthread_attr_init(&attr);
+ ret = pthread_attr_setstacksize(&attr, THREADSTACKSIZE);
+ ret = pthread_create(&thread, &attr, loadtest, &threadId[i]);
+ if (ret != 0) {
+ exit(ret);
+ }
+
+ }
+ loadtest(&threadId[0]);
return(0);
}