fuse2fs: flush dirty metadata periodically

Flush dirty metadata out to disk periodically like the kernel, to reduce
the potential for data loss if userspace doesn't explicitly fsync.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
diff --git a/fuse4fs/fuse4fs.c b/fuse4fs/fuse4fs.c
index 3b65aef..45b4004 100644
--- a/fuse4fs/fuse4fs.c
+++ b/fuse4fs/fuse4fs.c
@@ -28,6 +28,7 @@
 #include <unistd.h>
 #include <ctype.h>
 #include <assert.h>
+#include <limits.h>
 #ifdef HAVE_FUSE_LOOPDEV
 # include <fuse_loopdev.h>
 #endif
@@ -332,6 +333,10 @@
 #endif
 	struct psi *mem_psi;
 	struct psi_handler *mem_psi_handler;
+
+	struct bthread *flush_thread;
+	unsigned int flush_interval;
+	double last_flush;
 };
 
 #ifdef HAVE_FUSE_SERVICE
@@ -1005,6 +1010,71 @@
 	fp->keep_cache = 1;
 }
 
+static errcode_t fuse4fs_flush(struct fuse4fs *ff, int flags)
+{
+	double last_flush = gettime_monotonic();
+	errcode_t err;
+
+	err = ext2fs_flush2(ff->fs, flags);
+	if (err)
+		return err;
+
+	ff->last_flush = last_flush;
+	return 0;
+}
+
+static inline int fuse4fs_flush_wanted(struct fuse4fs *ff)
+{
+	return ff->fs != NULL && ff->opstate == F4OP_WRITABLE &&
+	       ff->last_flush + ff->flush_interval <= gettime_monotonic();
+}
+
+static void fuse4fs_flush_bthread(void *data)
+{
+	struct fuse4fs *ff = data;
+	ext2_filsys fs;
+	errcode_t err;
+	int ret = 0;
+
+	fs = fuse4fs_start(ff);
+	if (fuse4fs_flush_wanted(ff) && !bthread_cancelled(ff->flush_thread)) {
+		err = fuse4fs_flush(ff, 0);
+		if (err)
+			ret = translate_error(fs, 0, err);
+	}
+	fuse4fs_finish(ff, ret);
+}
+
+static void fuse4fs_flush_start(struct fuse4fs *ff)
+{
+	int ret;
+
+	if (!ff->flush_interval)
+		return;
+
+	ret = bthread_create("fuse4fs_flush", fuse4fs_flush_bthread, ff,
+			     ff->flush_interval, &ff->flush_thread);
+	if (ret) {
+		err_printf(ff, "flusher: %s.\n", error_message(ret));
+		return;
+	}
+
+	ret = bthread_start(ff->flush_thread);
+	if (ret)
+		err_printf(ff, "flusher: %s.\n", error_message(ret));
+}
+
+static void fuse4fs_flush_cancel(struct fuse4fs *ff)
+{
+	if (ff->flush_thread)
+		bthread_cancel(ff->flush_thread);
+}
+
+static void fuse4fs_flush_destroy(struct fuse4fs *ff)
+{
+	bthread_destroy(&ff->flush_thread);
+}
+
 #ifdef HAVE_FUSE_IOMAP
 static inline int fuse4fs_iomap_enabled(const struct fuse4fs *ff)
 {
@@ -2244,7 +2314,7 @@
 		ext2fs_set_tstamp(fs->super, s_mtime, time(NULL));
 		fs->super->s_state &= ~EXT2_VALID_FS;
 		ext2fs_mark_super_dirty(fs);
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err)
 			return translate_error(fs, 0, err);
 	}
@@ -2274,7 +2344,7 @@
 				translate_error(fs, 0, err);
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err)
 			translate_error(fs, 0, err);
 	}
@@ -2297,6 +2367,7 @@
 	 * that the block device will be released before umount(2) returns.
 	 */
 	if (ff->iomap_state == IOMAP_ENABLED) {
+		fuse4fs_flush_cancel(ff);
 		fuse4fs_mmp_cancel(ff);
 		fuse4fs_unmount(ff);
 	}
@@ -2502,6 +2573,7 @@
 	 */
 	fuse4fs_mmp_start(ff);
 	fuse4fs_psi_start(ff);
