Richard Imaoka's Blog

2017年より職業Scalaプログラマになった、リチャード・伊真岡のブログです。

OSSコントリビューション その2 ドキュメント修正のイシューを探す

こんにちはみなさん、リチャードです。

前回「OSSコントリビューション イシューの見つけ方 その1 - Richard Imaoka's Blog」という記事でOSSコントリビューションを始めたい方向けに取り組むべきイシューの探し方を紹介しました。

しかし、ある程度規模の大きなOSSになってくると、上の記事で紹介した探し方で100を超えるような数のイシューがみつかってしまい、どれから手を付けていいのかわからない…という場合もよくあります。そんなときにドキュメント修正のイシューから始めてみては?という提案です。

TL;DR

  • ドキュメント修正のイシューは比較的手を付けやすいものが多い
  • ドキュメント修正のイシューでもちゃんと知識は身につく
  • 普段ドキュメントを修正しつつ、できそうなコードの変更があればそれをやればいい

それではいってみましょう。

ドキュメント修正のイシューは比較的手を付けやすいものが多い

私はakkaというOSSに2年ほど貢献しています。自分があげたPull Requestはこの記事を書いている時点92、マ-ジされたものは86でそのうちドキュメント修正のイシューが70あります。

70 / 86 = 81%

つまり私の過去のakkaに対する貢献は8割以上ドキュメントの修正です。

「なんじゃこいつ、コードじゃなくてドキュメントばっかりやってる素人じゃねえか」と思う方もいるでしょうが、まさにその通りです。そんな私でも90近いコミット数を稼ぐことができました。僕自身はコミット数自体を伸ばしたいと思っていて、それを点数を稼ぐゲームのように捉えているので、ドキュメントであれ何であれ簡単なイシューでコミット数を稼げるのはありがたいことです。

そうやってせっせとドキュメントの修正をしていたら、なんと2017年6月、akka 2.5.3のリリース時に紹介してもらいました

f:id:richard-imaoka:20180217132247p:plain

実はこの2.5.3リリースの前にボーナスチャンスがあって、簡単なドキュメント修正を大量のファイルに対して行うことで30コミット近く稼ぐことができました。 github.com

簡単な作業しかしてないのですが、それでもこうやって言及してもらえると嬉しいものです。

ドキュメント修正のイシューでもちゃんと知識は身につく

ドキュメント修正のイシューをこなしていても、ちゃんと対象のOSSに対する知識は身についていきます。ドキュメントの修正とはいえ、当該範囲の知識なしにできる物は少ないのです。

よくあるイシューとして「新機能を足した、しかしドキュメントが更新されてない、新機能の説明を足すべき」「このドキュメントのこの部分が実際のコードの動作と違う、修正すべき」といった例があります。どちらも「新機能自体を理解する」「実際のコードの動作を検証する」という作業をしないと正しくドキュメントを書くことができないことはわかると思います。

私が取り組んだ実際のPull Requestだとこれがよい例ですね。サンプルコードを追加しましょうというイシューでしたが、サンプルコードを書いて走らせて、動作確認して、と何度か繰り返した後Pull Requestをあげることができました。サンプルコードが思った通りの動作をしてくれなくて試行錯誤した記憶があります。それが逆にakkaの内部を理解することにつながりました。

github.com

次のこのPull Requestは別の場所(akka-http)にあったドキュメントの一部をほぼコピーだけするような作業で終わるはずだったイシューでしたが、最終的には文章に多少のアレンジを加える必要がありましたし、もちろんサンプルコードの動作確認もおこないました。おかげでakkaのthread周りについて詳しくなるきっかけを掴むことができました。

github.com

つまり、ドキュメントの修正であっても自分でコードを書いて対象のOSSを動かす作業が必要な場合が多いので、続けていけば知識を身につけることができます。

普段ドキュメントを修正しつつ、できそうなコードの変更があればそれをやればいい

そして、ドキュメントの修正をやるからといって、それだけをやる必要はないわけです。コードの変更も、どちらもやればいいのです。

ドキュメント修正のイシューの方が簡単なことが多いので、普段そちらをやってOSSコントリビューションのペースを保ちつつ、時間がたくさん取れるときや自分が得意そうな範囲のコードの変更があればそれも行いましょう。たとえ「メソッドにパラメタを一つ足す」「細かいバクの対応」といったレベルでも、自分のコードの変更がOSSに取り込まれると嬉しいものです。

OSSコントリビュートしたい方には朗報!

