diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ee9705a1..78d46d272 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -273,6 +273,47 @@ jobs: sleep 10 ./scripts/benchmark.sh --duration=0 ./scripts/setup.sh stop + rados: + name: Rados + needs: dependencies + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + show-progress: false + + - name: Install dependencies + run: | + sudo apt update + sudo apt --yes --no-install-recommends install meson ninja-build pkgconf libglib2.0-dev libbson-dev libfabric-dev libgdbm-dev liblmdb-dev libsqlite3-dev libleveldb-dev libmongoc-dev libmariadb-dev librocksdb-dev libfuse3-dev libopen-trace-format-dev librados-dev + + - name: Configure and build + env: + CC: clang + run: | + ./scripts/ci/build.sh debug-no-sanitize system + + - name: Create configuration + run: | + . scripts/environment.sh + julea-config --user --object-servers="$(hostname)" --kv-servers="$(hostname)" --db-servers="$(hostname)" --object-backend=rados --object-path=/var/snap/microceph/current/conf/ceph.conf:julea --kv-backend=lmdb --kv-path=/tmp/julea/kv/lmdb --db-backend=sqlite --db-path=/tmp/julea/db/sqlite + + - name: Microceph setup + run: sudo ./scripts/rados-util.sh bootstrap + + - name: Tests + run: | + . scripts/environment.sh + ./scripts/setup.sh start + ./scripts/test.sh + # sudo apt --yes --no-install-recommends install valgrind + # valgrind --leak-check=full --track-origins=yes julea-test + sleep 10 + ./scripts/test.sh + ./scripts/setup.sh stop doxygen: name: Doxygen runs-on: ubuntu-24.04 diff --git a/backend/object/gio.c b/backend/object/gio.c index f31ad9c13..56b430a80 100644 --- a/backend/object/gio.c +++ b/backend/object/gio.c @@ -43,14 +43,14 @@ typedef struct JBackendIterator JBackendIterator; struct JBackendObject { - gchar* path; + gchar* name; GFileIOStream* stream; }; typedef struct JBackendObject JBackendObject; static gboolean -backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_create(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { JBackendData* bd = backend_data; JBackendObject* bo; @@ -59,7 +59,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, GFileIOStream* stream; gchar* full_path; - full_path = g_build_filename(bd->path, namespace, path, NULL); + full_path = g_build_filename(bd->path, namespace, name, NULL); file = g_file_new_for_path(full_path); j_trace_file_begin(full_path, J_TRACE_FILE_CREATE); @@ -73,7 +73,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, j_trace_file_end(full_path, J_TRACE_FILE_CREATE, 0, 0); bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->name = full_path; bo->stream = stream; *backend_object = bo; @@ -84,7 +84,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, } static gboolean -backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_open(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { JBackendData* bd = backend_data; JBackendObject* bo; @@ -92,7 +92,7 @@ backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, g GFileIOStream* stream; gchar* full_path; - full_path = g_build_filename(bd->path, namespace, path, NULL); + full_path = g_build_filename(bd->path, namespace, name, NULL); file = g_file_new_for_path(full_path); j_trace_file_begin(full_path, J_TRACE_FILE_OPEN); @@ -100,7 +100,7 @@ backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, g j_trace_file_end(full_path, J_TRACE_FILE_OPEN, 0, 0); bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->name = full_path; bo->stream = stream; *backend_object = bo; @@ -119,16 +119,16 @@ backend_delete(gpointer backend_data, gpointer backend_object) (void)backend_data; - file = g_file_new_for_path(bo->path); + file = g_file_new_for_path(bo->name); - j_trace_file_begin(bo->path, J_TRACE_FILE_DELETE); + j_trace_file_begin(bo->name, J_TRACE_FILE_DELETE); ret = g_file_delete(file, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_DELETE, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_DELETE, 0, 0); g_object_unref(file); g_object_unref(bo->stream); - g_free(bo->path); + g_free(bo->name); g_free(bo); return ret; @@ -142,12 +142,12 @@ backend_close(gpointer backend_data, gpointer backend_object) (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_CLOSE); + j_trace_file_begin(bo->name, J_TRACE_FILE_CLOSE); ret = g_io_stream_close(G_IO_STREAM(bo->stream), NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_CLOSE, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_CLOSE, 0, 0); g_object_unref(bo->stream); - g_free(bo->path); + g_free(bo->name); g_free(bo); return ret; @@ -166,11 +166,11 @@ backend_status(gpointer backend_data, gpointer backend_object, gint64* modificat GFile* file; GFileInfo* file_info; - file = g_file_new_for_path(bo->path); + file = g_file_new_for_path(bo->name); - j_trace_file_begin(bo->path, J_TRACE_FILE_STATUS); + j_trace_file_begin(bo->name, J_TRACE_FILE_STATUS); file_info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, G_FILE_QUERY_INFO_NONE, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_STATUS, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_STATUS, 0, 0); ret = (file_info != NULL); @@ -217,9 +217,9 @@ backend_sync(gpointer backend_data, gpointer backend_object) output = g_io_stream_get_output_stream(G_IO_STREAM(bo->stream)); - j_trace_file_begin(bo->path, J_TRACE_FILE_SYNC); + j_trace_file_begin(bo->name, J_TRACE_FILE_SYNC); ret = g_output_stream_flush(output, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_SYNC, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_SYNC, 0, 0); return ret; } @@ -237,13 +237,13 @@ backend_read(gpointer backend_data, gpointer backend_object, gpointer buffer, gu input = g_io_stream_get_input_stream(G_IO_STREAM(bo->stream)); - j_trace_file_begin(bo->path, J_TRACE_FILE_SEEK); + j_trace_file_begin(bo->name, J_TRACE_FILE_SEEK); g_seekable_seek(G_SEEKABLE(bo->stream), offset, G_SEEK_SET, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_SEEK, 0, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_SEEK, 0, offset); - j_trace_file_begin(bo->path, J_TRACE_FILE_READ); + j_trace_file_begin(bo->name, J_TRACE_FILE_READ); ret = g_input_stream_read_all(input, buffer, length, &nbytes, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_READ, nbytes, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_READ, nbytes, offset); if (bytes_read != NULL) { @@ -266,13 +266,13 @@ backend_write(gpointer backend_data, gpointer backend_object, gconstpointer buff output = g_io_stream_get_output_stream(G_IO_STREAM(bo->stream)); - j_trace_file_begin(bo->path, J_TRACE_FILE_SEEK); + j_trace_file_begin(bo->name, J_TRACE_FILE_SEEK); g_seekable_seek(G_SEEKABLE(bo->stream), offset, G_SEEK_SET, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_SEEK, 0, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_SEEK, 0, offset); - j_trace_file_begin(bo->path, J_TRACE_FILE_WRITE); + j_trace_file_begin(bo->name, J_TRACE_FILE_WRITE); ret = g_output_stream_write_all(output, buffer, length, &nbytes, NULL, NULL); - j_trace_file_end(bo->path, J_TRACE_FILE_WRITE, nbytes, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_WRITE, nbytes, offset); if (bytes_written != NULL) { diff --git a/backend/object/null.c b/backend/object/null.c index aad40136f..3825c8711 100644 --- a/backend/object/null.c +++ b/backend/object/null.c @@ -24,13 +24,13 @@ #include static gboolean -backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_create(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { gchar* full_path; (void)backend_data; - full_path = g_build_filename(namespace, path, NULL); + full_path = g_build_filename(namespace, name, NULL); j_trace_file_begin(full_path, J_TRACE_FILE_CREATE); j_trace_file_end(full_path, J_TRACE_FILE_CREATE, 0, 0); @@ -41,13 +41,13 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, } static gboolean -backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_open(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { gchar* full_path; (void)backend_data; - full_path = g_build_filename(namespace, path, NULL); + full_path = g_build_filename(namespace, name, NULL); j_trace_file_begin(full_path, J_TRACE_FILE_OPEN); j_trace_file_end(full_path, J_TRACE_FILE_OPEN, 0, 0); diff --git a/backend/object/posix.c b/backend/object/posix.c index 3ad262c89..7caaaecb1 100644 --- a/backend/object/posix.c +++ b/backend/object/posix.c @@ -47,7 +47,7 @@ typedef struct JBackendIterator JBackendIterator; struct JBackendObject { - gchar* path; + gchar* name; gint fd; guint ref_count; }; @@ -82,13 +82,13 @@ backend_file_unref(gpointer data) if (g_atomic_int_dec_and_test(&(bo->ref_count))) { - g_hash_table_remove(jd_backend_file_cache, bo->path); + g_hash_table_remove(jd_backend_file_cache, bo->name); - j_trace_file_begin(bo->path, J_TRACE_FILE_CLOSE); + j_trace_file_begin(bo->name, J_TRACE_FILE_CLOSE); close(bo->fd); - j_trace_file_end(bo->path, J_TRACE_FILE_CLOSE, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_CLOSE, 0, 0); - g_free(bo->path); + g_free(bo->name); g_free(bo); } @@ -126,7 +126,7 @@ backend_file_get(GHashTable* files, gchar const* key) if ((bo = g_hash_table_lookup(jd_backend_file_cache, key)) != NULL) { g_atomic_int_inc(&(bo->ref_count)); - g_hash_table_insert(files, bo->path, bo); + g_hash_table_insert(files, bo->name, bo); G_UNLOCK(jd_backend_file_cache); } @@ -141,15 +141,15 @@ backend_file_add(GHashTable* files, JBackendObject* object) { if (object != NULL) { - g_hash_table_insert(jd_backend_file_cache, object->path, object); - g_hash_table_insert(files, object->path, object); + g_hash_table_insert(jd_backend_file_cache, object->name, object); + g_hash_table_insert(files, object->name, object); } G_UNLOCK(jd_backend_file_cache); } static gboolean -backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_create(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { JBackendData* bd = backend_data; GHashTable* files = jd_backend_files_get_thread(); @@ -159,7 +159,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, gchar* full_path; gint fd; - full_path = g_build_filename(bd->path, namespace, path, NULL); + full_path = g_build_filename(bd->path, namespace, name, NULL); if ((bo = backend_file_get(files, full_path)) != NULL) { @@ -187,7 +187,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, } bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->name = full_path; bo->fd = fd; bo->ref_count = 1; @@ -200,7 +200,7 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, } static gboolean -backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_open(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { JBackendData* bd = backend_data; GHashTable* files = jd_backend_files_get_thread(); @@ -209,7 +209,7 @@ backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, g gchar* full_path; gint fd; - full_path = g_build_filename(bd->path, namespace, path, NULL); + full_path = g_build_filename(bd->path, namespace, name, NULL); if ((bo = backend_file_get(files, full_path)) != NULL) { @@ -232,7 +232,7 @@ backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, g } bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->name = full_path; bo->fd = fd; bo->ref_count = 1; @@ -253,11 +253,11 @@ backend_delete(gpointer backend_data, gpointer backend_object) (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_DELETE); - ret = (g_unlink(bo->path) == 0); - j_trace_file_end(bo->path, J_TRACE_FILE_DELETE, 0, 0); + j_trace_file_begin(bo->name, J_TRACE_FILE_DELETE); + ret = (g_unlink(bo->name) == 0); + j_trace_file_end(bo->name, J_TRACE_FILE_DELETE, 0, 0); - g_hash_table_remove(files, bo->path); + g_hash_table_remove(files, bo->name); return ret; } @@ -271,7 +271,7 @@ backend_close(gpointer backend_data, gpointer backend_object) (void)backend_data; - ret = g_hash_table_remove(files, bo->path); + ret = g_hash_table_remove(files, bo->name); return ret; } @@ -287,9 +287,9 @@ backend_status(gpointer backend_data, gpointer backend_object, gint64* modificat if (modification_time != NULL || size != NULL) { - j_trace_file_begin(bo->path, J_TRACE_FILE_STATUS); + j_trace_file_begin(bo->name, J_TRACE_FILE_STATUS); ret = (fstat(bo->fd, &buf) == 0); - j_trace_file_end(bo->path, J_TRACE_FILE_STATUS, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_STATUS, 0, 0); if (ret && modification_time != NULL) { @@ -317,9 +317,9 @@ backend_sync(gpointer backend_data, gpointer backend_object) (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_SYNC); + j_trace_file_begin(bo->name, J_TRACE_FILE_SYNC); ret = (fsync(bo->fd) == 0); - j_trace_file_end(bo->path, J_TRACE_FILE_SYNC, 0, 0); + j_trace_file_end(bo->name, J_TRACE_FILE_SYNC, 0, 0); return ret; } @@ -333,7 +333,7 @@ backend_read(gpointer backend_data, gpointer backend_object, gpointer buffer, gu (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_READ); + j_trace_file_begin(bo->name, J_TRACE_FILE_READ); while (nbytes_total < length) { @@ -356,7 +356,7 @@ backend_read(gpointer backend_data, gpointer backend_object, gpointer buffer, gu nbytes_total += nbytes; } - j_trace_file_end(bo->path, J_TRACE_FILE_READ, nbytes_total, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_READ, nbytes_total, offset); if (bytes_read != NULL) { @@ -375,7 +375,7 @@ backend_write(gpointer backend_data, gpointer backend_object, gconstpointer buff (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_WRITE); + j_trace_file_begin(bo->name, J_TRACE_FILE_WRITE); while (nbytes_total < length) { @@ -394,7 +394,7 @@ backend_write(gpointer backend_data, gpointer backend_object, gconstpointer buff nbytes_total += nbytes; } - j_trace_file_end(bo->path, J_TRACE_FILE_WRITE, nbytes_total, offset); + j_trace_file_end(bo->name, J_TRACE_FILE_WRITE, nbytes_total, offset); if (bytes_written != NULL) { diff --git a/backend/object/rados.c b/backend/object/rados.c index fc14b11f3..467358842 100644 --- a/backend/object/rados.c +++ b/backend/object/rados.c @@ -36,29 +36,48 @@ struct JBackendData typedef struct JBackendData JBackendData; +struct JBackendIterator +{ + rados_list_ctx_t rados_list; + gchar* prefix; +}; + +typedef struct JBackendIterator JBackendIterator; + struct JBackendObject { - gchar* path; + // This is a concatenation of the namespace and name of an object separated by a whitespace. + // Ideally, these would get stored as two separate strings and used with rados's `set_namespace` function. + // However, this can cause problems when multiple threads are attempting to set the namespace at the same time. + // This could be managed by having an independent `rados_ioctx_t` for each thread but for now this implementation will do. + gchar* namespace_and_name; }; typedef struct JBackendObject JBackendObject; static gboolean -backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_create(const gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { JBackendData* bd = backend_data; JBackendObject* bo; - gchar* full_path = g_strconcat(namespace, path, NULL); gint ret = 0; + gchar* namespace_and_name; - j_trace_file_begin(full_path, J_TRACE_FILE_CREATE); - ret = rados_write_full(bd->backend_io, full_path, "", 0); - j_trace_file_end(full_path, J_TRACE_FILE_CREATE, 0, 0); + g_return_val_if_fail(backend_data != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(name != NULL, FALSE); + g_return_val_if_fail(namespace != NULL, FALSE); + + namespace_and_name = g_strconcat(namespace, " ", name, NULL); + + j_trace_file_begin(namespace_and_name, J_TRACE_FILE_CREATE); + ret = rados_write_full(bd->backend_io, namespace_and_name, "", 0); + j_trace_file_end(namespace_and_name, J_TRACE_FILE_CREATE, 0, 0); g_return_val_if_fail(ret == 0, FALSE); bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->namespace_and_name = namespace_and_name; *backend_object = bo; @@ -66,21 +85,39 @@ backend_create(gpointer backend_data, gchar const* namespace, gchar const* path, } static gboolean -backend_open(gpointer backend_data, gchar const* namespace, gchar const* path, gpointer* backend_object) +backend_open(gpointer backend_data, gchar const* namespace, gchar const* name, gpointer* backend_object) { + JBackendData* bd = backend_data; JBackendObject* bo; - gchar* full_path = g_strconcat(namespace, path, NULL); gint ret = 0; + gchar* namespace_and_name; - (void)backend_data; + g_return_val_if_fail(bd != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(namespace != NULL, FALSE); - j_trace_file_begin(full_path, J_TRACE_FILE_OPEN); - j_trace_file_end(full_path, J_TRACE_FILE_OPEN, 0, 0); + namespace_and_name = g_strconcat(namespace, " ", name, NULL); - g_return_val_if_fail(ret == 0, FALSE); + j_trace_file_begin(namespace_and_name, J_TRACE_FILE_OPEN); + ret = rados_stat(bd->backend_io, namespace_and_name, NULL, NULL); + j_trace_file_end(namespace_and_name, J_TRACE_FILE_OPEN, 0, 0); + + if (ret != 0) + { + // ENOENT (Error NO ENTry) + // It is an expected "error" here because we are using the stat command to check if the object exists. + // This is done to ensure that the object was created before opening it, just as it has to be done with other APIs. + // As such, we only print other error kinds. + if (ret != -ENOENT) + { + g_critical("rados_stat() failed: %s", strerror(-ret)); + } + // In either case, we return false as we obviously weren't able to `open` to object :) + return FALSE; + } bo = g_new(JBackendObject, 1); - bo->path = full_path; + bo->namespace_and_name = namespace_and_name; *backend_object = bo; @@ -94,11 +131,16 @@ backend_delete(gpointer backend_data, gpointer backend_object) JBackendObject* bo = backend_object; gint ret = 0; - j_trace_file_begin(bo->path, J_TRACE_FILE_DELETE); - ret = rados_remove(bd->backend_io, bo->path); - j_trace_file_end(bo->path, J_TRACE_FILE_DELETE, 0, 0); + g_return_val_if_fail(bd != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(bo != NULL, FALSE); + g_return_val_if_fail(bo->namespace_and_name != NULL, FALSE); - g_free(bo->path); + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_DELETE); + ret = rados_remove(bd->backend_io, bo->namespace_and_name); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_DELETE, 0, 0); + + g_clear_pointer(&bo->namespace_and_name, g_free); g_free(bo); return (ret == 0 ? TRUE : FALSE); @@ -111,10 +153,10 @@ backend_close(gpointer backend_data, gpointer backend_object) (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_CLOSE); - j_trace_file_end(bo->path, J_TRACE_FILE_CLOSE, 0, 0); + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_CLOSE); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_CLOSE, 0, 0); - g_free(bo->path); + g_clear_pointer(&bo->namespace_and_name, g_free); g_free(bo); return TRUE; @@ -129,11 +171,16 @@ backend_status(gpointer backend_data, gpointer backend_object, gint64* modificat gint64 modification_time_ = 0; guint64 size_ = 0; + g_return_val_if_fail(bd != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(bo != NULL, FALSE); + g_return_val_if_fail(bo->namespace_and_name != NULL, FALSE); + if (modification_time != NULL || size != NULL) { - j_trace_file_begin(bo->path, J_TRACE_FILE_STATUS); - ret = (rados_stat(bd->backend_io, bo->path, &size_, &modification_time_) == 0); - j_trace_file_end(bo->path, J_TRACE_FILE_STATUS, 0, 0); + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_STATUS); + ret = (rados_stat(bd->backend_io, bo->namespace_and_name, &size_, &modification_time_) == 0); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_STATUS, 0, 0); if (ret && modification_time != NULL) { @@ -157,8 +204,8 @@ backend_sync(gpointer backend_data, gpointer backend_object) (void)backend_data; - j_trace_file_begin(bo->path, J_TRACE_FILE_SYNC); - j_trace_file_end(bo->path, J_TRACE_FILE_SYNC, 0, 0); + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_SYNC); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_SYNC, 0, 0); return TRUE; } @@ -170,9 +217,14 @@ backend_read(gpointer backend_data, gpointer backend_object, gpointer buffer, gu JBackendObject* bo = backend_object; gint ret = 0; - j_trace_file_begin(bo->path, J_TRACE_FILE_READ); - ret = rados_read(bd->backend_io, bo->path, buffer, length, offset); - j_trace_file_end(bo->path, J_TRACE_FILE_READ, length, offset); + g_return_val_if_fail(bd != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(bo != NULL, FALSE); + g_return_val_if_fail(bo->namespace_and_name != NULL, FALSE); + + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_READ); + ret = rados_read(bd->backend_io, bo->namespace_and_name, buffer, length, offset); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_READ, length, offset); g_return_val_if_fail(ret >= 0, FALSE); @@ -191,9 +243,25 @@ backend_write(gpointer backend_data, gpointer backend_object, gconstpointer buff JBackendObject* bo = backend_object; gint ret = 0; - j_trace_file_begin(bo->path, J_TRACE_FILE_WRITE); - ret = rados_write(bd->backend_io, bo->path, buffer, length, offset); - j_trace_file_end(bo->path, J_TRACE_FILE_WRITE, length, offset); + g_return_val_if_fail(bd != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + g_return_val_if_fail(bo != NULL, FALSE); + g_return_val_if_fail(bo->namespace_and_name != NULL, FALSE); + + if (length == 0) + { + if (bytes_written != NULL) + { + *bytes_written = 0; + } + return TRUE; + } + + g_return_val_if_fail(buffer != NULL, FALSE); + + j_trace_file_begin(bo->namespace_and_name, J_TRACE_FILE_WRITE); + ret = rados_write(bd->backend_io, bo->namespace_and_name, buffer, length, offset); + j_trace_file_end(bo->namespace_and_name, J_TRACE_FILE_WRITE, length, offset); g_return_val_if_fail(ret == 0, FALSE); @@ -205,58 +273,143 @@ backend_write(gpointer backend_data, gpointer backend_object, gconstpointer buff return TRUE; } -/// \todo implement backend_get_all -/// \todo implement backend_get_by_prefix -/// \todo implement backend_iterate +static gboolean +backend_get_all(gpointer backend_data, gchar const* namespace, gpointer* backend_iterator) +{ + JBackendData* bd = backend_data; + JBackendIterator* iterator = g_new(JBackendIterator, 1); + int err; + + g_return_val_if_fail(backend_data != NULL, FALSE); + g_return_val_if_fail(namespace != NULL, FALSE); + g_return_val_if_fail(bd->backend_io != NULL, FALSE); + + iterator->prefix = g_strdup(namespace); + + err = rados_nobjects_list_open(bd->backend_io, &iterator->rados_list); + g_return_val_if_fail(err == 0, FALSE); + + *backend_iterator = iterator; + return TRUE; +} + +static gboolean +backend_get_by_prefix(gpointer backend_data, gchar const* namespace, gchar const* prefix, gpointer* backend_iterator) +{ + const JBackendData* bd = backend_data; + JBackendIterator* iterator = g_new(JBackendIterator, 1); + int err; + + g_return_val_if_fail(backend_data != NULL, FALSE); + g_return_val_if_fail(namespace != NULL, FALSE); + g_return_val_if_fail(prefix != NULL, FALSE); + + iterator->prefix = g_strconcat(namespace, " ", prefix, NULL); + + err = rados_nobjects_list_open(bd->backend_io, &iterator->rados_list); + g_return_val_if_fail(err == 0, FALSE); + + *backend_iterator = iterator; + return TRUE; +} + +static gboolean +backend_iterate(gpointer backend_data, gpointer backend_iterator, gchar const** name) +{ + JBackendIterator* iterator = backend_iterator; + gchar const* current_name; + gchar** split_namespace_and_name; + + // Avoid unused parameter warning :) + (void)backend_data; + + // Loop until we have an object that matches the prefix + do + { + const int err = rados_nobjects_list_next(iterator->rados_list, ¤t_name, NULL, NULL); + + if (err == -ENOENT) + { + goto end; // No more objects + } + if (err != 0) // Something went wrong + { + g_error("rados_nobjects_list_next() failed: %s", strerror(-err)); + } + } while (!g_str_has_prefix(current_name, iterator->prefix)); + + // Separate the current_name and only return the name part. + split_namespace_and_name = g_strsplit(current_name, " ", 2); + *name = g_strdup(split_namespace_and_name[1]); + g_strfreev(split_namespace_and_name); + return TRUE; +end: + rados_nobjects_list_close(iterator->rados_list); + g_free(iterator->prefix); + g_free(iterator); + return FALSE; +} static gboolean backend_init(gchar const* path, gpointer* backend_data) { - JBackendData* bd; + JBackendData* bd = NULL; g_auto(GStrv) split = NULL; + int err = 0; g_return_val_if_fail(path != NULL, FALSE); /* Path syntax: [config-path]:[pool] e.g.: /etc/ceph/ceph.conf:data */ split = g_strsplit(path, ":", 0); + /* Expect at least two parts */ + if (split == NULL || split[0] == NULL || split[1] == NULL) + { + g_critical("Invalid RADOS backend path '%s'. Expected format: :", path); + return FALSE; + } - bd = g_new(JBackendData, 1); + bd = g_new0(JBackendData, 1); bd->backend_config = g_strdup(split[0]); bd->backend_pool = g_strdup(split[1]); - bd->backend_connection = NULL; - bd->backend_io = NULL; g_return_val_if_fail(bd->backend_pool != NULL, FALSE); g_return_val_if_fail(bd->backend_config != NULL, FALSE); /* Create cluster handle */ - if (rados_create(&(bd->backend_connection), NULL) != 0) + err = rados_create(&(bd->backend_connection), NULL); + if (err != 0) { - g_critical("Can not create a RADOS cluster handle."); + g_critical("Can not create a RADOS cluster handle. Error message:\n%s\n", strerror(-err)); + return FALSE; } /* Read config file */ - if (rados_conf_read_file(bd->backend_connection, bd->backend_config) != 0) + err = rados_conf_read_file(bd->backend_connection, bd->backend_config); + if (err != 0) { - g_critical("Can not read RADOS config file %s.", bd->backend_config); + g_critical("Can not read RADOS config file `%s`. Error message:\n%s\n", bd->backend_config, strerror(-err)); + return FALSE; } /* Connect to cluster */ - if (rados_connect(bd->backend_connection) != 0) + err = rados_connect(bd->backend_connection); + if (err != 0) { - g_critical("Can not connect to RADOS. Cluster online, config up-to-date and keyring correct linked?"); + g_critical("Can not connect to RADOS. Cluster online, config up-to-date and keyring correct linked? Error message:\n%s\n", strerror(-err)); + return FALSE; } /* Initialize IO and select pool */ - if (rados_ioctx_create(bd->backend_connection, bd->backend_pool, &(bd->backend_io)) != 0) + err = rados_ioctx_create(bd->backend_connection, bd->backend_pool, &(bd->backend_io)); + if (err != 0) { + g_critical("Can not connect to RADOS pool %s. Error message:\n%s\n", bd->backend_pool, strerror(-err)); rados_shutdown(bd->backend_connection); - g_critical("Can not connect to RADOS pool %s.", bd->backend_pool); + return FALSE; } *backend_data = bd; - return TRUE; } @@ -289,7 +442,11 @@ static JBackend rados_backend = { .backend_status = backend_status, .backend_sync = backend_sync, .backend_read = backend_read, - .backend_write = backend_write } + .backend_write = backend_write, + .backend_get_all = backend_get_all, + .backend_get_by_prefix = backend_get_by_prefix, + .backend_iterate = backend_iterate, + } }; G_MODULE_EXPORT diff --git a/lib/core/jbackend.c b/lib/core/jbackend.c index 62c47ea0f..070669273 100644 --- a/lib/core/jbackend.c +++ b/lib/core/jbackend.c @@ -306,7 +306,7 @@ j_backend_object_fini(JBackend* backend) } gboolean -j_backend_object_create(JBackend* backend, gchar const* namespace, gchar const* path, gpointer* data) +j_backend_object_create(JBackend* backend, gchar const* namespace, gchar const* name, gpointer* data) { J_TRACE_FUNCTION(NULL); @@ -315,12 +315,12 @@ j_backend_object_create(JBackend* backend, gchar const* namespace, gchar const* g_return_val_if_fail(backend != NULL, FALSE); g_return_val_if_fail(backend->type == J_BACKEND_TYPE_OBJECT, FALSE); g_return_val_if_fail(namespace != NULL, FALSE); - g_return_val_if_fail(path != NULL, FALSE); + g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(data != NULL, FALSE); { - J_TRACE("backend_create", "%s, %s, %p", namespace, path, (gpointer)data); - ret = backend->object.backend_create(backend->data, namespace, path, data); + J_TRACE("backend_create", "%s, %s, %p", namespace, name, (gpointer)data); + ret = backend->object.backend_create(backend->data, namespace, name, data); } return ret; diff --git a/lib/object/jdistributed-object.c b/lib/object/jdistributed-object.c index bee78b101..54c71f6e3 100644 --- a/lib/object/jdistributed-object.c +++ b/lib/object/jdistributed-object.c @@ -624,7 +624,7 @@ j_distributed_object_create_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_create(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_close(object_backend, object_handle) && ret; @@ -721,7 +721,7 @@ j_distributed_object_delete_exec(JList* operations, JSemantics* semantics) else { gboolean lret = FALSE; - gpointer object_handle; + gpointer object_handle = NULL; if (j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && j_backend_object_delete(object_backend, object_handle)) @@ -782,7 +782,7 @@ j_distributed_object_read_exec(JList* operations, JSemantics* semantics) g_autoptr(JListIterator) it = NULL; g_autofree JMessage** messages = NULL; JDistributedObject* object = NULL; - gpointer object_handle; + gpointer object_handle = NULL; gsize name_len = 0; gsize namespace_len = 0; guint32 server_count = 0; @@ -822,6 +822,10 @@ j_distributed_object_read_exec(JList* operations, JSemantics* semantics) else { ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; + if (object_handle == NULL) + { + return FALSE; + } } /* @@ -972,7 +976,7 @@ j_distributed_object_write_exec(JList* operations, JSemantics* semantics) g_autoptr(JListIterator) it = NULL; g_autofree JMessage** messages = NULL; JDistributedObject* object = NULL; - gpointer object_handle; + gpointer object_handle = NULL; gsize name_len = 0; gsize namespace_len = 0; guint32 server_count = 0; @@ -1012,6 +1016,10 @@ j_distributed_object_write_exec(JList* operations, JSemantics* semantics) else { ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; + if (object_handle == NULL) + { + return FALSE; + } } /* @@ -1228,7 +1236,7 @@ j_distributed_object_status_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_status(object_backend, object_handle, modification_time, size) && ret; @@ -1328,7 +1336,7 @@ j_distributed_object_sync_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_sync(object_backend, object_handle) && ret; diff --git a/lib/object/jobject-iterator.c b/lib/object/jobject-iterator.c index 12a5a537e..165bb90a3 100644 --- a/lib/object/jobject-iterator.c +++ b/lib/object/jobject-iterator.c @@ -41,15 +41,20 @@ struct JObjectIterator JBackend* object_backend; /** - * The iterate cursor. + * The current name. **/ - gpointer cursor; + gchar const* name; + // Client backend-specific: + // TODO: Union of client and server specific objects? /** - * The current name. + * The iterate cursor. **/ - gchar const* name; + gpointer cursor; + GArray* cached_names; + guint32 cached_names_cur; + // Server backend-specific: JMessage** replies; guint32 replies_n; guint32 replies_cur; @@ -104,6 +109,8 @@ j_object_iterator_new(gchar const* namespace, gchar const* prefix) { J_TRACE_FUNCTION(NULL); + const gchar* curr_name = NULL; + JObjectIterator* iterator; JConfiguration* configuration = j_configuration(); @@ -115,28 +122,40 @@ j_object_iterator_new(gchar const* namespace, gchar const* prefix) iterator = g_new(JObjectIterator, 1); iterator->object_backend = j_object_get_backend(); - iterator->cursor = NULL; iterator->name = NULL; + + iterator->cursor = NULL; + iterator->cached_names = g_array_new(FALSE, FALSE, sizeof(gchar*)); + iterator->cached_names_cur = 0; + iterator->replies_n = j_configuration_get_server_count(configuration, J_BACKEND_TYPE_OBJECT); iterator->replies = g_new0(JMessage*, iterator->replies_n); iterator->replies_cur = 0; if (iterator->object_backend == NULL) { + // Loop over all servers... for (guint32 i = 0; i < iterator->replies_n; i++) { + // ... and each then loops over all their objects and replies with a list of names. iterator->replies[i] = fetch_reply(i, namespace, prefix); } } else { + // Create the iterator, either with or without a prefix. if (prefix == NULL) { - /// \todo j_backend_object_get_all(iterator->object_backend, namespace, &(iterator->cursor)); + j_backend_object_get_all(iterator->object_backend, namespace, &(iterator->cursor)); } else { - /// \todo j_backend_object_get_by_prefix(iterator->object_backend, namespace, prefix, &(iterator->cursor)); + j_backend_object_get_by_prefix(iterator->object_backend, namespace, prefix, &(iterator->cursor)); + } + // Iterate over the objects until we reach the end and keep all resulting names. + while (j_backend_object_iterate(iterator->object_backend, iterator->cursor, &curr_name)) + { + iterator->cached_names = g_array_append_val(iterator->cached_names, curr_name); } } @@ -148,6 +167,8 @@ j_object_iterator_new_for_index(guint32 index, gchar const* namespace, gchar con { J_TRACE_FUNCTION(NULL); + const gchar* curr_name = NULL; + JObjectIterator* iterator; JConfiguration* configuration = j_configuration(); @@ -160,8 +181,12 @@ j_object_iterator_new_for_index(guint32 index, gchar const* namespace, gchar con iterator = g_new(JObjectIterator, 1); iterator->object_backend = j_object_get_backend(); - iterator->cursor = NULL; iterator->name = NULL; + + iterator->cursor = NULL; + iterator->cached_names = g_array_new(FALSE, FALSE, sizeof(gchar*)); + iterator->cached_names_cur = 0; + iterator->replies_n = 1; iterator->replies = g_new0(JMessage*, 1); iterator->replies_cur = 0; @@ -172,13 +197,19 @@ j_object_iterator_new_for_index(guint32 index, gchar const* namespace, gchar con } else { + // Create the iterator, either with or without a prefix. if (prefix == NULL) { - /// \todo j_backend_object_get_all(iterator->object_backend, namespace, &(iterator->cursor)); + j_backend_object_get_all(iterator->object_backend, namespace, &(iterator->cursor)); } else { - /// \todo j_backend_object_get_by_prefix(iterator->object_backend, namespace, prefix, &(iterator->cursor)); + j_backend_object_get_by_prefix(iterator->object_backend, namespace, prefix, &(iterator->cursor)); + } + // Iterate over the objects until we reach the end and keep all resulting names. + while (j_backend_object_iterate(iterator->object_backend, iterator->cursor, &curr_name)) + { + iterator->cached_names = g_array_append_val(iterator->cached_names, curr_name); } } @@ -201,7 +232,16 @@ j_object_iterator_free(JObjectIterator* iterator) } g_free(iterator->replies); - + // Free duplicated names stored in cached_names + if (iterator->cached_names) + { + for (guint i = 0; i < iterator->cached_names->len; i++) + { + gchar* s = g_array_index(iterator->cached_names, gchar*, i); + g_free(s); + } + g_array_free(iterator->cached_names, TRUE); + } g_free(iterator); } @@ -231,7 +271,12 @@ j_object_iterator_next(JObjectIterator* iterator) } else { - /// \todo ret = j_backend_object_iterate(iterator->object_backend, iterator->cursor, &(iterator->key), &(iterator->value), &(iterator->len)); + if (iterator->cached_names_cur < iterator->cached_names->len) + { + iterator->name = g_array_index(iterator->cached_names, gchar*, iterator->cached_names_cur); + iterator->cached_names_cur++; + ret = TRUE; + } } return ret; diff --git a/lib/object/jobject.c b/lib/object/jobject.c index 2a8212c90..439a0ddd0 100644 --- a/lib/object/jobject.c +++ b/lib/object/jobject.c @@ -289,7 +289,7 @@ j_object_create_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_create(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_close(object_backend, object_handle) && ret; @@ -376,7 +376,7 @@ j_object_delete_exec(JList* operations, JSemantics* semantics) else { gboolean lret = FALSE; - gpointer object_handle; + gpointer object_handle = NULL; if (j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && j_backend_object_delete(object_backend, object_handle)) @@ -431,11 +431,11 @@ j_object_read_exec(JList* operations, JSemantics* semantics) /// \todo check return value for messages gboolean ret = TRUE; - JBackend* object_backend; - JListIterator* it; + JBackend* object_backend = NULL; + JListIterator* it = NULL; g_autoptr(JMessage) message = NULL; - JObject* object; - gpointer object_handle; + JObject* object = NULL; + gpointer object_handle = NULL; /// \todo //JLock* lock = NULL; @@ -452,7 +452,6 @@ j_object_read_exec(JList* operations, JSemantics* semantics) g_assert(object != NULL); } - it = j_list_iterator_new(operations); object_backend = j_object_get_backend(); if (object_backend == NULL) @@ -471,6 +470,10 @@ j_object_read_exec(JList* operations, JSemantics* semantics) else { ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; + if (object_handle == NULL) + { + return FALSE; + } } /* @@ -480,6 +483,7 @@ j_object_read_exec(JList* operations, JSemantics* semantics) } */ + it = j_list_iterator_new(operations); while (j_list_iterator_next(it)) { JObjectOperation* operation = j_list_iterator_get(it); @@ -569,7 +573,6 @@ j_object_read_exec(JList* operations, JSemantics* semantics) } j_list_iterator_free(it); - j_connection_pool_push(J_BACKEND_TYPE_OBJECT, object->index, object_connection); } else @@ -586,7 +589,6 @@ j_object_read_exec(JList* operations, JSemantics* semantics) j_lock_free(lock); } */ - return ret; } @@ -598,11 +600,11 @@ j_object_write_exec(JList* operations, JSemantics* semantics) /// \todo check return value for messages gboolean ret = TRUE; - JBackend* object_backend; - JListIterator* it; + JBackend* object_backend = NULL; + JListIterator* it = NULL; g_autoptr(JMessage) message = NULL; - JObject* object; - gpointer object_handle; + JObject* object = NULL; + gpointer object_handle = NULL; /// \todo //JLock* lock = NULL; @@ -619,7 +621,6 @@ j_object_write_exec(JList* operations, JSemantics* semantics) g_assert(object != NULL); } - it = j_list_iterator_new(operations); object_backend = j_object_get_backend(); if (object_backend == NULL) @@ -638,6 +639,11 @@ j_object_write_exec(JList* operations, JSemantics* semantics) else { ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; + // If the object was not opened, we return. + if (object_handle == NULL) + { + return FALSE; + } } /* @@ -647,6 +653,8 @@ j_object_write_exec(JList* operations, JSemantics* semantics) } */ + it = j_list_iterator_new(operations); + while (j_list_iterator_next(it)) { JObjectOperation* operation = j_list_iterator_get(it); @@ -807,7 +815,7 @@ j_object_status_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_status(object_backend, object_handle, modification_time, size) && ret; @@ -916,7 +924,7 @@ j_object_sync_exec(JList* operations, JSemantics* semantics) } else { - gpointer object_handle; + gpointer object_handle = NULL; ret = j_backend_object_open(object_backend, object->namespace, object->name, &object_handle) && ret; ret = j_backend_object_sync(object_backend, object_handle) && ret; diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh index a4433c2f3..95849ab45 100755 --- a/scripts/ci/build.sh +++ b/scripts/ci/build.sh @@ -47,6 +47,12 @@ case "${MODE}" in meson setup -Db_sanitize=address,undefined ${CLANG_LUNDEF} ${GDBM_PREFIX} bld ninja -C bld ;; + debug-no-sanitize) + CLANG_LUNDEF='-Db_lundef=false' + # shellcheck disable=SC2086 + meson setup ${CLANG_LUNDEF} ${GDBM_PREFIX} bld + ninja -C bld + ;; coverage) # shellcheck disable=SC2086 meson setup -Db_coverage=true -Db_sanitize=address,undefined ${GDBM_PREFIX} bld diff --git a/scripts/rados-util.sh b/scripts/rados-util.sh new file mode 100755 index 000000000..11cee3be3 --- /dev/null +++ b/scripts/rados-util.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +# JULEA - Flexible storage framework +# Copyright (C) 2026 Jan Frase +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +set -e + +SELF_PATH="$(readlink --canonicalize-existing -- "$0")" +SELF_BASE="${SELF_PATH##*/}" + +usage () +{ + echo "Usage: sudo ${SELF_BASE} bootstrap|teardown" + echo "" + echo "This file offers some convenience commands for bootstrapping a non-persistent ceph cluster and tearing it down again." + echo "It is used in the GitHub CI but can also be used for local testing." + echo "However, do note that it assumes that snap is already installed on your system." + + exit 1 +} + +bootstrap () +{ + snap install microceph + microceph cluster bootstrap + microceph disk add loop,4G,3 + microceph enable rgw + microceph.ceph osd pool create julea + microceph status + cat /var/snap/microceph/current/conf/ceph.keyring >> /var/snap/microceph/current/conf/ceph.conf +} + +teardown () +{ + snap remove --purge microceph +} + + +test -n "$1" || usage + +MODE="$1" + +case "${MODE}" in + bootstrap) + bootstrap + ;; + teardown) + teardown + ;; + *) + usage + ;; +esac diff --git a/test/object/object.c b/test/object/object.c index 6df3ce05b..e26ee158f 100644 --- a/test/object/object.c +++ b/test/object/object.c @@ -169,6 +169,12 @@ test_object_read_write(void) object_noexist = j_object_new("test", "test-object-rw-noexist"); g_assert_true(object_noexist != NULL); + // As the object is never properly created, the following write is expected to fail. + // The creation could look like this: + // j_object_create(object_noexist, batch); + // ret = j_batch_execute(batch); + // g_assert_true(ret); + j_object_write(object_noexist, buffer, 1, 0, &nbytes, batch); ret = j_batch_execute(batch); g_assert_false(ret);