White Box技術部

WEB開発のあれこれ(と何か)

勉強会おじさんになって思うこと

チーム内交流のために勉強会を開催していたら、なんか勉強会おじさんっぽくなってしまったので 今後の実施方法も考えて、少し現状を振り返ってみました。

なぜ勉強会を始めたのか?

そもそも勉強会を始めるきっかけだったのは、もちろん単純にやりたかっただけというのもありますが、 急激なチームメンバー増と、チーム内でのチーム分けにより、チームメンバーの交流機会が減り、 メンバー間で信頼関係の構築がなかなかできていないように感じたからです。

そこで、開発機能や案件内容にかかわらず、メンバーが集って交流できる場の一つとして、 勉強会を企画することにしました。

勉強会が信頼関係の構築になるのか?

勉強会のスタイルはいくつかありますが、現在実施している勉強会は、

『発表者がいて、他の人はそれを聞いて、場合によって質問する』

というスタイルです。

一般募集で行われる勉強会やカンファレンスでよくあるスタイルのあれです。
これだと発表者以外とは交流できないように感じますが、私にはちょっとした期待がありました。

同じ経験を共有することによる効果

映画館で好きな映画を観たとき、周りの人に不思議な親しみを感じることがありませんか?
別に誰かと会話をしたわけでもないのに、です。
同じ場に集って一つのことを経験・共有すること、ただそれだけで見知らぬ他人と共感できるなら 同じチームのメンバー間であれば会話せずにはいられなくなるのではないでしょうか?

結果的には

思惑通り、発表者への質問から会話が盛り上がったり、終わってから雑談が行われたりと、交流のネタになってくれました。 KPTのKeepでも、複数人から勉強会に関することが度々上がり、受け入れられているように感じました。

またgitの勉強会後では、業務中gitの操作で詰まった場合は、発表していた人に相談しているところも見受けられ、 発表者には仕事が増えて申し訳ないですが、聞かないことによって問題が発生する機会はなくなっていくため、 いい方向に向かっているように感じます。

ふと思いましたが、勉強会の間に今集まっているメンバーがどういう人なのか、 軽く紹介しているとより一層効果があるかもしれませんね。

勉強会の運営方法について

ここからは勉強会の運営自体に関する内容になります。

1. テーマ決め

まずは勉強会のテーマを決めます。

メンバーが興味を持ちやすいように、単純に技術の話と、現在の開発内容に関連した話で、 交互にテーマを変えるようにしています。(勉強会は週1で定時後1時間に実施しています)

テーマは事前募集したり、KPT内容からストックしておいたりしています。

2. 発表依頼

次に、テーマに即した内容を発表して貰えそうな人に、こんなこと話してもらえませんかとお願いに行きます。

これ大事!本当に大事!!発表者がいないと始まらないですし、一生懸命お願いしています。
ちなみに、このときお願いする発表者は、なるべく違う人になるように心掛けています。

なかなか発表者が確保できなそうであれば、自分が一人目の発表者になり、一緒にやりましょうという感じで誘っています。

3. 場所決め

そして、集まりそうな人数を想定して、開催場所を押さえます。

個人的な理想としては、普段の開発作業場の近くにオープンスペースがあって、 ちらっと立ち寄っていけるような感じでやれたらよいのですが、 なかなかそう上手いようにオフィスができているわけではないため、適当な広さの会議室を押さえます。

最近は参加人数に合った会議室が押さえられず、ここが頭を悩ませています。。

4. 参加者募集

最後に参加の募集と、発表・LTの募集をチャットと全体定例でします。

勉強会への参加は強制ではないので、参加して貰えるように勉強会の内容をまとめた周知ページを作成しています。
また、1回の勉強会は定時後1時間を目安にしているので、発表人数が不足していればLTネタがありそうな人に声をかけたりします。

5. 開催

開催の挨拶と今日のテーマについて軽く話して、順次発表に入ります。

メンバー交流という目的があるため、発表中のツッコミもOKとしているので、 そのことを予め話しておくのと、発表中に合いの手を入れたりして、 会話できる雰囲気作りを忘れないようにしています。

あと、写真も撮るようにしています。

6. まとめ

勉強会の周知ページに、発表資料と撮った写真を上げて、チャットで周知します。

