diff --git a/src/lib/testsuite/test_zarray_cow.c b/src/lib/testsuite/test_zarray_cow.c index d6beef6..72c51f5 100644 --- a/src/lib/testsuite/test_zarray_cow.c +++ b/src/lib/testsuite/test_zarray_cow.c @@ -8,10 +8,16 @@ /* xzarray_type is defined in zarray.c */ extern typeinfo_t xzarray_type; +/* Helper: create an xzarray instance */ +static meta_obj_t* make_test_xzarray(void) +{ + xzarray_params_t params = { .delta = 4, .datakiller = NULL }; + return (meta_obj_t*)makeInstance(&xzarray_type, ¶ms); +} + static void test_freeze_thaw_slots_exist(void) { - /* After P0-0b, intf_varray_t has vfreeze and vthaw slots. - * They are NULL until P0-2 wires the implementations. */ + /* After P0-2, vfreeze and vthaw must be wired (non-NULL). */ const intf_varray_t* iface = (const intf_varray_t*)xzarray_type.interfaces.varray; @@ -21,15 +27,110 @@ static void test_freeze_thaw_slots_exist(void) (void)iface->vfreeze; (void)iface->vthaw; - /* Slots must be NULL until P0-2 wires the real implementations */ - TEST_ASSERT_NULL(iface->vfreeze); - TEST_ASSERT_NULL(iface->vthaw); + /* Slots must be non-NULL after P0-2 wires the real implementations */ + TEST_ASSERT_NOT_NULL(iface->vfreeze); + TEST_ASSERT_NOT_NULL(iface->vthaw); printf("test_freeze_thaw_slots_exist: PASS\n"); } +static void test_freeze_creates_shadow(void) +{ + meta_obj_t* arr = make_test_xzarray(); + TEST_ASSERT_NOT_NULL(arr); + + /* Store 3 items */ + char* items[3] = { "alpha", "beta", "gamma" }; + for (int i = 0; i < 3; i++) { + int idx = vstore(arr, items[i]); + TEST_ASSERT_TRUE(idx >= 0); + } + + /* Freeze should succeed */ + TEST_ASSERT_EQUAL_INT(0, vfreeze(arr)); + + /* After freeze, vget must still return stored values (COW mode active) */ + void* out = NULL; + TEST_ASSERT_EQUAL_INT(0, vget(arr, 0, &out)); + TEST_ASSERT_NOT_NULL(out); + + /* Thaw to clean up */ + vthaw(arr, NULL, NULL); + destroy(arr); +} + +static void test_freeze_thaw_preserves_all_entries(void) +{ + meta_obj_t* arr = make_test_xzarray(); + TEST_ASSERT_NOT_NULL(arr); + + int N = 5; + char* items[5] = { "a", "b", "c", "d", "e" }; + int idxs[5]; + for (int i = 0; i < N; i++) { + idxs[i] = vstore(arr, items[i]); + TEST_ASSERT_TRUE(idxs[i] >= 0); + } + + TEST_ASSERT_EQUAL_INT(0, vfreeze(arr)); + TEST_ASSERT_EQUAL_INT(0, vthaw(arr, NULL, NULL)); + + /* All 5 entries must be reachable after thaw */ + for (int i = 0; i < N; i++) { + void* out = NULL; + TEST_ASSERT_EQUAL_INT(0, vget(arr, idxs[i], &out)); + TEST_ASSERT_EQUAL_PTR(items[i], out); + } + + destroy(arr); +} + +static void test_mutation_during_cow_visible_after_thaw(void) +{ + meta_obj_t* arr = make_test_xzarray(); + TEST_ASSERT_NOT_NULL(arr); + + char* original = "original"; + char* updated = "updated"; + + int idx = vstore(arr, original); + TEST_ASSERT_TRUE(idx >= 0); + + TEST_ASSERT_EQUAL_INT(0, vfreeze(arr)); + + /* Overwrite index during COW — write to new active array */ + int put_idxs[1] = { idx }; + void* put_data[1] = { updated }; + vput_vec(arr, 1, put_idxs, put_data); + + TEST_ASSERT_EQUAL_INT(0, vthaw(arr, NULL, NULL)); + + /* After thaw, the updated value must win over the shadow value */ + void* out = NULL; + TEST_ASSERT_EQUAL_INT(0, vget(arr, idx, &out)); + TEST_ASSERT_EQUAL_PTR(updated, out); + + destroy(arr); +} + +static void test_double_freeze_fails(void) +{ + meta_obj_t* arr = make_test_xzarray(); + TEST_ASSERT_NOT_NULL(arr); + + TEST_ASSERT_EQUAL_INT(0, vfreeze(arr)); + /* Second freeze must fail (already in COW mode) */ + TEST_ASSERT_TRUE(vfreeze(arr) != 0); + vthaw(arr, NULL, NULL); + destroy(arr); +} + void test_zarray_cow(void) { printf("\n>> zarray COW interface slot test\n\n"); RUN_TEST(test_freeze_thaw_slots_exist); + RUN_TEST(test_freeze_creates_shadow); + RUN_TEST(test_freeze_thaw_preserves_all_entries); + RUN_TEST(test_mutation_during_cow_visible_after_thaw); + RUN_TEST(test_double_freeze_fails); } diff --git a/src/lib/zarray.c b/src/lib/zarray.c index e632416..abd050a 100644 --- a/src/lib/zarray.c +++ b/src/lib/zarray.c @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include @@ -351,13 +353,16 @@ void* zarray_get(zarray_t* array, int idx) } typedef struct { - mylock_t lock; -} xzarray_private_t; + _Atomic _Bool inuse; + void* data; +} xzarray_item_t; typedef struct { - unsigned int inuse : 1; - void* data; -} xzarray_item_t; + mylock_t lock; + xzarray_item_t* shadow; /* COW snapshot (may be NULL if array was empty at freeze) */ + size_t shadow_caps; /* capacity of shadow array */ + bool in_cow; /* true while between freeze and thaw */ +} xzarray_private_t; typedef struct { int count; @@ -392,6 +397,10 @@ static int xzarray_init(meta_obj_t* obj, va_list ap) a->pos = 0; a->killer = params->datakiller; + priv->shadow = NULL; + priv->shadow_caps = 0; + priv->in_cow = false; + mylock_init(&(priv->lock)); return 1; @@ -403,8 +412,16 @@ static void xzarray_destroy(meta_obj_t* obj) xzarray_t* a = RTTI_OBJ_TO_DATA(xzarray_t, obj); int i; - for (i = 0; i < a->caps; i++) { - if (a->data[i].inuse) { + /* Free shadow if still present (e.g. destroy called mid-COW) */ + if (priv->in_cow) { + free(priv->shadow); + priv->shadow = NULL; + priv->shadow_caps = 0; + priv->in_cow = false; + } + + for (i = 0; i < (int)a->caps; i++) { + if (atomic_load_explicit(&a->data[i].inuse, memory_order_relaxed)) { if (a->killer != NULL) { a->killer(a->data[i].data); } @@ -437,14 +454,14 @@ static int xzarray_ensure_items(xzarray_t* array, int mincap) array->data = p; for (i = array->caps; i < nc; i++) { - array->data[i].inuse = 0; + atomic_store_explicit(&array->data[i].inuse, false, memory_order_relaxed); array->data[i].data = NULL; } /* this will make we can get next free * location quickly */ - if (array->data[array->pos].inuse) { + if (atomic_load_explicit(&array->data[array->pos].inuse, memory_order_relaxed)) { array->pos = array->caps; } @@ -467,9 +484,9 @@ static int xzarray_store(meta_obj_t* obj, void* data) idx = a->pos; for (i = 0; i < a->caps; i++) { - if (a->data[idx].inuse == 0) { - a->data[idx].inuse = 1; - a->data[idx].data = data; + if (!atomic_load_explicit(&a->data[idx].inuse, memory_order_relaxed)) { + a->data[idx].data = data; + atomic_store_explicit(&a->data[idx].inuse, true, memory_order_relaxed); a->count++; rc = idx; @@ -503,7 +520,15 @@ static int xzarray_get_vec(meta_obj_t* obj, int count, int* idxs, void** data) for (i = 0; i < count; i++) { int idx = idxs[i]; - if ((idx >= a->caps) || (a->data[idx].inuse == 0)) { + /* During COW, check active first, then shadow */ + bool in_active = (idx < (int)a->caps) && + atomic_load_explicit(&a->data[idx].inuse, memory_order_relaxed); + bool in_shadow = !in_active && priv->in_cow && + priv->shadow != NULL && + (idx < (int)priv->shadow_caps) && + atomic_load_explicit(&priv->shadow[idx].inuse, memory_order_relaxed); + + if (!in_active && !in_shadow) { rc = -ENOENT; break; } @@ -513,7 +538,13 @@ static int xzarray_get_vec(meta_obj_t* obj, int count, int* idxs, void** data) for (i = 0; i < count; i++) { int idx = idxs[i]; - data[i] = a->data[idx].data; + /* Active takes precedence over shadow */ + if ((idx < (int)a->caps) && + atomic_load_explicit(&a->data[idx].inuse, memory_order_relaxed)) { + data[i] = a->data[idx].data; + } else { + data[i] = priv->shadow[idx].data; + } } } return rc; @@ -557,14 +588,16 @@ static int xzarray_put_vec(meta_obj_t* obj, int count, int* idxs, void** data) for (i = 0; i < count; i++) { int idx = idxs[i]; - if (a->data[idx].inuse == 1) { + if (atomic_load_explicit(&a->data[idx].inuse, memory_order_relaxed)) { if (a->killer != NULL) { a->killer(a->data[idx].data); } + } else { + a->count++; } - a->data[idx].data = data[i]; - a->data[idx].inuse = 1; + a->data[idx].data = data[i]; + atomic_store_explicit(&a->data[idx].inuse, true, memory_order_relaxed); } } @@ -584,15 +617,18 @@ static int xzarray_clear(meta_obj_t* obj, int idx) xzarray_private_t* priv = (xzarray_private_t*)RTTI_OBJ_TO_PRIV(obj); int rc = -EINVAL; - if (idx < a->caps) { + assert(!priv->in_cow && "xzarray_clear called during COW — deferred cleanup required"); + + if (idx < (int)a->caps) { rc = 0; - if (a->data[idx].inuse) { + if (atomic_load_explicit(&a->data[idx].inuse, memory_order_relaxed)) { if (a->killer != NULL) { a->killer(a->data[idx].data); } + a->count--; } - a->data[idx].inuse = 0; + atomic_store_explicit(&a->data[idx].inuse, false, memory_order_relaxed); a->data[idx].data = NULL; a->pos = idx; /* we can find the available idx next time */ } @@ -614,8 +650,8 @@ static int xzarray_iter_next(iterator_t* iter, void** data) xzarray_t* a = RTTI_OBJ_TO_DATA(xzarray_t, iter->obj); int i; - for (i = (int)(long)iter->data; i < a->caps; i++) { - if (a->data[i].inuse) { + for (i = (int)(long)iter->data; i < (int)a->caps; i++) { + if (atomic_load_explicit(&a->data[i].inuse, memory_order_relaxed)) { *data = a->data[i].data; iter->data = (void*)(long)(i + 1); @@ -656,6 +692,70 @@ static void xzarray_iter_destroy(iterator_t* iter) return; } +/* P0-2: COW freeze — O(1) pointer swap, pre-grow active array */ +static int xzarray_freeze(meta_obj_t* obj) +{ + xzarray_private_t* priv = (xzarray_private_t*)RTTI_OBJ_TO_PRIV(obj); + xzarray_t* a = RTTI_OBJ_TO_DATA(xzarray_t, obj); + + if (priv->in_cow) + return -EALREADY; /* already frozen */ + + /* Pre-grow active to 1.5x + 16 to avoid realloc during COW (A6-026) */ + size_t new_caps = a->caps + (a->caps / 2) + 16; + xzarray_item_t* active = calloc(new_caps, sizeof(xzarray_item_t)); + if (!active) + return -ENOMEM; + + priv->shadow = a->data; /* may be NULL if array was empty */ + priv->shadow_caps = a->caps; + priv->in_cow = true; + a->data = active; + a->caps = new_caps; + a->count = 0; + /* Start pos past shadow to prevent index collisions with shadow entries */ + a->pos = (int)priv->shadow_caps; + return 0; +} + +/* P0-2: COW thaw — merge shadow into active, free shadow */ +static int xzarray_thaw(meta_obj_t* obj, retain_fn_t should_retain, void* arg) +{ + xzarray_private_t* priv = (xzarray_private_t*)RTTI_OBJ_TO_PRIV(obj); + xzarray_t* a = RTTI_OBJ_TO_DATA(xzarray_t, obj); + + if (!priv->in_cow) + return -EINVAL; /* not in COW mode */ + + /* Merge: for each shadow entry not already written to active, copy it */ + for (size_t i = 0; i < priv->shadow_caps; i++) { + if (!atomic_load_explicit(&priv->shadow[i].inuse, memory_order_relaxed)) + continue; + + void* item = priv->shadow[i].data; + + /* GC: skip entries that should not be retained */ + if (should_retain != NULL && !should_retain(item, arg)) + continue; + + /* Only copy to active if active slot not already updated during COW */ + if (i < a->caps && + !atomic_load_explicit(&a->data[i].inuse, memory_order_relaxed)) { + a->data[i].data = item; + atomic_store_explicit(&a->data[i].inuse, true, memory_order_relaxed); + a->count++; + } + /* If active[i].inuse == true: mutation during COW wins — keep it */ + } + + free(priv->shadow); + priv->shadow = NULL; + priv->shadow_caps = 0; + priv->in_cow = false; + a->pos = 0; + return 0; +} + typeinfo_t xzarray_type = { .name = "xzarray", .privateSize = sizeof(xzarray_private_t), @@ -669,8 +769,8 @@ typeinfo_t xzarray_type = { .vput = (vput_fn_t)xzarray_put, .vput_vec = (vput_vec_fn_t)xzarray_put_vec, .vclear = (vclear_fn_t)xzarray_clear, - .vfreeze = NULL, /* wired in P0-2 */ - .vthaw = NULL /* wired in P0-2 */ }, + .vfreeze = (vfreeze_fn_t)xzarray_freeze, + .vthaw = (vthaw_fn_t)xzarray_thaw }, .iter = &(intf_iterator_t) { .make = (iter_make_fn_t)xzarray_make_iter, .next = (iter_next_fn_t)xzarray_iter_next,