cead9d
From 48f6929590157d9a1697e11c02441207afdc1bed Mon Sep 17 00:00:00 2001
cead9d
From: Xavi Hernandez <xhernandez@redhat.com>
cead9d
Date: Fri, 27 Mar 2020 23:56:15 +0100
cead9d
Subject: [PATCH 362/362] write-behind: fix data corruption
cead9d
cead9d
There was a bug in write-behind that allowed a previous completed write
cead9d
to overwrite the overlapping region of data from a future write.
cead9d
cead9d
Suppose we want to send three writes (W1, W2 and W3). W1 and W2 are
cead9d
sequential, and W3 writes at the same offset of W2:
cead9d
cead9d
    W2.offset = W3.offset = W1.offset + W1.size
cead9d
cead9d
Both W1 and W2 are sent in parallel. W3 is only sent after W2 completes.
cead9d
So W3 should *always* overwrite the overlapping part of W2.
cead9d
cead9d
Suppose write-behind processes the requests from 2 concurrent threads:
cead9d
cead9d
    Thread 1                    Thread 2
cead9d
cead9d
    <received W1>
cead9d
                                <received W2>
cead9d
    wb_enqueue_tempted(W1)
cead9d
    /* W1 is assigned gen X */
cead9d
                                wb_enqueue_tempted(W2)
cead9d
                                /* W2 is assigned gen X */
cead9d
cead9d
                                wb_process_queue()
cead9d
                                  __wb_preprocess_winds()
cead9d
                                    /* W1 and W2 are sequential and all
cead9d
                                     * other requisites are met to merge
cead9d
                                     * both requests. */
cead9d
                                    __wb_collapse_small_writes(W1, W2)
cead9d
                                    __wb_fulfill_request(W2)
cead9d
cead9d
                                  __wb_pick_unwinds() -> W2
cead9d
                                  /* In this case, since the request is
cead9d
                                   * already fulfilled, wb_inode->gen
cead9d
                                   * is not updated. */
cead9d
cead9d
                                wb_do_unwinds()
cead9d
                                  STACK_UNWIND(W2)
cead9d
cead9d
                                /* The application has received the
cead9d
                                 * result of W2, so it can send W3. */
cead9d
                                <received W3>
cead9d
cead9d
                                wb_enqueue_tempted(W3)
cead9d
                                /* W3 is assigned gen X */
cead9d
cead9d
                                wb_process_queue()
cead9d
                                  /* Here we have W1 (which contains
cead9d
                                   * the conflicting W2) and W3 with
cead9d
                                   * same gen, so they are interpreted
cead9d
                                   * as concurrent writes that do not
cead9d
                                   * conflict. */
cead9d
                                  __wb_pick_winds() -> W3
cead9d
cead9d
                                wb_do_winds()
cead9d
                                  STACK_WIND(W3)
cead9d
cead9d
    wb_process_queue()
cead9d
      /* Eventually W1 will be
cead9d
       * ready to be sent */
cead9d
      __wb_pick_winds() -> W1
cead9d
      __wb_pick_unwinds() -> W1
cead9d
        /* Here wb_inode->gen is
cead9d
         * incremented. */
cead9d
cead9d
    wb_do_unwinds()
cead9d
      STACK_UNWIND(W1)
cead9d
cead9d
    wb_do_winds()
cead9d
      STACK_WIND(W1)
