All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH RFC v3] bcache: flush backing device before cleaning the writeback dirty keys
@ 2026-05-27  8:27 Zhou Jifeng
  2026-07-08  3:56 ` Coly Li
  0 siblings, 1 reply; 5+ messages in thread
From: Zhou Jifeng @ 2026-05-27  8:27 UTC (permalink / raw)
  To: colyli, linux-bcache; +Cc: zhoujifeng

Coly Li proposed an RFC that collects written-back keys into an on-stack
per-pass batch (struct writeback_batch) inside read_dirty() and issues
one REQ_PREFLUSH per pass (~500 IOs, bounded by KEYBUF_NR).  This patch
builds directly on that design: the core data structures, function names
(writeback_flush(), writeback_finish_batch()), and naming convention all
follow Coly's RFC.

The extension is a persistent preallocated batch (dc->writeback_batch)
that survives across read_dirty() passes, controlled by a new sysfs knob
writeback_flush_interval:

  writeback_flush_interval = 0  (default):
    writeback_finish_batch() is triggered after every read_dirty() pass
    (because !0 is always true), reproducing Coly's one-flush-per-pass
    behavior exactly.  In this mode keys never accumulate between passes,
    so bch_writeback_finish_batch() (the GC hook) skips each device —
    there is nothing pending to flush.

  writeback_flush_interval > 0  (cross-pass batching):
    writeback_finish_batch() fires only when the accumulated key count
    reaches the threshold.  The GC hook flushes pending keys before
    btree_gc_start() so GC can reclaim their bucket space.

Signed-off-by: Coly Li <colyli@fygo.io>
Signed-off-by: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>
---
 drivers/md/bcache/bcache.h    |  20 +++++
 drivers/md/bcache/btree.c     |   2 +
 drivers/md/bcache/btree.h     |   1 +
 drivers/md/bcache/sysfs.c     |   6 ++
 drivers/md/bcache/writeback.c | 137 ++++++++++++++++++++++++++++------
 drivers/md/bcache/writeback.h |   1 +
 6 files changed, 145 insertions(+), 22 deletions(-)

diff --git a/drivers/md/bcache/bcache.h b/drivers/md/bcache/bcache.h
index ec9ff9715..03e401575 100644
--- a/drivers/md/bcache/bcache.h
+++ b/drivers/md/bcache/bcache.h
@@ -247,6 +247,24 @@ struct keybuf {
 	DECLARE_ARRAY_ALLOCATOR(struct keybuf_key, freelist, KEYBUF_NR);
 };
 
+struct writeback_bkey {
+	BKEY_PADDED(key);
+	struct list_head	list;
+};
+
+#define WRITEBACK_FLUSH_INTERVAL_DEFAULT	0
+#define WRITEBACK_FLUSH_INTERVAL_MIN		0
+#define WRITEBACK_FLUSH_INTERVAL_MAX		50000
+
+struct writeback_batch {
+	spinlock_t		lock;
+	u32			count;
+	struct list_head	keys;
+
+	DECLARE_ARRAY_ALLOCATOR(struct writeback_bkey, pool,
+				WRITEBACK_FLUSH_INTERVAL_MAX);
+};
+
 struct bcache_device {
 	struct closure		cl;
 
@@ -347,6 +365,8 @@ struct cached_dev {
 	struct workqueue_struct	*writeback_write_wq;
 
 	struct keybuf		writeback_keys;
+	struct writeback_batch	writeback_batch;
+	unsigned int		writeback_flush_interval;
 
 	struct task_struct	*status_update_thread;
 	/*
diff --git a/drivers/md/bcache/btree.c b/drivers/md/bcache/btree.c
index 27a129d47..d10c03f60 100644
--- a/drivers/md/bcache/btree.c
+++ b/drivers/md/bcache/btree.c
@@ -25,6 +25,7 @@
 #include "btree.h"
 #include "debug.h"
 #include "extents.h"
+#include "writeback.h"
 
 #include <linux/slab.h>
 #include <linux/bitops.h>
@@ -1837,6 +1838,7 @@ static void bch_btree_gc(struct cache_set *c)
 	closure_init_stack(&writes);
 	bch_btree_op_init(&op, SHRT_MAX);
 
+	bch_writeback_finish_batch(c);
 	btree_gc_start(c);
 
 	/* if CACHE_SET_IO_DISABLE set, gc thread should stop too */
diff --git a/drivers/md/bcache/btree.h b/drivers/md/bcache/btree.h
index 45d64b541..701b0009c 100644
--- a/drivers/md/bcache/btree.h
+++ b/drivers/md/bcache/btree.h
@@ -414,4 +414,5 @@ struct keybuf_key *bch_keybuf_next_rescan(struct cache_set *c,
 					  struct bkey *end,
 					  keybuf_pred_fn *pred);
 void bch_update_bucket_in_use(struct cache_set *c, struct gc_stat *stats);
+void bkey_put(struct cache_set *c, struct bkey *k);
 #endif
diff --git a/drivers/md/bcache/sysfs.c b/drivers/md/bcache/sysfs.c
index cfac56caa..7a87c64d7 100644
--- a/drivers/md/bcache/sysfs.c
+++ b/drivers/md/bcache/sysfs.c
@@ -150,6 +150,7 @@ rw_attribute(copy_gc_enabled);
 rw_attribute(idle_max_writeback_rate);
 rw_attribute(gc_after_writeback);
 rw_attribute(size);
+rw_attribute(writeback_flush_interval);
 
 static ssize_t bch_snprint_string_list(char *buf,
 				       size_t size,
@@ -212,6 +213,7 @@ SHOW(__bch_cached_dev)
 	var_print(writeback_rate_fp_term_mid);
 	var_print(writeback_rate_fp_term_high);
 	var_print(writeback_rate_minimum);
+	var_print(writeback_flush_interval);
 
 	if (attr == &sysfs_writeback_rate_debug) {
 		char rate[20];
@@ -353,6 +355,9 @@ STORE(__cached_dev)
 
 	sysfs_strtoul_clamp(io_error_limit, dc->error_limit, 0, INT_MAX);
 
+	sysfs_strtoul_clamp(writeback_flush_interval, dc->writeback_flush_interval,
+			    WRITEBACK_FLUSH_INTERVAL_MIN, WRITEBACK_FLUSH_INTERVAL_MAX);
+
 	if (attr == &sysfs_io_disable) {
 		int v = strtoul_or_return(buf);
 
@@ -540,6 +545,7 @@ static struct attribute *bch_cached_dev_attrs[] = {
 #endif
 	&sysfs_backing_dev_name,
 	&sysfs_backing_dev_uuid,
+	&sysfs_writeback_flush_interval,
 	NULL
 };
 ATTRIBUTE_GROUPS(bch_cached_dev);
diff --git a/drivers/md/bcache/writeback.c b/drivers/md/bcache/writeback.c
index 4b237074f..f36b515d6 100644
--- a/drivers/md/bcache/writeback.c
+++ b/drivers/md/bcache/writeback.c
@@ -348,39 +348,122 @@ static CLOSURE_CALLBACK(dirty_io_destructor)
 	kfree(io);
 }
 
-static CLOSURE_CALLBACK(write_dirty_finish)
+static void writeback_batch_init(struct writeback_batch *wb)
 {
-	closure_type(io, struct dirty_io, cl);
-	struct keybuf_key *w = io->bio.bi_private;
-	struct cached_dev *dc = io->dc;
+	wb->count = 0;
+	spin_lock_init(&wb->lock);
+	array_allocator_init(&wb->pool);
+	INIT_LIST_HEAD(&wb->keys);
+}
 
-	bio_free_pages(&io->bio);
+static void writeback_add_key(struct cached_dev *dc, struct bkey *key)
+{
+	struct writeback_bkey *bk;
+	unsigned int i;
 
-	/* This is kind of a dumb way of signalling errors. */
-	if (KEY_DIRTY(&w->key)) {
-		int ret;
-		unsigned int i;
-		struct keylist keys;
+	spin_lock(&dc->writeback_batch.lock);
+	bk = array_alloc(&dc->writeback_batch.pool);
+	if (!bk) {
+		spin_unlock(&dc->writeback_batch.lock);
+		return;
+	}
+
+	for (i = 0; i < KEY_PTRS(key); i++)
+		atomic_inc(&PTR_BUCKET(dc->disk.c, key, i)->pin);
 
-		bch_keylist_init(&keys);
+	bkey_copy(&bk->key, key);
+	INIT_LIST_HEAD(&bk->list);
+	list_add_tail(&bk->list, &dc->writeback_batch.keys);
+	dc->writeback_batch.count++;
+	spin_unlock(&dc->writeback_batch.lock);
+}
 
-		bkey_copy(keys.top, &w->key);
-		SET_KEY_DIRTY(keys.top, false);
-		bch_keylist_push(&keys);
+static int writeback_flush(struct cached_dev *dc)
+{
+	struct bio bio;
+	int ret;
 
-		for (i = 0; i < KEY_PTRS(&w->key); i++)
-			atomic_inc(&PTR_BUCKET(dc->disk.c, &w->key, i)->pin);
+	bio_init(&bio, dc->bdev, NULL, 0, REQ_OP_WRITE | REQ_PREFLUSH);
 
-		ret = bch_btree_insert(dc->disk.c, &keys, NULL, &w->key);
+	ret = submit_bio_wait(&bio);
+	if (ret)
+		bch_count_backing_io_errors(dc, &bio);
 
-		if (ret)
-			trace_bcache_writeback_collision(&w->key);
+	bio_uninit(&bio);
+	return ret;
+}
 
-		atomic_long_inc(ret
-				? &dc->disk.c->writeback_keys_failed
-				: &dc->disk.c->writeback_keys_done);
+static void writeback_finish_batch(struct cached_dev *dc)
+{
+	struct writeback_bkey *bk, *tmp;
+	struct keylist keys;
+	LIST_HEAD(local);
+	int flush_ret, ret;
+
+	spin_lock(&dc->writeback_batch.lock);
+	if (list_empty(&dc->writeback_batch.keys)) {
+		spin_unlock(&dc->writeback_batch.lock);
+		return;
+	}
+	list_splice_init(&dc->writeback_batch.keys, &local);
+	dc->writeback_batch.count = 0;
+	spin_unlock(&dc->writeback_batch.lock);
+
+	flush_ret = writeback_flush(dc);
+
+	list_for_each_entry(bk, &local, list) {
+		if (flush_ret == 0) {
+			bch_keylist_init(&keys);
+			bkey_copy(keys.top, &bk->key);
+			SET_KEY_DIRTY(keys.top, false);
+			bch_keylist_push(&keys);
+			ret = bch_btree_insert(dc->disk.c, &keys, NULL,
+					       &bk->key);
+			if (ret)
+				trace_bcache_writeback_collision(&bk->key);
+			atomic_long_inc(ret
+					? &dc->disk.c->writeback_keys_failed
+					: &dc->disk.c->writeback_keys_done);
+		} else {
+			bkey_put(dc->disk.c, &bk->key);
+		}
 	}
 
+	spin_lock(&dc->writeback_batch.lock);
+	list_for_each_entry_safe(bk, tmp, &local, list) {
+		list_del(&bk->list);
+		array_free(&dc->writeback_batch.pool, bk);
+	}
+	spin_unlock(&dc->writeback_batch.lock);
+}
+
+void bch_writeback_finish_batch(struct cache_set *c)
+{
+	unsigned int i;
+
+	for (i = 0; i < c->devices_max_used; i++) {
+		struct bcache_device *d = c->devices[i];
+		struct cached_dev *dc;
+
+		if (!d || UUID_FLASH_ONLY(&c->uuids[i]))
+			continue;
+		dc = container_of(d, struct cached_dev, disk);
+		if (dc->writeback_flush_interval)
+			writeback_finish_batch(dc);
+	}
+}
+
+static CLOSURE_CALLBACK(write_dirty_finish)
+{
+	closure_type(io, struct dirty_io, cl);
+	struct keybuf_key *w = io->bio.bi_private;
+	struct cached_dev *dc = io->dc;
+
+	bio_free_pages(&io->bio);
+
+	if (KEY_DIRTY(&w->key))
+		writeback_add_key(dc, &w->key);
+
 	bch_keybuf_del(&dc->writeback_keys, w);
 	up(&dc->in_flight);
 
@@ -818,9 +901,15 @@ static int bch_writeback_thread(void *arg)
 
 		read_dirty(dc);
 
+		if (!dc->writeback_flush_interval ||
+		    dc->writeback_batch.count >= dc->writeback_flush_interval)
+			writeback_finish_batch(dc);
+
 		if (searched_full_index) {
 			unsigned int delay = dc->writeback_delay * HZ;
 
+			writeback_finish_batch(dc);
+
 			while (delay &&
 			       !kthread_should_stop() &&
 			       !test_bit(CACHE_SET_IO_DISABLE, &c->flags) &&
@@ -831,6 +920,8 @@ static int bch_writeback_thread(void *arg)
 		}
 	}
 
+	writeback_finish_batch(dc);
+
 	if (dc->writeback_write_wq)
 		destroy_workqueue(dc->writeback_write_wq);
 
@@ -1049,6 +1140,7 @@ void bch_cached_dev_writeback_init(struct cached_dev *dc)
 	sema_init(&dc->in_flight, 64);
 	init_rwsem(&dc->writeback_lock);
 	bch_keybuf_init(&dc->writeback_keys);
+	writeback_batch_init(&dc->writeback_batch);
 
 	dc->writeback_metadata		= true;
 	dc->writeback_running		= false;
@@ -1064,6 +1156,7 @@ void bch_cached_dev_writeback_init(struct cached_dev *dc)
 	dc->writeback_rate_fp_term_mid = 10;
 	dc->writeback_rate_fp_term_high = 1000;
 	dc->writeback_rate_i_term_inverse = 10000;
+	dc->writeback_flush_interval	= WRITEBACK_FLUSH_INTERVAL_DEFAULT;
 
 	/* For dc->writeback_lock contention in update_writeback_rate() */
 	dc->rate_update_retry = 0;
diff --git a/drivers/md/bcache/writeback.h b/drivers/md/bcache/writeback.h
index 31df71695..12b4c4420 100644
--- a/drivers/md/bcache/writeback.h
+++ b/drivers/md/bcache/writeback.h
@@ -151,5 +151,6 @@ void bcache_dev_sectors_dirty_add(struct cache_set *c, unsigned int inode,
 void bch_sectors_dirty_init(struct bcache_device *d);
 void bch_cached_dev_writeback_init(struct cached_dev *dc);
 int bch_cached_dev_writeback_start(struct cached_dev *dc);
+void bch_writeback_finish_batch(struct cache_set *c);
 
 #endif
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC v3] bcache: flush backing device before cleaning the writeback dirty keys
  2026-05-27  8:27 [PATCH RFC v3] bcache: flush backing device before cleaning the writeback dirty keys Zhou Jifeng
@ 2026-07-08  3:56 ` Coly Li
  2026-07-09  6:23   ` [PATCH RFC v4] " Zhou Jifeng
  0 siblings, 1 reply; 5+ messages in thread
From: Coly Li @ 2026-07-08  3:56 UTC (permalink / raw)
  To: Zhou Jifeng; +Cc: linux-bcache

On Wed, May 27, 2026 at 01:27:50AM +0800, Zhou Jifeng wrote:
> Coly Li proposed an RFC that collects written-back keys into an on-stack
> per-pass batch (struct writeback_batch) inside read_dirty() and issues
> one REQ_PREFLUSH per pass (~500 IOs, bounded by KEYBUF_NR).  This patch
> builds directly on that design: the core data structures, function names
> (writeback_flush(), writeback_finish_batch()), and naming convention all
> follow Coly's RFC.
> 
> The extension is a persistent preallocated batch (dc->writeback_batch)
> that survives across read_dirty() passes, controlled by a new sysfs knob
> writeback_flush_interval:
> 
>   writeback_flush_interval = 0  (default):
>     writeback_finish_batch() is triggered after every read_dirty() pass
>     (because !0 is always true), reproducing Coly's one-flush-per-pass
>     behavior exactly.  In this mode keys never accumulate between passes,
>     so bch_writeback_finish_batch() (the GC hook) skips each device —
>     there is nothing pending to flush.
> 
>   writeback_flush_interval > 0  (cross-pass batching):
>     writeback_finish_batch() fires only when the accumulated key count
>     reaches the threshold.  The GC hook flushes pending keys before
>     btree_gc_start() so GC can reclaim their bucket space.
> 
> Signed-off-by: Coly Li <colyli@fygo.io>
> Signed-off-by: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>

Hi Jifeng,

I tried to review your change, but failed. Your patch mixed the add/delete
lines together, it is not easy to assemble the code context in my brain.
Further more, I am not able to place my review comments in proper location.

Can you re-generate the patch again? Keep the existed functions in their
original locations, and add your new code together.

I tried, after the re-format, the change is clean and much easier to
review. If you can regenerate the patch in this more clear why, that will
be very helpful.

Thanks.

Coly Li

> ---
>  drivers/md/bcache/bcache.h    |  20 +++++
>  drivers/md/bcache/btree.c     |   2 +
>  drivers/md/bcache/btree.h     |   1 +
>  drivers/md/bcache/sysfs.c     |   6 ++
>  drivers/md/bcache/writeback.c | 137 ++++++++++++++++++++++++++++------
>  drivers/md/bcache/writeback.h |   1 +
>  6 files changed, 145 insertions(+), 22 deletions(-)
> 
> diff --git a/drivers/md/bcache/bcache.h b/drivers/md/bcache/bcache.h
> index ec9ff9715..03e401575 100644
> --- a/drivers/md/bcache/bcache.h
> +++ b/drivers/md/bcache/bcache.h
> @@ -247,6 +247,24 @@ struct keybuf {
>  	DECLARE_ARRAY_ALLOCATOR(struct keybuf_key, freelist, KEYBUF_NR);
>  };
>  
> +struct writeback_bkey {
> +	BKEY_PADDED(key);
> +	struct list_head	list;
> +};
> +
> +#define WRITEBACK_FLUSH_INTERVAL_DEFAULT	0
> +#define WRITEBACK_FLUSH_INTERVAL_MIN		0
> +#define WRITEBACK_FLUSH_INTERVAL_MAX		50000
> +
> +struct writeback_batch {
> +	spinlock_t		lock;
> +	u32			count;
> +	struct list_head	keys;
> +
> +	DECLARE_ARRAY_ALLOCATOR(struct writeback_bkey, pool,
> +				WRITEBACK_FLUSH_INTERVAL_MAX);
> +};
> +
>  struct bcache_device {
>  	struct closure		cl;
>  
> @@ -347,6 +365,8 @@ struct cached_dev {
>  	struct workqueue_struct	*writeback_write_wq;
>  
>  	struct keybuf		writeback_keys;
> +	struct writeback_batch	writeback_batch;
> +	unsigned int		writeback_flush_interval;
>  
>  	struct task_struct	*status_update_thread;
>  	/*
> diff --git a/drivers/md/bcache/btree.c b/drivers/md/bcache/btree.c
> index 27a129d47..d10c03f60 100644
> --- a/drivers/md/bcache/btree.c
> +++ b/drivers/md/bcache/btree.c
> @@ -25,6 +25,7 @@
>  #include "btree.h"
>  #include "debug.h"
>  #include "extents.h"
> +#include "writeback.h"
>  
>  #include <linux/slab.h>
>  #include <linux/bitops.h>
> @@ -1837,6 +1838,7 @@ static void bch_btree_gc(struct cache_set *c)
>  	closure_init_stack(&writes);
>  	bch_btree_op_init(&op, SHRT_MAX);
>  
> +	bch_writeback_finish_batch(c);
>  	btree_gc_start(c);
>  
>  	/* if CACHE_SET_IO_DISABLE set, gc thread should stop too */
> diff --git a/drivers/md/bcache/btree.h b/drivers/md/bcache/btree.h
> index 45d64b541..701b0009c 100644
> --- a/drivers/md/bcache/btree.h
> +++ b/drivers/md/bcache/btree.h
> @@ -414,4 +414,5 @@ struct keybuf_key *bch_keybuf_next_rescan(struct cache_set *c,
>  					  struct bkey *end,
>  					  keybuf_pred_fn *pred);
>  void bch_update_bucket_in_use(struct cache_set *c, struct gc_stat *stats);
> +void bkey_put(struct cache_set *c, struct bkey *k);
>  #endif
> diff --git a/drivers/md/bcache/sysfs.c b/drivers/md/bcache/sysfs.c
> index cfac56caa..7a87c64d7 100644
> --- a/drivers/md/bcache/sysfs.c
> +++ b/drivers/md/bcache/sysfs.c
> @@ -150,6 +150,7 @@ rw_attribute(copy_gc_enabled);
>  rw_attribute(idle_max_writeback_rate);
>  rw_attribute(gc_after_writeback);
>  rw_attribute(size);
> +rw_attribute(writeback_flush_interval);
>  
>  static ssize_t bch_snprint_string_list(char *buf,
>  				       size_t size,
> @@ -212,6 +213,7 @@ SHOW(__bch_cached_dev)
>  	var_print(writeback_rate_fp_term_mid);
>  	var_print(writeback_rate_fp_term_high);
>  	var_print(writeback_rate_minimum);
> +	var_print(writeback_flush_interval);
>  
>  	if (attr == &sysfs_writeback_rate_debug) {
>  		char rate[20];
> @@ -353,6 +355,9 @@ STORE(__cached_dev)
>  
>  	sysfs_strtoul_clamp(io_error_limit, dc->error_limit, 0, INT_MAX);
>  
> +	sysfs_strtoul_clamp(writeback_flush_interval, dc->writeback_flush_interval,
> +			    WRITEBACK_FLUSH_INTERVAL_MIN, WRITEBACK_FLUSH_INTERVAL_MAX);
> +
>  	if (attr == &sysfs_io_disable) {
>  		int v = strtoul_or_return(buf);
>  
> @@ -540,6 +545,7 @@ static struct attribute *bch_cached_dev_attrs[] = {
>  #endif
>  	&sysfs_backing_dev_name,
>  	&sysfs_backing_dev_uuid,
> +	&sysfs_writeback_flush_interval,
>  	NULL
>  };
>  ATTRIBUTE_GROUPS(bch_cached_dev);
> diff --git a/drivers/md/bcache/writeback.c b/drivers/md/bcache/writeback.c
> index 4b237074f..f36b515d6 100644
> --- a/drivers/md/bcache/writeback.c
> +++ b/drivers/md/bcache/writeback.c
> @@ -348,39 +348,122 @@ static CLOSURE_CALLBACK(dirty_io_destructor)
>  	kfree(io);
>  }
>  
> -static CLOSURE_CALLBACK(write_dirty_finish)
> +static void writeback_batch_init(struct writeback_batch *wb)
>  {
> -	closure_type(io, struct dirty_io, cl);
> -	struct keybuf_key *w = io->bio.bi_private;
> -	struct cached_dev *dc = io->dc;
> +	wb->count = 0;
> +	spin_lock_init(&wb->lock);
> +	array_allocator_init(&wb->pool);
> +	INIT_LIST_HEAD(&wb->keys);
> +}
>  
> -	bio_free_pages(&io->bio);
> +static void writeback_add_key(struct cached_dev *dc, struct bkey *key)
> +{
> +	struct writeback_bkey *bk;
> +	unsigned int i;
>  
> -	/* This is kind of a dumb way of signalling errors. */
> -	if (KEY_DIRTY(&w->key)) {
> -		int ret;
> -		unsigned int i;
> -		struct keylist keys;
> +	spin_lock(&dc->writeback_batch.lock);
> +	bk = array_alloc(&dc->writeback_batch.pool);
> +	if (!bk) {
> +		spin_unlock(&dc->writeback_batch.lock);
> +		return;
> +	}
> +
> +	for (i = 0; i < KEY_PTRS(key); i++)
> +		atomic_inc(&PTR_BUCKET(dc->disk.c, key, i)->pin);
>  
> -		bch_keylist_init(&keys);
> +	bkey_copy(&bk->key, key);
> +	INIT_LIST_HEAD(&bk->list);
> +	list_add_tail(&bk->list, &dc->writeback_batch.keys);
> +	dc->writeback_batch.count++;
> +	spin_unlock(&dc->writeback_batch.lock);
> +}
>  
> -		bkey_copy(keys.top, &w->key);
> -		SET_KEY_DIRTY(keys.top, false);
> -		bch_keylist_push(&keys);
> +static int writeback_flush(struct cached_dev *dc)
> +{
> +	struct bio bio;
> +	int ret;
>  
> -		for (i = 0; i < KEY_PTRS(&w->key); i++)
> -			atomic_inc(&PTR_BUCKET(dc->disk.c, &w->key, i)->pin);
> +	bio_init(&bio, dc->bdev, NULL, 0, REQ_OP_WRITE | REQ_PREFLUSH);
>  
> -		ret = bch_btree_insert(dc->disk.c, &keys, NULL, &w->key);
> +	ret = submit_bio_wait(&bio);
> +	if (ret)
> +		bch_count_backing_io_errors(dc, &bio);
>  
> -		if (ret)
> -			trace_bcache_writeback_collision(&w->key);
> +	bio_uninit(&bio);
> +	return ret;
> +}
>  
> -		atomic_long_inc(ret
> -				? &dc->disk.c->writeback_keys_failed
> -				: &dc->disk.c->writeback_keys_done);
> +static void writeback_finish_batch(struct cached_dev *dc)
> +{
> +	struct writeback_bkey *bk, *tmp;
> +	struct keylist keys;
> +	LIST_HEAD(local);
> +	int flush_ret, ret;
> +
> +	spin_lock(&dc->writeback_batch.lock);
> +	if (list_empty(&dc->writeback_batch.keys)) {
> +		spin_unlock(&dc->writeback_batch.lock);
> +		return;
> +	}
> +	list_splice_init(&dc->writeback_batch.keys, &local);
> +	dc->writeback_batch.count = 0;
> +	spin_unlock(&dc->writeback_batch.lock);
> +
> +	flush_ret = writeback_flush(dc);
> +
> +	list_for_each_entry(bk, &local, list) {
> +		if (flush_ret == 0) {
> +			bch_keylist_init(&keys);
> +			bkey_copy(keys.top, &bk->key);
> +			SET_KEY_DIRTY(keys.top, false);
> +			bch_keylist_push(&keys);
> +			ret = bch_btree_insert(dc->disk.c, &keys, NULL,
> +					       &bk->key);
> +			if (ret)
> +				trace_bcache_writeback_collision(&bk->key);
> +			atomic_long_inc(ret
> +					? &dc->disk.c->writeback_keys_failed
> +					: &dc->disk.c->writeback_keys_done);
> +		} else {
> +			bkey_put(dc->disk.c, &bk->key);
> +		}
>  	}
>  
> +	spin_lock(&dc->writeback_batch.lock);
> +	list_for_each_entry_safe(bk, tmp, &local, list) {
> +		list_del(&bk->list);
> +		array_free(&dc->writeback_batch.pool, bk);
> +	}
> +	spin_unlock(&dc->writeback_batch.lock);
> +}
> +
> +void bch_writeback_finish_batch(struct cache_set *c)
> +{
> +	unsigned int i;
> +
> +	for (i = 0; i < c->devices_max_used; i++) {
> +		struct bcache_device *d = c->devices[i];
> +		struct cached_dev *dc;
> +
> +		if (!d || UUID_FLASH_ONLY(&c->uuids[i]))
> +			continue;
> +		dc = container_of(d, struct cached_dev, disk);
> +		if (dc->writeback_flush_interval)
> +			writeback_finish_batch(dc);
> +	}
> +}
> +
> +static CLOSURE_CALLBACK(write_dirty_finish)
> +{
> +	closure_type(io, struct dirty_io, cl);
> +	struct keybuf_key *w = io->bio.bi_private;
> +	struct cached_dev *dc = io->dc;
> +
> +	bio_free_pages(&io->bio);
> +
> +	if (KEY_DIRTY(&w->key))
> +		writeback_add_key(dc, &w->key);
> +
>  	bch_keybuf_del(&dc->writeback_keys, w);
>  	up(&dc->in_flight);
>  
> @@ -818,9 +901,15 @@ static int bch_writeback_thread(void *arg)
>  
>  		read_dirty(dc);
>  
> +		if (!dc->writeback_flush_interval ||
> +		    dc->writeback_batch.count >= dc->writeback_flush_interval)
> +			writeback_finish_batch(dc);
> +
>  		if (searched_full_index) {
>  			unsigned int delay = dc->writeback_delay * HZ;
>  
> +			writeback_finish_batch(dc);
> +
>  			while (delay &&
>  			       !kthread_should_stop() &&
>  			       !test_bit(CACHE_SET_IO_DISABLE, &c->flags) &&
> @@ -831,6 +920,8 @@ static int bch_writeback_thread(void *arg)
>  		}
>  	}
>  
> +	writeback_finish_batch(dc);
> +
>  	if (dc->writeback_write_wq)
>  		destroy_workqueue(dc->writeback_write_wq);
>  
> @@ -1049,6 +1140,7 @@ void bch_cached_dev_writeback_init(struct cached_dev *dc)
>  	sema_init(&dc->in_flight, 64);
>  	init_rwsem(&dc->writeback_lock);
>  	bch_keybuf_init(&dc->writeback_keys);
> +	writeback_batch_init(&dc->writeback_batch);
>  
>  	dc->writeback_metadata		= true;
>  	dc->writeback_running		= false;
> @@ -1064,6 +1156,7 @@ void bch_cached_dev_writeback_init(struct cached_dev *dc)
>  	dc->writeback_rate_fp_term_mid = 10;
>  	dc->writeback_rate_fp_term_high = 1000;
>  	dc->writeback_rate_i_term_inverse = 10000;
> +	dc->writeback_flush_interval	= WRITEBACK_FLUSH_INTERVAL_DEFAULT;
>  
>  	/* For dc->writeback_lock contention in update_writeback_rate() */
>  	dc->rate_update_retry = 0;
> diff --git a/drivers/md/bcache/writeback.h b/drivers/md/bcache/writeback.h
> index 31df71695..12b4c4420 100644
> --- a/drivers/md/bcache/writeback.h
> +++ b/drivers/md/bcache/writeback.h
> @@ -151,5 +151,6 @@ void bcache_dev_sectors_dirty_add(struct cache_set *c, unsigned int inode,
>  void bch_sectors_dirty_init(struct bcache_device *d);
>  void bch_cached_dev_writeback_init(struct cached_dev *dc);
>  int bch_cached_dev_writeback_start(struct cached_dev *dc);
> +void bch_writeback_finish_batch(struct cache_set *c);
>  
>  #endif
> -- 
> 2.34.1
>

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC v4] bcache: flush backing device before cleaning the writeback dirty keys
  2026-07-08  3:56 ` Coly Li
