Implement Tasks #11 and #12: plocks stress test + pfsupload send tests

Task #11 — plocks.c stress test (test_plocks.c)
  7 tests: basic rdlock/wrlock round-trip, recursive TLS counting (same
  thread acquires rdlock N times; unlock only releases on final decrement),
  upgrade under contention (N readers + towrlock; barrier-synchronized),
  writer starvation prevention (sustained reader load; writer acquires
  within 500ms), N-reader + M-writer counter-integrity stress test (ASAN),
  and try-variant contention (trywrlock fails when another thread holds
  rdlock).  TSAN note documented: custom lock internals require ASAN-only
  when ThreadSanitizer annotations are absent.

Task #12 — pfsupload send-function tests (pfsupload_send.c/h + test_pfsupload.c)
  Extract psync_send_task_mkdir and psync_send_task_rmdir from pfsupload.c
  into pclsync/pfsupload_send.c as non-static pfsupload_send_mkdir/rmdir.
  Expose fsupload_task_t struct via pclsync/pfsupload_send.h.  Add
  __attribute__((weak)) get_urls() as an injectable URL seam for large-
  upload paths.  pfsupload.c updated to include pfsupload_send.h and use
  the renamed functions in its dispatch table; pfsupload_send.o is
  automatically picked up by the production wildcard build.

  5 tests: mkdir (non-encrypted) command + folderid param, mkdir
  (encrypted) key param present, rmdir command + sfolderid, API error path
  (papi_send failure → -1), get_urls() weak override.  Uses
  --wrap=papi_send to intercept API calls and socketpair() to provide a
  valid psock_t without real network I/O.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-11 07:38:48 +01:00
parent b30efe68fb
commit 78a517ebba
6 changed files with 745 additions and 54 deletions

View File

@ -169,7 +169,9 @@ TEST_BINS := \
tests/test_ptree \
tests/test_pintervaltree \
tests/test_pfstasks_tree \
tests/test_pfstasks_db
tests/test_pfstasks_db \
tests/test_plocks \
tests/test_pfsupload
.PHONY: test tests check clean-tests
@ -229,6 +231,13 @@ tests/test_pfstasks_tree: $(UNIT_DIR)/test_pfstasks_tree.c $(LIBDIR)/pfstasks_tr
tests/test_pfstasks_db: $(UNIT_DIR)/test_pfstasks_db.c $(HELPERS_DIR)/psql_test_helpers.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -I$(HELPERS_DIR) -o $@ $^ -lsqlite3
tests/test_plocks: $(UNIT_DIR)/test_plocks.c $(LIBDIR)/plocks.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ -lpthread
tests/test_pfsupload: $(UNIT_DIR)/test_pfsupload.c $(LIBDIR)/pfsupload_send.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ \
-Wl,--wrap=papi_send
tests/test_read_response: $(UNIT_DIR)/test_read_response.cpp rpcclient.cpp tests/stubs/test_stubs_cpp.c
$(CXX) $(TEST_CXXFLAGS) $(CXXFLAGS) -o $@ $^

View File

