~launchpad-pqm/launchpad/devel

14291.1.2 by Jeroen Vermeulen
Lint.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4983.1.2 by Curtis Hovey
Added pylint exceptions to database classes.
4
# pylint: disable-msg=E0611,W0212
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
5
6
__metaclass__ = type
7
8
__all__ = [
9
    'BuildQueue',
7675.452.1 by Muharem Hrnjadovic
imported old development branch
10
    'BuildQueueSet',
7675.452.10 by Muharem Hrnjadovic
jtv's review comments
11
    'specific_job_classes',
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
12
    ]
13
10234.1.1 by Muharem Hrnjadovic
imported work in progress
14
from collections import defaultdict
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
15
from datetime import (
16
    datetime,
17
    timedelta,
18
    )
14513.2.1 by Raphael Badin
Preload build data prior to displaying the builders homepage.
19
from itertools import groupby
4674.2.8 by Celso Providelo
applying review comments, take 3, [r=kiko].
20
import logging
14513.2.1 by Raphael Badin
Preload build data prior to displaying the builders homepage.
21
from operator import attrgetter
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
10466.9.14 by Jeroen Vermeulen
Review changes.
23
import pytz
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
24
from sqlobject import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
25
    BoolCol,
26
    ForeignKey,
27
    IntCol,
28
    IntervalCol,
29
    SQLObjectNotFound,
30
    StringCol,
31
    )
32
from zope.component import (
33
    getSiteManager,
34
    getUtility,
35
    )
7675.575.1 by William Grant
Move lp.soyuz.interfaces.build.BuildStatus to lp.buildmaster.interfaces.buildbase. Sort various imports along the way.
36
from zope.interface import implements
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
37
11270.1.3 by Tim Penhey
Changed NotFoundError imports - gee there were a lot of them.
38
from lp.app.errors import NotFoundError
11458.1.2 by Jelmer Vernooij
Move BuildFarmJobType to lp.buildmaster.enums.
39
from lp.buildmaster.enums import BuildFarmJobType
14291.1.2 by Jeroen Vermeulen
Lint.
40
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
7675.420.12 by Michael Nelson
Made the BuildQueue item responsible for providing the required behavior.
41
from lp.buildmaster.interfaces.buildfarmjobbehavior import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
42
    IBuildFarmJobBehavior,
43
    )
44
from lp.buildmaster.interfaces.buildqueue import (
45
    IBuildQueue,
46
    IBuildQueueSet,
47
    )
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
48
from lp.services.database.constants import DEFAULT
49
from lp.services.database.enumcol import EnumCol
50
from lp.services.database.sqlbase import (
51
    SQLBase,
52
    sqlvalues,
53
    )
7675.391.14 by Muharem Hrnjadovic
Fixing code to make tests pass.
54
from lp.services.job.interfaces.job import JobStatus
55
from lp.services.job.model.job import Job
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
56
from lp.services.webapp.interfaces import (
57
    DEFAULT_FLAVOR,
58
    IStoreSelector,
59
    MAIN_STORE,
60
    )
4674.2.3 by Celso Providelo
Integrating 'score' implementation in IBuildQueue.
61
62
10234.1.1 by Muharem Hrnjadovic
imported work in progress
63
def normalize_virtualization(virtualized):
64
    """Jobs with NULL virtualization settings should be treated the
65
       same way as virtualized jobs."""
66
    return virtualized is None or virtualized
67
68
7675.452.1 by Muharem Hrnjadovic
imported old development branch
69
def specific_job_classes():
70
    """Job classes that may run on the build farm."""
71
    job_classes = dict()
72
    # Get all components that implement the `IBuildFarmJob` interface.
73
    components = getSiteManager()
74
    implementations = sorted(components.getUtilitiesFor(IBuildFarmJob))
75
    # The above yields a collection of 2-tuples where the first element
76
    # is the name of the `BuildFarmJobType` enum and the second element
77
    # is the implementing class respectively.
78
    for job_enum_name, job_class in implementations:
79
        job_enum = getattr(BuildFarmJobType, job_enum_name)
80
        job_classes[job_enum] = job_class
81
82
    return job_classes
83
84
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
85
def get_builder_data():
86
    """How many working builders are there, how are they configured?"""
87
    store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
88
    builder_data = """
89
        SELECT processor, virtualized, COUNT(id) FROM builder
90
        WHERE builderok = TRUE AND manual = FALSE
91
        GROUP BY processor, virtualized;
92
    """
