packages feed

interprocess-0.2.1.0: src/Control/Concurrent/Process/StoredMVarPosix.c

#include <common.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

#ifdef INTERPROCESS_DEBUG
#define ASSERT_ZERO(op) assert(op == 0)
#else
#define ASSERT_ZERO(op) op
#endif

// NB: use robust mutexes
// https://man7.org/linux/man-pages/man3/pthread_mutexattr_setrobust.3.html

typedef struct MVar MVar;

MVar *mvar_new(size_t byteSize, int max_wait_ms);
MVar *mvar_lookup(const char *name);
void mvar_destroy(MVar *mvar);

int mvar_take(MVar *mvar, void *localDataPtr, StgStablePtr tso);
int mvar_trytake(MVar *mvar, void *localDataPtr);
int mvar_put(MVar *mvar, void *localDataPtr, StgStablePtr tso);
int mvar_tryput(MVar *mvar, void *localDataPtr);
int mvar_read(MVar *mvar, void *localDataPtr, StgStablePtr tso);
int mvar_tryread(MVar *mvar, void *localDataPtr);
int mvar_swap(MVar *mvar, void *inPtr, void *outPtr, StgStablePtr tso);
int mvar_tryswap(MVar *mvar, void *inPtr, void *outPtr);
int mvar_isempty(MVar *mvar);

typedef struct MVarState {
  size_t dataSize;
  /**
   * Maximum wait time in milliseconds.
   * This determines maximum possible delay the mvar operation cancels in
   * the event of an async exception thrown to the calling Haskell thread.
   */
  int maxWaitMs;
  int pendingReaders;

  // following is OS-specific

  int isFull;
  atomic_int totalUsers;
  pthread_mutex_t mvMut;
  pthread_cond_t canPutC;
  pthread_cond_t canTakeC;
} MVarState;

typedef struct MVar {
  /**
   * State is stored in the shared data area, accessed by all processes.
   * It has a fixed size and kept at the beginning of a shared memory region.
   */
  MVarState *statePtr;
  /** Actual data is stored next to the MVarState */
  void *dataPtr;
  /** Name of the shared memory region, used to share mvar across processes. */
  SharedObjectName mvarName;
} MVar;

// ensure 64 byte alignment (maximum possible we can think of, e.g. AVX512)
size_t mvar_state_size64() {
  size_t x = sizeof(MVarState);
  return (x + 63) & (~63);
}

/**
 * Increase the total users counter by one and return zero;
 * if there are no users left (counter == 0), fail (return non-zero).
 */
static inline int inc_totalUsers(MVar *mvar) {
  atomic_int *p = &(mvar->statePtr->totalUsers);
  int exp = atomic_load_explicit(p, memory_order_relaxed);
  while (exp >= 1) {
    if (atomic_compare_exchange_weak_explicit(
            p, &exp, exp + 1, memory_order_relaxed, memory_order_relaxed))
      return 0;
  }
  return -1;
}

