diff options
| -rw-r--r-- | prototypes/connections/serve.c | 7 | ||||
| -rw-r--r-- | prototypes/connections/state.h | 2 | ||||
| -rw-r--r-- | prototypes/connections/store-tree.c | 237 | ||||
| -rw-r--r-- | prototypes/connections/test-tree-store.c | 51 |
4 files changed, 239 insertions, 58 deletions
diff --git a/prototypes/connections/serve.c b/prototypes/connections/serve.c index f90772a..349bacf 100644 --- a/prototypes/connections/serve.c +++ b/prototypes/connections/serve.c @@ -687,9 +687,12 @@ int main(int argc, char **argv) { expired += hosts[i].expired; stop_shard(&hosts[i]); } + unsigned long groups, grouped; + pure_store_group_counts(&groups, &grouped); fprintf(stderr, "{\"active\":%zu,\"retained_payload_bytes\":%zu,\"accepted\":%" PRIu64 ",\"requests\":%" PRIu64 - ",\"bursts\":%" PRIu64 ",\"burst_events\":%" PRIu64 ",\"expired\":%" PRIu64 ",\"threads\":%u}\n", - active, retained, accepted, requests, bursts, events, expired, threads); + ",\"bursts\":%" PRIu64 ",\"burst_events\":%" PRIu64 ",\"expired\":%" PRIu64 ",\"threads\":%u" + ",\"commit_groups\":%lu,\"grouped_commits\":%lu}\n", + active, retained, accepted, requests, bursts, events, expired, threads, groups, grouped); free(hosts); free(thread); return 0; diff --git a/prototypes/connections/state.h b/prototypes/connections/state.h index 60d333c..55474c0 100644 --- a/prototypes/connections/state.h +++ b/prototypes/connections/state.h @@ -61,5 +61,7 @@ PureResult pure_draft_commit(PureDraft *, int64_t *revision); * draft's base revision (a conflict names the stored revision in revisions), * and either every draft is published or none is. Keys must differ. */ PureResult pure_draft_commit_all(PureDraft *const *drafts, size_t count, int64_t *revisions); +/* Commits made so far in this process: groups formed, and requests in them. */ +void pure_store_group_counts(unsigned long *groups, unsigned long *requests); void pure_draft_close(PureDraft *); #endif diff --git a/prototypes/connections/store-tree.c b/prototypes/connections/store-tree.c index 97add45..5e15304 100644 --- a/prototypes/connections/store-tree.c +++ b/prototypes/connections/store-tree.c @@ -871,8 +871,16 @@ static PureResult open_session(const char *path, Session *session) { * the catalog (revisions come in as the base and leave as the new one), * then make the commit durable before its head becomes visible: every * document becomes visible at once, or none does. */ +#ifdef PURE_STORE_TEST +static unsigned long publications; +unsigned long pure_tree_test_publications(void) { return publications; } +#endif + static PureResult publish(Session *session, Writer *writer, size_t count, const char *const *keys, const Entry *documents, int64_t *revisions) { +#ifdef PURE_STORE_TEST + ++publications; +#endif Entry catalog = {session->head.catalog, session->head.catalog_bytes}; bool written = true; for (size_t i = 0; written && i < count; ++i) { @@ -1248,6 +1256,140 @@ void pure_draft_hold(PureDraft *draft, bool held) { if (!held) collect(draft); } +/* ---- Group commits ------------------------------------------------------ + * Threads that commit at the same time share one commit: they queue, the + * first becomes the leader and publishes every queued request of the same + * store with one head (one pair of syncs); the others wait for its result. + * Each request stays all or nothing on its own: a request with a stale draft + * is left out, and so is one whose key an earlier request of the same group + * already changes. The leader touches the waiters' drafts and handles only + * while they wait. */ +typedef struct Waiter { + PureDraft *const *drafts; + size_t count; + int64_t *revisions; + PureResult result; + bool accepted, done; + 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}; + +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); +} + +enum { GATHER_NS = 2000000 }; + +/* 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; + for (Waiter *w = first; w; w = w->next) + if (!strcmp(w->drafts[0]->path, path)) { total += w->count; w->accepted = false; } + Session session; + Writer *writer = NULL; + Entry *roots = malloc(total * sizeof(*roots)); + const char **keys = malloc(total * sizeof(*keys)); + int64_t *revisions = malloc(total * sizeof(*revisions)); + PureDraft **drafts = malloc(total * sizeof(*drafts)); + bool allocated = roots && keys && revisions && drafts, checked = false; + PureResult result = allocated ? open_session(path, &session) : PURE_ALLOCATION; + size_t count = 0; + if (result == PURE_OK) { + /* A request holds when each of its keys is at its draft's base + * revision and no earlier request of this group changes it. */ + for (Waiter *w = first; w; w = w->next) { + if (strcmp(w->drafts[0]->path, path)) continue; + w->result = PURE_OK; + for (size_t i = 0; i < w->count && w->result == PURE_OK; ++i) { + PureDraft *d = w->drafts[i]; + Entry current; + uint64_t index; + int64_t stored; + bool found; + if (!find(&session.tree, (Entry){session.head.catalog, session.head.catalog_bytes}, d->key, &index, + &found, ¤t, &stored)) { w->result = PURE_STORE_ERROR; break; } + for (size_t k = 0; k < count; ++k) + if (!strcmp(keys[k], d->key)) { ++stored; break; } /* it moves with the earlier request */ + if (stored != d->revision) { w->revisions[i] = stored; w->result = PURE_CONFLICT; } + else if (stored == INT64_MAX) w->result = PURE_LIMIT; + } + if (w->result != PURE_OK) continue; + w->accepted = true; + for (size_t i = 0; i < w->count; ++i) { + drafts[count] = w->drafts[i]; + roots[count] = w->drafts[i]->root; + keys[count] = w->drafts[i]->key; + revisions[count++] = w->drafts[i]->revision; + } + } + checked = true; + if (count) { + result = PURE_ALLOCATION; + if ((writer = malloc(sizeof(*writer)))) { + *writer = (Writer){.fd = session.fd, .at = session.head.end}; + bool written = true; + for (size_t i = 0; i < count && written; ++i) { + /* After compaction a draft's stored references name the old file. */ + bool relocate = session.file.st_dev != drafts[i]->device || session.file.st_ino != drafts[i]->inode; + written = write_root(&drafts[i]->tree, writer, &roots[i], relocate); + } + if (written) result = publish(&session, writer, count, keys, roots, revisions); + else { + if (ftruncate(session.fd, (off_t)session.head.end) == 0) sync_fd(session.fd); + result = PURE_STORE_ERROR; + } + } + } + /* Rebase while still holding the writer lock; the arenas are no longer + * needed. Drafts of a store handle share one new mapping. If remapping + * fails a draft keeps equal content at its new revision. */ + PureStore *refreshed[8] = {NULL}; + size_t fresh = 0; + for (size_t i = 0; result == PURE_OK && i < count; ++i) { + PureStore *store = drafts[i]->store; + bool rebased; + if (store) { + bool seen = false; + for (size_t k = 0; k < fresh && !seen; ++k) seen = refreshed[k] == store; + if (!seen && refresh(store) == PURE_OK) { + seen = true; + if (fresh < 8) refreshed[fresh++] = store; + } + rebased = seen && rebase(drafts[i]) == PURE_OK; + } else rebased = load_draft(drafts[i]) == PURE_OK; + if (!rebased) drafts[i]->revision = revisions[i]; + } + } + /* Before the checks every request shares the failure; after them an + * accepted request takes the publication's result and new revisions, and + * a refused one keeps its own result. */ + size_t at = 0; + for (Waiter *w = first; w; w = w->next) { + if (strcmp(w->drafts[0]->path, path)) continue; + if (!checked) { w->result = result; continue; } + if (!w->accepted) continue; + for (size_t i = 0; i < w->count; ++i) w->revisions[i] = revisions[at++]; + w->result = result; + } + free(writer); + free(roots); + free((void *)keys); + free(revisions); + free(drafts); + if (allocated) close_session(&session); +} + PureResult pure_draft_commit_all(PureDraft *const *drafts, size_t count, int64_t *revisions) { for (size_t i = 0; i < count; ++i) { revisions[i] = drafts[i]->revision; @@ -1255,62 +1397,49 @@ 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; - Session session; - Writer *writer = NULL; - Entry *roots = malloc(count * sizeof(*roots)); - const char **keys = malloc(count * sizeof(*keys)); - PureResult result = roots && keys ? open_session(drafts[0]->path, &session) : PURE_ALLOCATION; - if (result != PURE_OK) { - if (roots && keys) close_session(&session); - free(roots); - free((void *)keys); - return result; - } - result = PURE_STORE_ERROR; - /* Every key must still be at its draft's base revision. */ - for (size_t i = 0; i < count; ++i) { - Entry current; - uint64_t index; - int64_t stored; - bool found; - if (!find(&session.tree, (Entry){session.head.catalog, session.head.catalog_bytes}, drafts[i]->key, - &index, &found, ¤t, &stored)) goto finish; - if (stored != drafts[i]->revision) { revisions[i] = stored; result = PURE_CONFLICT; goto finish; } - if (stored == INT64_MAX) { result = PURE_LIMIT; goto finish; } - roots[i] = drafts[i]->root; - keys[i] = drafts[i]->key; - } - writer = malloc(sizeof(*writer)); - if (!writer) goto finish; - *writer = (Writer){.fd = session.fd, .at = session.head.end}; - for (size_t i = 0; i < count; ++i) { - /* After compaction a draft's stored references name the old file. */ - bool relocate = session.file.st_dev != drafts[i]->device || session.file.st_ino != drafts[i]->inode; - if (!write_root(&drafts[i]->tree, writer, &roots[i], relocate)) { - if (ftruncate(session.fd, (off_t)session.head.end) == 0) sync_fd(session.fd); - goto finish; + Waiter me = {.drafts = drafts, .count = count, .revisions = revisions}; + 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; + /* 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) { + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += GATHER_NS; + if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } + 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; + /* 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. */ + 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); + for (Waiter *w = batch, *next; w; w = next) { next = w->next; w->done = true; } + group.leading = false; + pthread_cond_broadcast(&group.finished); } - result = publish(&session, writer, count, keys, roots, revisions); - /* Rebase while still holding the writer lock; the arenas are no longer - * needed. Drafts of a store handle share one new mapping. If remapping - * fails a draft keeps equal content at its new revision. */ - PureStore *refreshed = NULL; - for (size_t i = 0; result == PURE_OK && i < count; ++i) { - PureStore *store = drafts[i]->store; - bool rebased; - if (store) { - if (store != refreshed && refresh(store) == PURE_OK) refreshed = store; - rebased = store == refreshed && rebase(drafts[i]) == PURE_OK; - } else rebased = load_draft(drafts[i]) == PURE_OK; - if (!rebased) drafts[i]->revision = revisions[i]; - } -finish: - free(writer); - free(roots); - free((void *)keys); - close_session(&session); - return result; + pthread_mutex_unlock(&group.lock); + return me.result; } PureResult pure_draft_commit(PureDraft *draft, int64_t *revision) { diff --git a/prototypes/connections/test-tree-store.c b/prototypes/connections/test-tree-store.c index bc8ba4b..9cb0701 100644 --- a/prototypes/connections/test-tree-store.c +++ b/prototypes/connections/test-tree-store.c @@ -10,13 +10,20 @@ #include <sys/stat.h> #include <pthread.h> #include <sys/wait.h> +#include <time.h> #include <unistd.h> #define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); abort(); } } while (0) static int fail_stage = -1; static int crash; +static int slow_once; /* the next publication pauses before its data sync */ +static void pause_ms(long ms) { + struct timespec wait = {ms / 1000, (ms % 1000) * 1000000}; + nanosleep(&wait, NULL); +} int pure_store_test_fault(PureStoreStage stage) { + if (stage == PURE_BEFORE_FILE_SYNC && __atomic_exchange_n(&slow_once, 0, __ATOMIC_SEQ_CST)) pause_ms(100); if ((int)stage != fail_stage) return 0; if (crash) _exit(77); return 1; @@ -25,6 +32,7 @@ bool pure_tree_test_shape(const char *path, const char *key, unsigned *height, uint64_t *leaves, uint64_t *small, uint64_t *file_end); void pure_draft_test_arena(const PureDraft *draft, size_t *nodes, size_t *bytes); +unsigned long pure_tree_test_publications(void); enum { KEYS = 4, CAPACITY = 1 << 20 }; typedef struct { unsigned char *bytes; size_t size; int64_t revision; } Oracle; @@ -880,6 +888,45 @@ static void threads(const char *path) { pure_store_release(&view); } +/* Commits that arrive while another is being published share the next + * publication: one head for all of them. */ +typedef struct { const char *path; unsigned number; PureResult result; } Single; + +static void *commit_one(void *context) { + Single *s = context; + PureStore *store; + PureDraft *draft; + char key[16]; + int64_t revision; + snprintf(key, sizeof(key), "g%u", s->number); + CHECK(pure_store_open(s->path, &store) == PURE_OK && pure_store_draft(store, key, &draft) == PURE_OK); + CHECK(pure_draft_splice(draft, 0, 0, (PureSpan){(const unsigned char *)key, strlen(key)}) == PURE_OK); + s->result = pure_draft_commit(draft, &revision); + CHECK(s->result != PURE_OK || revision == 1); + pure_draft_close(draft); + pure_store_close(store); + return NULL; +} + +static void grouped(const char *path) { + pthread_t thread[5]; + Single single[5]; + unsigned long before = pure_tree_test_publications(); + __atomic_store_n(&slow_once, 1, __ATOMIC_SEQ_CST); + for (unsigned i = 0; i < 5; ++i) { + single[i] = (Single){path, i, PURE_INVALID}; + CHECK(pthread_create(&thread[i], NULL, commit_one, &single[i]) == 0); + if (!i) pause_ms(20); /* the first is now publishing, slowly */ + } + for (unsigned i = 0; i < 5; ++i) CHECK(pthread_join(thread[i], NULL) == 0 && single[i].result == PURE_OK); + CHECK(pure_tree_test_publications() - before == 2); + char key[16]; + for (unsigned i = 0; i < 5; ++i) { + snprintf(key, sizeof(key), "g%u", i); + expect(path, key, &(Oracle){(unsigned char *)key, strlen(key), 1}); + } +} + static void clean(const char *path) { DIR *entries = opendir(path); CHECK(entries); @@ -896,7 +943,7 @@ int main(void) { void (*checks[])(const char *) = {oracle, amplification, faults, crashes, torn_head, isolation, corruption, many_keys, revision_limit, keys, drafts, typing, draft_conflict, draft_relocation, draft_faults, - draft_versions, anonymous_draft, batch, store_handle, threads}; + draft_versions, anonymous_draft, batch, store_handle, threads, grouped}; 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); @@ -907,6 +954,6 @@ 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\"}"); + "atomic-batch-runtime-keys-store-handle-threads-group-commit\"}"); return 0; } |
