generic: ENOSPC regression test in a multi-threaded scenario
authorPratik Rajesh Sampat <psampat@linux.ibm.com>
Tue, 1 Dec 2020 13:03:28 +0000 (18:33 +0530)
committerEryu Guan <guaneryu@gmail.com>
Sun, 6 Dec 2020 14:15:01 +0000 (22:15 +0800)
Test allocation strategies of the file system and validate space
anomalies as reported by the system versus the allocated by the
program.

The test is motivated by a bug in ext4 systems where-in ENOSPC is
reported by the file system even though enough space for allocations is
available[1].

[1]: https://patchwork.ozlabs.org/patch/1294003

Linux kernel patch series that fixes the above regression:
53f86b170dfa ("ext4: mballoc: add blocks to PA list under same spinlock
              after allocating blocks")
cf5e2ca6c990 ("ext4: mballoc: refactor ext4_mb_discard_preallocations()")
07b5b8e1ac40 ("ext4: mballoc: introduce pcpu seqcnt for freeing PA to
              improve ENOSPC handling")
8ef123fe02ca ("ext4: mballoc: refactor ext4_mb_good_group()")
993778306e79 ("ext4: mballoc: use lock for checking free blocks while
              retrying")

Suggested-by: Ritesh Harjani <riteshh@linux.ibm.com>
Co-authored-by: Sourabh Jain <sourabhjain@linux.ibm.com>
Signed-off-by: Sourabh Jain <sourabhjain@linux.ibm.com>
Signed-off-by: Pratik Rajesh Sampat <psampat@linux.ibm.com>
Reviewed-by: Eryu Guan <guaneryu@gmail.com>
Signed-off-by: Eryu Guan <guaneryu@gmail.com>
.gitignore
src/Makefile
src/t_enospc.c [new file with mode: 0644]
tests/generic/619 [new file with mode: 0755]
tests/generic/619.out [new file with mode: 0644]
tests/generic/group

index 5f5c4a0..041cc2d 100644 (file)
 /src/t_dir_offset2
 /src/t_dir_type
 /src/t_encrypted_d_revalidate
+/src/t_enospc
 /src/t_ext4_dax_inline_corruption
 /src/t_ext4_dax_journal_corruption
 /src/t_futimens
index 919d77c..3294014 100644 (file)
@@ -17,7 +17,7 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
        t_mmap_cow_race t_mmap_fallocate fsync-err t_mmap_write_ro \
        t_ext4_dax_journal_corruption t_ext4_dax_inline_corruption \
        t_ofd_locks t_mmap_collision mmap-write-concurrent \
-       t_get_file_time t_create_short_dirs t_create_long_dirs
+       t_get_file_time t_create_short_dirs t_create_long_dirs t_enospc
 
 LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
        preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
diff --git a/src/t_enospc.c b/src/t_enospc.c
new file mode 100644 (file)
index 0000000..d06c676
--- /dev/null
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2020 IBM.
+ * All Rights Reserved.
+ *
+ * simple mmap write multithreaded test for testing enospc
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <assert.h>
+#include <sys/mman.h>
+#include <pthread.h>
+#include <signal.h>
+
+#define pr_debug(fmt, ...) do { \
+    if (verbose) { \
+        printf (fmt, ##__VA_ARGS__); \
+    } \
+} while (0)
+
+struct thread_s {
+       int id;
+};
+
+static float file_ratio[2] = {0.5, 0.5};
+static unsigned long fsize = 0;
+static char *base_dir = ".";
+static char *ratio = "1";
+
+static bool use_fallocate = false;
+static bool verbose = false;
+
+pthread_barrier_t bar;
+
+void handle_sigbus(int sig)
+{
+       pr_debug("Enospc test failed with SIGBUS\n");
+       exit(7);
+}
+
+void enospc_test(int id)
+{
+       int fd;
+       char fpath[255] = {0};
+       char *addr;
+       unsigned long size = 0;
+
+       /*
+        * Comma separated values against -r option indicates that the file
+        * should be divided into two small files.
+        * The file_ratio specifies the proportion in which the file sizes must
+        * be divided into.
+        *
+        * Half of the files will be divided into size of the first ratio and the
+        * rest of the following ratio
+        */
+
+       if (id % 2 == 0)
+               size = fsize * file_ratio[0];
+       else
+               size = fsize * file_ratio[1];
+
+       pthread_barrier_wait(&bar);
+
+       sprintf(fpath, "%s/mmap-file-%d", base_dir, id);
+       pr_debug("Test write phase starting file %s fsize %lu, id %d\n", fpath, size, id);
+
+       signal(SIGBUS, handle_sigbus);
+
+       fd = open(fpath, O_RDWR | O_CREAT, 0644);
+       if (fd < 0) {
+               pr_debug("Open failed\n");
+               exit(1);
+       }
+
+       if (use_fallocate)
+               assert(fallocate(fd, 0, 0, size) == 0);
+       else
+               assert(ftruncate(fd, size) == 0);
+
+       addr = mmap(NULL, size, PROT_WRITE, MAP_SHARED, fd, 0);
+       assert(addr != MAP_FAILED);
+
+       for (int i = 0; i < size; i++) {
+               addr[i] = 0xAB;
+       }
+
+       assert(munmap(addr, size) != -1);
+       close(fd);
+       pr_debug("Test write phase completed...file %s, fsize %lu, id %d\n", fpath, size, id);
+}
+
+void *spawn_test_thread(void *arg)
+{
+       struct thread_s *thread_info = (struct thread_s *)arg;
+
+       enospc_test(thread_info->id);
+       return NULL;
+}
+
+void _run_test(int threads)
+{
+       pthread_t tid[threads];
+
+       pthread_barrier_init(&bar, NULL, threads+1);
+       for (int i = 0; i < threads; i++) {
+               struct thread_s *thread_info = (struct thread_s *) malloc(sizeof(struct thread_s));
+               thread_info->id = i;
+               assert(pthread_create(&tid[i], NULL, spawn_test_thread, thread_info) == 0);
+       }
+
+       pthread_barrier_wait(&bar);
+
+       for (int i = 0; i <threads; i++) {
+               assert(pthread_join(tid[i], NULL) == 0);
+       }
+
+       pthread_barrier_destroy(&bar);
+       return;
+}
+
+static void usage(void)
+{
+       printf("\nUsage: t_enospc [options]\n\n"
+              " -t                    thread count\n"
+              " -s size               file size\n"
+              " -p path               set base path\n"
+              " -f fallocate          use fallocate instead of ftruncate\n"
+              " -v verbose            print debug information\n"
+              " -r ratio              ratio of file sizes, ',' separated values (def: 0.5,0.5)\n");
+}
+
+int main(int argc, char *argv[])
+{
+       int opt;
+       int threads = 0;
+       char *ratio_ptr;
+
+       while ((opt = getopt(argc, argv, ":r:p:t:s:vf")) != -1) {
+               switch(opt) {
+               case 't':
+                       threads = atoi(optarg);
+                       pr_debug("Testing with (%d) threads\n", threads);
+                       break;
+               case 's':
+                       fsize = atoi(optarg);
+                       pr_debug("size: %lu Bytes\n", fsize);
+                       break;
+               case 'p':
+                       base_dir = optarg;
+                       break;
+               case 'r':
+                       ratio = optarg;
+                       ratio_ptr = strtok(ratio, ",");
+                       if (ratio_ptr[0] == '1') {
+                               file_ratio[0] = 1;
+                               file_ratio[1] = 1;
+                               break;
+                       }
+                       if (ratio_ptr != NULL)
+                               file_ratio[0] = strtod(ratio_ptr, NULL);
+                       ratio_ptr = strtok(NULL, ",");
+                       if (ratio_ptr != NULL)
+                               file_ratio[1] = strtod(ratio_ptr, NULL);
+                       break;
+               case 'f':
+                       use_fallocate = true;
+                       break;
+               case 'v':
+                       verbose = true;
+                       break;
+               case '?':
+                       usage();
+                       exit(1);
+               }
+       }
+
+       /* Sanity test of parameters */
+       assert(threads);
+       assert(fsize);
+       assert(file_ratio[0]);
+       assert(file_ratio[1]);
+
+       pr_debug("Testing with (%d) threads\n", threads);
+       pr_debug("size: %lu Bytes\n", fsize);
+
+       _run_test(threads);
+       return 0;
+}
diff --git a/tests/generic/619 b/tests/generic/619
new file mode 100755 (executable)
index 0000000..3c43fe7
--- /dev/null
@@ -0,0 +1,207 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2020 IBM Corporation. All Rights Reserved.
+#
+# FS QA Test 619
+#
+# ENOSPC regression test in a multi-threaded scenario. Test allocation
+# strategies of the file system and validate space anomalies as reported by
+# the system versus the allocated by the program.
+#
+# The test is motivated by a bug in ext4 systems where-in ENOSPC is
+# reported by the file system even though enough space for allocations is
+# available[1].
+# [1]: https://patchwork.ozlabs.org/patch/1294003
+#
+# Linux kernel patch series that fixes the above regression:
+# 53f86b170dfa ("ext4: mballoc: add blocks to PA list under same spinlock
+#               after allocating blocks")
+# cf5e2ca6c990 ("ext4: mballoc: refactor ext4_mb_discard_preallocations()")
+# 07b5b8e1ac40 ("ext4: mballoc: introduce pcpu seqcnt for freeing PA to
+#               improve ENOSPC handling")
+# 8ef123fe02ca ("ext4: mballoc: refactor ext4_mb_good_group()")
+# 993778306e79 ("ext4: mballoc: use lock for checking free blocks while
+#               retrying")
+#
+seq=`basename $0`
+seqres=$RESULT_DIR/$seq
+echo "QA output created by $seq"
+
+here=`pwd`
+tmp=/tmp/$$
+status=1       # failure is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+FS_SIZE=$((240*1024*1024)) # 240MB
+DEBUG=1 # set to 0 to disable debug statements in shell and c-prog
+FACT=0.7
+
+# Disk allocation methods
+FALLOCATE=1
+FTRUNCATE=2
+
+# Helps to build TEST_VECTORS
+SMALL_FILE_SIZE=$((512 * 1024)) # in Bytes
+BIG_FILE_SIZE=$((1536 * 1024))  # in Bytes
+MIX_FILE_SIZE=$((2048 * 1024))  # (BIG + SMALL small file size)
+
+_cleanup()
+{
+       cd /
+       rm -f $tmp.*
+}
+
+# get standard environment, filters and checks
+. ./common/rc
+. ./common/filter
+
+# remove previous $seqres.full before test
+rm -f $seqres.full
+
+# Modify as appropriate.
+_supported_fs generic
+_require_scratch
+_require_test_program "t_enospc"
+_require_xfs_io_command "falloc"
+
+debug()
+{
+       if [ $DEBUG -eq 1 ]; then
+               echo "$1" >> $seqres.full
+       fi
+}
+
+# Calculate the number of threads needed to fill the disk space
+# Arguments
+# $1: the size of a file
+# $2: ratio in which $1 file should be split into multiple files.
+# $3: percentage of the disk space should be used during the test
+# Calculate the number of threads needed to fill the disk space
+calc_thread_cnt()
+{
+       local file_ratio_unit=$1
+       local file_ratio=$2
+       local disk_saturation=$3
+       local tot_avail_size
+       local avail_size
+       local thread_cnt
+
+       IFS=',' read -ra fratio <<< $file_ratio
+       file_ratio_cnt=${#fratio[@]}
+
+       tot_avail_size=$($DF_PROG --block-size=1 $SCRATCH_DEV | awk 'FNR == 2 { print $5 }')
+       avail_size=$(echo $tot_avail_size*$disk_saturation | $BC_PROG)
+       thread_cnt=$(echo "$file_ratio_cnt*($avail_size/$file_ratio_unit)" | $BC_PROG)
+
+       debug "Total available size: $tot_avail_size"
+       debug "Available size: $avail_size"
+       debug "Thread count: $thread_cnt"
+
+       echo ${thread_cnt}
+}
+
+# Arguments
+# $1: a string containing test configuration separated by a colon.
+#     $1 is treated as an array of arguments to the function.
+#     Description of each array element is given below.
+#
+# @1: name of the test
+# @2: thread in t_enospec exerciser will allocate file of @2 size
+# @3: defines the proportion in which the file size defined in @2
+#     should be divided into two files.
+#     (valid @3: more than two values are not allowed)
+#                values should be comma separated)
+#                sum of all values must be 1)
+# @4: define the percentage of available memory should be used to
+#     during the test.
+# @5: defines the disk allocation method (fallocate/ftruncate)
+# @6: number of the test should run
+run_testcase()
+{
+       IFS=':' read -ra args <<< $1
+       local test_name=${args[0]}
+       local file_ratio_unit=${args[1]}
+       local file_ratio=${args[2]}
+       local disk_saturation=${args[3]}
+       local disk_alloc_method=${args[4]}
+       local test_iteration_cnt=${args[5]}
+       local extra_args=""
+       local thread_cnt
+
+       if [ "$disk_alloc_method" == "$FALLOCATE" ]; then
+               extra_args="$extra_args -f"
+       fi
+
+       # enable the debug statements in c program
+       if [ "$DEBUG" -eq 1 ]; then
+               extra_args="$extra_args -v"
+       fi
+
+       debug "============ Test details start ============"
+       debug "Test name: $test_name"
+       debug "File ratio unit: $file_ratio_unit"
+       debug "File ratio: $file_ratio"
+       debug "Disk saturation $disk_saturation"
+       debug "Disk alloc method $disk_alloc_method"
+       debug "Test iteration count: $test_iteration_cnt"
+       debug "Extra arg: $extra_args"
+
+       for i in $(eval echo "{1..$test_iteration_cnt}"); do
+               # Setup the device
+               _scratch_mkfs_sized $FS_SIZE >> $seqres.full 2>&1
+               _scratch_mount
+
+               debug "===== Test: $test_name iteration: $i starts ====="
+               thread_cnt=$(calc_thread_cnt $file_ratio_unit $file_ratio $disk_saturation)
+
+               # Start the test
+               $here/src/t_enospc -t $thread_cnt -s $file_ratio_unit -r $file_ratio -p $SCRATCH_MNT $extra_args >> $seqres.full
+
+               status=$(echo $?)
+               if [ $status -ne 0 ]; then
+                       use_per=$($DF_PROG -h | grep $SCRATCH_MNT | awk '{print substr($6, 1, length($6)-1)}' | $BC_PROG)
+                       alloc_per=$(echo "$FACT * 100" | $BC_PROG)
+                       # We are here since t_enospc failed with an error code.
+                       # If the used filesystem space is still < available space - that means
+                       # the test failed due to FS wrongly reported ENOSPC.
+                       if [ $(echo "$use_per < $alloc_per" | $BC_PROG) -ne 0 ]; then
+                               if [ $status -eq 134 ]; then
+                                       # SIGABRT asserted exit code = 134
+                                       echo "FAIL: Aborted assertion faliure"
+                               elif [ $status -eq 7 ]; then
+                                       # SIGBUS asserted exit code = 7
+                                       echo "FAIL: ENOSPC BUS faliure"
+                               fi
+                               echo "$test_name failed at iteration count: $i"
+                               echo "$($DF_PROG -h $SCRATCH_MNT)"
+                               echo "Allocated: $alloc_per% Used: $use_per%"
+                               exit
+                       fi
+               fi
+
+               # Make space for other tests
+               _scratch_unmount
+
+               debug "===== Test: $test_name iteration: $i ends ====="
+       done
+       debug "============ Test details end ============="
+}
+
+declare -a TEST_VECTORS=(
+# test-name:file-ratio-unit:file-ratio:disk-saturation:disk-alloc-method:test-iteration-cnt
+"Small-file-fallocate-test:$SMALL_FILE_SIZE:1:$FACT:$FALLOCATE:3"
+"Big-file-fallocate-test:$BIG_FILE_SIZE:1:$FACT:$FALLOCATE:3"
+"Mix-file-fallocate-test:$MIX_FILE_SIZE:0.75,0.25:$FACT:$FALLOCATE:3"
+"Small-file-ftruncate-test:$SMALL_FILE_SIZE:1:$FACT:$FTRUNCATE:3"
+"Big-file-ftruncate-test:$BIG_FILE_SIZE:1:$FACT:$FTRUNCATE:3"
+"Mix-file-ftruncate-test:$MIX_FILE_SIZE:0.75,0.25:$FACT:$FTRUNCATE:3"
+)
+
+# real QA test starts here
+for i in "${TEST_VECTORS[@]}"; do
+       run_testcase $i
+done
+
+echo "Silence is golden"
+status=0
+exit
diff --git a/tests/generic/619.out b/tests/generic/619.out
new file mode 100644 (file)
index 0000000..d17873d
--- /dev/null
@@ -0,0 +1,2 @@
+QA output created by 619
+Silence is golden
index eca9d61..15a2f40 100644 (file)
 616 auto rw io_uring stress
 617 auto rw io_uring stress
 618 auto quick attr
+619 auto rw enospc