cead9d
cead9d
So, as we can see, W3 is sent before W1, which shouldn't happen.
cead9d
cead9d
The problem is that wb_inode->gen is only incremented for requests that
cead9d
have not been fulfilled but, after a merge, the request is marked as
cead9d
fulfilled even though it has not been sent to the brick. This allows
cead9d
that future requests are assigned to the same generation, which could
cead9d
be internally reordered.
cead9d
cead9d
Solution:
cead9d
cead9d
Increment wb_inode->gen before any unwind, even if it's for a fulfilled
cead9d
request.
cead9d
cead9d
Special thanks to Stefan Ring for writing a reproducer that has been
cead9d
crucial to identify the issue.
cead9d
cead9d
Upstream patch:
cead9d
> Upstream patch link: https://review.gluster.org/c/glusterfs/+/24263
cead9d
> Change-Id: Id4ab0f294a09aca9a863ecaeef8856474662ab45
cead9d
> Signed-off-by: Xavi Hernandez <xhernandez@redhat.com>
cead9d
> Fixes: #884
cead9d
cead9d
Change-Id: Id4ab0f294a09aca9a863ecaeef8856474662ab45
cead9d
Signed-off-by: Xavi Hernandez <xhernandez@redhat.com>
cead9d
BUG: 1819059
cead9d
Reviewed-on: https://code.engineering.redhat.com/gerrit/196250
cead9d
Tested-by: RHGS Build Bot <nigelb@redhat.com>
cead9d
Reviewed-by: Sunil Kumar Heggodu Gopala Acharya <sheggodu@redhat.com>
cead9d
---
cead9d
 tests/bugs/write-behind/issue-884.c                | 267 +++++++++++++++++++++
cead9d
 tests/bugs/write-behind/issue-884.t                |  40 +++
cead9d
 .../performance/write-behind/src/write-behind.c    |   4 +-
cead9d
 3 files changed, 309 insertions(+), 2 deletions(-)
cead9d
 create mode 100644 tests/bugs/write-behind/issue-884.c
cead9d
 create mode 100755 tests/bugs/write-behind/issue-884.t