93
    results = store.execute(builder_data).get_all()
7675.509.141 by William Grant
Fix lint.
94
    builders_in_total = virtualized_total = 0
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
95
96
    builder_stats = defaultdict(int)
97
    for processor, virtualized, count in results:
98
        builders_in_total += count
99
        if virtualized:
100
            virtualized_total += count
101
        builder_stats[(processor, virtualized)] = count
102
103
    builder_stats[(None, True)] = virtualized_total
104
    # Jobs with a NULL virtualized flag should be treated the same as
105
    # jobs where virtualized=TRUE.
106
    builder_stats[(None, None)] = virtualized_total
107
    builder_stats[(None, False)] = builders_in_total - virtualized_total
108
    return builder_stats
109
10234.1.5 by Muharem Hrnjadovic
Cleaned up more test code.
110
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
111
class BuildQueue(SQLBase):
112
    implements(IBuildQueue)
113
    _table = "BuildQueue"
114
    _defaultOrder = "id"
115
10899.2.13 by Aaron Bentley
Fix more test failures.
116
    def __init__(self, job, job_type=DEFAULT,  estimated_duration=DEFAULT,
117
                 virtualized=DEFAULT, processor=DEFAULT, lastscore=None):
10899.2.10 by Aaron Bentley
Make scoring automatic and give manual builds a higher score.
118
        super(BuildQueue, self).__init__(job_type=job_type, job=job,
119
            virtualized=virtualized, processor=processor,
10899.2.12 by Aaron Bentley
Fix failing tests.
120
            estimated_duration=estimated_duration, lastscore=lastscore)
10899.2.13 by Aaron Bentley
Fix more test failures.
121
        if lastscore is None and self.specific_job is not None:
10899.2.12 by Aaron Bentley
Fix failing tests.
122
            self.score()
10899.2.10 by Aaron Bentley
Make scoring automatic and give manual builds a higher score.
123
7675.391.4 by Muharem Hrnjadovic
Removed unused IBuildQueue.buildduration property.
124
    job = ForeignKey(dbName='job', foreignKey='Job', notNull=True)
7675.391.14 by Muharem Hrnjadovic
Fixing code to make tests pass.
125
    job_type = EnumCol(
7675.390.7 by Muharem Hrnjadovic
Review comments, round 3.
126
        enum=BuildFarmJobType, notNull=True,
127
        default=BuildFarmJobType.PACKAGEBUILD, dbName='job_type')
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
128
    builder = ForeignKey(dbName='builder', foreignKey='Builder', default=None)
129
    logtail = StringCol(dbName='logtail', default=None)
130
    lastscore = IntCol(dbName='lastscore', default=0)
131
    manual = BoolCol(dbName='manual', default=False)
7675.403.2 by Muharem Hrnjadovic
Merged in code/test fixes.
132
    estimated_duration = IntervalCol()
10130.8.6 by William Grant
BuildQueue.processor can be NULL.
133
    processor = ForeignKey(dbName='processor', foreignKey='Processor')
7675.447.4 by Muharem Hrnjadovic
model class adjustments
134
    virtualized = BoolCol(dbName='virtualized')
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
135
7675.391.28 by Muharem Hrnjadovic
More improvements.
136
    @property
7675.420.12 by Michael Nelson
Made the BuildQueue item responsible for providing the required behavior.
137
    def required_build_behavior(self):
138
        """See `IBuildQueue`."""
7675.420.16 by Michael Nelson
Refactored to adapt on the job rather than the job type and added tests.
139
        return IBuildFarmJobBehavior(self.specific_job)
7675.420.12 by Michael Nelson
Made the BuildQueue item responsible for providing the required behavior.
140
14513.2.3 by Raphael Badin
Cachedproperty back to property.
141
    @property
7675.391.28 by Muharem Hrnjadovic
More improvements.
142
    def specific_job(self):
143
        """See `IBuildQueue`."""
7675.452.1 by Muharem Hrnjadovic
imported old development branch
144
        specific_class = specific_job_classes()[self.job_type]
10137.5.8 by Jeroen Vermeulen
Delegated the BuildQueue.specific_job query to BuildFarmJob, and specialized for my build-farm job class; added interface.
145
        return specific_class.getByJob(self.job)
7675.391.5 by Muharem Hrnjadovic
work in progress.
146
14513.2.1 by Raphael Badin
Preload build data prior to displaying the builders homepage.
147
    @staticmethod
