记录使用Buildbot遇到的坑

Buildbot Tips

Buildbot也是个大坑。。我并不熟悉python,偏偏文档又少。这几天使用buildbot出了不少坑。有的解决了,有的绕过去,这里都把它们一一记下来。

Force Build

第一个坑就是False Build,正常情况下在Web页面上的builder栏里,会有一个Force Build按钮。点击按钮会强制开始Build,这对于调试Buildbot非常重要。但是我的页面上没有。。。

这个坑还算小,其实是自动生成的master.cfg文件中设置了只有通过认证的才能Force Build,改法也简单,如下:

	authz_cfg=authz.Authz(
    forceBuild = True,
    cancelPendingBuild = False,
	)
	c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))

有关Buildbot的ShellCommand

我做的是一个web server,为了做测试我需要在build结束后启动它,然后用client给它发消息进行测试。server基于nodejs,因此启动命令是npm start
我在slave的build里添加ShellCommand,然后slave会一直停留在这一步不往下走。OK,这个可以理解,server启动后是会一直在等待,我加个&把它后台运行吧,于是加上&,结果不行,还是等待。然后我试了setsid, nohup全部不行。。。
受不了了,我最后跑去看buildbot的源码。原来它的shellcommand都是通过spawn一个新进程的方式来执行的,然后通过回调的形式获取执行结果。参见Twistd SpawnProcess,回调的IProcessProtocol, 接收执行结果的hook描述如下:

def processEnded(reason): (source)
	Called when the child process exits and all file descriptors associated with it have been closed.
	Parameters	reason	A failure giving the reason the child process terminated. The type of exception for this failure is either twisted.internet.error.ProcessDone or twisted.internet.error.ProcessTerminated. (type: twisted.python.failure.Failure )

Anyway,我试了各种办法也没法让buildbot在启动web server后继续往下走。只能考虑绕过去了,有两个饶的思路:

  • 把WebServer做成Service,比如利用Supervisor之类的,这样slave只需要做一个client发出一个启动通知。
  • 我采用的办法是另外写了一个slave,这个slave就负责build和start web server,另一个slave来做测试。

这个坑已经很磨人了。。。但是我决定另外用一个Slave之后遇到了一个更大的坑。。。。

Codebase

我在看Buildbot的文档的时候就看到过好多次Codebase的概念,但是文档里都是一笔带过。当时心里就嘀咕这玩意儿看起来会很麻烦。果然就遇上了!

我用一个Slave从一个Repository去拉下我的Web Server源代码,然后用另一个Slave在另一个Repository拉下测试数据和测试框架代码。谋算着第一个Slave把Web Server启动起来,然后另一个Slave启动测试程序把测试请求一个一个发出去做测试,我很快写完了Buildbot配置文件,然后噩梦来了。
第二个Slave拉代码会出错!

fatal: Could not parse object '9810ad5734d29523739206a28042fae87344c19b'.

啥?从Git上拉下来出错?我完全没搞明白,Google到的结果完全不像是一回事。这个看起来是Git的问题,我把Repository从Github移到Bitbucket也还是一样,我又换了几个Git Repository做实验,都会出错,但是只要两个Slave拉的是同一个仓就OK,搞毛线?

怎么办,只有老老实实的读了一下slave的那个繁琐的log。终于看到一行:

Cloning into '.'...
program finished with exit code 0
elapsedTime=14.558712
git reset --hard 9810ad5734d29523739206a28042fae87344c19b --

这不就是那个没法parse的号么,看起来就是那个reset --hard的问题。两个slave拉同一个仓就ok,拉不同的仓就出错,我推断是Buildbot的代码版本管理出了问题,貌似用来处理多仓库的关键就是Codebase了,好吧,那Codebase是什么?
坑爹的Buildbot对于Codebase基本啥都没写,完全不知道那是个什么东西。
我拼命的Google之后,从以下几个链接中终于推断出来了Codebase的本来面目:

终于解决问题后已经心力交瘁,我先给出我最后的解决code,具体的讲解以后再补上。。

repositories = {
    r'repository url of module1' : 'module1',
    r'repository url of module2' : 'module2',
}

def codebaseGenerator(chdict):
    return repositories[chdict['repository']]

