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
1=== modified file 'plainbox/plainbox/impl/commands/run.py'
2--- plainbox/plainbox/impl/commands/run.py 2013-03-14 17:06:41 +0000
3+++ plainbox/plainbox/impl/commands/run.py 2013-03-19 19:07:23 +0000
4@@ -33,6 +33,7 @@
5
6 from plainbox.impl.commands import PlainBoxCommand
7 from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
8+from plainbox.impl.depmgr import DependencyDuplicateError
9 from plainbox.impl.exporter import get_all_exporters
10 from plainbox.impl.result import JobResult
11 from plainbox.impl.runner import JobRunner
12@@ -127,7 +128,18 @@
13 matching_job_list = self._get_matching_job_list(ns, job_list)
14 print("[ Analyzing Jobs ]".center(80, '='))
15 # Create a session that handles most of the stuff needed to run jobs
16- session = SessionState(job_list)
17+ try:
18+ session = SessionState(job_list)
19+ except DependencyDuplicateError as exc:
20+ # Handle possible DependencyDuplicateError that can happen if
21+ # someone is using plainbox for job development.
22+ print("The job database you are currently using is broken")
23+ print("At least two jobs contend for the name {0}".format(
24+ exc.job.name))
25+ print("First job defined in: {0}".format(exc.job.origin))
26+ print("Second job defined in: {0}".format(
27+ exc.duplicate_job.origin))
28+ raise SystemExit(exc)
29 with session.open():
30 if session.previous_session_file():
31 if self.ask_for_resume():
32
33=== modified file 'plainbox/plainbox/impl/depmgr.py'
34--- plainbox/plainbox/impl/depmgr.py 2013-02-25 11:03:02 +0000
35+++ plainbox/plainbox/impl/depmgr.py 2013-03-19 19:07:23 +0000
36@@ -157,14 +157,21 @@
37 graph while still having access and knowledge of all jobs.
38
39 :returns list: the solution (a list of jobs to execute in order)
40- :raises DependencyCycleError: if a cyclic dependency is present.
41- :raises DependencyMissingErorr: if a required job does not exist.
42+ :raises DependencyDuplicateError:
43+ if a duplicate job definition is present
44+ :raises DependencyCycleError:
45+ if a cyclic dependency is present.
46+ :raises DependencyMissingErorr:
47+ if a required job does not exist.
48 """
49 return cls(job_list)._solve(visit_list)
50
51 def __init__(self, job_list):
52 """
53 Instantiate a new dependency solver with the specified list of jobs
54+
55+ :raises DependencyDuplicateError:
56+ if the initial job_list has any duplicate jobs
57 """
58 # Remember the jobs that were passed
59 self._job_list = job_list
60
61=== modified file 'plainbox/plainbox/impl/session.py'
62--- plainbox/plainbox/impl/session.py 2013-02-28 11:06:34 +0000
63+++ plainbox/plainbox/impl/session.py 2013-03-19 19:07:23 +0000
64@@ -1,6 +1,6 @@
65 # This file is part of Checkbox.
66 #
67-# Copyright 2012 Canonical Ltd.
68+# Copyright 2012, 2013 Canonical Ltd.
69 # Written by:
70 # Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
71 #
72@@ -31,7 +31,7 @@
73 import shutil
74 import tempfile
75
76-from plainbox.impl.depmgr import DependencyError
77+from plainbox.impl.depmgr import DependencyError, DependencyDuplicateError
78 from plainbox.impl.depmgr import DependencySolver
79 from plainbox.impl.job import JobDefinition
80 from plainbox.impl.resource import ExpressionCannotEvaluateError
81@@ -297,6 +297,12 @@
82 changes this will start out empty and will be changeable dynamically.
83 It can still change due to local jobs but there is no API yes.
84
85+ This list cannot have any duplicates, if that is the case a
86+ DependencyDuplicateError is raised. This has to be handled externally
87+ and is a sign that the job database is corrupted or has wrong data. As
88+ an exception if duplicates are perfectly identical this error is
89+ silently corrected.
90+
91 :ivar dict job_state_map: mapping that tracks the state of each job
92
93 Mapping from job name to :class:`JobState`. This basically has the test
94@@ -340,6 +346,38 @@
95 session_data_filename = 'session.json'
96
97 def __init__(self, job_list):
98+ # Start by making a copy of job_list as we may modify it below
99+ job_list = job_list[:]
100+ while True:
101+ try:
102+ # Construct a solver with the job list as passed by the caller.
103+ # This will do a little bit of validation and might raise
104+ # DepdendencyDuplicateError if there are any duplicates at this
105+ # stage.
106+ #
107+ # There's a single case that is handled here though, if both
108+ # jobs are identical this problem is silently fixed. This
109+ # should not happen in normal circumstances but is non the less
110+ # harmless (as long as both jobs are perfectly identical)
111+ #
112+ # Since this problem can happen any number of times (many
113+ # duplicates) this is performed in a loop. The loop breaks when
114+ # we cannot solve the problem _OR_ when no error occurs.
115+ DependencySolver(job_list)
116+ except DependencyDuplicateError as exc:
117+ # If both jobs are identical then silently fix the problem by
118+ # removing one of the jobs (here the second one we've seen but
119+ # it's not relevant as they are possibly identical) and try
120+ # again
121+ if exc.job == exc.duplicate_job:
122+ job_list.remove(exc.duplicate_job)
123+ continue
124+ else:
125+ # If the jobs differ report this back to the caller
126+ raise
127+ else:
128+ # If there are no problems then break the loop
129+ break
130 self._job_list = job_list
131 self._job_state_map = {job.name: JobState(job)
132 for job in self._job_list}
133
134=== modified file 'plainbox/plainbox/impl/test_depmgr.py'
135--- plainbox/plainbox/impl/test_depmgr.py 2013-03-14 10:36:25 +0000
136+++ plainbox/plainbox/impl/test_depmgr.py 2013-03-19 19:07:23 +0000
137@@ -226,6 +226,16 @@
138 observed = DependencySolver.resolve_dependencies(job_list)
139 self.assertEqual(expected, observed)
140
141+ def test_duplicate_error(self):
142+ A = make_job('A')
143+ another_A = make_job('A')
144+ job_list = [A, another_A]
145+ with self.assertRaises(DependencyDuplicateError) as call:
146+ DependencySolver.resolve_dependencies(job_list)
147+ self.assertIs(call.exception.job, A)
148+ self.assertIs(call.exception.duplicate_job, another_A)
149+
150+
151 def test_missing_direct_dependency(self):
152 # This tests missing dependencies
153 # A -> (inexisting B)
154
155=== modified file 'plainbox/plainbox/impl/test_session.py'
156--- plainbox/plainbox/impl/test_session.py 2013-03-14 10:36:25 +0000
157+++ plainbox/plainbox/impl/test_session.py 2013-03-19 19:07:23 +0000
158@@ -1,6 +1,6 @@
159 # This file is part of Checkbox.
160 #
161-# Copyright 2012 Canonical Ltd.
162+# Copyright 2012, 2013 Canonical Ltd.
163 # Written by:
164 # Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
165 #
166@@ -32,6 +32,7 @@
167 from tempfile import TemporaryDirectory
168 from unittest import TestCase
169
170+from plainbox.impl.depmgr import DependencyDuplicateError
171 from plainbox.impl.depmgr import DependencyMissingError
172 from plainbox.impl.resource import Resource
173 from plainbox.impl.result import JobResult
174@@ -293,6 +294,28 @@
175 self.assertIsInstance(problems[0], DependencyMissingError)
176 self.assertIs(problems[0].affected_job, A)
177
178+ def test_init_with_identical_jobs(self):
179+ A = make_job("A")
180+ second_A = make_job("A")
181+ third_A = make_job("A")
182+ # Identical jobs are folded for backwards compatibility with some local
183+ # jobs that re-added existing jobs
184+ session = SessionState([A, second_A, third_A])
185+ # But we don't really store both, just the first one
186+ self.assertEqual(session.job_list, [A])
187+
188+ def test_init_with_colliding_jobs(self):
189+ # This is similar to the test above but the jobs actually differ In
190+ # this case the _second_ job is rejected but it really signifies a
191+ # deeper problem that should only occur during development of jobs
192+ A = make_job("A")
193+ different_A = make_job("A", plugin="resource")
194+ with self.assertRaises(DependencyDuplicateError) as call:
195+ SessionState([A, different_A])
196+ self.assertIs(call.exception.job, A)
197+ self.assertIs(call.exception.duplicate_job, different_A)
198+ self.assertIs(call.exception.affected_job, different_A)
199+
200
201 class SessionStateSpecialTests(TestCase):
202

Subscribers

People subscribed via source and target branches