@ 2026-07-09  6:23   ` Zhou Jifeng
  2026-07-09  6:30     ` Coly Li
  0 siblings, 1 reply; 5+ messages in thread
From: Zhou Jifeng @ 2026-07-09  6:23 UTC (permalink / raw)
  To: colyli; +Cc: linux-bcache, zhoujifeng

From: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>

Currently, when writeback dirty data completes, write_dirty_finish()
immediately inserts the clean key into the btree.  If a power failure
occurs after the clean-key insert but before the backing device has
flushed its volatile write cache, the btree contains a clean key that
points at backing data that never reached stable media.  On the next
read, bcache serves stale data from the backing device.

Fix this by deferring the clean-key btree insert until after an
explicit backing-device flush (REQ_PREFLUSH).  Written keys are first
parked in a new rbtree (writeback_flush_pending) keyed by START_KEY.
After enough read_dirty() passes, a full btree scan, or a wrap-around
in refill_dirty(), writeback_finish_batch() issues a synchronous
backing-device flush, and only on success inserts the clean keys into
the btree and removes them from the pending tree.  On flush failure
the entries are re-inserted into the current tree for a later retry.

Key changes:

- Introduce struct writeback_pending (separate kmalloc'd allocation,
  decoupled from the dirty_io closure so the closure can be freed
  immediately and the pending entry lives until the flush).

