| /* |
| * Validate a race condition issue in pg_mkdir_p(). |
| * |
| * pg_mkdir_p() is used by tablespace, initdb and pg_basebackup to create a |
| * directory as well as its parent directories. The logic used to be like |
| * below: |
| * |
| * if (stat(path) < 0) |
| * { |
| * if (mkdir(path) < 0) |
| * retval = -1; |
| * } |
| * |
| * It first checks for the existence of the path, if path does not pre-exist |
| * then it creates the directory. However if two processes try to create path |
| * concurrently, then one possible execution order is as below: |
| * |
| * A: stat(path) returns -1, so decide to create it; |
| * B: stat(path) returns -1, so decide to create it; |
| * B: mkdir(path) returns 0, successfully created path; |
| * A: mkdir(path) returns -1, fail to create path as it already exist; |
| * |
| * It could be triggered easily with initdb: |
| * |
| * testdir=/tmp/testdir |
| * datadir=$testdir/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z |
| * |
| * rm -rf $testdir |
| * mkdir $testdir |
| * |
| * # init two databases with common parent directories |
| * initdb -D $datadir/db1 >$testdir/db1.log 2>&1 & |
| * initdb -D $datadir/db2 >$testdir/db2.log 2>&1 & |
| * |
| * # wait for them to finish and check for the error |
| * wait |
| * cd $testdir |
| * grep 'could not create directory' *.log |
| * |
| * The fail rate is not 100% but should be large enough to happen in 5 tries. |
| * |
| * This race condition could be fixed by swapping the order of stat() and |
| * mkdir(), this is also what the "mkdir -p" command does. |
| * |
| * In this test module we test concurrent calls to pg_mkdir_p() to ensure the |
| * race condition does not happen. |
| */ |
| |
| #include <errno.h> |
| #include <pthread.h> |
| #include <sys/stat.h> |
| |
| #include <stdarg.h> |
| #include <stddef.h> |
| #include <setjmp.h> |
| #include "cmockery.h" |
| |
| #include "postgres.h" |
| |
| #include "port.h" |
| #include "utils/memutils.h" |
| |
| #define TESTDIR "/tmp/testdir_pg_mkdir_p" |
| #define DATADIR TESTDIR "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" |
| |
| /* |
| * A struct to pass arguments to the thread and return the results. |
| */ |
| typedef struct |
| { |
| pthread_t tid; /* thread id */ |
| char path[MAXPGPATH]; /* the path to create */ |
| int retcode; /* return code of pg_mkdir_p() */ |
| int error; /* errno */ |
| } Job; |
| |
| static void * |
| job_thread(void *arg) |
| { |
| Job *job = (Job *) arg; |
| |
| errno = 0; |
| |
| job->retcode = pg_mkdir_p(job->path, S_IRWXU); |
| job->error = errno; |
| |
| return NULL; |
| } |
| |
| /* |
| * This function accepts in integer argument n, it will launch n concurrent |
| * threads to call pg_mkdir_p() to create the same dir and check for errors |
| * from them. |
| */ |
| static void |
| concurrent_pg_mkdir_p(int n) |
| { |
| Job *jobs = malloc(sizeof(Job) * n); |
| int failed = 0; |
| int i; |
| |
| /* Create concurrent threads to execute pg_mkdir_p() */ |
| for (i = 0; i < n; i++) |
| { |
| Job *job = &jobs[i]; |
| |
| strncpy(job->path, DATADIR, sizeof(job->path)); |
| pthread_create(&job->tid, NULL, job_thread, job); |
| } |
| |
| /* Check for the results */ |
| for (i = 0; i < n; i++) |
| { |
| Job *job = &jobs[i]; |
| |
| pthread_join(job->tid, NULL); |
| |
| if (job->retcode < 0) |
| { |
| /* |
| * Only show the message, do not error out until we joined all |
| * the threads. |
| */ |
| print_error("job %d: could not create directory \"%s\": %s\n", |
| i, job->path, strerror(job->error)); |
| failed++; |
| } |
| } |
| |
| assert_int_equal(failed, 0); |
| |
| free(jobs); |
| } |
| |
| static void |
| test__pgmkdirp__1(void **state) |
| { |
| concurrent_pg_mkdir_p(1); |
| } |
| |
| static void |
| test__pgmkdirp__2(void **state) |
| { |
| concurrent_pg_mkdir_p(2); |
| } |
| |
| static void |
| test__pgmkdirp__4(void **state) |
| { |
| concurrent_pg_mkdir_p(4); |
| } |
| |
| static void |
| test__pgmkdirp__8(void **state) |
| { |
| concurrent_pg_mkdir_p(8); |
| } |
| |
| static void |
| test__pgmkdirp__16(void **state) |
| { |
| concurrent_pg_mkdir_p(16); |
| } |
| |
| static void |
| test__pgmkdirp__32(void **state) |
| { |
| concurrent_pg_mkdir_p(32); |
| } |
| |
| static void |
| setup(void **state) |
| { |
| /* if the dir does not exist rmtree() would raise a warning, suppress it */ |
| mkdir(TESTDIR, S_IRWXU); |
| |
| rmtree(TESTDIR, true); |
| } |
| |
| static void |
| teardown(void **state) |
| { |
| rmtree(TESTDIR, true); |
| } |
| |
| int |
| main(int argc, char* argv[]) |
| { |
| cmockery_parse_arguments(argc, argv); |
| |
| const UnitTest tests[] = { |
| unit_test_setup_teardown(test__pgmkdirp__1, setup, teardown), |
| unit_test_setup_teardown(test__pgmkdirp__2, setup, teardown), |
| unit_test_setup_teardown(test__pgmkdirp__4, setup, teardown), |
| unit_test_setup_teardown(test__pgmkdirp__8, setup, teardown), |
| unit_test_setup_teardown(test__pgmkdirp__16, setup, teardown), |
| unit_test_setup_teardown(test__pgmkdirp__32, setup, teardown), |
| }; |
| |
| MemoryContextInit(); |
| |
| return run_tests(tests); |
| } |