これは参加できなかったメンバーにも好評のようなので、当日中にやってしまうといい感じでした。 感想等あればチャットのネタになりますし、話題になってる間ほど、次の発表をお願いしやすい気がします。

以上、繰り返し

勉強会のテーマ決めやスケジューリングは、大体2回先まで行うようにしています。

メイン発表者が1名だけだと、当日急遽都合がつかなくなった場合に開催できないので、 できれば2名以上に発表をお願いできているといい感じでした。

課題として感じていること

今のところ上手くいっているように見える勉強会ですが、個人的には課題も感じています。

人数について

最初は7人位だったのですが、すぐさま20人超えで集まるようになり、 いつの間にか部署をまたいでの参加者も出てきて、単純に場所の調整が難しくなってきました。

また、人数が増えるたことによって、参加を控える人が出てくることを懸念していたりします。

参加者が増えることは、交流の輪が広がり発表内容も膨らむため喜ばしいのですが、 チーム内交流という目的もあるため、チームメンバーが参加しやすいように、 テーマと発表者は偏らないように調整しないとダメですね。

主催者が自分だけ

今一番課題として感じているのは、主催者が私だけなところです。

私がいないから開催できない、ではよろしくないので、 勉強会の流れを定期的に周知しておき、私がいなくても参加者主導で進行できるように種を蒔いておきたいです。

そして勉強会おじさん問題

あんまり勉強会のことばかりを周知していると、「セリは勉強会のことしかやってないのではないか?」と思われてしまい、 私がチームの利益にならない人として認識され、そもそも勉強会に参加する人が減るのではないかという想いがあったりします。

これを防ぐには、業務で成果を出すしかないと思います。
そのため勉強会を主催する人は、業務+勉強会調整で単純に忙しくなります。

これはもうしょうがないですね。バランスを取りながらやっていくしかないでしょう。

それでも勉強会は良いです

色々ありますが、勉強会が開催できてよかったと思っています。

新しいことを知れたり、知識・考えを共有できたり、話のネタができたり、エンジニアならではの喜びが享受できました。 まだ始めて間もないので、定着するように頑張っていきたいと思います。

まあ、でもあれですね、あえて頑張りどころを言うなら・・・

「この人こんなにすごいんだ。あれ?なんかセリさんはリードエンジニアなのに感心してばっかじゃね?発表内容もしょぼくね?変わったほうがよくね?」

みたいなプレッシャーとの戦いですね!

【Groovy/ShangriLa】JsonSlurperとHTTPBuilderの使い方サンプル

引き続きShangriLaを例にしていますが、今度はGroovyでGETリクエストの実行と返却されたJSON文字列を処理してみました。

おや、Gradleサイトの様子が・・・!

README書くためにGradleのサイトに行ったところ、サイトがリニューアルしていました。

どうもgradle.comというコミュニティサイトっぽいものを作るから先行登録してね、 みたいなことを通知したいためのようです。 今のところサイトには簡単なGradleの使い方が載ってるぐらいで、 ほとんど情報はなく、このサイトがどうなっていくのかは不明です。

自分としては、ただGradleのインストール手順までの遷移がわかり辛くなったな、と感じただけでした。

なので、インストール手順が書いてあるページを以下にリンクしておきます。

HTTPBuilderでのGET方法

GroovyでHTTP通信をするのであれば、HTTPBuilderを使うのが良いと思います。

github.com

HttpBuilderの設定

build.gradleのdependenciesにhttp-builderを追加すると、対象のGradleプロジェクトから利用可能になります。

  • build.gradle
repositories {
    mavenCentral()
}

dependencies {
    compile ('org.codehaus.groovy:groovy-all:2.4.5') {transitive = false}

    compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
}

以前はbuild.gradleのrepositoriesに、HTTPBuilderが置いてあるMavenリポジトリのパスを2つ追記し、 以下のようにする必要がありましたが、現在はこの指定は不要になっているようです。

  • 昔の書き方
repositories {
    mavenCentral()
    maven {
        // for HTTP Builder
        url "http://repository.codehaus.org"
        url "http://snapshots.repository.codehaus.org"
    }
}

GETリクエストの送信方法