- Add __writeback_pending_insert() — inserts into the START_KEY-
  ordered rbtree with overlap detection.  The rbtree is also used as
  an interval tree by bch_writeback_drop_pending() for front-end
  write overlap queries; that search is correct only because dirty
  btree keys are pairwise disjoint, an invariant enforced at insert
  time.

- writeback_add_to_pending() parks a key after successful writeback
  IO.  Uses GFP_NOIO because the caller runs on the WQ_MEM_RECLAIM
  writeback workqueue.  kmalloc failure silently drops the key (it
  stays dirty and is re-selected by the next scan).

- writeback_finish_batch() detaches the entire pending tree into a
  local list under spinlock, issues writeback_flush(), then either
  cleans the keys (on success) or re-inserts them (on failure).

- writeback_flush() open-codes blkdev_issue_flush() to call
  bch_count_backing_io_errors() on failure.

- bch_writeback_drop_pending() drops overlapping entries from the
  pending tree.  Called from cached_dev_write() alongside the
  existing bch_keybuf_check_overlapping(); overlapping front-end
  writes now force writeback instead of bypassing the cache.

- refill_dirty() now returns enum writeback_scan_result (PARTIAL,
  PARTIAL_WRAP, FULL_SEARCHED) instead of a boolean.  Before
  wrapping around for a second scan pass, it flushes the pending
  tree so the wrap scan won't re-select keys that are still
  KEY_DIRTY in the btree but already written to backing and
  awaiting flush.  writeback_lock may be transiently released
  mid-function during this pre-wrap flush.

