diff options
| -rw-r--r-- | store/store-tree.c | 158 | ||||
| -rw-r--r-- | store/test-tree-store.c | 41 |
2 files changed, 139 insertions, 60 deletions
diff --git a/store/store-tree.c b/store/store-tree.c index ad251d7..ecdced6 100644 --- a/store/store-tree.c +++ b/store/store-tree.c @@ -52,10 +52,52 @@ static const unsigned char record_magic[8] = {'P','U','R','E','R','E','C','1'}; enum { CHECKPOINT = 16 }; static const unsigned char branch_magic[4] = {'P','T','B','1'}; static const char tree_name[] = "pure.tree"; -/* Threads of one process take turns as the writer: the file lock arbitrates - * only between processes (it is the process's, and closing any descriptor of - * the lock file drops it). */ -static pthread_mutex_t writers = PTHREAD_MUTEX_INITIALIZER; +/* A store's turns in this process, one for each store directory (known by + * its identity: two paths to one directory share them). Threads take turns + * as its writer: the file lock arbitrates only between processes (it is the + * process's, and closing any descriptor of the lock file drops it). And + * their commits to it group (Group commits, below). Stores apart wait for + * nothing of each other's, a sync least of all. */ +typedef struct Turns { + dev_t device; + ino_t inode; + pthread_mutex_t writer; + pthread_mutex_t lock; /* the group's */ + pthread_cond_t finished, joined; + struct Waiter *head, *tail; + size_t queued, expected; /* expected: how many requests groups have held lately */ + bool leading; + unsigned long groups, requests; + struct Turns *next; +} Turns; +static struct { pthread_mutex_t lock; Turns *first; } stores = {PTHREAD_MUTEX_INITIALIZER, NULL}; + +/* The turns of the store whose directory is info, made the first time; + * NULL without memory. */ +static Turns *turns_for(const struct stat *info) { + pthread_mutex_lock(&stores.lock); + Turns *t = stores.first; + while (t && (t->device != info->st_dev || t->inode != info->st_ino)) t = t->next; + if (!t && (t = calloc(1, sizeof(*t)))) { + *t = (Turns){.device = info->st_dev, .inode = info->st_ino, .expected = 1, .next = stores.first}; + bool made = !pthread_mutex_init(&t->writer, NULL) && !pthread_mutex_init(&t->lock, NULL) && + !pthread_cond_init(&t->finished, NULL); + /* joined waits on the monotonic clock (core/wait.h); else it does not gather */ + if (made && pure_cond_monotonic(&t->joined)) made = !pthread_cond_init(&t->joined, NULL); + if (made) stores.first = t; + else { + free(t); + t = NULL; + } + } + pthread_mutex_unlock(&stores.lock); + return t; +} + +static Turns *turns_of(int directory) { + struct stat info; + return fstat(directory, &info) ? NULL : turns_for(&info); +} static uint64_t temporary_sequence; /* A reference is a file offset, or an index into this edit's node arena. @@ -853,8 +895,9 @@ static bool find(const Tree *tree, Entry catalog, const char *key, uint64_t *ind static int create_temporary(int directory, char name[96]) { for (unsigned attempt = 0; attempt < 64; ++attempt) { - if (temporary_sequence == UINT64_MAX) break; - snprintf(name, 96, ".pending-%ld-%" PRIu64, (long)getpid(), ++temporary_sequence); + uint64_t sequence = __atomic_add_fetch(&temporary_sequence, 1, __ATOMIC_RELAXED); /* writers of other stores too */ + if (sequence == UINT64_MAX) break; + snprintf(name, 96, ".pending-%ld-%" PRIu64, (long)getpid(), sequence); int fd = openat(directory, name, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (fd >= 0) return fd; if (errno != EEXIST) break; @@ -945,7 +988,12 @@ PureResult pure_store_init(const char *path) { if (!created && errno != EEXIST) return PURE_STORE_ERROR; int directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (directory < 0) return PURE_STORE_ERROR; /* Never reinterpret an old DB file. */ - pthread_mutex_lock(&writers); + Turns *turns = turns_of(directory); + if (!turns) { + close(directory); + return PURE_STORE_ERROR; + } + pthread_mutex_lock(&turns->writer); int lock = openat(directory, ".writer.lock", O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0600); struct stat info; bool ok = lock >= 0 && fstat(lock, &info) == 0 && S_ISREG(info.st_mode) && lock_writer(lock, true); @@ -967,7 +1015,7 @@ PureResult pure_store_init(const char *path) { } ok = ok && sync_fd(lock) && sync_fd(directory); if (lock >= 0) close(lock); - pthread_mutex_unlock(&writers); + pthread_mutex_unlock(&turns->writer); close(directory); if (ok && created) { char *parent = strdup(path); @@ -1073,7 +1121,7 @@ typedef struct { uint64_t slots[2]; /* the sequences of the slots' heads */ Tree tree; struct stat file; - bool turn; /* holds this process's writer turn */ + Turns *turns; /* whose writer's turn it holds, once it does */ } Session; /* Keep written zeros after the published end, where commits and their @@ -1091,16 +1139,18 @@ static void close_session(Session *session) { if (session->fd >= 0) close(session->fd); if (session->lock >= 0) close(session->lock); if (session->directory >= 0) close(session->directory); - if (session->turn) pthread_mutex_unlock(&writers); - session->turn = false; + if (session->turns) pthread_mutex_unlock(&session->turns->writer); + session->turns = NULL; } static PureResult open_session(const char *path, Session *session) { *session = (Session){.directory = -1, .lock = -1, .fd = -1}; - pthread_mutex_lock(&writers); /* another thread of this process waits, not fails */ - session->turn = true; session->directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (session->directory < 0) return PURE_STORE_ERROR; + Turns *turns = turns_of(session->directory); + if (!turns) return PURE_STORE_ERROR; + pthread_mutex_lock(&turns->writer); /* another thread of this process waits, not fails */ + session->turns = turns; session->lock = openat(session->directory, ".writer.lock", O_RDWR | O_NOFOLLOW | O_CLOEXEC); struct stat info; if (session->lock < 0 || fstat(session->lock, &info) || !S_ISREG(info.st_mode)) return PURE_STORE_ERROR; @@ -1575,31 +1625,21 @@ typedef struct Waiter { struct Waiter *next; } Waiter; -static struct { - pthread_mutex_t lock; - pthread_cond_t finished, joined; - Waiter *head, *tail; - size_t queued, expected; /* expected: how many requests groups have held lately */ - bool leading; - unsigned long groups, requests; -} group = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER, NULL, NULL, 0, 1, false, 0, 0}; - +/* Every store's groups so far, and the requests they held. */ void pure_store_group_counts(unsigned long *groups, unsigned long *requests) { - pthread_mutex_lock(&group.lock); - *groups = group.groups; - *requests = group.requests; - pthread_mutex_unlock(&group.lock); + *groups = *requests = 0; + pthread_mutex_lock(&stores.lock); + for (Turns *t = stores.first; t; t = t->next) { + pthread_mutex_lock(&t->lock); + *groups += t->groups; + *requests += t->requests; + pthread_mutex_unlock(&t->lock); + } + pthread_mutex_unlock(&stores.lock); } enum { GATHER_NS = 2000000 }; -/* Once: joined waits on the monotonic clock (core/wait.h). */ -static pthread_once_t group_made = PTHREAD_ONCE_INIT; -static void make_group(void) { - pthread_cond_destroy(&group.joined); - if (pure_cond_monotonic(&group.joined)) pthread_cond_init(&group.joined, NULL); /* then it does not gather */ -} - /* Publish the queued requests of the store at path in one session. */ static void commit_group(const char *path, Waiter *first) { size_t total = 0; @@ -1702,46 +1742,48 @@ PureResult pure_draft_commit_all(PureDraft *const *drafts, size_t count, int64_t for (size_t k = 0; k < i; ++k) if (!strcmp(drafts[k]->key, drafts[i]->key)) return PURE_INVALID; } if (!count) return PURE_OK; + struct stat info; + Turns *group = stat(drafts[0]->path, &info) ? NULL : turns_for(&info); + if (!group) return PURE_STORE_ERROR; Waiter me = {.drafts = drafts, .count = count, .revisions = revisions}; - pthread_once(&group_made, make_group); - pthread_mutex_lock(&group.lock); - if (group.tail) group.tail->next = &me; else group.head = &me; - group.tail = &me; - ++group.queued; - pthread_cond_signal(&group.joined); + pthread_mutex_lock(&group->lock); + if (group->tail) group->tail->next = &me; else group->head = &me; + group->tail = &me; + ++group->queued; + pthread_cond_signal(&group->joined); while (!me.done) { - if (group.leading) { pthread_cond_wait(&group.finished, &group.lock); continue; } - group.leading = true; + if (group->leading) { pthread_cond_wait(&group->finished, &group->lock); continue; } + group->leading = true; /* When several threads have been committing together, give the others * a moment (at most 2 ms) to join, or they would miss this group and * wait for the next one: a sync costs far more. One thread never waits. */ - if (group.expected > 1 && group.queued < group.expected) { + if (group->expected > 1 && group->queued < group->expected) { struct timespec deadline = pure_wait_until(GATHER_NS); - while (group.queued < group.expected && - pthread_cond_timedwait(&group.joined, &group.lock, &deadline) == 0) {} + while (group->queued < group->expected && + pthread_cond_timedwait(&group->joined, &group->lock, &deadline) == 0) {} } - Waiter *batch = group.head; - size_t size = group.queued; - group.head = group.tail = NULL; - group.queued = 0; - ++group.groups; - group.requests += size; + Waiter *batch = group->head; + size_t size = group->queued; + group->head = group->tail = NULL; + group->queued = 0; + ++group->groups; + group->requests += size; /* Remember the largest recent group, forgetting one member per group. */ - group.expected = size > group.expected - 1 ? size : group.expected - 1; - if (!group.expected) group.expected = 1; - pthread_mutex_unlock(&group.lock); - /* One session per store among the queued requests. */ + group->expected = size > group->expected - 1 ? size : group->expected - 1; + if (!group->expected) group->expected = 1; + pthread_mutex_unlock(&group->lock); + /* One session per path among the queued requests (two paths may name the store). */ for (Waiter *w = batch; w; w = w->next) { bool earlier = false; for (Waiter *v = batch; v != w && !earlier; v = v->next) earlier = !strcmp(v->drafts[0]->path, w->drafts[0]->path); if (!earlier) commit_group(w->drafts[0]->path, batch); } - pthread_mutex_lock(&group.lock); + pthread_mutex_lock(&group->lock); for (Waiter *w = batch, *next; w; w = next) { next = w->next; w->done = true; } - group.leading = false; - pthread_cond_broadcast(&group.finished); + group->leading = false; + pthread_cond_broadcast(&group->finished); } - pthread_mutex_unlock(&group.lock); + pthread_mutex_unlock(&group->lock); return me.result; } diff --git a/store/test-tree-store.c b/store/test-tree-store.c index 3ec55c0..2ef9d22 100644 --- a/store/test-tree-store.c +++ b/store/test-tree-store.c @@ -1086,6 +1086,43 @@ static void pending_head(const char *path) { expect(path, "pending", &(Oracle){(unsigned char *)"new", 3, 2}); } +/* Each store has its writer's turn: while a commit to one waits in its sync, + * another store commits, and a second commit to the held store waits for + * the first. A second path to a store's directory takes the store's turn. */ +typedef struct { const char *path, *key; int64_t expected; int done; } Commit; +static void clean(const char *path); + +static void *put_one(void *context) { + Commit *c = context; + int64_t revision; + CHECK(pure_store_put(c->path, c->key, c->expected, (PureSpan){(const unsigned char *)"v", 1}, &revision) == PURE_OK); + __atomic_store_n(&c->done, 1, __ATOMIC_SEQ_CST); + return NULL; +} + +static void stores_apart(const char *path) { + char other[] = "/tmp/pure-tree-other-XXXXXX", alias[96]; + CHECK(mkdtemp(other) && pure_store_init(other) == PURE_OK); + snprintf(alias, sizeof(alias), "%s/.", path); + __atomic_store_n(&held_sync, 1, __ATOMIC_SEQ_CST); + Commit held = {path, "held", 0, 0}, second = {alias, "second", 0, 0}; + pthread_t first, next; + CHECK(pthread_create(&first, NULL, put_one, &held) == 0); + while (__atomic_load_n(&held_sync, __ATOMIC_SEQ_CST) != 2) pause_ms(1); + int64_t revision; + CHECK(pure_store_put(other, "free", 0, (PureSpan){(const unsigned char *)"w", 1}, &revision) == PURE_OK); + CHECK(revision == 1 && !__atomic_load_n(&held.done, __ATOMIC_SEQ_CST)); + CHECK(pthread_create(&next, NULL, put_one, &second) == 0); + pause_ms(50); + CHECK(!__atomic_load_n(&second.done, __ATOMIC_SEQ_CST)); /* the same store, by another path: it waits */ + __atomic_store_n(&held_sync, 0, __ATOMIC_SEQ_CST); + CHECK(pthread_join(first, NULL) == 0 && pthread_join(next, NULL) == 0); + expect(path, "held", &(Oracle){(unsigned char *)"v", 1, 1}); + expect(path, "second", &(Oracle){(unsigned char *)"v", 1, 1}); + expect(other, "free", &(Oracle){(unsigned char *)"w", 1, 1}); + clean(other); +} + /* A commit costs one sync, and commits into the written zeros after the end * leave the file size alone (no journal commit for a new size). */ static void one_sync(const char *path) { @@ -1157,7 +1194,7 @@ int main(void) { isolation, corruption, many_keys, revision_limit, keys, drafts, typing, draft_conflict, draft_relocation, draft_faults, draft_versions, anonymous_draft, batch, store_handle, threads, grouped, - lost_commit, pending_head, one_sync, records, cycles}; + lost_commit, pending_head, stores_apart, one_sync, records, cycles}; for (size_t i = 0; i < sizeof(checks) / sizeof(*checks); ++i) { char path[] = "/tmp/pure-tree-check-XXXXXX"; CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK); @@ -1168,7 +1205,7 @@ int main(void) { puts("{\"tree_store\":\"random-oracle-balance-held-views-one-byte-amplification-faults-" "process-crash-torn-head-lockless-isolation-compaction-corruption-1024-keys-revision-limit-key-listing-" "draft-oracle-typing-arena-conflict-relocation-commit-faults-held-versions-anonymous-" - "atomic-batch-runtime-keys-store-handle-threads-group-commit-lost-commit-pending-head-one-sync-records-" + "atomic-batch-runtime-keys-store-handle-threads-group-commit-lost-commit-pending-head-stores-apart-one-sync-records-" "cycles\"}"); return 0; } |