さて、最後に宣伝ですが、私もScalaMatsuri 2018スタッフとしてお手伝いしている以下のイベントがあります。 OSSコントリビュータとしてロケットスタートを切るには絶好の機会です。akkatwitter4ssbtaeronScalikeJDBCという有名OSSからメインの凄腕メンテナさん達が参加・指導してくれます。協力してくれるメンテナさんたちは「メンテナチームの一員」といったレベルではなく、それぞれのOSSプロダクトの創始者もしくはここ数年でコミット数1、2位の、本当に各プロダクトを代表するメンテナさんたちです。

さあ、あなたもOSS貢献の一歩を踏み出してみませんか?

jsa.connpass.com

それから例によって、僕に何かお手伝いできることがあればtwitterでなんでも聞いてくださいScala関連のOSSについて、OSS貢献したいけど英語で困っている、もうちょっと細かいイシューの探し方について聞きたい、akkaに関すること、できないことはできませんが、できることならみなさんのOSS活動をお手伝いしますよ。

OSSコントリビューション イシューの見つけ方 その1

こんばんは。リチャードです。

皆様OSS(Open Source Software)に貢献したいとおもっているけど、どこから初めていいかわからない人はいますか?OSS貢献している人ってかっこよく見えますよね。でも、カッコいい人、技術力が高い人ばかりがOSS貢献しているわけではありません。

わたしは技術的に全然大したことないですが、2年ほどakkaに貢献しています。 むしろ、私のように技術的に大したことないエンジニアでもOSS貢献を通して技術力を磨くことが出来ると思っています。

さて、今日はそんなOSS貢献をしたいと考えている皆さんに向けて、はじめの一歩の踏み出しかたを紹介したいと思います。

方法の紹介です

あ、ちょっと待ってください。話がそれますが、この記事で紹介する方法よりも効果的にOSS貢献したいひとには、OSSハッカソンという最強のファストトラックがありますよ。わたしもスタッフとしてお手伝いしているイベントです。ぜひぜひご応募を。

jsa.connpass.com

今度こそ方法の紹介です

TL;DR

  • help wantedタグで検索しましょう
  • すべての簡単なイシューにhelp wantedタグがついてるわけではないので、タグがついてないものもしらみつぶしにさがします
  • 見つけた後にすることはまた別の記事で書く予定です

help wantedタグで検索しましょう

私も貢献しているakkaを例にお話ししますが、akkaのように現在多くのOSSGitHubで管理されています。そしてGitHubには、akkaもそうですがIssuesというページがあります。

f:id:richard-imaoka:20180212195106p:plain

そこを開くとhelp wantedというタグが付いたイシューがあるので、そのhelp wantedというタグをクリックすると、help wantedタグがついたイシューのみが表示されるページに飛びます

プロジェクトごとのメンテナ陣が「これはコミュニティ(コアなメンテナ陣以外)の人たちに手伝ってもらいたい」と思ったイシューにはhelp wantedタグ(もしくはそれに類するタグ)が付けられます。簡単なイシューであることが多く、OSS貢献を始めるには手頃なイシューが比較的見つかりやすいかと思います。 あとはひたすらイシューを一件一件ひらいて、中身を見て、すこしでも自分が手を出せそうなイシューを探し出します

頑張って、頑張って探してください。これだけです、ほんとにこれだけです。ものすごく効果的なコツはないのですが、地道なイシュー探しが1コミット、そしてまた1コミットとあなたのコミット履歴を伸ばしてくれます。(GitHubはプロジェクトごとのコミット数ランキングを出してくれるので、励みになりますね。)

しかしhelp wantedを見ても手を出せそうなイシューがなかった見つかりそうもない場合はどうしましょう?正直な話をすると、わたしは2年もakkaにコミットし続けているのにhelp wantedがついているイシューの多くについて、どこから手を付けていいのかわからないものがたくさんあります笑

初めての人ならなおさら、help wantedの中から手頃なイシューが見つからないこともあるでしょう。そんなときは次の方法を試してみましょう。

全ての簡単なイシューにhelp wantedタグがついてるわけではないので、タグがついてないものもさがします

基本どのプロジェクトでもhelp wanted(もしくはそれに類似するタグ)は手作業でつけられています。ある程度規模の大きなOSSになると、イシューの管理が追いつきません。超人気OSSだと、tensorflowはイシュー1,000以上、Kubernetesは4,000以上のイシューがあります。各プロジェクト数人しかいないコアなメンテナ陣では全てに目を通すことが出来ません。

なので、本来ならばhelp wantedがつけられるような簡単なイシューでも、ついてないままになっていることはよくあります。基本的にOSSは一般の人々からの貢献は大歓迎なので、遠慮せず手頃なイシューを見つけたら取り掛かりましょう。