- refill_full_stripes() no longer wraps internally; it returns
  at the end of the stripe range and delegates the wrap to
  refill_dirty().  buf->last_scanned is no longer rewound past
  the current position to avoid re-selecting keys already in
  writeback_flush_pending.

- read_dirty() gates deferred flush-and-clean on PASS_PER_FLUSH
  (default 5) passes, a full btree scan, or a PARTIAL_WRAP.  It
  accepts the scan_result from refill_dirty() as a parameter.

- write_dirty_finish() is simplified: it no longer inserts clean
  keys inline; it only calls writeback_add_to_pending().

- BDEV_STATE_CLEAN eligibility in the main loop now also requires
  writeback_pending_empty(), so a device is not marked clean while
  entries are still in the pending tree.

- Thread exit: the workqueue is destroyed first, then a final
  writeback_finish_batch() is attempted (skipped when the cache
  set is disabled or the device is detaching), then any remaining
  pending entries are drained.  The exit path intentionally does
  NOT set BDEV_STATE_CLEAN because it lacks the WB_SCAN_FULL_SEARCHED
  gate that the in-loop check requires; a kthread_stop mid-scan
  (e.g. from bch_cached_dev_attach's error path) must not mark a
  still-dirty device clean.

Signed-off-by: Coly Li <colyli@fygo.io>
Signed-off-by: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>
---
 drivers/md/bcache/bcache.h    |  11 +
 drivers/md/bcache/request.c   |   6 +-
 drivers/md/bcache/writeback.c | 497 ++++++++++++++++++++++++++++++----
 drivers/md/bcache/writeback.h |  13 +
 4 files changed, 470 insertions(+), 57 deletions(-)

diff --git a/drivers/md/bcache/bcache.h b/drivers/md/bcache/bcache.h
index ec9ff9715..620f2d4a5 100644
--- a/drivers/md/bcache/bcache.h
+++ b/drivers/md/bcache/bcache.h
@@ -347,6 +347,17 @@ struct cached_dev {
 	struct workqueue_struct	*writeback_write_wq;
 
 	struct keybuf		writeback_keys;
+	/*
+	 * writeback_flush_pending and writeback_flush_lock are accessed
+	 * by both the writeback thread (via writeback_add_to_pending and
+	 * writeback_finish_batch) and foreground writes (via
+	 * bch_writeback_drop_pending).  writeback_flush_passes is
+	 * accessed only by the writeback thread.
+	 */
+	struct rb_root		writeback_flush_pending;
+	spinlock_t		writeback_flush_lock;
+	unsigned int		writeback_flush_passes;
+	mempool_t		*writeback_pending_pool;
 
 	struct task_struct	*status_update_thread;
 	/*
diff --git a/drivers/md/bcache/request.c b/drivers/md/bcache/request.c
index 3fa3b13a4..b2c5acd88 100644
--- a/drivers/md/bcache/request.c
+++ b/drivers/md/bcache/request.c
@@ -990,13 +990,15 @@ static void cached_dev_write(struct cached_dev *dc, struct search *s)
 	down_read_non_owner(&dc->writeback_lock);
 	if (bch_keybuf_check_overlapping(&dc->writeback_keys, &start, &end)) {
 		/*
-		 * We overlap with some dirty data undergoing background
-		 * writeback, force this write to writeback
+		 * We overlap with dirty data undergoing background
+		 * writeback, force this write to writeback.
 		 */
 		s->iop.bypass = false;
 		s->iop.writeback = true;
 	}
 
+	bch_writeback_drop_pending(dc, &start, &end);
+
 	/*
 	 * Discards aren't _required_ to do anything, so skipping if
 	 * check_overlapping returned true is ok
diff --git a/drivers/md/bcache/writeback.c b/drivers/md/bcache/writeback.c
index 4b237074f..7a1ec8f03 100644
--- a/drivers/md/bcache/writeback.c
+++ b/drivers/md/bcache/writeback.c
@@ -319,6 +319,12 @@ static unsigned int writeback_delay(struct cached_dev *dc,
 	return bch_next_delay(&dc->writeback_rate, sectors);
 }
 
+struct writeback_pending {
+	struct rb_node		node;
+	struct list_head	list;
+	BKEY_PADDED(key);
+};
+
 struct dirty_io {
 	struct closure		cl;
 	struct cached_dev	*dc;
@@ -348,38 +354,83 @@ static CLOSURE_CALLBACK(dirty_io_destructor)
 	kfree(io);
 }
 
+/*
+ * Insert wp into writeback_flush_pending rbtree (ordered by START_KEY).
+ * Caller must hold dc->writeback_flush_lock.
+ *
+ * The rbtree is used as an interval tree by bch_writeback_drop_pending()
+ * via a standard BST interval search.  That search is only correct because
+ * the entries are pairwise disjoint (dirty btree keys never overlap).
+ *
+ * Returns false if an overlapping entry already exists.  This is the
+ * expected outcome when PASS_PER_FLUSH > 1 and a key is re-selected by
+ * a later refill pass while still sitting in the pending tree — the key
+ * is still KEY_DIRTY in the btree until writeback_clean_key() runs.
+ * The read-and-rewrite cycle is wasted IO, but the tree stays consistent
+ * and the key will be cleaned when the next flush finally fires.
+ */
+static bool __writeback_pending_insert(struct cached_dev *dc,
+				       struct writeback_pending *wp)
+{
+	struct writeback_pending *entry;
+	struct rb_node **p, *parent = NULL;
+
+	lockdep_assert_held(&dc->writeback_flush_lock);
+
+	p = &dc->writeback_flush_pending.rb_node;
+	while (*p) {
+		parent = *p;
+		entry = rb_entry(parent, struct writeback_pending, node);
+		if (bkey_cmp(&wp->key, &START_KEY(&entry->key)) <= 0)
+			p = &(*p)->rb_left;
+		else if (bkey_cmp(&START_KEY(&wp->key), &entry->key) >= 0)
+			p = &(*p)->rb_right;
+		else
+			return false; /* overlap */
+	}
+	rb_link_node(&wp->node, parent, p);
+	rb_insert_color(&wp->node, &dc->writeback_flush_pending);
+	return true;
+}
+
+/*
+ * Temporarily add this key to writeback_flush_pending. After enough
+ * read_dirty() passes finish, or the full btree scan completes,
+ * writeback_finish_batch() explicitly flushes the backing device before
+ * inserting the cleaned keys back into the btree. This guarantees that
+ * the backing data will be on stable media before the dirty btree keys
+ * are overwritten by the clean keys, avoiding stale clean bkeys after
+ * power failure.
+ */
+static void writeback_add_to_pending(struct cached_dev *dc,
+				     struct bkey *key)
+{
+	struct writeback_pending *wp;
+
+	wp = mempool_alloc(dc->writeback_pending_pool, GFP_NOIO);
+
+	INIT_LIST_HEAD(&wp->list);
+	bkey_copy(&wp->key, key);
+	spin_lock(&dc->writeback_flush_lock);
+	if (!__writeback_pending_insert(dc, wp)) {
+		spin_unlock(&dc->writeback_flush_lock);
+		mempool_free(wp, dc->writeback_pending_pool);
+		return;
+	}
+	spin_unlock(&dc->writeback_flush_lock);
+}
+
 static CLOSURE_CALLBACK(write_dirty_finish)
 {
 	closure_type(io, struct dirty_io, cl);
 	struct keybuf_key *w = io->bio.bi_private;
 	struct cached_dev *dc = io->dc;
+	bool written = KEY_DIRTY(&w->key);
 
 	bio_free_pages(&io->bio);
 
-	/* This is kind of a dumb way of signalling errors. */
-	if (KEY_DIRTY(&w->key)) {
-		int ret;
-		unsigned int i;
-		struct keylist keys;
-
-		bch_keylist_init(&keys);
-
-		bkey_copy(keys.top, &w->key);
-		SET_KEY_DIRTY(keys.top, false);
-		bch_keylist_push(&keys);
-
-		for (i = 0; i < KEY_PTRS(&w->key); i++)
-			atomic_inc(&PTR_BUCKET(dc->disk.c, &w->key, i)->pin);
-
-		ret = bch_btree_insert(dc->disk.c, &keys, NULL, &w->key);
-
-		if (ret)
-			trace_bcache_writeback_collision(&w->key);
-
-		atomic_long_inc(ret
-				? &dc->disk.c->writeback_keys_failed
-				: &dc->disk.c->writeback_keys_done);
-	}
+	if (written)
+		writeback_add_to_pending(dc, &w->key);
 
 	bch_keybuf_del(&dc->writeback_keys, w);
 	up(&dc->in_flight);
@@ -400,6 +451,213 @@ static void dirty_endio(struct bio *bio)
 	closure_put(&io->cl);
 }
 
+/*
+ * Issue a flush to the backing device.  Open-codes blkdev_issue_flush()
+ * so we can call bch_count_backing_io_errors() on failure.
+ */
+static int writeback_flush(struct cached_dev *dc)
+{
+	struct bio bio;
+	int ret;
+
+	bio_init(&bio, dc->bdev, NULL, 0, REQ_OP_WRITE | REQ_PREFLUSH);
+
+	ret = submit_bio_wait(&bio);
+	if (ret)
+		bch_count_backing_io_errors(dc, &bio);
+
+	bio_uninit(&bio);
+	return ret;
+}
+
+static void writeback_clean_key(struct cached_dev *dc, struct bkey *key)
+{
+	int ret;
+	unsigned int i;
+	struct keylist keys;
+
+	bch_keylist_init(&keys);
+
+	bkey_copy(keys.top, key);
+	SET_KEY_DIRTY(keys.top, false);
+	bch_keylist_push(&keys);
+
+	for (i = 0; i < KEY_PTRS(key); i++)
+		atomic_inc(&PTR_BUCKET(dc->disk.c, key, i)->pin);
+
+	/*
+	 * If the insert fails (e.g. GC moved the cached data and the key's
+	 * pointers went stale), just give up. The key stays dirty in btree
+	 * and will be re-selected on the next writeback pass. The backing
+	 * device already has the correct data, so no harm done.
+	 */
+	ret = bch_btree_insert(dc->disk.c, &keys, NULL, key);
+
+	if (ret)
+		trace_bcache_writeback_collision(key);
+
+	atomic_long_inc(ret
+			? &dc->disk.c->writeback_keys_failed
+			: &dc->disk.c->writeback_keys_done);
+}
+
+static void writeback_drain_pending(struct cached_dev *dc)
+{
+	struct writeback_pending *wp, *n;
+	LIST_HEAD(keys);
+
+	/*
+	 * Called only at writeback-thread exit, after the workqueue has
+	 * been destroyed.  New foreground writers can no longer reach
+	 * bch_writeback_drop_pending() at this point, but we hold the
+	 * lock anyway to keep the API uniform with the rest of the
+	 * pending-tree code.
+	 */
+	spin_lock(&dc->writeback_flush_lock);
+	while (!RB_EMPTY_ROOT(&dc->writeback_flush_pending)) {
+		struct rb_node *rb_node =
+			rb_first(&dc->writeback_flush_pending);
+
+		wp = rb_entry(rb_node, struct writeback_pending, node);
+		rb_erase(rb_node, &dc->writeback_flush_pending);
+		list_add(&wp->list, &keys);
+	}
+	spin_unlock(&dc->writeback_flush_lock);
+
+	list_for_each_entry_safe(wp, n, &keys, list)
+		mempool_free(wp, dc->writeback_pending_pool);
+}
+
+static bool writeback_pending_empty(struct cached_dev *dc)
+{
+	bool ret;
+
+	spin_lock(&dc->writeback_flush_lock);
+	ret = RB_EMPTY_ROOT(&dc->writeback_flush_pending);
+	spin_unlock(&dc->writeback_flush_lock);
+
+	return ret;
+}
+
+/*
+ * Drop all entries in writeback_flush_pending that overlap [start, end).
+ * Called from the foreground write path to force-writeback overlapped
+ * dirty data instead of bypassing the cache.
+ *
+ * The search below is a standard BST interval lookup that relies on the
+ * tree being pairwise disjoint: an entry's (START_KEY, end_key) range
+ * never overlaps another entry's range.  The disjoint invariant is
+ * enforced by __writeback_pending_insert() which rejects overlapping
+ * inserts.  Without this invariant the search would need the "max subtree
+ * end" augmentation provided by lib/interval_tree.
+ */
+bool bch_writeback_drop_pending(struct cached_dev *dc,
+				struct bkey *start,
+				struct bkey *end)
+{
+	struct writeback_pending *wp, *n;
+	LIST_HEAD(overlap);
+	struct rb_node *node;
+	bool ret = false;
+
+	spin_lock(&dc->writeback_flush_lock);
+	node = dc->writeback_flush_pending.rb_node;
+	while (node) {
+		wp = rb_entry(node, struct writeback_pending, node);
+
+		if (bkey_cmp(end, &START_KEY(&wp->key)) <= 0) {
+			node = node->rb_left;
+		} else if (bkey_cmp(start, &wp->key) >= 0) {
+			node = node->rb_right;
+		} else {
+			rb_erase(&wp->node, &dc->writeback_flush_pending);
+			list_add(&wp->list, &overlap);
+			ret = true;
+			/* restart search from root after erase */
+			node = dc->writeback_flush_pending.rb_node;
+		}
+	}
+	spin_unlock(&dc->writeback_flush_lock);
+
+	list_for_each_entry_safe(wp, n, &overlap, list)
+		mempool_free(wp, dc->writeback_pending_pool);
+
+	return ret;
+}
+
+static bool writeback_finish_batch(struct cached_dev *dc)
+{
+	struct writeback_pending *wp, *n;
+	LIST_HEAD(keys);
+	int flush_ret = 0;
+
+	spin_lock(&dc->writeback_flush_lock);
+	if (RB_EMPTY_ROOT(&dc->writeback_flush_pending)) {
+		spin_unlock(&dc->writeback_flush_lock);
+		return true;
+	}
+	/*
+	 * Detach all entries into a local list. New completions arriving
+	 * during the flush below will be added to the (now empty) tree.
+	 *
+	 * While the entries sit on the local list, they are invisible to
+	 * bch_writeback_drop_pending(), so a concurrent front-end write
+	 * overlapping a flushing key will not force-writeback and instead
+	 * write around the cache.  The resulting invalidation key has
+	 * KEY_PTRS=0 (bch_data_invalidate), which trims or removes the
+	 * overlapping dirty btree key.  The subsequent clean-key insert in
+	 * writeback_clean_key() then hits a pointer mismatch in
+	 * bch_extent_insert_fixup() -> replace_key(), producing an
+	 * insert_collision (ESRCH).  The clean-key insert is a no-op.
+	 * Subsequent reads miss the cache and go to the backing device,
+	 * which has the new data.  No data is lost.
+	 */
+	while (!RB_EMPTY_ROOT(&dc->writeback_flush_pending)) {
+		struct rb_node *rb_node =
+			rb_first(&dc->writeback_flush_pending);
+
+		wp = rb_entry(rb_node, struct writeback_pending, node);
+		rb_erase(rb_node, &dc->writeback_flush_pending);
+		list_add(&wp->list, &keys);
+	}
+	spin_unlock(&dc->writeback_flush_lock);
+
+	flush_ret = writeback_flush(dc);
+
+	if (flush_ret) {
+		/*
+		 * Re-insert all saved entries back into the current tree
+		 * (which may have accumulated new entries during the flush).
+		 * This preserves both old and new keys instead of leaking
+		 * the new ones.
+		 */
+		spin_lock(&dc->writeback_flush_lock);
+		list_for_each_entry_safe(wp, n, &keys, list) {
+			if (!__writeback_pending_insert(dc, wp)) {
+				/*
+				 * A new writeback completion added this range
+				 * while we held the lock for re-insert —
+				 * expected with PASS_PER_FLUSH > 1.
+				 */
+				list_del_init(&wp->list);
+				mempool_free(wp, dc->writeback_pending_pool);
+				continue;
+			}
+			list_del_init(&wp->list);
+		}
+		spin_unlock(&dc->writeback_flush_lock);
+
+		return false;
+	}
+
+	list_for_each_entry_safe(wp, n, &keys, list) {
+		writeback_clean_key(dc, &wp->key);
+		mempool_free(wp, dc->writeback_pending_pool);
+	}
+
+	return true;
+}
+
 static CLOSURE_CALLBACK(write_dirty)
 {
 	closure_type(io, struct dirty_io, cl);
@@ -471,7 +729,14 @@ static CLOSURE_CALLBACK(read_dirty_submit)
 	continue_at(cl, write_dirty, io->dc->writeback_write_wq);
 }
 
-static void read_dirty(struct cached_dev *dc)
+enum writeback_scan_result {
+	WB_SCAN_PARTIAL,
+	WB_SCAN_PARTIAL_WRAP,
+	WB_SCAN_FULL_SEARCHED,
+};
+
+static void read_dirty(struct cached_dev *dc,
+		       enum writeback_scan_result scan_result)
 {
 	unsigned int delay = 0;
 	struct keybuf_key *next, *keys[MAX_WRITEBACKS_IN_PASS], *w;
@@ -585,10 +850,27 @@ static void read_dirty(struct cached_dev *dc)
 	}
 
 	/*
-	 * Wait for outstanding writeback IOs to finish (and keybuf slots to be
-	 * freed) before refilling again
+	 * Wait for outstanding writeback IOs to finish before refilling again.
+	 * Clean keys are inserted after writeback_finish_batch() flushes the
+	 * backing device.
 	 */
 	closure_sync(&cl);
+
+	dc->writeback_flush_passes++;
+	/*
+	 * WB_SCAN_PARTIAL_WRAP covers two sub-cases: (i) the pre-wrap
+	 * flush failed, so keys are still in the pending tree; (ii) the
+	 * wrap scan filled the keybuf before reaching start_pos.  In
+	 * case (ii) the flush is conservative — we could defer it until
+	 * another pass — but the extra flush is cheap and the simpler
+	 * predicate avoids tracking the distinction through refill_dirty.
+	 */
+	if (dc->writeback_flush_passes >= PASS_PER_FLUSH ||
+	    scan_result == WB_SCAN_FULL_SEARCHED ||
+	    scan_result == WB_SCAN_PARTIAL_WRAP) {
+		if (writeback_finish_batch(dc))
+			dc->writeback_flush_passes = 0;
+	}
 }
 
 /* Scan for dirty data */