HTTPBuilderクラスのgetメソッドを呼ぶ方法と、requestメソッドで実行する方法があります。

記載の通りに実行すればGETリクエストを送信できますが、せっかくなので色々試してみました。

requestメソッドの利用方法

公式の記述のように、HTTPBuilderクラスの引数を指定せずに、requestメソッドの第1引数にホスト名を渡す方法でもできますが、省略して以下のように書くこともできます。

def http = new HTTPBuilder("http://api.moemoe.tokyo")
def resJsonText = http.request(GET, TEXT) {
    uri.path = '/anime/v1/master/cours'
    headers.Accept = 'application/json'
  
    response.success = { resp, json ->
        assert resp.status == 200
        def jsonString = json.text
        println("GET /anime/v1/master/cours json response: $jsonString")
        jsonString
    }

    response.'404' = { resp -> println "Access denied" }
    response.failure = { resp -> println "Unexpected failure: ${resp.statusLine}" }
}

ヘッダー情報も付ける必要がなければ付けなくても動作します。もちろん付けたければ公式のように色々付けれます。

ちなみにresponse.'404'response.failureのときのresJsonTextの戻り値はnullになります。何も返してないですからね。

はまりポイント

HTTPBuilderのパラメータや、requestメソッドの第1引数にホスト名以降を書くと、404になります。

また、uriの指定で先頭に/がないとエラーになります。

getメソッドの利用方法

これは公式にある通りに書けば大丈夫なので、上記のサンプルを書き直すと以下のような感じになります。

def http = new HTTPBuilder("http://api.moemoe.tokyo")
def resJsonText = http.get(
    path : '/anime/v1/master/cours',
    contentType : TEXT) { resp, json ->
        if (resp.status < 400) {
            def jsonString = json.text
            println("GET /anime/v1/master/cours json response: $jsonString")
            jsonString
        }
        else if (resp.status == 404) {
            println "Access denied"
        }
        else {
            println "Unexpected failure: ${resp.statusLine}"
        }
    }

結果を受け取ることだけを考えると記述量が減って簡単ですが、リクエストの成功と失敗の判断を自前でやることになるので、状況によって使い分けるべきですね。

switchで書いた方が綺麗かも。

GroovyでJSONを扱う

次に返却値のJSONを処理するために、groovy.jsonのJsonSlurperクラスを使います。
Scalaを書いた後だったからなのか、これ、すごく便利に感じました。Groovyの最強感がすごい。

JSON文字列をGroovyで扱えるJSONオブジェクトに変換する

JsonSlurperを使ってJSONオブジェクトを作るのはとても簡単です。
JsonSlurper#parseTextメソッドJSON文字列を渡すだけで完了します。

def jsonSlurper = new JsonSlurper()
def resJsonObj = jsonSlurper.parseText('{"3":{"id":3,"year":2014,"cours":3}}')

こうして作成したJSONオブジェクトは、JSONデータをMapで表現するため、直感的に扱うことができます。
Mapなので、キーを得たい場合はresJsonObj.key、値を得たい場合はresJsonObj.valueで取得できますし、 キーを指定して値を得たい場合はassert resJsonObj.value.year == 2014のように、 キーの要素で指定するだけです。ポイントはvalue値がダブルクォート無しの数値であれば、ちゃんと数値型で取り出せるというところでしょうか。なので文字列比較にしていると失敗します。

今回のサンプルコード

今回の解説で利用したサンプルコードのベースとなるGroovyプロジェクトは、以下になります。こっちではもうちょっと色々やっているので、ぜひこちらもご覧ください。

github.com

正月早々

帰省中にプログラミングしようと思っていたのですが、このサンプルを書いたところで風邪でダウンしてしまいました。 今年は家に引きこもるのもほどほどにして、少し体力をつけて病気に負けないようにしたいです。

そして、久しぶりにGroovyでコード書きましたが、びっくりするぐらいに書きやすかったです、Groovy。
前回、ScalaJSON操作するのに四苦八苦したことを考えると、JsonSlurperの直感性は感動的でした。
なかなかプロダクトで使うのは難しい言語ですが、個人的にはこれからも使っていくので、都度トピックを紹介させて頂こうと思っています。