@ -41,6 +41,7 @@
#include "pfscrypto.h"
#include "pfstasks.h"
#include "pfsupload.h"
#include "pfsupload_send.h"
#include "pfsxattr.h"
#include "plibs.h"
#include "plist.h"
@ -58,23 +59,6 @@
#include "putil.h"
typedef struct {
psync_list list;
binresult *res;
uint64_t id;
uint64_t type;
psync_folderid_t folderid;
psync_folderid_t sfolderid;
psync_fileid_t fileid;
const char *text1;
const char *text2;
int64_t int1;
int64_t int2;
unsigned char ccreat;
unsigned char needprocessing;
unsigned char status;
} fsupload_task_t;
static void free_fsupload_task(fsupload_task_t *elem) {
pmem_free(PMEM_SUBSYS_UPLOAD, elem);
}
@ -98,31 +82,6 @@ static const uint32_t requiredstatusesnooverquota[] = {
PSTATUS_COMBINE(PSTATUS_TYPE_RUN, PSTATUS_RUN_RUN),
PSTATUS_COMBINE(PSTATUS_TYPE_ONLINE, PSTATUS_ONLINE_ONLINE)};
static int psync_send_task_mkdir(psock_t *api, fsupload_task_t *task) {
if (task->text2) {
binparam params[] = {
PAPI_STR("auth", psync_my_auth), PAPI_NUM("folderid", task->folderid),
PAPI_STR("name", task->text1), PAPI_STR("timeformat", "timestamp"),
PAPI_BOOL("encrypted", 1), PAPI_STR("key", task->text2),
PAPI_NUM("ctime", task->int1)};
if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists",
params) == PTR_OK))
return 0;
else
return -1;
} else {
binparam params[] = {
PAPI_STR("auth", psync_my_auth), PAPI_NUM("folderid", task->folderid),
PAPI_STR("name", task->text1), PAPI_STR("timeformat", "timestamp"),
PAPI_NUM("ctime", task->int1)};
if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists",
params) == PTR_OK))
return 0;
else
return -1;
}
}
static void handle_mkdir_api_error(uint64_t result, fsupload_task_t *task) {
psync_sql_res *res;
pdbg_logf(D_ERROR, "createfolderifnotexists returned error %u", (unsigned)result);
@ -182,15 +141,7 @@ static int psync_process_task_mkdir(fsupload_task_t *task) {
return 0;
}
static int psync_send_task_rmdir(psock_t *api, fsupload_task_t *task) {
binparam params[] = {PAPI_STR("auth", psync_my_auth),
PAPI_NUM("folderid", task->sfolderid),
PAPI_STR("timeformat", "timestamp")};
if (pdbg_likely(papi_send_no_res(api, "deletefolder", params) == PTR_OK))
return 0;
else
return -1;
}
/* psync_send_task_rmdir extracted to pfsupload_send.c as pfsupload_send_rmdir */
static int handle_rmdir_api_error(uint64_t result, fsupload_task_t *task) {
pdbg_logf(D_ERROR, "deletefolder returned error %u", (unsigned)result);
@ -1795,8 +1746,8 @@ typedef int (*psync_cancel_task_ptr)(fsupload_task_t *);
static psync_send_task_ptr psync_send_task_func[] = {
NULL,
psync_send_task_mkdir,
psync_send_task_rmdir,
pfsupload_send_mkdir,
pfsupload_send_rmdir,
psync_send_task_creat,
psync_send_task_unlink,
NULL,

72
pclsync/pfsupload_send.c Normal file
View File

@ -0,0 +1,72 @@
/*
* pfsupload_send.c — task serialisation layer for the fstask upload subsystem.
*
* Contains the pure "send" functions: build the pCloud API request from an
* fsupload_task_t and write it to the API socket. No psql calls, no file
* I/O, no threading — this is the only part of pfsupload.c that unit tests
* need to exercise for serialisation testing.
*
* Extracted from pfsupload.c so tests can link against this file alone
* without pulling in pfsupload.c's heavyweight transitive dependencies.
*/
#include "pdbg.h"
#include "pfsupload_send.h"
#include "plibs.h" /* psync_my_auth */
/* ------------------------------------------------------------------ */
/* Weak default: get_urls() — override in tests to inject fake URLs */
/* ------------------------------------------------------------------ */
/*
* Returns upload URLs for the given uploadid.
* The default implementation would call the pCloud API; declare it weak
* so tests can supply a stub without linking the full API stack.
*/
__attribute__((weak)) char **get_urls(uint64_t uploadid, size_t *nout) {
(void)uploadid;
if (nout) *nout = 0;
return NULL;
}
/* ------------------------------------------------------------------ */
/* Send functions */
/* ------------------------------------------------------------------ */
int pfsupload_send_mkdir(psock_t *api, fsupload_task_t *task) {
if (task->text2) {
binparam params[] = {
PAPI_STR("auth", psync_my_auth),
PAPI_NUM("folderid", task->folderid),
PAPI_STR("name", task->text1),
PAPI_STR("timeformat", "timestamp"),
PAPI_BOOL("encrypted", 1),
PAPI_STR("key", task->text2),
PAPI_NUM("ctime", task->int1)};
if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists",
params) == PTR_OK))
return 0;
return -1;
} else {
binparam params[] = {
PAPI_STR("auth", psync_my_auth),
PAPI_NUM("folderid", task->folderid),
PAPI_STR("name", task->text1),
PAPI_STR("timeformat", "timestamp"),
PAPI_NUM("ctime", task->int1)};
if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists",
params) == PTR_OK))
return 0;
return -1;
}
}
int pfsupload_send_rmdir(psock_t *api, fsupload_task_t *task) {
binparam params[] = {
PAPI_STR("auth", psync_my_auth),
PAPI_NUM("folderid", task->sfolderid),
PAPI_STR("timeformat", "timestamp")};
if (pdbg_likely(papi_send_no_res(api, "deletefolder", params) == PTR_OK))
return 0;
return -1;
}