これも先程と同じで、…いうよりも先程よりも更にイシューの数が多いので大変なのですが、一件一件詳細を開いて手を出せそうなイシューを探してください。わたしもこれをよくやっています。電車に乗っている時間などを活用して頑張って探してますが、だいたい飽きて5分でガールズちゃんねるに飛んでゴシップネタを見はじめています。

だんだん雰囲気がわかってきましたか…?そうです、多くの場合OSS貢献は「超絶技術力のあるスーパーエンジニアが、さくっと現れてクールな機能を実装してヒャッホー!x100コミット完了」ではなく、地道に地道にイシューを眺めて、眺めて、探して直すという地味さ極まる作業なのです。その反面、その地味な作業に耐え続ける根性があればだんだんOSS界隈で実績を積んでいくことが出来ます。

見つけた後にすることはまた別の記事で書く予定です

いったん「手を出せそうだな」と思っても、実際に手を出そうとすると思いの外苦戦することもOSS貢献では多いと思います。わたしがakkaに初めて貢献したときは、ほとんどドキュメンテーションを直すだけのとても簡単なイシューでしたが、取り掛かり始めてからイシューをちゃんと理解してコードサンプルの動作確認も含めて一ヶ月近くかかったと思います。

simplifyTestKit doc async sample #18804 by richardimaoka · Pull Request #19906 · akka/akka · GitHub

このイシューですね。Pull Request上がってすぐマージ(コミットの取り込み)が行われているように見えますが、Pull Request上げるまでが長かったんです。

イシューを見つけてからPull Requestをあげるまでもコツのようなものはあります。そのことはまた別の記事で書いていこうと思います。

その他の探し方

これもまた別の機会で記事にしようかと思いますが、他にもイシューの探し方のコツがあります。初心者に特にオススメなのはドキュメンテーション系のイシューを探すことですね。私も貢献の大部分はドキュメンテーションの修正や、単なる誤字脱字の訂正を含みます。取り掛かりたいOSSプロジェクトが本体レポジトリの中にドキュメンテーションを含んでいる場合特に、積極的にドキュメンテーションの修正イシューに手を出しましょう(ドキュメンテーションOSSプロダクトの本体と別レポジトリになっている場合やる気が若干削がれますね…でもやらないよりましだと思います。)

いつでも私リチャードに聞いてくださいね

特にakkaを中心に、Scala OSSに貢献したいけど、どうしていいかわからないときはぜひ私にtwitterで聞いてみてください。

twitter.com

出来る限りのことはサポートしますよ。わかんないときはわかんないっていいますし、出来ないことは出来ないですけど、だからこそ気軽に聞いてもらって、私が助けられることがあればラッキーみたいに思ってください。

なんでお金にもならないのにこんなことするかって?

建前論を言えば私は日本のOSSコミュニティがもっと盛り上がって欲しいと思っています…というのはもちろんウソではないのですが、私のように無名なエンジニアにとっては名前を覚えてもらう良い機会だと思っているからです。皆さんへのお手伝いを通して、その中から一人でも私の名前をちょっとの間だけでも覚えてもらえると、私にとってもすごく嬉しいことです。急には有名にはなれないので、地道な積み重ねをしようということですね。

最後にもう一度宣伝、やっぱりOSS貢献するんならエキスパートに指導受けるのが最大の近道です、こちらへの応募どうぞ!! jsa.connpass.com

builderscon 低温調理肉会 2017秋 @ 新宿御苑に行ってきた

buildersconスタッフのリチャード伊間岡です。lestrrat氏主催の肉会に行ってきました。

peatix.com

いきなりの写真ですけどこれ。これ、前菜ですからね!

f:id:richard-imaoka:20171118171222p:plain

(赤いけどちゃんと火のとおった肉です)

上記のリンクにもあるように

本会の目的は「主宰の牧が大量に肉を調理したい欲求を叶えること」です。

主宰氏が用意した大量の肉をbuildersconに関わる人々(主にスタッフなど)といっしょに地下の食堂でワイワイしながら食べようという企画がこの肉会です。

ちなみに低温調理と言われても聞いたことない人にはなんのことだかわからないですよね。要はこういうことです。anovaという調理機器を使っていい感じに肉を仕上げようという企みです。

medium.com

このブログに並ぶ肉料理記事の数々を見て主宰氏の本気度がお分かりになるのではないかと思います。

とにかく肉を食っただけの今日の会なんですけど、とにかく旨かったです。

そして旨い肉をパンに挟んで食べるのもそれはそれは幸せな体験でした。

f:id:richard-imaoka:20171118173250p:plain