148
    def preloadSpecificJobData(queues):
149
        key = attrgetter('job_type')
150
        for job_type, grouped_queues in groupby(queues, key=key):
151
            specific_class = specific_job_classes()[job_type]
152
            queue_subset = list(grouped_queues)
153
            # We need to preload the build farm jobs early to avoid
154
            # the call to _set_build_farm_job to look up BuildFarmBuildJobs
155
            # one by one.
156
            specific_class.preloadBuildFarmJobs(queue_subset)
157
            specific_jobs = specific_class.getByJobs(queue_subset)
158
            if len(list(specific_jobs)) == 0:
14513.2.2 by Raphael Badin
Fix shortcut.
159
                continue
14513.2.1 by Raphael Badin
Preload build data prior to displaying the builders homepage.
160
            specific_class.preloadJobsData(specific_jobs)
161
7675.390.5 by Muharem Hrnjadovic
Review changes, round 1.
162
    @property
163
    def date_started(self):
164
        """See `IBuildQueue`."""
165
        return self.job.date_started
166
10466.9.5 by Jeroen Vermeulen
Prototype with weird timezone-related test failure in test_min_time_to_next_builder.
167
    @property
168
    def current_build_duration(self):
169
        """See `IBuildQueue`."""
170
        date_started = self.date_started
171
        if date_started is None:
172
            return None
173
        else:
10466.9.14 by Jeroen Vermeulen
Review changes.
174
            return self._now() - date_started
10466.9.5 by Jeroen Vermeulen
Prototype with weird timezone-related test failure in test_min_time_to_next_builder.
175
10011.1.3 by Julian Edwards
Add fix and spruce up test
176
    def destroySelf(self):
177
        """Remove this record and associated job/specific_job."""
178
        job = self.job
179
        specific_job = self.specific_job
180
        SQLBase.destroySelf(self)
10793.1.1 by Jeroen Vermeulen
Delegate deletion of build-farm jobs to IBuildFarmJobDerived.
181
        specific_job.cleanUp()
10011.1.3 by Julian Edwards
Add fix and spruce up test
182
        job.destroySelf()
183
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
184
    def manualScore(self, value):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
185
        """See `IBuildQueue`."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
186
        self.lastscore = value
187
        self.manual = True
188
4674.2.8 by Celso Providelo
applying review comments, take 3, [r=kiko].
189
    def score(self):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
190
        """See `IBuildQueue`."""
4674.2.8 by Celso Providelo
applying review comments, take 3, [r=kiko].
191
        # Grab any logger instance available.
192
        logger = logging.getLogger()
7675.391.28 by Muharem Hrnjadovic
More improvements.
193
        name = self.specific_job.getName()
5089.2.2 by Celso Providelo
sorting slave-scanner slowness.
194
4674.2.3 by Celso Providelo
Integrating 'score' implementation in IBuildQueue.
195
        if self.manual:
4674.2.6 by Celso Providelo
applying review comments, [r=kiko].
196
            logger.debug(
7675.391.23 by Muharem Hrnjadovic
Fixed more tests.
197
                "%s (%d) MANUALLY RESCORED" % (name, self.lastscore))
4674.2.6 by Celso Providelo
applying review comments, [r=kiko].
198
            return
4674.2.3 by Celso Providelo
Integrating 'score' implementation in IBuildQueue.
199
7675.390.7 by Muharem Hrnjadovic
Review comments, round 3.
200
        # Allow the `IBuildFarmJob` instance with the data/logic specific to
7675.390.6 by Muharem Hrnjadovic
Review comments, round 2.
201
        # the job at hand to calculate the score as appropriate.
7675.391.29 by Muharem Hrnjadovic
Simplified code.
202
        self.lastscore = self.specific_job.score()
4674.2.3 by Celso Providelo
Integrating 'score' implementation in IBuildQueue.
203
3691.454.27 by Robert Collins
Delegate log file naming in the build slave scanner to the BuildQueue
204
    def getLogFileName(self):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
205
        """See `IBuildQueue`."""
7675.390.7 by Muharem Hrnjadovic
Review comments, round 3.
206
        # Allow the `IBuildFarmJob` instance with the data/logic specific to
7675.390.6 by Muharem Hrnjadovic
Review comments, round 2.
207
        # the job at hand to calculate the log file name as appropriate.
7675.391.29 by Muharem Hrnjadovic
Simplified code.
208
        return self.specific_job.getLogFileName()
3691.454.27 by Robert Collins
Delegate log file naming in the build slave scanner to the BuildQueue
209
5008.3.5 by Julian Edwards
Clean up code and tests.
210
    def markAsBuilding(self, builder):
211
        """See `IBuildQueue`."""
212
        self.builder = builder
7675.391.27 by Muharem Hrnjadovic
More fixes.
213
        if self.job.status != JobStatus.RUNNING:
214
            self.job.start()
7675.391.29 by Muharem Hrnjadovic
Simplified code.
215
        self.specific_job.jobStarted()
5008.3.5 by Julian Edwards
Clean up code and tests.
216
8137.17.24 by Barry Warsaw
thread merge
217
    def reset(self):
218
        """See `IBuildQueue`."""
219
        self.builder = None
7675.391.22 by Muharem Hrnjadovic
Fixed more breakage.
220
        if self.job.status != JobStatus.WAITING:
221
            self.job.queue()
7675.391.5 by Muharem Hrnjadovic
work in progress.
222
        self.job.date_started = None
7675.391.19 by Muharem Hrnjadovic
Refactored code related to build dispatching.
223
        self.job.date_finished = None
8137.17.24 by Barry Warsaw
thread merge
224
        self.logtail = None
7675.391.29 by Muharem Hrnjadovic
Simplified code.
225
        self.specific_job.jobReset()
8137.17.24 by Barry Warsaw
thread merge
226
14206.1.2 by Julian Edwards
merge a chunk of the backed-out change from r14192
227
    def cancel(self):
228
        """See `IBuildQueue`."""
229
        self.specific_job.jobCancel()
230
        self.destroySelf()
231
7675.391.38 by Muharem Hrnjadovic
Work in progress.
232
    def setDateStarted(self, timestamp):
233
        """See `IBuildQueue`."""
234
        self.job.date_started = timestamp
235
10234.1.20 by Muharem Hrnjadovic
jtv's review comments, round #3
236
    def _getFreeBuildersCount(self, processor, virtualized):
10096.1.1 by Muharem Hrnjadovic
imported builder data functions and tests
237
        """How many builders capable of running jobs for the given processor
