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',
163 self.vm.hmp_qemu_io('source', 'break write_aio A')
164 self.vm.hmp_qemu_io('source', 'aio_write 0 1M') # 1
165 self.vm.hmp_qemu_io('source', 'wait_break A')
166 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 2
167 self.vm.hmp_qemu_io('source', 'aio_write 0 2M') # 3
169 # Now 2 and 3 are in mirror_wait_on_conflicts, waiting for 1
171 self.vm.hmp_qemu_io('source', 'break write_aio B')
172 self.vm.hmp_qemu_io('source', 'aio_write 1M 2M') # 4
173 self.vm.hmp_qemu_io('source', 'wait_break B')
175 # 4 doesn't wait for 2 and 3, because they didn't yet set
176 # in_flight_bitmap. So, nothing prevents 4 to go except for our
179 self.vm.hmp_qemu_io('source', 'resume A')
181 # Now we resumed 1, so 2 and 3 goes to the next iteration of while loop
182 # in mirror_wait_on_conflicts(). They don't exit, as bitmap is dirty
184 # In the past at that point 2 and 3 would wait for each other producing
185 # a dead-lock. Now this is fixed and they will wait for request 4.
187 self.vm.hmp_qemu_io('source', 'resume B')
189 # After resuming 4, one of 2 and 3 goes first and set in_flight_bitmap,
190 # so the other will wait for it.
192 result = self.vm.qmp('block-job-set-speed', device='mirror', speed=0)
193 self.assert_qmp(result, 'return', {})
194 self.complete_and_wait(drive='mirror')
196 self.potential_writes_in_flight = False
199 class TestThrottledWithNbdExportBase(iotests.QMPTestCase):
200 image_len = 128 * 1024 * 1024 # MB
201 iops: Optional[int] = None
202 background_processes: List['subprocess.Popen[str]'] = []
205 # Must be set by subclasses
206 self.assertIsNotNone(self.iops)
208 qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
209 qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
211 self.vm = iotests.VM()
214 result = self.vm.qmp('object-add', **{
215 'qom-type': 'throttle-group',
218 'iops-total': self.iops,
219 'iops-total-max': self.iops
222 self.assert_qmp(result, 'return', {})
224 result = self.vm.qmp('blockdev-add', **{
225 'node-name': 'source-node',
226 'driver': 'throttle',
227 'throttle-group': 'thrgr',
229 'driver': iotests.imgfmt,
232 'filename': source_img
236 self.assert_qmp(result, 'return', {})
238 result = self.vm.qmp('blockdev-add', **{
239 'node-name': 'target-node',
240 'driver': iotests.imgfmt,
243 'filename': target_img
246 self.assert_qmp(result, 'return', {})
248 self.nbd_sock = iotests.file_path('nbd.sock',
249 base_dir=iotests.sock_dir)
250 self.nbd_url = f'nbd+unix:///source-node?socket={self.nbd_sock}'
252 result = self.vm.qmp('nbd-server-start', addr={
255 'path': self.nbd_sock
258 self.assert_qmp(result, 'return', {})
260 result = self.vm.qmp('block-export-add', id='exp0', type='nbd',
261 node_name='source-node', writable=True)
262 self.assert_qmp(result, 'return', {})
265 # Wait for background requests to settle
268 p = self.background_processes.pop()
273 except subprocess.TimeoutExpired:
274 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
278 # Cancel ongoing block jobs
279 for job in self.vm.qmp('query-jobs')['return']:
280 self.vm.qmp('block-job-cancel', device=job['id'], force=True)
283 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
284 if len(self.vm.qmp('query-jobs')['return']) == 0:
288 os.remove(source_img)
289 os.remove(target_img)
292 class TestLowThrottledWithNbdExport(TestThrottledWithNbdExportBase):
295 def testUnderLoad(self):
297 Throttle the source node, then issue a whole bunch of external requests
298 while the mirror job (in write-blocking mode) is running. We want to
299 see background requests being issued even while the source is under
300 full load by active writes, so that progress can be made towards READY.
303 # Fill the first half of the source image; do not fill the second half,
304 # that is where we will have active requests occur. This ensures that
305 # active mirroring itself will not directly contribute to the job's
306 # progress (because when the job was started, those areas were not
307 # intended to be copied, so active mirroring will only lead to not
308 # losing progress, but also not making any).
309 self.vm.hmp_qemu_io('source-node',
310 f'aio_write -P 1 0 {self.image_len // 2}')
311 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
313 # Launch the mirror job
314 mirror_buf_size = 65536
315 result = self.vm.qmp('blockdev-mirror',
317 filter_node_name='mirror-node',
318 device='source-node',
319 target='target-node',
321 copy_mode='write-blocking',
322 buf_size=mirror_buf_size)
323 self.assert_qmp(result, 'return', {})
325 # We create the external requests via qemu-io processes on the NBD
326 # server. Have their offset start in the middle of the image so they
327 # do not overlap with the background requests (which start from the
329 active_request_offset = self.image_len // 2
330 active_request_len = 4096
332 # Create enough requests to saturate the node for 5 seconds
333 for _ in range(0, 5 * self.iops):
334 req = f'write -P 42 {active_request_offset} {active_request_len}'
335 active_request_offset += active_request_len
336 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
337 self.background_processes += [p]
339 # Now advance the clock one I/O operation at a time by the 4 seconds
340 # (i.e. one less than 5). We expect the mirror job to issue background
341 # operations here, even though active requests are still in flight.
342 # The active requests will take precedence, however, because they have
343 # been issued earlier than mirror's background requests.
344 # Once the active requests we have started above are done (i.e. after 5
345 # virtual seconds), we expect those background requests to be worked
346 # on. We only advance 4 seconds here to avoid race conditions.
347 for _ in range(0, 4 * self.iops):
348 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
349 self.vm.qtest(f'clock_step {step}')
351 # Note how much remains to be done until the mirror job is finished
352 job_status = self.vm.qmp('query-jobs')['return'][0]
353 start_remaining = job_status['total-progress'] - \
354 job_status['current-progress']
356 # Create a whole bunch of more active requests
357 for _ in range(0, 10 * self.iops):
358 req = f'write -P 42 {active_request_offset} {active_request_len}'
359 active_request_offset += active_request_len
360 p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
361 self.background_processes += [p]
363 # Let the clock advance more. After 1 second, as noted above, we
364 # expect the background requests to be worked on. Give them a couple
365 # of seconds (specifically 4) to see their impact.
366 for _ in range(0, 5 * self.iops):
367 step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
368 self.vm.qtest(f'clock_step {step}')
370 # Note how much remains to be done now. We expect this number to be
371 # reduced thanks to those background requests.
372 job_status = self.vm.qmp('query-jobs')['return'][0]
373 end_remaining = job_status['total-progress'] - \
374 job_status['current-progress']
376 # See that indeed progress was being made on the job, even while the
377 # node was saturated with active requests
378 self.assertGreater(start_remaining - end_remaining, 0)
381 class TestHighThrottledWithNbdExport(TestThrottledWithNbdExportBase):
384 def testActiveOnCreation(self):
386 Issue requests on the mirror source node right as the mirror is
387 instated. It's possible that requests occur before the actual job is
388 created, but after the node has been put into the graph. Write
389 requests across the node must in that case be forwarded to the source
390 node without attempting to mirror them (there is no job object yet, so
391 attempting to access it would cause a segfault).
392 We do this with a lightly throttled node (i.e. quite high IOPS limit).
393 Using throttling seems to increase reproductivity, but if the limit is
394 too low, all requests allowed per second will be submitted before
395 mirror_start_job() gets to the problematic point.
398 # Let qemu-img bench create write requests (enough for two seconds on
400 bench_args = ['bench', '-w', '-d', '1024', '-f', 'nbd',
401 '-c', str(self.iops * 2), self.nbd_url]
402 p = iotests.qemu_tool_popen(iotests.qemu_img_args + bench_args)
403 self.background_processes += [p]
405 # Give qemu-img bench time to start up and issue requests
407 # Flush the request queue, so new requests can come in right as we
408 # start blockdev-mirror
409 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
411 result = self.vm.qmp('blockdev-mirror',
413 device='source-node',
414 target='target-node',
416 copy_mode='write-blocking')
417 self.assert_qmp(result, 'return', {})
420 if __name__ == '__main__':
421 iotests.main(supported_fmts=['qcow2', 'raw'],
422 supported_protocols=['file'])