Merge lp:~robru/cupstream2distro/scan-nonpublished-silos into lp:cupstream2distro

Proposed by Robert Bruce Park
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
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.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1163
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/833/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/833/rebuild

review: Approve (continuous-integration)
1164. By Robert Bruce Park

Run unbuilt phase unconditionally.

1165. By Robert Bruce Park

Clear failures for each silo.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1165
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/834/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/834/rebuild

review: Approve (continuous-integration)
1166. By Robert Bruce Park

Skip already discovered unbuilt silos.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1166
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/835/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/835/rebuild

review: Approve (continuous-integration)
Revision history for this message
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.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1167
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/836/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/836/rebuild

review: Approve (continuous-integration)
1168. By Robert Bruce Park

Implement rudimentary silo locking.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1168
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/837/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/837/rebuild

review: Approve (continuous-integration)
1169. By Robert Bruce Park

Set silo status better.

1170. By Robert Bruce Park

Whitespace fix.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1170
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/838/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/838/rebuild

review: Approve (continuous-integration)
1171. By Robert Bruce Park

Stop checking already-dirty silos.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:1171
http://jenkins.qa.ubuntu.com/job/cu2d-choo-choo-ci/839/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/cu2d-choo-choo-ci/839/rebuild

review: Approve (continuous-integration)
Revision history for this message
Robert Bruce Park (robru) wrote :

Alright this is looking brilliant in staging.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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')

Subscribers

People subscribed via source and target branches