Adam Litke has uploaded a new change for review.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Live Merge: fix race between libvirt event and vm stats
It's possible for a block job to stop getting reported by libvirt for a short period before the completion event is actually emitted. In this case, vdsm might clean up a block job too early and then be unable to handle the libvirt completion event. To fix it, handle the following two cases:
1) In merge() between trackBlockJob and libvirt call:
We create a placeholder job in vm.conf['_blockJobs'] before we start the libvirt job. Take a lock to ensure that queryBlockJobs() doesn't ask libvirt about this job and find it missing before it could be started.
2) In queryBlockJobs():
If a job is gone according to libvirt, just mark it 'done' so we will avoid polling on it in the future but let _onBlockJobEvent call untrackBlockJob() only after the cleanup work is completed. Continue reporting the job so engine will wait for us to complete the synchronization later. If we miss an event from libvirt (because vdsm was stopped), the situation will be handled by vm recovery.
Change-Id: Ic0314a39428419bb21bc579f07a92227d5a7acff Signed-off-by: Adam Litke alitke@redhat.com --- M vdsm/virt/vm.py 1 file changed, 46 insertions(+), 45 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/09/29309/1
diff --git a/vdsm/virt/vm.py b/vdsm/virt/vm.py index 84370be..2deed52 100644 --- a/vdsm/virt/vm.py +++ b/vdsm/virt/vm.py @@ -1784,11 +1784,6 @@ (SMARTCARD_DEVICES, SmartCardDevice), (TPM_DEVICES, TpmDevice))
- BlockJobTypeMap = {libvirt.VIR_DOMAIN_BLOCK_JOB_TYPE_UNKNOWN: 'unknown', - libvirt.VIR_DOMAIN_BLOCK_JOB_TYPE_PULL: 'pull', - libvirt.VIR_DOMAIN_BLOCK_JOB_TYPE_COPY: 'copy', - libvirt.VIR_DOMAIN_BLOCK_JOB_TYPE_COMMIT: 'commit'} - def _makeDeviceDict(self): return dict((dev, []) for dev, _ in self.DeviceMapping)
@@ -5432,7 +5427,7 @@ except LookupError: newJob = {'jobID': jobID, 'disk': driveSpec, 'baseVolume': base, 'topVolume': top, - 'strategy': strategy, + 'strategy': strategy, 'blockJobType': 'commit', 'chain': self._driveGetActualVolumeChain(drive)} self.conf['_blockJobs'][jobID] = newJob else: @@ -5455,29 +5450,33 @@
def queryBlockJobs(self): jobs = {} - for jobID, job in self.conf['_blockJobs'].iteritems(): - drive = self._findDriveByUUIDs(job['disk']) - ret = self._dom.blockJobInfo(drive.name, 0) - if not ret: - self.log.debug("Block Job for vm:%s, img:%s has ended", - self.conf['vmId'], job['disk']['imageID']) - jobs[jobID] = None - continue - - jobs[jobID] = {'id': jobID, 'jobType': 'block', - 'blockJobType': Vm.BlockJobTypeMap[ret['type']], - 'bandwidth': ret['bandwidth'], - 'cur': str(ret['cur']), 'end': str(ret['end']), - 'imgUUID': job['disk']['imageID']} - - # This function is meant to be called from multiple threads (ie. - # VMStatsThread and API calls. The _jobsLock ensures that a cohesive - # data set is returned by serializing each call. + # We need to take the jobs lock here to ensure that we don't race with + # another call to merge() where the job has been recorded but not yet + # started. with self._jobsLock: - for jobID in jobs.keys(): - if jobs[jobID] is None: - self.untrackBlockJob(jobID) - del jobs[jobID] + for job in self.conf['_blockJobs'].values()[:]: + jobID = job['jobID'] + drive = self._findDriveByUUIDs(job['disk']) + + ret = None + if 'done' not in job: + ret = self._dom.blockJobInfo(drive.name, 0) + if not ret: + self.log.debug("Block Job for vm:%s, img:%s has ended", + self.conf['vmId'], + job['disk']['imageID']) + job['done'] = True + + entry = {'id': jobID, 'jobType': 'block', + 'blockJobType': job['blockJobType'], + 'bandwidth': 0, 'cur': '0', 'end': '0', + 'imgUUID': job['disk']['imageID']} + + if ret: + entry['bandwidth'] = ret['bandwidth'] + entry['cur'] = str(ret['cur']) + entry['end'] = str(ret['end']) + jobs[jobID] = entry return jobs
def merge(self, driveSpec, baseVolUUID, topVolUUID, bandwidth, jobUUID): @@ -5543,24 +5542,26 @@ "missing VIR_DOMAIN_BLOCK_COMMIT_ACTIVE.") return errCode['mergeErr']
- try: - self.trackBlockJob(jobUUID, drive, baseVolUUID, topVolUUID, - 'commit') - except BlockJobExistsError: - self.log.error("Another block job is already active on this disk") - return errCode['mergeErr'] - self.log.debug("Starting merge with jobUUID='%s'", jobUUID) + # Take the jobs lock here to protect the new job we are tracking from + # being cleaned up by queryBlockJobs() since it won't exist right away + with self._jobsLock: + try: + self.trackBlockJob(jobUUID, drive, baseVolUUID, topVolUUID, + 'commit') + except BlockJobExistsError: + self.log.error("A block job is already active on this disk") + return errCode['mergeErr'] + self.log.debug("Starting merge with jobUUID='%s'", jobUUID)
- try: - ret = self._dom.blockCommit(drive.path, base, top, bandwidth, - flags) - if ret != 0: - raise RuntimeError("blockCommit operation failed rc:%i", ret) - except (RuntimeError, libvirt.libvirtError): - self.log.error("Live merge failed for '%s'", drive.path, - exc_info=True) - self.untrackBlockJob(jobUUID) - return errCode['mergeErr'] + try: + ret = self._dom.blockCommit(drive.path, base, top, bandwidth, + flags) + if ret != 0: + raise RuntimeError("blockCommit failed rc:%i", ret) + except (RuntimeError, libvirt.libvirtError): + self.log.error("Live merge failed for '%s'", drive.path) + self.untrackBlockJob(jobUUID) + return errCode['mergeErr']
# blockCommit will cause data to be written into the base volume. # Perform an initial extension to ensure there is enough space to
oVirt Jenkins CI Server has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 1:
Build Failed
http://jenkins.ovirt.org/job/vdsm_master_unit_tests_gerrit_el/9704/ : SUCCESS
http://jenkins.ovirt.org/job/vdsm_master_pep8_gerrit/10489/ : SUCCESS
http://jenkins.ovirt.org/job/vdsm_master_virt_functional_tests_gerrit/1037/ : There was an infra issue, please contact infra@ovirt.org
http://jenkins.ovirt.org/job/vdsm_master_unit-tests_created/10645/ : SUCCESS
Adam Litke has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 1: Verified+1
Greg Padgett has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 1: Verified+1 Code-Review+1
oVirt Jenkins CI Server has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
Build Failed
http://jenkins.ovirt.org/job/vdsm_master_unit_tests_gerrit_el/9820/ : SUCCESS
http://jenkins.ovirt.org/job/vdsm_master_pep8_gerrit/10605/ : SUCCESS
http://jenkins.ovirt.org/job/vdsm_master_virt_functional_tests_gerrit/1089/ : There was an infra issue, please contact infra@ovirt.org
http://jenkins.ovirt.org/job/vdsm_master_unit-tests_created/10762/ : SUCCESS
Federico Simoncelli has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2: Code-Review-1
(7 comments)
Minor comments, mostly just questions. Marking -1 for visibility.
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5430: job = self.getBlockJob(drive) Line 5431: except LookupError: Line 5432: newJob = {'jobID': jobID, 'disk': driveSpec, Line 5433: 'baseVolume': base, 'topVolume': top, Line 5434: 'strategy': strategy, 'blockJobType': 'commit', Is this change (and the BlockJobTypeMap one) unrelated? Line 5435: 'chain': self._driveGetActualVolumeChain(drive)} Line 5436: self.conf['_blockJobs'][jobID] = newJob Line 5437: else: Line 5438: self.log.error("A block job with id %s already exists for vm "
Line 5452: self.saveState() Line 5453: return True Line 5454: Line 5455: def queryBlockJobs(self): Line 5456: jobs = {} "jobs" and "job" gets me confused, we could try to find a longer, more descriptive name for this dictionary. Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock: Line 5461: for job in self.conf['_blockJobs'].values()[:]: I think "[:]" is redundant, values() should already create a new list. Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464: Line 5465: ret = None
Line 5460: with self._jobsLock: Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464: Could it make sense to prepare the entry here?
entry = {'id': jobID, 'jobType': 'block', ... Line 5465: ret = None Line 5466: if 'done' not in job: Line 5467: ret = self._dom.blockJobInfo(drive.name, 0) Line 5468: if not ret:
Line 5468: if not ret: Line 5469: self.log.debug("Block Job for vm:%s, img:%s has ended", Line 5470: self.conf['vmId'], Line 5471: job['disk']['imageID']) Line 5472: job['done'] = True If you prepared the entry at 5464, here you can set the bandwidth and all the rest, e.g.:
if ret: entry['bandwidth'] = ret['bandwidth'] entry['cur'] = str(ret['cur']) entry['end'] = str(ret['end']) else: self.log.debug(...) job['done'] = True Line 5473: Line 5474: entry = {'id': jobID, 'jobType': 'block', Line 5475: 'blockJobType': job['blockJobType'], Line 5476: 'bandwidth': 0, 'cur': '0', 'end': '0',
Line 5470: self.conf['vmId'], Line 5471: job['disk']['imageID']) Line 5472: job['done'] = True Line 5473: Line 5474: entry = {'id': jobID, 'jobType': 'block', Now you can remove all these, from line 5474 to 5483. Line 5475: 'blockJobType': job['blockJobType'], Line 5476: 'bandwidth': 0, 'cur': '0', 'end': '0', Line 5477: 'imgUUID': job['disk']['imageID']} Line 5478:
Line 5557: flags) Line 5558: if ret != 0: Line 5559: raise RuntimeError("blockCommit failed rc:%i", ret) Line 5560: except (RuntimeError, libvirt.libvirtError): Line 5561: self.log.error("Live merge failed for '%s'", drive.path) self.log.exception? Line 5562: self.untrackBlockJob(jobUUID) Line 5563: return errCode['mergeErr'] Line 5564: Line 5565: # blockCommit will cause data to be written into the base volume.
Adam Litke has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2: -Verified
(6 comments)
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5430: job = self.getBlockJob(drive) Line 5431: except LookupError: Line 5432: newJob = {'jobID': jobID, 'disk': driveSpec, Line 5433: 'baseVolume': base, 'topVolume': top, Line 5434: 'strategy': strategy, 'blockJobType': 'commit',
Is this change (and the BlockJobTypeMap one) unrelated?
No, this is part of the patch because we need to create a placeholder job when libvirt isn't reporting the information but vdsm still knows it is either starting or being cleaned up. In that case, we won't have libvirt to tell us what kind of job it is so we need to start tracking it ourselves. Since we are tracking it ourselves, we will just always use our info and no longer need to pay attention to the libvirt job type when sampling. Line 5435: 'chain': self._driveGetActualVolumeChain(drive)} Line 5436: self.conf['_blockJobs'][jobID] = newJob Line 5437: else: Line 5438: self.log.error("A block job with id %s already exists for vm "
Line 5452: self.saveState() Line 5453: return True Line 5454: Line 5455: def queryBlockJobs(self): Line 5456: jobs = {}
"jobs" and "job" gets me confused, we could try to find a longer, more desc
ok, will change to jobsRet and jobInfo Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock: Line 5461: for job in self.conf['_blockJobs'].values()[:]:
I think "[:]" is redundant, values() should already create a new list.
ok. Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464: Line 5465: ret = None
Line 5460: with self._jobsLock: Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Could it make sense to prepare the entry here?
sure. will rework it. Line 5465: ret = None Line 5466: if 'done' not in job: Line 5467: ret = self._dom.blockJobInfo(drive.name, 0) Line 5468: if not ret:
Line 5470: self.conf['vmId'], Line 5471: job['disk']['imageID']) Line 5472: job['done'] = True Line 5473: Line 5474: entry = {'id': jobID, 'jobType': 'block',
Now you can remove all these, from line 5474 to 5483.
ok, sure. Line 5475: 'blockJobType': job['blockJobType'], Line 5476: 'bandwidth': 0, 'cur': '0', 'end': '0', Line 5477: 'imgUUID': job['disk']['imageID']} Line 5478:
Line 5557: flags) Line 5558: if ret != 0: Line 5559: raise RuntimeError("blockCommit failed rc:%i", ret) Line 5560: except (RuntimeError, libvirt.libvirtError): Line 5561: self.log.error("Live merge failed for '%s'", drive.path)
self.log.exception?
ok Line 5562: self.untrackBlockJob(jobUUID) Line 5563: return errCode['mergeErr'] Line 5564: Line 5565: # blockCommit will cause data to be written into the base volume.
Michal Skrivanek has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
(1 comment)
keeping "0" as it already is an improvement anyway. ButI'd love to understand why we need to "trigger" stats from outside(API call) - hence why do we need this whole locking thing.
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5456: jobs = {} Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock: so there are only 2 places where the queryBlockJobs is called - stat thread and the end of merge(). Why do we have that "trigger" in merge()? Can't we live without it? If yes then we don't need any locking here
Is the concern a possibly incorrect "view" of engine the first time stats return? Couldn't this be solved on the engine side? Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Adam Litke has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
(1 comment)
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5456: jobs = {} Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
so there are only 2 places where the queryBlockJobs is called - stat thread
You're right, the main issue is engine polling on the stats and seeing no vmJob in the list. This could mean several things: the stats are outdated, the job hasn't started yet, or the job has already completed. It's important for us to guarantee to engine that once the merge verb returns success, the job will appear in the stats until it has finished. Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Michal Skrivanek has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
(1 comment)
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5456: jobs = {} Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
You're right, the main issue is engine polling on the stats and seeing no v
Well, it is a different approach from other commands. Even with your current design, how do you ensure that you report anything if the job finishes before you even get to the end of merge() and call the first stats cycle? Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Adam Litke has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
(1 comment)
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5456: jobs = {} Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
Well, it is a different approach from other commands.
In that case it's no problem (but please see my updated livemerge topic branch for the most recent code wrt this). That first call of the stats cycle would find the placeholder job and query libvirt. Since the job is done libvirt will return nothing back and vdsm will trigger the recovery logic. Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Michal Skrivanek has posted comments on this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Patch Set 2:
(1 comment)
http://gerrit.ovirt.org/#/c/29309/2/vdsm/virt/vm.py File vdsm/virt/vm.py:
Line 5456: jobs = {} Line 5457: # We need to take the jobs lock here to ensure that we don't race with Line 5458: # another call to merge() where the job has been recorded but not yet Line 5459: # started. Line 5460: with self._jobsLock:
In that case it's no problem (but please see my updated livemerge topic bra
so cannot this be used as a general solution to timing? - so you don't have to do this first stat cycle here - I think the elimination of the lock is worth it Line 5461: for job in self.conf['_blockJobs'].values()[:]: Line 5462: jobID = job['jobID'] Line 5463: drive = self._findDriveByUUIDs(job['disk']) Line 5464:
Adam Litke has abandoned this change.
Change subject: Live Merge: fix race between libvirt event and vm stats ......................................................................
Abandoned
vdsm-patches@lists.fedorahosted.org