+	fuse4fs_flush_start(ff);
 
 #if FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
 	/*
@@ -3059,7 +3131,7 @@
 		*flushed = 0;
 	return 0;
 flush:
-	err = ext2fs_flush2(fs, 0);
+	err = fuse4fs_flush(ff, 0);
 	if (err)
 		return translate_error(fs, 0, err);
 
@@ -4888,7 +4960,7 @@
 	if ((fp->flags & O_SYNC) &&
 	    fuse4fs_is_writeable(ff) &&
 	    (fh->open_flags & EXT2_FILE_WRITE)) {
-		err = ext2fs_flush2(fs, EXT2_FLAG_FLUSH_NO_SYNC);
+		err = fuse4fs_flush(ff, EXT2_FLAG_FLUSH_NO_SYNC);
 		if (err)
 			ret = translate_error(fs, fh->ino, err);
 	}
@@ -4919,7 +4991,7 @@
 	fs = fuse4fs_start(ff);
 	/* For now, flush everything, even if it's slow */
 	if (fuse4fs_is_writeable(ff) && fh->open_flags & EXT2_FILE_WRITE) {
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err)
 			ret = translate_error(fs, fh->ino, err);
 	}
@@ -6246,6 +6318,7 @@
 
 	err_printf(ff, "%s.\n", _("shut down requested"));
 
+	fuse4fs_flush_cancel(ff);
 	fuse4fs_mmp_cancel(ff);
 
 	/*
@@ -6254,7 +6327,7 @@
 	 * any of the flags.  Flush whatever is dirty and shut down.
 	 */
 	if (ff->opstate == F4OP_WRITABLE)
-		ext2fs_flush2(fs, 0);
+		fuse4fs_flush(ff, 0);
 	ff->opstate = F4OP_SHUTDOWN;
 	fs->flags &= ~EXT2_FLAG_RW;
 
@@ -6669,7 +6742,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -6705,7 +6778,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -6749,7 +6822,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse4fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -8146,6 +8219,7 @@
 	FUSE4FS_CACHE_SIZE,
 	FUSE4FS_DIRSYNC,
 	FUSE4FS_ERRORS_BEHAVIOR,
+	FUSE4FS_FLUSH_INTERVAL,
 #ifdef HAVE_FUSE_IOMAP
 	FUSE4FS_IOMAP,
 	FUSE4FS_IOMAP_PASSTHROUGH,
@@ -8174,6 +8248,7 @@
 #ifdef HAVE_CLOCK_MONOTONIC
 	FUSE4FS_OPT("timing",		timing,			1),
 #endif
+	FUSE_OPT_KEY("flush_interval=%s", FUSE4FS_FLUSH_INTERVAL),
 #ifdef HAVE_FUSE_IOMAP
 	FUSE4FS_OPT("iomap_cache",	iomap_cache,		1),
 	FUSE4FS_OPT("noiomap_cache",	iomap_cache,		0),
@@ -8257,6 +8332,21 @@
 
 		/* do not pass through to libfuse */
 		return 0;
+	case FUSE4FS_FLUSH_INTERVAL:
+		char *p;
+		unsigned long val;
+
+		errno = 0;
+		val = strtoul(arg + 15, &p, 0);
+		if (p != arg + strlen(arg) || errno || val > UINT_MAX) {
+			fprintf(stderr, "%s: %s.\n", arg,
+				_("Unrecognized flush interval"));
+			return -1;
+		}
+
+		/* do not pass through to libfuse */
+		ff->flush_interval = val;
+		return 0;
 #ifdef HAVE_FUSE_IOMAP
 	case FUSE4FS_IOMAP:
 		if (strcmp(arg, "iomap") == 0 || strcmp(arg + 6, "1") == 0)
@@ -8304,6 +8394,7 @@
 #ifdef HAVE_FUSE_IOMAP
 	"    -o iomap=              0 to disable iomap, 1 to enable iomap\n"
 #endif
+	"    -o flush=<time>        flush dirty metadata on this interval\n"
 	"\n",
 			outargs->argv[0]);
 		if (key == FUSE4FS_HELPFULL) {
@@ -8586,6 +8677,7 @@
 #endif
 		.translate_inums = 1,
 		.write_gdt_on_destroy = 1,
+		.flush_interval = 30,
 #ifdef HAVE_FUSE_SERVICE
 		.bdev_fd = -1,
 		.fusedev_fd = -1,
@@ -8762,6 +8854,7 @@
  _("Mount failed while opening filesystem.  Check dmesg(1) for details."));
 		fflush(orig_stderr);
 	}
+	fuse4fs_flush_destroy(&fctx);
 	fuse4fs_psi_destroy(&fctx);
 	fuse4fs_mmp_destroy(&fctx);
 	fuse4fs_unmount(&fctx);
@@ -8940,6 +9033,7 @@
  _("Remounting read-only due to errors."));
 			ff->opstate = F4OP_READONLY;
 		}
+		fuse4fs_flush_cancel(ff);
 		fuse4fs_mmp_cancel(ff);
 		fs->flags &= ~EXT2_FLAG_RW;
 		break;
