view bitten/queue.py @ 313:90422699a594

More and improved docstrings (using epydoc format).
author cmlenz
date Thu, 24 Nov 2005 12:34:27 +0000
parents 5f84af72d17f
children 87c9b1e8f086
line wrap: on
line source
# -*- coding: iso8859-1 -*-
#
# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://bitten.cmlenz.net/wiki/License.

"""Implements the scheduling of builds for a project.

This module provides the functionality for scheduling builds for a specific
Trac environment. It is used by both the build master and the web interface to
get the list of required builds (revisions not built yet).

Furthermore, the C{BuildQueue} class is used by the build master to determine
the next pending build, and to match build slaves against configured target
platforms.
"""

from itertools import ifilter
import logging
import re

from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep
from bitten.snapshot import SnapshotManager

log = logging.getLogger('bitten.queue')


def collect_changes(repos, config, db=None):
    """Collect all changes for a build configuration that either have already
    been built, or still need to be built.
    
    This function is a generator that yields `(platform, rev, build)` tuples,
    where `platform` is a `TargetPlatform` object, `rev` is the identifier of
    the changeset, and `build` is a `Build` object or `None`.

    @param repos: The version control repository
    @param config: The build configuration
    @param db: a database connection (optional)
    """
    env = config.env
    if not db:
        db = env.get_db_cnx()
    node = repos.get_node(config.path)

    for path, rev, chg in node.get_history():

        # Don't follow moves/copies
        if path != repos.normalize_path(config.path):
            break

        # Stay within the limits of the build config
        if config.min_rev and repos.rev_older_than(rev, config.min_rev):
            break
        if config.max_rev and repos.rev_older_than(config.max_rev, rev):
            continue

        # Make sure the repository directory isn't empty at this
        # revision
        old_node = repos.get_node(path, rev)
        is_empty = True
        for entry in old_node.get_entries():
            is_empty = False
            break
        if is_empty:
            continue

        # For every target platform, check whether there's a build
        # of this revision
        for platform in TargetPlatform.select(env, config.name, db=db):
            builds = list(Build.select(env, config.name, rev, platform.id,
                                       db=db))
            if builds:
                build = builds[0]
            else:
                build = None

            yield platform, rev, build


class BuildQueue(object):
    """Enapsulates the build queue of an environment.
    
    A build queue manages the the registration of build slaves, creation and
    removal of snapshot archives, and detection of repository revisions that
    need to be built.
    """

    def __init__(self, env):
        """Create the build queue.
        
        @param env: The Trac environment
        """
        self.env = env
        self.slaves = {} # Sets of slave names keyed by target platform ID

        # Snapshot managers, keyed by build config name
        self.snapshots = {}
        for config in BuildConfig.select(self.env, include_inactive=True):
            self.snapshots[config.name] = SnapshotManager(config)

        self.reset_orphaned_builds()

    # Build scheduling

    def get_next_pending_build(self, available_slaves):
        """Check whether one of the pending builds can be built by one of the
        available build slaves.
        
        If such a build is found, this method returns a `(build, slave)` tuple,
        where `build` is the `Build` object and `slave` is the name of the
        build slave.

        Otherwise, this function will return C{(None, None)}
        """
        log.debug('Checking for pending builds...')

        for build in Build.select(self.env, status=Build.PENDING):

            # Ignore pending builds for deactived build configs
            config = BuildConfig.fetch(self.env, name=build.config)
            if not config.active:
                continue

            # Find a slave for the build platform that is not already building
            # something else
            slaves = self.slaves.get(build.platform, [])
            for idx, slave in enumerate([name for name in slaves if name
                                         in available_slaves]):
                slaves.append(slaves.pop(idx)) # Round robin
                return build, slave

        return None, None

    def populate(self):
        """Add a build for the next change on each build configuration to the
        queue.

        The next change is the latest repository check-in for which there isn't
        a corresponding build on each target platform. Repeatedly calling this
        method will eventually result in the entire change history of the build
        configuration being in the build queue.
        """
        repos = self.env.get_repository()
        try:
            repos.sync()

            db = self.env.get_db_cnx()
            build = None
            builds = []
            for config in BuildConfig.select(self.env, db=db):
                for platform, rev, build in collect_changes(repos, config, db):
                    if build is None:
                        log.info('Enqueuing build of configuration "%s" at '
                                 'revision [%s] on %s', config.name, rev,
                                 platform.name)
                        build = Build(self.env)
                        build.config = config.name
                        build.rev = str(rev)
                        build.rev_time = repos.get_changeset(rev).date
                        build.platform = platform.id
                        builds.append(build)
                        break
            for build in builds:
                build.insert(db=db)
                db.commit()
        finally:
            repos.close()

    def reset_orphaned_builds(self):
        """Reset all in-progress builds to PENDING state.
        
        This is used to cleanup after a crash of the build master process,
        which would leave in-progress builds in the database that aren't
        actually being built because the slaves have disconnected.
        """
        db = self.env.get_db_cnx()
        for build in Build.select(self.env, status=Build.IN_PROGRESS, db=db):
            build.status = Build.PENDING
            build.slave = None
            build.slave_info = {}
            build.started = 0
            for step in list(BuildStep.select(self.env, build=build.id, db=db)):
                step.delete(db=db)
            build.update(db=db)
        db.commit()

    # Slave registry

    def register_slave(self, name, properties):
        """Register a build slave with the queue.
        
        This method tries to match the slave against the configured target
        platforms. Only if it matches at least one platform will the
        registration be successful.
        
        @param name: The name of the slave
        @param properties: A dict containing the properties of the slave
        @return: Whether the registration was successful
        """
        any_match = False
        for config in BuildConfig.select(self.env):
            for platform in TargetPlatform.select(self.env, config=config.name):
                if not platform.id in self.slaves:
                    self.slaves[platform.id] = []
                match = True
                for propname, pattern in ifilter(None, platform.rules):
                    try:
                        propvalue = properties.get(propname)
                        if not propvalue or not re.match(pattern, propvalue):
                            match = False
                            break
                    except re.error:
                        log.error('Invalid platform matching pattern "%s"',
                                  pattern, exc_info=True)
                        match = False
                        break
                if match:
                    log.debug('Slave %s matched target platform "%s"', name,
                              platform.name)
                    self.slaves[platform.id].append(name)
                    any_match = True
        return any_match

    def unregister_slave(self, name):
        """Unregister a build slave.
        
        This method removes the slave from the registry, and also resets any
        in-progress builds by this slave to `PENDING` state.
        
        @param name: The name of the slave
        @return: C{True} if the slave was registered for this build queue,
            C{False} otherwise
        """
        for slaves in self.slaves.values():
            if name in slaves:
                slaves.remove(name)
                return True
        return False
Copyright (C) 2012-2017 Edgewall Software