Skip to content

Commit 43b4ef3

Browse files
committed
qa: referent inodes - unlink, stray_reintegration
The following tests are borrowed from existing test_strays. and adjusted stray perf count numbers with referent inodes. Also, added validation of referent_inodes list test_mv_hardlink_cleanup_with_referent test_hardlink_reintegration_with_referent Also, added unlink test and ALLOW_REFERENT_INODES flag to cephfs_test_case. If ALLOW_REFERENT_INODES flag is set in any test class, referent inodes is enabled for all the tests in the class. Fixes: https://tracker.ceph.com/issues/69339 Signed-off-by: Kotresh HR <[email protected]>
1 parent ecd2496 commit 43b4ef3

File tree

3 files changed

+318
-2
lines changed

3 files changed

+318
-2
lines changed

qa/tasks/cephfs/cephfs_test_case.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class CephFSTestCase(CephTestCase):
7171
CLIENTS_REQUIRED = 1
7272
MDSS_REQUIRED = 1
7373
REQUIRE_ONE_CLIENT_REMOTE = False
74+
ALLOW_REFERENT_INODES = False
7475

7576
# Whether to create the default filesystem during setUp
7677
REQUIRE_FILESYSTEM = True
@@ -190,6 +191,10 @@ def setUp(self):
190191
for i in range(0, self.CLIENTS_REQUIRED):
191192
self.mounts[i].mount_wait()
192193

194+
# enable referent inodes
195+
if self.ALLOW_REFERENT_INODES:
196+
self.fs.set_allow_referent_inodes(True)
197+
193198
if self.REQUIRE_BACKUP_FILESYSTEM:
194199
if not self.REQUIRE_FILESYSTEM:
195200
self.skipTest("backup filesystem requires a primary filesystem as well")

qa/tasks/cephfs/filesystem.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,15 @@ def _enumerate_data_objects(self, ino, size):
16471647

16481648
return want_objects, exist_objects
16491649

