4 # Tests for active mirroring
6 # Copyright (C) 2018 Red Hat, Inc.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from typing import List, Optional
28 from iotests import qemu_img
30 source_img = os.path.join(iotests.test_dir, 'source.' + iotests.imgfmt)
31 target_img = os.path.join(iotests.test_dir, 'target.' + iotests.imgfmt)
33 class TestActiveMirror(iotests.QMPTestCase):
34 image_len = 128 * 1024 * 1024 # MB
35 potential_writes_in_flight = True
38 qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
39 qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
41 blk_source = {'id': 'source',
43 'node-name': 'source-node',
44 'driver': iotests.imgfmt,
45 'file': {'driver': 'blkdebug',
46 'image': {'driver': 'file',
47 'filename': source_img}}}
49 blk_target = {'node-name': 'target-node',
50 'driver': iotests.imgfmt,
51 'file': {'driver': 'file',
52 'filename': target_img}}
54 self.vm = iotests.VM()
55 self.vm.add_drive_raw(self.vm.qmp_to_opts(blk_source))
56 self.vm.add_blockdev(self.vm.qmp_to_opts(blk_target))
57 self.vm.add_device('virtio-blk,id=vblk,drive=source')
63 if not self.potential_writes_in_flight:
64 self.assertTrue(iotests.compare_images(source_img, target_img),
65 'mirror target does not match source')
70 def doActiveIO(self, sync_source_and_target):
71 # Fill the source image
72 self.vm.hmp_qemu_io('source',
73 'write -P 1 0 %i' % self.image_len);
75 # Start some background requests
76 for offset in range(1 * self.image_len // 8, 3 * self.image_len // 8, 1024 * 1024):
77 self.vm.hmp_qemu_io('source', 'aio_write -P 2 %i 1M' % offset)
78 for offset in range(2 * self.image_len // 8, 3 * self.image_len // 8, 1024 * 1024):
79 self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
82 self.vm.cmd('blockdev-mirror',
84 filter_node_name='mirror-node',
88 copy_mode='write-blocking')
90 # Start some more requests
91 for offset in range(3 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
92 self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
93 for offset in range(4 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
94 self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
96 # Wait for the READY event
97 self.wait_ready(drive='mirror')
99 # Now start some final requests; all of these (which land on
100 # the source) should be settled using the active mechanism.
101 # The mirror code itself asserts that the source BDS's dirty
102 # bitmap will stay clean between READY and COMPLETED.
103 for offset in range(5 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
104 self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
105 for offset in range(6 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
106 self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
108 if sync_source_and_target:
109 # If source and target should be in sync after the mirror,
110 # we have to flush before completion
111 self.vm.hmp_qemu_io('source', 'aio_flush')
112 self.potential_writes_in_flight = False
114 self.complete_and_wait(drive='mirror', wait_ready=False)
116 def testActiveIO(self):
117 self.doActiveIO(False)
119 def testActiveIOFlushed(self):
120 self.doActiveIO(True)
122 def testUnalignedActiveIO(self):
123 # Fill the source image
124 result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
126 # Start the block job (very slowly)
127 self.vm.cmd('blockdev-mirror',
129 filter_node_name='mirror-node',
130 device='source-node',
131 target='target-node',
133 copy_mode='write-blocking',
134 buf_size=(1048576 // 4),
137 # Start an unaligned request to a dirty area
138 result = self.vm.hmp_qemu_io('source', 'write -P 2 %i 1' % (1048576 + 42))
141 self.vm.cmd('block-job-set-speed', device='mirror', speed=0)
142 self.complete_and_wait(drive='mirror')
144 self.potential_writes_in_flight = False
146 def testIntersectingActiveIO(self):
147 # Fill the source image
148 result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
150 # Start the block job (very slowly)
151 self.vm.cmd('blockdev-mirror',
153 filter_node_name='mirror-node',
154 device='source-node',
155 target='target-node',
157 copy_mode='write-blocking',
160 self.vm.hmp_qemu_io('source', 'break write_aio A')
161 self.vm.hmp_qemu_io('source', 'aio_write 0 1M') # 1
162 self.vm.hmp_qemu_io('source', 'wait_break A')
163 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 2
164 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 3
166 # Now 2 and 3 are in mirror_wait_on_conflicts, waiting for 1
168 self.vm.hmp_qemu_io('source', 'break write_aio B')
169 self.vm.hmp_qemu_io('source', 'aio_write 1M 2M') # 4
170 self.vm.hmp_qemu_io('source', 'wait_break B')
172 # 4 doesn't wait for 2 and 3, because they didn't yet set
173 # in_flight_bitmap. So, nothing prevents 4 to go except for our
176 self.vm.hmp_qemu_io('source', 'resume A')
178 # Now we resumed 1, so 2 and 3 goes to the next iteration of while loop
179 # in mirror_wait_on_conflicts(). They don't exit, as bitmap is dirty
181 # In the past at that point 2 and 3 would wait for each other producing
182 # a dead-lock. Now this is fixed and they will wait for request 4.
184 self.vm.hmp_qemu_io('source', 'resume B')
186 # After resuming 4, one of 2 and 3 goes first and set in_flight_bitmap,
187 # so the other will wait for it.
189 self.vm.cmd('block-job-set-speed', device='mirror', speed=0)
190 self.complete_and_wait(drive='mirror')
192 self.potential_writes_in_flight = False
195 class TestThrottledWithNbdExportBase(iotests.QMPTestCase):
196 image_len = 128 * 1024 * 1024 # MB
197 iops: Optional[int] = None
198 background_processes: List['subprocess.Popen[str]'] = []
201 # Must be set by subclasses
202 self.assertIsNotNone(self.iops)
204 qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
205 qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
207 self.vm = iotests.VM()
210 self.vm.cmd('object-add', **{
211 'qom-type': 'throttle-group',
214 'iops-total': self.iops,
215 'iops-total-max': self.iops
219 self.vm.cmd('blockdev-add', **{
220 'node-name': 'source-node',
221 'driver': 'throttle',
222 'throttle-group': 'thrgr',
224 'driver': iotests.imgfmt,
227 'filename': source_img
232 self.vm.cmd('blockdev-add', **{
233 'node-name': 'target-node',
234 'driver': iotests.imgfmt,
237 'filename': target_img
241 self.nbd_sock = iotests.file_path('nbd.sock',
242 base_dir=iotests.sock_dir)
243 self.nbd_url = f'nbd+unix:///source-node?socket={self.nbd_sock}'
245 self.vm.cmd('nbd-server-start', addr={
248 'path': self.nbd_sock
252 self.vm.cmd('block-export-add', id='exp0', type='nbd',
253 node_name='source-node', writable=True)
256 # Wait for background requests to settle
259 p = self.background_processes.pop()
264 except subprocess.TimeoutExpired:
265 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
269 # Cancel ongoing block jobs
270 for job in self.vm.qmp('query-jobs')['return']:
271 self.vm.qmp('block-job-cancel', device=job['id'], force=True)
274 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
275 if len(self.vm.qmp('query-jobs')['return']) == 0:
279 os.remove(source_img)
280 os.remove(target_img)
283 class TestLowThrottledWithNbdExport(TestThrottledWithNbdExportBase):
286 def testUnderLoad(self):
288 Throttle the source node, then issue a whole bunch of external requests
289 while the mirror job (in write-blocking mode) is running. We want to
290 see background requests being issued even while the source is under
291 full load by active writes, so that progress can be made towards READY.
294 # Fill the first half of the source image; do not fill the second half,
295 # that is where we will have active requests occur. This ensures that
296 # active mirroring itself will not directly contribute to the job's
297 # progress (because when the job was started, those areas were not
298 # intended to be copied, so active mirroring will only lead to not
299 # losing progress, but also not making any).
300 self.vm.hmp_qemu_io('source-node',
301 f'aio_write -P 1 0 {self.image_len // 2}')
302 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
304 # Launch the mirror job
305 mirror_buf_size = 65536
306 self.vm.cmd('blockdev-mirror',
308 filter_node_name='mirror-node',
309 device='source-node',
310 target='target-node',
312 copy_mode='write-blocking',
313 buf_size=mirror_buf_size)
315 # We create the external requests via qemu-io processes on the NBD
316 # server. Have their offset start in the middle of the image so they
317 # do not overlap with the background requests (which start from the
319 active_request_offset = self.image_len // 2
320 active_request_len = 4096
322 # Create enough requests to saturate the node for 5 seconds
323 for _ in range(0, 5 * self.iops):
324 req = f'write -P 42 {active_request_offset} {active_request_len}'
325 active_request_offset += active_request_len
326 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
327 self.background_processes += [p]
329 # Now advance the clock one I/O operation at a time by the 4 seconds
330 # (i.e. one less than 5). We expect the mirror job to issue background
331 # operations here, even though active requests are still in flight.
332 # The active requests will take precedence, however, because they have
333 # been issued earlier than mirror's background requests.
334 # Once the active requests we have started above are done (i.e. after 5
335 # virtual seconds), we expect those background requests to be worked
336 # on. We only advance 4 seconds here to avoid race conditions.
337 for _ in range(0, 4 * self.iops):
338 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
339 self.vm.qtest(f'clock_step {step}')
341 # Note how much remains to be done until the mirror job is finished
342 job_status = self.vm.qmp('query-jobs')['return'][0]
343 start_remaining = job_status['total-progress'] - \
344 job_status['current-progress']
346 # Create a whole bunch of more active requests
347 for _ in range(0, 10 * self.iops):
348 req = f'write -P 42 {active_request_offset} {active_request_len}'
349 active_request_offset += active_request_len
350 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
351 self.background_processes += [p]
353 # Let the clock advance more. After 1 second, as noted above, we
354 # expect the background requests to be worked on. Give them a couple
355 # of seconds (specifically 4) to see their impact.
356 for _ in range(0, 5 * self.iops):
357 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
358 self.vm.qtest(f'clock_step {step}')
360 # Note how much remains to be done now. We expect this number to be
361 # reduced thanks to those background requests.
362 job_status = self.vm.qmp('query-jobs')['return'][0]
363 end_remaining = job_status['total-progress'] - \
364 job_status['current-progress']
366 # See that indeed progress was being made on the job, even while the
367 # node was saturated with active requests
368 self.assertGreater(start_remaining - end_remaining, 0)
371 class TestHighThrottledWithNbdExport(TestThrottledWithNbdExportBase):
374 def testActiveOnCreation(self):
376 Issue requests on the mirror source node right as the mirror is
377 instated. It's possible that requests occur before the actual job is
378 created, but after the node has been put into the graph. Write
379 requests across the node must in that case be forwarded to the source
380 node without attempting to mirror them (there is no job object yet, so
381 attempting to access it would cause a segfault).
382 We do this with a lightly throttled node (i.e. quite high IOPS limit).
383 Using throttling seems to increase reproductivity, but if the limit is
384 too low, all requests allowed per second will be submitted before
385 mirror_start_job() gets to the problematic point.
388 # Let qemu-img bench create write requests (enough for two seconds on
390 bench_args = ['bench', '-w', '-d', '1024', '-f', 'nbd',
391 '-c', str(self.iops * 2), self.nbd_url]
392 p = iotests.qemu_tool_popen(iotests.qemu_img_args + bench_args)
393 self.background_processes += [p]
395 # Give qemu-img bench time to start up and issue requests
397 # Flush the request queue, so new requests can come in right as we
398 # start blockdev-mirror
399 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
401 self.vm.cmd('blockdev-mirror',
403 device='source-node',
404 target='target-node',
406 copy_mode='write-blocking')
409 if __name__ == '__main__':
410 iotests.main(supported_fmts=['qcow2', 'raw'],
411 supported_protocols=['file'])