Merge lp:~zyga/checkbox/fix-session-init into lp:checkbox

Proposed by Zygmunt Krynicki
Status: Merged
Approved by: Daniel Manrique
Approved revision: 2002
Merged at revision: 1999
Proposed branch: lp:~zyga/checkbox/fix-session-init
Merge into: lp:checkbox
Diff against target: 201 lines (+96/-6)
5 files modified
plainbox/plainbox/impl/commands/run.py (+13/-1)
plainbox/plainbox/impl/depmgr.py (+9/-2)
plainbox/plainbox/impl/session.py (+40/-2)
plainbox/plainbox/impl/test_depmgr.py (+10/-0)
plainbox/plainbox/impl/test_session.py (+24/-1)
To merge this branch: bzr merge lp:~zyga/checkbox/fix-session-init
Reviewer Review Type Date Requested Status
Daniel Manrique (community) Approve
Review via email: mp+154140@code.launchpad.net

Description of the change

Fix for bug lp: #1157264 affecting SessionState.__init__()

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

This is how it looks like now:

vagrant@vagrant-ubuntu-raring-32:/usr/share/checkbox/jobs$ plainbox run -i cpu/scaling_test
===============================[ Analyzing Jobs ]===============================
The job database you are currently using is broken
At least two jobs contend for the name keys/volume
First job defined in: /usr/share/checkbox/jobs/test.txt:1-9
Second job defined in: /usr/share/checkbox/jobs/keys.txt:12-26
duplicate job name: 'keys/volume'

lp:~zyga/checkbox/fix-session-init updated
2001. By Zygmunt Krynicki

session: properly handle SessionState() called with duplicates

This is a part of fix to lp: #1157264

When SessionState() is initialized with a list containing duplicates
bad things happen. The error recovery code in update_desired_jobs()
is not prepared for any duplicates and things break in odd places.

To fix that, have SessionState reject duplicate jobs in the same way
that DependencySolver does. As a small exception to this rule, jobs
that are prefect clones are silently coalesced.

This makes the constructor of SessionState throw DependencyDuplicateError
exception when real problems have occurred and prevent any testing from
being started. Unlike local jobs that can also silently produce duplicate
jobs, having initial name clashes cannot be solved at this layer

Signed-off-by: Zygmunt Krynicki <email address hidden>
Reported-by: Sean Feole <email address hidden>

2002. By Zygmunt Krynicki

run: handle duplicate jobs when constructing SessionState

This fixes lp: #1157264

This builds on earlier patches, when the job database is wrong and
contains duplicate jobs that are not identical then tell the operator
about where each job came from.

Signed-off-by: Zygmunt Krynicki <email address hidden>
Reported-by: Sean Feole <email address hidden>
Tested-by: Zygmunt Krynicki <email address hidden>

Revision history for this message
Daniel Manrique (roadmr) wrote :

