aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 09:34:34 +0100
committereunhhu <[email protected]>2026-09-25 09:34:34 +0100
commit65386f68fb53e5a9a20a284b06afd872172c30eb (patch)
treeb04133aaa602fbf6a864df1d368ae091ab09b600 /prototypes
parent6a28879bd56354483fe940c0c80d511eccb89983 (diff)
downloadpure-65386f68fb53e5a9a20a284b06afd872172c30eb.tar.gz
pure-65386f68fb53e5a9a20a284b06afd872172c30eb.zip
Let the runtime own working versions as drafts
state.h gives any app a runtime-owned working version of a stored value on the tree store. A draft shares unchanged extents with the stored version, edits in memory by path copying, lends ordered extents without copying, and commits only its new nodes when the key is still at its base revision. A failed commit changes neither the draft nor the store; a draft whose file was replaced by compaction copies its stored extents on commit. Unreachable arena nodes are reclaimed once they outweigh max(reachable, 32 KiB). Typing into the middle of a 1 MiB document and saving each character peaked at about 1,660 KiB RSS with a draft against about 4,480 KiB for an app copy compared with storage on save; saves were only 1-2 ms faster. This corrects 073, which had attributed the TUI's remaining 8 ms to that comparison. Drafts are not yet charged to the runtime budget or visible to .pure definitions.
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;
}