OSS Gateは「OSS開発に参加する人を増やす」取り組みで、それを実現するための1つの手段として「OSS Gateワークショップ」を開催しています。これまでは東京でだけ開催していましたが、OSS Gateは「『東京の』OSS開発に参加する人を増やす」取り組みではないので、東京以外でも開催したいという考えがありました。
それが実現したのがOSS Gateワークショップin札幌2016-09-24です。札幌開催は札幌在住の@tricknotesを中心に準備しました。東京以外でもだれか中心になる人がいると開催できることがわかったので、東京以外の自分たちの地域でもOSS Gateワークショップを開催したい!という人はOSS Gateのチャットルームに来てください。相談しましょう!
今回はインフィニットループさんに会場を提供してもらって開催しました。1回目の開催で8名のビギナー(OSS開発未経験者)が参加しました。これは東京での1回目の開催時(4名)より多いです。札幌でも「OSS開発に参加する人を増やす」取り組みは必要とされていそうです。
参加した人がブログを書いています。
参加者のアンケート結果もあるので、内容に興味がある人は参考にしてください。
なお、同日に東京でもOSS Gateワークショップを開催していました。東京の内容に興味のある人は以下を参照してください。
この記事では内容ではなく運営方面視点のことをまとめます。(東京・札幌以外の)自分たちの地域でもこのワークショップを開催したいという人の参考になるはずです。
今回は1回目なので東京から以下の3人が応援に行きました。
応援に行った目的は次の通りです。札幌以外でも新しい地域で開催するときは応援に行く予定です。
「負荷を下げる」と「イレギュラーな対応を巻き取る」を少し補足します。
東京のワークショップではメンター1人でビギナー2人をサポートするという体制をとっています。札幌ではメンター1人でビギナー1人をサポートする体制としました。まずは、ビギナーをサポートするとはどういうことかを経験することに集中できるようにするためです。これを実現するために東京から応援に行ったメンターでビギナー3人をサポートしました。これが「負荷を下げる」ということです。
OSS Gateワークショップではビギナーとして想定している範囲はかなり広めですが、それでもたまに想定の範囲外のビギナーも参加します。その場合は個別の対応が必要になります。今回の場合は、普段開発はしていないしGitの使い方も不安というビギナーが参加していましたが、このケースは想定の範囲外です。(想定の範囲外ですが、ワークショップに来る気持ちがある人ならいい感じにサポートしたいので、そんな人でも遠慮せずに飛び込んできてください。)札幌の人たちがこのケースのサポートをすると想定の範囲内のサポートを経験する機会がなくなるので、東京から応援に行ったメンターで対応しました。他にもメンターくらいの経験の人がビギナーとして参加していましたが、これも想定の範囲外のケースでした。やはり、こちらも東京から行ったメンターで対応しました。これが「イレギュラーな対応を巻き取る」ということです。
東京以外の自分たちの地域でもOSS Gateワークショップを開催したい!という人はOSS Gateのチャットルームに来てください。相談しましょう!前述の通り最初のワークショップをサポートしに行きますし、OSS Gate ワークショップ in 札幌 キックオフ - 2016-07-16というようにワークショップ開催前のキックオフを開催して仲間集めをするところのサポートもできます。どういう風に進めていけばよいか一緒に考えましょう。
全文検索エンジンGroongaはmrubyを組み込んでいます。理由は、速度はそれほど必要ではなく込み入った処理をCではなくRubyで書けると開発が捗るからです。
この記事ではどのようにmrubyを組み込んでいるかについてビルド周りだけを説明します。(ビルド周り以外には、バインディングをどうやって書くか、.rb
ファイルをどこに置くか、実装をCにするかRubyにするかの判断基準などの話があります。)
mrubyはRakeを使ったビルドシステムを使っています。GroongaはGNU AutotoolsまたはCMakeを使ったビルドシステムを使っています。(どちらでもビルドできます。)
mrubyはRakeを使ってビルド、GroongaはGNU Autotoolsを使ってビルド、とするとコンパイルオプションの統一・クロスコンパイルあたりで面倒になります。また、Rakeを使ってビルドするためにはビルド時にRubyが必要になるのも面倒です。Groongaの開発者がGroongaをビルドするためにRubyが必要になるのはよいですが、GroongaのユーザーがGroongaをビルドするためにRubyが必要になるのはビルドの敷居が上がるのでできれば避けたいです。
以上の理由からGroongaではmrubyをビルドするためにRakeを使っていません。
ではどうしているかというと、次のようにしています。
それぞれどのようにしているか説明します。
Groongaはvendor/mruby-source
をGitのsubmoduleにしています。つまり、mrubyのリポジトリーのソースをまるっと参照しています。
この状態ではmrubyのビルドに必要なファイルは足りません。具体的にはRubyで実装されたコードをmrubyに組み込むファイル(mrblib.c
。mrbc -B
で生成するファイル。)や利用するmrbgemsを組み込むファイル(mrbgems_init.c
)が足りません。
これらを生成するためにRakeを使ってビルドします。出力先はvendor/mruby-build/
にしています。ビルドした後vendor/mruby-build/
に出力されたファイルの中から必要なファイルをvendor/mruby/
にコピーします。vendor/mruby-build/
を直接ビルドに使って「いません」。ビルドに使っているのはvendor/mruby/
にコピーしたファイルです。このあたりの実装がvendor/mruby/mruby_build.rb
です。
必要なファイルが揃ったらGNU Autotools(またはCMake)のビルドシステムに統合することは難しくありません。
GNU Autotoolsの場合はvendor/mruby/Makefile.am
で実現しています。
CMakeの場合はvendor/mruby/CMakeLists.txt
で実現しています。
工夫しているところはソースファイルのリストを共有しているところくらいです。mruby本体やmrbgemsが更新されるとソースファイルのリストは変わることがあるのでvendor/mruby/update.rb
で自動生成しています。Makefile.am
とCMakeLists.txt
では自動生成されたリストを読み込んでビルドシステムに統合しています。
mrubyをRakeでビルドして生成されたファイルをソースアーカイブに含めることでGroongaユーザーはビルドするときにRubyがなくてもビルドできます。
ソースアーカイブには生成されたファイルといつ生成されたかを示すタイムスタンプファイルを入れています。タイムスタンプファイルが新しければ再生成(mrubyをRakeでビルドし直すこと)せずにすでにあるファイルを使うようにしています。こうすることでソースアーカイブからビルドするGroongaユーザーはRubyがなくてもビルドできるようになっています。
実現方法のポイントはvendor/mruby/Makefile.am
でBUILT_SOURCES
をlibmruby_la_SOURCES
に追加しているところとmruby_build.timestamp
があったら再生成しないようにしているところです。
libmruby_la_SOURCES += $(BUILT_SOURCES)
mrblib.c: mruby_build.timestamp
mrbgems_init.c: mruby_build.timestamp
# ...
mruby_build.timestamp: build_config.rb version
# ...
Groongaがどうやってmrubyを組み込んでいるかについてビルド周りだけを説明しました。
Groongaのmruby組み込み周りを触る人や自分のアプリケーションにmrubyを組み込みたい人は参考にしてください。
クリアコードはFluentdというソフトウェアの開発に参加しています。Fluentdはログ収集や収集したデータの分配・集約などを行うソフトウェアです。
v0.14での新機能を使ったプラグインを作成する際にはこれまでの Fluent
以下のクラスではなく、Fluent::Plugin
以下のクラスを継承し、実装する必要が出てきました。
また、v0.14のOutputプラグインはv0.12とは異なり、Fluent::Plugin::Output
クラスに様々な機能が入っています。これらの機能をプラグイン開発者向けに解説することを目指します。
この記事はv0.14.8以降が対象です。 まずは、Outputプラグインが必ず実装するべきメソッドについてのおさらいです。
def emit(tag, es, chain)
# ...
chain.next
end
を
def process(tag, es)
# ...
end
と読み替えます。 output#process(tag, es)
だけを実装するとnon-bufferedプラグインになります。
例えば、out_relabel の使用例があります。
output#write(chunk)
を実装するとbuffered outputプラグインになります。
def write(chunk)
# ...
end
例えば、out_stdout の使用例があります。
output#try_write(chunk)
を実装するとbuffered asynchronous outputプラグインになります。
def try_write(chunk)
# ...
end
out_stdout の使用例があります。ただし、これはテスト用の実装のため、実用のものとは異なることに注意してください。
また、#commit_write(chunk_id)
を呼び、chunkのwriteを確定させることが必要です。
rollback_write
は commit_write
が行われないまま指定秒数が経過した chunk に対して自動的に呼ばれるので、プラグイン開発者が明示的に呼ぶ必要は通常はありません(秒数は delayed_commit_timeout
で設定から制御可能)。
ここまでがv0.14のOutputプラグインの基本的な事柄です。
では、さらにv0.14のプラグイン開発者にとって必要なことを順々に見ていきましょう。
#format(tag, time, record)
を実装すると、bufferのchunkでmsgpack以外のformatが使用できるようになります。
#format
を使用すると、
def formatted_to_msgpack_binary
true
end
としてtrueを返すようにしなければ chunk#msgpack_each
メソッドは使用できません。
v0.12のObjectBufferedOutput互換になるのは #format
を実装していない場合です。
#format
の有無や、 #formatted_to_msgpack_binary
の返り値によって挙動が異なってくるのに注意してください。
chunk#msgpack_each
でyieldされてくる値は #format
を実装している時とそうでない時で異なります。
def write(chunk)
chunk.msgpack_each do |time, record|
# ...
end
end
ただし、#msgpack_each
は互換性のために残されているものです。
通常は chunk.each
を使ってください。msgpack_each
も(主に互換性の関係から) alias が定義されていますが、本来 chunk の内部フォーマット(msgpack)を意識させたメソッドを使うのは好ましくありません。
tagが必要な場合は、
config_section :buffer do
config_set_default :@type, DEFAULT_BUFFER_TYPE
config_set_default :chunk_keys, ['tag']
end
のようなbufferのdefault confを足し、chunk.metadata.tag
で取得してください。
また、tag が必要な場合 config_set_default :chunk_keys, ['tag']
を指定しておくのはよいですが、これは設定で上書きされる可能性があるため #configure
でチェックを行うべきです。
def configure(conf)
super
raise Fluent::ConfigError, "chunk keys must include 'tag' for this plugin" unless @chunk_key_tag
# ...
end
#format(tag, time, record)
を実装した場合は、to_msgpackでmsgpackへパックした順にmsgpack_eachをすると得られます。
また、#formatted_to_msgpack_binary
をオーバーライドしてtrueを返すようにしてください。
def format(tag, time, record)
[tag, time, record].to_msgpack
end
def formatted_to_msgpack_binary
true
end
def write(chunk)
chunk.msgpack_each do |tag, time, record|
# ...
end
end
injectヘルパーを使う場合は #format(tag, time, record)
を通すことでより見通しが良くなります。そのため、 #format
を実装し、その中で inject_values_to_record(tag, time, record)
を呼ぶようにしてください。
v0.14のOutputプラグインはオーバーライドするメソッドや実装するメソッドにより、confの設定により実行時に3種の異なる種別のOutputプラグインへ切り替えることができます。
これは以下の優先順位で行われます:
#process
しか実装されていない → non-buffered)<buffer>
セクションが指定されている場合 → buffered#prefer_buffered_processing
を呼んで判定output#write
と output#try_write
を実装して #prefer_delayed_commit
の返り値のtrue/falseでbuffered synchronousとbuffered asynchronousを切り替えられます。
output#write
と output#try_write
のどちらか一方だけ実装している場合は、#prefer_delayed_commit
は呼ばれません。
#write
, #try_write
を実装していないOutputプラグインへのconfigには <buffer>
ディレクティブが使用できません。
#prefer_delayed_commit |
#prefer_buffered_processing |
結果 |
---|
false | false | non-buffered
false | true | buffered synchronous
true | true | buffered asynchronous
true | false | 選択不可
secondaryに指定されたプラグインはbufferingのサポートが必要です。out_fileなどのbufferingをサポートしたoutputプラグインを指定できます。
<buffer CHUNK_KEYS>
のようにbufferディレクティブにはCHUNK_KEYSのアトリビュートの指定が可能です。
tag, timekey, variablesの指定ができるようになっています。これはこのアトリビュートによってチャンクをひとまとめにするためにあります。
start時に <buffer>
ディレクティブにある flush_thread_count
で指定されている数のスレッドを作ります。#submit_flush_once
は単にそれらのスレッドを明示的にアクティブにしているだけです。
プラグインが自前で作成していたスレッドは以下のようにできるはずです。
Fluent::Output
プラグインを継承していたが(ある設定が有効なときのみ)バックグラウンドでflushするような処理を自前で書いていた → #process
および #write
両方を実装して設定により挙動を切り替えるそれ以外の場合は thread plugin helper を使います。自前で Thread.new
するべきではありません。thread plugin helperを使う場合、plugin test driverがそのスレッドの状態管理などの面倒を見てくれるため、たまに失敗するテスト、などの危険性が大幅に低下します。
chunk.metadata
が実際にどの値を有しているかは <buffer CHUNK_KEYS>
の CHUNK_KEYS
に何をユーザが指定したか(あるいは config_set_default
で何が指定されていたか)により異なります。
が、プラグイン作者が独自にチェックするべきではなく #configure
内で #placeholder_validate!("name_of_parameter", @name_of_parameter)
を使うべきです。使われているプレースホルダと chunk key の間に不整合があれば configuration error が上がります。
(もっと細かい制御もやろうと思えばできますが、コーナーケースです。こちらの議論を参照してください。)
つまりプラグイン作者は #configure
内で #placeholder_validate!
し、そこが通っているならあとは #write
で extract_placeholder(@name_of_parameter, chunk.metadata)
するだけでよいです。
chunkに含まれるタグに展開されます。 また、tag1.tag2.tag3.... のようなタグとなっている場合、 ${tag[0]}, ${tag[1]}, ${tag[2]},...のようにタグの添え字を指定することで個別に取り出すことができます。
strftimeのフォーマットに準じて展開されます。
variable_%Y-%m-%dT%H:%M:%S.%N
のように用います。
これは variable_2015-12-25T12:34:56.123450000
のように展開されます。
v0.14のOutputプラグインの仕様をFluentdの開発者の協力を仰ぎ*1書き出してみました。v0.12のoutputプラグインと変わっている箇所も多く、単純にv0.14への移行は難しい箇所もあります。 v0.14のAPIを使うように移行するとプラグインヘルパーやプレースホルダーの機能により、より柔軟なconfの設定を書くことが可能になります。例えば、プレースホルダーの機能を使ったものとしては、fluent-plugin-mysql のテーブル名へのプレースホルダーを指定可能にする機能*2 を実装したものがあります。このようにタグや日付ごとのデータ集計をサポートする機能を簡単に実装できるようになるというメリットがあるため、v0.14のAPIを使うように移行を試みてみるのはいかがでしょうか?
*1 この記事を書くに当たって @tagomoris さんのレビューの協力を仰ぎました。ありがとうございます。
*2 https://github.com/tagomoris/fluent-plugin-mysql#configuration-examplebulk-insert-with-tag-placeholder-for-table-name や https://github.com/tagomoris/fluent-plugin-mysql#configuration-examplebulk-insert-with-time-format-placeholder-for-table-name を参照。
クリアコードでは、普段の開発で実践していることを開発スタイルとしていくつかまとめています。 そのなかの一つが、「問題を見つけたらupstreamで直す」です。
今回は、自分達で実践するだけではなくて、「問題を見つけたらupstreamで直す」のをまわりに勧めてうまくいった事例(Kermitの不具合を報告)を紹介します。
Kermitとはシリアル通信でファイル転送をするためのプロトコルであり、組み込み用途にEmbedded Kermitが用意されています。 今回の事例では、シリアル通信でファイルを転送するのにEmbedded KermitのJavaの実装を採用していました。
Kermitを利用してファイル転送をしていると、ファイルの内容が破損するという問題に遭遇しました。 Embedded KermitのJavaの実装は10年以上修正されていないので、それなりに実装が枯れていると思われるのにも関わらず、です。
問題はKermitのJavaの実装におけるパケットのシーケンス番号の取扱いにありました。
Kermitのプロトコルは仕様がPDFで公開されています。
このPDFの4章に記載されているパケットフォーマットの仕様を見るとわかるのですが、4.1.のパケットのフィールドにおけるシーケンス番号の説明には次のようにあります。*1
The packet sequence number, modulo 64, ranging from 0 to 63. Sequence numbers "wrap around" to 0 after each group of 64 packet
パケットのシーケンス番号のとりうる値は0から63であり、63の次は0に戻るmodulo 64で処理しなければならないと規定されています。
この問題に対して、次のようなパッチを対策として当てることになりました。
diff --git a/src/com/lucent/kermit/Kermit.java b/src/com/lucent/kermit/Kermit.java
index cef2545..ece21d2 100644
--- a/src/com/lucent/kermit/Kermit.java
+++ b/src/com/lucent/kermit/Kermit.java
@@ -1294,7 +1294,7 @@ public class Kermit {
/** Returns the time we should wait for a timeout a*/
public int getTimeout() { return sendTime; }
- protected int getNextSeq( int num ) { return (num >=64)?0:num+1; }
+ protected int getNextSeq( int num ) { return (num >=63)?0:num+1; }
public String toString() {
return "kermit:"
修正前の処理だとgetNextSeq()の引数であるnumが63の場合に、次のシーケンス番号としてインクリメントした値(64)を返してしまいます。 ここで次のシーケンス番号は仕様書の規定どおりに0を返さなければいけないので、仕様に合致していません。 従ってシーケンス番号の判定条件が明らかに誤っています。
修正後の判定条件ではnumが63以上の場合には次のシーケンス番号として仕様通りに0を返すようになっています。
上記のJavaの実装では1パケット(シーケンス)で最大1024バイトまで送れるようになっていました。*2 そのため、この問題が発覚するのは、サイズが64KBを越えるファイルを転送した場合に限られていました。
10年ものの不具合を踏みぬいたということは、もしかすると64KBを越えるファイルサイズのシリアル通信という用途にJavaの実装は使われていなかったのかもしれません。
これでシリアル通信で、転送したファイルが壊れてしまう問題が解決しました。 解決はしたのですが、開発元へのフィードバックはまだなされていないようでした。
そこで、開発元にこのパッチをフィードバックしてみませんか、と開発に参加していたプロジェクト内で働きかけてみました。 独自にパッチをメンテナンスし続けるにはそれなりのコストがかかります。むしろ開発元に反映してその成果物を利用するほうが、結果としてメンテナンスコストが下がるというメリットが得られるためです。
そのプロジェクトではフリーソフトウェアに対する理解がもともとあったので、働きかけが実を結び作成されたパッチは無事開発元に反映されました。
今回は、バグを踏んだら開発元に報告するのをまわりに勧めてうまくいった事例を紹介しました。
遭遇した問題に対するパッチも作成して報告するというのは理想ではありますが、パッチを作成することそれ自体は必須ではありません。 バグ報告した内容が再現可能であれば、それだけでも開発元にとって助けになります。
フリーソフトウェアのバグを踏み抜いて、まだそのフィードバックをしていない事例を見つけたら、ぜひまわりにもバグ報告することを勧めてみてください。 フリーソフトウェアをよりよくすることで、その普及に繋げることができます。
(おそらく)2016年のクリスマスにRuby 2.4.0がリリースされます。Ruby 2.4.0で導入されるrb_gc_adjust_memory_usage()
というAPIについて紹介します。
このAPIはバインディングとGCの関係を改善するためのAPIです。(バインディングについてはRubyKaigi 2016:How to create bindings 2016を参照してください。)
まず、バインディングとGCの関係にどのような問題があったかを説明します。RubyのGCはRubyのオブジェクトがどのくらいメモリーを使っているかを知っているのでメモリーを結構使っているなーと思ったらGCして不要なメモリーを解放したり再利用したりします。これにより、プロセスのメモリー使用量の肥大化を防いでいます。しかし、RubyのGCはバインディング対象のライブラリーがどのくらいメモリーを使っているかを知りません。そのため、バインディング対象のライブラリー内で結構メモリーを使ってもRubyのGCはなかなか走りません。その結果、プロセスのメモリー使用量が肥大化しがちです。マルチメディア系のライブラリー(画像や動画を扱うライブラリー)やインプロセスのデータベースライブラリー(たとえばRroonga)はこうなりがちです。
rb_gc_adjust_memory_usage()
はこの問題を解決する手段を提供します。バインディングはrb_gc_adjust_memory_usage()
を使うことでバインディング対象のライブラリーがどのくらいメモリーを使っているかをRubyのGCに伝えることができます。これでRubyのGCはメモリー使用量をより正しく知ることができます。その結果、適切なタイミングでGCを実行しやすくなります。
rb_gc_adjust_memory_usage()
の使い方は簡単です。RubyのGCが知らないところでメモリーを確保したらそのサイズを正の値で呼び出し、そのメモリーを解放したらそのサイズを負の値にして呼び出します。なお、RubyのGCに伝えるサイズは確保したサイズと解放したサイズが同じになれば正確でなくても構いません。GCを実行するタイミングの参考に使われるだけなので概算で大丈夫です。
具体的な使用例を見てみましょう。rcairoという画像を扱うライブラリーのバインディングの使用例を見ます。rcairoはrb_gc_adjust_memory_usage()
に対応しているバインディングです。
rcairoでは2次元画像を扱うクラスCairo::ImageSurface
のインスタンスを生成したときに画像のバッファーサイズを確保したメモリーサイズとしてrb_gc_adjust_memory_usage()
を呼び出しています。バッファーサイズは1行のバイト数(cairo_image_surface_get_stride(surface)
)×行数(cairo_image_surface_get_height(surface)
)で計算できます。ソースはext/cairo/rb_cairo_surface.cです。
cairo_surface_t *surface;
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
NUM2INT (width),
NUM2INT (height));
{
ssize_t added_memory_usage;
added_memory_usage =
cairo_image_surface_get_stride (surface) *
cairo_image_surface_get_height (surface);
rb_gc_adjust_memory_usage (added_memory_usage);
}
インスタンスを解放したときは画像のバッファーサイズを負の値にしてrb_gc_adjust_memory_usage()
を呼び出します。
{
ssize_t freed_memory_usage;
freed_memory_usage =
-(cairo_image_surface_get_stride (surface) *
cairo_image_surface_get_height (surface));
rb_gc_adjust_memory_usage (freed_memory_usage);
}
cairo_surface_destroy (surface)
これだけです。実際は画像のバッファーサイズ+αのメモリーを確保していますが、そこは画像のバッファーサイズに比べれば誤差ですし正確にカウントすることも面倒なので、RubyのGCには伝えていません。
実際にどのくらい効果があるかrcairoのケースを見てみましょう。rcairoは画像を扱うライブラリーのバインディングなのでバインディング対象のライブラリーが大きめのメモリーを確保する傾向にあります。そのため、rb_gc_adjust_memory_usage()
が有効なケースです。
次のような600x600のサイズの画像を1000個作るだけのスクリプトを考えます。
require "cairo"
width = 600
height = 600
1000.times do
Cairo::ImageSurface.new(:argb32, width, height)
end
rb_gc_adjust_memory_usage()
を使ってバインディング対象のライブラリーが使っているメモリー使用量をRubyのGCに伝えた場合となにもしない場合のメモリー使用量は次のようになりました。横軸が繰り返し回数で縦軸がメモリー使用量です。青い線がrb_gc_adjust_memory_usage()
を使った場合で黄色い線が使っていない場合です。青い線の方は最大メモリー使用量が抑えられていますが、黄色い線の方は最大メモリー使用量が増え続けています。(繰り返し回数が少ないうちは黄色い線の方がメモリー使用量が少ないことは興味深い結果です。)
なお、このグラフは次のようなスクリプトでデータを取得しgnuplotで描画しました。gnuplotのスクリプトはgc-triggerで使っているものを少しいじって使いました。
require "cairo"
width = 600
height = 600
1000.times do |i|
Cairo::ImageSurface.new(:argb32, width, height)
case File.readlines("/proc/self/status").grep(/\AVmRSS:/)[0]
when /\AVmRSS:\s+(\d+)\s+kB/
vm_rss_mib = $1.to_i / 1024.0
end
puts [i, GC.count, vm_rss_mib].join("\t")
end
Ruby 2.4の新機能であるrb_gc_adjust_memory_usage()
を紹介しました。バインディングを作っている人は活用してください。
この機能は東京Ruby会議11で話したことがきっかけでRuby本体に提案した機能です。前からどうにかならないかなぁと問題意識は持っていましたが、よい案が浮かばないこともあり特になにもアクションを起こしていませんでした。話す機会があったことでちゃんと考える機会になり、結果としてRubyがよくなることにつながりました。みなさんも問題意識があることをまとめる機会を作ってみてはどうでしょうか。まとめることで解決案を思いついて改善につながるかもしれません。