diff --git a/misc/fuse2fs.c b/misc/fuse2fs.c
index 7af7ccc..8e646f2 100644
--- a/misc/fuse2fs.c
+++ b/misc/fuse2fs.c
@@ -26,6 +26,7 @@
 #include <sys/sysmacros.h>
 #include <unistd.h>
 #include <ctype.h>
+#include <limits.h>
 #ifdef HAVE_FUSE_LOOPDEV
 # include <fuse_loopdev.h>
 #endif
@@ -310,6 +311,10 @@
 #endif
 	struct psi *mem_psi;
 	struct psi_handler *mem_psi_handler;
+
+	struct bthread *flush_thread;
+	unsigned int flush_interval;
+	double last_flush;
 };
 
 #define FUSE2FS_CHECK_HANDLE(ff, fh) \
@@ -813,6 +818,71 @@
 	fp->fh = (uintptr_t)fh;
 }
 
+static errcode_t fuse2fs_flush(struct fuse2fs *ff, int flags)
+{
+	double last_flush = gettime_monotonic();
+	errcode_t err;
+
+	err = ext2fs_flush2(ff->fs, flags);
+	if (err)
+		return err;
+
+	ff->last_flush = last_flush;
+	return 0;
+}
+
+static inline int fuse2fs_flush_wanted(struct fuse2fs *ff)
+{
+	return ff->fs != NULL && ff->opstate == F2OP_WRITABLE &&
+	       ff->last_flush + ff->flush_interval <= gettime_monotonic();
+}
+
+static void fuse2fs_flush_bthread(void *data)
+{
+	struct fuse2fs *ff = data;
+	ext2_filsys fs;
+	errcode_t err;
+	int ret = 0;
+
+	fs = fuse2fs_start(ff);
+	if (fuse2fs_flush_wanted(ff) && !bthread_cancelled(ff->flush_thread)) {
+		err = fuse2fs_flush(ff, 0);
+		if (err)
+			ret = translate_error(fs, 0, err);
+	}
+	fuse2fs_finish(ff, ret);
+}
+
+static void fuse2fs_flush_start(struct fuse2fs *ff)
+{
+	int ret;
+
+	if (!ff->flush_interval)
+		return;
+
+	ret = bthread_create("fuse2fs_flush", fuse2fs_flush_bthread, ff,
+			     ff->flush_interval, &ff->flush_thread);
+	if (ret) {
+		err_printf(ff, "flusher: %s.\n", error_message(ret));
+		return;
+	}
+
+	ret = bthread_start(ff->flush_thread);
+	if (ret)
+		err_printf(ff, "flusher: %s.\n", error_message(ret));
+}
+
+static void fuse2fs_flush_cancel(struct fuse2fs *ff)
+{
+	if (ff->flush_thread)
+		bthread_cancel(ff->flush_thread);
+}
+
+static void fuse2fs_flush_destroy(struct fuse2fs *ff)
+{
+	bthread_destroy(&ff->flush_thread);
+}
+
 #ifdef HAVE_FUSE_IOMAP
 static inline int fuse2fs_iomap_enabled(const struct fuse2fs *ff)
 {
@@ -1730,7 +1800,7 @@
 		ext2fs_set_tstamp(fs->super, s_mtime, time(NULL));
 		fs->super->s_state &= ~EXT2_VALID_FS;
 		ext2fs_mark_super_dirty(fs);
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err)
 			return translate_error(fs, 0, err);
 	}
@@ -1760,7 +1830,7 @@
 				translate_error(fs, 0, err);
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err)
 			translate_error(fs, 0, err);
 	}
@@ -1783,6 +1853,7 @@
 	 * that the block device will be released before umount(2) returns.
 	 */
 	if (ff->iomap_state == IOMAP_ENABLED) {
+		fuse2fs_flush_cancel(ff);
 		fuse2fs_mmp_cancel(ff);
 		fuse2fs_unmount(ff);
 	}
@@ -2009,6 +2080,7 @@
 	 */
 	fuse2fs_mmp_start(ff);
 	fuse2fs_psi_start(ff);
+	fuse2fs_flush_start(ff);
 
 #if FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
 	/*
@@ -2539,7 +2611,7 @@
 		*flushed = 0;
 	return 0;
 flush:
-	err = ext2fs_flush2(fs, 0);
+	err = fuse2fs_flush(ff, 0);
 	if (err)
 		return translate_error(fs, 0, err);
 
@@ -4338,7 +4410,7 @@
 	if ((fp->flags & O_SYNC) &&
 	    fuse2fs_is_writeable(ff) &&
 	    (fh->open_flags & EXT2_FILE_WRITE)) {
-		err = ext2fs_flush2(fs, EXT2_FLAG_FLUSH_NO_SYNC);
+		err = fuse2fs_flush(ff, EXT2_FLAG_FLUSH_NO_SYNC);
 		if (err)
 			ret = translate_error(fs, fh->ino, err);
 	}
@@ -4367,7 +4439,7 @@
 	fs = fuse2fs_start(ff);
 	/* For now, flush everything, even if it's slow */
 	if (fuse2fs_is_writeable(ff) && fh->open_flags & EXT2_FILE_WRITE) {
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err)
 			ret = translate_error(fs, fh->ino, err);
 	}
