// SPDX-License-Identifier: GPL-2.0

#include "bcachefs.h"
#include "btree_iter.h"
#include "eytzinger.h"
#include "journal_seq_blacklist.h"
#include "super-io.h"

/*
 * journal_seq_blacklist machinery:
 *
 * To guarantee order of btree updates after a crash, we need to detect when a
 * btree node entry (bset) is newer than the newest journal entry that was
 * successfully written, and ignore it - effectively ignoring any btree updates
 * that didn't make it into the journal.
 *
 * If we didn't do this, we might have two btree nodes, a and b, both with
 * updates that weren't written to the journal yet: if b was updated after a,
 * but b was flushed and not a - oops; on recovery we'll find that the updates
 * to b happened, but not the updates to a that happened before it.
 *
 * Ignoring bsets that are newer than the newest journal entry is always safe,
 * because everything they contain will also have been journalled - and must
 * still be present in the journal on disk until a journal entry has been
 * written _after_ that bset was written.
 *
 * To accomplish this, bsets record the newest journal sequence number they
 * contain updates for; then, on startup, the btree code queries the journal
 * code to ask "Is this sequence number newer than the newest journal entry? If
 * so, ignore it."
 *
 * When this happens, we must blacklist that journal sequence number: the
 * journal must not write any entries with that sequence number, and it must
 * record that it was blacklisted so that a) on recovery we don't think we have
 * missing journal entries and b) so that the btree code continues to ignore
 * that bset, until that btree node is rewritten.
 */

static unsigned sb_blacklist_u64s(unsigned nr)
{
	struct bch_sb_field_journal_seq_blacklist *bl;

	return (sizeof(*bl) + sizeof(bl->start[0]) * nr) / sizeof(u64);
}

static struct bch_sb_field_journal_seq_blacklist *
blacklist_entry_try_merge(struct bch_fs *c,
			  struct bch_sb_field_journal_seq_blacklist *bl,
			  unsigned i)
{
	unsigned nr = blacklist_nr_entries(bl);

	if (le64_to_cpu(bl->start[i].end) >=
	    le64_to_cpu(bl->start[i + 1].start)) {
		bl->start[i].end = bl->start[i + 1].end;
		--nr;
		memmove(&bl->start[i],
			&bl->start[i + 1],
			sizeof(bl->start[0]) * (nr - i));

		bl = bch2_sb_field_resize(&c->disk_sb, journal_seq_blacklist,
					  sb_blacklist_u64s(nr));
		BUG_ON(!bl);
	}

	return bl;
}

static bool bl_entry_contig_or_overlaps(struct journal_seq_blacklist_entry *e,
					u64 start, u64 end)
{
	return !(end < le64_to_cpu(e->start) || le64_to_cpu(e->end) < start);
}

int bch2_journal_seq_blacklist_add(struct bch_fs *c, u64 start, u64 end)
{
	struct bch_sb_field_journal_seq_blacklist *bl;
	unsigned i, nr;
	int ret = 0;

	mutex_lock(&c->sb_lock);
	bl = bch2_sb_field_get(c->disk_sb.sb, journal_seq_blacklist);
	nr = blacklist_nr_entries(bl);

	for (i = 0; i < nr; i++) {
		struct journal_seq_blacklist_entry *e =
			bl->start + i;

		if (bl_entry_contig_or_overlaps(e, start, end)) {
			e->start = cpu_to_le64(min(start, le64_to_cpu(e->start)));
			e->end	= cpu_to_le64(max(end, le64_to_cpu(e->end)));

			if (i + 1 < nr)
				bl = blacklist_entry_try_merge(c,
							bl, i);
			if (i)
				bl = blacklist_entry_try_merge(c,
							bl, i - 1);
			goto out_write_sb;
		}
	}

	bl = bch2_sb_field_resize(&c->disk_sb, journal_seq_blacklist,
				  sb_blacklist_u64s(nr + 1));
	if (!bl) {
		ret = -BCH_ERR_ENOSPC_sb_journal_seq_blacklist;
		goto out;
	}

	bl->start[nr].start	= cpu_to_le64(start);
	bl->start[nr].end	= cpu_to_le64(end);
out_write_sb:
	c->disk_sb.sb->features[0] |= cpu_to_le64(1ULL << BCH_FEATURE_journal_seq_blacklist_v3);

	ret = bch2_write_super(c);
out:
	mutex_unlock(&c->sb_lock);

	return ret ?: bch2_blacklist_table_initialize(c);
}

static int journal_seq_blacklist_table_cmp(const void *_l,
					   const void *_r, size_t size)
{
	const struct journal_seq_blacklist_table_entry *l = _l;
	const struct journal_seq_blacklist_table_entry *r = _r;

	return cmp_int(l->start, r->start);
}

bool bch2_journal_seq_is_blacklisted(struct bch_fs *c, u64 seq,
				     bool dirty)
{
	struct journal_seq_blacklist_table *t = c->journal_seq_blacklist_table;
	struct journal_seq_blacklist_table_entry search = { .start = seq };
	int idx;

	if (!t)
		return false;

	idx = eytzinger0_find_le(t->entries, t->nr,
				 sizeof(t->entries[0]),
				 journal_seq_blacklist_table_cmp,
				 &search);
	if (idx < 0)
		return false;

	BUG_ON(t->entries[idx].start > seq);

	if (seq >= t->entries[idx].end)
		return false;

	if (dirty)
		t->entries[idx].dirty = true;
	return true;
}

