65.9K
CodeProject 正在变化。 阅读更多。
Home

为 CruiseControl.NET 项目提供轻量级同步信号量

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.11/5 (3投票s)

2006 年 8 月 4 日

6分钟阅读

viewsIcon

41236

downloadIcon

102

使用现有的 NAnt 任务在 CruiseControl.NET 中创建互斥项目。

Sample Image - ccnetsemaphore.jpg

引言

本文介绍了一种在 CruiseControl.NET 中创建互斥项目的简单方法。我们将通过在 XML 文件中实现一个轻量级信号量,利用*已有的*任务集来实现这一点,而不是扩展 NAnt 添加新任务。

背景

CruiseControl.NET 是 Microsoft .NET 世界中一个流行的持续集成服务器。该服务器可以托管多个构建项目,这些项目通过 NAnt 或 MSBuild 脚本实现。这些脚本负责编译代码、运行单元测试、验收测试、代码覆盖率测试、部署应用程序等。按设计,这些构建项目旨在并行运行。当服务启动时,或者检测到中心配置文件发生更改时,每个构建项目类型都会分配到一个不同的线程。这有利于安全性和性能,但使得这些项目运行实例之间的直接通信几乎不可能。然而,有时项目之间存在依赖关系。有时,我们希望在另一个项目运行时阻止某个项目运行,例如,在另一个构建项目编译解决方案程序集时,我们不希望运行单元测试。有时,我们确实希望项目之间进行通信……

轻量级信号量

构建项目无法直接通信,但我们可以让它们通过一个中心文件进行信息交换,该文件用作信号量。一个小型的 XML 文件将用于获取、释放和验证独占锁。上面的类图是在 Visual Studio 2005 中绘制的。它描述了信号量如何在真正的面向对象环境中实现。我使用类来表示 CruiseControl.NET 构建项目,使用抽象类来表示项目中相似行为的组(这将作为构建项目中的 `<include>` 任务实现),并使用一个结构来表示中心 XML 文件。

让我们先看看由 `LockInfo` 结构表示的 XML 文件内容。当没有项目运行时,文件如下所示:

    <semaphore>
        <locked>false</locked>
        <project />
        <label />
    </semaphore>

为了在服务器上获得独占锁,已启动的构建项目必须将其标识放入该文件中:项目的名称和当前的构建标签。因此,当构建*30*的*编译*项目持有锁时,文件内容如下,只要文件是这样,其他任何项目都无法获得锁。

    <semaphore>
        <locked>true</locked>
        <project>Compilation</project>
        <label>30</label>
    </semaphore>

所有对该文件的访问都由一组 NAnt 目标控制,这些目标被分组到一个 `semaphore.build` 文件中。在类图中,这由 `Semaphore` 类表示。此构建文件包含必要变量(XML 文件路径、锁状态以及锁拥有者的项目名称和构建标签)的声明,以及访问文件内容的*方法*。使用 `<xmlpeek>` 和 `<xmlpoke>` NAnt 任务分别读取和写入此文件。以下是用于将 XML 内容与相应 NAnt 属性同步的代码:

    <target name="Semaphore.SetLockInfo">
        <!-- Write XML file -->
        <xmlpoke file="${semaphore.xml}"
                 value="${semaphore.locked}"
                 xpath="/semaphore/locked" />
        <xmlpoke file="${semaphore.xml}" 
                 value="${semaphore.project}"
                 xpath="/semaphore/project" />
        <xmlpoke file="${semaphore.xml}" 
                 value="${semaphore.label}" 
                 xpath="/semaphore/label" />
    </target>

    <target name="Semaphore.GetLockInfo">
        <!-- Read XML file and fill properties -->
        <xmlpeek file="${semaphore.xml}" 
                 xpath="/semaphore/locked" 
                 property="semaphore.locked" />
        <xmlpeek file="${semaphore.xml}" 
                 xpath="/semaphore/project" 
                 property="semaphore.project" />
        <xmlpeek file="${semaphore.xml}" 
                 xpath="/semaphore/label" 
                 property="semaphore.label" />
    </target>

使用信号量

构建服务器上定义的完整构建脚本集分为三类:

  1. 第一类包含需要在开始前获取独占锁的项目。这些项目包括编译、部署或每日备份。此类由 `LockMaster` 抽象类表示。
  2. 另一类包含不需要独占锁的构建项目,但如果另一个项目持有独占锁,则它们不会启动,或者会立即失败。此类由 `LockSlave` 抽象类表示,包含单元测试和覆盖率测试等项目。这些是您不希望在 `LockMaster` 忙碌时运行的项目。
  3. 第三类包含完全忽略 `Semaphore` 的 `LockNeutral` 项目。此类未在类图中表示,但您可以在附件的源代码中找到模板。

获取独占锁

当触发 `LockMaster` 构建时,它将尝试从 `Semaphore` 获取独占锁。如果此尝试不成功,则整个构建项目将立即因 `<fail>` 任务而失败。以下是相应的代码:

    <target name="Semaphore.Lock">
        <!-- Message -->
        <echo message="Project '${CCNetProject}' 
            (Build ${CCNetLabel}) requested an exclusive lock." />
        <!-- Check Lock -->
        <call target="Semaphore.GetLockInfo" />
        <fail message="Semaphore was locked by build ${semaphore.label}
        of project ${semaphore.project}." if="${semaphore.locked}" />
        <!-- Apply Lock --> 
        <property name="semaphore.locked" value="true" />
        <property name="semaphore.project" value="${CCNetProject}" />
        <property name="semaphore.label" value="${CCNetLabel}" />
        <call target="Semaphore.SetLockInfo" />
    </target>

