Skip to content

Commit 2a0f2ef

Browse files
committed
Replace L1 dirty walk with DNODE_FIND_DIRTY
This walk is inherently racy w.r.t. dbuf eviction and sync. Consider: 0. A large sparse file with 3 levels of indirection. 1. A new L1 block is added to a brand new L2 block. 2. The L1 block syncs out and is immediately evicted. 3. Before the L3->L2 BP is updated in the L3 block, dnode_free_range attempts to free the new L1. In this case neither dnode_dirty_l1range nor dnode_next_offset can find the newly synced-out L1 block and its L0 blocks: - dnode_dirty_l1range uses in-memory index but the L1 is evicted - dnode_next_offset considers on-disk BPs but the L3->L2 is missing And then free_children will later PANIC because the L1 was not dirtied during open context when freeing the range. This case was found during testing llseek(SEEK_HOLE/SEEK_DATA) without txg sync and is distinct from the _other_ free_childen panic found and addressed by openzfs#16025. The fix is to replace dnode_dirty_l1range with dnode_next_offset(DNODE_FIND_DIRTY) which knows how to find all dirty L1 blocks. This PR also changes to use minlvl=1 to avoid redirtying L2 blocks that are only dirtied in a prior txg. Successive frees otherwise needlessly redirty already-empty L1s which wastes time during txg sync turning them back into holes. Signed-off-by: Robert Evans <[email protected]>
1 parent 0027d2a commit 2a0f2ef

File tree

1 file changed

+8
-74
lines changed

1 file changed

+8
-74
lines changed

module/zfs/dnode.c

Lines changed: 8 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,76 +2109,6 @@ dnode_dirty_l1(dnode_t *dn, uint64_t l1blkid, dmu_tx_t *tx)
21092109
}
21102110
}
21112111

2112-
/*
2113-
* Dirty all the in-core level-1 dbufs in the range specified by start_blkid
2114-
* and end_blkid.
2115-
*/
2116-
static void
2117-
dnode_dirty_l1range(dnode_t *dn, uint64_t start_blkid, uint64_t end_blkid,
2118-
dmu_tx_t *tx)
2119-
{
2120-
dmu_buf_impl_t *db_search;
2121-
dmu_buf_impl_t *db;
2122-
avl_index_t where;
2123-
2124-
db_search = kmem_zalloc(sizeof (dmu_buf_impl_t), KM_SLEEP);
2125-
2126-
mutex_enter(&dn->dn_dbufs_mtx);
2127-
2128-
db_search->db_level = 1;
2129-
db_search->db_blkid = start_blkid + 1;
2130-
db_search->db_state = DB_SEARCH;
2131-
for (;;) {
2132-
2133-
db = avl_find(&dn->dn_dbufs, db_search, &where);
2134-
if (db == NULL)
2135-
db = avl_nearest(&dn->dn_dbufs, where, AVL_AFTER);
2136-
2137-
if (db == NULL || db->db_level != 1 ||
2138-
db->db_blkid >= end_blkid) {
2139-
break;
2140-
}
2141-
2142-
/*
2143-
* Setup the next blkid we want to search for.
2144-
*/
2145-
db_search->db_blkid = db->db_blkid + 1;
2146-
ASSERT3U(db->db_blkid, >=, start_blkid);
2147-
2148-
/*
2149-
* If the dbuf transitions to DB_EVICTING while we're trying
2150-
* to dirty it, then we will be unable to discover it in
2151-
* the dbuf hash table. This will result in a call to
2152-
* dbuf_create() which needs to acquire the dn_dbufs_mtx
2153-
* lock. To avoid a deadlock, we drop the lock before
2154-
* dirtying the level-1 dbuf.
2155-
*/
2156-
mutex_exit(&dn->dn_dbufs_mtx);
2157-
dnode_dirty_l1(dn, db->db_blkid, tx);
2158-
mutex_enter(&dn->dn_dbufs_mtx);
2159-
}
2160-
2161-
#ifdef ZFS_DEBUG
2162-
/*
2163-
* Walk all the in-core level-1 dbufs and verify they have been dirtied.
2164-
*/
2165-
db_search->db_level = 1;
2166-
db_search->db_blkid = start_blkid + 1;
2167-
db_search->db_state = DB_SEARCH;
2168-
db = avl_find(&dn->dn_dbufs, db_search, &where);
2169-
if (db == NULL)
2170-
db = avl_nearest(&dn->dn_dbufs, where, AVL_AFTER);
2171-
for (; db != NULL; db = AVL_NEXT(&dn->dn_dbufs, db)) {
2172-
if (db->db_level != 1 || db->db_blkid >= end_blkid)
2173-
break;
2174-
if (db->db_state != DB_EVICTING)
2175-
ASSERT(db->db_dirtycnt > 0);
2176-
}
2177-
#endif
2178-
kmem_free(db_search, sizeof (dmu_buf_impl_t));
2179-
mutex_exit(&dn->dn_dbufs_mtx);
2180-
}
2181-
21822112
void
21832113
dnode_set_dirtyctx(dnode_t *dn, dmu_tx_t *tx, const void *tag)
21842114
{
@@ -2362,8 +2292,6 @@ dnode_free_range(dnode_t *dn, uint64_t off, uint64_t len, dmu_tx_t *tx)
23622292
if (last != first)
23632293
dnode_dirty_l1(dn, last, tx);
23642294

2365-
dnode_dirty_l1range(dn, first, last, tx);
2366-
23672295
int shift = dn->dn_datablkshift + dn->dn_indblkshift -
23682296
SPA_BLKPTRSHIFT;
23692297
for (uint64_t i = first + 1; i < last; i++) {
@@ -2372,10 +2300,16 @@ dnode_free_range(dnode_t *dn, uint64_t off, uint64_t len, dmu_tx_t *tx)
23722300
* level-1 indirect block at or after i. Note
23732301
* that dnode_next_offset() operates in terms of
23742302
* level-0-equivalent bytes.
2303+
* N.B. this uses minlvl=1 to avoid redirtying L1s
2304+
* freed in prior txgs as minlvl=1 checks L0s and skips
2305+
* dirty L1s containing no L0 BPs or only freed L0s.
2306+
* minlvl=2 would also work, but that would then match
2307+
* every dirty L1 pointer unconditionally.
23752308
*/
23762309
uint64_t ibyte = i << shift;
2377-
int err = dnode_next_offset(dn, DNODE_FIND_HAVELOCK,
2378-
&ibyte, 2, 1, 0);
2310+
int err = dnode_next_offset(
2311+
dn, DNODE_FIND_HAVELOCK | DNODE_FIND_DIRTY,
2312+
&ibyte, 1, 1, 0);
23792313
i = ibyte >> shift;
23802314
if (i >= last)
23812315
break;

0 commit comments

Comments
 (0)