238
        and virtualization combination are idle/free at present?"""
239
        query = """
240
            SELECT COUNT(id) FROM builder
241
            WHERE
242
                builderok = TRUE AND manual = FALSE
243
                AND id NOT IN (
244
                    SELECT builder FROM BuildQueue WHERE builder IS NOT NULL)
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
245
                AND virtualized = %s
246
            """ % sqlvalues(normalize_virtualization(virtualized))
10096.1.1 by Muharem Hrnjadovic
imported builder data functions and tests
247
        if processor is not None:
248
            query += """
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
249
                AND processor = %s
250
            """ % sqlvalues(processor)
10096.1.1 by Muharem Hrnjadovic
imported builder data functions and tests
251
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
252
        result_set = store.execute(query)
253
        free_builders = result_set.get_one()[0]
254
        return free_builders
255
10234.1.1 by Muharem Hrnjadovic
imported work in progress
256
    def _estimateTimeToNextBuilder(self):
10122.1.3 by Muharem Hrnjadovic
Rectified the _estimateTimeToNextBuilder() method and the tests for it.
257
        """Estimate time until next builder becomes available.
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
258
10122.1.3 by Muharem Hrnjadovic
Rectified the _estimateTimeToNextBuilder() method and the tests for it.
259
        For the purpose of estimating the dispatch time of the job of interest
260
        (JOI) we need to know how long it will take until the job at the head
261
        of JOI's queue is dispatched.
262
263
        There are two cases to consider here: the head job is
264
265
            - processor dependent: only builders with the matching
266
              processor/virtualization combination should be considered.
10234.1.1 by Muharem Hrnjadovic
imported work in progress
267
            - *not* processor dependent: all builders with the matching
268
              virtualization setting should be considered.
10122.1.3 by Muharem Hrnjadovic
Rectified the _estimateTimeToNextBuilder() method and the tests for it.
269
270
        :return: The estimated number of seconds untils a builder capable of
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
271
            running the head job becomes available.
