From 1fd1d35855de6e7e37c94729305420a2655a7356 Mon Sep 17 00:00:00 2001 From: Rob Johnson Date: Mon, 26 Oct 2026 07:59:37 -0700 Subject: [PATCH 1/2] Add reusable public value blob handles --- Makefile | 6 + docs/usage.md | 50 +++ include/splinterdb/splinterdb.h | 39 +++ src/splinterdb.c | 152 +++++++- tests/unit/splinterdb_blob_test.c | 565 ++++++++++++++++++++++++++++++ 5 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 tests/unit/splinterdb_blob_test.c diff --git a/Makefile b/Makefile index 6c537349b..26164d7d6 100644 --- a/Makefile +++ b/Makefile @@ -530,6 +530,11 @@ $(BINDIR)/$(UNITDIR)/splinterdb_quick_test: $(COMMON_TESTOBJ) $(OBJDIR)/$(FUNCTIONAL_TESTSDIR)/test_async.o \ $(LIBDIR)/libsplinterdb.so +$(BINDIR)/$(UNITDIR)/splinterdb_blob_test: $(COMMON_TESTOBJ) \ + $(COMMON_UNIT_TESTOBJ) \ + $(OBJDIR)/$(FUNCTIONAL_TESTSDIR)/test_async.o \ + $(LIBDIR)/libsplinterdb.so + $(BINDIR)/$(UNITDIR)/splinterdb_stress_test: $(COMMON_TESTOBJ) \ $(COMMON_UNIT_TESTOBJ) \ $(OBJDIR)/$(FUNCTIONAL_TESTSDIR)/test_async.o \ @@ -616,6 +621,7 @@ unit/merge_test: $(BINDIR)/$(UNITDIR)/merge_test unit/trunk_split_test: $(BINDIR)/$(UNITDIR)/trunk_split_test unit/splinter_test: $(BINDIR)/$(UNITDIR)/splinter_test unit/splinterdb_quick_test: $(BINDIR)/$(UNITDIR)/splinterdb_quick_test +unit/splinterdb_blob_test: $(BINDIR)/$(UNITDIR)/splinterdb_blob_test unit/splinterdb_stress_test: $(BINDIR)/$(UNITDIR)/splinterdb_stress_test unit/notification_test: $(BINDIR)/$(UNITDIR)/notification_test unit/splinterdb_optimize_test: $(BINDIR)/$(UNITDIR)/splinterdb_optimize_test diff --git a/docs/usage.md b/docs/usage.md index ec4238573..66c580de0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -69,6 +69,56 @@ looks out-of-date, please open an issue or pull request. functional tests. +## Reusing a value blob + +Use `splinterdb_blob_create()` to copy a value into blob storage once, then +`splinterdb_insert_blob()` to insert it under multiple keys. Full extents are +shared; tails may be copied on insertion or compaction. Small values may be +copied entirely, and each handle has allocation overhead, so this API is most +useful for large repeated values. + +```c +int +insert_shared_values(splinterdb *db, slice value, slice first_key, slice second_key) +{ + splinterdb_blob *blob = NULL; + int rc = splinterdb_blob_create(db, value, &blob); + if (rc != 0) { + return rc; + } + + rc = splinterdb_insert_blob(db, first_key, blob, NULL); + if (rc == 0) { + rc = splinterdb_insert_blob(db, second_key, blob, NULL); + } + splinterdb_blob_destroy(&blob); + if (rc == 0) { + rc = splinterdb_durable_barrier(db, NULL); + } + return rc; +} +``` + +The input value may be released after creation succeeds. Destroying a handle +releases the caller's ownership and clears its pointer; successfully inserted +values retain their own ownership. The two inserts above are independent +operations: if the second fails, the first is still applied. + +A handle is immutable and belongs to the same open database instance that +created it. Threads may insert it concurrently, but destruction must not race +any use. Handles cannot be serialized or reused after close/reopen. Destroy +all handles before `splinterdb_close(&db, FALSE)`, which returns `EBUSY` while +handles remain live. Forced close releases the handles and invalidates every +outstanding pointer; do not use or destroy those pointers afterward. + +Creating a standalone blob does not establish crash durability. Inserted +references follow ordinary insert and durability-barrier semantics; a +successful blocking barrier covers the preceding successful inserts and their +blob data. Recovery reclaims storage that no recovered data references. +Keys still use the existing `slice` API and key-size limit; large-key support +is separate from this value-blob API. + + ## Example programs - [`tests/unit/splinterdb_quick_test.c`](../tests/unit/splinterdb_quick_test.c) covers various basic operations on a single thread. The diff --git a/include/splinterdb/splinterdb.h b/include/splinterdb/splinterdb.h index e058cdec7..8d2af8bca 100644 --- a/include/splinterdb/splinterdb.h +++ b/include/splinterdb/splinterdb.h @@ -255,6 +255,9 @@ splinterdb_open(const splinterdb_config *cfg, splinterdb **kvs); // teardown. The database remains open and *kvs is unchanged, so the caller can // retry or investigate. // +// Live splinterdb_blob handles cause a non-forced close to return EBUSY. +// Destroy them and retry, or force close to release and invalidate them. +// // With force, teardown always completes. An error means data preservation // could not be guaranteed; it does not prove that data was actually lost. // @@ -460,6 +463,42 @@ splinterdb_insert(splinterdb *kvsb, slice value, splinterdb_lookup_result *old_result); +// An immutable, reusable value owned by one open database instance. +// Create copies value into blob storage without inserting a key. The input +// slice may be released after create returns. On failure, *blob is NULL. +// Creation alone does not establish crash durability; inserted references use +// the ordinary update/durable_barrier contract. Unreferenced storage is +// reclaimed by allocation reconstruction after a crash. +// +// Handles may be reused concurrently by registered/automatically registered +// threads. Destroy must not race any use of that handle. Handles cannot be +// serialized or used with a different database or after close/reopen. +// +// Existing blob cloning rules apply: full extents are shared, tails may be +// copied. Small values may be copied entirely. Each handle owns a separate +// mini allocator, so creating blobs for tiny values has allocation overhead. +typedef struct splinterdb_blob splinterdb_blob; + +int +splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **blob); + +// Release the caller's ownership and set *blob to NULL. Inserted values retain +// their own ownership. Passing NULL or a pointer to NULL is harmless. +// Destroy all handles before ordinary close. Forced close releases all handles +// and invalidates every outstanding handle pointer. +void +splinterdb_blob_destroy(splinterdb_blob **blob); + +// Insert a previously created blob as a value. This has the same overwrite, +// old_result, and durability semantics as splinterdb_insert(). The handle +// remains reusable on success or failure. A handle from another open database +// is rejected with EINVAL. Keys use the existing slice API and key-size limit. +int +splinterdb_insert_blob(splinterdb *kvs, + slice key, + const splinterdb_blob *blob, + splinterdb_lookup_result *old_result); + // Delete a given key and any associated value / messages int splinterdb_delete(splinterdb *kvsb, diff --git a/src/splinterdb.c b/src/splinterdb.c index bf5008e42..64888a48a 100644 --- a/src/splinterdb.c +++ b/src/splinterdb.c @@ -15,6 +15,7 @@ #include "splinterdb/splinterdb.h" #include "clockcache.h" +#include "blob_build.h" #include "data_internal.h" #include "rc_allocator.h" #include "superblock.h" @@ -66,9 +67,33 @@ typedef struct splinterdb { data_config *data_cfg; bool we_created_heap; batch_rwlock stats_lock; + splinterdb_blob *blobs; } splinterdb; -#define SPLINTERDB_STATS_LOCK_IDX 0 +struct splinterdb_blob { + splinterdb *owner; + uint64 meta_head; + writable_buffer descriptor; + splinterdb_blob *next; + splinterdb_blob *prev; +}; + +#define SPLINTERDB_STATS_LOCK_IDX 0 +#define SPLINTERDB_BLOB_LIST_LOCK_IDX 1 + +static void +splinterdb_blob_list_lock(splinterdb *kvs) +{ + batch_rwlock_get(&kvs->stats_lock, SPLINTERDB_BLOB_LIST_LOCK_IDX); + batch_rwlock_claim_loop(&kvs->stats_lock, SPLINTERDB_BLOB_LIST_LOCK_IDX); + batch_rwlock_lock(&kvs->stats_lock, SPLINTERDB_BLOB_LIST_LOCK_IDX); +} + +static void +splinterdb_blob_list_unlock(splinterdb *kvs) +{ + batch_rwlock_full_unlock(&kvs->stats_lock, SPLINTERDB_BLOB_LIST_LOCK_IDX); +} static inline void splinterdb_stats_read_lock(splinterdb *kvs) @@ -810,6 +835,16 @@ splinterdb_close(splinterdb **kvs_in, bool32 force) // IN platform_assert(kvs != NULL); splinterdb_assert_thread_registered(); + // Close already requires that all other API calls have stopped. Drop + // standalone references before core_unmount persists the allocation map. + if (kvs->blobs != NULL && !force) { + return EBUSY; + } + while (kvs->blobs != NULL) { + splinterdb_blob *blob = kvs->blobs; + splinterdb_blob_destroy(&blob); + } + // Print stats if shared memory is enabled. if (kvs->heap_id) { splinterdb_close_print_stats(kvs); @@ -1031,6 +1066,121 @@ splinterdb_insert(splinterdb *kvsb, return splinterdb_insert_message(kvsb, user_key, msg, _old_result); } +int +splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **out) +{ + if (out == NULL) { + return EINVAL; + } + *out = NULL; + if (kvs == NULL || (slice_length(value) != 0 && slice_data(value) == NULL)) { + return EINVAL; + } + int rc = splinterdb_ensure_thread_registered(); + if (rc != 0) { + return rc; + } + + splinterdb_blob *handle = TYPED_ZALLOC(kvs->heap_id, handle); + if (handle == NULL) { + return ENOMEM; + } + handle->owner = kvs; + writable_buffer_init(&handle->descriptor, kvs->heap_id); + + splinterdb_stats_read_lock(kvs); + cache *cc = kvs->spl.cc; + allocator *al = cache_get_allocator(cc); + platform_status status = + allocator_alloc(al, &handle->meta_head, PAGE_TYPE_BLOB); + if (!SUCCESS(status)) { + goto fail; + } + + mini_allocator mini; + status = mini_init(&mini, cc, handle->meta_head, 0, 1, PAGE_TYPE_BLOB, NULL); + if (!SUCCESS(status)) { + refcount ref = allocator_dec_ref(al, handle->meta_head, PAGE_TYPE_BLOB); + platform_assert(ref == AL_NO_REFS); + cache_extent_discard(cc, handle->meta_head, PAGE_TYPE_BLOB); + ref = allocator_dec_ref(al, handle->meta_head, PAGE_TYPE_BLOB); + platform_assert(ref == AL_FREE); + goto fail; + } + + // A private mini allocator isolates the caller's lifetime from memtable + // rotations and log reclamation. One batch suffices for this one blob. + blob_build_config cfg = kvs->btree_cfg.blob_cfg; + cfg.extent_batch = cfg.page_batch = cfg.subpage_batch = 0; + status = blob_build(&cfg, cc, &mini, value, &handle->descriptor); + mini_release(&mini); + if (!SUCCESS(status)) { + mini_dec_ref(cc, handle->meta_head, PAGE_TYPE_BLOB); + goto fail; + } + + splinterdb_blob_list_lock(kvs); + handle->next = kvs->blobs; + if (handle->next != NULL) { + handle->next->prev = handle; + } + kvs->blobs = handle; + splinterdb_blob_list_unlock(kvs); + splinterdb_stats_read_unlock(kvs); + *out = handle; + return 0; + +fail: + splinterdb_stats_read_unlock(kvs); + writable_buffer_deinit(&handle->descriptor); + platform_free(kvs->heap_id, handle); + return platform_status_to_int(status); +} + +void +splinterdb_blob_destroy(splinterdb_blob **blob) +{ + if (blob == NULL || *blob == NULL) { + return; + } + splinterdb_assert_thread_registered(); + splinterdb_blob *handle = *blob; + splinterdb *kvs = handle->owner; + splinterdb_stats_read_lock(kvs); + splinterdb_blob_list_lock(kvs); + if (handle->prev != NULL) { + handle->prev->next = handle->next; + } else { + kvs->blobs = handle->next; + } + if (handle->next != NULL) { + handle->next->prev = handle->prev; + } + splinterdb_blob_list_unlock(kvs); + mini_dec_ref(kvs->spl.cc, handle->meta_head, PAGE_TYPE_BLOB); + writable_buffer_deinit(&handle->descriptor); + platform_free(kvs->heap_id, handle); + *blob = NULL; + splinterdb_stats_read_unlock(kvs); +} + +int +splinterdb_insert_blob(splinterdb *kvs, + slice user_key, + const splinterdb_blob *blob, + splinterdb_lookup_result *old_result) +{ + if (kvs == NULL || blob == NULL || blob->owner != kvs) { + return EINVAL; + } + message msg = message_create(MESSAGE_TYPE_INSERT, + kvs->spl.cc, + writable_buffer_to_slice(&blob->descriptor)); + lookup_result *result = + old_result == NULL ? NULL : lookup_result_from_splinterdb(old_result); + return splinterdb_insert_message(kvs, user_key, msg, result); +} + int splinterdb_delete(splinterdb *kvsb, slice user_key, diff --git a/tests/unit/splinterdb_blob_test.c b/tests/unit/splinterdb_blob_test.c new file mode 100644 index 000000000..83602e70f --- /dev/null +++ b/tests/unit/splinterdb_blob_test.c @@ -0,0 +1,565 @@ +// Copyright 2026 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/* Public reusable-blob ownership, sharing, and durability regressions. */ +#include +#include +#include +#include +#include +#include +#include + +#include "splinterdb/default_data_config.h" +#include "splinterdb/splinterdb.h" +#include "blob.h" +#include "config.h" +#include "lookup_result.h" +#include "platform_threads.h" +#include "splinterdb_tests_private.h" +#include "unit_tests.h" +#include "util.h" +#include "ctest.h" + +static uint64 +blob_test_in_use_bytes(allocator *al) +{ + return allocator_in_use(al) * allocator_get_config(al)->io_cfg->extent_size; +} + +CTEST_DATA(splinterdb_blob) +{ + splinterdb *db; + splinterdb_config cfg; + data_config data_cfg; +}; + +CTEST_SETUP(splinterdb_blob) +{ + default_data_config_init(&data->data_cfg); + data->db = NULL; + data->cfg = (splinterdb_config){ + .filename = TEST_CONFIG_DEFAULT_IO_FILENAME, + .cache_size = 64 * Mega, + .disk_size = 256 * Mega, + .data_cfg = &data->data_cfg, + .use_shmem = config_parse_use_shmem(Ctest_argc, (char **)Ctest_argv), + .blob_max_descriptor_size = 128, + .log_group_size_pages = UINT64_MAX, + .checkpoint_log_size_bytes = UINT64_MAX, + .checkpoint_log_grace_bytes = UINT64_MAX, + }; +} + +CTEST_TEARDOWN(splinterdb_blob) +{ + if (data->db != NULL) { + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + } + platform_deregister_thread(); +} + +static void +blob_test_check_result(splinterdb_lookup_result *result, slice expected) +{ + ASSERT_TRUE(splinterdb_lookup_found(result)); + slice actual; + ASSERT_EQUAL(0, splinterdb_lookup_result_value(result, &actual)); + ASSERT_EQUAL(slice_length(expected), slice_length(actual)); + if (slice_length(expected) != 0) { + ASSERT_EQUAL(0, + memcmp(slice_data(expected), + slice_data(actual), + slice_length(expected))); + } +} + +/* Inspect descriptors before the public value accessor materializes them. + * Whole extents (including pointer trees) must be physically shared. Packed + * tails are intentionally allowed to move when each owner clones its value. */ +static void +blob_test_check_shared_values(splinterdb *db, uint64 count, slice expected) +{ + uint64 root_addrs[64] = {0}; + uint64 root_count = 0; + uint64 height = 0; + splinterdb_lookup_result result; + splinterdb_lookup_result_init(db, &result); + for (uint64 i = 0; i < count; i++) { + ASSERT_EQUAL(0, + splinterdb_lookup(db, slice_create(sizeof(i), &i), &result)); + ASSERT_TRUE(splinterdb_lookup_found(&result)); + lookup_result *internal = lookup_result_from_splinterdb(&result); + ASSERT_TRUE(merge_accumulator_is_blob(&internal->value)); + message msg = merge_accumulator_to_message(&internal->value); + parsed_blob parsed; + ASSERT_TRUE( + SUCCESS(blob_parse_descriptor((cache *)splinterdb_get_cache_handle(db), + message_slice(msg), + &parsed))); + ASSERT_TRUE(parsed.num_extents <= ARRAY_SIZE(root_addrs)); + if (i == 0) { + root_count = parsed.num_extents; + height = parsed.height; + memcpy(root_addrs, parsed.base->addrs, root_count * sizeof(uint64)); + } else { + ASSERT_EQUAL(root_count, parsed.num_extents); + ASSERT_EQUAL(height, parsed.height); + ASSERT_EQUAL(0, + memcmp(root_addrs, + parsed.base->addrs, + root_count * sizeof(uint64))); + } + blob_test_check_result(&result, expected); + } + splinterdb_lookup_result_deinit(&result); +} + +CTEST2(splinterdb_blob, test_reuse_release_compact_and_reopen) +{ + const uint64 sizes[] = {0, 17, 4097, 128 * KiB + 17, 2 * Mega + 17}; + const uint64 count = 12; + for (unsigned use_log = 0; use_log < 2; use_log++) { + data->cfg.use_log = use_log; + for (unsigned variant = 0; variant < ARRAY_SIZE(sizes); variant++) { + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + const uint64 size = sizes[variant]; + char *value = malloc(size ? size : 1); + ASSERT_TRUE(value != NULL); + memset(value, 0x39, size); + splinterdb_blob *handle = NULL; + ASSERT_EQUAL(0, + splinterdb_blob_create( + data->db, slice_create(size, value), &handle)); + ASSERT_TRUE(handle != NULL); + + /* Creation owns the bytes before the user's input changes. */ + memset(value, 0x7c, size); + for (uint64 i = 0; i < count; i++) { + ASSERT_EQUAL( + 0, + splinterdb_insert_blob( + data->db, slice_create(sizeof(i), &i), handle, NULL)); + } + memset(value, 0x39, size); + slice expected = slice_create(size, value); + blob_test_check_shared_values(data->db, count, expected); + + splinterdb_blob_destroy(&handle); + ASSERT_NULL(handle); + ASSERT_EQUAL( + 0, splinterdb_optimize(data->db, .full_leaf_compactions = TRUE)); + blob_test_check_shared_values(data->db, count, expected); + ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); + blob_test_check_shared_values(data->db, count, expected); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + free(value); + } + } +} + +CTEST2(splinterdb_blob, test_overwrite_delete_and_old_result) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + const uint64 size = 2 * 128 * KiB + 19; + char *value = malloc(size); + ASSERT_TRUE(value != NULL); + memset(value, 0x65, size); + slice expected = slice_create(size, value); + slice key = slice_create(3, "key"); + slice ordinary = slice_create(8, "ordinary"); + splinterdb_blob *handle = NULL; + ASSERT_EQUAL(0, splinterdb_blob_create(data->db, expected, &handle)); + ASSERT_EQUAL(0, splinterdb_insert(data->db, key, ordinary, NULL)); + + splinterdb_lookup_result old; + splinterdb_lookup_result_init(data->db, &old); + ASSERT_EQUAL(0, splinterdb_insert_blob(data->db, key, handle, &old)); + blob_test_check_result(&old, ordinary); + + ASSERT_EQUAL(0, splinterdb_insert_blob(data->db, key, handle, &old)); + blob_test_check_result(&old, expected); + ASSERT_EQUAL(0, splinterdb_delete(data->db, key, &old)); + splinterdb_blob_destroy(&handle); + blob_test_check_result(&old, expected); + + splinterdb_lookup_result absent; + splinterdb_lookup_result_init(data->db, &absent); + ASSERT_EQUAL(0, splinterdb_lookup(data->db, key, &absent)); + ASSERT_FALSE(splinterdb_lookup_found(&absent)); + splinterdb_lookup_result_deinit(&absent); + splinterdb_lookup_result_deinit(&old); + free(value); +} + +CTEST2(splinterdb_blob, test_wrong_database_and_unused_release) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + const uint64 before = blob_test_in_use_bytes(al); + char value[8193]; + memset(value, 0x29, sizeof(value)); + for (unsigned i = 0; i < 32; i++) { + splinterdb_blob *handle = NULL; + ASSERT_EQUAL(0, + splinterdb_blob_create( + data->db, slice_create(sizeof(value), value), &handle)); + splinterdb_blob_destroy(&handle); + ASSERT_NULL(handle); + splinterdb_blob_destroy(&handle); + ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); + } + splinterdb_blob_destroy(NULL); + + char other_filename[1024]; + int length = snprintf(other_filename, + sizeof(other_filename), + "%s.blob-other", + data->cfg.filename); + ASSERT_TRUE(length > 0 && length < sizeof(other_filename)); + splinterdb_config other_cfg = data->cfg; + other_cfg.filename = other_filename; + splinterdb *other = NULL; + ASSERT_EQUAL(0, splinterdb_create(&other_cfg, &other)); + splinterdb_blob *handle = NULL; + ASSERT_EQUAL(0, + splinterdb_blob_create( + data->db, slice_create(sizeof(value), value), &handle)); + ASSERT_EQUAL( + EINVAL, + splinterdb_insert_blob(other, slice_create(3, "key"), handle, NULL)); + splinterdb_blob_destroy(&handle); + ASSERT_EQUAL(0, splinterdb_close(&other, FALSE)); + ASSERT_EQUAL(0, unlink(other_filename)); +} + +typedef struct blob_test_worker { + splinterdb *db; + const splinterdb_blob *handle; + pthread_barrier_t *barrier; + uint64 begin; + uint64 count; + int rc; +} blob_test_worker; + +static void * +blob_test_insert_worker(void *arg) +{ + blob_test_worker *worker = arg; + pthread_barrier_wait(worker->barrier); + for (uint64 i = worker->begin; i < worker->begin + worker->count; i++) { + worker->rc = splinterdb_insert_blob( + worker->db, slice_create(sizeof(i), &i), worker->handle, NULL); + if (worker->rc != 0) { + break; + } + } + platform_deregister_thread(); + return NULL; +} + +CTEST2(splinterdb_blob, test_concurrent_inserts_share_one_handle) +{ + data->cfg.use_log = TRUE; + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + const uint64 size = 2 * 128 * KiB + 17; + char *value = malloc(size); + ASSERT_TRUE(value != NULL); + memset(value, 0x52, size); + splinterdb_blob *handle = NULL; + ASSERT_EQUAL( + 0, splinterdb_blob_create(data->db, slice_create(size, value), &handle)); + pthread_t threads[4]; + blob_test_worker workers[4]; + pthread_barrier_t barrier; + ASSERT_EQUAL(0, pthread_barrier_init(&barrier, NULL, ARRAY_SIZE(threads))); + for (unsigned i = 0; i < ARRAY_SIZE(threads); i++) { + workers[i] = (blob_test_worker){ + .db = data->db, + .handle = handle, + .barrier = &barrier, + .begin = i * 16, + .count = 16, + }; + ASSERT_EQUAL(0, + pthread_create( + &threads[i], NULL, blob_test_insert_worker, &workers[i])); + } + for (unsigned i = 0; i < ARRAY_SIZE(threads); i++) { + ASSERT_EQUAL(0, pthread_join(threads[i], NULL)); + ASSERT_EQUAL(0, workers[i].rc); + } + ASSERT_EQUAL(0, pthread_barrier_destroy(&barrier)); + splinterdb_blob_destroy(&handle); + ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); + blob_test_check_shared_values(data->db, 64, slice_create(size, value)); + free(value); +} + + +CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) +{ + const uint64 size = 2 * Mega + 17; + const uint64 orphan_size = 32 * Mega; + const uint64 count = 12; + char *value = malloc(size); + ASSERT_TRUE(value != NULL); + memset(value, 0x26, size); + + const char *child_mode = getenv("SPLINTER_TEST_PUBLIC_BLOB_CRASH_CHILD"); + if (child_mode != NULL) { + data->cfg.use_log = child_mode[0] == '1'; + const char *report_fd = getenv("SPLINTER_TEST_PUBLIC_BLOB_CRASH_FD"); + if (report_fd == NULL) { + _exit(9); + } + splinterdb *db = NULL; + if (splinterdb_create(&data->cfg, &db) != 0) { + _exit(1); + } + splinterdb_blob *shared = NULL; + if (splinterdb_blob_create(db, slice_create(size, value), &shared) != 0) { + _exit(2); + } + for (uint64 i = 0; i < count; i++) { + if (splinterdb_insert_blob( + db, slice_create(sizeof(i), &i), shared, NULL) + != 0) + { + _exit(3); + } + } + if (splinterdb_durable_barrier(db, NULL) != 0) { + _exit(4); + } + allocator *al = (allocator *)splinterdb_get_allocator_handle(db); + uint64 before_orphan = blob_test_in_use_bytes(al); + char *orphan_value = malloc(orphan_size); + if (orphan_value == NULL) { + _exit(5); + } + memset(orphan_value, 0x79, orphan_size); + splinterdb_blob *orphan = NULL; + if (splinterdb_blob_create( + db, slice_create(orphan_size, orphan_value), &orphan) + != 0) + { + _exit(6); + } + if (blob_test_in_use_bytes(al) < before_orphan + orphan_size) { + _exit(7); + } + if (write(atoi(report_fd), &before_orphan, sizeof(before_orphan)) + != sizeof(before_orphan)) + { + _exit(8); + } + /* Deliberately retain both handles and skip clean close. The durable + * keys own shared's data; the unreferenced blob has no recovery root. */ + _exit(0); + } + + for (unsigned use_log = 0; use_log < 2; use_log++) { + data->cfg.use_log = use_log; + int report[2]; + ASSERT_EQUAL(0, pipe(report)); + pid_t child = fork(); + ASSERT_TRUE(child >= 0); + if (child == 0) { + close(report[0]); + char fd_string[32]; + snprintf(fd_string, sizeof(fd_string), "%d", report[1]); + setenv( + "SPLINTER_TEST_PUBLIC_BLOB_CRASH_CHILD", use_log ? "1" : "0", 1); + setenv("SPLINTER_TEST_PUBLIC_BLOB_CRASH_FD", fd_string, 1); + /* A fresh process must own thread-registration state; reusing the + * parent's initialized state across fork can affect later tests. */ + if (data->cfg.use_shmem) { + execl("/proc/self/exe", + "splinterdb_blob_test", + "--use-shmem", + "test_durable_reuse_and_orphans_after_process_exit", + (char *)NULL); + } else { + execl("/proc/self/exe", + "splinterdb_blob_test", + "test_durable_reuse_and_orphans_after_process_exit", + (char *)NULL); + } + _exit(10); + } + close(report[1]); + int status; + ASSERT_EQUAL(child, waitpid(child, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQUAL(0, WEXITSTATUS(status)); + uint64 before_orphan; + ASSERT_EQUAL(sizeof(before_orphan), + read(report[0], &before_orphan, sizeof(before_orphan))); + ASSERT_EQUAL(0, close(report[0])); + ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); + blob_test_check_shared_values(data->db, count, slice_create(size, value)); + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + /* Recovery creates fresh working trees/logs, so allow that overhead; + * retaining the 32 MiB unreachable object would exceed this bound. */ + ASSERT_TRUE(blob_test_in_use_bytes(al) < before_orphan + orphan_size / 2); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + } + free(value); +} + +CTEST2(splinterdb_blob, test_close_refuses_live_handles_and_can_retry) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + splinterdb_blob *handle = NULL; + slice value = slice_create(5, "value"); + ASSERT_EQUAL(0, splinterdb_blob_create(data->db, value, &handle)); + splinterdb *original = data->db; + ASSERT_EQUAL(EBUSY, splinterdb_close(&data->db, FALSE)); + ASSERT_TRUE(data->db == original); + ASSERT_EQUAL( + 0, + splinterdb_insert_blob(data->db, slice_create(3, "key"), handle, NULL)); + splinterdb_blob_destroy(&handle); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + ASSERT_NULL(data->db); + ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); + splinterdb_lookup_result result; + splinterdb_lookup_result_init(data->db, &result); + ASSERT_EQUAL(0, + splinterdb_lookup(data->db, slice_create(3, "key"), &result)); + blob_test_check_result(&result, value); + splinterdb_lookup_result_deinit(&result); +} + +CTEST2(splinterdb_blob, test_force_close_releases_live_handles) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + const uint64 size = 2 * 128 * KiB + 17; + const uint64 orphan_size = 32 * Mega; + char *value = malloc(orphan_size); + ASSERT_TRUE(value != NULL); + memset(value, 0x41, orphan_size); + splinterdb_blob *shared = NULL; + ASSERT_EQUAL( + 0, splinterdb_blob_create(data->db, slice_create(size, value), &shared)); + for (uint64 i = 0; i < 12; i++) { + ASSERT_EQUAL(0, + splinterdb_insert_blob( + data->db, slice_create(sizeof(i), &i), shared, NULL)); + } + ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + uint64 before_orphan = blob_test_in_use_bytes(al); + splinterdb_blob *orphan = NULL; + ASSERT_EQUAL(0, + splinterdb_blob_create( + data->db, slice_create(orphan_size, value), &orphan)); + ASSERT_TRUE(blob_test_in_use_bytes(al) >= before_orphan + orphan_size); + ASSERT_EQUAL(0, splinterdb_close(&data->db, TRUE)); + /* Forced close invalidated both caller pointers. */ + shared = NULL; + orphan = NULL; + ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); + blob_test_check_shared_values(data->db, 12, slice_create(size, value)); + al = (allocator *)splinterdb_get_allocator_handle(data->db); + ASSERT_TRUE(blob_test_in_use_bytes(al) < before_orphan + orphan_size / 2); + free(value); +} + +CTEST2(splinterdb_blob, test_reject_invalid_arguments) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + splinterdb_blob *handle = NULL; + ASSERT_EQUAL( + EINVAL, splinterdb_blob_create(data->db, slice_create(1, NULL), &handle)); + ASSERT_NULL(handle); + ASSERT_EQUAL(EINVAL, + splinterdb_blob_create(NULL, slice_create(0, NULL), &handle)); + ASSERT_NULL(handle); + ASSERT_EQUAL(EINVAL, + splinterdb_blob_create(data->db, slice_create(0, NULL), NULL)); + ASSERT_EQUAL( + EINVAL, + splinterdb_insert_blob(data->db, slice_create(3, "key"), NULL, NULL)); +} + +CTEST2(splinterdb_blob, test_destroy_middle_head_and_tail_handles) +{ + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + splinterdb_blob *handles[5] = {0}; + for (uint64 i = 0; i < ARRAY_SIZE(handles); i++) { + char value = 'a' + i; + ASSERT_EQUAL(0, + splinterdb_blob_create(data->db, + slice_create(sizeof(value), &value), + &handles[i])); + } + const unsigned order[] = {2, 4, 0, 1, 3}; + for (unsigned i = 0; i < ARRAY_SIZE(order); i++) { + uint64 key = order[i]; + ASSERT_EQUAL( + 0, + splinterdb_insert_blob( + data->db, slice_create(sizeof(key), &key), handles[key], NULL)); + splinterdb_blob_destroy(&handles[key]); + ASSERT_NULL(handles[key]); + } + splinterdb_lookup_result result; + splinterdb_lookup_result_init(data->db, &result); + for (uint64 i = 0; i < ARRAY_SIZE(handles); i++) { + char value = 'a' + i; + ASSERT_EQUAL( + 0, splinterdb_lookup(data->db, slice_create(sizeof(i), &i), &result)); + blob_test_check_result(&result, slice_create(sizeof(value), &value)); + } + splinterdb_lookup_result_deinit(&result); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); +} + +CTEST2(splinterdb_blob, test_failed_creation_releases_allocations) +{ + data->cfg.disk_size = 64 * Mega; + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + uint64 before = blob_test_in_use_bytes(al); + char byte = 'x'; + splinterdb_blob *failed = NULL; + /* The bound must be checked before reading this deliberately short input. */ + ASSERT_EQUAL( + ENOSPC, + splinterdb_blob_create( + data->db, slice_create(data->cfg.disk_size + 1, &byte), &failed)); + ASSERT_NULL(failed); + ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); + + const uint64 size = 4 * Mega; + char *value = malloc(size); + ASSERT_TRUE(value != NULL); + memset(value, 0x23, size); + splinterdb_blob *handles[32] = {0}; + unsigned count = 0; + int rc = 0; + for (; count < ARRAY_SIZE(handles); count++) { + rc = splinterdb_blob_create( + data->db, slice_create(size, value), &handles[count]); + if (rc != 0) { + ASSERT_NULL(handles[count]); + break; + } + } + ASSERT_EQUAL(ENOSPC, rc); + ASSERT_TRUE(count > 0 && count < ARRAY_SIZE(handles)); + for (unsigned i = 0; i < count; i++) { + splinterdb_blob_destroy(&handles[i]); + } + ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); + /* The failed creation did not poison subsequent allocator or list use. */ + ASSERT_EQUAL( + 0, splinterdb_blob_create(data->db, slice_create(size, value), &failed)); + splinterdb_blob_destroy(&failed); + ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); + free(value); +} From 61fafeb02197a67a00608fade4f68b8219687335 Mon Sep 17 00:00:00 2001 From: Rob Johnson Date: Mon, 26 Oct 2026 10:59:45 -0700 Subject: [PATCH 2/2] Use public messages for insert and update payloads --- .../allocation_table/kvbench_controlled.c | 12 +- .../allocation_table/prepare_controlled.py | 9 +- benchmarks/allocation_table/prepare_final.py | 10 +- benchmarks/barrier_bench.c | 2 +- benchmarks/btree_prefix_bench.c | 6 +- benchmarks/build_barrier_bench.py | 2 + benchmarks/build_btree_prefix_bench.py | 2 + benchmarks/build_prefix_bench.py | 2 + benchmarks/comparison/build.sh | 20 +- benchmarks/hash_range_benchmark.c | 2 +- benchmarks/insert_benchmark.c | 2 +- benchmarks/kvbench.c | 12 +- benchmarks/optimize_notification_bench.c | 2 +- docs/usage.md | 80 +++-- ...interdb_custom_ipv4_addr_sortcmp_example.c | 3 +- examples/splinterdb_intro_example.c | 15 +- examples/splinterdb_iterators_example.c | 3 +- examples/splinterdb_wide_values_example.c | 2 +- include/splinterdb/data.h | 27 +- include/splinterdb/splinterdb.h | 76 +++-- src/btree.c | 2 +- src/data_internal.h | 13 +- src/splinterdb.c | 124 +++++--- tests/functional/btree_test.c | 2 +- tests/functional/log_test.c | 26 +- tests/functional/scan_benchmark.c | 2 +- tests/functional/ycsb_test.c | 8 +- tests/unit/btree_stress_test.c | 45 +-- tests/unit/btree_test.c | 42 +-- tests/unit/large_inserts_stress_test.c | 5 +- tests/unit/limitations_test.c | 8 +- tests/unit/merge_test.c | 2 +- tests/unit/ondisk_ref_test.c | 14 +- tests/unit/splinter_test.c | 28 +- tests/unit/splinterdb_blob_test.c | 296 +++++++++++++----- tests/unit/splinterdb_forked_child_test.c | 10 +- tests/unit/splinterdb_optimize_test.c | 11 +- tests/unit/splinterdb_quick_test.c | 245 +++++++++------ tests/unit/splinterdb_stress_test.c | 10 +- tests/unit/trunk_split_test.c | 7 +- 40 files changed, 751 insertions(+), 438 deletions(-) diff --git a/benchmarks/allocation_table/kvbench_controlled.c b/benchmarks/allocation_table/kvbench_controlled.c index 0d6a5d13f..fcc66bc9f 100644 --- a/benchmarks/allocation_table/kvbench_controlled.c +++ b/benchmarks/allocation_table/kvbench_controlled.c @@ -850,8 +850,10 @@ load_worker(void *arg) make_key(2 * id, key); make_value(id, 0, val); uint64_t t0 = now_ns(); - int rc = splinterdb_insert( - kvs, slice_create(KEY_SIZE, key), slice_create(VALUE_SIZE, val), NULL); + int rc = splinterdb_insert(kvs, + slice_create(KEY_SIZE, key), + message_create(VALUE_SIZE, val), + NULL); uint64_t t1 = now_ns(); if (rc) { w->errors++; @@ -1133,8 +1135,10 @@ ow_worker(void *arg) make_key(2 * id, key); make_value(id, generation, val); uint64_t t0 = now_ns(); - int rc = splinterdb_insert( - kvs, slice_create(KEY_SIZE, key), slice_create(VALUE_SIZE, val), NULL); + int rc = splinterdb_insert(kvs, + slice_create(KEY_SIZE, key), + message_create(VALUE_SIZE, val), + NULL); uint64_t t1 = now_ns(); n++; atomic_store_explicit(&w->progress, n, memory_order_relaxed); diff --git a/benchmarks/allocation_table/prepare_controlled.py b/benchmarks/allocation_table/prepare_controlled.py index e7c249aae..c0ec2f640 100644 --- a/benchmarks/allocation_table/prepare_controlled.py +++ b/benchmarks/allocation_table/prepare_controlled.py @@ -44,9 +44,12 @@ def main(): close_flags = (["-DKVBENCH_CLOSE_HAS_FORCE"] if "splinterdb_close(splinterdb **kvs, bool32 force)" in (source / "include/splinterdb/splinterdb.h").read_text() else []) - command = flags + close_flags + ["-I" + str(source / "include"), - str(harness), str(library), "-laio", "-lxxhash", "-lm", - "-o", str(binary)] + value_flags = ([] if "message_create(" in + (source / "include/splinterdb/data.h").read_text() + else ["-Dmessage_create=slice_create"]) + command = flags + close_flags + value_flags + [ + "-I" + str(source / "include"), str(harness), str(library), + "-laio", "-lxxhash", "-lm", "-o", str(binary)] subprocess.run(command, check=True) manifest["variants"][name] = dict(item, binary=str(binary), sha256=sha(binary), link_command=command) diff --git a/benchmarks/allocation_table/prepare_final.py b/benchmarks/allocation_table/prepare_final.py index a00ecfa9b..00199ec49 100644 --- a/benchmarks/allocation_table/prepare_final.py +++ b/benchmarks/allocation_table/prepare_final.py @@ -128,9 +128,13 @@ def main(): close_flags = (["-DKVBENCH_CLOSE_HAS_FORCE"] if "splinterdb_close(splinterdb **kvs, bool32 force)" in (source / "include/splinterdb/splinterdb.h").read_text() else []) - link = flags + close_flags + ["-DKVBENCH_SPACE_MODE", "-I" + str(source / "include"), - str(harness), str(library), "-laio", "-lxxhash", "-lm", - "-o", str(binary)] + value_flags = ([] if "message_create(" in + (source / "include/splinterdb/data.h").read_text() + else ["-Dmessage_create=slice_create"]) + link = flags + close_flags + value_flags + [ + "-DKVBENCH_SPACE_MODE", "-I" + str(source / "include"), + str(harness), str(library), "-laio", "-lxxhash", "-lm", + "-o", str(binary)] run(link) manifest["variants"][name] = dict( source=str(source), revision=baseline_commit if name == "original" else current_commit, diff --git a/benchmarks/barrier_bench.c b/benchmarks/barrier_bench.c index ebe54300d..e7ae40642 100644 --- a/benchmarks/barrier_bench.c +++ b/benchmarks/barrier_bench.c @@ -106,7 +106,7 @@ run_workload(splinterdb_config *cfg, memcpy(value, &id, sizeof(id)); checked(splinterdb_insert(db, slice_create(sizeof(key), key), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL), "insert"); } diff --git a/benchmarks/btree_prefix_bench.c b/benchmarks/btree_prefix_bench.c index 8d7cd0ea7..5e6f9dc5f 100644 --- a/benchmarks/btree_prefix_bench.c +++ b/benchmarks/btree_prefix_bench.c @@ -472,7 +472,7 @@ make_inputs(benchmark *b) } b->tuples[i] = (tuple){ .tuple_key = generated[2 * i].tuple_key, - .msg = message_create( + .msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(o->value_bytes, v))}; b->misses[i] = generated[2 * i + 1].tuple_key; } @@ -658,7 +658,7 @@ run_inserts(benchmark *b) } verify_tree(b, root, PAGE_TYPE_MEMTABLE, false); for (uint64 i = 0; i < b->opt.keys; i++) - b->tuples[i].msg = message_create( + b->tuples[i].msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(b->opt.value_bytes, @@ -700,7 +700,7 @@ run_inserts(benchmark *b) } if (overwrite) { for (uint64 i = 0; i < b->opt.keys; i++) - b->tuples[i].msg = message_create( + b->tuples[i].msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(b->opt.value_bytes, diff --git a/benchmarks/build_barrier_bench.py b/benchmarks/build_barrier_bench.py index 6ec5f5367..eb2a076e2 100644 --- a/benchmarks/build_barrier_bench.py +++ b/benchmarks/build_barrier_bench.py @@ -23,6 +23,8 @@ command += ['-DHAVE_ASYNC_BARRIER', '-DHAVE_SENTINEL_NOTIFICATION'] elif 'splinterdb_durable_barrier_async' in header.read_text(): command.append('-DHAVE_ASYNC_BARRIER') +if 'message_create(' not in (tree / 'include/splinterdb/data.h').read_text(): + command.append('-Dmessage_create=slice_create') command += ['-I', str(tree / 'include'), str(source), str(library), '-laio', '-lxxhash', '-lm', '-lpthread', '-o', str(binary)] diff --git a/benchmarks/build_btree_prefix_bench.py b/benchmarks/build_btree_prefix_bench.py index acaf845a7..1bea14341 100644 --- a/benchmarks/build_btree_prefix_bench.py +++ b/benchmarks/build_btree_prefix_bench.py @@ -131,6 +131,8 @@ def main(): '-I', str(tree / 'src/platform_linux'), str(source_copy), identity['library'], '-laio', '-lxxhash', '-lm', '-lpthread', '-o', str(binary)] + if 'message_create_internal(' not in (tree / 'src/data_internal.h').read_text(): + command.append('-Dmessage_create_internal=message_create') log = artifact_dir / f'{name}-compile.log' invoke(command, log, environment) verify_profile(profiles[name]) diff --git a/benchmarks/build_prefix_bench.py b/benchmarks/build_prefix_bench.py index 2f848d58f..02c647933 100644 --- a/benchmarks/build_prefix_bench.py +++ b/benchmarks/build_prefix_bench.py @@ -117,6 +117,8 @@ '-DKVBENCH_SPACE_MODE'] if 'key_compare_is_lexical' in (tree / 'include/splinterdb/data.h').read_text(): command.append('-DKVBENCH_PREFIX_SUPPORT') +if 'message_create(' not in (tree / 'include/splinterdb/data.h').read_text(): + command.append('-Dmessage_create=slice_create') command += ['-I', str(tree / 'include'), str(generated), str(tree / a.build_root / 'release/lib/libsplinterdb.a'), '-laio', '-lxxhash', '-lm', '-lpthread', '-o', str(binary)] diff --git a/benchmarks/comparison/build.sh b/benchmarks/comparison/build.sh index 09343f57a..dec4fa924 100755 --- a/benchmarks/comparison/build.sh +++ b/benchmarks/comparison/build.sh @@ -29,20 +29,32 @@ CF='-D_GNU_SOURCE -ggdb -Wall -pthread -Wfatal-errors -Werror -Wvla -DXXH_STATIC LF='-ggdb -pthread -O3 -ffast-math -flto' LIBS='-laio -lxxhash -lm -lpthread' -cc $CF $LF -DKVBENCH_CLOSE_HAS_FORCE \ +head_value_flags=() +main_value_flags=() +for name in head main; do + if ! python3 -c 'import pathlib, sys; sys.exit("message_create(" not in pathlib.Path(sys.argv[1]).read_text())' "$OUT/src/$name/include/splinterdb/data.h"; then + if [ "$name" = head ]; then + head_value_flags=(-Dmessage_create=slice_create) + else + main_value_flags=(-Dmessage_create=slice_create) + fi + fi +done + +cc $CF $LF "${head_value_flags[@]}" -DKVBENCH_CLOSE_HAS_FORCE \ -I "$OUT/src/head/include" "$ROOT/benchmarks/kvbench.c" \ "$OUT/src/head/build/release/lib/libsplinterdb.a" $LIBS \ -o "$OUT/bin/kvbench-head" -cc $CF $LF \ +cc $CF $LF "${main_value_flags[@]}" \ -I "$OUT/src/main/include" "$ROOT/benchmarks/kvbench.c" \ "$OUT/src/main/build/release/lib/libsplinterdb.a" $LIBS \ -o "$OUT/bin/kvbench-main" -cc $CF $LF -I "$OUT/src/head/include" -I "$OUT/src/head/src" \ +cc $CF $LF "${head_value_flags[@]}" -I "$OUT/src/head/include" -I "$OUT/src/head/src" \ -I "$OUT/src/head/src/platform_linux" "$ROOT/benchmarks/insert_benchmark.c" \ "$OUT/src/head/build/release/lib/libsplinterdb.a" $LIBS \ -o "$OUT/bin/insert_benchmark-head" -cc $CF $LF -DKVBENCH_LEGACY_CLOSE -DKVBENCH_NO_TRACE \ +cc $CF $LF "${main_value_flags[@]}" -DKVBENCH_LEGACY_CLOSE -DKVBENCH_NO_TRACE \ -DKVBENCH_NO_DURABLE_BARRIER -I "$OUT/src/main/include" \ -I "$OUT/src/main/src" -I "$OUT/src/main/src/platform_linux" \ "$ROOT/benchmarks/insert_benchmark.c" \ diff --git a/benchmarks/hash_range_benchmark.c b/benchmarks/hash_range_benchmark.c index 209991287..9c461e803 100644 --- a/benchmarks/hash_range_benchmark.c +++ b/benchmarks/hash_range_benchmark.c @@ -205,7 +205,7 @@ main(int argc, char **argv) memcpy(value, key, sizeof(key)); check(splinterdb_insert(db, slice_create(sizeof(key), key), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL)); } } diff --git a/benchmarks/insert_benchmark.c b/benchmarks/insert_benchmark.c index 3fec74304..7254e5545 100644 --- a/benchmarks/insert_benchmark.c +++ b/benchmarks/insert_benchmark.c @@ -304,7 +304,7 @@ run_worker(void *arg) } else { rc = splinterdb_insert(b->db, slice_create(o->key_size, key), - slice_create(o->value_size, value), + message_create(o->value_size, value), NULL); } uint64_t end = now_ns(); diff --git a/benchmarks/kvbench.c b/benchmarks/kvbench.c index 7214f2689..d65789515 100644 --- a/benchmarks/kvbench.c +++ b/benchmarks/kvbench.c @@ -728,8 +728,10 @@ load_worker(void *arg) make_key(2 * id, key); make_value(id, 0, val); uint64_t t0 = now_ns(); - int rc = splinterdb_insert( - kvs, slice_create(KEY_SIZE, key), slice_create(VALUE_SIZE, val), NULL); + int rc = splinterdb_insert(kvs, + slice_create(KEY_SIZE, key), + message_create(VALUE_SIZE, val), + NULL); uint64_t t1 = now_ns(); if (rc) { w->errors++; @@ -1011,8 +1013,10 @@ ow_worker(void *arg) make_key(2 * id, key); make_value(id, generation, val); uint64_t t0 = now_ns(); - int rc = splinterdb_insert( - kvs, slice_create(KEY_SIZE, key), slice_create(VALUE_SIZE, val), NULL); + int rc = splinterdb_insert(kvs, + slice_create(KEY_SIZE, key), + message_create(VALUE_SIZE, val), + NULL); uint64_t t1 = now_ns(); n++; atomic_store_explicit(&w->progress, n, memory_order_relaxed); diff --git a/benchmarks/optimize_notification_bench.c b/benchmarks/optimize_notification_bench.c index 17ce4c4fb..3c47c2d01 100644 --- a/benchmarks/optimize_notification_bench.c +++ b/benchmarks/optimize_notification_bench.c @@ -105,7 +105,7 @@ main(int argc, char **argv) memcpy(value, &id, sizeof(id)); checked(splinterdb_insert(db, slice_create(sizeof(key), key), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL), "insert"); } diff --git a/docs/usage.md b/docs/usage.md index 66c580de0..4dab831e2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -69,29 +69,41 @@ looks out-of-date, please open an issue or pull request. functional tests. -## Reusing a value blob - -Use `splinterdb_blob_create()` to copy a value into blob storage once, then -`splinterdb_insert_blob()` to insert it under multiple keys. Full extents are -shared; tails may be copied on insertion or compaction. Small values may be -copied entirely, and each handle has allocation overhead, so this API is most -useful for large repeated values. +## Passing values and reusable blobs + +Both `splinterdb_insert()` and `splinterdb_update()` accept a public `message`. +Use `message_create(length, pointer)` for direct bytes, or +`message_from_slice(slice)` for an existing slice. These constructors borrow +the input without allocation or copying. Keys and lookup results still use +slices. Existing callers can replace value-side `slice_create()` calls with +`message_create()`. + +The called operation determines the message type: insert always overwrites, +and update always applies a delta through the configured merge callbacks. +Neither modifies the caller's message. Its `type` field is ignored on input, +so the same message can be passed to either operation. + +Use `splinterdb_blob_create()` to copy bytes into blob storage once, then pass +the returned message to either operation. Full extents are shared; tails may +be copied on insertion or compaction. Small values may be copied entirely, +and each created blob has allocation overhead, so this API is most useful +for large repeated payloads. ```c int -insert_shared_values(splinterdb *db, slice value, slice first_key, slice second_key) +insert_shared_values(splinterdb *db, slice bytes, slice first_key, slice second_key) { - splinterdb_blob *blob = NULL; - int rc = splinterdb_blob_create(db, value, &blob); + message value; + int rc = splinterdb_blob_create(db, bytes, &value); if (rc != 0) { return rc; } - rc = splinterdb_insert_blob(db, first_key, blob, NULL); + rc = splinterdb_insert(db, first_key, value, NULL); if (rc == 0) { - rc = splinterdb_insert_blob(db, second_key, blob, NULL); + rc = splinterdb_insert(db, second_key, value, NULL); } - splinterdb_blob_destroy(&blob); + splinterdb_blob_release(&value); if (rc == 0) { rc = splinterdb_durable_barrier(db, NULL); } @@ -99,25 +111,33 @@ insert_shared_values(splinterdb *db, slice value, slice first_key, slice second_ } ``` -The input value may be released after creation succeeds. Destroying a handle -releases the caller's ownership and clears its pointer; successfully inserted -values retain their own ownership. The two inserts above are independent -operations: if the second fails, the first is still applied. - -A handle is immutable and belongs to the same open database instance that -created it. Threads may insert it concurrently, but destruction must not race -any use. Handles cannot be serialized or reused after close/reopen. Destroy -all handles before `splinterdb_close(&db, FALSE)`, which returns `EBUSY` while -handles remain live. Forced close releases the handles and invalidates every -outstanding pointer; do not use or destroy those pointers afterward. +A reusable delta is passed as `splinterdb_update(db, key, value, NULL)`. +It is processed by the configured merge callbacks just like a direct delta; +the resulting value need not retain the delta's blob storage. + +The input bytes may be released after blob creation succeeds. Releasing the +message drops the caller's ownership and resets that message to an empty +direct message; successfully inserted values or deltas retain their own +ownership. Operations are independent: if a second operation fails, a first +successful operation remains applied. + +Copying a message only borrows the same payload. Release each created blob +exactly once, after all uses of all copies finish; release invalidates the +other copies. Callback messages are borrowed and must not be released. +Do not change a blob message's `cc`, `data`, or descriptor bytes. +For a blob, `message_data()`, `message_length()`, and `message_slice()` +describe its encoded descriptor, not its logical bytes. + +A blob belongs to the open database instance that created it. Threads may +reuse it concurrently, but release must not race any use. Blob messages cannot +be serialized or reused after close/reopen. Release all created blobs before +`splinterdb_close(&db, FALSE)`, which returns `EBUSY` while they remain live. +Forced close releases them and invalidates every outstanding copy. Creating a standalone blob does not establish crash durability. Inserted -references follow ordinary insert and durability-barrier semantics; a -successful blocking barrier covers the preceding successful inserts and their -blob data. Recovery reclaims storage that no recovered data references. -Keys still use the existing `slice` API and key-size limit; large-key support -is separate from this value-blob API. - +references follow ordinary update and durability-barrier semantics. Recovery +reclaims storage that no recovered data references. Keys retain the existing +size limit; large-key support is separate. ## Example programs - [`tests/unit/splinterdb_quick_test.c`](../tests/unit/splinterdb_quick_test.c) covers diff --git a/examples/splinterdb_custom_ipv4_addr_sortcmp_example.c b/examples/splinterdb_custom_ipv4_addr_sortcmp_example.c index d54a9407a..081b0937b 100644 --- a/examples/splinterdb_custom_ipv4_addr_sortcmp_example.c +++ b/examples/splinterdb_custom_ipv4_addr_sortcmp_example.c @@ -318,7 +318,8 @@ do_inserts(splinterdb *spl_handle, kv_pair *kv_pairs, int num_kv_pairs) slice_create(strlen(kv_pairs[ictr].kv_key), kv_pairs[ictr].kv_key); slice value = slice_create(WWW_PING_SIZE(&kv_pairs[ictr].kv_val), (const char *)&kv_pairs[ictr].kv_val); - int rc = splinterdb_insert(spl_handle, key, value, NULL); + int rc = + splinterdb_insert(spl_handle, key, message_from_slice(value), NULL); if (rc) { printf( "Insert of key '%s' failed; rc=%d\n", kv_pairs[ictr].kv_key, rc); diff --git a/examples/splinterdb_intro_example.c b/examples/splinterdb_intro_example.c index c59a159cb..498f28cd2 100644 --- a/examples/splinterdb_intro_example.c +++ b/examples/splinterdb_intro_example.c @@ -48,7 +48,7 @@ main() const char *fruit = "apple"; const char *descr = "An apple a day keeps the doctor away!"; slice key = slice_create((size_t)strlen(fruit), fruit); - slice value = slice_create((size_t)strlen(descr), descr); + message value = message_create((size_t)strlen(descr), descr); rc = splinterdb_insert(spl_handle, key, value, NULL); printf("Inserted key '%s'\n", fruit); @@ -56,37 +56,38 @@ main() fruit = "Orange"; descr = "Is a good source of vitamin-C."; key = slice_create((size_t)strlen(fruit), fruit); - value = slice_create((size_t)strlen(descr), descr); + value = message_create((size_t)strlen(descr), descr); rc = splinterdb_insert(spl_handle, key, value, NULL); printf("Inserted key '%s'\n", fruit); fruit = "Mango"; descr = "Mango is the king of fruits."; key = slice_create((size_t)strlen(fruit), fruit); - value = slice_create((size_t)strlen(descr), descr); + value = message_create((size_t)strlen(descr), descr); rc = splinterdb_insert(spl_handle, key, value, NULL); printf("Inserted key '%s'\n", fruit); // Retrieve a key-value pair. + slice lookup_value; splinterdb_lookup_result result; splinterdb_lookup_result_init(spl_handle, &result); fruit = "Orange"; key = slice_create((size_t)strlen(fruit), fruit); rc = splinterdb_lookup(spl_handle, key, &result); - rc = splinterdb_lookup_result_value(&result, &value); + rc = splinterdb_lookup_result_value(&result, &lookup_value); if (!rc) { printf("Found key: '%s', value: '%.*s'\n", fruit, - (int)slice_length(value), - (char *)slice_data(value)); + (int)slice_length(lookup_value), + (char *)slice_data(lookup_value)); } // Handling non-existent keys fruit = "Banana"; key = slice_create((size_t)strlen(fruit), fruit); rc = splinterdb_lookup(spl_handle, key, &result); - rc = splinterdb_lookup_result_value(&result, &value); + rc = splinterdb_lookup_result_value(&result, &lookup_value); if (rc) { printf("Key: '%s' not found. (rc=%d)\n", fruit, rc); } diff --git a/examples/splinterdb_iterators_example.c b/examples/splinterdb_iterators_example.c index f89b54c4f..f8adaeebc 100644 --- a/examples/splinterdb_iterators_example.c +++ b/examples/splinterdb_iterators_example.c @@ -112,7 +112,8 @@ do_inserts(splinterdb *spl_handle, kv_pair *kv_pairs, int num_kv_pairs) slice_create(strlen(kv_pairs[ictr].kv_key), kv_pairs[ictr].kv_key); slice value = slice_create(strlen(kv_pairs[ictr].kv_val), kv_pairs[ictr].kv_val); - int rc = splinterdb_insert(spl_handle, key, value, NULL); + int rc = + splinterdb_insert(spl_handle, key, message_from_slice(value), NULL); if (rc) { printf( "Insert for key='%s' failed, rc=%d\n", kv_pairs[ictr].kv_key, rc); diff --git a/examples/splinterdb_wide_values_example.c b/examples/splinterdb_wide_values_example.c index f3170cab6..d65c5e1e3 100644 --- a/examples/splinterdb_wide_values_example.c +++ b/examples/splinterdb_wide_values_example.c @@ -62,7 +62,7 @@ main() slice key = slice_create(strlen(key_buf), key_buf); slice value = slice_create(val_len, val_buf); - rc = splinterdb_insert(spl_handle, key, value, NULL); + rc = splinterdb_insert(spl_handle, key, message_from_slice(value), NULL); if (rc) { break; } diff --git a/include/splinterdb/data.h b/include/splinterdb/data.h index da8e5974c..d43af790f 100644 --- a/include/splinterdb/data.h +++ b/include/splinterdb/data.h @@ -49,11 +49,36 @@ typedef enum message_type { */ typedef struct message { message_type type; - // Internal: non-NULL means data is a blob descriptor. + // Internal: non-NULL means data is a blob descriptor. Do not change cc or + // descriptor bytes; use splinterdb_blob_create() to create reusable blobs. void *cc; slice data; } message; +// Construct an input message borrowing direct bytes, without allocation or +// copying. insert/update choose the operation type; the constructor leaves it +// INVALID. The bytes must remain valid until the operation returns. +// Keys and lookup results use slices. +static inline message +message_create(uint64 length, const void *data) +{ + return (message){.type = MESSAGE_TYPE_INVALID, + .cc = NULL, + .data = slice_create(length, data)}; +} + +static inline message +message_from_slice(slice data) +{ + return message_create(slice_length(data), slice_data(data)); +} + +// Copying a message borrows the same payload; it does not acquire ownership. +// Release an API-created blob exactly once, after all uses of all copies +// finish. Callback messages are borrowed and must not be released by callers. +// For blobs, message_data/length/slice below describe the encoded descriptor, +// not the logical bytes. Ordinary merge callbacks receive materialized data. + static inline message_type message_class(message msg) { diff --git a/include/splinterdb/splinterdb.h b/include/splinterdb/splinterdb.h index 8d2af8bca..2d074c56d 100644 --- a/include/splinterdb/splinterdb.h +++ b/include/splinterdb/splinterdb.h @@ -255,8 +255,8 @@ splinterdb_open(const splinterdb_config *cfg, splinterdb **kvs); // teardown. The database remains open and *kvs is unchanged, so the caller can // retry or investigate. // -// Live splinterdb_blob handles cause a non-forced close to return EBUSY. -// Destroy them and retry, or force close to release and invalidate them. +// Unreleased blob values cause a non-forced close to return EBUSY. +// Release them and retry, or force close to release and invalidate them. // // With force, teardown always completes. An error means data preservation // could not be guaranteed; it does not prove that data was actually lost. @@ -451,53 +451,46 @@ splinterdb_lookup(splinterdb *kvs, // IN // incorporated before catch-up can increment its reference count. In that // case the update has not been applied. -// Insert a key and value. Overwrites any previous value associated with the -// key. +// Insert a key and value. Overwrites any previous value associated with key. +// The message may hold direct bytes or a blob from this open database instance. +// Its type field is ignored: this operation always inserts/overwrites. +// A blob remains reusable on success or failure; insertion retains its own +// ownership. A blob belonging to another open database is rejected with EINVAL. // // If old_result is non-NULL, it must have been initialized using -// splinterdb_lookup_result_init() and will receive the value previously -// associated with key. +// splinterdb_lookup_result_init() and will receive the previous value. int splinterdb_insert(splinterdb *kvsb, slice key, - slice value, + message value, splinterdb_lookup_result *old_result); -// An immutable, reusable value owned by one open database instance. -// Create copies value into blob storage without inserting a key. The input -// slice may be released after create returns. On failure, *blob is NULL. -// Creation alone does not establish crash durability; inserted references use -// the ordinary update/durable_barrier contract. Unreferenced storage is -// reclaimed by allocation reconstruction after a crash. +// Copy bytes into immutable blob storage without inserting a key. On success, +// fill *value with a reusable blob message; the input bytes may then be +// released. On failure, *value is an empty direct value. Release any previous +// blob in *value before using it as output storage. // -// Handles may be reused concurrently by registered/automatically registered -// threads. Destroy must not race any use of that handle. Handles cannot be -// serialized or used with a different database or after close/reopen. +// Creation alone does not establish crash durability. Inserted references use +// the ordinary insert/update/durable_barrier contract. Allocation +// reconstruction reclaims unreferenced storage after a crash. // -// Existing blob cloning rules apply: full extents are shared, tails may be -// copied. Small values may be copied entirely. Each handle owns a separate -// mini allocator, so creating blobs for tiny values has allocation overhead. -typedef struct splinterdb_blob splinterdb_blob; - +// Blob values belong to one open database instance and cannot be serialized or +// reused after close/reopen. Threads may reuse them concurrently; release must +// not race any use. Cloning shares full extents and may copy tails. Small +// values may be copied entirely. Each created +// blob has its own mini allocator, so tiny blobs have allocation overhead. int -splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **blob); +splinterdb_blob_create(splinterdb *kvs, slice data, message *value); -// Release the caller's ownership and set *blob to NULL. Inserted values retain -// their own ownership. Passing NULL or a pointer to NULL is harmless. -// Destroy all handles before ordinary close. Forced close releases all handles -// and invalidates every outstanding handle pointer. +// Release ownership of a message returned by splinterdb_blob_create(). Inserted +// values/deltas retain their own ownership. Set *value to an empty direct +// value. NULL and direct values are harmless no-ops. Other copies of a released +// blob value become invalid. +// +// Release all created blobs before ordinary close. Forced close releases them +// and invalidates every outstanding copy; do not release those copies again. void -splinterdb_blob_destroy(splinterdb_blob **blob); - -// Insert a previously created blob as a value. This has the same overwrite, -// old_result, and durability semantics as splinterdb_insert(). The handle -// remains reusable on success or failure. A handle from another open database -// is rejected with EINVAL. Keys use the existing slice API and key-size limit. -int -splinterdb_insert_blob(splinterdb *kvs, - slice key, - const splinterdb_blob *blob, - splinterdb_lookup_result *old_result); +splinterdb_blob_release(message *value); // Delete a given key and any associated value / messages int @@ -505,11 +498,16 @@ splinterdb_delete(splinterdb *kvsb, slice key, splinterdb_lookup_result *old_result); -// Update the value associated with key. +// Apply a delta using the configured merge callbacks. The message's type field +// is ignored: this operation always applies an UPDATE. Both direct and blob +// values are accepted, with the same ownership and database restrictions as +// insert. Merge callbacks receive logical bytes through the configured +// callbacks; applying a blob delta need not preserve its storage in the +// resulting value. int splinterdb_update(splinterdb *kvsb, slice key, - slice delta, + message delta, splinterdb_lookup_result *old_result); // Optimize already-incorporated trunk data in [min_key, max_key). A null diff --git a/src/btree.c b/src/btree.c index 28b23d5bd..708c0aea7 100644 --- a/src/btree.c +++ b/src/btree.c @@ -3939,7 +3939,7 @@ btree_iterator_curr(iterator *base_itor, key *curr_key, message *data) .child_addr = entry->pivot_data.child_addr, .stats = index_entry_stats(entry), }; - *data = message_create( + *data = message_create_internal( MESSAGE_TYPE_PIVOT_DATA, NULL, slice_create(sizeof(itor->pivot_data), &itor->pivot_data)); diff --git a/src/data_internal.h b/src/data_internal.h index 62b8a8789..ac48e2982 100644 --- a/src/data_internal.h +++ b/src/data_internal.h @@ -358,7 +358,7 @@ message_type_string(message_type type) ((message){.type = MESSAGE_TYPE_DELETE, .data = NULL_SLICE}) static inline message -message_create(message_type type, cache *cc, slice data) +message_create_internal(message_type type, cache *cc, slice data) { return (message){.type = type, .cc = cc, .data = data}; } @@ -419,7 +419,7 @@ message_materialize_if_needed(platform_heap_id heap_id, writable_buffer_deinit(tmp); return rc; } - *result = message_create( + *result = message_create_internal( message_class(msg), NULL, writable_buffer_to_slice(tmp)); } else { *result = msg; @@ -510,9 +510,9 @@ ondisk_tuple_message(cache *cc, const ondisk_tuple *odt) { slice data = slice_create(odt->message_length, odt->key_and_message + odt->key_length); - return message_create(ondisk_tuple_message_class(odt), - ondisk_tuple_message_is_blob(odt) ? cc : NULL, - data); + return message_create_internal(ondisk_tuple_message_class(odt), + ondisk_tuple_message_is_blob(odt) ? cc : NULL, + data); } static inline void @@ -594,7 +594,8 @@ merge_accumulator_is_blob(const merge_accumulator *ma) static inline message merge_accumulator_to_message(const merge_accumulator *ma) { - return message_create(ma->type, ma->cc, writable_buffer_to_slice(&ma->data)); + return message_create_internal( + ma->type, ma->cc, writable_buffer_to_slice(&ma->data)); } static inline slice diff --git a/src/splinterdb.c b/src/splinterdb.c index 64888a48a..0dbd53b8a 100644 --- a/src/splinterdb.c +++ b/src/splinterdb.c @@ -46,6 +46,8 @@ splinterdb_get_version() return BUILD_VERSION; } +typedef struct splinterdb_blob splinterdb_blob; + typedef struct splinterdb { splinterdb_config effective_cfg; task_system task_sys; @@ -73,9 +75,10 @@ typedef struct splinterdb { struct splinterdb_blob { splinterdb *owner; uint64 meta_head; - writable_buffer descriptor; + slice descriptor; splinterdb_blob *next; splinterdb_blob *prev; + char bytes[]; }; #define SPLINTERDB_STATS_LOCK_IDX 0 @@ -835,14 +838,15 @@ splinterdb_close(splinterdb **kvs_in, bool32 force) // IN platform_assert(kvs != NULL); splinterdb_assert_thread_registered(); - // Close already requires that all other API calls have stopped. Drop + // Close requires that all other API calls have stopped. Drop // standalone references before core_unmount persists the allocation map. if (kvs->blobs != NULL && !force) { return EBUSY; } while (kvs->blobs != NULL) { - splinterdb_blob *blob = kvs->blobs; - splinterdb_blob_destroy(&blob); + message value = message_create_internal( + MESSAGE_TYPE_INVALID, kvs->spl.cc, kvs->blobs->descriptor); + splinterdb_blob_release(&value); } // Print stats if shared memory is enabled. @@ -1054,25 +1058,53 @@ splinterdb_insert_message(splinterdb *kvs, // IN return platform_status_to_int(status); } +// Only API-created blob values have this private prefix. Public copies are +// borrowed aliases of the same descriptor and do not add an ownership count. +static splinterdb_blob * +splinterdb_message_blob(message value) +{ + return (splinterdb_blob *)((char *)slice_data(value.data) + - offsetof(splinterdb_blob, bytes)); +} + +static int +splinterdb_apply_value(splinterdb *kvs, + slice user_key, + message value, + message_type type, + splinterdb_lookup_result *old_result) +{ + if (kvs == NULL + || (slice_length(value.data) != 0 && slice_data(value.data) == NULL)) + { + return EINVAL; + } + if (message_is_blob(value) && value.cc != kvs->spl.cc) { + return EINVAL; + } + message msg = message_create_internal(type, value.cc, value.data); + lookup_result *result = + old_result == NULL ? NULL : lookup_result_from_splinterdb(old_result); + return splinterdb_insert_message(kvs, user_key, msg, result); +} + int splinterdb_insert(splinterdb *kvsb, slice user_key, - slice value, + message value, splinterdb_lookup_result *old_result) { - message msg = message_create(MESSAGE_TYPE_INSERT, NULL, value); - lookup_result *_old_result = - old_result == NULL ? NULL : lookup_result_from_splinterdb(old_result); - return splinterdb_insert_message(kvsb, user_key, msg, _old_result); + return splinterdb_apply_value( + kvsb, user_key, value, MESSAGE_TYPE_INSERT, old_result); } int -splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **out) +splinterdb_blob_create(splinterdb *kvs, slice value, message *out) { if (out == NULL) { return EINVAL; } - *out = NULL; + *out = message_create(0, NULL); if (kvs == NULL || (slice_length(value) != 0 && slice_data(value) == NULL)) { return EINVAL; } @@ -1081,18 +1113,31 @@ splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **out) return rc; } - splinterdb_blob *handle = TYPED_ZALLOC(kvs->heap_id, handle); + cache *cc = kvs->spl.cc; + allocator *al = cache_get_allocator(cc); + if (slice_length(value) > allocator_get_config(al)->capacity) { + return ENOSPC; + } + blob_build_config cfg = kvs->btree_cfg.blob_cfg; + cfg.extent_batch = cfg.page_batch = cfg.subpage_batch = 0; + uint64 descriptor_capacity; + platform_status status = blob_descriptor_size_bound( + &cfg, cache_extent_size(cc), slice_length(value), &descriptor_capacity); + if (!SUCCESS(status)) { + return platform_status_to_int(status); + } + splinterdb_blob *handle = TYPED_FLEXIBLE_STRUCT_ZALLOC( + kvs->heap_id, handle, bytes, descriptor_capacity); if (handle == NULL) { return ENOMEM; } handle->owner = kvs; - writable_buffer_init(&handle->descriptor, kvs->heap_id); + writable_buffer descriptor; + writable_buffer_init_with_buffer( + &descriptor, kvs->heap_id, descriptor_capacity, handle->bytes, 0); splinterdb_stats_read_lock(kvs); - cache *cc = kvs->spl.cc; - allocator *al = cache_get_allocator(cc); - platform_status status = - allocator_alloc(al, &handle->meta_head, PAGE_TYPE_BLOB); + status = allocator_alloc(al, &handle->meta_head, PAGE_TYPE_BLOB); if (!SUCCESS(status)) { goto fail; } @@ -1110,15 +1155,16 @@ splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **out) // A private mini allocator isolates the caller's lifetime from memtable // rotations and log reclamation. One batch suffices for this one blob. - blob_build_config cfg = kvs->btree_cfg.blob_cfg; - cfg.extent_batch = cfg.page_batch = cfg.subpage_batch = 0; - status = blob_build(&cfg, cc, &mini, value, &handle->descriptor); + status = blob_build(&cfg, cc, &mini, value, &descriptor); mini_release(&mini); if (!SUCCESS(status)) { mini_dec_ref(cc, handle->meta_head, PAGE_TYPE_BLOB); goto fail; } + platform_assert(writable_buffer_data(&descriptor) == handle->bytes); + handle->descriptor = writable_buffer_to_slice(&descriptor); + writable_buffer_deinit(&descriptor); splinterdb_blob_list_lock(kvs); handle->next = kvs->blobs; if (handle->next != NULL) { @@ -1127,24 +1173,24 @@ splinterdb_blob_create(splinterdb *kvs, slice value, splinterdb_blob **out) kvs->blobs = handle; splinterdb_blob_list_unlock(kvs); splinterdb_stats_read_unlock(kvs); - *out = handle; + *out = message_create_internal(MESSAGE_TYPE_INVALID, cc, handle->descriptor); return 0; fail: splinterdb_stats_read_unlock(kvs); - writable_buffer_deinit(&handle->descriptor); + writable_buffer_deinit(&descriptor); platform_free(kvs->heap_id, handle); return platform_status_to_int(status); } void -splinterdb_blob_destroy(splinterdb_blob **blob) +splinterdb_blob_release(message *value) { - if (blob == NULL || *blob == NULL) { + if (value == NULL || !message_is_blob(*value)) { return; } splinterdb_assert_thread_registered(); - splinterdb_blob *handle = *blob; + splinterdb_blob *handle = splinterdb_message_blob(*value); splinterdb *kvs = handle->owner; splinterdb_stats_read_lock(kvs); splinterdb_blob_list_lock(kvs); @@ -1158,29 +1204,11 @@ splinterdb_blob_destroy(splinterdb_blob **blob) } splinterdb_blob_list_unlock(kvs); mini_dec_ref(kvs->spl.cc, handle->meta_head, PAGE_TYPE_BLOB); - writable_buffer_deinit(&handle->descriptor); platform_free(kvs->heap_id, handle); - *blob = NULL; + *value = message_create(0, NULL); splinterdb_stats_read_unlock(kvs); } -int -splinterdb_insert_blob(splinterdb *kvs, - slice user_key, - const splinterdb_blob *blob, - splinterdb_lookup_result *old_result) -{ - if (kvs == NULL || blob == NULL || blob->owner != kvs) { - return EINVAL; - } - message msg = message_create(MESSAGE_TYPE_INSERT, - kvs->spl.cc, - writable_buffer_to_slice(&blob->descriptor)); - lookup_result *result = - old_result == NULL ? NULL : lookup_result_from_splinterdb(old_result); - return splinterdb_insert_message(kvs, user_key, msg, result); -} - int splinterdb_delete(splinterdb *kvsb, slice user_key, @@ -1195,7 +1223,7 @@ splinterdb_delete(splinterdb *kvsb, int splinterdb_update(splinterdb *kvsb, slice user_key, - slice update, + message update, splinterdb_lookup_result *old_result) { if (kvsb == NULL || kvsb->data_cfg == NULL @@ -1204,10 +1232,8 @@ splinterdb_update(splinterdb *kvsb, { return platform_status_to_int(STATUS_BAD_PARAM); } - message msg = message_create(MESSAGE_TYPE_UPDATE, NULL, update); - lookup_result *_old_result = - old_result == NULL ? NULL : lookup_result_from_splinterdb(old_result); - return splinterdb_insert_message(kvsb, user_key, msg, _old_result); + return splinterdb_apply_value( + kvsb, user_key, update, MESSAGE_TYPE_UPDATE, old_result); } int diff --git a/tests/functional/btree_test.c b/tests/functional/btree_test.c index 185e805f9..bd8db29ff 100644 --- a/tests/functional/btree_test.c +++ b/tests/functional/btree_test.c @@ -78,7 +78,7 @@ btree_height_limit_iterator_curr(iterator *base, key *curr_key, message *msg) platform_assert(itor->pos < BTREE_HEIGHT_LIMIT_TEST_NUM_KEYS); *curr_key = key_create( FALSE, BTREE_HEIGHT_LIMIT_TEST_KEY_SIZE, itor->keys[itor->pos]); - *msg = message_create( + *msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(itor->data), &itor->data)); } diff --git a/tests/functional/log_test.c b/tests/functional/log_test.c index 7a245fe97..af857ba71 100644 --- a/tests/functional/log_test.c +++ b/tests/functional/log_test.c @@ -1365,10 +1365,10 @@ test_log_prepare_failure_does_not_poison(clockcache *cc, .checksum = {0}, .format = BLOB_FORMAT + 1, }; - message invalid_msg = - message_create(MESSAGE_TYPE_INSERT, - cacheh, - slice_create(sizeof(invalid_blob), &invalid_blob)); + message invalid_msg = message_create_internal( + MESSAGE_TYPE_INSERT, + cacheh, + slice_create(sizeof(invalid_blob), &invalid_blob)); DECLARE_AUTO_KEY_BUFFER(keybuffer, hid); key invalid_key = test_key(&keybuffer, TEST_RANDOM, 257, 0, 0, key_size, 0); int append_rc = log_write(log, invalid_key, invalid_msg, 257, 0); @@ -1703,7 +1703,7 @@ test_log_rejects_oversized_blob_descriptor(cache *cc, log_handle *log; platform_assert_status_ok(shard_log_create(cc, cfg, hid, &log)); log_head head = log_get_head(log); - message msg = message_create( + message msg = message_create_internal( MESSAGE_TYPE_INSERT, cc, writable_buffer_to_slice(&descriptor)); int append_rc = log_write(log, skey, msg, 0, 0); platform_assert(append_rc == STATUS_LIMIT_EXCEEDED.r); @@ -1743,9 +1743,9 @@ test_log_malformed_record_bounds(clockcache *cc, char value_data[] = "value"; key skey = key_create(FALSE, sizeof(key_data) - 1, key_data); message msg = - message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(value_data) - 1, value_data)); + message_create_internal(MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(value_data) - 1, value_data)); platform_assert_status_ok(shard_log_create(cacheh, cfg, hid, &log)); log_head sealed = log_get_head(log); @@ -1819,9 +1819,9 @@ test_log_group_page_cycle(clockcache *cc, char value_data[] = "value"; key skey = key_create(FALSE, sizeof(key_data) - 1, key_data); message msg = - message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(value_data) - 1, value_data)); + message_create_internal(MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(value_data) - 1, value_data)); platform_assert_status_ok(shard_log_create(cacheh, cfg, hid, &log)); log_head sealed = log_get_head(log); @@ -2364,7 +2364,7 @@ test_log_indirect_blob_recovery(clockcache *cc, char key_data[] = "indirect-recovery"; key skey = key_create(FALSE, sizeof(key_data) - 1, key_data); message small = - message_create(MESSAGE_TYPE_INSERT, NULL, slice_create(2, "ok")); + message_create_internal(MESSAGE_TYPE_INSERT, NULL, slice_create(2, "ok")); platform_assert(log_write(log, skey, small, 0, 0) == 0); platform_assert_status_ok(log_make_durable(log)); merge_accumulator value; @@ -2782,7 +2782,7 @@ test_log_group_size_hint(cache *cc, shard_log_config *cfg, platform_heap_id hid) log_head head = log_get_head(logh); key k = key_create(FALSE, 1, "k"); message msg = - message_create(MESSAGE_TYPE_INSERT, NULL, slice_create(1, "v")); + message_create_internal(MESSAGE_TYPE_INSERT, NULL, slice_create(1, "v")); platform_assert(log_write(logh, k, msg, 0, 0) == 0); platform_assert(((shard_log *)logh)->durable_ticket == 0); platform_assert(!log_group_size_reached(logh)); diff --git a/tests/functional/scan_benchmark.c b/tests/functional/scan_benchmark.c index fc77a01f5..1c56dcf3e 100644 --- a/tests/functional/scan_benchmark.c +++ b/tests/functional/scan_benchmark.c @@ -473,7 +473,7 @@ scan_benchmark_load_database(const splinterdb_config *cfg, scan_benchmark_encode_record_key( key_record_no, splinter_random_keys, keybuf); slice key = slice_create(sizeof(keybuf), keybuf); - rc = splinterdb_insert(kvs, key, value, NULL); + rc = splinterdb_insert(kvs, key, message_from_slice(value), NULL); if (rc != 0) { platform_error_log( "scan_benchmark: insert failed at record %lu: %d\n", record_no, rc); diff --git a/tests/functional/ycsb_test.c b/tests/functional/ycsb_test.c index e598e7fbd..92b8332b5 100644 --- a/tests/functional/ycsb_test.c +++ b/tests/functional/ycsb_test.c @@ -356,10 +356,10 @@ ycsb_thread(void *arg) case 'i': case 'u': { - message val = - message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(YCSB_DATA_SIZE, ops->value)); + message val = message_create_internal( + MESSAGE_TYPE_INSERT, + NULL, + slice_create(YCSB_DATA_SIZE, ops->value)); rc = core_insert( spl, key_create(FALSE, YCSB_KEY_SIZE, ops->key), val, NULL); platform_assert_status_ok(rc); diff --git a/tests/unit/btree_stress_test.c b/tests/unit/btree_stress_test.c index e570f88d4..b7ae6459d 100644 --- a/tests/unit/btree_stress_test.c +++ b/tests/unit/btree_stress_test.c @@ -342,7 +342,7 @@ CTEST2(btree_stress, insert_callback_failure_after_root_growth) root_addr, &mini, key_create(FALSE, sizeof(key_data), &key_data), - message_create( + message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(value), value)), &results); btree_insert_results_deinit(&results); @@ -375,9 +375,9 @@ CTEST2(btree_stress, insert_callback_failure_after_root_growth) root_addr, &mini, rejected_key, - message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(large_value), large_value)), + message_create_internal(MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(large_value), large_value)), &results); btree_insert_results_deinit(&results); ASSERT_TRUE(STATUS_IS_EQ(rc, STATUS_LIMIT_EXCEEDED)); @@ -461,7 +461,7 @@ CTEST2(btree_stress, insert_rebuild_failure_releases_parent_claim) root_addr, &mini, key_create(FALSE, sizeof(key_data), &key_data), - message_create( + message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(value), value)), &results); btree_insert_results_deinit(&results); @@ -472,9 +472,9 @@ CTEST2(btree_stress, insert_rebuild_failure_releases_parent_claim) uint64 key_data = htobe64(UINT64_MAX); uint8 large_value[1400] = {2}; message replacement = - message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(large_value), large_value)); + message_create_internal(MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(large_value), large_value)); failing_insert_callback_context callback_ctxt = {0}; btree_insert_results_init(&results, NULL); btree_insert_results_set_callback( @@ -514,16 +514,16 @@ CTEST2(btree_stress, insert_rebuild_failure_releases_parent_claim) key_data = htobe64(0); key target = key_create(FALSE, sizeof(key_data), &key_data); btree_insert_results_init(&results, NULL); - rc = btree_insert( - cc, - cfg, - data->hid, - data->test_scratch, - root_addr, - &mini, - target, - message_create(MESSAGE_TYPE_UPDATE, NULL, message_slice(replacement)), - &results); + rc = btree_insert(cc, + cfg, + data->hid, + data->test_scratch, + root_addr, + &mini, + target, + message_create_internal( + MESSAGE_TYPE_UPDATE, NULL, message_slice(replacement)), + &results); btree_insert_results_deinit(&results); cc->ops = ops.original; cfg->data_cfg = original_data_cfg; @@ -913,9 +913,10 @@ gen_msg(btree_config *cfg, uint64 i, uint8 *buffer, size_t length) dh->ref_count = 1; memset(dh->data, 0, datalen); memcpy(dh->data, &i, sizeof(i)); - return message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(data_handle) + datalen, buffer)); + return message_create_internal( + MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(data_handle) + datalen, buffer)); } static int @@ -1188,7 +1189,7 @@ bounds_insert(cache *cc, root, mini, bounds_key(n, &keybuf), - message_create( + message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(msgbuf), msgbuf)), &result); btree_insert_results_deinit(&result); diff --git a/tests/unit/btree_test.c b/tests/unit/btree_test.c index 01cc542a4..505eec33d 100644 --- a/tests/unit/btree_test.c +++ b/tests/unit/btree_test.c @@ -415,7 +415,7 @@ leaf_hdr_tests(btree_config *cfg, btree_scratch *scratch, platform_heap_id hid) hdr, i, key_create(FALSE, i % sizeof(i), &i), - message_create( + message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(i % sizeof(i), &i))); ASSERT_TRUE(rv, "Failed to insert 4-byte %d\n", i); } @@ -428,10 +428,10 @@ leaf_hdr_tests(btree_config *cfg, btree_scratch *scratch, platform_heap_id hid) cfg->data_cfg, key_create(FALSE, i % sizeof(i), &i), tuple_key); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte key %d\n", i); - cmp_rv = message_lex_cmp(message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(i % sizeof(i), &i)), - msg); + cmp_rv = message_lex_cmp( + message_create_internal( + MESSAGE_TYPE_INSERT, NULL, slice_create(i % sizeof(i), &i)), + msg); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte message %d\n", i); } @@ -442,7 +442,7 @@ leaf_hdr_tests(btree_config *cfg, btree_scratch *scratch, platform_heap_id hid) hdr, i, key_create(FALSE, i % sizeof(i), &i), - message_create( + message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(i % sizeof(i), &i))); ASSERT_TRUE(rv, "Failed to insert 8-byte %ld\n", i); } @@ -455,10 +455,10 @@ leaf_hdr_tests(btree_config *cfg, btree_scratch *scratch, platform_heap_id hid) cfg->data_cfg, key_create(FALSE, i % sizeof(i), &i), tuple_key); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte key %d\n", i); - cmp_rv = message_lex_cmp(message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(i % sizeof(i), &i)), - msg); + cmp_rv = message_lex_cmp( + message_create_internal( + MESSAGE_TYPE_INSERT, NULL, slice_create(i % sizeof(i), &i)), + msg); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte message %d\n", i); } @@ -472,10 +472,10 @@ leaf_hdr_tests(btree_config *cfg, btree_scratch *scratch, platform_heap_id hid) cfg->data_cfg, key_create(FALSE, i % sizeof(i), &i), tuple_key); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte key %d\n", i); - cmp_rv = message_lex_cmp(message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(i % sizeof(i), &i)), - msg); + cmp_rv = message_lex_cmp( + message_create_internal( + MESSAGE_TYPE_INSERT, NULL, slice_create(i % sizeof(i), &i)), + msg); ASSERT_EQUAL(0, cmp_rv, "Bad 4-byte message %d\n", i); } @@ -501,7 +501,7 @@ leaf_hdr_search_tests(btree_config *cfg, platform_heap_id hid) messagebuf[0] = i; key tuple_key = key_create(FALSE, 1, &keybuf); - message msg = message_create( + message msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(i % 8, messagebuf)); leaf_incorporate_spec spec; @@ -640,9 +640,9 @@ leaf_split_tests(btree_config *cfg, btree_init_hdr(cfg, hdr); int msgsize = bt_page_size / (nkvs + 1); - message msg = message_create( + message msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(msgsize, msg_buffer)); - message bigger_msg = message_create( + message bigger_msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(msgsize + sizeof(table_entry) + 1, msg_buffer)); @@ -724,7 +724,7 @@ defragmented_leaf_bytes_tests(btree_config *cfg, btree_init_hdr(cfg, hdr); for (uint8 i = 0; i < nkvs; i++) { uint8 keybuf = 2 * i + 1; - message msg = message_create( + message msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(i % 7 + 1, msgbuf)); ASSERT_TRUE( btree_set_leaf_entry(cfg, hdr, i, key_create(FALSE, 1, &keybuf), msg)); @@ -732,7 +732,7 @@ defragmented_leaf_bytes_tests(btree_config *cfg, // Growing every third message leaves its old copy behind as garbage. for (uint8 i = 0; i < nkvs; i += 3) { uint8 keybuf = 2 * i + 1; - message msg = message_create( + message msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(i % 7 + 11, msgbuf)); ASSERT_TRUE( btree_set_leaf_entry(cfg, hdr, i, key_create(FALSE, 1, &keybuf), msg)); @@ -749,8 +749,8 @@ defragmented_leaf_bytes_tests(btree_config *cfg, memcpy(copy_buffer, leaf_buffer, page_size); btree_hdr *copy = (btree_hdr *)copy_buffer; leaf_incorporate_spec spec; - message msg = - message_create(MESSAGE_TYPE_INSERT, NULL, slice_create(9, msgbuf)); + message msg = message_create_internal( + MESSAGE_TYPE_INSERT, NULL, slice_create(9, msgbuf)); platform_status rc = btree_create_leaf_incorporate_spec(cfg, NULL, diff --git a/tests/unit/large_inserts_stress_test.c b/tests/unit/large_inserts_stress_test.c index 2b2a293ff..88c0021ba 100644 --- a/tests/unit/large_inserts_stress_test.c +++ b/tests/unit/large_inserts_stress_test.c @@ -192,7 +192,8 @@ CTEST2_SKIP(large_inserts_stress, slice key = slice_create(strlen(key_data), key_data); slice val = slice_create(strlen(val_data), val_data); - int rc = splinterdb_insert(data->kvsb, key, val, NULL); + int rc = + splinterdb_insert(data->kvsb, key, message_from_slice(val), NULL); ASSERT_EQUAL(0, rc); } uint64 elapsed_ns = platform_timestamp_elapsed(start_time); @@ -705,7 +706,7 @@ exec_worker_thread(void *w) slice key = slice_create(key_len, key_data); slice val = slice_create(val_len, val_data); - int rc = splinterdb_insert(kvsb, key, val, NULL); + int rc = splinterdb_insert(kvsb, key, message_from_slice(val), NULL); ASSERT_EQUAL(0, rc); } if (verbose_progress) { diff --git a/tests/unit/limitations_test.c b/tests/unit/limitations_test.c index 1937c94eb..be3f73823 100644 --- a/tests/unit/limitations_test.c +++ b/tests/unit/limitations_test.c @@ -376,12 +376,12 @@ CTEST2(limitations, test_splinterdb_create_invalid_data_callbacks) merge_accumulator accumulator; message update = - message_create(MESSAGE_TYPE_UPDATE, NULL, slice_create(1, "u")); + message_create_internal(MESSAGE_TYPE_UPDATE, NULL, slice_create(1, "u")); ASSERT_TRUE(merge_accumulator_init_from_message( &accumulator, PROCESS_PRIVATE_HEAP_ID, update)); key tuple_key = key_create_from_slice(FALSE, slice_create(1, "k")); message old_message = - message_create(MESSAGE_TYPE_INSERT, NULL, slice_create(1, "v")); + message_create_internal(MESSAGE_TYPE_INSERT, NULL, slice_create(1, "v")); ASSERT_NOT_EQUAL( 0, data_merge_tuples(&invalid, tuple_key, old_message, &accumulator)); ASSERT_NOT_EQUAL(0, @@ -392,7 +392,7 @@ CTEST2(limitations, test_splinterdb_create_invalid_data_callbacks) ASSERT_EQUAL(0, splinterdb_create(&cfg, &kvsb)); ASSERT_EQUAL(STATUS_BAD_PARAM.r, splinterdb_update( - kvsb, slice_create(1, "k"), slice_create(1, "v"), NULL)); + kvsb, slice_create(1, "k"), message_create(1, "v"), NULL)); ASSERT_EQUAL(0, splinterdb_close(&kvsb, FALSE)); } @@ -442,7 +442,7 @@ CTEST2(limitations, test_runtime_allocation_exhaustion_returns_error) ASSERT_TRUE(0 < key_len && (uint64)key_len < sizeof(key)); rc = splinterdb_insert(kvsb, slice_create((uint64)key_len, key), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL); } ASSERT_EQUAL(STATUS_NO_SPACE.r, rc); diff --git a/tests/unit/merge_test.c b/tests/unit/merge_test.c index 27eb09557..6f5d9c6a8 100644 --- a/tests/unit/merge_test.c +++ b/tests/unit/merge_test.c @@ -38,7 +38,7 @@ merge_test_curr(iterator *base, key *k, message *msg) ? MESSAGE_TYPE_UPDATE : MESSAGE_TYPE_INSERT; const char *val = itor->vals ? itor->vals + itor->pos : "v"; - *msg = message_create(type, NULL, slice_create(1, val)); + *msg = message_create_internal(type, NULL, slice_create(1, val)); } static bool32 diff --git a/tests/unit/ondisk_ref_test.c b/tests/unit/ondisk_ref_test.c index 9b5fdd40a..97e243322 100644 --- a/tests/unit/ondisk_ref_test.c +++ b/tests/unit/ondisk_ref_test.c @@ -149,9 +149,10 @@ CTEST(ondisk_ref, lookup_stages_ownership_before_publishing_blob) sizeof(result_buffer), result_buffer); // This path only copies a descriptor; no cache or blob I/O is involved. - message msg = message_create(MESSAGE_TYPE_INSERT, - (cache *)&owner, - slice_create(sizeof(descriptor), descriptor)); + message msg = + message_create_internal(MESSAGE_TYPE_INSERT, + (cache *)&owner, + slice_create(sizeof(descriptor), descriptor)); ASSERT_TRUE(STATUS_IS_EQ( STATUS_NO_MEMORY, lookup_result_update(&result, NULL_KEY, msg, &source))); ASSERT_TRUE(merge_accumulator_is_null(&result.value)); @@ -185,9 +186,10 @@ CTEST(ondisk_ref, failed_message_copy_releases_staged_owner) char descriptor[128] = "borrowed descriptor"; lookup_result result; lookup_result_init(&result, NULL, SPLINTERDB_LOOKUP_VALUE, 0, NULL); - message msg = message_create(MESSAGE_TYPE_INSERT, - (cache *)&owner, - slice_create(sizeof(descriptor), descriptor)); + message msg = + message_create_internal(MESSAGE_TYPE_INSERT, + (cache *)&owner, + slice_create(sizeof(descriptor), descriptor)); platform_memory_fault_config saved; platform_memory_fault_config_get(&saved); platform_memory_fault_config fault = saved; diff --git a/tests/unit/splinter_test.c b/tests/unit/splinter_test.c index b09e0ca61..0bbaa7ea4 100644 --- a/tests/unit/splinter_test.c +++ b/tests/unit/splinter_test.c @@ -433,10 +433,10 @@ core_log_write_prepare_invalid_blob(log_handle *log, .checksum = {0}, .format = BLOB_FORMAT + 1, }; - message invalid_msg = - message_create(MESSAGE_TYPE_INSERT, - ((shard_log *)log)->cc, - slice_create(sizeof(invalid_blob), &invalid_blob)); + message invalid_msg = message_create_internal( + MESSAGE_TYPE_INSERT, + ((shard_log *)log)->cc, + slice_create(sizeof(invalid_blob), &invalid_blob)); return fault->saved_ops->write_prepare( log, tuple_key, invalid_msg, force_blob, prepared); } @@ -1296,7 +1296,7 @@ CTEST2(splinter, test_retain_failure_blob_lookup_and_old_value_are_atomic) ASSERT_TRUE( SUCCESS(writable_buffer_resize(&bytes, 2 * cache_page_size(cc)))); blob_checksum_test_fill(&bytes, writable_buffer_length(&bytes)); - message original = message_create( + message original = message_create_internal( MESSAGE_TYPE_INSERT, NULL, writable_buffer_to_slice(&bytes)); btree_insert_results inserted; btree_insert_results_init(&inserted, NULL); @@ -1348,7 +1348,7 @@ CTEST2(splinter, test_retain_failure_blob_lookup_and_old_value_are_atomic) } uint64 replacement_value = 0x12345678; - message replacement = message_create( + message replacement = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(replacement_value), &replacement_value)); @@ -1443,7 +1443,7 @@ CTEST2(splinter, test_retain_failure_blob_clone_prefix_cleanup) ASSERT_EQUAL_U(2, parsed.num_extents); uint64 first = parsed.base->addrs[0]; uint64 second = parsed.base->addrs[1]; - message incoming = message_create( + message incoming = message_create_internal( MESSAGE_TYPE_INSERT, cc, writable_buffer_to_slice(&descriptor)); DECLARE_AUTO_KEY_BUFFER(keybuf, data->hid); test_key(&keybuf, TEST_RANDOM, 1, 0, 0, data->workload_cfg->key_size, 0); @@ -1642,9 +1642,9 @@ CTEST2(splinter, test_blob_checksums) goto cleanup; } - message inline_msg = message_create( + message inline_msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, writable_buffer_to_slice(&source_data)); - message blob_msg = message_create( + message blob_msg = message_create_internal( MESSAGE_TYPE_INSERT, cc, writable_buffer_to_slice(&descriptor)); if (!SUCCESS(message_validate(inline_msg)) || !SUCCESS(message_validate(blob_msg))) @@ -1754,7 +1754,7 @@ CTEST2(splinter, test_blob_checksums) rc = STATUS_TEST_FAILED; goto cleanup; } - blob_msg = message_create( + blob_msg = message_create_internal( MESSAGE_TYPE_INSERT, cc, writable_buffer_to_slice(&checked_clone)); rc = message_validate(blob_msg); if (!STATUS_IS_EQ(rc, STATUS_IO_ERROR)) { @@ -1879,7 +1879,7 @@ CTEST2(splinter, test_btree_pack_rejects_counter_overflow) pack_abort_test_iterator itor = { .super = {.ops = &pack_abort_test_ops}, .tuple_key = key_create(FALSE, 1, &key_byte), - .msg = message_create( + .msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(UINT64_MAX, &value_byte)), }; btree_pack_req req; @@ -3297,7 +3297,7 @@ CTEST2(splinter, test_shard_log_first_fit_decreasing_packing) TYPED_ARRAY_MALLOC(data->hid, payload[rank], message_size); platform_assert(payload[rank] != NULL); memset(payload[rank], (int)(rank + 1), message_size); - expected_msg[rank] = message_create( + expected_msg[rank] = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(message_size, payload[rank])); } @@ -5809,7 +5809,7 @@ shadow_entry_key(const shadow_entry *entry, char *data) static message shadow_entry_value(const shadow_entry *entry, char *data) { - return message_create( + return message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(entry->value_length, @@ -7205,7 +7205,7 @@ CTEST2(splinter, test_blob_height_three_and_large_stats) blob_tree_pack_iterator iter = { .super = {.ops = &blob_tree_pack_ops}, - .msg = message_create(MESSAGE_TYPE_INSERT, cc, desc)}; + .msg = message_create_internal(MESSAGE_TYPE_INSERT, cc, desc)}; btree_pack_req *req = TYPED_MALLOC(data->hid, req); ASSERT_NOT_NULL(req); ASSERT_TRUE(SUCCESS(btree_pack_req_init( diff --git a/tests/unit/splinterdb_blob_test.c b/tests/unit/splinterdb_blob_test.c index 83602e70f..3e534f110 100644 --- a/tests/unit/splinterdb_blob_test.c +++ b/tests/unit/splinterdb_blob_test.c @@ -27,6 +27,35 @@ blob_test_in_use_bytes(allocator *al) return allocator_in_use(al) * allocator_get_config(al)->io_cfg->extent_size; } +// Append is deliberately non-idempotent: treating UPDATE as INSERT would +// discard the prefix and fail the expected-byte comparisons below. +static int +blob_test_append(const data_config *cfg, + slice key, + message older, + merge_accumulator *newer) +{ + uint64 old_length = message_length(older); + uint64 new_length = merge_accumulator_length(newer); + if (!merge_accumulator_resize(newer, old_length + new_length)) { + return ENOMEM; + } + char *bytes = merge_accumulator_data(newer); + memmove(bytes + old_length, bytes, new_length); + memcpy(bytes, message_data(older), old_length); + merge_accumulator_set_class(newer, message_class(older)); + return 0; +} + +static int +blob_test_append_final(const data_config *cfg, + slice key, + merge_accumulator *oldest) +{ + merge_accumulator_set_class(oldest, MESSAGE_TYPE_INSERT); + return 0; +} + CTEST_DATA(splinterdb_blob) { splinterdb *db; @@ -127,26 +156,26 @@ CTEST2(splinterdb_blob, test_reuse_release_compact_and_reopen) char *value = malloc(size ? size : 1); ASSERT_TRUE(value != NULL); memset(value, 0x39, size); - splinterdb_blob *handle = NULL; + message handle = {0}; ASSERT_EQUAL(0, splinterdb_blob_create( data->db, slice_create(size, value), &handle)); - ASSERT_TRUE(handle != NULL); + ASSERT_TRUE(message_is_blob(handle)); /* Creation owns the bytes before the user's input changes. */ memset(value, 0x7c, size); for (uint64 i = 0; i < count; i++) { ASSERT_EQUAL( 0, - splinterdb_insert_blob( + splinterdb_insert( data->db, slice_create(sizeof(i), &i), handle, NULL)); } memset(value, 0x39, size); slice expected = slice_create(size, value); blob_test_check_shared_values(data->db, count, expected); - splinterdb_blob_destroy(&handle); - ASSERT_NULL(handle); + splinterdb_blob_release(&handle); + ASSERT_FALSE(message_is_blob(handle)); ASSERT_EQUAL( 0, splinterdb_optimize(data->db, .full_leaf_compactions = TRUE)); blob_test_check_shared_values(data->db, count, expected); @@ -167,22 +196,23 @@ CTEST2(splinterdb_blob, test_overwrite_delete_and_old_result) char *value = malloc(size); ASSERT_TRUE(value != NULL); memset(value, 0x65, size); - slice expected = slice_create(size, value); - slice key = slice_create(3, "key"); - slice ordinary = slice_create(8, "ordinary"); - splinterdb_blob *handle = NULL; + slice expected = slice_create(size, value); + slice key = slice_create(3, "key"); + slice ordinary = slice_create(8, "ordinary"); + message handle = {0}; ASSERT_EQUAL(0, splinterdb_blob_create(data->db, expected, &handle)); - ASSERT_EQUAL(0, splinterdb_insert(data->db, key, ordinary, NULL)); + ASSERT_EQUAL( + 0, splinterdb_insert(data->db, key, message_from_slice(ordinary), NULL)); splinterdb_lookup_result old; splinterdb_lookup_result_init(data->db, &old); - ASSERT_EQUAL(0, splinterdb_insert_blob(data->db, key, handle, &old)); + ASSERT_EQUAL(0, splinterdb_insert(data->db, key, handle, &old)); blob_test_check_result(&old, ordinary); - ASSERT_EQUAL(0, splinterdb_insert_blob(data->db, key, handle, &old)); + ASSERT_EQUAL(0, splinterdb_insert(data->db, key, handle, &old)); blob_test_check_result(&old, expected); ASSERT_EQUAL(0, splinterdb_delete(data->db, key, &old)); - splinterdb_blob_destroy(&handle); + splinterdb_blob_release(&handle); blob_test_check_result(&old, expected); splinterdb_lookup_result absent; @@ -202,16 +232,20 @@ CTEST2(splinterdb_blob, test_wrong_database_and_unused_release) char value[8193]; memset(value, 0x29, sizeof(value)); for (unsigned i = 0; i < 32; i++) { - splinterdb_blob *handle = NULL; + message handle = {0}; ASSERT_EQUAL(0, splinterdb_blob_create( data->db, slice_create(sizeof(value), value), &handle)); - splinterdb_blob_destroy(&handle); - ASSERT_NULL(handle); - splinterdb_blob_destroy(&handle); + splinterdb_blob_release(&handle); + ASSERT_FALSE(message_is_blob(handle)); + splinterdb_blob_release(&handle); ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); } - splinterdb_blob_destroy(NULL); + splinterdb_blob_release(NULL); + message direct = message_create(sizeof(value), value); + splinterdb_blob_release(&direct); + ASSERT_TRUE(message_data(direct) == value); + ASSERT_EQUAL(sizeof(value), message_length(direct)); char other_filename[1024]; int length = snprintf(other_filename, @@ -219,29 +253,32 @@ CTEST2(splinterdb_blob, test_wrong_database_and_unused_release) "%s.blob-other", data->cfg.filename); ASSERT_TRUE(length > 0 && length < sizeof(other_filename)); - splinterdb_config other_cfg = data->cfg; - other_cfg.filename = other_filename; - splinterdb *other = NULL; + data->data_cfg.merge_tuples = blob_test_append; + data->data_cfg.merge_tuples_final = blob_test_append_final; + splinterdb_config other_cfg = data->cfg; + other_cfg.filename = other_filename; + splinterdb *other = NULL; ASSERT_EQUAL(0, splinterdb_create(&other_cfg, &other)); - splinterdb_blob *handle = NULL; + message handle = {0}; ASSERT_EQUAL(0, splinterdb_blob_create( data->db, slice_create(sizeof(value), value), &handle)); - ASSERT_EQUAL( - EINVAL, - splinterdb_insert_blob(other, slice_create(3, "key"), handle, NULL)); - splinterdb_blob_destroy(&handle); + ASSERT_EQUAL(EINVAL, + splinterdb_insert(other, slice_create(3, "key"), handle, NULL)); + ASSERT_EQUAL(EINVAL, + splinterdb_update(other, slice_create(3, "key"), handle, NULL)); + splinterdb_blob_release(&handle); ASSERT_EQUAL(0, splinterdb_close(&other, FALSE)); ASSERT_EQUAL(0, unlink(other_filename)); } typedef struct blob_test_worker { - splinterdb *db; - const splinterdb_blob *handle; - pthread_barrier_t *barrier; - uint64 begin; - uint64 count; - int rc; + splinterdb *db; + message handle; + pthread_barrier_t *barrier; + uint64 begin; + uint64 count; + int rc; } blob_test_worker; static void * @@ -250,7 +287,7 @@ blob_test_insert_worker(void *arg) blob_test_worker *worker = arg; pthread_barrier_wait(worker->barrier); for (uint64 i = worker->begin; i < worker->begin + worker->count; i++) { - worker->rc = splinterdb_insert_blob( + worker->rc = splinterdb_insert( worker->db, slice_create(sizeof(i), &i), worker->handle, NULL); if (worker->rc != 0) { break; @@ -268,7 +305,7 @@ CTEST2(splinterdb_blob, test_concurrent_inserts_share_one_handle) char *value = malloc(size); ASSERT_TRUE(value != NULL); memset(value, 0x52, size); - splinterdb_blob *handle = NULL; + message handle = {0}; ASSERT_EQUAL( 0, splinterdb_blob_create(data->db, slice_create(size, value), &handle)); pthread_t threads[4]; @@ -292,7 +329,7 @@ CTEST2(splinterdb_blob, test_concurrent_inserts_share_one_handle) ASSERT_EQUAL(0, workers[i].rc); } ASSERT_EQUAL(0, pthread_barrier_destroy(&barrier)); - splinterdb_blob_destroy(&handle); + splinterdb_blob_release(&handle); ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); blob_test_check_shared_values(data->db, 64, slice_create(size, value)); free(value); @@ -301,10 +338,13 @@ CTEST2(splinterdb_blob, test_concurrent_inserts_share_one_handle) CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) { - const uint64 size = 2 * Mega + 17; - const uint64 orphan_size = 32 * Mega; - const uint64 count = 12; - char *value = malloc(size); + const uint64 update_count = 2; + const uint64 size = 2 * Mega + 17; + const uint64 orphan_size = 32 * Mega; + const uint64 count = 12; + data->data_cfg.merge_tuples = blob_test_append; + data->data_cfg.merge_tuples_final = blob_test_append_final; + char *value = malloc(size); ASSERT_TRUE(value != NULL); memset(value, 0x26, size); @@ -319,18 +359,24 @@ CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) if (splinterdb_create(&data->cfg, &db) != 0) { _exit(1); } - splinterdb_blob *shared = NULL; + message shared = {0}; if (splinterdb_blob_create(db, slice_create(size, value), &shared) != 0) { _exit(2); } for (uint64 i = 0; i < count; i++) { - if (splinterdb_insert_blob( - db, slice_create(sizeof(i), &i), shared, NULL) + if (splinterdb_insert(db, slice_create(sizeof(i), &i), shared, NULL) != 0) { _exit(3); } } + for (uint64 i = 100; i < 100 + update_count; i++) { + if (splinterdb_update(db, slice_create(sizeof(i), &i), shared, NULL) + != 0) + { + _exit(11); + } + } if (splinterdb_durable_barrier(db, NULL) != 0) { _exit(4); } @@ -341,7 +387,7 @@ CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) _exit(5); } memset(orphan_value, 0x79, orphan_size); - splinterdb_blob *orphan = NULL; + message orphan = {0}; if (splinterdb_blob_create( db, slice_create(orphan_size, orphan_value), &orphan) != 0) @@ -401,9 +447,19 @@ CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) ASSERT_EQUAL(0, close(report[0])); ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); blob_test_check_shared_values(data->db, count, slice_create(size, value)); + splinterdb_lookup_result updated; + splinterdb_lookup_result_init(data->db, &updated); + for (uint64 i = 100; i < 100 + update_count; i++) { + ASSERT_EQUAL( + 0, + splinterdb_lookup(data->db, slice_create(sizeof(i), &i), &updated)); + blob_test_check_result(&updated, slice_create(size, value)); + } + splinterdb_lookup_result_deinit(&updated); allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); - /* Recovery creates fresh working trees/logs, so allow that overhead; - * retaining the 32 MiB unreachable object would exceed this bound. */ + /* Recovery creates fresh working trees/logs and may materialize the + * two UPDATE messages. Allow that overhead; retaining the 32 MiB + * unreachable object would exceed this bound. */ ASSERT_TRUE(blob_test_in_use_bytes(al) < before_orphan + orphan_size / 2); ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); } @@ -413,16 +469,15 @@ CTEST2(splinterdb_blob, test_durable_reuse_and_orphans_after_process_exit) CTEST2(splinterdb_blob, test_close_refuses_live_handles_and_can_retry) { ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); - splinterdb_blob *handle = NULL; - slice value = slice_create(5, "value"); + message handle = {0}; + slice value = slice_create(5, "value"); ASSERT_EQUAL(0, splinterdb_blob_create(data->db, value, &handle)); splinterdb *original = data->db; ASSERT_EQUAL(EBUSY, splinterdb_close(&data->db, FALSE)); ASSERT_TRUE(data->db == original); ASSERT_EQUAL( - 0, - splinterdb_insert_blob(data->db, slice_create(3, "key"), handle, NULL)); - splinterdb_blob_destroy(&handle); + 0, splinterdb_insert(data->db, slice_create(3, "key"), handle, NULL)); + splinterdb_blob_release(&handle); ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); ASSERT_NULL(data->db); ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); @@ -442,26 +497,26 @@ CTEST2(splinterdb_blob, test_force_close_releases_live_handles) char *value = malloc(orphan_size); ASSERT_TRUE(value != NULL); memset(value, 0x41, orphan_size); - splinterdb_blob *shared = NULL; + message shared = {0}; ASSERT_EQUAL( 0, splinterdb_blob_create(data->db, slice_create(size, value), &shared)); for (uint64 i = 0; i < 12; i++) { ASSERT_EQUAL(0, - splinterdb_insert_blob( + splinterdb_insert( data->db, slice_create(sizeof(i), &i), shared, NULL)); } ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); - allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); - uint64 before_orphan = blob_test_in_use_bytes(al); - splinterdb_blob *orphan = NULL; + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + uint64 before_orphan = blob_test_in_use_bytes(al); + message orphan = {0}; ASSERT_EQUAL(0, splinterdb_blob_create( data->db, slice_create(orphan_size, value), &orphan)); ASSERT_TRUE(blob_test_in_use_bytes(al) >= before_orphan + orphan_size); ASSERT_EQUAL(0, splinterdb_close(&data->db, TRUE)); /* Forced close invalidated both caller pointers. */ - shared = NULL; - orphan = NULL; + shared = message_create(0, NULL); + orphan = message_create(0, NULL); ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); blob_test_check_shared_values(data->db, 12, slice_create(size, value)); al = (allocator *)splinterdb_get_allocator_handle(data->db); @@ -472,24 +527,25 @@ CTEST2(splinterdb_blob, test_force_close_releases_live_handles) CTEST2(splinterdb_blob, test_reject_invalid_arguments) { ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); - splinterdb_blob *handle = NULL; + message handle = {0}; ASSERT_EQUAL( EINVAL, splinterdb_blob_create(data->db, slice_create(1, NULL), &handle)); - ASSERT_NULL(handle); + ASSERT_FALSE(message_is_blob(handle)); ASSERT_EQUAL(EINVAL, splinterdb_blob_create(NULL, slice_create(0, NULL), &handle)); - ASSERT_NULL(handle); + ASSERT_FALSE(message_is_blob(handle)); ASSERT_EQUAL(EINVAL, splinterdb_blob_create(data->db, slice_create(0, NULL), NULL)); ASSERT_EQUAL( EINVAL, - splinterdb_insert_blob(data->db, slice_create(3, "key"), NULL, NULL)); + splinterdb_insert( + data->db, slice_create(3, "key"), message_create(1, NULL), NULL)); } -CTEST2(splinterdb_blob, test_destroy_middle_head_and_tail_handles) +CTEST2(splinterdb_blob, test_release_middle_head_and_tail_handles) { ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); - splinterdb_blob *handles[5] = {0}; + message handles[5] = {0}; for (uint64 i = 0; i < ARRAY_SIZE(handles); i++) { char value = 'a' + i; ASSERT_EQUAL(0, @@ -502,10 +558,10 @@ CTEST2(splinterdb_blob, test_destroy_middle_head_and_tail_handles) uint64 key = order[i]; ASSERT_EQUAL( 0, - splinterdb_insert_blob( + splinterdb_insert( data->db, slice_create(sizeof(key), &key), handles[key], NULL)); - splinterdb_blob_destroy(&handles[key]); - ASSERT_NULL(handles[key]); + splinterdb_blob_release(&handles[key]); + ASSERT_FALSE(message_is_blob(handles[key])); } splinterdb_lookup_result result; splinterdb_lookup_result_init(data->db, &result); @@ -523,43 +579,129 @@ CTEST2(splinterdb_blob, test_failed_creation_releases_allocations) { data->cfg.disk_size = 64 * Mega; ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); - allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); - uint64 before = blob_test_in_use_bytes(al); - char byte = 'x'; - splinterdb_blob *failed = NULL; + allocator *al = (allocator *)splinterdb_get_allocator_handle(data->db); + uint64 before = blob_test_in_use_bytes(al); + char byte = 'x'; + message failed = {0}; /* The bound must be checked before reading this deliberately short input. */ ASSERT_EQUAL( ENOSPC, splinterdb_blob_create( data->db, slice_create(data->cfg.disk_size + 1, &byte), &failed)); - ASSERT_NULL(failed); + ASSERT_FALSE(message_is_blob(failed)); ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); const uint64 size = 4 * Mega; char *value = malloc(size); ASSERT_TRUE(value != NULL); memset(value, 0x23, size); - splinterdb_blob *handles[32] = {0}; - unsigned count = 0; - int rc = 0; + message handles[32] = {0}; + unsigned count = 0; + int rc = 0; for (; count < ARRAY_SIZE(handles); count++) { rc = splinterdb_blob_create( data->db, slice_create(size, value), &handles[count]); if (rc != 0) { - ASSERT_NULL(handles[count]); + ASSERT_FALSE(message_is_blob(handles[count])); break; } } ASSERT_EQUAL(ENOSPC, rc); ASSERT_TRUE(count > 0 && count < ARRAY_SIZE(handles)); for (unsigned i = 0; i < count; i++) { - splinterdb_blob_destroy(&handles[i]); + splinterdb_blob_release(&handles[i]); } ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); /* The failed creation did not poison subsequent allocator or list use. */ ASSERT_EQUAL( 0, splinterdb_blob_create(data->db, slice_create(size, value), &failed)); - splinterdb_blob_destroy(&failed); + splinterdb_blob_release(&failed); ASSERT_EQUAL(before, blob_test_in_use_bytes(al)); free(value); } + +CTEST2(splinterdb_blob, test_same_message_for_insert_and_update) +{ + data->data_cfg.merge_tuples = blob_test_append; + data->data_cfg.merge_tuples_final = blob_test_append_final; + const uint64 sizes[] = {17, 2 * Mega + 17}; + for (unsigned use_log = 0; use_log < 2; use_log++) { + data->cfg.use_log = use_log; + for (unsigned variant = 0; variant < ARRAY_SIZE(sizes); variant++) { + ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->db)); + const uint64 length = sizes[variant]; + char *bytes = malloc(length + 4); + ASSERT_TRUE(bytes != NULL); + memcpy(bytes, "seed", 4); + memset(bytes + 4, 'x', length); + message payload = {0}; + ASSERT_EQUAL(0, + splinterdb_blob_create( + data->db, slice_create(length, bytes + 4), &payload)); + // Even a different message class must not change the called operation. + payload.type = MESSAGE_TYPE_DELETE; + message alias = payload; + + slice direct_key = slice_create(6, "direct"); + slice absent_key = slice_create(6, "absent"); + slice older_key = slice_create(5, "older"); + slice active_key = slice_create(6, "active"); + ASSERT_EQUAL(0, + splinterdb_insert( + data->db, older_key, message_create(4, bytes), NULL)); + ASSERT_EQUAL(0, splinterdb_optimize(data->db)); + ASSERT_EQUAL( + 0, + splinterdb_insert(data->db, + active_key, + message_from_slice(slice_create(4, bytes)), + NULL)); + + splinterdb_lookup_result old; + splinterdb_lookup_result_init(data->db, &old); + ASSERT_EQUAL(0, splinterdb_insert(data->db, direct_key, alias, &old)); + ASSERT_FALSE(splinterdb_lookup_found(&old)); + ASSERT_EQUAL(0, splinterdb_update(data->db, absent_key, alias, &old)); + ASSERT_FALSE(splinterdb_lookup_found(&old)); + ASSERT_EQUAL(0, splinterdb_update(data->db, older_key, alias, &old)); + blob_test_check_result(&old, slice_create(4, bytes)); + ASSERT_EQUAL(0, splinterdb_update(data->db, active_key, alias, &old)); + blob_test_check_result(&old, slice_create(4, bytes)); + splinterdb_lookup_result_deinit(&old); + // Operations take message by value and must not consume or retag it. + ASSERT_EQUAL(MESSAGE_TYPE_DELETE, message_class(payload)); + ASSERT_TRUE(message_is_blob(payload)); + // A copied message can carry the single caller-owned reference. + splinterdb_blob_release(&alias); + ASSERT_FALSE(message_is_blob(alias)); + ASSERT_EQUAL(0, message_length(alias)); + payload = + message_create(0, NULL); // the borrowed original is now invalid + + for (unsigned phase = 0; phase < 3; phase++) { + splinterdb_lookup_result result; + splinterdb_lookup_result_init(data->db, &result); + const slice keys[] = { + direct_key, absent_key, older_key, active_key}; + for (unsigned i = 0; i < ARRAY_SIZE(keys); i++) { + ASSERT_EQUAL(0, splinterdb_lookup(data->db, keys[i], &result)); + blob_test_check_result(&result, + i < 2 ? slice_create(length, bytes + 4) + : slice_create(length + 4, bytes)); + } + splinterdb_lookup_result_deinit(&result); + if (phase == 0) { + ASSERT_EQUAL( + 0, + splinterdb_optimize(data->db, .full_leaf_compactions = TRUE)); + } else if (phase == 1) { + ASSERT_EQUAL(0, splinterdb_durable_barrier(data->db, NULL)); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->db)); + } + } + free(bytes); + ASSERT_EQUAL(0, splinterdb_close(&data->db, FALSE)); + } + } +} diff --git a/tests/unit/splinterdb_forked_child_test.c b/tests/unit/splinterdb_forked_child_test.c index 69a8461f4..3964e43df 100644 --- a/tests/unit/splinterdb_forked_child_test.c +++ b/tests/unit/splinterdb_forked_child_test.c @@ -127,7 +127,7 @@ CTEST2(splinterdb_forked_child, automatic_log_barriers_survive_process_exit) memset(value, i + 1, value_size); if (splinterdb_insert(db, slice_create(sizeof(i), &i), - slice_create(value_size, value), + message_create(value_size, value), NULL) != 0) { @@ -296,12 +296,14 @@ CTEST2(splinterdb_forked_child, test_one_insert_then_close_bug) size_t to_insert_len = strlen(to_insert_data); slice to_insert = slice_create(to_insert_len, to_insert_data); - rc = splinterdb_insert(spl_handle, key, to_insert, NULL); + rc = splinterdb_insert( + spl_handle, key, message_from_slice(to_insert), NULL); ASSERT_EQUAL(0, rc); key_len = snprintf(key_data, sizeof(key_data), "%d", 2); key = slice_create(key_len, key_data); - rc = splinterdb_insert(spl_handle, key, to_insert, NULL); + rc = splinterdb_insert( + spl_handle, key, message_from_slice(to_insert), NULL); ASSERT_EQUAL(0, rc); } @@ -642,7 +644,7 @@ do_many_inserts(splinterdb *kvsb, uint64 num_inserts) slice key = slice_create(key_len, key_data); slice val = slice_create(val_len, val_data); - int rc = splinterdb_insert(kvsb, key, val, NULL); + int rc = splinterdb_insert(kvsb, key, message_from_slice(val), NULL); ASSERT_EQUAL(0, rc); } if (verbose_progress) { diff --git a/tests/unit/splinterdb_optimize_test.c b/tests/unit/splinterdb_optimize_test.c index e2447ae9e..ee5a019e8 100644 --- a/tests/unit/splinterdb_optimize_test.c +++ b/tests/unit/splinterdb_optimize_test.c @@ -189,10 +189,11 @@ CTEST2(splinterdb_optimize, test_merge_can_grow_and_shrink_values) const uint8 lengths[][2] = {{100, 100}, {200, 20}, {1, 2}}; for (uint64 round = 0; round < ARRAY_SIZE(lengths); round++) { for (uint64 i = 0; i < ARRAY_SIZE(keys); i++) { - ASSERT_EQUAL( - 0, - splinterdb_update( - data->kvsb, keys[i], slice_create(1, &lengths[round][i]), NULL)); + ASSERT_EQUAL(0, + splinterdb_update(data->kvsb, + keys[i], + message_create(1, &lengths[round][i]), + NULL)); } force_flush_current_memtable(data->kvsb); ASSERT_EQUAL( @@ -431,7 +432,7 @@ insert_key(splinterdb *kvsb, uint32 key_no) int rc = splinterdb_insert(kvsb, slice_create(strlen(key), key), - slice_create(strlen(value), value), + message_create(strlen(value), value), NULL); ASSERT_EQUAL(0, rc); } diff --git a/tests/unit/splinterdb_quick_test.c b/tests/unit/splinterdb_quick_test.c index d51052ed2..9c8bd8e30 100644 --- a/tests/unit/splinterdb_quick_test.c +++ b/tests/unit/splinterdb_quick_test.c @@ -425,7 +425,8 @@ CTEST2(splinterdb_quick, test_blob_lookup_and_update_retain_failures) char payload[8192] = {1}; slice key = slice_create(11, "retain-blob"); slice value = slice_create(sizeof(payload), payload); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, key, value, NULL)); + ASSERT_EQUAL( + 0, splinterdb_insert(data->kvsb, key, message_from_slice(value), NULL)); refcount *counts = retain_test_counts(core->al); allocator_ops fault_ops = *core->al->ops; fault_ops.inc_ref = retain_fault_inc; @@ -461,7 +462,7 @@ CTEST2(splinterdb_quick, test_blob_lookup_and_update_retain_failures) rc = status.r; } else { rc = splinterdb_insert( - data->kvsb, key, slice_create(11, "replacement"), &result); + data->kvsb, key, message_create(11, "replacement"), &result); } core->al->ops = fault.original; active_retain_fault = NULL; @@ -542,7 +543,7 @@ CTEST2(splinterdb_quick, test_durable_barrier) slice key = slice_create(strlen("durable-key"), "durable-key"); slice value = slice_create(strlen("durable-value"), "durable-value"); - rc = splinterdb_insert(data->kvsb, key, value, NULL); + rc = splinterdb_insert(data->kvsb, key, message_from_slice(value), NULL); ASSERT_EQUAL(0, rc); rc = splinterdb_durable_barrier(data->kvsb, NULL); @@ -623,7 +624,9 @@ CTEST2(splinterdb_quick, test_logged_large_value_replaces_old_value) slice user_key = slice_create(strlen("oversized-log-value"), "oversized-log-value"); slice old_value = slice_create(strlen("old-value"), "old-value"); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, user_key, old_value, NULL)); + ASSERT_EQUAL(0, + splinterdb_insert( + data->kvsb, user_key, message_from_slice(old_value), NULL)); /* This used to exceed the flat descriptor's log-page limit. */ const uint64 value_len = 500 * IO_DEFAULT_EXTENT_SIZE; @@ -633,7 +636,7 @@ CTEST2(splinterdb_quick, test_logged_large_value_replaces_old_value) memset(value_data, 'v', value_len); int rc = splinterdb_insert( - data->kvsb, user_key, slice_create(value_len, value_data), NULL); + data->kvsb, user_key, message_create(value_len, value_data), NULL); ASSERT_EQUAL(0, rc); splinterdb_lookup_result result; @@ -647,7 +650,9 @@ CTEST2(splinterdb_quick, test_logged_large_value_replaces_old_value) ASSERT_EQUAL(0, splinterdb_durable_barrier(data->kvsb, NULL)); slice flat_key = slice_create(4, "flat"); slice flat_value = slice_create(32 * IO_DEFAULT_EXTENT_SIZE, value_data); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, flat_key, flat_value, NULL)); + ASSERT_EQUAL(0, + splinterdb_insert( + data->kvsb, flat_key, message_from_slice(flat_value), NULL)); ASSERT_EQUAL(0, splinterdb_close(&data->kvsb, FALSE)); data->cfg.blob_max_descriptor_size = 128; ASSERT_EQUAL(0, splinterdb_open(&data->cfg, &data->kvsb)); @@ -692,7 +697,8 @@ CTEST2(splinterdb_quick, test_basic_flow) slice to_insert = slice_create(to_insert_len, to_insert_data); // Basic insert of new key should succeed. - rc = splinterdb_insert(data->kvsb, user_key, to_insert, NULL); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(to_insert), NULL); ASSERT_EQUAL(0, rc); // Lookup of inserted key should succeed. @@ -776,7 +782,7 @@ CTEST2(splinterdb_quick, test_result_auto_registration_with_stats) ASSERT_EQUAL( 0, splinterdb_insert( - data->kvsb, slice_create(3, "key"), slice_create(5, "value"), NULL)); + data->kvsb, slice_create(3, "key"), message_create(5, "value"), NULL)); pthread_t thread; auto_registration_thread_args args = {.kvsb = data->kvsb}; @@ -796,7 +802,8 @@ CTEST2(splinterdb_quick, test_stats_reset_clears_cache_io) ASSERT_EQUAL(0, splinterdb_create(&data->cfg, &data->kvsb)); slice key = slice_create(3, "key"); slice value = slice_create(5, "value"); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, key, value, NULL)); + ASSERT_EQUAL( + 0, splinterdb_insert(data->kvsb, key, message_from_slice(value), NULL)); force_flush_current_memtable(data->kvsb); splinterdb_cache_flush(data->kvsb); cache *cc = (cache *)splinterdb_get_cache_handle(data->kvsb); @@ -885,7 +892,9 @@ CTEST2(splinterdb_quick, test_final_merge_error_propagation) for (uint64 i = 0; i < 2; i++) { slice key = i == 0 ? slice_create(6, "inline") : slice_create(4, "blob"); slice value = slice_create(i == 0 ? 7 : sizeof(payload), payload); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, value, NULL)); + ASSERT_EQUAL( + 0, + splinterdb_update(data->kvsb, key, message_from_slice(value), NULL)); splinterdb_lookup_result result; splinterdb_lookup_result_init(data->kvsb, &result); @@ -910,8 +919,10 @@ CTEST2(splinterdb_quick, test_final_merge_error_propagation) cfg->fail = TRUE; slice replacement = slice_create(11, "replacement"); - ASSERT_EQUAL(ENOMEM, - splinterdb_insert(data->kvsb, key, replacement, &result)); + ASSERT_EQUAL( + ENOMEM, + splinterdb_insert( + data->kvsb, key, message_from_slice(replacement), &result)); // Resolving the old value failed before the replacement was inserted. // Ensure the error path released its resources and locks and left the // logical value unchanged. @@ -946,10 +957,12 @@ CTEST2(splinterdb_quick, test_fetch_and_update_catches_up_after_rotation) slice skipped = slice_create(7, "skipped"); slice replacement = slice_create(11, "replacement"); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, lower, NULL)); + ASSERT_EQUAL( + 0, splinterdb_update(data->kvsb, key, message_from_slice(lower), NULL)); core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); ASSERT_TRUE(SUCCESS(memtable_force_rotation(&core->mt_ctxt, NULL))); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, skipped, NULL)); + ASSERT_EQUAL( + 0, splinterdb_update(data->kvsb, key, message_from_slice(skipped), NULL)); splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); @@ -1007,11 +1020,15 @@ CTEST2(splinterdb_quick, test_fetch_and_update_catch_up_retain_failure) slice lower = slice_create(5, "lower"); slice skipped = slice_create(7, "skipped"); slice replacement = slice_create(11, "replacement"); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, lower, NULL)); + ASSERT_EQUAL(0, + splinterdb_update( + data->kvsb, key, message_from_slice(lower), NULL)); core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); ASSERT_TRUE(SUCCESS(memtable_force_rotation(&core->mt_ctxt, NULL))); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, skipped, NULL)); + ASSERT_EQUAL(0, + splinterdb_update( + data->kvsb, key, message_from_slice(skipped), NULL)); splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); @@ -1050,7 +1067,10 @@ CTEST2(splinterdb_quick, test_fetch_and_update_catch_up_retain_failure) free(counts); ASSERT_EQUAL(0, splinterdb_lookup(data->kvsb, key, &old_result)); assert_lookup_result_matches_slice(&old_result, skipped); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, key, replacement, NULL)); + ASSERT_EQUAL( + 0, + splinterdb_insert( + data->kvsb, key, message_from_slice(replacement), NULL)); splinterdb_lookup_result_deinit(&old_result); } } @@ -1079,10 +1099,12 @@ CTEST2(splinterdb_quick, test_fetch_and_update_rejects_incorporated_boundary) slice skipped = slice_create(7, "skipped"); slice replacement = slice_create(11, "replacement"); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, lower, NULL)); + ASSERT_EQUAL( + 0, splinterdb_update(data->kvsb, key, message_from_slice(lower), NULL)); core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); ASSERT_TRUE(SUCCESS(memtable_force_rotation(&core->mt_ctxt, NULL))); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, key, skipped, NULL)); + ASSERT_EQUAL( + 0, splinterdb_update(data->kvsb, key, message_from_slice(skipped), NULL)); splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); @@ -1137,9 +1159,13 @@ CTEST2(splinterdb_quick, test_range_init_error_cleanup_preserves_branches) slice old_key = slice_create(3, "zzz"); slice new_key = slice_create(3, "aaa"); slice value = slice_create(5, "value"); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, old_key, value, NULL)); + ASSERT_EQUAL( + 0, + splinterdb_insert(data->kvsb, old_key, message_from_slice(value), NULL)); force_flush_current_memtable(data->kvsb); - ASSERT_EQUAL(0, splinterdb_update(data->kvsb, new_key, value, NULL)); + ASSERT_EQUAL( + 0, + splinterdb_update(data->kvsb, new_key, message_from_slice(value), NULL)); core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); uint64 visited = 0; @@ -1230,7 +1256,8 @@ CTEST2(splinterdb_quick, test_apis_for_large_key) slice to_insert = slice_create(to_insert_len, to_insert_data); // **** Insert of a larger key should succeed. - int rc = splinterdb_insert(data->kvsb, large_key, to_insert, NULL); + int rc = splinterdb_insert( + data->kvsb, large_key, message_from_slice(to_insert), NULL); ASSERT_EQUAL(0, rc); splinterdb_lookup_result result; @@ -1273,7 +1300,7 @@ CTEST2(splinterdb_quick, test_key_size_gt_max_inline_key_size) slice too_large_key = slice_create(too_large_key_len, too_large_key_data); int rc = splinterdb_insert( - data->kvsb, too_large_key, slice_create(sizeof("foo"), "foo"), NULL); + data->kvsb, too_large_key, message_create(sizeof("foo"), "foo"), NULL); ASSERT_EQUAL(EINVAL, rc); splinterdb_lookup_result result; @@ -1298,8 +1325,10 @@ CTEST2(splinterdb_quick, test_value_size_gt_max_value_size) slice too_large_value = slice_create(too_large_value_len, too_large_value_data); - int rc = splinterdb_insert( - data->kvsb, slice_create(sizeof("foo"), "foo"), too_large_value, NULL); + int rc = splinterdb_insert(data->kvsb, + slice_create(sizeof("foo"), "foo"), + message_from_slice(too_large_value), + NULL); ASSERT_EQUAL(0, rc); splinterdb_lookup_result result; @@ -1347,7 +1376,8 @@ CTEST2(splinterdb_quick, test_large_lookup_survives_memtable_recycle) slice large_value = slice_create(large_value_len, large_value_data); slice user_key = slice_create(sizeof("foo"), "foo"); - int rc = splinterdb_insert(data->kvsb, user_key, large_value, NULL); + int rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(large_value), NULL); ASSERT_EQUAL(0, rc); splinterdb_lookup_result result; @@ -1371,7 +1401,7 @@ CTEST2(splinterdb_quick, test_large_lookup_survives_memtable_recycle) snprintf(recycle_val, sizeof(recycle_val), "v-%06lu", i); rc = splinterdb_insert(data->kvsb, slice_create(strlen(recycle_key), recycle_key), - slice_create(strlen(recycle_val), recycle_val), + message_create(strlen(recycle_val), recycle_val), NULL); ASSERT_EQUAL(0, rc); force_flush_current_memtable(data->kvsb); @@ -1441,26 +1471,26 @@ CTEST2(splinterdb_quick, test_variable_length_values) // Insert keys with different value (lengths) int rc = splinterdb_insert( - data->kvsb, key_empty, slice_create(0, empty_string), NULL); + data->kvsb, key_empty, message_create(0, empty_string), NULL); ASSERT_EQUAL(0, rc); rc = splinterdb_insert(data->kvsb, key_short, - slice_create(sizeof(short_string), short_string), + message_create(sizeof(short_string), short_string), NULL); ASSERT_EQUAL(0, rc); - rc = splinterdb_insert( - data->kvsb, - key_long, - slice_create(sizeof(almost_max_length_string), almost_max_length_string), - NULL); + rc = splinterdb_insert(data->kvsb, + key_long, + message_create(sizeof(almost_max_length_string), + almost_max_length_string), + NULL); ASSERT_EQUAL(0, rc); rc = splinterdb_insert( data->kvsb, key_max, - slice_create(sizeof(max_length_string), max_length_string), + message_create(sizeof(max_length_string), max_length_string), NULL); ASSERT_EQUAL(0, rc); @@ -1597,7 +1627,7 @@ prefix_pack_test_curr(iterator *base, key *tuple_key, message *msg) { prefix_pack_test_iterator *itor = (prefix_pack_test_iterator *)base; *tuple_key = key_create(FALSE, sizeof(itor->keys[0]), itor->keys[itor->pos]); - *msg = message_create( + *msg = message_create_internal( MESSAGE_TYPE_INSERT, NULL, slice_create(sizeof(itor->values[0]), &itor->values[itor->pos])); @@ -1776,7 +1806,7 @@ CTEST2(splinterdb_quick, test_branch_prefix_minimum) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, prefix_minimum_test_key(keybuf, i), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL)); } core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); @@ -1928,7 +1958,7 @@ CTEST2(splinterdb_quick, test_branch_prefix_variable_iterator) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, prefix_variable_test_key(keybuf, i), - slice_create(sizeof(i), &i), + message_create(sizeof(i), &i), NULL)); } core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); @@ -2153,7 +2183,7 @@ CTEST2(splinterdb_quick, test_branch_prefix_malformed_leaf_entries) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, prefix_test_key(keybuf, i), - slice_create(sizeof(i), &i), + message_create(sizeof(i), &i), NULL)); } core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); @@ -2202,16 +2232,17 @@ CTEST2(splinterdb_quick, test_branch_prefix_roundtrip) slice k = prefix_test_key(keybuf, i * 2); ASSERT_EQUAL(0, splinterdb_insert( - data->kvsb, k, slice_create(sizeof(i), &i), NULL)); + data->kvsb, k, message_create(sizeof(i), &i), NULL)); } /* A key equal to a leaf's prefix exercises zero-length suffixes. */ memset(keybuf, 'm', 48); uint64 sentinel = UINT64_MAX; - ASSERT_EQUAL(0, - splinterdb_insert(data->kvsb, - slice_create(48, keybuf), - slice_create(sizeof(sentinel), &sentinel), - NULL)); + ASSERT_EQUAL( + 0, + splinterdb_insert(data->kvsb, + slice_create(48, keybuf), + message_create(sizeof(sentinel), &sentinel), + NULL)); core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); ASSERT_TRUE(SUCCESS(core_checkpoint(core, 0))); @@ -3172,7 +3203,7 @@ CTEST2(splinterdb_quick, test_rewriting_keys_does_not_grow_trunk) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, slice_create(20, key), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL)); } ASSERT_TRUE(SUCCESS(task_perform_until_quiescent(core->ts))); @@ -3199,8 +3230,8 @@ CTEST2(splinterdb_quick, test_close_and_reopen) const char *val = "some-value"; const size_t val_len = strlen(val); - int rc = - splinterdb_insert(data->kvsb, user_key, slice_create(val_len, val), NULL); + int rc = splinterdb_insert( + data->kvsb, user_key, message_create(val_len, val), NULL); ASSERT_EQUAL(0, rc); // Close and re-open the database @@ -3247,8 +3278,8 @@ CTEST2(splinterdb_quick, test_logged_close_and_reopen) slice user_key = slice_create(strlen("logged-key"), "logged-key"); const char *val = "logged-value"; const size_t val_len = strlen(val); - rc = - splinterdb_insert(data->kvsb, user_key, slice_create(val_len, val), NULL); + rc = splinterdb_insert( + data->kvsb, user_key, message_create(val_len, val), NULL); ASSERT_EQUAL(0, rc); rc = splinterdb_close(&data->kvsb, FALSE); @@ -3289,7 +3320,7 @@ CTEST2(splinterdb_quick, test_repeated_insert_close_reopen) for (int i = 0; i < 20; i++) { int rc = splinterdb_insert(data->kvsb, slice_create(key_len, keystring), - slice_create(val_len, val), + message_create(val_len, val), NULL); ASSERT_EQUAL(0, rc, "Insert is expected to pass, iter=%d.", i); @@ -3319,7 +3350,8 @@ CTEST2(splinterdb_quick, test_custom_data_config) slice msg_slice = slice_create(sizeof(msg), &msg); ASSERT_EQUAL(0, rc); - rc = splinterdb_insert(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); // confirm its there splinterdb_lookup_result result; @@ -3335,7 +3367,8 @@ CTEST2(splinterdb_quick, test_custom_data_config) // insert a message that adds to the refcount msg.ref_count = 5; - rc = splinterdb_update(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); ASSERT_EQUAL(0, rc); // check still found @@ -3345,7 +3378,8 @@ CTEST2(splinterdb_quick, test_custom_data_config) // insert a message that drops the refcount to zero msg.ref_count = -6; - rc = splinterdb_update(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); ASSERT_EQUAL(0, rc); // on lookup, merge will decide the tuple is deleted @@ -3355,7 +3389,8 @@ CTEST2(splinterdb_quick, test_custom_data_config) // add it back as a value msg.ref_count = 12; - rc = splinterdb_insert(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); ASSERT_EQUAL(0, rc); // delete it using a raw message @@ -3383,7 +3418,8 @@ CTEST2(splinterdb_quick, test_existence_only_memtable_lookup) ASSERT_EQUAL(0, rc); ASSERT_FALSE(splinterdb_lookup_found(&result)); - rc = splinterdb_insert(data->kvsb, user_key, value, NULL); + rc = + splinterdb_insert(data->kvsb, user_key, message_from_slice(value), NULL); ASSERT_EQUAL(0, rc); rc = splinterdb_lookup(data->kvsb, user_key, &result); @@ -3416,7 +3452,8 @@ CTEST2(splinterdb_quick, test_existence_only_trunk_lookup_skips_branches) slice user_key = slice_create(strlen("trunk-key"), "trunk-key"); slice value = slice_create(strlen("trunk-value"), "trunk-value"); - rc = splinterdb_insert(data->kvsb, user_key, value, NULL); + rc = + splinterdb_insert(data->kvsb, user_key, message_from_slice(value), NULL); ASSERT_EQUAL(0, rc); splinterdb_close(&data->kvsb, FALSE); @@ -3462,11 +3499,13 @@ CTEST2(splinterdb_quick, test_write_api_returns_old_value) splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); - int rc = splinterdb_insert(data->kvsb, user_key, value0, &old_result); + int rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value0), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_not_found(&old_result); - rc = splinterdb_insert(data->kvsb, user_key, value1, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value1), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_matches_slice(&old_result, value0); @@ -3474,7 +3513,8 @@ CTEST2(splinterdb_quick, test_write_api_returns_old_value) ASSERT_EQUAL(0, rc); assert_lookup_result_matches_slice(&old_result, value1); - rc = splinterdb_insert(data->kvsb, user_key, value0, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value0), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_not_found(&old_result); @@ -3491,11 +3531,13 @@ CTEST2(splinterdb_quick, test_write_api_old_result_existence_only) splinterdb_lookup_result_init( data->kvsb, &old_result, .flags = SPLINTERDB_LOOKUP_MIGHT_EXIST); - int rc = splinterdb_insert(data->kvsb, user_key, value0, &old_result); + int rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value0), &old_result); ASSERT_EQUAL(0, rc); ASSERT_FALSE(splinterdb_lookup_found(&old_result)); - rc = splinterdb_insert(data->kvsb, user_key, value1, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value1), &old_result); ASSERT_EQUAL(0, rc); ASSERT_TRUE(splinterdb_lookup_found(&old_result)); @@ -3507,7 +3549,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_existence_only) ASSERT_EQUAL(0, rc); ASSERT_TRUE(splinterdb_lookup_found(&old_result)); - rc = splinterdb_insert(data->kvsb, user_key, value0, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value0), &old_result); ASSERT_EQUAL(0, rc); ASSERT_FALSE(splinterdb_lookup_found(&old_result)); @@ -3528,13 +3571,15 @@ CTEST2(splinterdb_quick, test_write_api_old_result_custom_merge_semantics) splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); - rc = splinterdb_insert(data->kvsb, user_key, msg_slice, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(msg_slice), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_not_found(&old_result); msg.ref_count = 5; msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_update(data->kvsb, user_key, msg_slice, &old_result); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_matches_slice( &old_result, slice_create(sizeof(msg), &(data_handle){.ref_count = 1})); @@ -3546,7 +3591,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_custom_merge_semantics) msg.ref_count = -6; msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_update(data->kvsb, user_key, msg_slice, &old_result); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_matches_slice(&old_result, merge_accumulator_to_value(&expected)); @@ -3555,7 +3601,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_custom_merge_semantics) msg.ref_count = 12; msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_insert(data->kvsb, user_key, msg_slice, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(msg_slice), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_not_found(&old_result); @@ -3573,7 +3620,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_merges_memtable_and_trunk) data_handle msg = {.ref_count = 3}; slice msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_insert(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); ASSERT_EQUAL(0, rc); splinterdb_close(&data->kvsb, FALSE); @@ -3582,7 +3630,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_merges_memtable_and_trunk) msg.ref_count = 4; msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_update(data->kvsb, user_key, msg_slice, NULL); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), NULL); ASSERT_EQUAL(0, rc); splinterdb_lookup_result old_result; @@ -3595,7 +3644,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_merges_memtable_and_trunk) msg.ref_count = 2; msg_slice = slice_create(sizeof(msg), &msg); - rc = splinterdb_update(data->kvsb, user_key, msg_slice, &old_result); + rc = splinterdb_update( + data->kvsb, user_key, message_from_slice(msg_slice), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_matches_slice(&old_result, merge_accumulator_to_value(&expected)); @@ -3610,7 +3660,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_respects_trunk_delete_shadow) slice value0 = slice_create(strlen("shadow-old"), "shadow-old"); slice value1 = slice_create(strlen("shadow-new"), "shadow-new"); - int rc = splinterdb_insert(data->kvsb, user_key, value0, NULL); + int rc = + splinterdb_insert(data->kvsb, user_key, message_from_slice(value0), NULL); ASSERT_EQUAL(0, rc); splinterdb_close(&data->kvsb, FALSE); @@ -3623,7 +3674,8 @@ CTEST2(splinterdb_quick, test_write_api_old_result_respects_trunk_delete_shadow) splinterdb_lookup_result old_result; splinterdb_lookup_result_init(data->kvsb, &old_result); - rc = splinterdb_insert(data->kvsb, user_key, value1, &old_result); + rc = splinterdb_insert( + data->kvsb, user_key, message_from_slice(value1), &old_result); ASSERT_EQUAL(0, rc); assert_lookup_result_not_found(&old_result); @@ -3697,7 +3749,7 @@ CTEST2(splinterdb_quick, test_immutable_branch_custom_comparator) 0, splinterdb_insert(data->kvsb, nonlexical_branch_test_key(keybuf, 2 * ordinal), - slice_create(sizeof(ordinal), &ordinal), + message_create(sizeof(ordinal), &ordinal), NULL)); } core_handle *core = (core_handle *)splinterdb_get_trunk_handle(data->kvsb); @@ -4058,7 +4110,7 @@ static void insert_string_kv(splinterdb *kvsb, const char *k, const char *v) { int rc = splinterdb_insert( - kvsb, slice_create(strlen(k), k), slice_create(strlen(v), v), NULL); + kvsb, slice_create(strlen(k), k), message_create(strlen(v), v), NULL); ASSERT_EQUAL(0, rc); } @@ -4128,8 +4180,8 @@ static void * fetch_and_update_thread(void *arg) { fetch_and_update_thread_args *args = arg; - args->rc = - splinterdb_insert(args->kvsb, args->key, args->value, args->old_result); + args->rc = splinterdb_insert( + args->kvsb, args->key, message_from_slice(args->value), args->old_result); return NULL; } @@ -4155,7 +4207,8 @@ stats_reset_worker(void *arg) __atomic_fetch_add(params->started, 1, __ATOMIC_RELEASE); while (!__atomic_load_n(params->stop, __ATOMIC_ACQUIRE)) { - params->rc = splinterdb_insert(params->kvsb, tuple_key, value, NULL); + params->rc = splinterdb_insert( + params->kvsb, tuple_key, message_from_slice(value), NULL); if (params->rc != 0) { break; } @@ -4191,7 +4244,7 @@ insert_some_keys(const int num_inserts, splinterdb *kvsb) rc = splinterdb_insert(kvsb, slice_create(sizeof(key), key), - slice_create(sizeof(val), val), + message_create(sizeof(val), val), NULL); ASSERT_EQUAL(0, rc); } @@ -4232,7 +4285,7 @@ insert_keys(splinterdb *kvsb, const int minkey, int numkeys, const int incr) rc = splinterdb_insert(kvsb, slice_create(sizeof(key), key), - slice_create(sizeof(val), val), + message_create(sizeof(val), val), NULL); ASSERT_EQUAL(0, rc); } @@ -4532,10 +4585,11 @@ CTEST2(splinterdb_quick, test_filtered_range_merges_only_matching_keys) } force_flush_current_memtable(data->kvsb); for (uint64 i = 0; i < ARRAY_SIZE(keys); i++) { - ASSERT_EQUAL( - 0, - splinterdb_update( - data->kvsb, slice_create(2, keys[i]), slice_create(1, "2"), NULL)); + ASSERT_EQUAL(0, + splinterdb_update(data->kvsb, + slice_create(2, keys[i]), + message_create(1, "2"), + NULL)); } ASSERT_EQUAL(0, splinterdb_delete(data->kvsb, slice_create(2, "1a"), NULL)); insert_string_kv(data->kvsb, "3a", "3"); @@ -4543,7 +4597,7 @@ CTEST2(splinterdb_quick, test_filtered_range_merges_only_matching_keys) ASSERT_EQUAL( 0, splinterdb_update( - data->kvsb, slice_create(2, "4b"), slice_create(1, "2"), NULL)); + data->kvsb, slice_create(2, "4b"), message_create(1, "2"), NULL)); hash_range_reject_unmatched_merge = TRUE; for (int forwards = 0; forwards < 2; forwards++) { splinterdb_iterator *it = NULL; @@ -4675,7 +4729,7 @@ CTEST2(splinterdb_quick, test_filtered_range_crosses_empty_trunk_leaves) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, slice_create(sizeof(encoded), &encoded), - slice_create(sizeof(value), value), + message_create(sizeof(value), value), NULL)); } force_flush_current_memtable(data->kvsb); @@ -4961,7 +5015,7 @@ CTEST2(splinterdb_quick, test_filtered_range_persisted_mixed_seeds) 0, splinterdb_insert(data->kvsb, range_mixed_seed_key(key, id, tenant), - slice_create(sizeof(initial), &initial), + message_create(sizeof(initial), &initial), NULL)); } if (id % 1024 == 1023) { @@ -4974,7 +5028,7 @@ CTEST2(splinterdb_quick, test_filtered_range_persisted_mixed_seeds) 0, splinterdb_update(data->kvsb, range_mixed_seed_key(key, id, tenant), - slice_create(sizeof(second), &second), + message_create(sizeof(second), &second), NULL)); } } @@ -4996,7 +5050,7 @@ CTEST2(splinterdb_quick, test_filtered_range_persisted_mixed_seeds) 0, splinterdb_update(data->kvsb, range_mixed_seed_key(key, id, tenant), - slice_create(sizeof(third), &third), + message_create(sizeof(third), &third), NULL)); } } @@ -5010,7 +5064,7 @@ CTEST2(splinterdb_quick, test_filtered_range_persisted_mixed_seeds) ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, range_mixed_seed_key(key, total, 'a'), - slice_create(sizeof(live), &live), + message_create(sizeof(live), &live), NULL)); range_mixed_seed_reject_b_merge = TRUE; for (bool32 bounded = FALSE; bounded <= TRUE; bounded++) { @@ -5060,7 +5114,7 @@ CTEST2(splinterdb_quick, test_filtered_range_predicate_only_and_bounds) ASSERT_EQUAL( 0, splinterdb_insert( - data->kvsb, slice_create(1, &key), slice_create(1, "v"), NULL)); + data->kvsb, slice_create(1, &key), message_create(1, "v"), NULL)); if (key == '4') { force_flush_current_memtable(data->kvsb); } @@ -5190,12 +5244,12 @@ CTEST2(splinterdb_quick, test_filtered_range_propagates_matching_merge_errors) ASSERT_EQUAL( 0, splinterdb_update( - data->kvsb, slice_create(2, "0b"), slice_create(1, "v"), NULL)); + data->kvsb, slice_create(2, "0b"), message_create(1, "v"), NULL)); insert_string_kv(data->kvsb, "1a", "v"); ASSERT_EQUAL( 0, splinterdb_update( - data->kvsb, slice_create(2, "2a"), slice_create(1, "v"), NULL)); + data->kvsb, slice_create(2, "2a"), message_create(1, "v"), NULL)); splinterdb_iterator *it = NULL; ASSERT_EQUAL(0, splinterdb_iterator_init(data->kvsb, @@ -5415,7 +5469,7 @@ CTEST2(splinterdb_quick, test_blob_tree_crash_recovery) splinterdb *db; if (splinterdb_create(&data->cfg, &db) != 0) _exit(10); - if (splinterdb_insert(db, prefix, small, NULL) != 0) + if (splinterdb_insert(db, prefix, message_from_slice(small), NULL) != 0) _exit(11); if (splinterdb_durable_barrier(db, NULL) != 0) _exit(12); @@ -5423,11 +5477,12 @@ CTEST2(splinterdb_quick, test_blob_tree_crash_recovery) if (bytes == NULL) _exit(13); memset(bytes, 'L', length); - if (splinterdb_insert(db, large, slice_create(length, bytes), NULL) != 0) + if (splinterdb_insert(db, large, message_create(length, bytes), NULL) + != 0) _exit(14); if (splinterdb_durable_barrier(db, NULL) != 0) _exit(15); - if (splinterdb_insert(db, suffix, small, NULL) != 0) + if (splinterdb_insert(db, suffix, message_from_slice(small), NULL) != 0) _exit(16); if (splinterdb_durable_barrier(db, NULL) != 0) _exit(17); diff --git a/tests/unit/splinterdb_stress_test.c b/tests/unit/splinterdb_stress_test.c index f45a4ad29..badf23fc5 100644 --- a/tests/unit/splinterdb_stress_test.c +++ b/tests/unit/splinterdb_stress_test.c @@ -122,7 +122,7 @@ CTEST2(splinterdb_stress, test_naive_range_delete) random_bytes(&rand_state, value_buffer, TEST_VALUE_SIZE); int rc = splinterdb_insert(data->kvsb, slice_create(TEST_KEY_SIZE, key_buffer), - slice_create(TEST_VALUE_SIZE, value_buffer), + message_create(TEST_VALUE_SIZE, value_buffer), NULL); ASSERT_EQUAL(0, rc); } @@ -156,7 +156,8 @@ CTEST2(splinterdb_stress, test_iterator_over_many_kvs) snprintf(key_str, sizeof(key_str), "key-%08x", i); slice key = slice_create(sizeof(key_str), key_str); slice val = slice_create(sizeof(value_str), value_str); - ASSERT_EQUAL(0, splinterdb_insert(data->kvsb, key, val, NULL)); + ASSERT_EQUAL( + 0, splinterdb_insert(data->kvsb, key, message_from_slice(val), NULL)); } // create an iterator at end of keys @@ -249,7 +250,8 @@ CTEST2_SKIP(splinterdb_stress, test_issue_458_mini_destroy_unused_debug_assert) slice key = slice_create(strlen(key_data), key_data); slice val = slice_create(strlen(val_data), val_data); - int rc = splinterdb_insert(data->kvsb, key, val, NULL); + int rc = + splinterdb_insert(data->kvsb, key, message_from_slice(val), NULL); ASSERT_EQUAL(0, rc); } uint64 elapsed_ns = platform_timestamp_elapsed(start_time); @@ -301,7 +303,7 @@ exec_worker_thread(void *w) rc = splinterdb_insert(kvsb, slice_create(TEST_KEY_SIZE, key_buf), - slice_create(TEST_VALUE_SIZE, value_buf), + message_create(TEST_VALUE_SIZE, value_buf), NULL); ASSERT_EQUAL(0, rc); diff --git a/tests/unit/trunk_split_test.c b/tests/unit/trunk_split_test.c index ccf8a734c..d517d6864 100644 --- a/tests/unit/trunk_split_test.c +++ b/tests/unit/trunk_split_test.c @@ -19,9 +19,10 @@ split_test_curr(iterator *base, key *k, message *msg) { split_test_iterator *itor = (split_test_iterator *)base; *k = key_create(FALSE, 1, &itor->keys[itor->pos]); - *msg = message_create(MESSAGE_TYPE_INSERT, - NULL, - slice_create(sizeof(itor->value), itor->value)); + *msg = + message_create_internal(MESSAGE_TYPE_INSERT, + NULL, + slice_create(sizeof(itor->value), itor->value)); } static bool32