(贅沢サンド)

つまり何が言いたいかというと「buildersconに関わってるとこんなに美味しい思いをできるんですよ!!!」主宰が食の求道者だから。

f:id:richard-imaoka:20171118173514p:plain 基本肉はでかい、かなりでかい。「厚さは肉の量の問題だけじゃない。完璧な肉の外部と内部の焼き加減のバランスを得るためにはそれなりの厚さが必要だ。」 ( - iOSDC Japan 2017 にてlestrrat氏のトークより"Serious Eats"の引用部分)

f:id:richard-imaoka:20171118173637p:plain

なんかバナナを入れたホットワインとかオシャレ(?)な物が出てくるなど。

食うのに夢中になってたらこれ以外写真取ってなかった…!

とにかく大満足な会でした。これだけでもbuildersconに参加してよかったと思いますし、むしろいい肉がでな いならbuilderaconじゃないですし、私はそこに肉をもたらす会がある限りbuildersconには参加し続けようと思う次第です。私は食の奴隷。

lestrratさんありがとねー。

builderscon tokyo 2017動画は外注でなく我々スタッフが撮影しました! 振り返りと次回への野望

みなさま、builderscon tokyo 2017 - Aug 3, 4, 5 2017は楽しんでいただけましたでしょうか?

楽しんでいただけた方も、または残念ながらご参加いただけなかった方も、こちらに動画をアップロードしています。 ぜひぜひ楽しんでください。

www.youtube.com

(2017/08/12現在、まだ動画の編集及び確認作業中で、すべては公開されていません。)

ちなみに私、今回は主に動画撮影・編集担当としてはたらいておりました。 そう、↑で公開している動画は、我々スタッフで撮影して編集しているのです。外部業者に依頼はしていません。 手作り感あふれる趣があるのはそのためですね。

buildersconでは動画においてもbuilderの精神で、自分たちでやっちゃうのです。 自分たちでやると工夫のし甲斐があってノウハウがたまる面もあるのでおもしろいですね。 今回もいろいろ勉強させていただき、私の動画力が格段に上がりました。 次回以降の開催に活かしたいことがたくさんありましたし、次回以降に試したいこともたくさんできました。

ただ、とんでもない金額のお金が転がり込んできたら外注して超絶美麗な動画を作りますので、膨大な資金をbuildersconに注ぎ込んでくださる方はいつでも猛烈に大歓迎です。

f:id:richard-imaoka:20170804234807p:plain

もしそうなったらCG入れたりとか、火花飛ばしたりとかしますよ。 あと、映りこむスタッフの顔をイケメンに差し替えたり。

まあ実際のところ、今後もbuildersconでは動画を自家製でやっていく可能性が高いので、 もしビデオの撮り方を覚えたいという人は是非次回以降の動画作成を手伝ってみてはいかがですか? 一緒にユーチューバーの夢を見ましょう(違)

個人的には次回以降に映像のクオリティを上げたい野望はあります。 そのためには圧倒的な手間と、それからややお金が掛かるので実現はむずかしいんですけどね。 あと、動画の超早出し公開のワークフロー作成に燃え上がれる人とか。 これも手間がすごいので今はできる目途がありません。 でも、やりたいという情熱がある人が来れば、カンファレンスを利用して、動画作成の技を覚えるチャンス。

それからさらに個人的にカンファレンス動画以外もつくりたいです。 ウェブサイトトップページに載せるムダにカッコいいインタビューとか。 あと、ドキュメンタリー映像的なのも作りたいですね笑「builderの夜明け」みたいな(おや、そのタイトル…?)

動画で何かやらかしたい野望がある人、buildersconではそんな情熱を待っています。 誰も手を上げなければ私がやらかすだけなので、もったいないですよ笑。

とにかく、撮影担当は(失敗を考えて胃が痛かったけど笑)楽しかったです。エンジニアが手を広げる趣味としてはよい候補ではないでしょうかね?

Typesafe Akka Remote Sampleの図解 - 2/2 LookupApplication編

TypeSafeのAkka Remote Samples with Scalaに含まれる2つ目のサンプルアプリケーション

前回の記事に引き続き、何かとわかりにくいTypeSafe社の@TypeSafeのAkka Remoteのサンプルについて、図解していきたいと思います。

サンプルの中には2つのアプリケーションが含まれていて、この記事はその2つ目、LookupApplicationについてです

メッセージの型としてのcase class MathOpは前回の記事でも解説した通りです

f:id:richard-imaoka:20151105024105p:plain

