/*
CTDB cluster mutex test
Copyright (C) Martin Schwenke 2019
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
*/
#include "replace.h"
#include "system/network.h"
#include "system/wait.h"
#include
#include
#include
#include "lib/util/util.h"
#include "lib/util/smb_strtox.h"
#include "tests/src/test_backtrace.h"
/*
* ctdb_cluster_mutex.c is included below. This requires a few hacks...
*/
/* Avoid inclusion of ctdb_private.h */
#define _CTDB_PRIVATE_H
/* Fake ctdb_context */
struct ctdb_context {
struct tevent_context *ev;
};
/*
* ctdb_fork() and ctdb_kill() are used in ctdb_cluster_mutex.c for
* safer tracking of PIDs. Fake them here to avoid dragging in the
* world.
*/
static pid_t ctdb_fork(struct ctdb_context *ctdb)
{
return fork();
}
static int ctdb_kill(struct ctdb_context *ctdb, pid_t pid, int signum)
{
/*
* Tests need to wait for the child to exit to ensure that the
* lock really has been released. The PID is only accessible
* in ctdb_cluster_mutex.c, so make a best attempt to ensure
* that the child process is waited for after it is killed.
* Avoid waiting if the process is already gone.
*/
int ret;
if (signum == 0) {
return kill(pid, signum);
}
ret = kill(pid, signum);
waitpid(pid, NULL, 0);
return ret;
}
#include "server/ctdb_cluster_mutex.c"
/*
* Mutex testing support
*/
struct mutex_handle {
bool done;
bool locked;
struct ctdb_cluster_mutex_handle *h;
};
struct do_lock_context {
struct mutex_handle *mh;
struct ctdb_context *ctdb;
};
static void do_lock_handler(char status, double latency, void *private_data)
{
struct do_lock_context *dl = talloc_get_type_abort(
private_data, struct do_lock_context);
struct mutex_handle *mh;
assert(dl->mh != NULL);
mh = dl->mh;
mh->locked = (status == '0') ;
/*
* If unsuccessful then ensure the process has exited and that
* the file descriptor event handler has been cancelled
*/
if (! mh->locked) {
TALLOC_FREE(mh->h);
}
switch (status) {
case '0':
printf("LOCK\n");
break;
case '1':
printf("CONTENTION\n");
break;
case '2':
printf("TIMEOUT\n");
break;
default:
printf("ERROR\n");
}
fflush(stdout);
mh->done = true;
}
static void do_lock_lost_handler(void *private_data)
{
struct do_lock_context *dl = talloc_get_type_abort(
private_data, struct do_lock_context);
printf("LOST\n");
fflush(stdout);
TALLOC_FREE(dl->mh);
}
static void do_lock_take(struct do_lock_context *dl,
const char *mutex_string)
{
struct ctdb_cluster_mutex_handle *h;
dl->mh = talloc_zero(dl, struct mutex_handle);
assert(dl->mh != NULL);
h = ctdb_cluster_mutex(dl->mh,
dl->ctdb,
mutex_string,
120,
do_lock_handler,
dl,
do_lock_lost_handler,
dl);
assert(h != NULL);
dl->mh->h = h;
}
static void do_lock_wait_done(struct do_lock_context *dl)
{
assert(dl->mh != NULL);
while (! dl->mh->done) {
tevent_loop_once(dl->ctdb->ev);
}
}
static void do_lock_check(struct do_lock_context *dl)
{
assert(dl->mh != NULL);
if (! dl->mh->locked) {
printf("NOLOCK\n");
fflush(stdout);
TALLOC_FREE(dl->mh);
}
}
static void do_lock(struct do_lock_context *dl,
const char *mutex_string)
{
do_lock_take(dl, mutex_string);
do_lock_wait_done(dl);
do_lock_check(dl);
}
static void do_unlock(struct do_lock_context *dl)
{
if (dl->mh == NULL) {
return;
}
if (! dl->mh->done) {
/*
* Taking of lock still in progress. Free the cluster
* mutex handle to release it but leave the lock
* handle in place to allow taking of the lock to
* fail.
*/
printf("CANCEL\n");
fflush(stdout);
TALLOC_FREE(dl->mh->h);
dl->mh->done = true;
dl->mh->locked = false;
return;
}
printf("UNLOCK\n");
fflush(stdout);
TALLOC_FREE(dl->mh);
}
static void wait_handler(struct tevent_context *ev,
struct tevent_timer *te,
struct timeval t,
void *private_data)
{
bool *done = (bool *)private_data;
*done = true;
}
static void do_lock_wait_time(struct do_lock_context *dl,
unsigned long wait_time)
{
struct tevent_timer *tt;
bool done = false;
tt = tevent_add_timer(dl->ctdb->ev,
dl,
tevent_timeval_current_ofs(wait_time, 0),
wait_handler,
&done);
assert(tt != NULL);
while (!done && dl->mh != NULL) {
tevent_loop_once(dl->ctdb->ev);
}
}
/*
* Testcases
*/
static void test_lock_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
static void test_lock_lock_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl1;
struct do_lock_context *dl2;
dl1 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl1 != NULL);
dl1->ctdb = ctdb;
dl2 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl2 != NULL);
dl2->ctdb = ctdb;
/* LOCK */
do_lock(dl1, mutex_string);
assert(dl1->mh != NULL);
/* CONTENTION */
do_lock(dl2, mutex_string);
assert(dl2->mh == NULL);
/* UNLOCK */
do_unlock(dl1);
assert(dl1->mh == NULL);
}
static void test_lock_unlock_lock_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl1;
struct do_lock_context *dl2;
dl1 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl1 != NULL);
dl1->ctdb = ctdb;
dl2 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl2 != NULL);
dl2->ctdb = ctdb;
/* LOCK */
do_lock(dl1, mutex_string);
assert(dl1->mh != NULL);
/* UNLOCK */
do_unlock(dl1);
assert(dl1->mh == NULL);
/* LOCK */
do_lock(dl2, mutex_string);
assert(dl2->mh != NULL);
/* UNLOCK */
do_unlock(dl2);
assert(dl2->mh == NULL);
}
static void test_lock_cancel_check(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
do_lock_take(dl, mutex_string);
assert(dl->mh != NULL);
/* CANCEL */
do_unlock(dl);
assert(dl->mh != NULL);
do_lock_wait_done(dl);
/* NOLOCK */
do_lock_check(dl);
assert(dl->mh == NULL);
}
static void test_lock_cancel_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
do_lock_take(dl, mutex_string);
assert(dl->mh != NULL);
/* CANCEL */
do_unlock(dl);
assert(dl->mh != NULL);
do_lock_wait_done(dl);
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
static void test_lock_wait_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
/* Wait for twice as long as the PPID timeout */
do_lock_wait_time(dl, 2 * 5);
assert(dl->mh != NULL);
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
static void fd_done_handler(struct tevent_context *ev,
struct tevent_fd *fde,
uint16_t flags,
void *private_data)
{
bool *done = (bool *)private_data;
*done = true;
}
static void test_lock_ppid_gone_lock_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string)
{
struct do_lock_context *dl;
struct tevent_fd *fde;
int pipefd[2];
int ret;
pid_t pid, pid2;
ssize_t nread;
bool done;
/*
* Do this in the parent - debugging aborts of the child is
* trickier
*/
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
ret = pipe(pipefd);
assert(ret == 0);
pid = fork();
assert(pid != -1);
if (pid == 0) {
ssize_t nwritten;
close(pipefd[0]);
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
/*
* Note that we never see corresponding LOST. That
* would come from this process, but it is killed
* below.
*/
nwritten = write(pipefd[1], &ret, sizeof(ret));
assert(nwritten == sizeof(ret));
sleep(999);
exit(1);
}
close(pipefd[1]);
nread = read(pipefd[0], &ret, sizeof(ret));
assert(nread == sizeof(ret));
assert(ret == 0);
/*
* pipefd[1] is leaked into the helper, so there will be an
* event generated when the helper exits
*/
done = false;
fde = tevent_add_fd(ctdb->ev,
ctdb,
pipefd[0],
TEVENT_FD_READ,
fd_done_handler,
&done);
assert(fde != NULL);
ret = kill(pid, SIGKILL);
assert(ret == 0);
pid2 = waitpid(pid, &ret, 0);
assert(pid2 == pid);
while (! done) {
tevent_loop_once(ctdb->ev);
}
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
static void test_lock_file_removed_no_recheck(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string,
const char *lock_file)
{
struct do_lock_context *dl1;
struct do_lock_context *dl2;
int ret;
dl1 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl1 != NULL);
dl1->ctdb = ctdb;
dl2 = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl2 != NULL);
dl2->ctdb = ctdb;
/* LOCK */
do_lock(dl1, mutex_string);
assert(dl1->mh != NULL);
ret = unlink(lock_file);
assert(ret == 0);
/* LOCK */
do_lock(dl2, mutex_string);
assert(dl2->mh != NULL);
/* UNLOCK */
do_unlock(dl2);
assert(dl2->mh == NULL);
/* UNLOCK */
do_unlock(dl1);
assert(dl1->mh == NULL);
}
static void test_lock_file_wait_recheck_unlock(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string,
unsigned long wait_time)
{
struct do_lock_context *dl;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
do_lock_wait_time(dl, wait_time);
assert(dl->mh != NULL);
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
static void test_lock_file_removed(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string,
const char *lock_file)
{
struct do_lock_context *dl;
int ret;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
ret = unlink(lock_file);
assert(ret == 0);
while (dl->mh != NULL) {
/* LOST */
tevent_loop_once(ctdb->ev);
}
}
static void test_lock_file_changed(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string,
const char *lock_file)
{
struct do_lock_context *dl;
char *t;
int fd;
int ret;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
t = talloc_asprintf(ctdb, "%s.new", lock_file);
assert(t != NULL);
fd = open(t, O_RDWR|O_CREAT, 0600);
assert(fd != -1);
close(fd);
ret = rename(t, lock_file);
assert(ret == 0);
while (dl->mh != NULL) {
/* LOST */
tevent_loop_once(ctdb->ev);
}
}
static void test_lock_io_timeout(TALLOC_CTX *mem_ctx,
struct ctdb_context *ctdb,
const char *mutex_string,
const char *lock_file,
unsigned long block_wait,
unsigned long block_time)
{
struct do_lock_context *dl;
int pipefd[2];
int ret;
pid_t pid, pid2;
ssize_t nwritten;
dl = talloc_zero(mem_ctx, struct do_lock_context);
assert(dl != NULL);
dl->ctdb = ctdb;
ret = pipe(pipefd);
assert(ret == 0);
pid = fork();
assert(pid != -1);
if (pid == 0) {
static struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 1,
.l_len = 1,
.l_pid = 0,
};
ssize_t nread;
int fd;
close(pipefd[1]);
/* Only continue when the parent is ready */
nread = read(pipefd[0], &ret, sizeof(ret));
assert(nread == sizeof(ret));
assert(ret == 0);
sleep(block_wait);
fd = open(lock_file, O_RDWR, 0600);
assert(fd != -1);
ret = fcntl(fd, F_SETLKW, &lock);
assert(ret == 0);
sleep(block_time);
close(fd);
sleep(999);
_exit(0);
}
close(pipefd[0]);
/* LOCK */
do_lock(dl, mutex_string);
assert(dl->mh != NULL);
nwritten = write(pipefd[1], &ret, sizeof(ret));
assert(nwritten == sizeof(ret));
do_lock_wait_time(dl, block_wait + block_time * 2);
if (dl->mh != NULL) {
/* UNLOCK */
do_unlock(dl);
assert(dl->mh == NULL);
}
ret = kill(pid, SIGKILL);
assert(ret == 0);
pid2 = waitpid(pid, &ret, 0);
assert(pid2 == pid);
}
/*
* Main
*/
static const char *prog;
static void usage(void)
{
fprintf(stderr, "usage: %s [...]\n", prog);
exit(1);
}
static void alarm_handler(int sig)
{
abort();
}
int main(int argc, const char *argv[])
{
TALLOC_CTX *mem_ctx;
struct ctdb_context *ctdb;
const char *mutex_string;
const char *test;
struct sigaction sa = { .sa_handler = NULL, };
int ret;
const char *lock_file;
unsigned int wait_time;
prog = argv[0];
if (argc < 3) {
usage();
}
mem_ctx = talloc_new(NULL);
assert(mem_ctx != NULL);
ctdb = talloc_zero(mem_ctx, struct ctdb_context);
assert(ctdb != NULL);
ctdb->ev = tevent_context_init(ctdb);
assert(ctdb->ev != NULL);
/* Add a 60s timeout for the whole test */
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
ret = sigaction(SIGALRM, &sa, NULL);
assert(ret == 0);
alarm(60);
test_backtrace_setup();
test = argv[1];
mutex_string = argv[2];
if (strcmp(test, "lock-unlock") == 0) {
test_lock_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-lock-unlock") == 0) {
test_lock_lock_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-unlock-lock-unlock") == 0) {
test_lock_unlock_lock_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-cancel-check") == 0) {
test_lock_cancel_check(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-cancel-unlock") == 0) {
test_lock_cancel_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-wait-unlock") == 0) {
test_lock_wait_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-ppid-gone-lock-unlock") == 0) {
test_lock_ppid_gone_lock_unlock(mem_ctx, ctdb, mutex_string);
} else if (strcmp(test, "lock-file-removed-no-recheck") == 0) {
if (argc != 4) {
usage();
}
lock_file = argv[3];
test_lock_file_removed_no_recheck(mem_ctx,
ctdb,
mutex_string,
lock_file);
} else if (strcmp(test, "lock-file-wait-recheck-unlock") == 0) {
if (argc != 4) {
usage();
}
wait_time = smb_strtoul(argv[3],
NULL,
10,
&ret,
SMB_STR_STANDARD);
if (ret != 0) {
usage();
}
test_lock_file_wait_recheck_unlock(mem_ctx,
ctdb,
mutex_string,
wait_time);
} else if (strcmp(test, "lock-file-removed") == 0) {
if (argc != 4) {
usage();
}
lock_file = argv[3];
test_lock_file_removed(mem_ctx,
ctdb,
mutex_string,
lock_file);
} else if (strcmp(test, "lock-file-changed") == 0) {
if (argc != 4) {
usage();
}
lock_file = argv[3];
test_lock_file_changed(mem_ctx,
ctdb,
mutex_string,
lock_file);
} else if (strcmp(test, "lock-io-timeout") == 0) {
unsigned long block_wait;
unsigned long block_time;
if (argc != 6) {
usage();
}
lock_file = argv[3];
block_wait = (unsigned long)atol(argv[4]);
block_time = (unsigned long)atol(argv[5]);
test_lock_io_timeout(mem_ctx,
ctdb,
mutex_string,
lock_file,
block_wait,
block_time);
} else {
fprintf(stderr, "Unknown test\n");
exit(1);
}
talloc_free(mem_ctx);
return 0;
}