@@ -652,28 +934,44 @@ static bool dirty_pred(struct keybuf *buf, struct bkey *k)
 static void refill_full_stripes(struct cached_dev *dc)
 {
 	struct keybuf *buf = &dc->writeback_keys;
-	unsigned int start_stripe, next_stripe;
+	unsigned int next_stripe;
 	int stripe;
-	bool wrapped = false;
 
 	stripe = offset_to_stripe(&dc->disk, KEY_OFFSET(&buf->last_scanned));
 	if (stripe < 0)
 		stripe = 0;
 
-	start_stripe = stripe;
-
 	while (1) {
 		stripe = find_next_bit(dc->disk.full_dirty_stripes,
 				       dc->disk.nr_stripes, stripe);
 
+		/*
+		 * Reached the end of stripes.  Do not wrap here — return
+		 * and let refill_dirty() handle the wrap (which optionally
+		 * flushes pending entries first), so keys already in
+		 * writeback_flush_pending are not re-selected.
+		 */
 		if (stripe == dc->disk.nr_stripes)
-			goto next;
+			return;
 
 		next_stripe = find_next_zero_bit(dc->disk.full_dirty_stripes,
 						 dc->disk.nr_stripes, stripe);
 
-		buf->last_scanned = KEY(dc->disk.id,
-					stripe * dc->disk.stripe_size, 0);
+		/*
+		 * Don't rewind last_scanned: if the keybuf filled
+		 * mid-range on the previous pass, keys from the
+		 * start of this stripe up to last_scanned have
+		 * already been written back and may be sitting in
+		 * writeback_flush_pending (still dirty in btree).
+		 * Rewinding would re-select them, creating
+		 * duplicates.
+		 */
+		if (bkey_cmp(&buf->last_scanned,
+			     &KEY(dc->disk.id,
+				  stripe * dc->disk.stripe_size, 0)) < 0)
+			buf->last_scanned = KEY(dc->disk.id,
+						stripe * dc->disk.stripe_size,
+						0);
 
 		bch_refill_keybuf(dc->disk.c, buf,
 				  &KEY(dc->disk.id,
@@ -684,21 +982,19 @@ static void refill_full_stripes(struct cached_dev *dc)
 			return;
 
 		stripe = next_stripe;
-next:
-		if (wrapped && stripe > start_stripe)
-			return;
-
-		if (stripe == dc->disk.nr_stripes) {
-			stripe = 0;
-			wrapped = true;
-		}
 	}
 }
 
 /*
- * Returns true if we scanned the entire disk
+ * Refill the writeback keybuf by scanning the btree for dirty keys.
+ *
+ * IMPORTANT: writeback_lock is held on entry but may be transiently
+ * released mid-function when flushing pending keys before a wrap-around
+ * scan.  Callers must not rely on the lock being held across the call.
+ *
+ * Returns the scan result indicating scan coverage.
  */
-static bool refill_dirty(struct cached_dev *dc)
+static enum writeback_scan_result refill_dirty(struct cached_dev *dc)
 {
 	struct keybuf *buf = &dc->writeback_keys;
 	struct bkey start = KEY(dc->disk.id, 0, 0);
@@ -717,30 +1013,49 @@ static bool refill_dirty(struct cached_dev *dc)
 	if (dc->partial_stripes_expensive) {
 		refill_full_stripes(dc);
 		if (array_freelist_empty(&buf->freelist))
-			return false;
+			return WB_SCAN_PARTIAL;
 	}
 
 	start_pos = buf->last_scanned;
 	bch_refill_keybuf(dc->disk.c, buf, &end, dirty_pred);
 
 	if (bkey_cmp(&buf->last_scanned, &end) < 0)
-		return false;
+		return WB_SCAN_PARTIAL;
 
 	/*
-	 * If we get to the end start scanning again from the beginning, and
-	 * only scan up to where we initially started scanning from:
+	 * Forward scan reached the end. Flush pending writeback keys
+	 * before wrapping around, so the wrap scan won't re-select
+	 * keys that are already on writeback_flush_pending.
+	 */
+	if (!writeback_pending_empty(dc)) {
+		bool flushed;
+
+		up_write(&dc->writeback_lock);
+		flushed = writeback_finish_batch(dc);
+		down_write(&dc->writeback_lock);
+
+		if (!flushed)
+			return WB_SCAN_PARTIAL_WRAP;
+	}
+
+	/*
+	 * Start scanning again from the beginning, up to where we
+	 * initially started.
 	 */
 	buf->last_scanned = start;
 	bch_refill_keybuf(dc->disk.c, buf, &start_pos, dirty_pred);
 
-	return bkey_cmp(&buf->last_scanned, &start_pos) >= 0;
+	if (bkey_cmp(&buf->last_scanned, &start_pos) < 0)
+		return WB_SCAN_PARTIAL_WRAP;
+
+	return WB_SCAN_FULL_SEARCHED;
 }
 
 static int bch_writeback_thread(void *arg)
 {
 	struct cached_dev *dc = arg;
 	struct cache_set *c = dc->disk.c;
-	bool searched_full_index;
+	enum writeback_scan_result searched_full_index;
 
 	bch_ratelimit_reset(&dc->writeback_rate);
 
@@ -765,6 +1080,26 @@ static int bch_writeback_thread(void *arg)
 				break;
 			}
 
+			/*
+			 * Drain writeback_flush_pending before sleeping.
+			 * If writeback_running is cleared mid-cycle (e.g., at
+			 * pass 37 of PASS_PER_FLUSH=40), read_dirty() waits
+			 * for all IOs via closure_sync, so write_dirty_finish()
+			 * has already pushed entries into
+			 * writeback_flush_pending before this sleep check is
+			 * reached.  With writeback stopped, no further
+			 * read_dirty() call fires and the next PASS_PER_FLUSH
+			 * threshold is never reached.
+			 *
+			 * Use continue so the sleep condition is re-evaluated
+			 * after the drain completes.
+			 */
+			if (!writeback_pending_empty(dc)) {
+				set_current_state(TASK_RUNNING);
+				writeback_finish_batch(dc);
+				continue;
+			}
+
 			schedule();
 			continue;
 		}
@@ -772,8 +1107,9 @@ static int bch_writeback_thread(void *arg)
 
 		searched_full_index = refill_dirty(dc);
 
-		if (searched_full_index &&
-		    RB_EMPTY_ROOT(&dc->writeback_keys.keys)) {
+		if (searched_full_index == WB_SCAN_FULL_SEARCHED &&
+		    RB_EMPTY_ROOT(&dc->writeback_keys.keys) &&
+		    writeback_pending_empty(dc)) {
 			atomic_set(&dc->has_dirty, 0);
 			SET_BDEV_STATE(&dc->sb, BDEV_STATE_CLEAN);
 			bch_write_bdev_super(dc, NULL);
@@ -816,9 +1152,9 @@ static int bch_writeback_thread(void *arg)
 
 		up_write(&dc->writeback_lock);
 
-		read_dirty(dc);
+		read_dirty(dc, searched_full_index);
 
-		if (searched_full_index) {
+		if (searched_full_index == WB_SCAN_FULL_SEARCHED) {
 			unsigned int delay = dc->writeback_delay * HZ;
 
 			while (delay &&
@@ -831,8 +1167,40 @@ static int bch_writeback_thread(void *arg)
 		}
 	}
 
-	if (dc->writeback_write_wq)
+	if (dc->writeback_write_wq) {
 		destroy_workqueue(dc->writeback_write_wq);
+		dc->writeback_write_wq = NULL;
+	}
+
+	/*
+	 * Try a final flush-and-clean before draining so pending keys are
+	 * cleaned rather than left dirty for the next mount.
+	 *
+	 * Skip when the cache set is disabled (flush would hang on a dead
+	 * backing device) or the device is detaching (the in-loop drain
+	 * has already cleaned everything; otherwise we tolerate redoing
+	 * the work on next mount).
+	 *
+	 * Note: a backing-device failure that races the flag checks above
+	 * can still hang kthread teardown — bounded by the device's own
+	 * IO-error timeout.
+	 *
+	 * We do NOT set BDEV_STATE_CLEAN here because the in-loop check
+	 * additionally requires WB_SCAN_FULL_SEARCHED (a full btree scan
+	 * found nothing dirty).  Without that gate, the on-disk superblock
+	 * could be marked clean while the btree still holds dirty keys
+	 * that were never scanned by this thread (e.g. kthread_stop from
+	 * bch_cached_dev_attach's error path).  Let the next mount observe
+	 * BDEV_STATE_DIRTY and re-confirm.
+	 */
+	if (!test_bit(CACHE_SET_IO_DISABLE, &c->flags) &&
+	    !test_bit(BCACHE_DEV_DETACHING, &dc->disk.flags))
+		writeback_finish_batch(dc);
+
+	writeback_drain_pending(dc);
+
+	mempool_destroy(dc->writeback_pending_pool);
+	dc->writeback_pending_pool = NULL;
 
 	cached_dev_put(dc);
 	wait_for_kthread_stop();
@@ -1049,6 +1417,9 @@ void bch_cached_dev_writeback_init(struct cached_dev *dc)
 	sema_init(&dc->in_flight, 64);
 	init_rwsem(&dc->writeback_lock);
 	bch_keybuf_init(&dc->writeback_keys);
+	dc->writeback_flush_pending = RB_ROOT;
+	spin_lock_init(&dc->writeback_flush_lock);
+	dc->writeback_flush_passes	= 0;
 
 	dc->writeback_metadata		= true;
 	dc->writeback_running		= false;
@@ -1079,11 +1450,27 @@ int bch_cached_dev_writeback_start(struct cached_dev *dc)
 	if (!dc->writeback_write_wq)
 		return -ENOMEM;
 
+	/*
+	 * Pre-allocate a pool sized to the maximum number of concurrent
+	 * writeback IOs (bounded by the in_flight semaphore, initialised to
+	 * 64).  mempool_alloc(GFP_NOIO) will draw from this reserve when the
+	 * normal allocator fails under memory pressure, guaranteeing that
+	 * writeback_add_to_pending() never drops an entry silently.
+	 */
+	dc->writeback_pending_pool = mempool_create_kmalloc_pool(
+			64, sizeof(struct writeback_pending));
+	if (!dc->writeback_pending_pool) {
+		destroy_workqueue(dc->writeback_write_wq);
+		return -ENOMEM;
+	}
+
 	cached_dev_get(dc);
 	dc->writeback_thread = kthread_create(bch_writeback_thread, dc,
 					      "bcache_writeback");
 	if (IS_ERR(dc->writeback_thread)) {
 		cached_dev_put(dc);
+		mempool_destroy(dc->writeback_pending_pool);
+		dc->writeback_pending_pool = NULL;
 		destroy_workqueue(dc->writeback_write_wq);
 		return PTR_ERR(dc->writeback_thread);
 	}
diff --git a/drivers/md/bcache/writeback.h b/drivers/md/bcache/writeback.h
index 31df71695..51aba51b2 100644
--- a/drivers/md/bcache/writeback.h
+++ b/drivers/md/bcache/writeback.h
@@ -10,6 +10,14 @@
 
 #define MAX_WRITEBACKS_IN_PASS  5
 #define MAX_WRITESIZE_IN_PASS   5000	/* *512b */
+/*
+ * Number of read_dirty() passes before forcing a backing-device flush
+ * and clean-key insert.  Larger values amortize flush cost but may
+ * re-select keys still in writeback_flush_pending (still KEY_DIRTY)
+ * and re-write them, causing IO amplification.  Small values flush
+ * more often, reducing amplification at the cost of more flushes.
+ */
+#define PASS_PER_FLUSH		5
 
 #define WRITEBACK_RATE_UPDATE_SECS_MAX		60
 #define WRITEBACK_RATE_UPDATE_SECS_DEFAULT	5
@@ -21,6 +29,7 @@
 #define BCH_WRITEBACK_FRAGMENT_THRESHOLD_HIGH 64
 
 #define BCH_DIRTY_INIT_THRD_MAX	12
+
 /*
  * 14 (16384ths) is chosen here as something that each backing device
  * should be a reasonable fraction of the share, and not to blow up
@@ -28,6 +37,10 @@
  */
 #define WRITEBACK_SHARE_SHIFT   14
 
+bool bch_writeback_drop_pending(struct cached_dev *dc,
+				struct bkey *start,
+				struct bkey *end);
+
 struct bch_dirty_init_state;
 struct dirty_init_thrd_info {
 	struct bch_dirty_init_state	*state;
-- 
2.52.0.windows.1


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC v4] bcache: flush backing device before cleaning the writeback dirty keys
  2026-07-09  6:23   ` [PATCH RFC v4] " Zhou Jifeng
@ 2026-07-09  6:30     ` Coly Li
  2026-07-10 10:17       ` Zhou Jifeng
  0 siblings, 1 reply; 5+ messages in thread
From: Coly Li @ 2026-07-09  6:30 UTC (permalink / raw)
  To: Zhou Jifeng; +Cc: linux-bcache, zhoujifeng

> 2026年7月9日 14:23,Zhou Jifeng <zhoujifeng@kylinos.com.cn> 写道:
> 
> From: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>
> 
> Currently, when writeback dirty data completes, write_dirty_finish()
> immediately inserts the clean key into the btree.  If a power failure
> occurs after the clean-key insert but before the backing device has
> flushed its volatile write cache, the btree contains a clean key that
> points at backing data that never reached stable media.  On the next
> read, bcache serves stale data from the backing device.
> 
> Fix this by deferring the clean-key btree insert until after an
> explicit backing-device flush (REQ_PREFLUSH).  Written keys are first
> parked in a new rbtree (writeback_flush_pending) keyed by START_KEY.
> After enough read_dirty() passes, a full btree scan, or a wrap-around
> in refill_dirty(), writeback_finish_batch() issues a synchronous
> backing-device flush, and only on success inserts the clean keys into
> the btree and removes them from the pending tree.  On flush failure
> the entries are re-inserted into the current tree for a later retry.
> 
> Key changes:
> 
> - Introduce struct writeback_pending (separate kmalloc'd allocation,
>  decoupled from the dirty_io closure so the closure can be freed
>  immediately and the pending entry lives until the flush).
> 
> - Add __writeback_pending_insert() — inserts into the START_KEY-
>  ordered rbtree with overlap detection.  The rbtree is also used as
>  an interval tree by bch_writeback_drop_pending() for front-end
>  write overlap queries; that search is correct only because dirty
>  btree keys are pairwise disjoint, an invariant enforced at insert
>  time.
> 
> - writeback_add_to_pending() parks a key after successful writeback
>  IO.  Uses GFP_NOIO because the caller runs on the WQ_MEM_RECLAIM
>  writeback workqueue.  kmalloc failure silently drops the key (it
>  stays dirty and is re-selected by the next scan).
> 
> - writeback_finish_batch() detaches the entire pending tree into a
>  local list under spinlock, issues writeback_flush(), then either
>  cleans the keys (on success) or re-inserts them (on failure).
> 
> - writeback_flush() open-codes blkdev_issue_flush() to call
>  bch_count_backing_io_errors() on failure.
> 
> - bch_writeback_drop_pending() drops overlapping entries from the
>  pending tree.  Called from cached_dev_write() alongside the
>  existing bch_keybuf_check_overlapping(); overlapping front-end
>  writes now force writeback instead of bypassing the cache.
> 
> - refill_dirty() now returns enum writeback_scan_result (PARTIAL,
>  PARTIAL_WRAP, FULL_SEARCHED) instead of a boolean.  Before
>  wrapping around for a second scan pass, it flushes the pending
>  tree so the wrap scan won't re-select keys that are still
>  KEY_DIRTY in the btree but already written to backing and
>  awaiting flush.  writeback_lock may be transiently released
>  mid-function during this pre-wrap flush.
> 
> - refill_full_stripes() no longer wraps internally; it returns
>  at the end of the stripe range and delegates the wrap to
>  refill_dirty().  buf->last_scanned is no longer rewound past
>  the current position to avoid re-selecting keys already in
>  writeback_flush_pending.
> 
> - read_dirty() gates deferred flush-and-clean on PASS_PER_FLUSH
>  (default 5) passes, a full btree scan, or a PARTIAL_WRAP.  It
>  accepts the scan_result from refill_dirty() as a parameter.
> 
> - write_dirty_finish() is simplified: it no longer inserts clean
>  keys inline; it only calls writeback_add_to_pending().
> 
> - BDEV_STATE_CLEAN eligibility in the main loop now also requires
>  writeback_pending_empty(), so a device is not marked clean while
>  entries are still in the pending tree.
> 
> - Thread exit: the workqueue is destroyed first, then a final
>  writeback_finish_batch() is attempted (skipped when the cache
>  set is disabled or the device is detaching), then any remaining
>  pending entries are drained.  The exit path intentionally does
>  NOT set BDEV_STATE_CLEAN because it lacks the WB_SCAN_FULL_SEARCHED
>  gate that the in-loop check requires; a kthread_stop mid-scan
>  (e.g. from bch_cached_dev_attach's error path) must not mark a
>  still-dirty device clean.
> 
> Signed-off-by: Coly Li <colyli@fygo.io>
> Signed-off-by: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>

You cannot add my SOB for me. I know you mean the base idea of this version was
from me, but it is fine to only do your own SOB. I am good with this, since you already
do a lot of analysis and testing.

The v4 version format is much more easier to review.
I will try my best to response you soon.

Thanks for the fix up and testing.

Coly Li

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC v4] bcache: flush backing device before cleaning the writeback dirty keys
  2026-07-09  6:30     ` Coly Li
@ 2026-07-10 10:17       ` Zhou Jifeng
  0 siblings, 0 replies; 5+ messages in thread
From: Zhou Jifeng @ 2026-07-10 10:17 UTC (permalink / raw)
  To: Coly Li; +Cc: linux-bcache

>> 2026年7月9日 14:23,Zhou Jifeng <zhoujifeng@kylinos.com.cn> 写道:
>>
>> From: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>
>>
>> Currently, when writeback dirty data completes, write_dirty_finish()
>> immediately inserts the clean key into the btree.  If a power failure
>> occurs after the clean-key insert but before the backing device has
>> flushed its volatile write cache, the btree contains a clean key that
>> points at backing data that never reached stable media.  On the next
>> read, bcache serves stale data from the backing device.
>>
>> Fix this by deferring the clean-key btree insert until after an
>> explicit backing-device flush (REQ_PREFLUSH).  Written keys are first
>> parked in a new rbtree (writeback_flush_pending) keyed by START_KEY.
>> After enough read_dirty() passes, a full btree scan, or a wrap-around
>> in refill_dirty(), writeback_finish_batch() issues a synchronous
>> backing-device flush, and only on success inserts the clean keys into
>> the btree and removes them from the pending tree.  On flush failure
>> the entries are re-inserted into the current tree for a later retry.
>>
>> Key changes:
>>
>> - Introduce struct writeback_pending (separate kmalloc'd allocation,
>>  decoupled from the dirty_io closure so the closure can be freed
>>  immediately and the pending entry lives until the flush).
>>
>> - Add __writeback_pending_insert() — inserts into the START_KEY-
>>  ordered rbtree with overlap detection.  The rbtree is also used as
>>  an interval tree by bch_writeback_drop_pending() for front-end
>>  write overlap queries; that search is correct only because dirty
>>  btree keys are pairwise disjoint, an invariant enforced at insert
>>  time.
>>
>> - writeback_add_to_pending() parks a key after successful writeback
>>  IO.  Uses GFP_NOIO because the caller runs on the WQ_MEM_RECLAIM
>>  writeback workqueue.  kmalloc failure silently drops the key (it
>>  stays dirty and is re-selected by the next scan).
>>
>> - writeback_finish_batch() detaches the entire pending tree into a
>>  local list under spinlock, issues writeback_flush(), then either
>>  cleans the keys (on success) or re-inserts them (on failure).
>>
>> - writeback_flush() open-codes blkdev_issue_flush() to call
>>  bch_count_backing_io_errors() on failure.
>>
>> - bch_writeback_drop_pending() drops overlapping entries from the
>>  pending tree.  Called from cached_dev_write() alongside the
>>  existing bch_keybuf_check_overlapping(); overlapping front-end
>>  writes now force writeback instead of bypassing the cache.
>>
>> - refill_dirty() now returns enum writeback_scan_result (PARTIAL,
>>  PARTIAL_WRAP, FULL_SEARCHED) instead of a boolean.  Before
>>  wrapping around for a second scan pass, it flushes the pending
>>  tree so the wrap scan won't re-select keys that are still
>>  KEY_DIRTY in the btree but already written to backing and
>>  awaiting flush.  writeback_lock may be transiently released
>>  mid-function during this pre-wrap flush.
>>
>> - refill_full_stripes() no longer wraps internally; it returns
>>  at the end of the stripe range and delegates the wrap to
>>  refill_dirty().  buf->last_scanned is no longer rewound past
>>  the current position to avoid re-selecting keys already in
>>  writeback_flush_pending.
>>
>> - read_dirty() gates deferred flush-and-clean on PASS_PER_FLUSH
>>  (default 5) passes, a full btree scan, or a PARTIAL_WRAP.  It
>>  accepts the scan_result from refill_dirty() as a parameter.
>>
>> - write_dirty_finish() is simplified: it no longer inserts clean
>>  keys inline; it only calls writeback_add_to_pending().
>>
>> - BDEV_STATE_CLEAN eligibility in the main loop now also requires
>>  writeback_pending_empty(), so a device is not marked clean while
>>  entries are still in the pending tree.
>>
>> - Thread exit: the workqueue is destroyed first, then a final
>>  writeback_finish_batch() is attempted (skipped when the cache
>>  set is disabled or the device is detaching), then any remaining
>>  pending entries are drained.  The exit path intentionally does
>>  NOT set BDEV_STATE_CLEAN because it lacks the WB_SCAN_FULL_SEARCHED
>>  gate that the in-loop check requires; a kthread_stop mid-scan
>>  (e.g. from bch_cached_dev_attach's error path) must not mark a
>>  still-dirty device clean.
>>
>> Signed-off-by: Coly Li <colyli@fygo.io>
>> Signed-off-by: Zhou Jifeng <zhoujifeng@kylinsec.com.cn>

>
> You cannot add my SOB for me. I know you mean the base idea of this version was
> from me, but it is fine to only do your own SOB. I am good with this, since you already
> do a lot of analysis and testing.
>
> The v4 version format is much more easier to review.
> I will try my best to response you soon.
>
> Thanks for the fix up and testing.
>
> Coly Li


Hi Coly,​
I really appreciate your time and guidance on this patch.

Best regards,
Zhou Jifeng

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-07-10 10:17 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-27  8:27 [PATCH RFC v3] bcache: flush backing device before cleaning the writeback dirty keys Zhou Jifeng
2026-07-08  3:56 ` Coly Li
2026-07-09  6:23   ` [PATCH RFC v4] " Zhou Jifeng
2026-07-09  6:30     ` Coly Li
2026-07-10 10:17       ` Zhou Jifeng

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.