cead9d
cead9d
diff --git a/tests/bugs/write-behind/issue-884.c b/tests/bugs/write-behind/issue-884.c
cead9d
new file mode 100644
cead9d
index 0000000..e9c33b3
cead9d
--- /dev/null
cead9d
+++ b/tests/bugs/write-behind/issue-884.c
cead9d
@@ -0,0 +1,267 @@
cead9d
+
cead9d
+#define _GNU_SOURCE
cead9d
+
cead9d
+#include <stdlib.h>
cead9d
+#include <stdio.h>
cead9d
+#include <string.h>
cead9d
+#include <time.h>
cead9d
+#include <assert.h>
cead9d
+#include <errno.h>
cead9d
+#include <sys/types.h>
cead9d
+#include <sys/stat.h>
cead9d
+#include <pthread.h>
cead9d
+
cead9d
+#include <glusterfs/api/glfs.h>
cead9d
+
cead9d
+/* Based on a reproducer by Stefan Ring. It seems to be quite sensible to any
cead9d
+ * timing modification, so the code has been maintained as is, only with minor
cead9d
+ * changes. */
cead9d
+
cead9d
+struct glfs *glfs;
cead9d
+
cead9d
+pthread_mutex_t the_mutex = PTHREAD_MUTEX_INITIALIZER;
cead9d
+pthread_cond_t the_cond = PTHREAD_COND_INITIALIZER;
cead9d
+
cead9d
+typedef struct _my_aiocb {
cead9d
+    int64_t size;
cead9d
+    volatile int64_t seq;
cead9d
+    int which;
cead9d
+} my_aiocb;
cead9d
+
cead9d
+typedef struct _worker_data {
cead9d
+    my_aiocb cb;
cead9d
+    struct iovec iov;
cead9d
+    int64_t offset;
cead9d
+} worker_data;
cead9d
+
cead9d
+typedef struct {
cead9d
+    worker_data wdata[2];
cead9d
+
cead9d
+    volatile unsigned busy;
cead9d
+} all_data_t;
cead9d
+
cead9d
+all_data_t all_data;
cead9d
+
cead9d
+static void
cead9d
+completion_fnc(struct glfs_fd *fd, ssize_t ret, struct glfs_stat *pre,
cead9d
+               struct glfs_stat *post, void *arg)
cead9d
+{
cead9d
+    void *the_thread;
cead9d
+    my_aiocb *cb = (my_aiocb *)arg;
cead9d
+    long seq = cb->seq;
cead9d
+
cead9d
+    assert(ret == cb->size);
cead9d
+
cead9d
+    pthread_mutex_lock(&the_mutex);
cead9d
+    pthread_cond_broadcast(&the_cond);
cead9d
+
cead9d
+    all_data.busy &= ~(1 << cb->which);
cead9d
+    cb->seq = -1;
cead9d
+
cead9d
+    the_thread = (void *)pthread_self();
cead9d
+    printf("worker %d is done from thread %p, seq %ld!\n", cb->which,
cead9d
+           the_thread, seq);
cead9d
+
cead9d
+    pthread_mutex_unlock(&the_mutex);
cead9d
+}
cead9d
+
cead9d
+static void
cead9d
+init_wdata(worker_data *data, int which)
cead9d
+{
cead9d
+    data->cb.which = which;
cead9d
+    data->cb.seq = -1;
cead9d
+
cead9d
+    data->iov.iov_base = malloc(1024 * 1024);
cead9d
+    memset(data->iov.iov_base, 6,
cead9d
+           1024 * 1024); /* tail part never overwritten */
cead9d
+}
cead9d
+
cead9d
+static void
cead9d
+init()
cead9d
+{
cead9d
+    all_data.busy = 0;
cead9d
+
cead9d
+    init_wdata(&all_data.wdata[0], 0);
cead9d
+    init_wdata(&all_data.wdata[1], 1);
cead9d
+}
cead9d
+
cead9d
+static void
cead9d
+do_write(struct glfs_fd *fd, int content, int size, int64_t seq,
cead9d
+         worker_data *wdata, const char *name)
cead9d
+{
cead9d
+    int ret;
cead9d
+
cead9d
+    wdata->cb.size = size;
cead9d
+    wdata->cb.seq = seq;
cead9d
+
cead9d
+    if (content >= 0)
cead9d
+        memset(wdata->iov.iov_base, content, size);
cead9d
+    wdata->iov.iov_len = size;
cead9d
+
cead9d
+    pthread_mutex_lock(&the_mutex);
cead9d
+    printf("(%d) dispatching write \"%s\", offset %lx, len %x, seq %ld\n",
cead9d
+           wdata->cb.which, name, (long)wdata->offset, size, (long)seq);
cead9d
+    pthread_mutex_unlock(&the_mutex);
cead9d
+    ret = glfs_pwritev_async(fd, &wdata->iov, 1, wdata->offset, 0,
cead9d
+                             completion_fnc, &wdata->cb);
cead9d
+    assert(ret >= 0);
cead9d
+}
cead9d
+
cead9d
+#define IDLE 0  // both workers must be idle
cead9d
+#define ANY 1   // use any worker, other one may be busy
cead9d
+
cead9d
+int
cead9d
+get_worker(int waitfor, int64_t excl_seq)
cead9d
+{
cead9d
+    int which;
cead9d
+
cead9d
+    pthread_mutex_lock(&the_mutex);
cead9d
+
cead9d
+    while (waitfor == IDLE && (all_data.busy & 3) != 0 ||
cead9d
+           waitfor == ANY &&
cead9d
+               ((all_data.busy & 3) == 3 ||
cead9d
+                excl_seq >= 0 && (all_data.wdata[0].cb.seq == excl_seq ||
cead9d
+                                  all_data.wdata[1].cb.seq == excl_seq)))
cead9d
+        pthread_cond_wait(&the_cond, &the_mutex);
cead9d
+
cead9d
+    if (!(all_data.busy & 1))
cead9d
+        which = 0;
cead9d
+    else
cead9d
+        which = 1;
cead9d
+
cead9d
+    all_data.busy |= (1 << which);
cead9d
+
cead9d
+    pthread_mutex_unlock(&the_mutex);
cead9d
+
cead9d
+    return which;
cead9d
+}
cead9d
+
cead9d
+static int
cead9d
+doit(struct glfs_fd *fd)
cead9d
+{
cead9d
+    int ret;
cead9d
+    int64_t seq = 0;
cead9d
+    int64_t offset = 0;     // position in file, in blocks
cead9d
+    int64_t base = 0x1000;  // where to place the data, in blocks
cead9d
+
cead9d
+    int async_mode = ANY;
cead9d
+
cead9d
+    init();
cead9d
+
cead9d
+    for (;;) {
cead9d
+        int which;
cead9d
+        worker_data *wdata;
cead9d
+
cead9d
+        // for growing to the first offset
cead9d
+        for (;;) {
cead9d
+            int gap = base + 0x42 - offset;
cead9d
+            if (!gap)
cead9d
+                break;
cead9d
+            if (gap > 80)
cead9d
+                gap = 80;
cead9d
+
cead9d
+            which = get_worker(IDLE, -1);
cead9d
+            wdata = &all_data.wdata[which];
cead9d
+
cead9d
+            wdata->offset = offset << 9;
cead9d
+            do_write(fd, 0, gap << 9, seq++, wdata, "gap-filling");
cead9d
+
cead9d
+            offset += gap;
cead9d
+        }
cead9d
+
cead9d
+        // 8700
cead9d
+        which = get_worker(IDLE, -1);
cead9d
+        wdata = &all_data.wdata[which];
cead9d
+
cead9d
+        wdata->offset = (base + 0x42) << 9;
cead9d
+        do_write(fd, 1, 62 << 9, seq++, wdata, "!8700");
cead9d
+
cead9d
+        // 8701
cead9d
+        which = get_worker(IDLE, -1);
cead9d
+        wdata = &all_data.wdata[which];
cead9d
+
cead9d
+        wdata->offset = (base + 0x42) << 9;
cead9d
+        do_write(fd, 2, 55 << 9, seq++, wdata, "!8701");
cead9d
+
cead9d
+        // 8702
cead9d
+        which = get_worker(async_mode, -1);
cead9d
+        wdata = &all_data.wdata[which];
cead9d
+
cead9d
+        wdata->offset = (base + 0x79) << 9;
cead9d
+        do_write(fd, 3, 54 << 9, seq++, wdata, "!8702");
cead9d
+
cead9d
+        // 8703
cead9d
+        which = get_worker(async_mode, -1);
cead9d
+        wdata = &all_data.wdata[which];
cead9d
+
cead9d
+        wdata->offset = (base + 0xaf) << 9;
cead9d
+        do_write(fd, 4, 81 << 9, seq++, wdata, "!8703");
cead9d
+
cead9d
+        // 8704
cead9d
+        // this writes both 5s and 6s
cead9d
+        // the range of 5s is the one that overwrites 8703
cead9d
+
cead9d
+        which = get_worker(async_mode, seq - 1);
cead9d
+        wdata = &all_data.wdata[which];
cead9d
+
cead9d
+        memset(wdata->iov.iov_base, 5, 81 << 9);
cead9d
+        wdata->offset = (base + 0xaf) << 9;
cead9d
+        do_write(fd, -1, 1623 << 9, seq++, wdata, "!8704");
cead9d
+
cead9d
+        offset = base + 0x706;
cead9d
+        base += 0x1000;
cead9d
+        if (base >= 0x100000)
cead9d
+            break;
cead9d
+    }
cead9d
+
cead9d
+    printf("done!\n");
cead9d
+    fflush(stdout);
cead9d
+
cead9d
+    pthread_mutex_lock(&the_mutex);
cead9d
+
cead9d
+    while ((all_data.busy & 3) != 0)
cead9d
+        pthread_cond_wait(&the_cond, &the_mutex);
cead9d
+
cead9d
+    pthread_mutex_unlock(&the_mutex);
cead9d
+
cead9d
+    ret = glfs_close(fd);
cead9d
+    assert(ret >= 0);
cead9d
+    /*
cead9d
+        ret = glfs_fini(glfs);
cead9d
+        assert(ret >= 0);
cead9d
+    */
cead9d
+    return 0;
cead9d
+}
cead9d
+
cead9d
+int
cead9d
+main(int argc, char *argv[])
cead9d
+{
cead9d
+    int ret;
cead9d
+    int open_flags = O_RDWR | O_DIRECT | O_TRUNC;
cead9d
+    struct glfs_fd *fd;
cead9d
+
cead9d
+    glfs = glfs_new(argv[1]);
cead9d
+    if (!glfs) {
cead9d
+        printf("glfs_new!\n");
cead9d
+        goto out;
cead9d
+    }
cead9d
+    ret = glfs_set_volfile_server(glfs, "tcp", "localhost", 24007);
cead9d
+    if (ret < 0) {
cead9d
+        printf("set_volfile!\n");
cead9d
+        goto out;
cead9d
+    }
cead9d
+    ret = glfs_init(glfs);
cead9d
+    if (ret) {
cead9d
+        printf("init!\n");
cead9d
+        goto out;
cead9d
+    }
cead9d
+    fd = glfs_open(glfs, argv[2], open_flags);
cead9d
+    if (!fd) {
cead9d
+        printf("open!\n");
cead9d
+        goto out;
cead9d
+    }
cead9d
+    srand(time(NULL));
cead9d
+    return doit(fd);
cead9d
+out:
cead9d
+    return 1;
cead9d
+}
cead9d
diff --git a/tests/bugs/write-behind/issue-884.t b/tests/bugs/write-behind/issue-884.t
cead9d
new file mode 100755
cead9d
index 0000000..2bcf7d1
cead9d
--- /dev/null
cead9d
+++ b/tests/bugs/write-behind/issue-884.t
cead9d
@@ -0,0 +1,40 @@
cead9d
+#!/bin/bash
cead9d
+
cead9d
+. $(dirname $0)/../../include.rc
cead9d
+. $(dirname $0)/../../volume.rc
cead9d
+
cead9d
+# This test tries to detect a race condition in write-behind. It's based on a
cead9d
+# reproducer written by Stefan Ring that is able to hit it sometimes. On my
cead9d
+# system, it happened around 10% of the runs. This means that if this bug
cead9d
+# appears again, this test will fail once every 10 runs. Most probably this
cead9d
+# failure will be hidden by the automatic test retry of the testing framework.
cead9d
+#
cead9d
+# Please, if this test fails, it needs to be analyzed in detail.
cead9d
+
cead9d
+function run() {
cead9d
+    "${@}" >/dev/null
cead9d
+}
cead9d
+
cead9d
+cleanup
cead9d
+
cead9d
+TEST glusterd
cead9d
+TEST pidof glusterd
cead9d
+
cead9d
+TEST $CLI volume create $V0 $H0:$B0/$V0
cead9d
+# This makes it easier to hit the issue
cead9d
+TEST $CLI volume set $V0 client-log-level TRACE
cead9d
+TEST $CLI volume start $V0
cead9d
+
cead9d
+TEST $GFS --volfile-server=$H0 --volfile-id=$V0 $M0
cead9d
+
cead9d
+build_tester $(dirname $0)/issue-884.c -lgfapi
cead9d
+
cead9d
+TEST touch $M0/testfile
cead9d
+
cead9d
+# This program generates a file of 535694336 bytes with a fixed pattern
cead9d
+TEST run $(dirname $0)/issue-884 $V0 testfile
cead9d
+
cead9d
+# This is the md5sum of the expected pattern without corruption
cead9d
+EXPECT "ad105f9349345a70fc697632cbb5eec8" echo "$(md5sum $B0/$V0/testfile | awk '{ print $1; }')"
cead9d
+
cead9d
+cleanup
cead9d
diff --git a/xlators/performance/write-behind/src/write-behind.c b/xlators/performance/write-behind/src/write-behind.c
cead9d
index 70e281a..90a0bcf 100644
cead9d
--- a/xlators/performance/write-behind/src/write-behind.c
cead9d
+++ b/xlators/performance/write-behind/src/write-behind.c
cead9d
@@ -1284,14 +1284,14 @@ __wb_pick_unwinds(wb_inode_t *wb_inode, list_head_t *lies)
cead9d
 
cead9d
         wb_inode->window_current += req->orig_size;
cead9d
 
cead9d
+        wb_inode->gen++;
cead9d
+
cead9d
         if (!req->ordering.fulfilled) {
cead9d
             /* burden increased */
cead9d
             list_add_tail(&req->lie, &wb_inode->liability);
cead9d
 
cead9d
             req->ordering.lied = 1;
cead9d
 
cead9d
-            wb_inode->gen++;
cead9d
-
cead9d
             uuid_utoa_r(req->gfid, gfid);
cead9d
             gf_msg_debug(wb_inode->this->name, 0,
cead9d
                          "(unique=%" PRIu64
cead9d
-- 
cead9d
1.8.3.1
cead9d