Merge lp:~robru/cupstream2distro/scan-nonpublished-silos into lp:cupstream2distro
- scan-nonpublished-silos
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Robert Bruce Park | ||||
Approved revision: | 1171 | ||||
Merged at revision: | 1163 | ||||
Proposed branch: | lp:~robru/cupstream2distro/scan-nonpublished-silos | ||||
Merge into: | lp:cupstream2distro | ||||
Diff against target: |
278 lines (+100/-29) 6 files modified
citrain/migration.py (+16/-4) citrain/recipes/merge.py (+4/-1) cupstream2distro/silomanager.py (+18/-0) tests/unit/test_recipe_merge.py (+9/-7) tests/unit/test_script_migration.py (+46/-17) tests/unit/test_silomanager.py (+7/-0) |
||||
To merge this branch: | bzr merge lp:~robru/cupstream2distro/scan-nonpublished-silos | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Bruce Park (community) | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Review via email: mp+275116@code.launchpad.net |
Commit message
Scan all silos every 5 minutes looking for new commits.
Description of the change
PS Jenkins bot (ps-jenkins) wrote : | # |
- 1164. By Robert Bruce Park
-
Run unbuilt phase unconditionally.
- 1165. By Robert Bruce Park
-
Clear failures for each silo.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1165
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 1166. By Robert Bruce Park
-
Skip already discovered unbuilt silos.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1166
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Robert Bruce Park (robru) wrote : | # |
Ok, this seems to be halfways workable, but there's one thing I'm not crazy about. There's no silo locking, so most likely this'll behave like this:
1. User pushes new commit to branch.
2. Train notices this and indicates the new commit in the status.
3. User clicks build
4. Train sees the new commit still isn't built so sets the status again, mid-build.
Although the build job does record the commit info early in the build so this might not actually be a problem in practise, but it's still a bit racy.
Would be nice to have a way of saying "actually this silo is building *RIGHT NOW* so don't scan it for new commits"
- 1167. By Robert Bruce Park
-
Mark packages dirty on disk as well.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1167
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 1168. By Robert Bruce Park
-
Implement rudimentary silo locking.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1168
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 1169. By Robert Bruce Park
-
Set silo status better.
- 1170. By Robert Bruce Park
-
Whitespace fix.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1170
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 1171. By Robert Bruce Park
-
Stop checking already-dirty silos.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1171
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Robert Bruce Park (robru) wrote : | # |
Alright this is looking brilliant in staging.
Preview Diff
1 | === modified file 'citrain/migration.py' |
2 | --- citrain/migration.py 2015-09-23 22:56:07 +0000 |
3 | +++ citrain/migration.py 2015-10-21 23:12:49 +0000 |
4 | @@ -31,6 +31,7 @@ |
5 | |
6 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) |
7 | |
8 | +from citrain.recipes.base import BuildBase |
9 | from citrain.recipes.manager import Manager |
10 | from cupstream2distro.errors import CITrainError |
11 | from cupstream2distro.utils import env, run_script |
12 | @@ -45,14 +46,25 @@ |
13 | """ |
14 | for silo_state in SiloState.iterate(): |
15 | env.SILONAME = silo_state |
16 | - if silo_state.is_published: |
17 | + BuildBase.failures.clear() |
18 | + try: |
19 | + silo_state.enforce_lock() |
20 | + except CITrainError as err: |
21 | + logging.info(str(err) + '\n') |
22 | + continue |
23 | + |
24 | + if 'dirty' not in silo_state.tokenize(): |
25 | logging.info('Inspecting silo {}:'.format(silo_state.ppa.web_link)) |
26 | + manager = Manager(silo_state) |
27 | try: |
28 | - Manager(silo_state).do('migration') |
29 | - merge_main() |
30 | + manager.do('unbuilt') |
31 | + if silo_state.is_published: |
32 | + manager.do('migration') |
33 | + merge_main() |
34 | except CITrainError as err: |
35 | logging.info(str(err) + '\n') |
36 | - silo_state.status = 'Migration: ' + str(err) |
37 | + if not silo_state.mark_dirty(): |
38 | + silo_state.status = 'Migration: ' + str(err) |
39 | silo_state.save_config() |
40 | return 0 |
41 | |
42 | |
43 | === modified file 'citrain/recipes/merge.py' |
44 | --- citrain/recipes/merge.py 2015-10-20 09:03:25 +0000 |
45 | +++ citrain/recipes/merge.py 2015-10-21 23:12:49 +0000 |
46 | @@ -31,6 +31,7 @@ |
47 | from cupstream2distro.errors import BuildError, PrepError |
48 | from cupstream2distro.settings import BOT_DEBFULLNAME |
49 | from cupstream2distro.utils import env, log_value_of |
50 | +from cupstream2distro.silomanager import mark_packages_dirty |
51 | |
52 | |
53 | PUBLISHABLE_STATES = ('Approved', 'Merged') |
54 | @@ -242,7 +243,9 @@ |
55 | |
56 | def unbuilt_phase(self): |
57 | """Block publication of merges that have new commits.""" |
58 | - if check_for_unbuilt_revids(self.name, self.merges): |
59 | + merges = Merge.all_mps[self.name] |
60 | + if check_for_unbuilt_revids(self.name, merges): |
61 | + mark_packages_dirty(env.SILONAME, [self.name]) |
62 | self.failures.add(self.name + ' has new, unbuilt commits.') |
63 | |
64 | def merge_phase(self): |
65 | |
66 | === modified file 'cupstream2distro/silomanager.py' |
67 | --- cupstream2distro/silomanager.py 2015-10-15 00:19:37 +0000 |
68 | +++ cupstream2distro/silomanager.py 2015-10-21 23:12:49 +0000 |
69 | @@ -47,6 +47,7 @@ |
70 | STABLE_OVERLAY_PPA, |
71 | ) |
72 | from cupstream2distro.utils import ( |
73 | + atomic_write, |
74 | env, |
75 | memoize, |
76 | log_value_of, |
77 | @@ -196,6 +197,8 @@ |
78 | if self.strict: |
79 | signal.signal(signal.SIGTERM, self.cleanup_on_kill) |
80 | sys.excepthook = self.cleanup_on_exception |
81 | + with atomic_write(self.lockfile, 'w') as lock: |
82 | + lock.write(str(os.getpid())) |
83 | |
84 | def __str__(self): |
85 | """Convert SiloState instance to printable string.""" |
86 | @@ -214,6 +217,20 @@ |
87 | else: |
88 | super().__setattr__(key, value) |
89 | |
90 | + @property |
91 | + def lockfile(self): |
92 | + """Identify where the lockfile should be.""" |
93 | + return join(SILOS_DIR, self.siloname, 'lock') |
94 | + |
95 | + def enforce_lock(self): |
96 | + """Raise PrepError if this silo is locked by a running process.""" |
97 | + try: |
98 | + with utf8_open(self.lockfile) as lock: |
99 | + os.kill(int(lock.read().strip()), 0) |
100 | + except (FileNotFoundError, ProcessLookupError): |
101 | + return |
102 | + raise PrepError('Silo {} is locked by another process.'.format(self)) |
103 | + |
104 | def assign(self, requestid): |
105 | """Assign a new requestid to an available silo.""" |
106 | self.requestid = requestid |
107 | @@ -319,6 +336,7 @@ |
108 | if packages: |
109 | self.status = msg.format(', '.join(packages)) |
110 | self.push_to_bileto() |
111 | + return packages |
112 | |
113 | def load_bileto(self): |
114 | """Fetch request from Bileto.""" |
115 | |
116 | === modified file 'tests/unit/test_recipe_merge.py' |
117 | --- tests/unit/test_recipe_merge.py 2015-10-20 08:35:12 +0000 |
118 | +++ tests/unit/test_recipe_merge.py 2015-10-21 23:12:49 +0000 |
119 | @@ -420,15 +420,17 @@ |
120 | self.assertFalse(merge.failures) |
121 | |
122 | @patch('citrain.recipes.merge.Rev') |
123 | - def test_merge_unbuilt_phase(self, rev_mock): |
124 | + @patch('citrain.recipes.merge.mark_packages_dirty') |
125 | + def test_merge_unbuilt_phase(self, dirty, rev_mock): |
126 | """Block publication if merges have new commits.""" |
127 | rev_mock.return_value.read.return_value = None |
128 | - merge = Merge('foobaz', self.series, self.dest, self.ppa) |
129 | - merge.merges = [Mock( |
130 | + Merge.all_mps = dict(foobaz=[Mock( |
131 | web_link='foo', |
132 | source_branch=Mock(last_scanned_id='zip'), |
133 | - )] |
134 | + )]) |
135 | + merge = Merge('foobaz', self.series, self.dest, self.ppa) |
136 | merge.unbuilt_phase() |
137 | + dirty.assert_called_once_with(self.tempdir, ['foobaz']) |
138 | self.assertEqual(merge.failures, { |
139 | 'foobaz has new, unbuilt commits.' |
140 | }) |
141 | @@ -437,11 +439,11 @@ |
142 | def test_merge_unbuilt_phase_none(self, rev_mock): |
143 | """Allow publication if there are no new commits.""" |
144 | rev_mock.return_value.read.return_value = 'zip' |
145 | - merge = Merge('foobaz', self.series, self.dest, self.ppa) |
146 | - merge.merges = [Mock( |
147 | + Merge.all_mps = dict(foobaz=[Mock( |
148 | web_link='foo', |
149 | source_branch=Mock(last_scanned_id='zip'), |
150 | - )] |
151 | + )]) |
152 | + merge = Merge('foobaz', self.series, self.dest, self.ppa) |
153 | merge.unbuilt_phase() |
154 | self.assertFalse(merge.failures) |
155 | |
156 | |
157 | === modified file 'tests/unit/test_script_migration.py' |
158 | --- tests/unit/test_script_migration.py 2015-09-24 05:06:40 +0000 |
159 | +++ tests/unit/test_script_migration.py 2015-10-21 23:12:49 +0000 |
160 | @@ -43,17 +43,24 @@ |
161 | |
162 | def test_main(self): |
163 | """Trigger merges after successful migrations.""" |
164 | + tokenize = lambda: Mock(return_value={'hi'}) |
165 | states = self.script.SiloState.iterate.return_value = [ |
166 | - Mock(is_published=True) for rid in range(3)] |
167 | + Mock(tokenize=tokenize(), is_published=True) for rid in range(3)] |
168 | self.assertEqual(self.script.main(), 0) |
169 | for state in states: |
170 | - self.assertEqual(state.mock_calls, []) |
171 | + self.assertEqual(state.mock_calls, [ |
172 | + call.enforce_lock(), |
173 | + call.tokenize(), |
174 | + ]) |
175 | self.assertEqual(self.script.Manager.mock_calls, [ |
176 | call(states[0]), |
177 | + call().do('unbuilt'), |
178 | call().do('migration'), |
179 | call(states[1]), |
180 | + call().do('unbuilt'), |
181 | call().do('migration'), |
182 | call(states[2]), |
183 | + call().do('unbuilt'), |
184 | call().do('migration'), |
185 | ]) |
186 | self.assertEqual(self.script.merge_main.mock_calls, [call()] * 3) |
187 | @@ -65,12 +72,29 @@ |
188 | self.assertEqual(self.script.Manager.mock_calls, []) |
189 | self.assertEqual(self.script.merge_main.mock_calls, []) |
190 | |
191 | + def test_main_skip_locked_silos(self): |
192 | + """Skip over silos that are currently used by other processes.""" |
193 | + states = self.script.SiloState.iterate.return_value = [Mock()] |
194 | + states[0].enforce_lock.side_effect = self.script.CITrainError |
195 | + self.assertEqual(self.script.main(), 0) |
196 | + self.assertEqual(self.script.Manager.mock_calls, []) |
197 | + self.assertEqual(self.script.merge_main.mock_calls, []) |
198 | + self.assertEqual(states[0].mock_calls, [call.enforce_lock()]) |
199 | + |
200 | def test_main_skip_unpublished_silos(self): |
201 | """Don't merge silos that are unpublished.""" |
202 | + tokenize = Mock(return_value={'hi'}) |
203 | states = self.script.SiloState.iterate.return_value = [ |
204 | - Mock(is_published=False) for rid in range(3)] |
205 | + Mock(tokenize=tokenize, is_published=False) for rid in range(3)] |
206 | self.assertEqual(self.script.main(), 0) |
207 | - self.assertEqual(self.script.Manager.mock_calls, []) |
208 | + self.assertEqual(self.script.Manager.mock_calls, [ |
209 | + call(states[0]), |
210 | + call().do('unbuilt'), |
211 | + call(states[1]), |
212 | + call().do('unbuilt'), |
213 | + call(states[2]), |
214 | + call().do('unbuilt'), |
215 | + ]) |
216 | self.assertEqual(self.script.merge_main.mock_calls, []) |
217 | for state in states: |
218 | self.assertEqual(state.set_migrating.mock_calls, []) |
219 | @@ -78,23 +102,28 @@ |
220 | |
221 | def test_main_dont_merge_migrating_silos(self): |
222 | """Don't merge silos that are still migrating.""" |
223 | + tokenize = Mock(return_value={'hi'}) |
224 | states = self.script.SiloState.iterate.return_value = [ |
225 | - Mock(requestid=str(rid)) for rid in range(3)] |
226 | - self.script.Manager.return_value.do.side_effect = MigrationError( |
227 | - 'foo is in the Proposed pocket.') |
228 | + Mock(tokenize=tokenize, is_published=True, requestid=str(1))] |
229 | + states[0].mark_dirty.return_value = False |
230 | + |
231 | + def set_side_effect(*ignore): |
232 | + """Raise an exception second time mock is called.""" |
233 | + self.script.Manager.return_value.do.side_effect = MigrationError( |
234 | + 'foo is in the Proposed pocket.') |
235 | + self.script.Manager.return_value.do.side_effect = set_side_effect |
236 | self.assertEqual(self.script.main(), 0) |
237 | - for state in states: |
238 | - self.assertEqual( |
239 | - state.status, 'Migration: foo is in the Proposed pocket.') |
240 | - self.assertEqual(state.mock_calls, [ |
241 | - call.save_config(), |
242 | - ]) |
243 | + self.assertEqual( |
244 | + states[0].status, 'Migration: foo is in the Proposed pocket.') |
245 | + self.assertEqual(states[0].mock_calls, [ |
246 | + call.enforce_lock(), |
247 | + call.tokenize(), |
248 | + call.mark_dirty(), |
249 | + call.save_config(), |
250 | + ]) |
251 | self.assertEqual(self.script.Manager.mock_calls, [ |
252 | call(states[0]), |
253 | - call().do('migration'), |
254 | - call(states[1]), |
255 | - call().do('migration'), |
256 | - call(states[2]), |
257 | + call().do('unbuilt'), |
258 | call().do('migration'), |
259 | ]) |
260 | self.assertEqual(self.script.merge_main.mock_calls, []) |
261 | |
262 | === modified file 'tests/unit/test_silomanager.py' |
263 | --- tests/unit/test_silomanager.py 2015-10-07 19:07:01 +0000 |
264 | +++ tests/unit/test_silomanager.py 2015-10-21 23:12:49 +0000 |
265 | @@ -120,6 +120,13 @@ |
266 | self.assertIsNot(state.info, silomanager.logging.info) |
267 | self.assertFalse(state.strict) |
268 | |
269 | + def test_silostate_enforce_lock(self): |
270 | + """Bail out when silo is locked.""" |
271 | + with self.assertRaisesRegexp(PrepError, 'locked by another'): |
272 | + self.state.enforce_lock() |
273 | + state = SiloState(siloname='ubuntu/landing-xyz') |
274 | + self.assertIsNone(state.enforce_lock()) |
275 | + |
276 | @patch('cupstream2distro.silomanager.SiloState.summarize') |
277 | @patch('cupstream2distro.silomanager.SiloState.set_ready') |
278 | @patch('cupstream2distro.silomanager.SiloState.save_config') |
PASSED: Continuous integration, rev:1163 jenkins. qa.ubuntu. com/job/ cu2d-choo- choo-ci/ 833/
http://
Executed test runs:
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/cu2d- choo-choo- ci/833/ rebuild
http://