// sample/remote/calculator/MathOp.scala
trait MathOp
final case class Add(nbr1: Int, nbr2: Int) extends MathOp
final case class Subtract(nbr1: Int, nbr2: Int) extends MathOp
final case class Multiply(nbr1: Int, nbr2: Int) extends MathOp
final case class Divide(nbr1: Double, nbr2: Int) extends MathOp

Akkaでよく使われるcase classをメッセージの型として使う方法です。足し、引き、掛け、割り算に相当する以上の4つが定義されています。

MathResultも同様に前回の記事の通りです

f:id:richard-imaoka:20151105024107p:plain

// sample/remote/calculator/MathOp.scala
trait MathResult
final case class AddResult(nbr: Int, nbr2: Int, result: Int) extends MathResult
final case class SubtractResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class MultiplicationResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class DivisionResult(nbr1: Double, nbr2: Int, result: Double) extends MathResult

それぞれに対する結果型も用意されています。以下で見るようにActorはこれらの型のメッセージをやり取りして、計算の入力と結果を受け渡します。

CalculatorActorは計算入力を受け取って結果を返す、LookupActorはRemoteで生成されたCalculatorActorを探して、監視したうえで、計算を行わせます

f:id:richard-imaoka:20151105024114p:plain

// sample/remote/calculator/calculatorActor.scala
class CalculatorActor extends Actor {
  def receive = {
    case Add(n1, n2) =>
      println("Calculating %d + %d".format(n1, n2))
      sender() ! AddResult(n1, n2, n1 + n2)
    case Subtract(n1, n2) =>
      println("Calculating %d - %d".format(n1, n2))
      sender() ! SubtractResult(n1, n2, n1 - n2)
    case Multiply(n1, n2) =>
      println("Calculating %d * %d".format(n1, n2))
      sender() ! MultiplicationResult(n1, n2, n1 * n2)
    case Divide(n1, n2) =>
      println("Calculating %.0f / %d".format(n1, n2))
      sender() ! DivisionResult(n1, n2, n1 / n2)
  }
}

こちらも前回の記事で解説した通りです。

例えばMultiply型のメッセージを受け取ったときは、その結果であるMultiplicationResult型のメッセージを送信元"sender"に投げ返します。

LookupActorは(LookupActorから見て)RemoteにあるCalculatorActorを探しに行きます

f:id:richard-imaoka:20151106020837p:plain

今まで出てきたActorに比べてLookupActorはやや複雑です

f:id:richard-imaoka:20151106020802p:plain

//sample/remote/calculator/LookupActor.scala
class LookupActor(path: String) extends Actor { ... }

まず、上記のConstructionの部分を見ましょう。path変数には

  • path = "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

が入ってきます。

LookupActorは最初に呼び出される関数sendIdentifyRequest()はActorSelectionに対して、Identifyメッセージを送っています

f:id:richard-imaoka:20151106020816p:plain

Identifyについては後述しますので、まずはActorSelectionについて。

ActorSelectionは、上記の"akka.tcp://..."のようなパス(URL)に対して!メソッドでメッセージを送ることができます。

f:id:richard-imaoka:20151106020826p:plain

つまり、Akkaでは

  • ActorRef
  • ActorSelection

の2つに対して!メソッドでメッセージが送れることになります。

Akkaに備わっているのIdentify, ActorIdentityメッセージ型は、ActorSelection宛てにメッセージを送ったときにActorRefを得ることができます

f:id:richard-imaoka:20151106020843p:plain

次に、Identify, ActorIdentityは、Akkaに備わっているメッセージ型です。

AkkaのActorはIdentifyを受け取ると、ActorIdentityをsender()に返します。その際、ActorIdentityはActorRefを第2引数にもっています。

//akka.actor.ActorIdentity
 case class ActorIdentity(correlationId: Any, ref: Option[ActorRef]) 

LookupActorではその第2引数として返ってきたActorRefを使って、context.watch()しています。

f:id:richard-imaoka:20151106023558p:plain

context.watch()すると、別のActorを監視することができる、すなわち監視対象のActorがStopすると、Terminatedメッセージを受け取ることになります。

f:id:richard-imaoka:20151106020918p:plain

    case Terminated(`actor`) =>
      println("Calculator terminated")
      sendIdentifyRequest()
      context.become(identifying)

LookupActorではこのあともう一度sendIdentifyRequest()を呼んでいるので、TerminatedになったCalculatorActorの代わりのCalculatorActor(別インスタンス)が同じパス

  • "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

上にあれば、再びCalculatorActorを監視に入れることになります。

context.become(active(actor))によって、receiveメソッドの実装はactiveメソッドに切り替わります

f:id:richard-imaoka:20151106020857p:plain