释放独占锁

当 `LockMaster` 构建完成时,它必须释放其锁。整个构建尚未成功:它仍可能在以下三种情况之一中失败:

  • 不再有锁,或者
  • 锁被另一个项目持有,或者
  • 锁被同一项目的另一个构建持有。
如果您的所有脚本都具有适当的错误处理,并且没有其他进程可以访问中心 XML 文件,那么这种失败将永远不会发生。无论如何,这是相应的 `Semaphore` 目标:
    <target name="Semaphore.UnLock">
        <!-- Message -->
        <echo message="Build ${CCNetLabel} 
           of Project '${CCNetProject}' required to release its lock." />
        <!-- Check Lock -->
        <call target="Semaphore.GetLockInfo" />
        <fail message="The output of this build may be corrupt: 
                  the lock was already released."
              unless="${semaphore.locked}" />
        <fail message="The output of this build may be corrupt: 
                the lock was held by another project (${semaphore.project})."
              unless="${CCNetProject==semaphore.project}" />
        <fail message="The output of this build may be corrupt: 
                  the lock was held by another build (${semaphore.label})."
              unless="${CCNetLabel==semaphore.label}" />
        <!-- Apply UnLock --> 
        <property name="semaphore.locked" value="false" />
        <property name="semaphore.project" value="" />
        <property name="semaphore.label" value="" />
        <call target="Semaphore.SetLockInfo" />
    </target>

验证独占锁

当触发 `LockSlave` 类别的项目构建时,它应首先验证是否有其他项目持有独占锁,并在这种情况下立即失败。相应的 `Semaphore` 目标如下所示:

    <target name="Semaphore.FailIfLocked">
        <call target="Semaphore.GetLockInfo" />
        <fail message="Project not run: project ${semaphore.project} 
                 (build ${semaphore.label}) held an exclusive lock."
              if="${semaphore.locked}" />
    </target>

LockMaster 和 LockSlave 行为

LockMaster 行为

`LockMaster` 项目需要在其工作完成(无论成功与否)后释放其锁。这意味着我们必须实现一种 `try-catch-finally` 结构,其中锁在 `finally` 块中释放。内置的 `nant.onfailure` 属性在此非常有用:其值指向项目失败时需要运行的目标。不幸的是,此目标必须是运行项目的一部分,因此我们不能直接指向 `Semaphore` 中的目标;我们需要将此调用嵌入到本地错误处理程序中。以下项目结构是您可用于 `LockMaster` 项目的模板。我将其设计为*编译*构建,以便不那么抽象:

    <project default="Compilation" name="Compilation" 
            xmlns="http://nant.sf.net/release/0.85-rc3/nant.xsd">
        <include buildfile="semaphore.build" />
        <target name="Compilation">
            <!-- Error Handling -->
            <property name="nant.onfailure" value="Compilation.Fail" />
            <!-- Obtain exclusive lock on CruiseControl.NET -->
            <call target="Semaphore.Lock" />
            <!-- Start Compilation -->
            <call target="Compilation.Core" />
            <!-- Release lock -->
            <call target="Semaphore.UnLock" />
        </target>
        <target name="Compilation.Core">
            <!-- Standard Compilation, e.g. get sources from 
                Subversion and launch MSBuild -->
            <!-- ... -->
        </target>
        <target name="Compilation.Fail">
            <!-- Execute project specific error handling, 
               e.g., cleaning up working directories. -->
            <!-- ... -->
            <!-- Release lock -->
            <call target="Semaphore.UnLock" />
        </target>
    </project>

LockMaster 行为

`LockSlave` 构建项目的结构更简单:基本上是相同的 `try-catch` 结构,但没有 `finally`。这是模式;它看起来像一个*单元测试*构建,以便不那么抽象:

    <project default="Testing" name="Testing" 
            xmlns="http://nant.sf.net/release/0.85-rc3/nant.xsd">
        <include buildfile="semaphore.build" />
        <target name="Testing">
            <!-- Error Handling -->
            <property name="nant.onfailure" value="Testing.Fail" />
            <!-- Check Lock -->
            <call target="Semaphore.FailIfLocked" />
            <!-- Start Unit Tests -->
            <call target="Testing.Core" />
        </target> 
        <target name="Testing.Core">
            <!-- Run Unit Tests -->
            <!-- ... -->
        </target> 
        <target name="Testing.Fail" >
            <!-- Execute project specific error handling, 
                    e.g., cleaning up working directories. -->
            <!-- ... -->
        </target>
    </project>

安全网

提出的解决方案轻量级且并非 100% 防错。但在大多数构建服务器环境中已经足够:您始终可以重新运行失败的构建项目来修复情况。尽管如此,作为安全网,我建议实现一个目标以强制无条件重置锁,并在 `ccnet.config` 文件中注册此目标。这样,如果您的 `MasterLock` 脚本之一出现异常,您始终可以从 CruiseControl.NET Web 仪表板或通过 CCTray 应用程序手动重置锁。目标如下所示:

    <target name="Semaphore.ForceUnLock">
        <!-- Message -->
        <echo message="Build ${CCNetLabel} of Project 
               '${CCNetProject}' forced an unlock." level="Warning" />
        <!-- Apply UnLock --> 
        <property name="semaphore.locked" value="false" />
        <property name="semaphore.project" value="" />
        <property name="semaphore.label" value="" />
        <call target="Semaphore.SetLockInfo" />
    </target>

请记住:此目标绝不应从“常规”构建脚本调用!

历史

这是文章的 1.1 版本(轻微的语言更改)。

© . All rights reserved.