/* robust.c -- what happens to a shared-memory mutex when its owner dies.
 *
 *     cc -O2 -std=c11 -pthread -o robust data/sync/robust.c && ./robust
 *
 * A PTHREAD_PROCESS_SHARED mutex in mmap'd memory is the fastest way for two
 * processes to agree on anything: no syscall on the uncontended path, no
 * kernel object, no IPC. It also introduces a failure the single-process case
 * does not have -- a lock holder can *die*, and a plain mutex it held is then
 * held forever.
 *
 * Both halves below are the same program with one attribute changed.
 */
#define _GNU_SOURCE
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

typedef struct {
    pthread_mutex_t lock;
    long counter;
} shared_t;

static shared_t *map_shared(int robust)
{
    shared_t *s = mmap(NULL, sizeof *s, PROT_READ | PROT_WRITE,
                       MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (s == MAP_FAILED) { perror("mmap"); exit(1); }

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    if (robust)
        pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);
    /* pthread_* return an error number and do not set errno, so perror would
       print "Success" here. strerror on the return value is the correct pair. */
    int rc = pthread_mutex_init(&s->lock, &attr);
    if (rc != 0) { fprintf(stderr, "mutex_init: %s\n", strerror(rc)); exit(1); }
    pthread_mutexattr_destroy(&attr);
    s->counter = 0;
    return s;
}

/* The child takes the lock, writes half an update, and dies holding it. */
static void kill_holder(shared_t *s)
{
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); exit(1); }
    if (pid == 0) {
        pthread_mutex_lock(&s->lock);
        s->counter = -1;                 /* the "half-written" state */
        _exit(0);                        /* dies WITHOUT unlocking */
    }
    waitpid(pid, NULL, 0);
}

static void try_lock_for(shared_t *s, int seconds, const char *label)
{
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    ts.tv_sec += seconds;

    int rc = pthread_mutex_timedlock(&s->lock, &ts);
    if (rc == ETIMEDOUT) {
        printf("%s: timed out after %ds -- the lock is held by a dead process,\n"
               "        and nothing will ever release it. Without the timeout this\n"
               "        call blocks forever.\n", label, seconds);
        return;
    }
    if (rc == EOWNERDEAD) {
        printf("%s: EOWNERDEAD -- the kernel handed the lock over and told us the\n"
               "        previous owner died. State may be torn: counter = %ld\n",
               label, s->counter);
        s->counter = 0;                              /* repair the invariant */
        if (pthread_mutex_consistent(&s->lock) != 0)
            printf("        pthread_mutex_consistent failed\n");
        else
            printf("        repaired, pthread_mutex_consistent() called, counter = %ld\n",
                   s->counter);
        pthread_mutex_unlock(&s->lock);
        return;
    }
    if (rc == 0) {
        printf("%s: acquired normally (rc=0)\n", label);
        pthread_mutex_unlock(&s->lock);
        return;
    }
    printf("%s: pthread_mutex_timedlock returned %d (%s)\n", label, rc, strerror(rc));
}

int main(void)
{
    printf("plain PTHREAD_PROCESS_SHARED mutex\n");
    shared_t *plain = map_shared(0);
    kill_holder(plain);
    try_lock_for(plain, 2, "  parent");
    munmap(plain, sizeof *plain);

    printf("\nsame mutex with PTHREAD_MUTEX_ROBUST\n");
    shared_t *rob = map_shared(1);
    kill_holder(rob);
    try_lock_for(rob, 2, "  parent");

    /* And the recovered mutex is usable again, which is the whole point. */
    if (pthread_mutex_lock(&rob->lock) == 0) {
        rob->counter++;
        pthread_mutex_unlock(&rob->lock);
        printf("  parent: lock reused after recovery, counter = %ld\n", rob->counter);
    }
    munmap(rob, sizeof *rob);
    return 0;
}