module1_codebases = {
    'module1' : {
        'repository' : 'repository url',
        'branch' : 'master',
        'revision' : None
    }
}

module2_codebases = {
    'module2' : {
        'repository' : 'repository url',
        'branch' : 'master',
        'revision' : None
    }
}

...

c['schedulers'].append(schedulers.SingleBranchScheduler(
                            name="***",
                            change_filter=util.ChangeFilter(branch='master'),
                            treeStableTimer=None,
                            codebases = module1_codebases,
                            builderNames=["builder-***"]))

c['schedulers'].append(schedulers.Triggerable(
                            name="scheduler-***",
                            builderNames=["builder-***"],
                            codebases = module2_codebases))

c['schedulers'].append(schedulers.ForceScheduler(
                            name="force",
                            codebases = ["module1", "module2"],
                            builderNames=["builder-exchange"]))
...

module1Factory.addStep(steps.Git(
        repourl="*****"
        , name="pull  code"
        , description="git codes"
        , descriptionDone="code pulled"
        , mode='full'
        , codebase="module1"
        , method='clobber'))

module2Factory.addStep(steps.Git(
        repourl="****"
        , name="pull  data"
        , description="git pulling test data"
        , descriptionDone="test data pulled"
        , mode='full'
        , codebase='module2'
        , method='clobber'))

Hosts

这也是一个小坑,我本来在Dockerfile里写了

	RUN echo -e "127.0.0.1 dsp" >> /etc/hosts

然而,启动image之后,根本就没有作用。。不过当我用docker exec直接在运行中的Container修改/etc/hosts,又是可以工作的。这是为啥?。。

终于找到了原因:

Docker will generate /etc/hosts dynamically every time you create a new container. So that it can link others. You can use --add-host option:

docker run --add-host www.domain.com:8.8.8.8 ubuntu ping www.domain.com

OMG,每次新Container的/etc/hosts是会自动生成的。。。这跟Docker的Volumn一样,是个大坑啊!因为Dockfile生成Image的机制是每行命令都会用一个新的Container来运行,然后Commit Image,所以我在这一行做的修改再下一行立刻就被新的Container覆盖了。。
解决方案也很简单,使用--add-host就行了。

Supervisor控制Nodejs程序

这个坑其实跟Docker没什么关系,但是因为Docker会经常需要运行Supervisor,所以暂且先放在这里。
我在docker里用supervisor反复跑tests时我启动的nodejs server不稳定,总是突然就挂掉了,一直不明白是什么原因。反复测试之后发现,即使我用Supervisorctl stop去停掉了nodejs server,它也根本没停,端口仍然在被占用,所以反复跑时之后的启动server命令就会出错。
奇怪,原因是什么呢?
我勘察了一下我的supervisor conf文件

[program:exchange]
command=npm start
directory=/data/buildbot/exchange/builder-exchange/exchange
autostart=false

对于Supervisor的具体工作原理我并不了解,不过我猜想是它每次启动一个service后,通过父进程或者记录pid的方式“控制”这个service,需要停止时把它kill掉。那么问题可能就是出在这个npm start上面了。在Express 4里,启动脚本默认为bin/www,那么正常的启动命令应该是node bin/www。因为npm会读取package.json中设置的start项运行方式,调用npm start,也会最终运行node bin/www,但是因为这一个中间层,可能导致supervisor无法准确的记录service了,我估计npm的工作方式是另外spawn一个进程来运行最后对应的命令,这样当我们调用supervisor stop时,它会试着去杀掉npm。。。导致最后的Nodejs程序仍然在运行中。

fix也很简单:

[program:exchange]
command=node bin/www
directory=/data/buildbot/exchange/builder-exchange/exchange
autostart=false

把command改成正确的就可以了。
这个坑好难绕,我还不确定我现在的推断是否正确,不过80%可能是这样吧。

结束语

还会不停的添加新的坑。。肯定的。。Buildbot还是很好用的,除了测试它还能做很多事情。上面的不少坑我只是解决了问题,或者绕开了。真正的原因我还没有完全分析完,也希望大家多多指教。

原文地址:https://www.cnblogs.com/lkiversonlk/p/4885565.html