無事CalculatorActorの監視に成功したら、次は

      context.become(active(actor))

によって、receiveメソッドの動作をactive()メソッドに入れ替えます。

  def active(actor: ActorRef): Actor.Receive = {
    case op: MathOp => actor ! op
    case result: MathResult => result match {
      case AddResult(n1, n2, r) =>
        printf("Add result: %d + %d = %d\n", n1, n2, r)
      case SubtractResult(n1, n2, r) =>
        printf("Sub result: %d - %d = %d\n", n1, n2, r)
    }
    case Terminated(`actor`) =>
      println("Calculator terminated")
      sendIdentifyRequest()
      context.become(identifying)
    case ReceiveTimeout =>
    // ignore

これは、

  • MathOpを受け取ればCalculatorActor (actor) に転送
  • MatuResultを受け取ればprintf表示
  • Terminatedを(優位つの監視対象である)CalculatorActorから受け取れば、もう一度でsendIdentifyRequest()監視

f:id:richard-imaoka:20151106023558p:plain

という動作をします。

LokupApplication

最後にアプリケーションの説明です。これも前回の記事同様、main関数はややこしいのですが…、とにかくstartRemoteCalculatorSystem()とstartRemoteLookupSystem()という二つの関数を走らせるだけです。

コマンドライン引数」のCalculatorとLookupを渡すと、2つの関数をの別のプロセスで走らせることができます。

sbt "runMain sample.remote.calculator.CreationApplication Calculator"
sbt "runMain sample.remote.calculator.CreationApplication Lookup"

args.isEmpty、すなわちコマンドライン引数を渡さないと、一つのプロセスの中で2つの関数を走らせます。s

object LookupApplication {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty || args.head == "Calculator")
      startRemoteCalculatorSystem()
    if (args.isEmpty || args.head == "Lookup")
      startRemoteLookupSystem()
  }

  def startRemoteCalculatorSystem(): Unit = {
    ...
  }

  def startRemoteLookupSystem(): Unit = {
    ...
  }
}

startRemoteCalculatorSystem()はActorSystemを初期化したうえで、そこからCalculatorActorも生成してしまいます。

これにより、CalculatorActorは後述のLookupActorから見てRemoteになります。

startRemoteLookupSystem()は,CalculatorActorのパス

  • "akka.tcp://CalculatorSystem@127.0.0.1:2552/user/calculator"

をLookupActorに渡して、後はLookupActor経由でどんどんAddとSubtractメッセージを投げ続けるだけです。

Typesafe Akka Remote Sampleの図解 - 1/2 CreationAppliation編

TypeSafeのAkka Remoteのサンプルがなんともわかりにくい気がするので、ここで図解してみることにしてみました。

TypeSafeのAkka Remote Samples with Scalaには2つのサンプルアプリケーションが含まれている

一つはCreationApplication, もう一つはLookupApplicationです。これらはコードに共通部分もありますが、別々のアプリケーションです

この記事ではCreationAppliationのみを解説します。LookupApplicationについては別記事にしようと思います。

メッセージの型としてのcase class

Akkaでよく使われるcase classをメッセージの型として使う方法です。足し、引き、掛け、割り算に相当する以下の4つが定義されています。

// sample/remote/calculator/MathOp.scala
trait MathOp
final case class Add(nbr1: Int, nbr2: Int) extends MathOp
final case class Subtract(nbr1: Int, nbr2: Int) extends MathOp
final case class Multiply(nbr1: Int, nbr2: Int) extends MathOp
final case class Divide(nbr1: Double, nbr2: Int) extends MathOp

f:id:richard-imaoka:20151105024105p:plain

それぞれに対する結果型も用意されています。以下で見るようにActorはこれらの型のメッセージをやり取りして、計算の入力と結果を受け渡します。

// sample/remote/calculator/MathOp.scala
trait MathResult
final case class AddResult(nbr: Int, nbr2: Int, result: Int) extends MathResult
final case class SubtractResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class MultiplicationResult(nbr1: Int, nbr2: Int, result: Int) extends MathResult
final case class DivisionResult(nbr1: Double, nbr2: Int, result: Double) extends MathResult

f:id:richard-imaoka:20151105024107p:plain

CalculatorActorは計算入力を受け取って結果を返す、CreationActorはCalculatorActorを生成して、計算を行わせる

CalculatorActor

CalculatorActorの実装は以下の通りです。