@@ -5477,6 +5549,7 @@
 
 	err_printf(ff, "%s.\n", _("shut down requested"));
 
+	fuse2fs_flush_cancel(ff);
 	fuse2fs_mmp_cancel(ff);
 
 	/*
@@ -5485,7 +5558,7 @@
 	 * any of the flags.  Flush whatever is dirty and shut down.
 	 */
 	if (ff->opstate == F2OP_WRITABLE)
-		ext2fs_flush2(fs, 0);
+		fuse2fs_flush(ff, 0);
 	ff->opstate = F2OP_SHUTDOWN;
 	fs->flags &= ~EXT2_FLAG_RW;
 
@@ -5883,7 +5956,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -5918,7 +5991,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -5960,7 +6033,7 @@
 			goto out_unlock;
 		}
 
-		err = ext2fs_flush2(fs, 0);
+		err = fuse2fs_flush(ff, 0);
 		if (err) {
 			ret = translate_error(fs, 0, err);
 			goto out_unlock;
@@ -7339,6 +7412,7 @@
 	FUSE2FS_CACHE_SIZE,
 	FUSE2FS_DIRSYNC,
 	FUSE2FS_ERRORS_BEHAVIOR,
+	FUSE2FS_FLUSH_INTERVAL,
 #ifdef HAVE_FUSE_IOMAP
 	FUSE2FS_IOMAP,
 	FUSE2FS_IOMAP_PASSTHROUGH,
@@ -7367,6 +7441,7 @@
 #ifdef HAVE_CLOCK_MONOTONIC
 	FUSE2FS_OPT("timing",		timing,			1),
 #endif
+	FUSE_OPT_KEY("flush_interval=%s", FUSE2FS_FLUSH_INTERVAL),
 #ifdef HAVE_FUSE_IOMAP
 	FUSE2FS_OPT("iomap_cache",	iomap_cache,		1),
 	FUSE2FS_OPT("noiomap_cache",	iomap_cache,		0),
@@ -7450,6 +7525,21 @@
 
 		/* do not pass through to libfuse */
 		return 0;
+	case FUSE2FS_FLUSH_INTERVAL:
+		char *p;
+		unsigned long val;
+
+		errno = 0;
+		val = strtoul(arg + 15, &p, 0);
+		if (p != arg + strlen(arg) || errno || val > UINT_MAX) {
+			fprintf(stderr, "%s: %s.\n", arg,
+				_("Unrecognized flush interval"));
+			return -1;
+		}
+
+		/* do not pass through to libfuse */
+		ff->flush_interval = val;
+		return 0;
 #ifdef HAVE_FUSE_IOMAP
 	case FUSE2FS_IOMAP:
 		if (strcmp(arg, "iomap") == 0 || strcmp(arg + 6, "1") == 0)
@@ -7497,6 +7587,7 @@
 #ifdef HAVE_FUSE_IOMAP
 	"    -o iomap=              0 to disable iomap, 1 to enable iomap\n"
 #endif
+	"    -o flush=<time>        flush dirty metadata on this interval\n"
 	"\n",
 			outargs->argv[0]);
 		if (key == FUSE2FS_HELPFULL) {
@@ -7657,6 +7748,7 @@
 		.loop_fd = -1,
 #endif
 		.write_gdt_on_destroy = 1,
+		.flush_interval = 30,
 	};
 	errcode_t err;
 	FILE *orig_stderr = stderr;
@@ -7790,6 +7882,7 @@
  _("Mount failed while opening filesystem.  Check dmesg(1) for details."));
 		fflush(orig_stderr);
 	}
+	fuse2fs_flush_destroy(&fctx);
 	fuse2fs_psi_destroy(&fctx);
 	fuse2fs_mmp_destroy(&fctx);
 	fuse2fs_unmount(&fctx);
@@ -7967,6 +8060,7 @@
  _("Remounting read-only due to errors."));
 			ff->opstate = F2OP_READONLY;
 		}
+		fuse2fs_flush_cancel(ff);
 		fuse2fs_mmp_cancel(ff);
 		fs->flags &= ~EXT2_FLAG_RW;
 		break;