10122.1.3 by Muharem Hrnjadovic
Rectified the _estimateTimeToNextBuilder() method and the tests for it.
272
        """
10234.1.1 by Muharem Hrnjadovic
imported work in progress
273
        head_job_platform = self._getHeadJobPlatform()
274
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
275
        # Return a zero delay if we still have free builders available for the
276
        # given platform/virtualization combination.
10234.1.20 by Muharem Hrnjadovic
jtv's review comments, round #3
277
        free_builders = self._getFreeBuildersCount(*head_job_platform)
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
278
        if free_builders > 0:
279
            return 0
280
10234.1.1 by Muharem Hrnjadovic
imported work in progress
281
        head_job_processor, head_job_virtualized = head_job_platform
10122.1.1 by Muharem Hrnjadovic
merged development branch
282
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
283
        now = self._now()
10122.1.1 by Muharem Hrnjadovic
merged development branch
284
        delay_query = """
285
            SELECT MIN(
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
286
              CASE WHEN
10122.1.1 by Muharem Hrnjadovic
merged development branch
287
                EXTRACT(EPOCH FROM
288
                  (BuildQueue.estimated_duration -
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
289
                   (((%s AT TIME ZONE 'UTC') - Job.date_started))))  >= 0
10122.1.1 by Muharem Hrnjadovic
merged development branch
290
              THEN
291
                EXTRACT(EPOCH FROM
292
                  (BuildQueue.estimated_duration -
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
293
                   (((%s AT TIME ZONE 'UTC') - Job.date_started))))
10122.1.1 by Muharem Hrnjadovic
merged development branch
294
              ELSE
295
                -- Assume that jobs that have overdrawn their estimated
296
                -- duration time budget will complete within 2 minutes.
297
                -- This is a wild guess but has worked well so far.
10122.1.4 by Muharem Hrnjadovic
Graham B.'s review comments
298
                --
299
                -- Please note that this is entirely innocuous i.e. if our
300
                -- guess is off nothing bad will happen but our estimate will
301
                -- not be as good as it could be.
10122.1.1 by Muharem Hrnjadovic
merged development branch
302
                120
303
              END)
304
            FROM
305
                BuildQueue, Job, Builder
306
            WHERE
307
                BuildQueue.job = Job.id
308
                AND BuildQueue.builder = Builder.id
309
                AND Builder.manual = False
310
                AND Builder.builderok = True
311
                AND Job.status = %s
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
312
                AND Builder.virtualized = %s
313
            """ % sqlvalues(
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
314
                now, now, JobStatus.RUNNING,
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
315
                normalize_virtualization(head_job_virtualized))
316
317
        if head_job_processor is not None:
318
            # Only look at builders with specific processor types.
319
            delay_query += """
320
                AND Builder.processor = %s
321
                """ % sqlvalues(head_job_processor)
10122.1.1 by Muharem Hrnjadovic
merged development branch
322
10234.1.1 by Muharem Hrnjadovic
imported work in progress
323
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
10122.1.1 by Muharem Hrnjadovic
merged development branch
324
        result_set = store.execute(delay_query)
10122.1.3 by Muharem Hrnjadovic
Rectified the _estimateTimeToNextBuilder() method and the tests for it.
325
        head_job_delay = result_set.get_one()[0]
10234.1.3 by Muharem Hrnjadovic
Streamlined code and tests, _estimateTimeToNextBuilder() is now only concerned with estimating the time to the next builder. The calling function needs to check whether there are available builders in first place.
326
        return (0 if head_job_delay is None else int(head_job_delay))
10122.1.1 by Muharem Hrnjadovic
merged development branch
327
10234.1.1 by Muharem Hrnjadovic
imported work in progress
328
    def _getPendingJobsClauses(self):
329
        """WHERE clauses for pending job queries, used for dipatch time
330
        estimation."""
331
        virtualized = normalize_virtualization(self.virtualized)
332
        clauses = """
10234.1.21 by Muharem Hrnjadovic
jtv's review comments, round #4
333
            BuildQueue.job = Job.id
334
            AND Job.status = %s
335
            AND (
336
                -- The score must be either above my score or the
337
                -- job must be older than me in cases where the
338
                -- score is equal.
339
                BuildQueue.lastscore > %s OR
340
                (BuildQueue.lastscore = %s AND Job.id < %s))
10234.1.23 by Muharem Hrnjadovic
jtv's review comments, round #6
341
            -- The virtualized values either match or the job
342
            -- does not care about virtualization and the job
343
            -- of interest (JOI) is to be run on a virtual builder
344
            -- (we want to prevent the execution of untrusted code
345
            -- on native builders).
346
            AND COALESCE(buildqueue.virtualized, TRUE) = %s
