かまたま日記3

プログラミングメイン、たまに日常

embulk-filter-hash v0.4.0 リリース

約1年ぶりのリリース、Embulk v0.9系だと動かなかったようで、対応しました。 あとKotlinも最新版に上げました。

CircleCI上のRSpecのテスト時間をparallel_split_testを使って短縮する

CircleCIを使ってテストを実行する場合、テストはcircleci tests splitコマンドで分割した上で複数コンテナを使うことでテストの実行時間を短縮することができます(参考リンク)。ただ、コンテナ数を増やすのも限界がありますし*1、少し前までtiming-based splittingがCircleCI Workflowでは有効にならないという問題もあり、コンテナごとのテストケース数や実行時間に偏りがあって、結局一番遅いコンテナに引きづられてスループットが思ったより上がらないという問題がありました。*2

そこで、コンテナ上でさらにテスト(RSpec)を並列実行することで、コンテナごとの実行時間を短縮し、それによってコンテナごとのテストのスループットを上げるというのを試みてみました。

Gem探し

RSpecを並列実行するGemは何個かあって、rrrspec, parallel_tests, rspec-parallelなどがありますが、一長一短があり、どれもしっくり来ない感じでした。悩みポイントとしては、こちらのqiitaの記事で書かれていることがかなり近いです。

qiita.com

こちらの記事のyuku_tさんがが作られたparallel-rspecも試してみたのですが、まだProduction Readyでないようで通常のRSpecの挙動と違うところがあり、自分のケースでは使えませんでした。

それで色々他を探した結果 parallel_split_test というGemに行き着きました。こちらは最小限のハックでRSpecを拡張していて、なおかつファイルベースではなくspecベースでテストを分散できるという、まさに自分の求めるものでした。*3

使ってみる on CircleCI

使い方は至ってシンプルで、 rspec コマンドを parallel_split_test に変えるだけです、rspecで使っているオプションは全部使える(はず)です。 CircleCI的な実行の仕方としてはこんな感じになると思います。デフォルトのmediumコンテナは2CPUなので並列実行数(PARALLEL_SPLIT_TEST_PROCESSES)は2を指定しています。

export PARALLEL_SPLIT_TEST_PROCESSES=2
bundle exec parallel_split_test --profile 10 \
  --format RspecJunitFormatter \
  --out /tmp/test-results/rspec.xml \
  --no-merge \
  --format progress \
  $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

--no-merge オプション

このオプションはparallel_split_test独自のオプションです。これを付けない場合、 アウトプットで指定したファイル*4に各プロセスの実行結果がすべてマージされます。 RspecJunitFormatter の場合各プロセスの実行結果それぞれが個別のxmlファイルになって、マージされるとxmlのフォーマットではなくなってしまうので、--no-mergeオプションをつけることで別々のファイルに書き出します。ファイル名は、もともと指定したものにプロセス番号が追加されます*5。CircleCIは複数の結果ファイルをよしなにマージして集計してくれるので、この方法で全部のテスト結果を集計できます。

パッチ当てた

また、使ってる中で何個かはまったポイントはPRだして修正したりして、自分の使っているケースでは今はちゃんと動いています。 https://github.com/grosser/parallel_split_test/pull/13 https://github.com/grosser/parallel_split_test/pull/14 https://github.com/grosser/parallel_split_test/pull/15

よかったら、使ってみて下さい!

*1:お金が無限にあればその限りではない

*2:time-based効かない問題は2018/01ごろに解決しています、参考リンク

*3:ちょっとgemの名前がジェネラル過ぎるんじゃないかとか、このGem自体のテストが難しいというのもあったりしますが...w

*4:ここでは /tmp/test-results/rspec.xml

*5:この例の場合 /tmp/test-results/rspec.1.xml のようになる

envsubstで簡易テンプレートを実現する

設定ファイルとかで一部だけ可変にしたテンプレートを使いたいんだけど、RubyとかPerlみたいな別言語を使うまでも無くシェルスクリプトだけで完結したい場合、envsubstコマンドを使うとテンプレートに環境変数を埋め込むことが出来ます。

インストール (on Ubuntu)

% docker run -it --rm ubuntu:16.04 

root@a70695314424:/# apt-get update
root@a70695314424:/# apt-get install gettext-base -y
root@a70695314424:/# envsubst --version
envsubst (GNU gettext-runtime) 0.19.7
Copyright (C) 2003-2007 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Bruno Haible.

使い方

root@a70695314424:/# envsubst --help
Usage: envsubst [OPTION] [SHELL-FORMAT]

Substitutes the values of environment variables.

