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






4.11/5 (3投票s)
2006 年 8 月 4 日
6分钟阅读

41236

102
使用现有的 NAnt 任务在 CruiseControl.NET 中创建互斥项目。
引言
本文介绍了一种在 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>
使用信号量
构建服务器上定义的完整构建脚本集分为三类:
- 第一类包含需要在开始前获取独占锁的项目。这些项目包括编译、部署或每日备份。此类由 `LockMaster` 抽象类表示。
- 另一类包含不需要独占锁的构建项目,但如果另一个项目持有独占锁,则它们不会启动,或者会立即失败。此类由 `LockSlave` 抽象类表示,包含单元测试和覆盖率测试等项目。这些是您不希望在 `LockMaster` 忙碌时运行的项目。
- 第三类包含完全忽略 `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` 构建完成时,它必须释放其锁。整个构建尚未成功:它仍可能在以下三种情况之一中失败:
- 不再有锁,或者
- 锁被另一个项目持有,或者
- 锁被同一项目的另一个构建持有。
<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 版本(轻微的语言更改)。