10234.1.21 by Muharem Hrnjadovic
jtv's review comments, round #4
347
            """ % sqlvalues(
348
                JobStatus.WAITING, self.lastscore, self.lastscore, self.job,
10234.1.23 by Muharem Hrnjadovic
jtv's review comments, round #6
349
                virtualized)
10234.1.1 by Muharem Hrnjadovic
imported work in progress
350
        processor_clause = """
10234.1.21 by Muharem Hrnjadovic
jtv's review comments, round #4
351
            AND (
352
                -- The processor values either match or the candidate
353
                -- job is processor-independent.
354
                buildqueue.processor = %s OR
355
                buildqueue.processor IS NULL)
356
            """ % sqlvalues(self.processor)
10234.1.1 by Muharem Hrnjadovic
imported work in progress
357
        # We don't care about processors if the estimation is for a
358
        # processor-independent job.
359
        if self.processor is not None:
360
            clauses += processor_clause
361
        return clauses
362
363
    def _getHeadJobPlatform(self):
364
        """Find the processor and virtualization setting for the head job.
365
366
        Among the jobs that compete with the job of interest (JOI) for
367
        builders and are queued ahead of it the head job is the one in pole
368
        position i.e. the one to be dispatched to a builder next.
369
370
        :return: A (processor, virtualized) tuple which is the head job's
371
        platform or None if the JOI is the head job.
372
        """
373
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
374
        my_platform = (
375
            getattr(self.processor, 'id', None),
376
            normalize_virtualization(self.virtualized))
377
        query = """
378
            SELECT
379
                processor,
380
                virtualized
381
            FROM
382
                BuildQueue, Job
383
            WHERE
384
            """
385
        query += self._getPendingJobsClauses()
386
        query += """
387
            ORDER BY lastscore DESC, job LIMIT 1
388
            """
10234.1.19 by Muharem Hrnjadovic
jtv's review comments, round #2
389
        result = store.execute(query).get_one()
390
        return (my_platform if result is None else result)
10234.1.1 by Muharem Hrnjadovic
imported work in progress
391
7675.503.18 by Muharem Hrnjadovic
Removed superfluous parameter
392
    def _estimateJobDelay(self, builder_stats):
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
393
        """Sum of estimated durations for *pending* jobs ahead in queue.
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
394
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
395
        For the purpose of estimating the dispatch time of the job of
396
        interest (JOI) we need to know the delay caused by all the pending
397
        jobs that are ahead of the JOI in the queue and that compete with it
398
        for builders.
399
400
        :param builder_stats: A dictionary with builder counts where the
401
            key is a (processor, virtualized) combination (aka "platform") and
402
            the value is the number of builders that can take on jobs
403
            requiring that combination.
7675.503.29 by Muharem Hrnjadovic
allenap's review comments, round 3
404
        :return: An integer value holding the sum of delays (in seconds)
405
            caused by the jobs that are ahead of and competing with the JOI.
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
406
        """
7675.503.13 by Muharem Hrnjadovic
Juhu! First dispatch estimation for a recipe build job works!
407
        def jobs_compete_for_builders(a, b):
408
            """True if the two jobs compete for builders."""
409
            a_processor, a_virtualized = a
410
            b_processor, b_virtualized = b
411
            if a_processor is None or b_processor is None:
412
                # If either of the jobs is platform-independent then the two
413
                # jobs compete for the same builders if the virtualization
414
                # settings match.
415
                if a_virtualized == b_virtualized:
416
                    return True
417
            else:
418
                # Neither job is platform-independent, match processor and
419
                # virtualization settings.
420
                return a == b
421
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
422
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
7675.503.13 by Muharem Hrnjadovic
Juhu! First dispatch estimation for a recipe build job works!
423
        my_platform = (
424
            getattr(self.processor, 'id', None),
425
            normalize_virtualization(self.virtualized))
7675.503.5 by Muharem Hrnjadovic
simplified code and tests, next step: removal of the IBuildFarmJobDispatchEstimation interface
426
        query = """
427
            SELECT
428
                BuildQueue.processor,
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
429
                BuildQueue.virtualized,
430
                COUNT(BuildQueue.job),
431
                CAST(EXTRACT(
432
                    EPOCH FROM
433
                        SUM(BuildQueue.estimated_duration)) AS INTEGER)
7675.503.5 by Muharem Hrnjadovic
simplified code and tests, next step: removal of the IBuildFarmJobDispatchEstimation interface
434
            FROM
