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 result = self.vm.qmp('blockdev-mirror',
84 filter_node_name='mirror-node',
88 copy_mode='write-blocking')
89 self.assert_qmp(result, 'return', {})
91 # Start some more requests
92 for offset in range(3 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
93 self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
94 for offset in range(4 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
95 self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
97 # Wait for the READY event
98 self.wait_ready(drive='mirror')
100 # Now start some final requests; all of these (which land on
101 # the source) should be settled using the active mechanism.
102 # The mirror code itself asserts that the source BDS's dirty
103 # bitmap will stay clean between READY and COMPLETED.
104 for offset in range(5 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
105 self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
106 for offset in range(6 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
107 self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
109 if sync_source_and_target:
110 # If source and target should be in sync after the mirror,
111 # we have to flush before completion
112 self.vm.hmp_qemu_io('source', 'aio_flush')
113 self.potential_writes_in_flight = False
115 self.complete_and_wait(drive='mirror', wait_ready=False)
117 def testActiveIO(self):
118 self.doActiveIO(False)
120 def testActiveIOFlushed(self):
121 self.doActiveIO(True)
123 def testUnalignedActiveIO(self):
124 # Fill the source image
125 result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
127 # Start the block job (very slowly)
128 result = self.vm.qmp('blockdev-mirror',
130 filter_node_name='mirror-node',
131 device='source-node',
132 target='target-node',
134 copy_mode='write-blocking',
135 buf_size=(1048576 // 4),
137 self.assert_qmp(result, 'return', {})
139 # Start an unaligned request to a dirty area
140 result = self.vm.hmp_qemu_io('source', 'write -P 2 %i 1' % (1048576 + 42))
143 result = self.vm.qmp('block-job-set-speed', device='mirror', speed=0)
144 self.assert_qmp(result, 'return', {})
145 self.complete_and_wait(drive='mirror')
147 self.potential_writes_in_flight = False
149 def testIntersectingActiveIO(self):
150 # Fill the source image
151 result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
153 # Start the block job (very slowly)
154 result = self.vm.qmp('blockdev-mirror',
156 filter_node_name='mirror-node',
157 device='source-node',
158 target='target-node',
160 copy_mode='write-blocking',
162 self.assert_qmp(result, 'return', {})
164 self.vm.hmp_qemu_io('source', 'break write_aio A')
165 self.vm.hmp_qemu_io('source', 'aio_write 0 1M') # 1
166 self.vm.hmp_qemu_io('source', 'wait_break A')
167 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 2
168 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 3
170 # Now 2 and 3 are in mirror_wait_on_conflicts, waiting for 1
172 self.vm.hmp_qemu_io('source', 'break write_aio B')
173 self.vm.hmp_qemu_io('source', 'aio_write 1M 2M') # 4
174 self.vm.hmp_qemu_io('source', 'wait_break B')
176 # 4 doesn't wait for 2 and 3, because they didn't yet set
177 # in_flight_bitmap. So, nothing prevents 4 to go except for our
180 self.vm.hmp_qemu_io('source', 'resume A')
182 # Now we resumed 1, so 2 and 3 goes to the next iteration of while loop
183 # in mirror_wait_on_conflicts(). They don't exit, as bitmap is dirty
185 # In the past at that point 2 and 3 would wait for each other producing
186 # a dead-lock. Now this is fixed and they will wait for request 4.
188 self.vm.hmp_qemu_io('source', 'resume B')
190 # After resuming 4, one of 2 and 3 goes first and set in_flight_bitmap,
191 # so the other will wait for it.
193 result = self.vm.qmp('block-job-set-speed', device='mirror', speed=0)
194 self.assert_qmp(result, 'return', {})
195 self.complete_and_wait(drive='mirror')
197 self.potential_writes_in_flight = False
200 class TestThrottledWithNbdExportBase(iotests.QMPTestCase):
201 image_len = 128 * 1024 * 1024 # MB
202 iops: Optional[int] = None
203 background_processes: List['subprocess.Popen[str]'] = []
206 # Must be set by subclasses
207 self.assertIsNotNone(self.iops)
209 qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
210 qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
212 self.vm = iotests.VM()
215 result = self.vm.qmp('object-add', **{
216 'qom-type': 'throttle-group',
219 'iops-total': self.iops,
220 'iops-total-max': self.iops
223 self.assert_qmp(result, 'return', {})
225 result = self.vm.qmp('blockdev-add', **{
226 'node-name': 'source-node',
227 'driver': 'throttle',
228 'throttle-group': 'thrgr',
230 'driver': iotests.imgfmt,
233 'filename': source_img
237 self.assert_qmp(result, 'return', {})
239 result = self.vm.qmp('blockdev-add', **{
240 'node-name': 'target-node',
241 'driver': iotests.imgfmt,
244 'filename': target_img
247 self.assert_qmp(result, 'return', {})
249 self.nbd_sock = iotests.file_path('nbd.sock',
250 base_dir=iotests.sock_dir)
251 self.nbd_url = f'nbd+unix:///source-node?socket={self.nbd_sock}'
253 result = self.vm.qmp('nbd-server-start', addr={
256 'path': self.nbd_sock
259 self.assert_qmp(result, 'return', {})
261 result = self.vm.qmp('block-export-add', id='exp0', type='nbd',
262 node_name='source-node', writable=True)
263 self.assert_qmp(result, 'return', {})
266 # Wait for background requests to settle
269 p = self.background_processes.pop()
274 except subprocess.TimeoutExpired:
275 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
279 # Cancel ongoing block jobs
280 for job in self.vm.qmp('query-jobs')['return']:
281 self.vm.qmp('block-job-cancel', device=job['id'], force=True)
284 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
285 if len(self.vm.qmp('query-jobs')['return']) == 0:
289 os.remove(source_img)
290 os.remove(target_img)
293 class TestLowThrottledWithNbdExport(TestThrottledWithNbdExportBase):
296 def testUnderLoad(self):
298 Throttle the source node, then issue a whole bunch of external requests
299 while the mirror job (in write-blocking mode) is running. We want to
300 see background requests being issued even while the source is under
301 full load by active writes, so that progress can be made towards READY.
304 # Fill the first half of the source image; do not fill the second half,
305 # that is where we will have active requests occur. This ensures that
306 # active mirroring itself will not directly contribute to the job's
307 # progress (because when the job was started, those areas were not
308 # intended to be copied, so active mirroring will only lead to not
309 # losing progress, but also not making any).
310 self.vm.hmp_qemu_io('source-node',
311 f'aio_write -P 1 0 {self.image_len // 2}')
312 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
314 # Launch the mirror job
315 mirror_buf_size = 65536
316 result = self.vm.qmp('blockdev-mirror',
318 filter_node_name='mirror-node',
319 device='source-node',
320 target='target-node',
322 copy_mode='write-blocking',
323 buf_size=mirror_buf_size)
324 self.assert_qmp(result, 'return', {})
326 # We create the external requests via qemu-io processes on the NBD
327 # server. Have their offset start in the middle of the image so they
328 # do not overlap with the background requests (which start from the
330 active_request_offset = self.image_len // 2
331 active_request_len = 4096
333 # Create enough requests to saturate the node for 5 seconds
334 for _ in range(0, 5 * self.iops):
335 req = f'write -P 42 {active_request_offset} {active_request_len}'
336 active_request_offset += active_request_len
337 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
338 self.background_processes += [p]
340 # Now advance the clock one I/O operation at a time by the 4 seconds
341 # (i.e. one less than 5). We expect the mirror job to issue background
342 # operations here, even though active requests are still in flight.
343 # The active requests will take precedence, however, because they have
344 # been issued earlier than mirror's background requests.
345 # Once the active requests we have started above are done (i.e. after 5
346 # virtual seconds), we expect those background requests to be worked
347 # on. We only advance 4 seconds here to avoid race conditions.
348 for _ in range(0, 4 * self.iops):
349 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
350 self.vm.qtest(f'clock_step {step}')
352 # Note how much remains to be done until the mirror job is finished
353 job_status = self.vm.qmp('query-jobs')['return'][0]
354 start_remaining = job_status['total-progress'] - \
355 job_status['current-progress']
357 # Create a whole bunch of more active requests
358 for _ in range(0, 10 * self.iops):
359 req = f'write -P 42 {active_request_offset} {active_request_len}'
360 active_request_offset += active_request_len
361 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
362 self.background_processes += [p]
364 # Let the clock advance more. After 1 second, as noted above, we
365 # expect the background requests to be worked on. Give them a couple
366 # of seconds (specifically 4) to see their impact.
367 for _ in range(0, 5 * self.iops):
368 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
369 self.vm.qtest(f'clock_step {step}')
371 # Note how much remains to be done now. We expect this number to be
372 # reduced thanks to those background requests.
373 job_status = self.vm.qmp('query-jobs')['return'][0]
374 end_remaining = job_status['total-progress'] - \
375 job_status['current-progress']
377 # See that indeed progress was being made on the job, even while the
378 # node was saturated with active requests
379 self.assertGreater(start_remaining - end_remaining, 0)
382 class TestHighThrottledWithNbdExport(TestThrottledWithNbdExportBase):
385 def testActiveOnCreation(self):
387 Issue requests on the mirror source node right as the mirror is
388 instated. It's possible that requests occur before the actual job is
389 created, but after the node has been put into the graph. Write
390 requests across the node must in that case be forwarded to the source
391 node without attempting to mirror them (there is no job object yet, so
392 attempting to access it would cause a segfault).
393 We do this with a lightly throttled node (i.e. quite high IOPS limit).
394 Using throttling seems to increase reproductivity, but if the limit is
395 too low, all requests allowed per second will be submitted before
396 mirror_start_job() gets to the problematic point.
399 # Let qemu-img bench create write requests (enough for two seconds on
401 bench_args = ['bench', '-w', '-d', '1024', '-f', 'nbd',
402 '-c', str(self.iops * 2), self.nbd_url]
403 p = iotests.qemu_tool_popen(iotests.qemu_img_args + bench_args)
404 self.background_processes += [p]
406 # Give qemu-img bench time to start up and issue requests
408 # Flush the request queue, so new requests can come in right as we
409 # start blockdev-mirror
410 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
412 result = self.vm.qmp('blockdev-mirror',
414 device='source-node',
415 target='target-node',
417 copy_mode='write-blocking')
418 self.assert_qmp(result, 'return', {})
421 if __name__ == '__main__':
422 iotests.main(supported_fmts=['qcow2', 'raw'],
423 supported_protocols=['file'])