// sample/remote/calculator/calculatorActor.scala
class CalculatorActor extends Actor {
  def receive = {
    case Add(n1, n2) =>
      println("Calculating %d + %d".format(n1, n2))
      sender() ! AddResult(n1, n2, n1 + n2)
    case Subtract(n1, n2) =>
      println("Calculating %d - %d".format(n1, n2))
      sender() ! SubtractResult(n1, n2, n1 - n2)
    case Multiply(n1, n2) =>
      println("Calculating %d * %d".format(n1, n2))
      sender() ! MultiplicationResult(n1, n2, n1 * n2)
    case Divide(n1, n2) =>
      println("Calculating %.0f / %d".format(n1, n2))
      sender() ! DivisionResult(n1, n2, n1 / n2)
  }
}

例えばMultiply型のメッセージを受け取ったときは、その結果であるMultiplicationResult型のメッセージを送信元"sender"に投げ返します。

f:id:richard-imaoka:20151105024114p:plain

CreationActor

CreationActorの方は、

// sample/remote/calculator/CreationActor .scala
class CreationActor extends Actor {
  def receive = {
    case op: MathOp =>
      val calculator = context.actorOf(Props[CalculatorActor])
      calculator ! op
    case result: MathResult => result match {
      case MultiplicationResult(n1, n2, r) =>
        printf("Mul result: %d * %d = %d\n", n1, n2, r)
        context.stop(sender())
      case DivisionResult(n1, n2, r) =>
        printf("Div result: %.0f / %d = %.2f\n", n1, n2, r)
        context.stop(sender())
    }
  }
}

計算の入力(MathOp型のメッセージ)を受け取ると、CalculatorActorを生成します。

     val calculator = context.actorOf(Props[CalculatorActor])

f:id:richard-imaoka:20151105024118p:plain

そしてその生成したCalculatorActorに計算入力(MathOp型のメッセージ)を投げて

      calculator ! op

MathResult型のメッセージを受け取ります。

  def receive = {
  ...
    case result: MathResult => result match {
      case MultiplicationResult(n1, n2, r) =>
        ...
      case DivisionResult(n1, n2, r) =>
        ...
    }

f:id:richard-imaoka:20151105024121p:plain

以上の手順が終わったら、生成したCalculatorActorを以下のコードによって停止します。sender()となっていますが、これはMathResultのsenderなので、すなわちCalculatorActorです。

        context.stop(sender())

f:id:richard-imaoka:20151105024124p:plain

CreateApplication

最後にアプリケーションの説明です。なんだかこのmain関数はややこしいのですが…、とにかくstartRemoteWorkerSystem()とstartRemoteCreationSystem()という二つの関数を走らせるだけです。

コマンドライン引数」のCalculatorWorkerとCreationを渡すと、2つの関数をの別のプロセスで走らせることができます。

sbt "runMain sample.remote.calculator.CreationApplication CalculatorWorker"
sbt "runMain sample.remote.calculator.CreationApplication Creation"

args.isEmpty、すなわちコマンドライン引数を渡さないと、一つのプロセスの中で2つの関数を走らせます。s

object CreationApplication {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty || args.head == "CalculatorWorker")
      startRemoteWorkerSystem()
    if (args.isEmpty || args.head == "Creation")
      startRemoteCreationSystem()
  }

  def startRemoteWorkerSystem(): Unit = {
    ...
  }

  def startRemoteCreationSystem(): Unit = {
    ...
  }
}

startRemoteWorkerSystem()はActorSystemを初期化するだけで、それ自体は何もしません。

startRemoteCreationSystem()はもう一つの関数startRemoteWorkerSystem()ないで作られたActorSystem("CalculatorSystem")のしたにCreationActorを生成します。

f:id:richard-imaoka:20151105024126p:plain

これはAkka Remotingで説明されているRemote Creationになり、remotecreation.confに以下を指定することによって実現しています。

//remotecreation.conf
akka {
  actor {
    deployment {
      "/creationActor/*" {
        remote = "akka.tcp://CalculatorWorkerSystem@127.0.0.1:2552"
      }
    }
  }
  ...
}

そしてあとはAkkaのスケジューラを使って、MultiplyとDivideメッセージを送り続け、CreationActorはCalculationActorを逐一生成、停止して計算を行っていきます。 f:id:richard-imaoka:20151105024129p:plain

Akka Internals (Akkaの内部動作を知る) Remoteでメッセージを送る場合の ! メソッドの動作

今回の記事のポイント

Remote Actorを使うときにAkkaの ! メソッドの内部動作は変わり、

Remote Actorにメッセージを送るときは、数段階の「内部」Actorを経由して送られる

ということを確認していきたいと思います。

前回の記事

richard-imaoka.hatenablog.com

概要の説明