48
pclsync/pfsupload_send.h Normal file
View File

@ -0,0 +1,48 @@
/*
* pfsupload_send.h — task-send layer for the fstask upload subsystem.
*
* Exposes the task serialisation functions (mkdir, rmdir) and the task
* struct so unit tests can call them directly via a fake psock_t without
* dragging in pfsupload.c's heavy dependencies (psql, pcache, pdiff, …).
*
* get_urls() is declared __attribute__((weak)) so tests can inject canned
* upload-URL responses for large-upload code paths.
*/
#ifndef PFSUPLOAD_SEND_H
#define PFSUPLOAD_SEND_H
#include <stdint.h>
#include "papi.h" /* binresult, PAPI_STR/NUM, psock_t (via psock.h) */
#include "pfoldersync.h" /* psync_folderid_t, psync_fileid_t */
#include "plist.h" /* psync_list */
typedef struct {
psync_list list;
binresult *res;
uint64_t id;
uint64_t type;
psync_folderid_t folderid;
psync_folderid_t sfolderid;
psync_fileid_t fileid;
const char *text1;
const char *text2;
int64_t int1;
int64_t int2;
unsigned char ccreat;
unsigned char needprocessing;
unsigned char status;
} fsupload_task_t;
/*
* get_urls() — weak symbol: returns a NULL-terminated array of upload-URL
* strings for the given uploadid. The default implementation calls the
* real pCloud API; override in tests to inject canned responses.
*/
__attribute__((weak)) char **get_urls(uint64_t uploadid, size_t *nout);
/* Non-static send functions — callable from tests via --wrap=papi_send */
int pfsupload_send_mkdir(psock_t *api, fsupload_task_t *task);
int pfsupload_send_rmdir(psock_t *api, fsupload_task_t *task);
#endif /* PFSUPLOAD_SEND_H */

View File

