aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes')
-rw-r--r--prototypes/connections/Makefile2
-rw-r--r--prototypes/connections/README.md2
-rw-r--r--prototypes/connections/state.h31
-rw-r--r--prototypes/connections/store-tree.c193
-rw-r--r--prototypes/connections/test-tree-store.c218
5 files changed, 437 insertions, 9 deletions
diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile
index 661fbcd..a416bee 100644
--- a/prototypes/connections/Makefile
+++ b/prototypes/connections/Makefile
@@ -11,7 +11,7 @@ HOST = $(STORE) store-port.c evaluate.c
# Document browsers list keys through the store. The tree store keeps its own
# catalog; per-object file stores share a directory listing.
KEYS = $(if $(filter store-tree.c,$(STORE)),,store-keys.c)
-HEADERS = runtime.h program.h store.h store-port.h flow.h result.h
+HEADERS = runtime.h program.h store.h store-port.h state.h flow.h result.h
all: core fixed-editor
core: $(BUILD)/libpure-execution.a
async-core: $(BUILD)/libpure-execution-async.a
diff --git a/prototypes/connections/README.md b/prototypes/connections/README.md
index f0b041e..94d05f0 100644
--- a/prototypes/connections/README.md
+++ b/prototypes/connections/README.md
@@ -148,6 +148,8 @@ python3 prototypes/connections/export_flat_store.py \
트리로 둔다. 편집은 바뀐 leaf와 경로만 기록하고, 읽기는 잠그지 않는다. 기존 런타임·부분 수정·
비동기·실제 PTY 검사와 자체 고장·오라클 검사를 함께 실행한다. 형식이 달라 별도 `--store`
디렉터리를 쓴다. [설계·측정·한계](../../docs/research/073-single-file-persistent-tree.md).
+같은 저장소에서 런타임이 앱의 작업 버전을 소유하는 draft API는 [`state.h`](state.h)이며,
+설계와 측정은 [074](../../docs/research/074-runtime-owned-drafts.md)에 있다.
```sh
make -C prototypes/connections check-tree
diff --git a/prototypes/connections/state.h b/prototypes/connections/state.h
new file mode 100644
index 0000000..d1bc0fa
--- /dev/null
+++ b/prototypes/connections/state.h
@@ -0,0 +1,31 @@
+#ifndef PURE_STATE_H
+#define PURE_STATE_H
+#include "flow.h"
+#include "result.h"
+#include <stdint.h>
+
+/* Runtime-owned working versions of stored values (selected tree store).
+ * A draft starts at the committed version of a key and changes only in
+ * memory: an edit copies the leaf and path it touches and shares every other
+ * extent with the stored version. An app therefore keeps no copy of the value
+ * and never compares a copy with storage to find what it changed. Commit
+ * appends only the draft's new extents. One owner thread per draft. */
+typedef struct PureDraft PureDraft;
+
+/* A missing key opens as an empty draft at revision 0; commit creates it. */
+PureResult pure_draft_open(const char *path, const char *key, PureDraft **out);
+/* Memory only. Fails without change on a bad range or when memory runs out. */
+PureResult pure_draft_splice(PureDraft *, size_t offset, size_t remove, PureSpan insert);
+size_t pure_draft_size(const PureDraft *);
+/* The committed revision this draft is based on; 0 if the key was absent. */
+int64_t pure_draft_revision(const PureDraft *);
+/* Visit [offset, offset + length) in order as borrowed extents, without
+ * copying. Returning false stops early. Spans stay valid until the next
+ * splice, commit or close of this draft. */
+PureResult pure_draft_read(const PureDraft *, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *context);
+/* Publish if the key is still at the draft's base revision. On success the
+ * draft continues from the new revision; on any failure it is unchanged. */
+PureResult pure_draft_commit(PureDraft *, int64_t *revision);
+void pure_draft_close(PureDraft *);
+#endif
diff --git a/prototypes/connections/store-tree.c b/prototypes/connections/store-tree.c
index 618028a..1c0ad91 100644
--- a/prototypes/connections/store-tree.c
+++ b/prototypes/connections/store-tree.c
@@ -10,6 +10,7 @@
#define _DARWIN_C_SOURCE
#endif
#include "store.h"
+#include "state.h"
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
@@ -20,6 +21,7 @@
#include <sys/stat.h>
#include <unistd.h>
+enum { FREE = 0xffff };
enum { SLOT = 64, DATA = 2 * SLOT, LEAF_MAX = 4096, LEAF_MIN = 1024,
FAN_MAX = 32, FAN_MIN = 8, RECORD = 96, KEY = 64, HEIGHT_MAX = 12,
BUFFER = 65536 };
@@ -43,14 +45,15 @@ typedef struct { Entry *items; size_t count, capacity; } List;
typedef struct {
unsigned char *bytes; /* leaf only */
Entry *children; /* branch only */
- size_t size; /* leaf bytes or child count */
- unsigned height;
+ size_t size; /* leaf bytes, child count, or next free slot + 1 */
+ unsigned height; /* FREE for a reclaimed slot */
} Node;
typedef struct {
const unsigned char *map;
uint64_t end;
Node *nodes;
- size_t count, capacity;
+ size_t count, capacity, free_head, free_count; /* free_head is slot + 1 */
+ size_t bytes; /* payload held by arena nodes, including unreachable ones */
} Tree;
typedef struct { uint64_t seq, end, catalog, catalog_bytes, live; } Head;
typedef struct {
@@ -309,11 +312,23 @@ static bool push(List *list, Entry entry) {
return true;
}
+static size_t node_bytes(const Node *node) {
+ return node->bytes ? node->size : node->size * sizeof(Entry);
+}
+
static bool add_node(Tree *tree, Node node, uint64_t *index) {
+ tree->bytes += node_bytes(&node);
+ if (tree->free_head) {
+ *index = tree->free_head - 1;
+ tree->free_head = tree->nodes[*index].size;
+ --tree->free_count;
+ tree->nodes[*index] = node;
+ return true;
+ }
if (tree->count == tree->capacity) {
size_t capacity = tree->capacity ? 2 * tree->capacity : 64;
Node *nodes = realloc(tree->nodes, capacity * sizeof(*nodes));
- if (!nodes) return false;
+ if (!nodes) { tree->bytes -= node_bytes(&node); return false; }
tree->nodes = nodes;
tree->capacity = capacity;
}
@@ -537,8 +552,9 @@ static uint64_t put_branch(Writer *writer, const Entry *kids, size_t count, unsi
}
/* Append a subtree's arena nodes, children first, and set entry to the
- * stored reference without changing the arena. With relocate, stored nodes
- * are copied too; compaction uses this to rewrite reachable nodes. */
+ * stored reference. The arena is not changed, so a failed commit leaves a
+ * draft intact. With relocate, stored nodes are copied too (compaction, or a
+ * draft whose base file was replaced by compaction). */
static bool write_subtree(const Tree *tree, Writer *writer, Entry *entry, unsigned height,
bool relocate) {
if (!entry->bytes || (!(entry->ref & MEMORY_BIT) && !relocate)) return true;
@@ -918,6 +934,160 @@ PureResult pure_store_keys(const char *path, bool (*visit)(void *, const char *k
return result;
}
+struct PureDraft {
+ char *path;
+ char key[KEY + 1];
+ Tree tree; /* the base file's mapping plus this draft's node arena */
+ dev_t device; /* identity of the base file; compaction replaces it */
+ ino_t inode;
+ Entry root;
+ int64_t revision;
+ size_t live; /* arena bytes reachable at the last collection */
+};
+
+/* Rebase the draft on the committed version of its key. */
+static PureResult load_draft(PureDraft *draft) {
+ int directory = open(draft->path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (directory < 0) return PURE_STORE_ERROR;
+ int fd = openat(directory, tree_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+ close(directory);
+ if (fd < 0) return PURE_STORE_ERROR;
+ Tree tree = {0};
+ Head head;
+ struct stat info;
+ Entry document;
+ uint64_t index;
+ int64_t revision;
+ bool found;
+ bool ok = !fstat(fd, &info) && read_head(fd, &head) && map_tree(fd, &head, &tree) &&
+ find(&tree, (Entry){head.catalog, head.catalog_bytes}, draft->key, &index, &found,
+ &document, &revision);
+ close(fd);
+ if (!ok) { release_tree(&tree); return PURE_STORE_ERROR; }
+ release_tree(&draft->tree);
+ draft->tree = tree;
+ draft->device = info.st_dev;
+ draft->inode = info.st_ino;
+ draft->root = document;
+ draft->revision = revision;
+ draft->live = 0;
+ return PURE_OK;
+}
+
+PureResult pure_draft_open(const char *path, const char *key, PureDraft **out) {
+ *out = NULL;
+ if (!path || !valid_key(key)) return PURE_INVALID;
+ PureDraft *draft = calloc(1, sizeof(*draft));
+ if (!draft || !(draft->path = strdup(path))) { free(draft); return PURE_ALLOCATION; }
+ memcpy(draft->key, key, strlen(key) + 1);
+ PureResult result = load_draft(draft);
+ if (result != PURE_OK) { pure_draft_close(draft); return result; }
+ *out = draft;
+ return PURE_OK;
+}
+
+void pure_draft_close(PureDraft *draft) {
+ if (!draft) return;
+ release_tree(&draft->tree);
+ free(draft->path);
+ free(draft);
+}
+
+size_t pure_draft_size(const PureDraft *draft) { return (size_t)draft->root.bytes; }
+
+int64_t pure_draft_revision(const PureDraft *draft) { return draft->revision; }
+
+static size_t mark(const Tree *tree, Entry entry, unsigned char *marks) {
+ if (!(entry.ref & MEMORY_BIT)) return 0;
+ size_t at = (size_t)(entry.ref & LOCATION);
+ if (marks[at]) return 0;
+ marks[at] = 1;
+ const Node *node = &tree->nodes[at];
+ size_t bytes = node_bytes(node);
+ if (!node->bytes)
+ for (size_t i = 0; i < node->size; ++i) bytes += mark(tree, node->children[i], marks);
+ return bytes;
+}
+
+/* Every edit copies a leaf and path, so reclaim arena nodes the root no
+ * longer reaches once they outweigh the reachable ones (and 32 KiB). */
+static void collect(PureDraft *draft) {
+ Tree *tree = &draft->tree;
+ size_t allowance = draft->live > 32768 ? draft->live : 32768;
+ if (tree->bytes <= draft->live || tree->bytes - draft->live <= allowance) return;
+ unsigned char *marks = calloc(tree->count, 1);
+ if (!marks) return;
+ draft->live = mark(tree, draft->root, marks);
+ for (size_t i = 0; i < tree->count; ++i) {
+ Node *node = &tree->nodes[i];
+ if (marks[i] || node->height == FREE) continue;
+ tree->bytes -= node_bytes(node);
+ free(node->bytes);
+ free(node->children);
+ *node = (Node){.size = tree->free_head, .height = FREE};
+ tree->free_head = i + 1;
+ ++tree->free_count;
+ }
+ free(marks);
+}
+
+PureResult pure_draft_splice(PureDraft *draft, size_t offset, size_t remove, PureSpan insert) {
+ uint64_t size = draft->root.bytes;
+ if (insert.size && !insert.data) return PURE_INVALID;
+ if (offset > size || remove > size - offset || insert.size > PURE_OBJECT_LIMIT - (size - remove))
+ return PURE_LIMIT;
+ if (!remove && !insert.size) return PURE_OK;
+ Entry next;
+ bool ok = rebuild(&draft->tree, draft->root, offset, remove, insert, &next);
+ if (ok) draft->root = next;
+ collect(draft);
+ return ok ? PURE_OK : PURE_STORE_ERROR;
+}
+
+PureResult pure_draft_read(const PureDraft *draft, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *context) {
+ unsigned height;
+ if (offset > draft->root.bytes || length > draft->root.bytes - offset) return PURE_LIMIT;
+ if (!length) return PURE_OK;
+ if (!root_height(&draft->tree, draft->root, &height)) return PURE_STORE_ERROR;
+ return visit_range(&draft->tree, draft->root, height, offset, length, visit, context) ?
+ PURE_OK : PURE_STORE_ERROR;
+}
+
+PureResult pure_draft_commit(PureDraft *draft, int64_t *revision) {
+ *revision = draft->revision;
+ Session session;
+ Writer *writer = NULL;
+ Entry current, root = draft->root;
+ uint64_t index;
+ int64_t stored;
+ bool found;
+ PureResult result = open_session(draft->path, &session);
+ if (result != PURE_OK) goto finish;
+ result = PURE_STORE_ERROR;
+ if (!find(&session.tree, (Entry){session.head.catalog, session.head.catalog_bytes}, draft->key,
+ &index, &found, &current, &stored)) goto finish;
+ if (stored != draft->revision) { *revision = stored; result = PURE_CONFLICT; goto finish; }
+ if (stored == INT64_MAX) { result = PURE_LIMIT; goto finish; }
+ /* After compaction the draft's stored references name the old file. */
+ bool relocate = session.file.st_dev != draft->device || session.file.st_ino != draft->inode;
+ writer = malloc(sizeof(*writer));
+ if (!writer) goto finish;
+ *writer = (Writer){.fd = session.fd, .at = session.head.end};
+ if (!write_root(&draft->tree, writer, &root, relocate)) {
+ if (ftruncate(session.fd, (off_t)session.head.end) == 0) sync_fd(session.fd);
+ goto finish;
+ }
+ result = publish(&session, writer, draft->key, index, found, root, revision);
+ /* Rebase while still holding the writer lock; the arena is no longer needed.
+ * If remapping fails the draft keeps equal content at the new revision. */
+ if (result == PURE_OK && load_draft(draft) != PURE_OK) draft->revision = *revision;
+finish:
+ free(writer);
+ close_session(&session);
+ return result;
+}
+
PureResult pure_store_put(const char *path, const char *key, int64_t expected,
PureSpan body, int64_t *revision) {
return mutate(path, key, expected, body, false, 0, 0, PURE_OBJECT_LIMIT, revision);
@@ -946,6 +1116,17 @@ static bool shape(const Tree *tree, Entry entry, unsigned height, bool root,
return true;
}
+/* Test-only: live arena nodes and their payload bytes. */
+void pure_draft_test_arena(const PureDraft *draft, size_t *nodes, size_t *bytes) {
+ *nodes = *bytes = 0;
+ for (size_t i = 0; i < draft->tree.count; ++i) {
+ const Node *node = &draft->tree.nodes[i];
+ if (node->height == FREE) continue;
+ ++*nodes;
+ *bytes += node->bytes ? node->size : node->size * sizeof(Entry);
+ }
+}
+
/* Test-only: the stored height, leaf count and number of underfull nodes. */
bool pure_tree_test_shape(const char *path, const char *key, unsigned *height,
uint64_t *leaves, uint64_t *small, uint64_t *file_end) {
diff --git a/prototypes/connections/test-tree-store.c b/prototypes/connections/test-tree-store.c
index 3a1e8f0..8cf1f1e 100644
--- a/prototypes/connections/test-tree-store.c
+++ b/prototypes/connections/test-tree-store.c
@@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "store.h"
+#include "state.h"
#include <assert.h>
#include <dirent.h>
#include <fcntl.h>
@@ -22,6 +23,8 @@ int pure_store_test_fault(PureStoreStage stage) {
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);
+
enum { KEYS = 4, CAPACITY = 1 << 20 };
typedef struct { unsigned char *bytes; size_t size; int64_t revision; } Oracle;
@@ -366,6 +369,215 @@ static void keys(const char *path) {
CHECK(pure_store_keys(path, count_key, &count) == PURE_OK && count == 5);
}
+typedef struct { unsigned char *out; size_t spans, largest, limit; } Gather;
+
+static bool gather(void *context, PureSpan span) {
+ Gather *g = context;
+ memcpy(g->out, span.data, span.size);
+ g->out += span.size;
+ if (span.size > g->largest) g->largest = span.size;
+ return ++g->spans != g->limit;
+}
+
+static void expect_draft(const PureDraft *draft, const Oracle *o, unsigned char *scratch) {
+ Gather g = {scratch, 0, 0, 0};
+ CHECK(pure_draft_size(draft) == o->size && pure_draft_revision(draft) == o->revision);
+ CHECK(pure_draft_read(draft, 0, o->size, gather, &g) == PURE_OK);
+ CHECK((size_t)(g.out - scratch) == o->size && (!o->size || !memcmp(scratch, o->bytes, o->size)));
+ CHECK(g.largest <= 4096);
+}
+
+static void apply(Oracle *o, size_t offset, size_t remove, const unsigned char *insert, size_t count) {
+ memmove(o->bytes + offset + count, o->bytes + offset + remove, o->size - offset - remove);
+ if (count) memcpy(o->bytes + offset, insert, count);
+ o->size = o->size - remove + count;
+}
+
+/* Edits live only in the draft until commit; each commit continues from the
+ * published version with an empty arena. */
+static void drafts(const char *path) {
+ Oracle o = {malloc(CAPACITY), 0, 0};
+ unsigned char *scratch = malloc(CAPACITY), *insert = malloc(65536);
+ CHECK(o.bytes && scratch && insert);
+ PureDraft *draft;
+ PureStoredView view;
+ CHECK(pure_draft_open(path, "draft", &draft) == PURE_OK);
+ expect_draft(draft, &o, scratch);
+ uint32_t state = 0x1234567u;
+ for (unsigned step = 0; step < 20000; ++step) {
+ uint32_t kind = random_value(&state) % 100;
+ size_t count = kind < 80 ? random_value(&state) % 8 :
+ kind < 98 ? random_value(&state) % 4000 : random_value(&state) % 65536;
+ size_t offset = o.size ? random_value(&state) % (o.size + 1) : 0;
+ size_t remove = o.size - offset ? random_value(&state) % (o.size - offset + 1) : 0;
+ if (random_value(&state) % 64) remove %= 16;
+ if (o.size - remove + count > CAPACITY) count = 0;
+ fill(insert, count, &state);
+ CHECK(pure_draft_splice(draft, offset, remove, (PureSpan){insert, count}) == PURE_OK);
+ apply(&o, offset, remove, insert, count);
+ if (step % 97 == 0) expect_draft(draft, &o, scratch);
+ if (step % 1000 == 999) {
+ if (o.revision) CHECK(pure_store_get(path, "draft", PURE_OBJECT_LIMIT, &view) == PURE_OK &&
+ view.revision == o.revision);
+ if (o.revision) pure_store_release(&view);
+ int64_t revision;
+ CHECK(pure_draft_commit(draft, &revision) == PURE_OK && revision == o.revision + 1);
+ o.revision = revision;
+ expect(path, "draft", &o);
+ expect_draft(draft, &o, scratch);
+ size_t nodes, bytes;
+ pure_draft_test_arena(draft, &nodes, &bytes);
+ CHECK(nodes == 0);
+ }
+ }
+ CHECK(o.size > 100000);
+ Gather g = {scratch, 0, 0, 3};
+ CHECK(pure_draft_read(draft, 0, o.size, gather, &g) == PURE_OK && g.spans == 3);
+ CHECK(pure_draft_splice(draft, o.size + 1, 0, (PureSpan){0}) == PURE_LIMIT);
+ CHECK(pure_draft_splice(draft, 0, o.size + 1, (PureSpan){0}) == PURE_LIMIT);
+ CHECK(pure_draft_read(draft, o.size, 1, gather, &g) == PURE_LIMIT);
+ expect_draft(draft, &o, scratch);
+ pure_draft_close(draft);
+ printf("{\"draft_steps\":20000,\"draft_commits\":%lld,\"draft_bytes\":%zu}\n",
+ (long long)o.revision, o.size);
+ free(o.bytes);
+ free(scratch);
+ free(insert);
+}
+
+/* Long typing and deleting at one place keeps only the live path in memory,
+ * and its commit appends the typed bytes and path, not the document. */
+static void typing(const char *path) {
+ size_t size = 1048576;
+ Oracle o = {malloc(size + 40000), size, 0};
+ unsigned char *scratch = malloc(size + 40000);
+ uint32_t state = 5;
+ CHECK(o.bytes && scratch);
+ fill(o.bytes, size, &state);
+ CHECK(pure_store_put(path, "typed", 0, (PureSpan){o.bytes, size}, &o.revision) == PURE_OK);
+ PureDraft *draft;
+ CHECK(pure_draft_open(path, "typed", &draft) == PURE_OK);
+ size_t cursor = size / 2, nodes, bytes, peak = 0;
+ for (unsigned i = 0; i < 30000; ++i) {
+ unsigned char byte = (unsigned char)('a' + i % 26);
+ if (i % 3 == 2) {
+ CHECK(pure_draft_splice(draft, cursor - 1, 1, (PureSpan){0}) == PURE_OK);
+ apply(&o, --cursor, 1, NULL, 0);
+ } else {
+ CHECK(pure_draft_splice(draft, cursor, 0, (PureSpan){&byte, 1}) == PURE_OK);
+ apply(&o, cursor++, 0, &byte, 1);
+ }
+ pure_draft_test_arena(draft, &nodes, &bytes);
+ if (bytes > peak) peak = bytes;
+ }
+ expect_draft(draft, &o, scratch);
+ /* Reachable: 10,000 net typed bytes, the leaves they landed in and one
+ * path. Unreachable copies are reclaimed past max(reachable, 32 KiB). */
+ CHECK(peak < 2 * (10000 + 4 * 4096 + 2048) + 32768 + 8192);
+ uint64_t before = file_size(path);
+ int64_t revision;
+ CHECK(pure_draft_commit(draft, &revision) == PURE_OK);
+ o.revision = revision;
+ CHECK(file_size(path) - before < 10000 + 16384);
+ expect(path, "typed", &o);
+ pure_draft_close(draft);
+ printf("{\"typing_keystrokes\":30000,\"peak_arena_bytes\":%zu,\"commit_appended_bytes\":%llu}\n",
+ peak, (unsigned long long)(file_size(path) - before));
+ free(o.bytes);
+ free(scratch);
+}
+
+/* Two drafts of one key: the second commit conflicts and keeps its edits. */
+static void draft_conflict(const char *path) {
+ unsigned char scratch[64];
+ int64_t revision;
+ CHECK(pure_store_put(path, "shared", 0, (PureSpan){(const unsigned char *)"base", 4}, &revision) == PURE_OK);
+ PureDraft *first, *second;
+ CHECK(pure_draft_open(path, "shared", &first) == PURE_OK && pure_draft_open(path, "shared", &second) == PURE_OK);
+ CHECK(pure_draft_splice(first, 4, 0, (PureSpan){(const unsigned char *)"-one", 4}) == PURE_OK);
+ CHECK(pure_draft_splice(second, 0, 0, (PureSpan){(const unsigned char *)"two-", 4}) == PURE_OK);
+ CHECK(pure_draft_commit(first, &revision) == PURE_OK && revision == 2);
+ CHECK(pure_draft_commit(second, &revision) == PURE_CONFLICT && revision == 2);
+ Oracle kept = {(unsigned char *)"two-base", 8, 1};
+ expect_draft(second, &kept, scratch);
+ Oracle stored = {(unsigned char *)"base-one", 8, 2};
+ expect(path, "shared", &stored);
+ pure_draft_close(second);
+ CHECK(pure_draft_open(path, "shared", &second) == PURE_OK);
+ expect_draft(second, &stored, scratch);
+ CHECK(pure_store_put(path, "shared", 2, (PureSpan){(const unsigned char *)"x", 1}, &revision) == PURE_OK);
+ CHECK(pure_draft_commit(first, &revision) == PURE_CONFLICT && revision == 3);
+ pure_draft_close(first);
+ pure_draft_close(second);
+}
+
+/* Another key's writes compact the file under an open draft; the draft's
+ * stored extents are copied from its old mapping on commit. */
+static void draft_relocation(const char *path) {
+ unsigned char *big = malloc(300000), scratch[20000];
+ uint32_t state = 11;
+ CHECK(big);
+ fill(big, 300000, &state);
+ Oracle o = {malloc(20000), 12000, 0};
+ CHECK(o.bytes);
+ fill(o.bytes, o.size, &state);
+ CHECK(pure_store_put(path, "kept", 0, (PureSpan){o.bytes, o.size}, &o.revision) == PURE_OK);
+ PureDraft *draft;
+ CHECK(pure_draft_open(path, "kept", &draft) == PURE_OK);
+ CHECK(pure_draft_splice(draft, 6000, 10, (PureSpan){(const unsigned char *)"relocated", 9}) == PURE_OK);
+ apply(&o, 6000, 10, (const unsigned char *)"relocated", 9);
+ char name[512];
+ snprintf(name, sizeof(name), "%s/pure.tree", path);
+ struct stat before, after;
+ CHECK(stat(name, &before) == 0);
+ int64_t revision = 0;
+ for (unsigned i = 0; i < 8; ++i)
+ CHECK(pure_store_put(path, "other", revision, (PureSpan){big, 300000 - i}, &revision) == PURE_OK);
+ CHECK(stat(name, &after) == 0 && after.st_ino != before.st_ino);
+ CHECK(pure_draft_commit(draft, &revision) == PURE_OK && revision == o.revision + 1);
+ o.revision = revision;
+ expect(path, "kept", &o);
+ expect_draft(draft, &o, scratch);
+ Oracle other = {big, 300000 - 7, 8};
+ expect(path, "other", &other);
+ pure_draft_close(draft);
+ free(o.bytes);
+ free(big);
+}
+
+/* A failed commit changes neither the store nor the draft. */
+static void draft_faults(const char *path) {
+ unsigned char scratch[64];
+ int64_t revision;
+ CHECK(pure_store_put(path, "faulty", 0, (PureSpan){(const unsigned char *)"stable", 6}, &revision) == PURE_OK);
+ PureDraft *draft;
+ CHECK(pure_draft_open(path, "faulty", &draft) == PURE_OK);
+ CHECK(pure_draft_splice(draft, 6, 0, (PureSpan){(const unsigned char *)"+edit", 5}) == PURE_OK);
+ Oracle stored = {(unsigned char *)"stable", 6, 1}, edited = {(unsigned char *)"stable+edit", 11, 1};
+ uint64_t size = file_size(path);
+ for (fail_stage = PURE_AFTER_HEADER; fail_stage <= PURE_BEFORE_RENAME; ++fail_stage) {
+ CHECK(pure_draft_commit(draft, &revision) == PURE_STORE_ERROR);
+ expect(path, "faulty", &stored);
+ expect_draft(draft, &edited, scratch);
+ CHECK(file_size(path) == size);
+ }
+ fail_stage = PURE_BEFORE_DIR_SYNC;
+ CHECK(pure_draft_commit(draft, &revision) == PURE_UNCERTAIN && revision == 2);
+ fail_stage = -1;
+ expect_draft(draft, &edited, scratch);
+ CHECK(pure_draft_commit(draft, &revision) == PURE_CONFLICT && revision == 2);
+ pure_draft_close(draft);
+ CHECK(pure_draft_open(path, "faulty", &draft) == PURE_OK);
+ edited.revision = 2;
+ expect_draft(draft, &edited, scratch);
+ CHECK(pure_draft_splice(draft, 0, 6, (PureSpan){0}) == PURE_OK);
+ CHECK(pure_draft_commit(draft, &revision) == PURE_OK && revision == 3);
+ Oracle final = {(unsigned char *)"+edit", 5, 3};
+ expect(path, "faulty", &final);
+ pure_draft_close(draft);
+ CHECK(pure_draft_open(path, "bad/key", &draft) == PURE_INVALID && !draft);
+}
+
static void clean(const char *path) {
DIR *entries = opendir(path);
CHECK(entries);
@@ -380,7 +592,8 @@ static void clean(const char *path) {
int main(void) {
void (*checks[])(const char *) = {oracle, amplification, faults, crashes, torn_head,
- isolation, corruption, many_keys, revision_limit, keys};
+ isolation, corruption, many_keys, revision_limit, keys,
+ drafts, typing, draft_conflict, draft_relocation, draft_faults};
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);
@@ -389,6 +602,7 @@ int main(void) {
clean(path);
}
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\"}");
+ "process-crash-torn-head-lockless-isolation-compaction-corruption-1024-keys-revision-limit-key-listing-"
+ "draft-oracle-typing-arena-conflict-relocation-commit-faults\"}");
return 0;
}