もし興味を持っていただけたのであれば、試しに今まで書いたJavaのコードをGroovyに変換してみてください。そうすると魅力に気付きやすいかと思います。

そして慣れてきたら、是非Spockを使ってGroovyでテストコードを書きましょう!

2015年の振り返り

2015年を振り返って

去年に引き続き、今年も本年を振り返ります。

ブログ関連

まずはブログ関連の話から。

ブログへのアクセス数

今月のアクセス数が今のところ12,003で、現在のトータルアクセス数が123,140です。
去年の同月時点では、月が5,432、トータルが14,535でした。

去年までのトータルアクセス数に近いアクセスが、ひと月で出るようになりました。すごい。
相変わらずアクセスはPowerDVDのエントリがほとんどですが、その他だと以下のエントリが今年アクセスの多かったページになります。

ブログで書いた内容について

前半はJavaのテストのことばっかり書いていた気がします。
今の会社/チームはあまりテストを書く習慣がなく、テストへの考え方もメンバーそれぞれなので、 来年はここらへんをいい感じに意識共有できたらいいなと考えています。 なので、相変わらず記事の内容はテストかもですね。

あと、今はチーム内のインフラ・ミドルウェア関連の作業を担当しているので、ここら辺の情報もまとめていければいいかなと考えています。

私生活

今年は私生活でも色々ありました。

転職

まずはなんといっても転職でしょうか、だいぶ変わりました。これなくして今年は語れないですね。
SIerからWEB屋への転職で、良い点悪い点色々ありますが、総評的にはSIerは二度とやらないかなーと思った次第です。ここら辺の違いは覚えているうちにまとめます。

確かにWEB屋の方がプレッシャー強い気がしますが、能力へのプレッシャーなので、むしろいい刺激になっています。残業時間も減りましたし、精神的には余裕があるように感じています。
SIerのときのプレッシャーは、精神へのプレッシャーだったみたいですね。転職前はあれだけ飲んでいた頭痛薬を全く飲まなくなりましたし。

あと、音楽聴きながら仕事できるのがすごくいいです。これで作業がコーディングだったらいくらでも仕事ができてしまう。

勉強会への参加

結構勉強会にも出てました。
記事にしたのはDroidKaigiだけでしたが、以下の勉強会に参加しました。

しかしDotsは発表資料がページにリンクされていないのが微妙で、なんとかして欲しいです。。

全部転職してからですね。参加するの控えてコード書いてるほうがいいかなぁと思っていた時もありましたが、週末1日潰れるくらいならそれほど影響なかったので、スケジュールの都合が付く間は出るようにしたいなと思います。

あ、JJUGで生ひしだまを見れて感動しました。本当に実在していたとは・・・
しかもちょうどどうしようかと考えていたバッチの話をしてくださったので、今度Asakusa Frameworkを調査しようと思いました。やはりこういう気付きがあるので、いいですね、勉強会。

社内でも勉強会やってます

社外だけでも月1で出てるのでだいぶ出ている感じが出するのですが、 今の会社は社内でも勉強会をやっているので、勉強会というものにはもっと参加しています。

とかに参加しました。知見が広がって楽しいです。 更に、チーム内でも私主催で開催しているので、去年までとは参加率が比べ物にならないですね。

そして社外へ

来年は社外でも発表・LTするようにしていきたいと考えています。
効率よく開発するためにはちょっと名前を売っておかないと、調整作業が頻発してしまってよろしくないなぁと思う機会が多くなったのが契機ですね。チームのためにも、少し泊を付けておかないといけないかなと。

なので機会があれば発表していこうかなと思っています。ネタも溜まってきましたし。

直近では、2月のデブサミにコミュニティの手伝いで参加しますので、 私が発表するとかではないですが、ご都合がつく方は是非ブースまでいらしてください。

仕事

てっきりJava関連の仕事になるかと思っていましたが、なぜかPHP開発のインフラ関連の仕事をしています。
予想外のアサインでしたが、良い機会だと思って勉強しながらやってます。まあ思えば似たようなことはやってましたし。

ただ、使っているミドルウェアがJenkins以外は今までと全然違うので、早く基礎知識を習得して、最適解が出せるようになりたいところです。