MVar *mvar_new(size_t byteSize, int max_wait_ms) {
  int r = 0;
  // allocate MVar
  MVar *mvar = malloc(sizeof(MVar));
  if (mvar == NULL) goto failed;
  genSharedObjectName(mvar->mvarName);

  // allocate shared memory for MVarState and data
  size_t dataShift = mvar_state_size64();
  size_t totalSize = dataShift + byteSize;
  int memFd = shm_open(mvar->mvarName, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
  if (memFd < 0) {
    INTERPROCESS_LOG_DEBUG("Could not open shared memory segment (%d).\n",
                           errno);
    goto failed_after_mvar_allocated;
  }

  if (ftruncate(memFd, totalSize) != 0) {
    INTERPROCESS_LOG_DEBUG(
        "Could not allocate shared memory (ftruncate) (%d).\n", errno);
    goto failed_after_shmem_open;
  }
  mvar->statePtr = (MVarState *)mmap(NULL, totalSize, PROT_READ | PROT_WRITE,
                                     MAP_SHARED, memFd, 0);
  if (mvar->statePtr == MAP_FAILED) {
    INTERPROCESS_LOG_DEBUG("Could not map shared memory (mmap) (%d).\n", errno);
    goto failed_after_shmem_open;
  }
  mvar->dataPtr = ((void *)(mvar->statePtr)) + dataShift;

  // setup state
  *(mvar->statePtr) = (struct MVarState){.isFull = 0,
                                         .pendingReaders = 0,
                                         .dataSize = byteSize,
                                         .totalUsers = 1,
                                         .maxWaitMs = max_wait_ms};

  // init mutex and condition variables
  pthread_mutexattr_t mvMAttr;
  r = pthread_mutexattr_init(&mvMAttr);
  if (r != 0) goto failed_on_mutex_init;
#ifdef INTERPROCESS_DEBUG
  r = pthread_mutexattr_settype(&mvMAttr, PTHREAD_MUTEX_ERRORCHECK);
  if (r != 0) goto failed_on_mutex_init;
#endif
  r = pthread_mutexattr_setpshared(&mvMAttr, PTHREAD_PROCESS_SHARED);
  if (r != 0) goto failed_on_mutex_init;
  r = pthread_mutex_init(&(mvar->statePtr->mvMut), &mvMAttr);
  if (r != 0) goto failed_on_mutex_init;
  r = pthread_mutexattr_destroy(&mvMAttr);
  if (r != 0) {
  failed_on_mutex_init:
    INTERPROCESS_LOG_DEBUG("Could not init a mutex (%d).\n", r);
    goto failed_after_mmap;
  }

  pthread_condattr_t condAttr;
  r = pthread_condattr_init(&condAttr);
  if (r != 0) goto failed_on_cond_init;
  r = pthread_condattr_setpshared(&condAttr, PTHREAD_PROCESS_SHARED);
  if (r != 0) goto failed_on_cond_init;
  r = pthread_cond_init(&(mvar->statePtr->canPutC), &condAttr);
  if (r != 0) goto failed_on_cond_init;
  r = pthread_cond_init(&(mvar->statePtr->canTakeC), &condAttr);
  if (r != 0) goto failed_on_cond_init;
  pthread_condattr_destroy(&condAttr);
  if (r != 0) {
  failed_on_cond_init:
    INTERPROCESS_LOG_DEBUG("Could not init a condition variable (%d).\n", r);
    goto failed_after_mmap;
  }

  return mvar;

failed_after_mmap:
  munmap(mvar->statePtr, totalSize);
failed_after_shmem_open:
  shm_unlink(mvar->mvarName);
failed_after_mvar_allocated:
  free(mvar);
failed:
  return NULL;
}

MVar *mvar_lookup(const char *name) {
  // allocate MVar
  MVar *mvar = malloc(sizeof(MVar));
  if (mvar == NULL) goto failed;
  strcpy(mvar->mvarName, name);

  int memFd = shm_open(name, O_RDWR, S_IRUSR | S_IWUSR);
  if (memFd < 0) {
    INTERPROCESS_LOG_DEBUG("Could not open shared memory segment (%d).\n",
                           errno);
    goto failed_after_mvar_allocated;
  }

  // first, map only sizeof(MVarState) bytes
  // then, read the actual size and remap memory
  mvar->statePtr = (MVarState *)mmap(NULL, sizeof(MVarState), PROT_READ,
                                     MAP_SHARED, memFd, 0);
  if (mvar->statePtr == MAP_FAILED) {
    INTERPROCESS_LOG_DEBUG("Could not map shared memory (mmap) (%d).\n", errno);
    goto failed_after_mvar_allocated;
  }
  size_t dataShift = mvar_state_size64();
  size_t totalSize = dataShift + mvar->statePtr->dataSize;
  ASSERT_ZERO(munmap(mvar->statePtr, sizeof(MVarState)));

  // now we know the actual size, map the memory again
  mvar->statePtr = (MVarState *)mmap(NULL, totalSize, PROT_READ | PROT_WRITE,
                                     MAP_SHARED, memFd, 0);
  if (mvar->statePtr == MAP_FAILED) {
    INTERPROCESS_LOG_DEBUG("Could not map shared memory (mmap) (%d).\n", errno);
    goto failed_after_mvar_allocated;
  }
  mvar->dataPtr = ((void *)(mvar->statePtr)) + dataShift;

  // update state
  if (inc_totalUsers(mvar) != 0) {
    INTERPROCESS_LOG_DEBUG(
        "Found that the mvar was already destroyed when updating the total "
        "users counter.\n");
    goto failed_after_mmap;
  }
  return mvar;

failed_after_mmap:
  munmap(mvar->statePtr, totalSize);
failed_after_mvar_allocated:
  free(mvar);
failed:
  return NULL;
}

void mvar_destroy(MVar *mvar) {
  size_t storeSize = mvar->statePtr->dataSize + mvar_state_size64();
  int usersLeft = atomic_fetch_sub_explicit(&(mvar->statePtr->totalUsers), 1,
                                            memory_order_relaxed) -
                  1;
  if (usersLeft > 0) {
    INTERPROCESS_LOG_DEBUG(
        "Destroying local instance of mvar %s, %d users left.\n",
        mvar->mvarName, usersLeft);
    munmap(mvar->statePtr, storeSize);
  } else {
    INTERPROCESS_LOG_DEBUG(
        "Destroying mvar %s globally (no other users left).\n", mvar->mvarName);
    pthread_cond_destroy(&(mvar->statePtr->canTakeC));
    pthread_cond_destroy(&(mvar->statePtr->canPutC));
    pthread_mutex_destroy(&(mvar->statePtr->mvMut));
    munmap(mvar->statePtr, storeSize);
    shm_unlink(mvar->mvarName);
  }
  free(mvar);
}

static inline void finilize_USE_MUTEX(pthread_mutex_t **mp) {
  pthread_mutex_unlock(*mp);
}

#define USE_MUTEX(state)                                               \
  {                                                                    \
    int r = 0;                                                         \
    r = pthread_mutex_lock(&(state->mvMut));                           \
    if (r != 0) return r;                                              \
  }                                                                    \
  __attribute__((cleanup(finilize_USE_MUTEX))) pthread_mutex_t *_mut = \
      &(state->mvMut);

int interprocess_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mut,
                                int millis) {
  struct timespec time_to_wait;
  clock_gettime(CLOCK_REALTIME, &time_to_wait);
  ldiv_t t = ldiv(millis * 1000 + (time_to_wait.tv_nsec / 1000), 1000000);
  time_to_wait.tv_sec += t.quot;
  time_to_wait.tv_nsec = t.rem * 1000;
  int r = pthread_cond_timedwait(cond, mut, &time_to_wait);
#ifdef __APPLE__
  // Workaround for macOS failing in timedwait for absolutely no reason
  if (r == EINVAL) {
    r = pthread_mutex_unlock(mut);
    if (r != 0) return r;
    t = ldiv(millis, 1000);
    time_to_wait.tv_sec = t.quot;
    time_to_wait.tv_nsec = t.rem * 1000000;
    nanosleep(&time_to_wait, NULL);
    r = pthread_mutex_lock(mut);
  }
#endif
  return r;
}

