Demonstrate a use of the new nbd_pread_structured_verify API by
writing a strict validation that a server's structured replies comply
with the specification (well, 99% strict, as I did not check that the
server does not return an error at the same offset twice).
I was able to test that qemu-nbd is compliant. An example run:
$ qemu-img create -f qcow2 file 32m
$ for i in `seq 32`; do
qemu-io -f qcow2 -d unmap -c "w -zu $((i-1))m 512k" file; done
$ qemu-nbd -f qcow2 -p 10888 file
$ ./examples/strict-structured-reads nbd://localhost:10888
totals:
data chunks: 1768
data bytes: 1559232512
hole chunks: 1284
hole bytes: 537919488
all chunks: 3052
reads: 1000
bytes read: 2097152000
compliant: 1000
But since qemu-nbd always returns chunks in order, there may still be
lurking bugs in my code to handle out-of-order replies. Maybe someday
nbdkit will make it easy to write a server that returns out-of-order
chunks.
---
.gitignore | 1 +
examples/Makefile.am | 14 ++
examples/strict-structured-reads.c | 270 +++++++++++++++++++++++++++++
3 files changed, 285 insertions(+)
create mode 100644 examples/strict-structured-reads.c
diff --git a/.gitignore b/.gitignore
index d4828fa..edbf941 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,7 @@ Makefile.in
/examples/threaded-reads-and-writes
/examples/simple-fetch-first-sector
/examples/simple-reads-and-writes
+/examples/strict-structured-reads
/generator/generator-cache.v1
/generator/stamp-generator
/html/*.?.html
diff --git a/examples/Makefile.am b/examples/Makefile.am
index f0d03f1..7560855 100644
--- a/examples/Makefile.am
+++ b/examples/Makefile.am
@@ -24,6 +24,7 @@ noinst_PROGRAMS = \
simple-fetch-first-sector \
simple-reads-and-writes \
threaded-reads-and-writes \
+ strict-structured-reads \
$(NULL)
simple_fetch_first_sector_SOURCES = \
@@ -52,6 +53,19 @@ simple_reads_and_writes_LDADD = \
$(top_builddir)/lib/libnbd.la \
$(NULL)
+strict_structured_reads_SOURCES = \
+ strict-structured-reads.c \
+ $(NULL)
+strict_structured_reads_CPPFLAGS = \
+ -I$(top_srcdir)/include \
+ $(NULL)
+strict_structured_reads_CFLAGS = \
+ $(WARNINGS_CFLAGS) \
+ $(NULL)
+strict_structured_reads_LDADD = \
+ $(top_builddir)/lib/libnbd.la \
+ $(NULL)
+
threaded_reads_and_writes_SOURCES = \
threaded-reads-and-writes.c \
$(NULL)
diff --git a/examples/strict-structured-reads.c b/examples/strict-structured-reads.c
new file mode 100644
index 0000000..e75f5a3
--- /dev/null
+++ b/examples/strict-structured-reads.c
@@ -0,0 +1,270 @@
+/* Example usage with qemu-nbd:
+ *
+ * sock=`mktemp -u`
+ * qemu-nbd -f $format -k $sock -r image
+ * ./strict-structured-reads $sock
+ *
+ * This will perform read randomly over the image and check that all
+ * structured replies comply with the NBD spec (chunks may be out of
+ * order or interleaved, but no read succeeds unless chunks cover the
+ * entire region, with no overlapping or zero-length chunks).
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <time.h>
+#include <assert.h>
+#include <errno.h>
+#include <string.h>
+
+#include <libnbd.h>
+
+/* A linked list of ranges still not seen. */
+struct range {
+ uint64_t first;
+ uint64_t last;
+ struct range *next;
+};
+
+/* Per-read data. */
+struct data {
+ uint64_t offset;
+ size_t count;
+ uint32_t flags;
+ size_t chunks;
+ struct range *remaining;
+};
+
+#define MAX_BUF (2 * 1024 * 1024)
+static char buf[MAX_BUF];
+
+/* Various statistics */
+static int total_data_chunks;
+static int total_data_bytes;
+static int total_hole_chunks;
+static int total_hole_bytes;
+static int total_chunks;
+static int total_df_reads;
+static int total_reads;
+static int64_t total_bytes;
+static int total_success;
+
+static int
+read_chunk (void *opaque, const void *bufv, size_t count, uint64_t offset,
+ int *error, int status)
+{
+ struct data *data = opaque;
+ struct range *r, **prev;
+
+ /* libnbd guarantees this: */
+ assert (offset >= data->offset);
+ assert (offset + count <= data->offset + data->count);
+
+ switch (status) {
+ case LIBNBD_READ_DATA:
+ total_data_chunks++;
+ total_data_bytes += count;
+ break;
+ case LIBNBD_READ_HOLE:
+ total_hole_chunks++;
+ total_hole_bytes += count;
+ break;
+ case LIBNBD_READ_ERROR:
+ assert (count == 0);
+ count = 1; /* Ensure no further chunks visit that offset */
+ break;
+ default:
+ goto error;
+ }
+ data->chunks++;
+ if (count == 0) {
+ fprintf (stderr, "buggy server: chunk must have non-zero size\n");
+ goto error;
+ }
+
+ /* Find element in remaining, or the server is in error */
+ for (prev = &data->remaining, r = *prev; r; prev = &r->next, r =
r->next) {
+ if (offset >= r->first)
+ break;
+ }
+ if (r == NULL || offset + count > r->last) {
+ /* we fail to detect double errors reported at the same offset,
+ * but at least the read is already going to fail.
+ */
+ if (status == LIBNBD_READ_ERROR)
+ return 0;
+ fprintf (stderr, "buggy server: chunk with overlapping range\n");
+ goto error;
+ }
+
+ /* Resize or split r to track new remaining bytes */
+ if (offset == r->first) {
+ if (offset + count == r->last) {
+ *prev = r->next;
+ free (r);
+ }
+ else
+ r->first += count;
+ }
+ else if (offset + count == r->last) {
+ r->last -= count;
+ }
+ else {
+ struct range *n = malloc (sizeof *n);
+ assert (n);
+ n->next = r->next;
+ r->next = n;
+ n->last = r->last;
+ r->last = offset - r->first;
+ n->first = offset + count;
+ }
+
+ return 0;
+ error:
+ *error = EPROTO;
+ return -1;
+}
+
+static int
+read_verify (void *opaque, int64_t handle, int *error)
+{
+ struct data *data = opaque;
+ int ret = -1;
+
+ total_reads++;
+ total_chunks += data->chunks;
+ if (*error)
+ goto cleanup;
+ assert (data->chunks > 0);
+ if (data->flags & LIBNBD_CMD_FLAG_DF) {
+ total_df_reads++;
+ if (data->chunks > 1) {
+ fprintf (stderr, "buggy server: too many chunks for DF flag\n");
+ *error = EPROTO;
+ goto cleanup;
+ }
+ }
+ if (data->remaining && !*error) {
+ fprintf (stderr, "buggy server: not enough chunks on success\n");
+ *error = EPROTO;
+ goto cleanup;
+ }
+ total_bytes += data->count;
+ total_success++;
+ ret = 0;
+
+ cleanup:
+ while (data->remaining) {
+ struct range *r = data->remaining;
+ data->remaining = r->next;
+ free (r);
+ }
+ free (data);
+ return ret;
+}
+
+int
+main (int argc, char *argv[])
+{
+ struct nbd_handle *nbd;
+ size_t i;
+ int64_t exportsize;
+ int64_t maxsize = MAX_BUF;
+ uint64_t offset;
+
+ srand (time (NULL));
+
+ if (argc != 2) {
+ fprintf (stderr, "%s socket|uri\n", argv[0]);
+ exit (EXIT_FAILURE);
+ }
+
+ nbd = nbd_create ();
+ if (nbd == NULL) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ if (strstr (argv[1], "://")) {
+ if (nbd_connect_uri (nbd, argv[1]) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+ else if (nbd_connect_unix (nbd, argv[1]) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+
+ exportsize = nbd_get_size (nbd);
+ if (exportsize == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ if (exportsize < 512) {
+ fprintf (stderr, "image is too small for useful testing\n");
+ exit (EXIT_FAILURE);
+ }
+ if (exportsize <= maxsize)
+ maxsize = exportsize - 1;
+
+ /* Queue up 1000 parallel reads. We are reusing the same buffer,
+ * which is not safe in real life, but okay here because we aren't
+ * validating contents, only server behavior.
+ */
+ for (i = 0; i < 1000; ++i) {
+ uint32_t flags = 0;
+ struct data *d = malloc (sizeof *d);
+ struct range *r = malloc (sizeof *r);
+
+ assert (d && r);
+ offset = rand () % (exportsize - maxsize);
+ if (rand() & 1)
+ flags = LIBNBD_CMD_FLAG_DF;
+ *r = (struct range) { .first = offset, .last = offset + maxsize, };
+ *d = (struct data) { .offset = offset, .count = maxsize, .flags = flags,
+ .remaining = r, };
+ if (nbd_aio_pread_structured_notify (nbd, buf, sizeof buf, offset, d,
+ read_chunk, read_verify,
+ flags) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ while (nbd_aio_in_flight (nbd) > 0) {
+ int64_t handle = nbd_aio_peek_command_completed (nbd);
+
+ if (handle == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ if (handle == 0) {
+ if (nbd_poll (nbd, -1) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+ else
+ nbd_aio_command_completed (nbd, handle);
+ }
+
+ if (nbd_shutdown (nbd) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+
+ nbd_close (nbd);
+
+ printf ("totals:\n");
+ printf (" data chunks: %10d\n", total_data_chunks);
+ printf (" data bytes: %10d\n", total_data_bytes);
+ printf (" hole chunks: %10d\n", total_hole_chunks);
+ printf (" hole bytes: %10d\n", total_hole_bytes);
+ printf (" all chunks: %10d\n", total_chunks);
+ printf (" reads: %10d\n", total_reads);
+ printf (" bytes read: %10" PRId64 "\n", total_bytes);
+ printf (" compliant: %10d\n", total_success);
+
+ exit (EXIT_SUCCESS);
+}
--
2.20.1