まず、送信元のActor Aからあて先のActor Bには、Local Actorの時と同じように、 ! メソッドを使って送ります。

//Code within actorA
    actorB ! "Some Message"

f:id:richard-imaoka:20151104010843p:plain

ただし、実際には以下の3つの「内部」Actorを経由します。

f:id:richard-imaoka:20151104010848p:plain

RemoteActorRefの実装によって、Remoteのメッセージ送信の動作がLocalの時とは変わっている

この違いは、Remote Actorに送るときは、メッセージの宛先のActorRefが、RemoteActorRefになっているためで、その! メソッドは以下のようになっています。

//class RemoteActorRef in akka/remote/RemoteActorRefProvider.scala
  override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit = {
    ...
    try remote.send(message, Option(sender), this) catch handleException
  }

このメソッドは以下のsendメソッドを呼び出しますが、sendメソッドメッセージの元々のあて先(actorB)をEndpointManager型のActorに差し替えます

//class Remoting (extends RemoteTranspor) in akka/remote/Remoting.scala
  override def send(message: Any, senderOption: Option[ActorRef], recipient: RemoteActorRef): Unit = endpointManager match {
    case Some(manager) ⇒ manager.tell(Send(message, senderOption, recipient), sender = senderOption getOrElse Actor.noSender)
    ...
  }

これは、Sendというcase classに元々の宛先を保存し、manager変数(EndpointManager型のActor)のtellメソッドを呼ぶことで実現しています。

f:id:richard-imaoka:20151104010916p:plain

Image courtesy of digitalart at FreeDigitalPhotos.net

Sendというcase classに内包されたメッセージは、さらに(前回の記事で見たように)Envelopeというcase classに内包されて、EndPointManager actorのMailboxに届けられます。

f:id:richard-imaoka:20151104010935p:plain

Image courtesy of digitalart at FreeDigitalPhotos.net

そこから、ReliableDeliverySupervisorというActorを経由して、EndPointWriterというActorに届けられます。

f:id:richard-imaoka:20151104010938p:plain

Image courtesy of digitalart at FreeDigitalPhotos.net

このEndPointWriterが実際に元々の宛先であったRemote ActorのActorBにメッセージを送ります。

Sendというcase classに元々の宛先を保存しているので、複数のActorを経由しても最終的にActorBにメッセージを送ることができます。

EndPointWriterの動作

このEndPointWriterの実装はやや複雑なのですが、簡単に言うと

  • メッセージを逐一送っていくのではなく、一旦バッファにためて後で一気に送る

このEndPointWriteの実装や「どれくらいの頻度でバッファを全部クリアに一気にメッセージを送るか」のチューニング、というのはメッセージングのパフォーマンスを左右する重要なところなので、いつか記事を書ければと思います。

さて、このEndPointWriterというActorの実装を見ていくと、メッセージをためるバッファはJavaの標準クラスであるLinkedListを使っています。

//class EndpointReader in akka.remote.Endpoint.scala
  val buffer = new java.util.LinkedList[AnyRef]

このバッファの実装はAkkaのパフォーマンスを大きく左右すると考えられるので、おそらくLinkedListは十分なパフォーマンスを持っているということなのでしょう。

次に以下のメソッドですが

//class EndpointReader in akka.remote.Endpoint.scala
  def sendBufferedMessages(): Unit = {
    ...
    val ok = writePrioLoop() && writeLoop(SendBufferBatchSize)
    ...
  }

このsendBufferedMessages()メソッドが呼ばれると、メッセージをためたバッファがクリアされ、メッセージがRemote Actor宛てに一気に送信されることになります。

EndPointWriterのなかではsendBufferedMessages()が繰り返し呼ばれて(EndPointWriter自身が呼び出しを決められた秒数毎にスケジュールしている)いることがわかります。

sendBufferedMessages()内で呼び出されるwritePrioLoop() も writeLoop(SendBufferBatchSize) も、バッファ内部の個別のメッセージを送る際には以下のwriteSend()メソッドを呼び出していて、

//class EndpointReader in akka.remote.Endpoint.scala
  def writeSend(s: Send): Boolean = try {
    handle match {
      case Some(h) ⇒
        ...
        val pdu = codec.constructMessage( ... )
        ...
          val ok = h.write(pdu)
        ...
  }

このhandleというのは

handle = Option[AkkaProtocolHandle]

となっていて、

          val ok = h.write(pdu)

をよびだすと、AkkaProtocolHandle traitのインスタンス(デフォルトではtcp向けの実装)を使って、実装されたプロトコルでメッセージを送ります。

AkkaProtocolHandle についても、いつか別の記事を書こうと思います。