PHPでcronのように定期実行をするプログラムを書いた
kohkimakimoto/workerphp · GitHub
自分で使う分には最低限、必要な機能を実装できたので紹介します。以下のようなPHPファイルを記述してPHPのコマンドラインから実行するとプロセスが起動しっぱなしになりcront_time
で指定したスケジュールでジョブを定期実行してくれます。
<?php require_once __DIR__.'/vendor/autoload.php'; $worker = new \Kohkimakimoto\Worker\Worker(); // job for every minute. $worker->job("hello", ['cron_time' => '* * * * *', 'command' => function(){ echo "Hello world\n"; }]); // job runs a shell command. $worker->job("uptime", ['cron_time' => '10 * * * *', 'command' => "uptime"]); $worker->start();
ジョブはクロージャでPHPのコードとして書くか、シェルのコマンドを文字列で直接指定することができます。インストールはcomposer install
するだけですが、その他詳しい情報はリポジトリのREADMEを参照してください。内部的にジョブはプロセスをforkして実行させているので、PHPのpcntl
エクステンションがインストールされている必要があります。linuxならyumとかでインストールできると思います。
cronが普通に使える環境ならまあそれを使えばいいんですが、個人の用途でherokuの無料枠dynoを使って定期実行処理をしたかったので、こんなの作りました。
Web API
cronにない拡張機能として、簡易なWeb APIを提供するHTTPサーバを組み込んでいます。
$worker->httpServer->listen();
のように書くと、8080ポートを開きます。ポートを変更したいときは
$worker->httpServer->listen(8888, 'localhost');
のようにもかけます。HTTPサーバが立ち上がったら、
$ curl -XGET http://localhost:8080/{ジョブ名}
でジョブの情報を取得
$ curl -XPOST http://localhost:8080/{ジョブ名}
でジョブを実行することができます。また$worker->httpServer->setAPIKey()
を使えば、文字列一致だけで判定する簡易な認証機能をつけられます。
ちなみに、私は以下の様な設定でherokuのPHPスタック上で動かしています。
<?php require_once __DIR__.'/vendor/autoload.php'; date_default_timezone_set('Asia/Tokyo'); use Kohkimakimoto\Worker\Worker; $worker = new Worker(); // herokuはhttpのportを動的に割り当てるので環境変数から取得する $worker->httpServer->listen(getenv('PORT')); // apiにアクセスするときのキー $worker->httpServer->setAPIKey('vjdioaewdg49q3tg...'); // 5分毎にメモリ使用量などをログに出力する設定 $worker->stats->on(); $worker->job("hoge", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){ // ... }]); $worker->job("foo", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){ // ... }]); // 30分に一回dynoをwebからアクセスしてスリープさせないようにする。 $worker->job("wakeup_dyno", ['cron_time' => '*/30 * * * *', 'max_processes' => 1, 'command' => function(){ file_get_contents("https://my_worker_xxx.herokuapp.com/?apiKey=vjdioaewdg49q3tg..."); }]); $worker->start();
とりあえず、1日動かしっぱなしにしてみたが問題はなさそうです。
UIPageViewControllerをUINavigationControllerにいれたらナビゲーションバー部分に潜り込まなくなった件
iOS8でUIPageViewController
のトランジションスタイルをUIPageViewControllerTransitionStyleScroll
にしたときのみ発生する。。。マジなんなのこれ。。。しかも一度でも画面をタップすると、潜り込む(正常な)位置に移動するという。挙動が奇妙すぎてStack Overflowでも見つからないし。iOS8のバグを疑ってしまう。ヾ(`Д´)ノ"
このサンプルはXcodeのPage-Based Application
が作った雛形のプロジェクトを元に、RootViewController
をNavigationControllerにいれる、のとUIPageViewController
の初期化処理を以下のようにUIPageViewControllerTransitionStyleScroll
を指定するように変えたもの。
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
表示されるViewのframeなどを調べてみてもx=0,y=0となっていて変なところはない。それでViewの階層を見てみたらこんな感じになっている。 なんか下にズレてる。。。
_UIQueuingScrollView
というViewの位置がy=-64.0
という謎な状態になっていて、その子となるViewがその分だけ下にずれている。
しかもこれviewWillAppear:
などのフェーズでは確認できず、viewDidAppear:
まで到達して初めてこの状態になる。
というわけで以下のようなワークアラウンドを入れた。
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)) { for (UIView *v in self.pageViewController.view.subviews){ if ([v isKindOfClass:[UIScrollView class]]) { UIScrollView *scv = (UIScrollView *)v; CGPoint offset = scv.contentOffset; if (offset.y < 0) { offset.y = 0; scv.contentOffset = offset; } } } } }
iOS8の場合、UIPageViewController
のUIScrollView
をとってきてマイナス値なら0に書き換えるということをやっています。これでなんとか対応できた。
追記
コメント欄でアドバイスをいただいて「UIPageViewControllerのAdjust Scroll View Insetsのチェックを外す」で対応できることがわかりました。
Goの開発環境
最近Go言語をいじっているので、現時点での開発環境についてまとめておく。ツールを先に列挙しておくと以下のものを使っている。
エディタ
Sublime Text3を使っている。普段PHPを書くのに使っていて慣れているからであって最適だとは思ってはいない(静的型言語はIDEを使う方がいいとは思っているのだが、ショートカットキーを新たに覚えたりするのがつらいので使い続けている)。
これにGo用のプラグインのGoSublimeをインストールしている。
また関数定義などにジャンプするためにctagsも使っている。ctags自体はhomebrewでインストールして、SublimeにはCtagsプラグインを入れている。標準だとctagsはGoに対応していないのだが
go - ctag database for golang - Stack Overflow
にあるように、~/.ctags
に
--langdef=Go --langmap=Go:.go --regex-Go=/func([ \t]+\([^)]+\))?[ \t]+([a-zA-Z0-9_]+)/\2/d,func/ --regex-Go=/var[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,var/ --regex-Go=/type[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,type/
と書き込んでおけば、関数定義元に(ある程度)ジャンプできるようなる。完璧にとはいかないが、今のところはこれでいいかと。
GOPATHとディレクトリ構造
ghqを使ったローカルリポジトリの統一的・効率的な管理について - delirious thoughts
基本は上記の記事の通り。GOPATH=$HOME
に設定してghqでGo以外のリポジトリも含めて~/src
配下で一括管理する。
ただ、これだとctagsでタグを生成する際に全てのリポジトリを対象にしなくてはならず、タグ生成処理がものすごく重くなってしまい使いものにならなかった。やはり依存パッケージは個々のリポジトリ配下にほしい。
いろいろ試行錯誤した後、次のようにdirenvを使う方法に落ちついた。direnvはカレントディレクトリごとに環境変数を自動で切り替えてくれるツールで、以下の記事が参考になる。
まず
brew install direnv
でdirenv
をインストールし、zshの場合は~/.zshrc
に以下を記述しておく。
export EDITOR=vim eval "$(direnv hook zsh)"
さて、ホームのsrcディレクトリは以下のようになっている。
src ├── github.com │ ├── laravel │ │ ├── framework │ │ └── laravel │ ├── kohkimakimoto │ │ └── altax │ ├── php │ │ └── php-src │ └── piranha │ └── gostatic
ここでGo言語の場合はリポジトリのルートに.envrc
を作成して
export GOPATH=$(pwd)/_vendor/:$GOPATH
と書いておく。これでGo言語のリポジトリに移動した時のみGOPATHがそのリポジトリ配下の_vendor
ディレクトリにも設定される。go get
で外部パッケージをインストールするとき_vendoer
配下にインストールされるようになるし、go build
時もここのパッケージを参照するようになる。ctagsのタグ生成もリポジトリルートで行えば依存のあるパッケージをだけを対象にタグを生成できる。
参考にしたサイトは以下。
ghqによるGoのプロジェクト管理 & プロジェクト毎で外部モジュール管理 - Qiita
これからGoを始める人のためのTips集 #golang - The Wacul Blog
Gomで依存パッケージを一括で_vendorにダウンロード
_vendor
に依存パッケージを配置できるようになったのはいいのだが、依存物をいちいち手作業でgo get
するのは非効率なので一括でダウンロードできるようにGo言語版のBundlerなやつであるGomを使っている。
以下のコマンドを叩くとgo build
を実行してインストールされていないパッケージをエラーメッセージから抽出してGomfile
に吐き出す。
go build 2>&1 | awk '/cannot find package/{print "gom " $5}' >> Gomfile
これでGomfile
ができたら
gom install
として全ての依存パッケージを_vendor
配下にダウンロードする。これでGoのプロジェクトがビルドできる\(^o^)/
xcode6でViewのconstraintに-16がついてiOS7.1でレイアウトがずれる件
xcode6でビューを重ねている状態で、子のビューを親のビューの端にぴったりあわせるるために、オートレイアウトを指定するとなぜかHorizontal Spaceに-16が設定される。しかもこれをiOS8で動かすと綺麗に端がぴったりあって表示されるのに、iOS7.1で動かすとずれて表示されるという状態に。
これがiOS8で
こっちがiOS7.1
青い領域が子のViewでconstraintが指定してある。青いViewの中にあるテキストフィールドがずれてしまっているが判るだろう。ついでに画面上にピッタリつけたはずの部分にも隙間ができてしまっている。。。
そしていつものstackoverflowから解決策を。
Constraintの指定からRelative to margin
のチェックを外す。これが付いているとConstraintの対象が親のVIewの端にならないためなんだそうだ。しかしiOS7と8でUIレンダリング(オートレイアウト周り?)の挙動が違うのは辛いものがあるなあ(T . T)。。。
JenkinsとDockerでTravisっぽいCIサーバを育ててみている
最近プライベートなプロジェクトのCIにはcircleciとかが人気なんでしょうかね。
GitHub 時代のデプロイ戦略 - naoyaのはてなダイアリー
近頃のCIサーバはアプリケーションのテストだけじゃなく、インフラのテストやデプロイ、ChatOpsなどgitやチャットツールなど他のシステムと連携した自動化のための必須プラットフォームといった感じになってきてる。とはいえ、趣味で開発しているプロダクトに余計なコストはかけたくない。ああ、でもやっぱCIはしたい。
そんなわけで以前から契約だけしていて放置気味だった、さくらのVPSの1GにjenkinsをたててオレオレCIを育てているのでその辺のことを書いてみる。CIの実行環境はDockerを使って仮想化し、ジョブの内容はTravisやcircleciのようにリポジトリ側のyamlファイルに記述できるようにしてみた。構成をざっくりと図解すると以下のようになる。
また参考にさせていただいたのは以下の記事。
Docker + Jenkins + travis.yml parser 作って Travis っぽいものを作った話 - from scratch
Use Docker + Jenkins to run GitHub tests
jenkinsでのunitテストは、dockerでクリーン環境を作って行う!!shinofara's Blog (*´ω`*) | shinofara's Blog (*´ω`*)
ベースとなっている環境
CentOS6.5の上に構築している。さくらのVPSのデフォルトがそうなのと、個人的に使い慣れているというのが最大の理由。ただCentOSのDockerはカーネル周りのバグでディスク領域が開放されないことがあるらしいので、今後移行するかもしれない。
Docker on CentOS 6.5 で詰んだのでメモ - sonots:blog
CentOS6系のカーネルに上記のバグフィックスがバックポートされるのも期待している。。。まあ今のところ、仕事でつかっているわけではなく、ディスク領域にも余裕があるのと環境構築はchefでなるだけ自動化しながら作っているので、ディスクが詰まったら最悪、環境を再インストールすればいいかなという判断でやっている。
Dockerのインストール
Dockerのインストール自体について特記することはあまりない。epelリポジトリを使えるようにしておいて以下の様なchefのレシピを書いた。実際にやってることはyum install docker-io
と/etc/init.d/docker start
と同じだ。
docker/recipes/default.rb
package "docker-io" do action :install end service "docker" do action [:enable, :start] end
jenkinsのインストール
CentOS上のjenkinsのインストールについては以前に記事を書いた。今回はそれをちょこちょこ修正した(yumリポジトリからインストールするなど)。詳細は以下にペーストしたレシピの内容を見てもらえばいいが、jenkinsからdockerを実行するために
jenkins
ユーザのuidを明示的(uid:45678
)に指定したりdocker
グループの追加したりしている。Dockerコンテナ内でジョブを実行するユーザとjenkinsの実行ユーザのIDは同じにしておかないといろいろパーミッション周りでハマる。
jenkins/recipes/default.rb
group "jenkins" do gid 45678 action [:create, :manage] end user 'jenkins' do comment 'Jenkins Continuous Build server' uid 45678 group 'jenkins' home '/var/lib/jenkins' shell '/bin/false' password nil action [:create, :manage] end script "install_jenkins_yum_repo" do interpreter "bash" user "root" cwd "/tmp" code <<-EOH wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key EOH not_if "test -e /etc/yum.repos.d/jenkins.repo" end package "jenkins" do action :install end service "jenkins" do action [:enable, :start] end group "docker" do action [:modify] members ["jenkins"] append true end
jenkins cliとプラグインのインストール
jenkinsのプラグインもchefでインストールさせるため以下のようなレシピを書いた。jenkinsは起動までに時間がかかるのでscript[install-jenkins-cli]
でjenkins-cli.jar
をダウンロードする際に10秒待ち、HTTPレスポンス503はリトライする仕組みにしている。
jenkins/attributes/default.rb
default['jenkins']['cli_url'] = "http://127.0.0.1:8080/jnlpJars/jenkins-cli.jar" default['jenkins']['jenkins_url'] = "http://127.0.0.1:8080/" default['jenkins']['wait_for_boot'] = "10" default['jenkins']['plugins'] = [ "git", "cloverphp", "simple-theme-plugin", "jquery", "gravatar", "disk-usage", "envinject", "extra-columns", "categorized-view", "ci-skip", "timestamper", "monitoring", "view-job-filters", "locale", "sidebar-link", "pegdown-formatter", "ansicolor" ]
jenkins/templates/default/jenkins.rb
#!/usr/bin/env bash java -jar /usr/lib/jenkins/jenkins-cli.jar -s <%=node['jenkins']['jenkins_url']%> "$@"
jenkins/recipes/default.rb
template "/usr/local/bin/jenkins" do source "jenkins.erb" owner "root" group "root" mode "0755" end cli_url = node['jenkins']['cli_url'] wait_for_boot = node['jenkins']['wait_for_boot'] script "install-jenkins-cli" do interpreter "bash" user "root" cwd "/tmp" code <<-EOH sleep #{wait_for_boot} http_response_code=503 while [ $http_response_code -eq 503 ] do http_response_code=`curl -LI #{cli_url} -o /dev/null -w '%{http_code}' -s` sleep 5 done wget -t 5 --waitretry 5 -O /usr/lib/jenkins/jenkins-cli.jar #{cli_url} EOH not_if "test -e /usr/lib/jenkins/jenkins-cli.jar" end directory "/var/lib/jenkins/updates" do owner "jenkins" group "jenkins" mode "0755" action :create end # https://issues.jenkins-ci.org/browse/JENKINS-10061 # https://gist.github.com/rowan-m/1026918 script "update-jenkins-updatecenter" do interpreter "bash" user "jenkins" cwd "/tmp" code <<-EOH curl -L http://updates.jenkins-ci.org/update-center.json | sed '1d;$d' > /var/lib/jenkins/updates/default.json EOH not_if "test -e /var/lib/jenkins/updates/default.json" end node['jenkins']['plugins'].each do |plugin_name| execute "install-jenkins-plugin-" + plugin_name do user "root" command "/usr/local/bin/jenkins install-plugin " + plugin_name action :run not_if "/usr/local/bin/jenkins list-plugins | awk '{print $1}' | grep ^#{plugin_name}$" notifies :run, "execute[jenkins-safe-restart]" end end execute "jenkins-safe-restart" do command "/usr/local/bin/jenkins safe-restart" action :nothing end
yamlパーサとコンテナの起動スクリプト
CIのジョブはTravisのようにリポジトリ側のyamlで制御、設定できるようにした。yamlファイルはこんな感じ。
.jenkins.yml
container: image: jenkins-ci-base before_script: - composer install --dev --no-interaction script: - php vendor/bin/phpunit -c phpunit-ci.xml.dist
これをパースしてimage
で指定されたDockerイメージをrunする。この辺の処理は使い慣れたPHPで実装した。ソースは公開していないが、具体的には以下のようなことをやっている。
- リポジトリのルートにある
.jenkins.yml
をパース。 before_script
やscript
で指定された内容からそれぞれbefore_script.sh
やscript.sh
のようなスクリプトファイルを生成してJenkinsのワークスペースに出力する。- 出力したスクリプトファイルを順に起動する
start.sh
をJenkinsのワークスペースに出力する。 image
で指定されたdockerイメージにワークスペースをマウントしてコンテナを起動。具体的にはdocker run -v $WORKSPACE:/home/worker/workspace -w /home/worker/workspace -u worker $IMAGE /bin/bash -l start.sh
のようなコマンドを実行する。- dockerが処理を終えたあと
docker rm
を実行してコンテナを削除する。
この一連の処理を行うPHPスクリプトをjenkinsのジョブ設定の「ビルド」->「シェルの実行」で起動するように設定しておく。
Dockerイメージ
Dockerイメージはあらかじめ作っておくのだが、jenkinsから起動するために以下の決められた仕様で構成している
- uid
45678
のworker
ユーザがいる(ホスト側のjenkinsユーザと同じuid) /home/worker/workspace
ディレクトリがある(ホスト側のjenkinsワークスペースがマウントされる)worker
はパスワードなしでsudoできる
この仕様を満たす基本的なDockerイメージを作成するDockerfileは以下のようになる
FROM centos:centos6 # basic settings RUN rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm RUN rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm RUN yum -y groupinstall "Base" "Development tools" RUN yum -y install --enablerepo=remi,epel \ sudo \ readline \ readline-devel \ compat-readline5 \ libxml2-devel \ libxslt-devel \ libyaml-devel \ git \ make \ autoconf \ automake \ bison \ libtool \ sysstat \ gettext \ traceroute \ openssl \ openssl-devel \ curl \ wget # add worker user RUN useradd -u 45678 -d /home/worker -m -s /bin/bash worker && \ mkdir /home/worker/workspace && \ chown worker:worker /home/worker/workspace && \ echo "worker ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/worker && \ sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers # Enable to run sudo in the script RUN sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers # timezone RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \ rm -f /etc/localtime && \ ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime ################################# # default behavior is to login by worker user ################################# CMD ["su", "-", "worker"]
これで、dockerコンテナ内でworkerというユーザが処理を実行する。あとは必要な環境ごとにカスタマイズしたDockerイメージを用意しておけば、いろんな環境でCIができる。ちょうどTravisがPHPやRubyといった言語ごとのテスト環境を用意してくれるように、例えばPHPをインストールしたjenkins-ci-php
やRubyをインストールしたjenkins-ci-ruby
というDockerイメージを作っておいて、
container: image: jenkins-ci-php
や
container: image: jenkins-ci-ruby
などと.jenkins.yml
で指定すればいい。
まとめ
ここ数日運用してみての感想だが、これはかなりいい感じ。現在の自分の用途の範囲では、ほぼTravisでやれることがプライベートでも実現できている。 CIの実行環境はDockerによって独立し常に使い捨てにされるので、サーバ設定を丸ごと書き換えるようなプロビジョニングのテストなどにも使えると思う。
あとjenkinsのテーマをアトラシアン風にする djonsson/jenkins-atlassian-theme · GitHub があるので、これをちょっとカスタマイズして、UIの見た目も変えてみた。
やっぱし、コンソール表示は黒背景がいい!
UITableViewのPlainスタイルで空のセルを表示させないようにする
iOSアプリを作っていて普通にUITableViewを使っていると、表示するデータをもつセルが1,2行しかないとき、空のセルが画面の残りの部分を埋めてしまいます。
この空のセルを表示させたくないときは、以下のようなコードを書けばよいです。
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; }
このコードによってUITableViewが(高さ0の)フッターを表示させようとするため、明示的に指定したセル以外は表示させないようになるそうです。
参考にした記事(いつもお世話になっておりますstackoverflow)
ios - How to remove empty cells in UITableView? - Stack Overflow
PHP5.5+OPcacheでシンボリックリンクでデプロイするとキャッシュが消えない
という問題にぶち当たりました。 どういうことかを話す前にPHPアプリケーションの個人的なデプロイ構成について説明します。 要はCapistranoのデプロイと同様なのですが、デプロイ先は以下のようなディレクトリ構成になっています。
. |-- current -> /path/to/releases/20141011000001 |-- releases | |-- 20141011000000 | `-- 20141011000001
current
がアプリケーションサーバ(apacheやphp-fpm)から参照される箇所で、releases
配下の最新のアプリケーションへのシンボリックリンクになっている。新しくアプリをデプロイするとreleases
配下に20141011000002
のようなタイムスタンプで新しくディレクトリが作成され、そこにアプリケーションのコードが配置される。その後current
のシンボリックリンクが新しいアプリケーションへと切り替えられる。これによってダウンタイムなしでPHPアプリのコードをデプロイできます。
OPcacheのキャッシュ
ところが、PHP5.5+php-fpm+OPcacheという環境でこの構成のデプロイをためしたところ、実行中のアプリケーションにデプロイコードが反映されないという問題が生じました。 (※OPcacheはPHP5.5の新しいコードキャッシュシステムでPHP5.4以前のAPCに替わるものです)
調査していたら以下の解説記事を発見しました。
OpCache and symlink-based deployments
要約すると
- OPcacheはAPCとはファイルのキャッシュの仕方が違う。
- OPcacheはシンボリックを解決して、実ファイルパスの状態でキャッシュする。
- よってシンボリックリンクを更新しても、実ファイルパスのキャッシュが保持されてしまう。
というわけだそうです。(このへんをちゃんとPHPのソース読んで、自分で裏付けとれるといいと思うのですが。。。技術力の低さが露呈しますね。。。(ー_ー;))
上記の解説記事では対応方法もいくつか提示されていて
- webサーバの設定を直接書き換える(シンボリックリンクの代わりにweb(AP)サーバが参照するパスを直接書き換える)
- OPcacheをリセットする
- php-fpmを再起動する(多少のダウンタイムあり)
ansibleやpuppetを使っていればデプロイでwebサーバの設定を書き換えるのは簡単かも、と言及されています。
ただ、シンボリックリンクを使い続けたいならキャッシュをリセットするか、php-fpmを再起動する。
OPcacheはキャッシュをリセットする関数opcache_reset
をもっているので、
これを使用した対応方法も(Laravelを使った例で)示されています。
さて、自分はどーしようか悩み中。 デプロイタスクがちょっとだけど複雑になるのと、コードキャッシュはアプリケーションより一段下の技術レイヤと考えているので、 それのリセット処理をアプリケーションのコードに直接入れるような対応はやりたくない。
この際、最近盛り上がってきているdockerでBlue-Green Deployment的なことをやってしまうのもアリかなと考えています。