1650+
def list_data_objects (self):
1651+
"""
1652+
Get the list of existing data objects in the data pool
1653+
"""
1654+
existing_objects = self.rados(["ls"], pool=self.get_data_pool_name(), stdout=StringIO()).stdout.getvalue().split("\n")
1655+
1656+
return existing_objects
1657+
1658+
16501659
def data_objects_present(self, ino, size):
16511660
"""
16521661
Check that *all* the expected data objects for an inode are present in the data pool

qa/tasks/cephfs/test_referent.py

Lines changed: 304 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,56 @@
99
class TestReferentInode(CephFSTestCase):
1010
MDSS_REQUIRED = 1
1111
CLIENTS_REQUIRED = 1
12+
ALLOW_REFERENT_INODES = True
13+
14+
def get_mdc_stat(self, name, mds_id=None):
15+
return self.get_stat("mds_cache", name, mds_id)
16+
17+
def get_stat(self, subsys, name, mds_id=None):
18+
return self.fs.mds_asok(['perf', 'dump', subsys, name],
19+
mds_id=mds_id)[subsys][name]
20+
21+
def _wait_for_counter(self, subsys, counter, expect_val, timeout=60,
22+
mds_id=None):
23+
self.wait_until_equal(
24+
lambda: self.get_stat(subsys, counter, mds_id),
25+
expect_val=expect_val, timeout=timeout,
26+
reject_fn=lambda x: x > expect_val
27+
)
28+
29+
def assert_backtrace(self, ino, expected_path):
30+
"""
31+
Assert that the backtrace in the data pool for an inode matches
32+
an expected /foo/bar path.
33+
"""
34+
expected_elements = expected_path.strip("/").split("/")
35+
bt = self.fs.read_backtrace(ino)
36+
actual_elements = list(reversed([dn['dname'] for dn in bt['ancestors']]))
37+
self.assertListEqual(expected_elements, actual_elements)
38+
39+
def get_backtrace_path(self, ino):
40+
bt = self.fs.read_backtrace(ino)
41+
elements = reversed([dn['dname'] for dn in bt['ancestors']])
42+
return "/".join(elements)
43+
44+
def assert_purge_idle(self):
45+
"""
46+
Assert that the MDS perf counters indicate no strays exist and
47+
no ongoing purge activity. Sanity check for when PurgeQueue should
48+
be idle.
49+
"""
50+
mdc_stats = self.fs.mds_asok(['perf', 'dump', "mds_cache"])['mds_cache']
51+
pq_stats = self.fs.mds_asok(['perf', 'dump', "purge_queue"])['purge_queue']
52+
self.assertEqual(mdc_stats["num_strays"], 0)
53+
self.assertEqual(mdc_stats["num_strays_delayed"], 0)
54+
self.assertEqual(pq_stats["pq_executing"], 0)
55+
self.assertEqual(pq_stats["pq_executing_ops"], 0)
1256

1357
def test_referent_link(self):
1458
"""
1559
test_referent_link - Test creation of referent inode and backtrace on link
1660
"""
1761

18-
self.fs.set_allow_referent_inodes(True);
1962
self.mount_a.run_shell(["mkdir", "dir0"])
2063
self.mount_a.run_shell(["touch", "dir0/file1"])
2164
self.mount_a.run_shell(["ln", "dir0/file1", "dir0/hardlink_file1"])
@@ -47,8 +90,267 @@ def test_referent_link(self):
4790
self.assertEqual(referent_inode['ino'], backtrace['ino'])
4891
self.assertEqual([dir_ino, 1], [a['dirino'] for a in backtrace['ancestors']])
4992

93+
def test_referent_unlink(self):
94+
"""
95+
test_referent_unlink - Test deletion of referent inode and backtrace on unlink
96+
"""
97+
98+
self.mount_a.run_shell(["mkdir", "dir0"])
99+
self.mount_a.run_shell(["touch", "dir0/file1"])
100+
self.mount_a.run_shell(["ln", "dir0/file1", "dir0/hardlink_file1"])
101+
file_ino = self.mount_a.path_to_ino("dir0/file1")
102+
103+
# write out the backtrace - this would writeout the backtrace
104+
# of the newly introduced referent inode to the data pool.
105+
self.fs.mds_asok(["flush", "journal"])
106+
107+
# read the primary inode
108+
dir_ino = self.mount_a.path_to_ino("dir0")
109+
file1_inode = self.fs.read_meta_inode(dir_ino, "file1")
110+
111+
# read the referent inode from metadata pool
112+
referent_inode = self.fs.read_meta_inode(dir_ino, "hardlink_file1")
113+
114+
self.assertFalse(file1_inode['ino'] == referent_inode['ino'])
115+
116+
# reverse link - the real inode should track the referent inode number
117+
self.assertIn(referent_inode['ino'], file1_inode['referent_inodes'])
118+
# link - the referent inode should point to real inode
119+
self.assertEqual(referent_inode['remote_ino'], file_ino)
120+
121+
# backtrace of referent inode from data pool
122+
backtrace = self.fs.read_backtrace(referent_inode['ino'])
123+
# path validation
124+
self.assertEqual(['hardlink_file1', 'dir0'], [a['dname'] for a in backtrace['ancestors']])
125+
# inode validation
126+
self.assertEqual(referent_inode['ino'], backtrace['ino'])
127+
self.assertEqual([dir_ino, 1], [a['dirino'] for a in backtrace['ancestors']])
128+
129+
# unlink
130+
self.mount_a.run_shell(["rm", "-f", "dir0/hardlink_file1"])
131+
self.fs.mds_asok(["flush", "journal"])
132+
# referent inode should be removed from the real inode
133+
# in-memory
134+
file1_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(file_ino)])
135+
self.assertNotIn(referent_inode['ino'], file1_inode_dump['referent_inodes'])
136+
# on-disk
137+
file1_inode = self.fs.read_meta_inode(dir_ino, "file1")
138+
self.assertNotIn(referent_inode['ino'], file1_inode['referent_inodes'])
139+
# referent inode object from data pool should be deleted
140+
with self.assertRaises(ObjectNotFound):
141+
self.fs.read_backtrace(referent_inode['ino'])
142+
# omap value of referent inode from metadata pool should be deleted
143+
with self.assertRaises(ObjectNotFound):
144+
self.fs.read_meta_inode(dir_ino, "hardlink_file1")
145+
50146
def test_hardlink_reintegration_with_referent(self):
51-
pass
147+
"""
148+
test_hardlink_reintegration_with_referent - That removal of primary dentry
149+
of hardlinked inode results in reintegration of inode into the previously
150+
referent remote dentry, rather than lingering as a stray indefinitely.
151+
"""
152+
153+
# Write some bytes to file_a
154+
size_mb = 8
155+
self.mount_a.run_shell(["mkdir", "dir_1"])
156+
self.mount_a.write_n_mb("dir_1/file_a", size_mb)
157+
ino = self.mount_a.path_to_ino("dir_1/file_a")
158+
159+
# Create a hardlink named file_b
160+
self.mount_a.run_shell(["mkdir", "dir_2"])
161+
self.mount_a.run_shell(["ln", "dir_1/file_a", "dir_2/file_b"])
162+
self.assertEqual(self.mount_a.path_to_ino("dir_2/file_b"), ino)
163+
164+
# Flush journal
165+
self.fs.mds_asok(['flush', 'journal'])
166+
167+
# Validate referent_inodes list
168+
# read the primary inode
169+
dir_1_ino = self.mount_a.path_to_ino("dir_1")
170+
file_a_inode = self.fs.read_meta_inode(dir_1_ino, "file_a")
171+
# read the hardlink referent inode
172+
dir_2_ino = self.mount_a.path_to_ino("dir_2")
173+
file_b_inode = self.fs.read_meta_inode(dir_2_ino, "file_b")
174+
self.assertIn(file_b_inode['ino'], file_a_inode['referent_inodes'])
175+
# link - the referent inode should point to real inode
176+
self.assertEqual(file_b_inode['remote_ino'], ino)
177+
178+
# See that backtrace for the file points to the file_a path
179+
pre_unlink_bt = self.fs.read_backtrace(ino)
180+
self.assertEqual(pre_unlink_bt['ancestors'][0]['dname'], "file_a")
181+
182+
# empty mds cache. otherwise mds reintegrates stray when unlink finishes
183+
self.mount_a.umount_wait()
184+
self.fs.mds_asok(['flush', 'journal'])
185+
self.fs.mds_fail_restart()
186+
self.fs.wait_for_daemons()
187+
self.mount_a.mount_wait()
188+
189+
# Unlink file_a
190+
self.mount_a.run_shell(["rm", "-f", "dir_1/file_a"]) #strays_created=1
191+
192+
# See that a stray was created
193+
self.assertEqual(self.get_mdc_stat("num_strays"), 1)
194+
self.assertEqual(self.get_mdc_stat("strays_created"), 1)
195+
196+
# Wait, see that data objects are still present (i.e. that the
197+
# stray did not advance to purging given time)
198+
time.sleep(30)
199+
self.assertTrue(self.fs.data_objects_present(ino, size_mb * 1024 * 1024))
200+
self.assertEqual(self.get_mdc_stat("strays_enqueued"), 0)
201+
202+
# See that before reintegration, the inode's backtrace points to a stray dir
203+
self.fs.mds_asok(['flush', 'journal'])
204+
self.assertTrue(self.get_backtrace_path(ino).startswith("stray"))
205+
206+
last_reintegrated = self.get_mdc_stat("strays_reintegrated")
207+
208+
# Do a metadata operation on the remaining link (mv is heavy handed, but
209+
# others like touch may be satisfied from caps without poking MDS)
210+
self.mount_a.run_shell(["mv", "dir_2/file_b", "dir_2/file_c"])
211+
212+
#strays_created=2, after reintegration
213+
214+
# Stray reintegration should happen as a result of the eval_remote call
215+
# on responding to a client request.
216+
self.wait_until_equal(
217+
lambda: self.get_mdc_stat("num_strays"),
218+
expect_val=0,
219+
timeout=60
220+
)
221+
222+
# See the reintegration counter increment
223+
curr_reintegrated = self.get_mdc_stat("strays_reintegrated")
224+
self.assertGreater(curr_reintegrated, last_reintegrated)
225+
last_reintegrated = curr_reintegrated
226+
227+
# Flush the journal
228+
self.fs.mds_asok(['flush', 'journal'])
229+
230+
# Validate for empty referent_inodes list after reintegration
231+
file_c_inode = self.fs.read_meta_inode(dir_2_ino, "file_c")
232+
self.assertEqual(file_c_inode['referent_inodes'], [])
233+
234+
# See that the backtrace for the file points to the remaining link's path
235+
post_reint_bt = self.fs.read_backtrace(ino)
236+
self.assertEqual(post_reint_bt['ancestors'][0]['dname'], "file_c")
237+
238+
# mds should reintegrates stray when unlink finishes
239+
self.mount_a.run_shell(["ln", "dir_2/file_c", "dir_2/file_d"])
240+
241+
# validate in-memory referent inodes list for non-emptiness
242+
file_c_ino = self.mount_a.path_to_ino("dir_2/file_c")
243+
file_c_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(file_c_ino)])
244+
self.assertTrue(file_c_inode_dump['referent_inodes'])
245+
246+
# mds should reintegrates stray when unlink finishes
247+
self.mount_a.run_shell(["rm", "-f", "dir_2/file_c"])
248+
249+
#strays_created=4 (primary unlink=1, reintegration=1 referent file_d goes stray)
250+
251+
# Stray reintegration should happen as a result of the notify_stray call
252+
# on completion of unlink
253+
self.wait_until_equal(
254+
lambda: self.get_mdc_stat("num_strays"),
255+
expect_val=0,
256+
timeout=60
257+
)
258+
259+
# See the reintegration counter increment
260+
curr_reintegrated = self.get_mdc_stat("strays_reintegrated")
261+
self.assertGreater(curr_reintegrated, last_reintegrated)
262+
last_reintegrated = curr_reintegrated
263+
264+
# validate in-memory referent inodes list for emptiness
265+
file_d_ino = self.mount_a.path_to_ino("dir_2/file_d")
266+
file_d_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(file_d_ino)])
267+
self.assertEqual(file_d_inode_dump['referent_inodes'], [])
268+
269+
# Flush the journal
270+
self.fs.mds_asok(['flush', 'journal'])
271+
272+
# See that the backtrace for the file points to the newest link's path
273+
post_reint_bt = self.fs.read_backtrace(ino)
274+
self.assertEqual(post_reint_bt['ancestors'][0]['dname'], "file_d")
275+
276+
# Now really delete it
277+
self.mount_a.run_shell(["rm", "-f", "dir_2/file_d"])
278+
# strays_created=5, after unlink
279+
self._wait_for_counter("mds_cache", "strays_enqueued", 3)
280+
self._wait_for_counter("purge_queue", "pq_executed", 3)
281+
282+
self.assert_purge_idle()
283+
self.assertTrue(self.fs.data_objects_absent(ino, size_mb * 1024 * 1024))
284+
285+
# We caused the inode to go stray 5 times. Follow the comments above for the count
286+
self.assertEqual(self.get_mdc_stat("strays_created"), 5)
287+
# We purged it at the last (1 primary, 2 hardlinks with referent)
288+
self.assertEqual(self.get_mdc_stat("strays_enqueued"), 3)
289+
290+
# Flush the journal
291+
self.fs.mds_asok(['flush', 'journal'])
292+
# All data objects include referent inodes should go away
293+
self.assertEqual(self.fs.list_data_objects(), [''])
294+
295+
def test_mv_hardlink_cleanup_with_referent(self):
296+
"""
297+
test_mv_hardlink_cleanup_with_referent - That when doing a rename from A to B, and B
298+
has hardlinks, then we make a stray for B which is then reintegrated into one of it's
299+
hardlinks with referent inodes.
300+
"""
301+
# Create file_a, file_b, and a hardlink to file_b
302+
size_mb = 8
303+
self.mount_a.write_n_mb("file_a", size_mb)
304+
file_a_ino = self.mount_a.path_to_ino("file_a")
305+
306+
self.mount_a.write_n_mb("file_b", size_mb)
307+
file_b_ino = self.mount_a.path_to_ino("file_b")
308+
309+
# empty referent_inodes list for file_b
310+
file_b_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(file_b_ino)])
311+
self.assertEqual(file_b_inode_dump['referent_inodes'], [])
312+
313+
self.mount_a.run_shell(["ln", "file_b", "linkto_b"])
314+
self.assertEqual(self.mount_a.path_to_ino("linkto_b"), file_b_ino)
315+
316+
# linkto_b's referent inode got added to referent_inodes list of file_b
317+
# flush the journal
318+
self.fs.mds_asok(['flush', 'journal'])
319+
file_b_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(file_b_ino)])
320+
self.assertTrue(file_b_inode_dump['referent_inodes'])
321+
referent_ino_linkto_b = file_b_inode_dump['referent_inodes'][0]
322+
linkto_b_referent_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(referent_ino_linkto_b)])
323+
self.assertEqual(linkto_b_referent_inode_dump['remote_ino'], file_b_ino )
324+
325+
# mv file_a file_b
326+
self.mount_a.run_shell(["mv", "file_a", "file_b"])
327+
328+
# Stray reintegration should happen as a result of the notify_stray call on
329+
# completion of rename
330+
self.wait_until_equal(
331+
lambda: self.get_mdc_stat("num_strays"),
332+
expect_val=0,
333+
timeout=60
334+
)
335+
336+
self.assertEqual(self.get_mdc_stat("strays_created"), 2)
337+
self.assertGreaterEqual(self.get_mdc_stat("strays_reintegrated"), 1)
338+
339+
# No data objects should have been deleted, as both files still have linkage.
340+
self.assertTrue(self.fs.data_objects_present(file_a_ino, size_mb * 1024 * 1024))
341+
self.assertTrue(self.fs.data_objects_present(file_b_ino, size_mb * 1024 * 1024))
342+
343+
# After stray reintegration, referent inode should be removed from list
344+
linkto_b_ino = self.mount_a.path_to_ino("linkto_b")
345+
linkto_b_inode_dump = self.fs.mds_asok(['dump', 'inode', hex(linkto_b_ino)])
346+
self.assertEqual(linkto_b_inode_dump['referent_inodes'], [])
347+
# referent ino should be deleted
348+
self.assertTrue(self.fs.data_objects_absent(referent_ino_linkto_b, size_mb * 1024 * 1024))
349+
350+
self.fs.mds_asok(['flush', 'journal'])
351+
352+
post_reint_bt = self.fs.read_backtrace(file_b_ino)
353+
self.assertEqual(post_reint_bt['ancestors'][0]['dname'], "linkto_b")
52354

53355
def test_multiple_referent_post_reintegration(self):
54356
pass

0 commit comments

Comments
 (0)