int bch2_blacklist_table_initialize(struct bch_fs *c)
{
	struct bch_sb_field_journal_seq_blacklist *bl =
		bch2_sb_field_get(c->disk_sb.sb, journal_seq_blacklist);
	struct journal_seq_blacklist_table *t;
	unsigned i, nr = blacklist_nr_entries(bl);

	if (!bl)
		return 0;

	t = kzalloc(sizeof(*t) + sizeof(t->entries[0]) * nr,
		    GFP_KERNEL);
	if (!t)
		return -BCH_ERR_ENOMEM_blacklist_table_init;

	t->nr = nr;

	for (i = 0; i < nr; i++) {
		t->entries[i].start	= le64_to_cpu(bl->start[i].start);
		t->entries[i].end	= le64_to_cpu(bl->start[i].end);
	}

	eytzinger0_sort(t->entries,
			t->nr,
			sizeof(t->entries[0]),
			journal_seq_blacklist_table_cmp,
			NULL);

	kfree(c->journal_seq_blacklist_table);
	c->journal_seq_blacklist_table = t;
	return 0;
}

static int bch2_sb_journal_seq_blacklist_validate(struct bch_sb *sb,
						  struct bch_sb_field *f,
						  struct printbuf *err)
{
	struct bch_sb_field_journal_seq_blacklist *bl =
		field_to_type(f, journal_seq_blacklist);
	unsigned i, nr = blacklist_nr_entries(bl);

	for (i = 0; i < nr; i++) {
		struct journal_seq_blacklist_entry *e = bl->start + i;

		if (le64_to_cpu(e->start) >=
		    le64_to_cpu(e->end)) {
			prt_printf(err, "entry %u start >= end (%llu >= %llu)",
			       i, le64_to_cpu(e->start), le64_to_cpu(e->end));
			return -BCH_ERR_invalid_sb_journal_seq_blacklist;
		}

		if (i + 1 < nr &&
		    le64_to_cpu(e[0].end) >
		    le64_to_cpu(e[1].start)) {
			prt_printf(err, "entry %u out of order with next entry (%llu > %llu)",
			       i + 1, le64_to_cpu(e[0].end), le64_to_cpu(e[1].start));
			return -BCH_ERR_invalid_sb_journal_seq_blacklist;
		}
	}

	return 0;
}

static void bch2_sb_journal_seq_blacklist_to_text(struct printbuf *out,
						  struct bch_sb *sb,
						  struct bch_sb_field *f)
{
	struct bch_sb_field_journal_seq_blacklist *bl =
		field_to_type(f, journal_seq_blacklist);
	struct journal_seq_blacklist_entry *i;
	unsigned nr = blacklist_nr_entries(bl);

	for (i = bl->start; i < bl->start + nr; i++) {
		if (i != bl->start)
			prt_printf(out, " ");

		prt_printf(out, "%llu-%llu",
		       le64_to_cpu(i->start),
		       le64_to_cpu(i->end));
	}
	prt_newline(out);
}

const struct bch_sb_field_ops bch_sb_field_ops_journal_seq_blacklist = {
	.validate	= bch2_sb_journal_seq_blacklist_validate,
	.to_text	= bch2_sb_journal_seq_blacklist_to_text
};

void bch2_blacklist_entries_gc(struct work_struct *work)
{
	struct bch_fs *c = container_of(work, struct bch_fs,
					journal_seq_blacklist_gc_work);
	struct journal_seq_blacklist_table *t;
	struct bch_sb_field_journal_seq_blacklist *bl;
	struct journal_seq_blacklist_entry *src, *dst;
	struct btree_trans *trans = bch2_trans_get(c);
	unsigned i, nr, new_nr;
	int ret;

	for (i = 0; i < BTREE_ID_NR; i++) {
		struct btree_iter iter;
		struct btree *b;

		bch2_trans_node_iter_init(trans, &iter, i, POS_MIN,
					  0, 0, BTREE_ITER_PREFETCH);
retry:
		bch2_trans_begin(trans);

		b = bch2_btree_iter_peek_node(&iter);

		while (!(ret = PTR_ERR_OR_ZERO(b)) &&
		       b &&
		       !test_bit(BCH_FS_stopping, &c->flags))
			b = bch2_btree_iter_next_node(&iter);

		if (bch2_err_matches(ret, BCH_ERR_transaction_restart))
			goto retry;

		bch2_trans_iter_exit(trans, &iter);
	}

	bch2_trans_put(trans);
	if (ret)
		return;

	mutex_lock(&c->sb_lock);
	bl = bch2_sb_field_get(c->disk_sb.sb, journal_seq_blacklist);
	if (!bl)
		goto out;

	nr = blacklist_nr_entries(bl);
	dst = bl->start;

	t = c->journal_seq_blacklist_table;
	BUG_ON(nr != t->nr);

	for (src = bl->start, i = eytzinger0_first(t->nr);
	     src < bl->start + nr;
	     src++, i = eytzinger0_next(i, nr)) {
		BUG_ON(t->entries[i].start	!= le64_to_cpu(src->start));
		BUG_ON(t->entries[i].end	!= le64_to_cpu(src->end));

		if (t->entries[i].dirty)
			*dst++ = *src;
	}

	new_nr = dst - bl->start;

	bch_info(c, "nr blacklist entries was %u, now %u", nr, new_nr);

	if (new_nr != nr) {
		bl = bch2_sb_field_resize(&c->disk_sb, journal_seq_blacklist,
				new_nr ? sb_blacklist_u64s(new_nr) : 0);
		BUG_ON(new_nr && !bl);

		if (!new_nr)
			c->disk_sb.sb->features[0] &= cpu_to_le64(~(1ULL << BCH_FEATURE_journal_seq_blacklist_v3));

		bch2_write_super(c);
	}
out:
	mutex_unlock(&c->sb_lock);
}
