记录使用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的本来面目:
- http://markmail.org/thread/ne6hdbf4wyhx25cj#query:+page:1+mid:ilx7ahbyzu4jdm64+state:results
- https://github.com/ethereum/ethereum-buildbot
- http://docs.buildbot.net/latest/manual/concepts.html
- http://docs.buildbot.net/latest/manual/cfg-global.html#cfg-codebaseGenerator
终于解决问题后已经心力交瘁,我先给出我最后的解决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还是很好用的,除了测试它还能做很多事情。上面的不少坑我只是解决了问题,或者绕开了。真正的原因我还没有完全分析完,也希望大家多多指教。