Operation mode:
  -v, --variables             output the variables occurring in SHELL-FORMAT

Informative output:
  -h, --help                  display this help and exit
  -V, --version               output version information and exit

In normal operation mode, standard input is copied to standard output,
with references to environment variables of the form $VARIABLE or ${VARIABLE}
being replaced with the corresponding values.  If a SHELL-FORMAT is given,
only those environment variables that are referenced in SHELL-FORMAT are
substituted; otherwise all environment variables references occurring in
standard input are substituted.

When --variables is used, standard input is ignored, and the output consists
of the environment variables that are referenced in SHELL-FORMAT, one per line.

Report bugs to <bug-gnu-gettext@gnu.org>.

標準入力が標準出力にアウトプットされます。標準入力中の $VARIABLE もしくは ${VARIABLE} が実際の環境変数に置換されます。

root@a70695314424:/# echo 'pwd=${PWD}' > sample.tpl
root@a70695314424:/# envsubst < sample.tpl
pwd=/

基本的にはファイルに書き込んで使います

root@a70695314424:/# envsubst < sample.tpl > sample.txt
root@a70695314424:/# cat sample.txt
pwd=/

SHELL-FORMAT が指定されてる場合、そこに書かれている変数だけが使われます、デフォルトは全環境変数が利用可能

root@a70695314424:/# echo 'pwd=${PWD} home=${HOME}' > sample.tpl
root@a70695314424:/# envsubst '$HOME' < sample.tpl
pwd=${PWD} home=/root

--variables が指定されてる場合、標準入力は無視されて SHELL-FORMAT で指定されている変数が出力されます (値ではない)。

root@a70695314424:/# envsubst --variables '$HOME $PWD FOO'
HOME
PWD

クラスインスタンス変数はそのクラスからしか参照できない

RubyのクラスはClassクラスのオブジェクトなので、インスタンス変数を持てます。クラスレベルとかクラスメソッドの中で定義された@付きの変数はクラスインスタンス変数(Class Instance Variable)と呼ばれ、そのクラスからしか参照できません、継承されてる場合、継承先でも参照できません。仕事中に継承しているクラスでBaseの値を参照しようとしてちょっとハマった次第であります。

バージョンは2.5.0です。

class Base
  def self.get
    @keys
  end

  def self.add(key)
    @keys ||= []
    @keys << key
  end

  add("Base")
end

class A1 < Base
  add("A1-1")
  add("A1-2")
end

class A2 < A1
  add("A2")
end

class B1 < Base
  add("B1")
end

class B2 < B1
  add("B2")
end

puts "Base=#{Base.get}"
puts "A1=#{A1.get}"
puts "A2=#{A2.get}"
puts "B1=#{B1.get}"
puts "B2=#{B2.get}"

出力はこんな感じになります。

Base=["Base"]
A1=["A1-1", "A1-2"]
A2=["A2"]
B1=["B1"]
B2=["B2"]

なので、継承ツリー上にある全部の @keys の値が欲しい場合、全部で get をしてやる必要があります。

p A2.ancestors.select{|k| k <= Base}.inject([]){|sum, k| sum + k.get}
=> ["A2", "A1-1", "A1-2", "Base"]

JenkinsでJobを起動して、終了まで待つシェルスクリプト

jqとcurlがインストールされている必要があります。Jenkinsのバージョンは 2.46.3 で認証を有効にしています。

#!/bin/bash
#
# Trigger a Jenkins build and wait for the build to finish.
#
set -eu

JENKINS_HOST="jenkins.kamatama41.com"
JENKINS_USER=kamatama41
JENKINS_TOKEN=xxxxxxxxxx
JENKINS_JOB="a-jenkins-job"
BODY=$(cat << EOS
{"parameter": [
  {"name":"FOO", "value":"BAR"}
]}
EOS
)

get_build_status() {
  local build_number=$1
  curl -sS --fail \
    --user ${JENKINS_USER}:${JENKINS_TOKEN} \
    https://${JENKINS_HOST}/job/${JENKINS_JOB}/${build_number}/api/json
}

echo "Start to build ${JENKINS_JOB} with ${BODY}"

last_build_id=$(get_build_status lastBuild | jq -r .id)
echo "Last build_id is #${last_build_id}"

# Trigger the Job
curl -sS --fail -X POST \
  --user ${JENKINS_USER}:${JENKINS_TOKEN} \
  --data-urlencode json="${BODY}" \
  https://${JENKINS_HOST}/job/${JENKINS_JOB}/build