#define WAIT_A_BIT(cond, mut, ms)                       \
  {                                                     \
    int r = interprocess_cond_timedwait(cond, mut, ms); \
    if (r != 0 && r != ETIMEDOUT) return r;             \
  }

int mvar_take(MVar *mvar, void *localDataPtr, StgStablePtr tso) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  while (!(state->isFull) || state->pendingReaders > 0) {
    if (state->pendingReaders > 0) pthread_cond_broadcast(&(state->canTakeC));
    WAIT_A_BIT(&(state->canTakeC), &(state->mvMut), state->maxWaitMs);
    if (has_blocked_exceptions(tso)) return EINTR;
  }
  memcpy(localDataPtr, mvar->dataPtr, state->dataSize);
  state->isFull = 0;
  pthread_cond_signal(&(state->canPutC));
  return 0;
}

int mvar_trytake(MVar *mvar, void *localDataPtr) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  // shortcut if is empty
  if (!(state->isFull)) return 1;
  while (state->pendingReaders > 0) {
    // make sure readers do not sleep
    pthread_cond_broadcast(&(state->canTakeC));
    // last reader should wake me up, wait for it.
    WAIT_A_BIT(&(state->canTakeC), &(state->mvMut), state->maxWaitMs);
    // repeat emptyness check (if another trytake was faster)
    if (!(state->isFull)) {
      return 1;
    }
  }
  memcpy(localDataPtr, mvar->dataPtr, state->dataSize);
  state->isFull = 0;
  pthread_cond_signal(&(state->canPutC));
  return 0;
}