435
                BuildQueue, Job
436
            WHERE
10234.1.1 by Muharem Hrnjadovic
imported work in progress
437
            """
438
        query += self._getPendingJobsClauses()
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
439
        query += """
440
            GROUP BY BuildQueue.processor, BuildQueue.virtualized
441
            """
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
442
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
443
        delays_by_platform = store.execute(query).get_all()
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
444
445
        # This will be used to capture per-platform delay totals.
10234.1.1 by Muharem Hrnjadovic
imported work in progress
446
        delays = defaultdict(int)
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
447
        # This will be used to capture per-platform job counts.
10234.1.1 by Muharem Hrnjadovic
imported work in progress
448
        job_counts = defaultdict(int)
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
449
10234.1.22 by Muharem Hrnjadovic
jtv's review comments, round #5
450
        # Divide the estimated duration of the jobs as follows:
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
451
        #   - if a job is tied to a processor TP then divide the estimated
452
        #     duration of that job by the number of builders that target TP
453
        #     since only these can build the job.
454
        #   - if the job is processor-independent then divide its estimated
7675.503.19 by Muharem Hrnjadovic
corrected comment
455
        #     duration by the total number of builders with the same
456
        #     virtualization setting because any one of them may run it.
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
457
        for processor, virtualized, job_count, delay in delays_by_platform:
7675.503.13 by Muharem Hrnjadovic
Juhu! First dispatch estimation for a recipe build job works!
458
            virtualized = normalize_virtualization(virtualized)
459
            platform = (processor, virtualized)
10234.1.1 by Muharem Hrnjadovic
imported work in progress
460
            builder_count = builder_stats.get(platform, 0)
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
461
            if builder_count == 0:
462
                # There is no builder that can run this job, ignore it
463
                # for the purpose of dispatch time estimation.
464
                continue
465
7675.503.13 by Muharem Hrnjadovic
Juhu! First dispatch estimation for a recipe build job works!
466
            if jobs_compete_for_builders(my_platform, platform):
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
467
                # The jobs that target the platform at hand compete with
468
                # the JOI for builders, add their delays.
10234.1.1 by Muharem Hrnjadovic
imported work in progress
469
                delays[platform] += delay
470
                job_counts[platform] += job_count
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
471
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
472
        sum_of_delays = 0
10234.1.22 by Muharem Hrnjadovic
jtv's review comments, round #5
473
        # Now devide the delays based on a jobs/builders comparison.
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
474
        for platform, duration in delays.iteritems():
475
            jobs = job_counts[platform]
476
            builders = builder_stats[platform]
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
477
            # If there are less jobs than builders that can take them on,
478
            # the delays should be averaged/divided by the number of jobs.
479
            denominator = (jobs if jobs < builders else builders)
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
480
            if denominator > 1:
14291.1.2 by Jeroen Vermeulen
Lint.
481
                duration = int(duration / float(denominator))
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
482
483
            sum_of_delays += duration
7675.503.23 by Muharem Hrnjadovic
allenap's review comments, round 2
484
7675.503.26 by Muharem Hrnjadovic
The python-domain iteration were replaced by aggregate functions in the db.
485
        return sum_of_delays
7675.503.1 by Muharem Hrnjadovic
Imported code from mega-branch and some initial tests.
486
10234.1.1 by Muharem Hrnjadovic
imported work in progress
487
    def getEstimatedJobStartTime(self):
488
        """See `IBuildQueue`.
489
490
        The estimated dispatch time for the build farm job at hand is
491
        calculated from the following ingredients:
492
            * the start time for the head job (job at the
493
              head of the respective build queue)
494
            * the estimated build durations of all jobs that
495
              precede the job of interest (JOI) in the build queue
10234.1.22 by Muharem Hrnjadovic
jtv's review comments, round #5
496
              (divided by the number of machines in the respective
10234.1.1 by Muharem Hrnjadovic
imported work in progress
497
              build pool)