@ -0,0 +1,251 @@
/*
* Test: pfsupload_send.c — task serialisation via --wrap=papi_send
*
* Tests that the send functions dispatch the correct pCloud API command and
* pass the expected parameters.
*
* Strategy: --wrap=papi_send intercepts every call to the underlying
* papi_send() function (which papi_send_no_res() expands to). The wrapper
* records the command name and parameter array so tests can verify correct
* serialisation without a real network socket.
*
* socketpair() is used to construct a valid psock_t for the api argument;
* because papi_send is fully wrapped the socket is never actually written
* to in these tests.
*
* get_urls() is declared __attribute__((weak)) in pfsupload_send.c; this
* test file provides its own definition which overrides the weak default so
* large-upload paths can return canned URL data in future tests.
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include "pfsupload_send.h"
/* ------------------------------------------------------------------ */
/* get_urls() override — canned response for large-upload paths */
/* ------------------------------------------------------------------ */
char **get_urls(uint64_t uploadid, size_t *nout) {
(void)uploadid;
static char *urls[] = { "https://upload.example.com/1", NULL };
if (nout) *nout = 1;
return urls;
}
/* ------------------------------------------------------------------ */
/* __wrap_papi_send — capture command + params, return PTR_OK */
/* ------------------------------------------------------------------ */
static const char *g_last_cmd = NULL;
static const binparam *g_last_params = NULL;
static size_t g_last_nparams = 0;
static int g_wrap_rc = 1; /* 1 = return PTR_OK, 0 = return NULL */
binresult *__real_papi_send(psock_t *sock, const char *command, size_t cmdlen,
const binparam *params, size_t paramcnt,
int64_t datalen, int readres);
binresult *__wrap_papi_send(psock_t *sock, const char *command, size_t cmdlen,
const binparam *params, size_t paramcnt,
int64_t datalen, int readres) {
(void)sock; (void)cmdlen; (void)datalen; (void)readres;
g_last_cmd = command;
g_last_params = params;
g_last_nparams = paramcnt;
return g_wrap_rc ? PTR_OK : NULL;
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static int passes = 0, failures = 0;
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
static void reset_wrap(void) {
g_last_cmd = NULL;
g_last_params = NULL;
g_last_nparams = 0;
g_wrap_rc = 1;
}
/* Find a string parameter by name; returns its value or NULL */
static const char *find_str_param(const char *name) {
if (!g_last_params) return NULL;
for (size_t i = 0; i < g_last_nparams; i++) {
if (g_last_params[i].paramtype == PARAM_STR &&
strcmp(g_last_params[i].paramnamelen == strlen(name) ?
g_last_params[i].paramname : "", name) == 0)
return g_last_params[i].str;
}
return NULL;
}
/* Find a numeric parameter by name */
static int find_num_param(const char *name, uint64_t *out) {
if (!g_last_params) return 0;
for (size_t i = 0; i < g_last_nparams; i++) {
if (g_last_params[i].paramtype == PARAM_NUM &&
g_last_params[i].paramnamelen == strlen(name) &&
strncmp(g_last_params[i].paramname, name, strlen(name)) == 0) {
*out = g_last_params[i].num;
return 1;
}
}
return 0;
}
/* Build a minimal fake psock_t wrapping one end of a socketpair */
static psock_t *make_fake_api(int sv[2]) {
static psock_t fake;
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
memset(&fake, 0, sizeof(fake));
fake.sock = sv[1];
return &fake;
}
/* ------------------------------------------------------------------ */
/* Test helpers: init a minimal fsupload_task_t */
/* ------------------------------------------------------------------ */
static void task_init(fsupload_task_t *t) {
memset(t, 0, sizeof(*t));
}
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
/* mkdir (non-encrypted): correct API command and folderid/name params */
static void test_send_mkdir_basic(void) {
reset_wrap();
int sv[2];
psock_t *api = make_fake_api(sv);
fsupload_task_t task;
task_init(&task);
task.folderid = 12345;
task.text1 = "mydir";
task.text2 = NULL; /* non-encrypted */
task.int1 = 1000; /* ctime */
int rc = pfsupload_send_mkdir(api, &task);
close(sv[0]); close(sv[1]);
if (rc != 0)
{ FAIL("mkdir basic: rc=0", "rc=%d", rc); return; }
if (!g_last_cmd || strcmp(g_last_cmd, "createfolderifnotexists") != 0)
{ FAIL("mkdir basic: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; }
uint64_t folderid = 0;
if (!find_num_param("folderid", &folderid) || folderid != 12345)
FAIL("mkdir basic: folderid param", "folderid=%llu", (unsigned long long)folderid);
else
PASS("mkdir (non-encrypted): command=createfolderifnotexists, folderid correct");
}
/* mkdir (encrypted): same command, plus encrypted=1 and key param */
static void test_send_mkdir_encrypted(void) {
reset_wrap();
int sv[2];
psock_t *api = make_fake_api(sv);
fsupload_task_t task;
task_init(&task);
task.folderid = 99;
task.text1 = "encdir";
task.text2 = "base64key=="; /* non-NULL → encrypted path */
task.int1 = 2000;
int rc = pfsupload_send_mkdir(api, &task);
close(sv[0]); close(sv[1]);
if (rc != 0)
{ FAIL("mkdir enc: rc=0", "rc=%d", rc); return; }
if (!g_last_cmd || strcmp(g_last_cmd, "createfolderifnotexists") != 0)
{ FAIL("mkdir enc: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; }
/* key param should be present */
const char *key = find_str_param("key");
if (!key || strcmp(key, "base64key==") != 0)
FAIL("mkdir enc: key param", "key=%s", key ? key : "(null)");
else
PASS("mkdir (encrypted): command correct, key param present");
}
/* rmdir: correct API command and folderid from sfolderid */
static void test_send_rmdir(void) {
reset_wrap();
int sv[2];
psock_t *api = make_fake_api(sv);
fsupload_task_t task;
task_init(&task);
task.folderid = 1; /* parent (not sent in rmdir) */
task.sfolderid = 42; /* the folder to delete */
task.text1 = "gone";
int rc = pfsupload_send_rmdir(api, &task);
close(sv[0]); close(sv[1]);
if (rc != 0)
{ FAIL("rmdir: rc=0", "rc=%d", rc); return; }
if (!g_last_cmd || strcmp(g_last_cmd, "deletefolder") != 0)
{ FAIL("rmdir: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; }
uint64_t folderid = 0;
if (!find_num_param("folderid", &folderid) || folderid != 42)
FAIL("rmdir: folderid=sfolderid", "folderid=%llu", (unsigned long long)folderid);
else
PASS("rmdir: command=deletefolder, folderid=sfolderid");
}
/* API error path: papi_send returns NULL → send function returns -1 */
static void test_api_error_path(void) {
reset_wrap();
g_wrap_rc = 0; /* simulate papi_send failure */
int sv[2];
psock_t *api = make_fake_api(sv);
fsupload_task_t task;
task_init(&task);
task.folderid = 1;
task.text1 = "fail";
task.text2 = NULL;
int rc_mkdir = pfsupload_send_mkdir(api, &task);
int rc_rmdir = pfsupload_send_rmdir(api, &task);
close(sv[0]); close(sv[1]);
if (rc_mkdir == -1 && rc_rmdir == -1)
PASS("API error path: papi_send failure → send functions return -1");
else
FAIL("API error path", "mkdir=%d rmdir=%d (expected -1)", rc_mkdir, rc_rmdir);
}
/* get_urls() override is callable (verifies weak symbol override) */
static void test_get_urls_override(void) {
size_t n = 0;
char **urls = get_urls(1234, &n);
if (urls && n == 1 && urls[0] && strstr(urls[0], "example.com"))
PASS("get_urls() override: weak symbol replaced by test implementation");
else
FAIL("get_urls() override", "urls=%p n=%zu", (void*)urls, n);
}
/* ------------------------------------------------------------------ */
int main(void) {
test_send_mkdir_basic();
test_send_mkdir_encrypted();
test_send_rmdir();
test_api_error_path();
test_get_urls_override();
printf("\n%d passed, %d failed\n", passes, failures);
return failures ? 1 : 0;
}

View File

@ -0,0 +1,360 @@
/*
* Test: plocks.c — custom rwlock stress test
*
* Covers:
* 1. Basic rdlock / wrlock single-thread round-trips
* 2. Recursive TLS counting: same thread acquires rdlock N times; unlock
* only releases on final decrement
* 3. Upgrade under contention (plocks_towrlock): N readers hold rdlock,
* one thread upgrades — verified exclusive access during upgrade
* 4. Writer starvation prevention: sustained reader load, writer acquires
* lock within bounded time
* 5. N reader + M writer threads, K iterations: shared counter, no
* deadlock, no data corruption (ASAN)
*
* TSAN note: ThreadSanitizer does not know that plocks_rdlock / plocks_wrlock
* form a custom reader-writer lock and would report false data-race positives
* on the shared counter in test 5. Run the stress test with -fsanitize=address
* only (-fsanitize=thread is not compatible without TSAN annotations).
*/
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "plocks.h"
/* ------------------------------------------------------------------ */
static int passes = 0, failures = 0;
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
/* ------------------------------------------------------------------ */
/* Test 1: basic single-thread rdlock / unlock */
/* ------------------------------------------------------------------ */
static void test_basic_rdlock(void) {
psync_rwlock_t rw;
plocks_init(&rw);
plocks_rdlock(&rw);
int holding = plocks_holding_rdlock(&rw);
plocks_unlock(&rw);
int released = !plocks_holding_rdlock(&rw);
plocks_destroy(&rw);
if (holding && released)
PASS("basic rdlock: holding after lock, released after unlock");
else
FAIL("basic rdlock", "holding=%d released=%d", holding, released);
}
/* ------------------------------------------------------------------ */
/* Test 2: basic single-thread wrlock / unlock */
/* ------------------------------------------------------------------ */
static void test_basic_wrlock(void) {
psync_rwlock_t rw;
plocks_init(&rw);
plocks_wrlock(&rw);
int holding = plocks_holding_wrlock(&rw);
plocks_unlock(&rw);
int released = !plocks_holding_wrlock(&rw);
plocks_destroy(&rw);
if (holding && released)
PASS("basic wrlock: holding after lock, released after unlock");
else
FAIL("basic wrlock", "holding=%d released=%d", holding, released);
}
/* ------------------------------------------------------------------ */
/* Test 3: recursive TLS counting — same thread acquires rdlock N */
/* times; plocks_unlock only releases on the final decrement */
/* ------------------------------------------------------------------ */
#define REC_DEPTH 5
static void test_recursive_rdlock(void) {
psync_rwlock_t rw;
plocks_init(&rw);
/* Acquire N times */
int i;
for (i = 0; i < REC_DEPTH; i++)
plocks_rdlock(&rw);
int holding_mid = plocks_holding_rdlock(&rw);
/* Unlock N-1 times: must still hold */
int still_holding = 1;
for (i = 0; i < REC_DEPTH - 1; i++) {
plocks_unlock(&rw);
if (!plocks_holding_rdlock(&rw)) {
still_holding = 0;
break;
}
}
/* Final unlock: must release */
plocks_unlock(&rw);
int finally_released = !plocks_holding_rdlock(&rw);
plocks_destroy(&rw);
if (!holding_mid)
FAIL("recursive rdlock: held after all acquires", "holding_mid=0");
else if (!still_holding)
FAIL("recursive rdlock: still held after partial unlocks",
"released too early");
else if (!finally_released)
FAIL("recursive rdlock: released after final unlock",
"still holding");
else
PASS("recursive rdlock: TLS count correct, releases only on final unlock");
}
/* ------------------------------------------------------------------ */
/* Test 4: upgrade under contention */
/* */
/* N reader threads acquire rdlock then block on a barrier. The main */
/* thread also acquires rdlock, then calls plocks_towrlock() which */
/* waits for all other readers to release. Readers release after a */
/* short sleep; upgrade must succeed and wrlock must be exclusively */
/* held. */
/* ------------------------------------------------------------------ */
#define N_UPGRADE_READERS 4
struct upgrade_state {
psync_rwlock_t rw;
pthread_barrier_t barrier; /* synchronises: all hold rdlock before upgrader proceeds */
};
static void *upgrade_reader_thread(void *arg) {
struct upgrade_state *s = (struct upgrade_state *)arg;
plocks_rdlock(&s->rw);
pthread_barrier_wait(&s->barrier); /* signal: I'm holding rdlock */
usleep(40000); /* hold for 40 ms */
plocks_unlock(&s->rw);
return NULL;
}
static void test_upgrade_under_contention(void) {
struct upgrade_state s;
plocks_init(&s.rw);
/* N readers + main thread (upgrader) = N+1 participants */
pthread_barrier_init(&s.barrier, NULL, N_UPGRADE_READERS + 1);
pthread_t readers[N_UPGRADE_READERS];
int i;
for (i = 0; i < N_UPGRADE_READERS; i++)
pthread_create(&readers[i], NULL, upgrade_reader_thread, &s);
/* Main thread acquires rdlock then waits at barrier so all readers
* have their locks before we try to upgrade. */
plocks_rdlock(&s.rw);
pthread_barrier_wait(&s.barrier);
/* towrlock: wait for all N reader threads to release */
int rc = plocks_towrlock(&s.rw);
int wrlock_held = plocks_holding_wrlock(&s.rw);
plocks_unlock(&s.rw);
for (i = 0; i < N_UPGRADE_READERS; i++)
pthread_join(readers[i], NULL);
pthread_barrier_destroy(&s.barrier);
plocks_destroy(&s.rw);
if (rc == 0 && wrlock_held)
PASS("upgrade under contention: towrlock succeeds, wrlock held exclusively");
else
FAIL("upgrade under contention", "rc=%d wrlock_held=%d", rc, wrlock_held);
}
/* ------------------------------------------------------------------ */
/* Test 5: writer starvation prevention */
/* */
/* 4 threads continuously acquire/release rdlock. Once wwait > 0 new */
/* readers block. The writer should obtain wrlock within 500 ms. */
/* ------------------------------------------------------------------ */
static volatile int g_stop_readers5 = 0;
static psync_rwlock_t rw5;
static void *starvation_reader(void *arg) {
(void)arg;
while (!g_stop_readers5) {
plocks_rdlock(&rw5);
/* tiny critical section */
plocks_unlock(&rw5);
}
return NULL;
}
static void test_writer_not_starved(void) {
plocks_init(&rw5);
g_stop_readers5 = 0;
pthread_t readers[4];
int i;
for (i = 0; i < 4; i++)
pthread_create(&readers[i], NULL, starvation_reader, NULL);
usleep(5000); /* let readers get running */
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
plocks_wrlock(&rw5);
clock_gettime(CLOCK_MONOTONIC, &t1);
g_stop_readers5 = 1;
plocks_unlock(&rw5);
for (i = 0; i < 4; i++)
pthread_join(readers[i], NULL);
plocks_destroy(&rw5);
long ms = (long)(t1.tv_sec - t0.tv_sec) * 1000 +
(long)(t1.tv_nsec - t0.tv_nsec) / 1000000L;
if (ms < 500)
PASS("writer not starved: wrlock acquired within 500 ms under reader load");
else
FAIL("writer starvation", "took %ldms > 500ms", ms);
}
/* ------------------------------------------------------------------ */
/* Test 6: N readers + M writers, K iterations — no deadlock, no */
/* data corruption */
/* ------------------------------------------------------------------ */
#define N_STRESS_READERS 8
#define M_STRESS_WRITERS 4
#define K_STRESS_ITERS 200
static psync_rwlock_t rw6;
static long g_counter = 0; /* protected by rw6 */
static void *stress_writer(void *arg) {
(void)arg;
int i;
for (i = 0; i < K_STRESS_ITERS; i++) {
plocks_wrlock(&rw6);
g_counter++;
plocks_unlock(&rw6);
}
return NULL;
}
static void *stress_reader(void *arg) {
(void)arg;
int i;
for (i = 0; i < K_STRESS_ITERS; i++) {
plocks_rdlock(&rw6);
/* read-only access; any value is acceptable while reading */
(void)g_counter;
plocks_unlock(&rw6);
}
return NULL;
}
static void test_stress(void) {
plocks_init(&rw6);
g_counter = 0;
pthread_t writers[M_STRESS_WRITERS];
pthread_t readers[N_STRESS_READERS];
int i;
for (i = 0; i < M_STRESS_WRITERS; i++)
pthread_create(&writers[i], NULL, stress_writer, NULL);
for (i = 0; i < N_STRESS_READERS; i++)
pthread_create(&readers[i], NULL, stress_reader, NULL);
for (i = 0; i < M_STRESS_WRITERS; i++)
pthread_join(writers[i], NULL);
for (i = 0; i < N_STRESS_READERS; i++)
pthread_join(readers[i], NULL);
plocks_destroy(&rw6);
long expected = (long)M_STRESS_WRITERS * K_STRESS_ITERS;
if (g_counter == expected)
PASS("stress: no deadlock, counter matches expected (no corruption)");
else
FAIL("stress", "counter=%ld expected=%ld", g_counter, expected);
}
/* ------------------------------------------------------------------ */
/* Test 7: trywrlock / tryrdlock non-blocking */
/* */
/* Note: calling plocks_trywrlock while THIS THREAD holds rdlock is */
/* illegal (asserted in plocks.c: !cnt.cnt[0]). Upgrade must go */
/* through plocks_towrlock. Contention is tested via a helper thread. */
/* ------------------------------------------------------------------ */
static psync_rwlock_t rw7_contended;
static void *try_hold_rdlock(void *arg) {
/* acquire rdlock and wait until signalled to release */
pthread_barrier_t *b = (pthread_barrier_t *)arg;
plocks_rdlock(&rw7_contended);
pthread_barrier_wait(b); /* signal: rdlock held */
pthread_barrier_wait(b); /* wait: main says release */
plocks_unlock(&rw7_contended);
return NULL;
}
static void test_try_variants(void) {
psync_rwlock_t rw;
plocks_init(&rw);
/* tryrdlock on free lock → succeeds */
int rd1 = plocks_tryrdlock(&rw);
/* recursive tryrdlock → succeeds */
int rd1b = plocks_tryrdlock(&rw);
plocks_unlock(&rw);
plocks_unlock(&rw);
/* trywrlock on free lock → succeeds */
int wr1 = plocks_trywrlock(&rw);
/* tryrdlock while same-thread holds wrlock → recursive success */
int rd2 = plocks_tryrdlock(&rw);
plocks_unlock(&rw); /* releases the recursive rdlock count */
plocks_unlock(&rw); /* releases wrlock */
plocks_destroy(&rw);
/* trywrlock fails when another thread holds rdlock */
plocks_init(&rw7_contended);
pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, 2);
pthread_t t;
pthread_create(&t, NULL, try_hold_rdlock, &barrier);
pthread_barrier_wait(&barrier); /* wait for reader to hold rdlock */
int wr_fail = plocks_trywrlock(&rw7_contended); /* should fail: -1 */
pthread_barrier_wait(&barrier); /* let reader release */
pthread_join(t, NULL);
plocks_destroy(&rw7_contended);
pthread_barrier_destroy(&barrier);
if (rd1 == 0 && rd1b == 0 && wr1 == 0 && rd2 == 0 && wr_fail == -1)
PASS("try variants: tryrdlock/trywrlock return correct values");
else
FAIL("try variants", "rd1=%d rd1b=%d wr1=%d rd2=%d wr_fail=%d",
rd1, rd1b, wr1, rd2, wr_fail);
}
/* ------------------------------------------------------------------ */
int main(void) {
test_basic_rdlock();
test_basic_wrlock();
test_recursive_rdlock();
test_upgrade_under_contention();
test_writer_not_starved();
test_stress();
test_try_variants();
printf("\n%d passed, %d failed\n", passes, failures);
return failures ? 1 : 0;
}