int mvar_put(MVar *mvar, void *localDataPtr, StgStablePtr tso) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  while (state->isFull) {
    WAIT_A_BIT(&(state->canPutC), &(state->mvMut), state->maxWaitMs);
    if (has_blocked_exceptions(tso)) return EINTR;
  }
  memcpy(mvar->dataPtr, localDataPtr, state->dataSize);
  state->isFull = 1;
  pthread_cond_broadcast(&(state->canTakeC));
  return 0;
}

int mvar_tryput(MVar *mvar, void *localDataPtr) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  // shortcut if is full
  if (state->isFull) return 1;
  memcpy(mvar->dataPtr, localDataPtr, state->dataSize);
  state->isFull = 1;
  pthread_cond_broadcast(&(state->canTakeC));
  return 0;
}

// Split mvar_read/worker to make sure pendingReaders counter is never leaked
static inline int mvar_read_loop(MVarState *state, StgStablePtr tso) {
  while (!(state->isFull)) {
    WAIT_A_BIT(&(state->canTakeC), &(state->mvMut), state->maxWaitMs);
    if (has_blocked_exceptions(tso)) return EINTR;
  }
  return 0;
}

int mvar_read(MVar *mvar, void *localDataPtr, StgStablePtr tso) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  state->pendingReaders++;
  int r = mvar_read_loop(state, tso);
  if (r == 0) memcpy(localDataPtr, mvar->dataPtr, state->dataSize);
  if ((--(state->pendingReaders)) == 0) pthread_cond_signal(&(state->canTakeC));
  return r;
}

int mvar_tryread(MVar *mvar, void *localDataPtr) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  // shortcut if is empty
  if (!(state->isFull)) return 1;
  memcpy(localDataPtr, mvar->dataPtr, state->dataSize);
  return 0;
}

int mvar_swap(MVar *mvar, void *inPtr, void *outPtr, StgStablePtr tso) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  while (!(state->isFull) || state->pendingReaders > 0) {
    if (state->pendingReaders > 0) {
      pthread_cond_broadcast(&(state->canTakeC));
    }
    WAIT_A_BIT(&(state->canTakeC), &(state->mvMut), state->maxWaitMs);
    if (has_blocked_exceptions(tso)) return EINTR;
  }
  memcpy(outPtr, mvar->dataPtr, state->dataSize);
  memcpy(mvar->dataPtr, inPtr, state->dataSize);
  pthread_cond_signal(&(state->canTakeC));
  return 0;
}

int mvar_tryswap(MVar *mvar, void *inPtr, void *outPtr) {
  MVarState *state = mvar->statePtr;
  USE_MUTEX(state);
  // shortcut if is empty
  if (!(state->isFull)) return 1;
  while (state->pendingReaders > 0) {
    // make sure readers do not sleep
    pthread_cond_broadcast(&(state->canTakeC));
    // last reader should wake me up, wait for it.
    WAIT_A_BIT(&(state->canTakeC), &(state->mvMut), state->maxWaitMs);
    // repeat emptyness check (if another trytake was faster)
    if (!(state->isFull)) return 1;
  }
  memcpy(outPtr, mvar->dataPtr, state->dataSize);
  memcpy(mvar->dataPtr, inPtr, state->dataSize);
  pthread_cond_signal(&(state->canPutC));
  return 0;
}

int mvar_isempty(MVar *mvar) { return mvar->statePtr->isFull == 0; }

void mvar_name(MVar *mvar, char *const name) { strcpy(name, mvar->mvarName); }