# Wait for new build to start
build_id=$(get_build_status lastBuild | jq -r .id)
wait_seconds=3
while [ "${build_id}" == "${last_build_id}" ]; do
  echo "New build is not running yet, wait for ${wait_seconds} seconds..."
  sleep ${wait_seconds}
  build_id=$(get_build_status lastBuild | jq -r .id)
done

echo "A new build has been started."
echo "https://${JENKINS_HOST}/job/${JENKINS_JOB}/${build_id}/"

# Wait for the build to end
result=$(get_build_status ${build_id} | jq -r .result)
wait_seconds=30
while [ "${result}" == "null" ]; do
  echo "Build is not finished yet, wait for ${wait_seconds} seconds..."
  sleep ${wait_seconds}
  result=$(get_build_status ${build_id} | jq -r .result)
done

if [ "${result}" != "SUCCESS" ]; then
  echo "Not succeeded, result is ${result}."
  exit 1
else
  echo "Succeeded!"
fi

解説

  • /job/${JOB_NAME}/${BUILD_ID}/api/json でビルドのステータスをJSONで取得出来ます。 lastBuild をIDに指定すると、最後に実施された(or 実施中の)ビルド状態を取得できます
  • /job/${JOB_NAME}/build にPOSTすると、新しいビルドをトリガー出来ます、JSONでビルドパラメタを渡すことも可能です。 トリガーされたビルドは開始されるまで数秒のタイムラグがあるので、最初にlastBuildのidを取得しておき、ポーリングして番号が変わったらそれが今回起動されたビルドだと判断しています。*1
  • その後はビルドステータスをポーリングし、resultがnullで無くなったらビルド完了と判断します。

*1:並列にバンバンビルドが起動されている環境だとこのやり方は誤検知の可能性があります。厳密にチェックする方法も無くはないのですが、面倒なので今回のケースではこういう方法を選びました。

xargsとddcpで高速にファイルをコピーする

総量350GBくらいの大量にファイルが入ってるディレクトリをコピーしないといけなくなったので、xargsとddcpを使って頑張ってコピーした時の記録

  • もっと簡単に出来る
  • もっといい方法がある

ご指摘お待ちしております!

#!/bin/bash

SRC_PATH=/foo
DST_PATH=/bar
PARALLELS=$(nproc)

find "${SRC_PATH}" -mindepth 1 -type d -printf "/%P\n" | xargs -P${PARALLELS} -n1 -I {} mkdir -p ${DST_PATH}{}
find "${SRC_PATH}" -type f -printf "/%P\n" | xargs -P${PARALLELS} -n1 -I {} /usr/local/bin/ddcp -s ${SRC_PATH}{} -d ${DST_PATH}{}

解説

  • ddcpはコピー先のディレクトリが無いとエラーになるので、最初のfindコマンドでSRC配下のディレクトリを見つけて先に作ります。
  • xargsの -P オプションでプロセッサの数だけ並列実行します -n1 のオプションで渡ってきた入力を一個ずつ後続のコマンドに渡します、 -I {} オプションで渡ってきた引数を {} で再利用することが出来ます。
  • ddcpは並列処理でcpを実行するので、ddcp自体を並列にする必要は無かったかも.. (要ベンチマーク)

加地さん引退

発表されてからだいぶ経ってますが*1ガンバ大阪ジーコジャパンなどで活躍した加地選手が引退しました。

www.fagiano-okayama.com

引退会見も"500試合にこだわらない"、"半分以上楽しくなかった"など28歳で代表引退した加地さんらしい引き方かなあと思いました。

加地さんとの思い出

2005年くらいの代表戦でみたのが最初だったと思うのですが、ジーコジャパンでもお荷物扱いで2chでも"ペットボトラー"とか言われてたのが、コンフェデくらいからブラジル戦での幻のゴールもあったり、代表でも不可欠な存在になっていったのがハマるきっかけだったと思います。

このブログでも2006のワールドカップくらいまではかなり言及がありますねw シュバインシュタイガーに削られたときは本気で落ち込みましたよ...

加地 の検索結果 - かまたま日記3

2chの加地スレ影響も大きかったと思います。本当の加地さんの人となりは正直わからないのですがw 加地スレで作られたキャラはすごく良かったです。

  • "ロベカルじゃなくてごめんな" 発言
  • 将来は保育士になりたい
  • ペットボトラー, ロベカジ, キングカジなどの愛称
  • 数年に一回のスーパーゴール
  • 謎のアメリカ挑戦
  • などなど

とにかく、20年間お疲れ様でした。まだ38歳なので第二の人生楽しんで下さい!

*1:今日気づいた