Looks good, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'plainbox/plainbox/impl/commands/run.py'
--- plainbox/plainbox/impl/commands/run.py 2013-03-14 17:06:41 +0000
+++ plainbox/plainbox/impl/commands/run.py 2013-03-19 19:07:23 +0000
@@ -33,6 +33,7 @@
3333
34from plainbox.impl.commands import PlainBoxCommand34from plainbox.impl.commands import PlainBoxCommand
35from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn35from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
36from plainbox.impl.depmgr import DependencyDuplicateError
36from plainbox.impl.exporter import get_all_exporters37from plainbox.impl.exporter import get_all_exporters
37from plainbox.impl.result import JobResult38from plainbox.impl.result import JobResult
38from plainbox.impl.runner import JobRunner39from plainbox.impl.runner import JobRunner
@@ -127,7 +128,18 @@
127 matching_job_list = self._get_matching_job_list(ns, job_list)128 matching_job_list = self._get_matching_job_list(ns, job_list)
128 print("[ Analyzing Jobs ]".center(80, '='))129 print("[ Analyzing Jobs ]".center(80, '='))
129 # Create a session that handles most of the stuff needed to run jobs130 # Create a session that handles most of the stuff needed to run jobs
130 session = SessionState(job_list)131 try:
132 session = SessionState(job_list)
133 except DependencyDuplicateError as exc:
134 # Handle possible DependencyDuplicateError that can happen if
135 # someone is using plainbox for job development.
136 print("The job database you are currently using is broken")
137 print("At least two jobs contend for the name {0}".format(
138 exc.job.name))
139 print("First job defined in: {0}".format(exc.job.origin))
140 print("Second job defined in: {0}".format(
141 exc.duplicate_job.origin))
142 raise SystemExit(exc)
131 with session.open():143 with session.open():
132 if session.previous_session_file():144 if session.previous_session_file():
133 if self.ask_for_resume():145 if self.ask_for_resume():
134146
=== modified file 'plainbox/plainbox/impl/depmgr.py'
--- plainbox/plainbox/impl/depmgr.py 2013-02-25 11:03:02 +0000
+++ plainbox/plainbox/impl/depmgr.py 2013-03-19 19:07:23 +0000
@@ -157,14 +157,21 @@
157 graph while still having access and knowledge of all jobs.157 graph while still having access and knowledge of all jobs.
158158
159 :returns list: the solution (a list of jobs to execute in order)159 :returns list: the solution (a list of jobs to execute in order)
160 :raises DependencyCycleError: if a cyclic dependency is present.160 :raises DependencyDuplicateError:
161 :raises DependencyMissingErorr: if a required job does not exist.161 if a duplicate job definition is present
162 :raises DependencyCycleError:
163 if a cyclic dependency is present.
164 :raises DependencyMissingErorr:
165 if a required job does not exist.
162 """166 """
163 return cls(job_list)._solve(visit_list)167 return cls(job_list)._solve(visit_list)
164168
165 def __init__(self, job_list):169 def __init__(self, job_list):
166 """170 """
167 Instantiate a new dependency solver with the specified list of jobs171 Instantiate a new dependency solver with the specified list of jobs
172
173 :raises DependencyDuplicateError:
174 if the initial job_list has any duplicate jobs
168 """175 """
169 # Remember the jobs that were passed176 # Remember the jobs that were passed
170 self._job_list = job_list177 self._job_list = job_list
171178
=== modified file 'plainbox/plainbox/impl/session.py'
--- plainbox/plainbox/impl/session.py 2013-02-28 11:06:34 +0000
+++ plainbox/plainbox/impl/session.py 2013-03-19 19:07:23 +0000
@@ -1,6 +1,6 @@
1# This file is part of Checkbox.1# This file is part of Checkbox.
2#2#
3# Copyright 2012 Canonical Ltd.3# Copyright 2012, 2013 Canonical Ltd.
4# Written by:4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#6#
@@ -31,7 +31,7 @@
31import shutil31import shutil
32import tempfile32import tempfile
3333
34from plainbox.impl.depmgr import DependencyError34from plainbox.impl.depmgr import DependencyError, DependencyDuplicateError
35from plainbox.impl.depmgr import DependencySolver35from plainbox.impl.depmgr import DependencySolver
36from plainbox.impl.job import JobDefinition36from plainbox.impl.job import JobDefinition
37from plainbox.impl.resource import ExpressionCannotEvaluateError37from plainbox.impl.resource import ExpressionCannotEvaluateError
@@ -297,6 +297,12 @@
297 changes this will start out empty and will be changeable dynamically.297 changes this will start out empty and will be changeable dynamically.
298 It can still change due to local jobs but there is no API yes.298 It can still change due to local jobs but there is no API yes.
299299
300 This list cannot have any duplicates, if that is the case a
301 DependencyDuplicateError is raised. This has to be handled externally
302 and is a sign that the job database is corrupted or has wrong data. As
303 an exception if duplicates are perfectly identical this error is
304 silently corrected.
305
300 :ivar dict job_state_map: mapping that tracks the state of each job306 :ivar dict job_state_map: mapping that tracks the state of each job
301307
302 Mapping from job name to :class:`JobState`. This basically has the test308 Mapping from job name to :class:`JobState`. This basically has the test
@@ -340,6 +346,38 @@
340 session_data_filename = 'session.json'346 session_data_filename = 'session.json'
341347
342 def __init__(self, job_list):348 def __init__(self, job_list):
349 # Start by making a copy of job_list as we may modify it below
350 job_list = job_list[:]
351 while True:
352 try:
353 # Construct a solver with the job list as passed by the caller.
354 # This will do a little bit of validation and might raise
355 # DepdendencyDuplicateError if there are any duplicates at this
356 # stage.
357 #
358 # There's a single case that is handled here though, if both
359 # jobs are identical this problem is silently fixed. This
360 # should not happen in normal circumstances but is non the less
361 # harmless (as long as both jobs are perfectly identical)
362 #
363 # Since this problem can happen any number of times (many
364 # duplicates) this is performed in a loop. The loop breaks when
365 # we cannot solve the problem _OR_ when no error occurs.
366 DependencySolver(job_list)
367 except DependencyDuplicateError as exc:
368 # If both jobs are identical then silently fix the problem by
369 # removing one of the jobs (here the second one we've seen but
370 # it's not relevant as they are possibly identical) and try
371 # again
372 if exc.job == exc.duplicate_job:
373 job_list.remove(exc.duplicate_job)
374 continue
375 else:
376 # If the jobs differ report this back to the caller
377 raise
378 else:
379 # If there are no problems then break the loop
380 break
343 self._job_list = job_list381 self._job_list = job_list
344 self._job_state_map = {job.name: JobState(job)382 self._job_state_map = {job.name: JobState(job)
345 for job in self._job_list}383 for job in self._job_list}
346384
=== modified file 'plainbox/plainbox/impl/test_depmgr.py'
--- plainbox/plainbox/impl/test_depmgr.py 2013-03-14 10:36:25 +0000
+++ plainbox/plainbox/impl/test_depmgr.py 2013-03-19 19:07:23 +0000
@@ -226,6 +226,16 @@
226 observed = DependencySolver.resolve_dependencies(job_list)226 observed = DependencySolver.resolve_dependencies(job_list)
227 self.assertEqual(expected, observed)227 self.assertEqual(expected, observed)
228228
229 def test_duplicate_error(self):
230 A = make_job('A')
231 another_A = make_job('A')
232 job_list = [A, another_A]
233 with self.assertRaises(DependencyDuplicateError) as call:
234 DependencySolver.resolve_dependencies(job_list)
235 self.assertIs(call.exception.job, A)
236 self.assertIs(call.exception.duplicate_job, another_A)
237
238
229 def test_missing_direct_dependency(self):239 def test_missing_direct_dependency(self):
230 # This tests missing dependencies240 # This tests missing dependencies
231 # A -> (inexisting B)241 # A -> (inexisting B)
232242
=== modified file 'plainbox/plainbox/impl/test_session.py'
--- plainbox/plainbox/impl/test_session.py 2013-03-14 10:36:25 +0000
+++ plainbox/plainbox/impl/test_session.py 2013-03-19 19:07:23 +0000
@@ -1,6 +1,6 @@
1# This file is part of Checkbox.1# This file is part of Checkbox.
2#2#
3# Copyright 2012 Canonical Ltd.3# Copyright 2012, 2013 Canonical Ltd.
4# Written by:4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#6#
@@ -32,6 +32,7 @@
32from tempfile import TemporaryDirectory32from tempfile import TemporaryDirectory
33from unittest import TestCase33from unittest import TestCase
3434
35from plainbox.impl.depmgr import DependencyDuplicateError
35from plainbox.impl.depmgr import DependencyMissingError36from plainbox.impl.depmgr import DependencyMissingError
36from plainbox.impl.resource import Resource37from plainbox.impl.resource import Resource
37from plainbox.impl.result import JobResult38from plainbox.impl.result import JobResult
@@ -293,6 +294,28 @@
293 self.assertIsInstance(problems[0], DependencyMissingError)294 self.assertIsInstance(problems[0], DependencyMissingError)
294 self.assertIs(problems[0].affected_job, A)295 self.assertIs(problems[0].affected_job, A)
295296
297 def test_init_with_identical_jobs(self):
298 A = make_job("A")
299 second_A = make_job("A")
300 third_A = make_job("A")
301 # Identical jobs are folded for backwards compatibility with some local
302 # jobs that re-added existing jobs
303 session = SessionState([A, second_A, third_A])
304 # But we don't really store both, just the first one
305 self.assertEqual(session.job_list, [A])
306
307 def test_init_with_colliding_jobs(self):
308 # This is similar to the test above but the jobs actually differ In
309 # this case the _second_ job is rejected but it really signifies a
310 # deeper problem that should only occur during development of jobs
311 A = make_job("A")
312 different_A = make_job("A", plugin="resource")
313 with self.assertRaises(DependencyDuplicateError) as call:
314 SessionState([A, different_A])
315 self.assertIs(call.exception.job, A)
316 self.assertIs(call.exception.duplicate_job, different_A)
317 self.assertIs(call.exception.affected_job, different_A)
318
296319
297class SessionStateSpecialTests(TestCase):320class SessionStateSpecialTests(TestCase):
298321

Subscribers

People subscribed via source and target branches