Slackの通知を受け取っているので、フルタイム社畜とか言われそうですが、 特に不満とかはなくて、許可してもらっている分、セキュリティ的に気をつけないとなぁと思う感じです。 まあでもこれなら個人の端末購入費をサポートしてくれてもいい気がしますがw

で、コーディングできているかというと

立場的には、標準化とかプラットフォーム設計をすることになっているため、コーディングする機会は今のところないですねー。

とはいえ、急に書くことになったりしますし、最適なアーキテクチャを考えるためにも、まだまだ勉強が必要だなと感じています。
直近でも、急にPHPだったりSQLだったりScalaだったりPythonだったりCoffeeScriptだったりでコードを書くことになったりはしたので、それほど不満があるわけではないです。

今年はFoveも出るので、C#もやっていかないとですね。

総括

こんなところでしょうか?
転職してからの日々は、あっという間でした。
その間、色々吸収できていることもあると思うので、 しっかり成果を残していけるように来年も頑張っていこうと思います。

といったとことで、今年一年のご愛読ありがとうございました。
来年もよろしくお願い致します。

【Scala/ShangriLa】秋アニメ情報のJSONをGETリクエストで取得する

秋アニメは何観てますか?

こんなグラフを見たことある方はいないでしょうか?

実はこれ、友達が作成して公開しているものだったようです(知ったときは驚きました)。 グラフ作成のためのデータを取得するAPIも公開していて、これが今scalaベースで動いています。

qiita.com

どうも最近彼はこの個人プロジェクトに情熱を傾けているので、色々話を聞いていたら、いつの間にか自分も参加していました(ぇ

というわけで、今回はSora APIを叩くScalaのサンプルコードについてです。

ちなみに私は「すべてがFになる」を観ています

原作を読んだのは大学生の頃で、その頃はこれがアニメになるとも、将来それを自分が観てるとも思わなかったですが、相変わらずコードは書いているので、不思議な連続感を感じています。

書いてるコードはCからScalaに変わっていましたが。

sbt設定の手直し

さて、さっそくScalaのコードをEclipseで書こうと思ったのですが、sbtがエラーを吐くのでその修正から行うことになりました。
しかし、sbt使おうとするといつも不具合対応しているような気がします。利用する機会が不定期なのが悪いんですかね。

ちなみに実行環境は以下の通りです。

ユーザホーム\.sbt\repositoriesのfileプロトコル指定が間違っている

sbtコマンドを実行すると、以下のようなエラーになりました。

$ sbt
java.lang.IllegalArgumentException: URI has an authority component
        at java.io.File.<init>(File.java:423)
        at sbt.Classpaths$.sbt$Classpaths$$bootRepository(Defaults.scala:1758)
        at sbt.Classpaths$$anonfun$appRepositories$1.apply(Defaults.scala:1729)
        at sbt.Classpaths$$anonfun$appRepositories$1.apply(Defaults.scala:1729)
        at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
        at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
        at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33)
        at scala.collection.mutable.WrappedArray.foreach(WrappedArray.scala:34)
        at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
        at scala.collection.AbstractTraversable.map(Traversable.scala:105)
        at sbt.Classpaths$.appRepositories(Defaults.scala:1729)
        at sbt.Classpaths$$anonfun$41.apply(Defaults.scala:1102)
        at sbt.Classpaths$$anonfun$41.apply(Defaults.scala:1102)
        at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47)
        at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47)
        at sbt.EvaluateSettings$MixedNode.evaluate0(INode.scala:175)
        at sbt.EvaluateSettings$INode.evaluate(INode.scala:135)
        at sbt.EvaluateSettings$$anonfun$sbt$EvaluateSettings$$submitEvaluate$1.apply$mcV$sp(INode.scala:69)
        at sbt.EvaluateSettings.sbt$EvaluateSettings$$run0(INode.scala:78)
        at sbt.EvaluateSettings$$anon$3.run(INode.scala:74)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)
[error] java.lang.IllegalArgumentException: URI has an authority component
[error] Use 'last' for the full log.
Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? 

以下に書いてありましたが、repositoriesファイルの中の記述にダメなところがあるようです。
ファイル指定のスラッシュが足りないようなので、file://をfile:///に修正したらエラーは消えました。

  • 修正対象ファイル
    C:\Users\ユーザ名.sbt\repositories

  • 参考
    stackoverflow.com