498
        """
499
        # This method may only be invoked for pending jobs.
500
        if self.job.status != JobStatus.WAITING:
501
            raise AssertionError(
502
                "The start time is only estimated for pending jobs.")
503
10234.1.5 by Muharem Hrnjadovic
Cleaned up more test code.
504
        builder_stats = get_builder_data()
10234.1.9 by Muharem Hrnjadovic
added test for a (None,False) job
505
        platform = (getattr(self.processor, 'id', None), self.virtualized)
506
        if builder_stats[platform] == 0:
10234.1.1 by Muharem Hrnjadovic
imported work in progress
507
            # No builders that can run the job at hand
508
            #   -> no dispatch time estimation available.
10234.1.5 by Muharem Hrnjadovic
Cleaned up more test code.
509
            return None
10234.1.1 by Muharem Hrnjadovic
imported work in progress
510
511
        # Get the sum of the estimated run times for *pending* jobs that are
512
        # ahead of us in the queue.
513
        sum_of_delays = self._estimateJobDelay(builder_stats)
514
515
        # Get the minimum time duration until the next builder becomes
516
        # available.
517
        min_wait_time = self._estimateTimeToNextBuilder()
518
10234.1.12 by Muharem Hrnjadovic
added boundary tests
519
        # A job will not get dispatched in less than 5 seconds no matter what.
520
        start_time = max(5, min_wait_time + sum_of_delays)
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
521
        result = self._now() + timedelta(seconds=start_time)
10234.1.1 by Muharem Hrnjadovic
imported work in progress
522
523
        return result
524
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
525
    @staticmethod
526
    def _now():
10466.9.7 by Jeroen Vermeulen
Cleaned up some uses of utcnow(), which isn't timezone-aware.
527
        """Return current time (UTC).  Overridable for test purposes."""
10466.9.14 by Jeroen Vermeulen
Review changes.
528
        return datetime.now(pytz.UTC)
10409.4.2 by Muharem Hrnjadovic
Monkey-patched BuildQueue._now() in unit tests to return a constant time stamp.
529
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
530
531
class BuildQueueSet(object):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
532
    """Utility to deal with BuildQueue content class."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
533
    implements(IBuildQueueSet)
534
535
    def __init__(self):
536
        self.title = "The Launchpad build queue"
537
538
    def __iter__(self):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
539
        """See `IBuildQueueSet`."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
540
        return iter(BuildQueue.select())
541
10137.5.1 by Jeroen Vermeulen
Added BuildQueueSet.getByJob.
542
    def __getitem__(self, buildqueue_id):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
543
        """See `IBuildQueueSet`."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
544
        try:
10137.5.1 by Jeroen Vermeulen
Added BuildQueueSet.getByJob.
545
            return BuildQueue.get(buildqueue_id)
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
546
        except SQLObjectNotFound:
10137.5.1 by Jeroen Vermeulen
Added BuildQueueSet.getByJob.
547
            raise NotFoundError(buildqueue_id)
548
549
    def get(self, buildqueue_id):
550
        """See `IBuildQueueSet`."""
551
        return BuildQueue.get(buildqueue_id)
552
553
    def getByJob(self, job):
554
        """See `IBuildQueueSet`."""
555
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
556
        return store.find(BuildQueue, BuildQueue.job == job).one()
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
557
558
    def count(self):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
559
        """See `IBuildQueueSet`."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
560
        return BuildQueue.select().count()
561
562
    def getByBuilder(self, builder):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
563
        """See `IBuildQueueSet`."""
3147.5.54 by Celso Providelo
Builder & BuildQueue content objects in separated files, general code cleanup, established IBuilder.trusted behaviour (trusted -> builds ubuntu pkgs & untrusted-> ppa/grumpy pkgs)
564
        return BuildQueue.selectOneBy(builder=builder)
565
566
    def getActiveBuildJobs(self):
5089.2.9 by Celso Providelo
applying review comments, r=intellectronica.
567
        """See `IBuildQueueSet`."""
7675.391.12 by Muharem Hrnjadovic
Tons of fixes.
568
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
569
        result_set = store.find(
570
            BuildQueue,
571
            BuildQueue.job == Job.id,
10360.4.1 by Michael Nelson
Ensure that active build jobs returned by getActiveBuildJobs() have an associated builder.
572
            # XXX Michael Nelson 2010-02-22 bug=499421
573
            # Avoid corrupt build jobs where the builder is None.
574
            BuildQueue.builder != None,
10130.2.16 by William Grant
BuildQueueSet.getActiveBuildJobs only returns RUNNING jobs.
575
            # status is a property. Let's use _status.
576
            Job._status == JobStatus.RUNNING,
7675.392.5 by Julian Edwards
Fix buildqueue.txt test
577
            Job.date_started != None)
7675.391.12 by Muharem Hrnjadovic
Tons of fixes.
578
        return result_set