Permanent指定のオプションを無効化

これは別に不具合ではないのですが、Java 8からPermanent領域が消えているので、 sbtconfig.txtからMaxPermSizeオプションを外します(付けているとWarningが出るので)。

  • C:\Program Files (x86)\sbt\conf\sbtconfig.txt
変更前:-XX:MaxPermSize=256m
変更後:# -XX:MaxPermSize=256m

オプションについての詳しい話は、以下が参考になります。

equj65.net

Eclipseプラグインの有効化

前は普通に実行できていた気がするのですが、 sbt eclipseコマンドもエラーが出たので、公式の記述の通りに対応しました。

  • 対処法
    C:\Users\ユーザ名.sbt\0.13\pluginsにplugins.sbtファイルを作成して、以下を書き込み
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")

その後、Eclipseプロジェクトを作成するフォルダ配下でsbt eclipseを実行。

sbt、まとめました

ここら辺のことはQiitaにまとめました。 新規インストール時の参考にどうぞ。

qiita.com

Scalaプログラミングをするぞ!

これでScalaを書く準備ができました。

私のScalaの先生は、コップ本(第1版)とScala逆引きレシピなのですが、Scalaのバージョンアップの影響をどちらも受けていて、 書いている通りにやろうとすると結構な率でハマります。そして言うまでもなく、今回もしっかりハマりました。

逆引きレシピの改訂版が待ち遠しいです。

Sora APIの利用サンプル(Scala

以下がアニメ情報を取得するSora APIを、Scalaを使ってGETで叩き、JSONデータを取得するサンプルコードです。

github.com

APIのサンプルというよりは、Play JSON libraryDispatchのサンプルみたいになってしまいました。

Dispatchは予めハマっていたので今回はそれほどハマらなかったのですが、 Play JSONJSONからScala変換で結構ハマりました。

以下がそのJSON to Scalaを実行している箇所のコードを抜き出したものになるのですが、

  • JsValueはListとして保持されているので、一度Listから取り出して処理をしないといけない。
  • JSONのvalue値を文字列比較で一致を見るときは、そのまま文字列で比較するのではなく、JsStringのオブジェクトを使って比較すること。
  • JSONのvalue値をStringに変換しても、ダブルクォートが除去されず、文字列としてくっついたまま返却されてくること。

これらのことに気をつけて、やっと得たかった情報を得ることができました。

    case Success(content) => {
      val responseJson = Json.parse(content)
      println("GET /anime/v1/master/2015/2 json response" + responseJson.toString())

      val animeInfo = responseJson.as[List[JsValue]] map { seed =>
        seed.as[Map[String, JsValue]]
      }

      // 取得した情報から、「てさぐれ!部活もの すぴんおふ プルプルんシャルムと遊ぼう」の情報を利用する場合
      val tesagure: List[Map[String, JsValue]] = for (
        anime <- animeInfo if anime("title_short1") == JsString("てさぐれ!部活もの")
      ) yield anime

      println("\n「てさぐれ!部活もの すぴんおふ プルプルんシャルムと遊ぼう」 のアニメ情報")
      tesagure.head.toSeq.sortBy(_._1).foreach { parameter =>

        // valueをString変換してもダブルクォートが付いたままなので、表示前に除去する
        val value = parameter._2.toString().replaceFirst("""^\"(.*)\"$""", "$1")
        println(s"""  ${parameter._1} : ${value}""")
      }
    }

また、これらの処理はonComplete式中で実行しているのですが、そうなると非同期処理として実施されるため、 このままだとEclipseで実行したプロセスが終了せずに残り続ける問題がありました。

そこで今回は簡単な対処として、2秒待ったらプログラム終了、というコードを最後に入れています。

  // 非同期処理の終了
  Thread.sleep(2000)
  sys.exit(0)

最後のsys.exit(0)sys.exit()でも同じ挙動のはずです。

しかし、これはもうちょっとスマートに対応したいものですね。いい方法が見つかったら更新しておきます。 他にいい方法があるよ!という知見を持っている方がいればプルリク等で指摘いただけると助かります。