UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

ペアプロと育休の取得しやすさの関係について

こんにちは。SPEEDA開発チームの鈴木です。

昨年一児(娘)の父になりまして、凄い勢いで変化していく様子に喜んだり困ったりしながら過ごしております。
色々できることが増えると嬉しいのですが、それは同時にいたずらの幅が広がることも意味するんですよね。例えばものを引っ張ることを覚えたのは嬉しいのですが、私の髪の毛をひっぱってむしるのはやめていただきたい。そんな感じです。

f:id:kenji-suzuki:20191014235944p:plain:w250
髪をむしる娘の図。言葉は通じない。

今回はそんなうちの子が産まれたときに取得した育休の話をしたいと思います。
育休の話とはいっても育休をハックする話とか育児アプリとかの話ではなく、「育休を取得しやすい(と私は思う)SPEEDA開発チームの環境」についての話をします。
本編がそこそこ長い(スミマセン)ので前置きはここら辺で切り上げることとします。

男性の育休取得率

さて突然ですが、2018年度の日本における男性の育休取得率がどのくらいかご存知でしょうか?

6.16%です。

これは過去最高の取得率だそうです。
ちなみに同年の女性の育休取得率が82.2%だそうなので、過去最高とはいえども残念ながらまだまだ低い数値といえるのではないでしょうか。

このような状況の育休ですが、SPEEDA開発チームでは昨年私を含めた2人が育休を取得しています。
母数が少ないので一概に「育休取得率が高い」とは言えないですが、間違いなく育休を取得しやすい環境であると思いますし、だからこそ私は育休を取得したので、今回は私たちの環境の紹介を兼ねて次のような話をしようと思います。

1. 男性が育休を取得しなかった理由  
2.「1」の理由の原因を考える
3.「2」の原因を解決するにはどうしたらいいか  
4. SPEEDA開発チームにおいてはどうしているか  

男性が育休を取らなかった理由

では、まず一般的にどういう理由で育休が取られていないのか、2018年度のデータを見てみましょう。

f:id:kenji-suzuki:20191003170319p:plain
育休を取得しなかった理由2018

これを見ると、男性が一体どういう理由で育休を取得しなかったかがわかります。
(出展: 三菱UFJリサーチ&コンサルティング「平成29年度仕事と育児の両立に関する実態把握のための調査研究事業」)

Top3はこのようになっています。

1位: 業務が繁忙で職場の人手が不足していた(38.5%)
2位: 職場が育児休業を取得しづらい雰囲気だった(33.7%)
3位: 自分にしかできない仕事や担当している仕事があった(22.1%)

今回はこのうち2位の「職場が育児休業を取得しづらい雰囲気だった」と3位の「自分にしかできない仕事や担当している仕事があった」にフォーカスして話を展開したいと思います(人手不足の話は採用や業務効率化に絡んだ話だと思いますが、そこについては別軸の問題だと思いますので今回は言及しません)。

育休を取らなかった理由の原因を考えてみる

次に、上記2つの理由について私なりに原因を考えてみたいと思います。

職場が育休を取得しづらい雰囲気になっている原因

なぜ育休を取得しづらい雰囲気になってしまっているのでしょうか。
原因を私なりに考えてみましたが、このようなことが考えられるのではないかと思います。

1.普通の休暇すら取りづらい
普通に1日2日の休暇を取ることが難しい環境の場合、ある程度まとまった期間になるであろう育休は余計取りづらく感じることかと思います。
どういう場合に休暇が取りづらくなるのか、2つ思い当たります。
1つ目は、自分が担当している仕事に期限があり、休むことで間に合わなくなったり、後々辛くなったりと自分が困るケースです。
2つ目は、自分が休むと自分の仕事を周囲の誰かが余分に担当することになり、迷惑がかかるケースです。
1つ目のケースで休んだ分を自分でカバーできずに間に合わなくなった場合は、2つ目のように他の誰かがフォローにまわり結局周囲の負荷が増えることが考えられます。
そういうことを考えていくとやはり休みづらくなるのではないでしょうか。
また、こういった懸念は想像に過ぎず実際は自分が休んだところで大した迷惑はかからないのかもしれませんが「安心して休める仕組み」が整っていなければ心理的に休みが取りづらいということはあるでしょう。

2.周りの人が普通の休暇すら取らない
自分だけではなく、周りの人も「1」と同じように自分が困ったり、周囲への迷惑を気にして休みを取得しないような環境では休みづらくなるかと思います。
お互いをお互いで縛り合っている感じです。同調バイアスによる負の連鎖と言えるかもしれません。
休暇と同様に、子供が産まれたのに誰も育休を取得しないような環境では遠慮してしまう(遠慮しなければならない気持ちになる)のではないでしょうか。

3.男性の育休に理解がない
周りの人たちが男性の育休に対して理解がない場合、育休は取得しづらい雰囲気になるでしょう。

どうすれば育休を取得しやすい環境になるか

私が挙げた原因はどれも環境が原因となっていますが、どうしたら育休を取得しやすい雰囲気を作れるでしょうか。
「1」は色々な解決方法があるでしょうが、上述のとおり休んだ場合に自分を含めて誰も困らないような「安心して休める仕組み」があれば解決しそうです。
「2」の解決には「1」の仕組みの存在に加えて実際その仕組みが働いている必要があります。
「3」は「1」の仕組みで部分的に解決しそうです。なぜなら「3」のような人たちが育休に反対する理由は”育休を取得することによって自分や他の人たちに迷惑がかかる”ということにあったりするからです。
「誰かが迷惑するから育休には反対」とは言えても「誰にも迷惑はかからないが育休には反対」とは言えないのではないでしょうか。

このように考えてみると 「安心して休める仕組み」が存在して運用されていることが、育休を取得しづらい雰囲気を解消する方法の一つになりそうです。

安心して休める仕組みとは

休みを取得しづらい理由として「休むと自分が困る」というものがありました。
なぜ自分が休むと困る状況になっているのでしょうか。それは「仕事が個人に対してアサインされている」からではないでしょうか。
そもそも自分がやらないといけない仕事なので、誰かに代わってもらいづらいというわけです。
このような環境では他の人も同様に「自分の仕事」をもっているでしょうから。
では「仕事がチーム対してアサインされている」ならば問題はないのでしょうか。
いいえ。 チームで仕事をしていても、仕事が属人化しないようにしないと今回の文脈では結局困ることになります。
仕事が属人化していると、計画的に休む場合は引き継ぎが発生するでしょうし、突発的に休むなら引き継げない分を自力で調査したりする手間が発生するからです。
ですので「仕事がチームに対してアサイン」されており、属人化も防がれていなければなりません。

SPEEDAの開発チームではどうしているか

SPEEDAの開発チームでは基本的に「仕事はチームに対してアサイン」されます。
そして「ペアプロ」が属人化を防ぐのに役立っています(やっと本題!)。
(属人化を防ぐためだけにペアプロをしているわけではないことを補足しておきます。ペアプロには属人化の軽減以外にも沢山のメリットがあるんです。ただ今回の話では「属人化」の観点にフォーカスします。)

ペアプロについて具体的に

SPEEDAの開発チームは、すべてのチームがほぼ100%ペアプロで作業しています(ペアプロそのものについての詳しい説明は割愛させていただきます) 。
そしてプログラミング以外(例えば採用活動)でもペア作業をします。
更にペアを組むメンバーを一日のうち何度も入れ替えており、様々なメンバーと様々な開発ストーリーに取り組むことになります。
領域もUI/UX含めたフロント周りから、バックエンド、インフラ、CI/CDなどすべてを皆で担当します。
その結果として、チームの中で「フロント部分はAさんしか知らない」「バックエンド部分はBさんしか知らない」という属人化が起きなくなっています。

チームの人数が多く、かつ小規模なストーリーの場合、自分が担当する前にストーリーが終わってたということはありますが、自分しか知らないという状況にはなりません。
またチームの人数が奇数の場合は一人作業※が発生しますが、その場合は一人でも十分なストーリーを選ぶような工夫をしています。 ※一人で作業するというのは普通かもしれませんが私たちの場合ペアが基本なため一人は特別な感じです。
そのため、誰かが休んで困るということが基本的にありません。
なので用事があれば休みますし、周りがそうなので上述のように「周りが休まないから休みづらい」ということがありません。

ペア作業のイメージ

ここで補足としてペア作業についてイメージしやすいように絵で表してみることにします。
あるチームにメンバーとして、Aさん、Bさん、Cさん、Dさんがいるとします。
f:id:kenji-suzuki:20191006232514p:plain

ペアは、AさんとBさん、CさんとDさんという組み合わせとします。
環境はこのような感じです。

f:id:kenji-suzuki:20191014011451p:plain
ペアプロ環境

一つのデスクトップPCにモニター2つとキーボード2つが接続されています。
マウスが見当たらないのは画像の手抜きではなく、Thinkpadのトラックポイント付きキーボードを使っているためです。
各々に対してキーボードが存在するため、ドライバーとナビゲーターの役割がスムーズに交代できます(キーボードが一つしかなかったら、心理的にキーボードを渡してもらったり渡したりというのがやりづらくなるかと思います)。
モニターはミラーリングされていたり左右で分割されていたり、ペアによってやりやすいように変えています。

CさんDさんペアも同じ構成で作業します。
f:id:kenji-suzuki:20191014011751p:plain

作業の流れ

時間は13時、AさんとBさんペアはストーリーXに着手します。CさんとDさんペアはストーリーYに着手します。
f:id:kenji-suzuki:20191006232845p:plain

時間が14時になりました。ここでペアを入れ替えます。1コマの時間(ペア交代までの時間)は、チームで話し合って決めます。この例では1時間で交代するものとします。今度はDさんAさんペア、BさんCさんがペアになりました。横にスライドする形ですね。
f:id:kenji-suzuki:20191006234446p:plain
入れ替えの際は、作業開始時にいまどういう状況なのかの共有が行われることが多いです。
(省いていますが、本当は適宜休憩を入れます(休憩超大事!))
また、OSやディレクトリ構成などは意図的に統一しているため、別のマシンに使ったとき困りません。

時間が15時になりました。またペアを入れ替えます。CさんDさんペアと、AさんBさんペアです。
f:id:kenji-suzuki:20191006235005p:plain
これで全員がストーリーX、ストーリーY両方に少しずつ関わったことになり、特定のメンバーしかわからないということがなくなりました。
複数のストーリーに少しずつ関わるということは、一つのストーリーに100%関われないことも同時に意味し、自分の知らないコードが存在し得るようになります。
こういった問題に対しては、他のメンバーに伝えた方がよさそうなことについて(例えば設計の部分)は都度々々共有したり相談したりしながら進めることで対応しています。
メンバーが増えていくとこういった共有も大変になってきたり、上記のペアの入れ替え方法だとペアを組めない人たち(例えばAさんとCさんはペアになれない)が出てきたりと、ペア作業については話題が尽きないのですが、今回の趣旨はペア作業によって属人化が軽減されているということであるため割愛させていただきます。

チームの入れ替え

チーム内でペアの入れ替えを行っていることは上述しましたが、チームをまたいだメンバーの入れ替えも行われています。
例えばXチームとYチームという2つのチームがあったとします。 このチームに対し、それぞれが担当している仕事はそのままに、メンバーを一部入れ替えるのです。 f:id:kenji-suzuki:20191007004951p:plain

↑を↓のように入れ替える。

f:id:kenji-suzuki:20191007005034p:plain
入れ替えの頻度は決まっておらず、リソース調整の結果であったり、タイミング的なものであったりします。
また、基本的には本人の意思が最大限に尊重され本人が希望するチームに移動することになります。強い理由があって同じチームに長く残るということもあります。
このような入れ替えが行われることにより、SPEEDAの開発チームにおける属人性は極めて少なくなっているといってもよいかと思います。
※ペアプロと同様に属人化を防ぐためだけに入れ替えをしているわけではなく、知見の共有であったりチームに新たな風を起こしたりと他にも色々メリットがあるから入れ替えが行われています。

最後に

私がSPEEDA開発チームにおいて、育休を取得しづらいとは思わなかった理由は、ユーザベースという会社自体も周りの人たちも男性の育休に理解があったということもありますが、ペアプロとチームの入れ替えによって「属人化」が軽減されており「自分が休むと誰かが困る」という心理的負担がなかったことも大きいと思います。
今回は主に「属人化」という点にフォーカスしてペアプロの話をしましたが、ペアプロがもたらす良い作用は他にも沢山あるので、(いきなりは難しいかもしれませんが)興味がわいた方は是非ペアプロを取り入れてみてはいかがでしょうか。
いきなりガッツリではなく、小規模に始めたり、短時間やってみたりするだけでも「良さ」がわかるかもしれません。

Ringアプリケーションで例外をいい感じにハンドリングする方法(Ductでの解説も含む)

こんにちは!こんにちは!SPEEDA開発チームのあやぴーです。 社内のClojureを使ったAPIにおいて、「例外をうまくハンドリングしたいんだけど…」という話が出てきたので、今回はRingアプリケーションにおける例外のハンドリング方法について解説します。また、昨今Ductを使ってアプリケーションを作る機会が増えているので、それについては最後の方に解説をします。

また具体的なコードはGitHubにあるので、参考にしてみてください。

https://github.com/ayato-p/exception-handling

この記事で解説する具体的な内容については以下の通り

  • 例外のハンドリングにRingミドルウェアを使う
  • 例外によってレスポンスを変更する
  • Ductアプリケーションへの適用方法
  • まとめ

例外のハンドリングにRingミドルウェアを使う

Ringアプリケーションの特にRingハンドラー(以下、ハンドラー)部分で例外が出る場合について考えてみます。簡単のために以下のように、例外を投げる処理do-somethingを実行するハンドラーthrow-exception-handlerがあることにします。

(ns demo.core
  (:require [ring.util.response :as response]))

(defn- do-something []
  (throw (ex-info "err" {:exception/type :server-error})))

(defn throw-exception-handler [req]
  (let [res (do-something)]
    (response/response res)))

本来であればdo-somethingが適当な値を返して、それをハンドラーのレスポンスとして返すところですが、この例ではdo-somethingが例外を起こすため、適切なレスポンスを返すことができません(Ringアダプターの実装にもよりますが、多くの場合は500エラーが返ると思います)。このハンドラーの挙動は以下のテストで確認することができます。

(ns demo.core-test
  (:require [clojure.test :as t]
            [demo.core :as sut]
            [ring.mock.request :as mock]))

(t/deftest throw-exception-handler-test
  (t/testing "throw-exception-handlerが例外を返すこと"
    (let [req (mock/request :get "/err")]
      (t/is
       (thrown-with-msg? clojure.lang.ExceptionInfo #"err"
                         (sut/throw-exception-handler req)))

      (let [data (try
                   (sut/throw-exception-handler req)
                   (catch clojure.lang.ExceptionInfo e
                     (ex-data e)))]
        (t/is (= {:exception/type :server-error}
                 data))))))

このようにハンドラーの中で例外が発生する場合、各ハンドラーやその手前のレイヤーで適切にハンドリングして、例外をハンドラーの外側に伝播させないというのも場合によってはやると思います。しかし、予期しない例外や一般的な例外などに対してすべてのハンドラーで対応するのはあまりにも面倒です。そういった例外をハンドルするための、いい感じの機能が欲しいところです。

Javaの場合、JAX-RSのExceptionMapperあたりが今回欲しい機能に該当しそうです。Ringには残念ながら同じようなものはありません。そのため、少しだけ考えてみることにします。上記のテストコードがヒントになりそうです。上のコードのようにthrow-exception-handlerにリクエストマップを渡すようなコードをtry~catchで囲んで、例外を吐き出したときのみ例外のレスポンスマップを返すように書くことを考えれないでしょうか。

(try
  (throw-exception-handler req)
  (catch Exception e
    (-> (response/response "Internal Server Error!!")
        (response/status 500))))

このようにハンドラーを実行して、その結果次第でレスポンスに影響を与える方法を僕らは既に知っているはずです。Ringミドルウェア(以下、ミドルウェア)です。次のようなミドルウェアwrap-exception-handlerを考えてみます。

;; demo.core
(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (-> (response/response "Internal Server Error!!")
            (response/status 500))))))

これはハンドラーを受け取って、新しいハンドラーを返します。新しいハンドラーは元のハンドラーに対して、自身が受け取ったリクエストを適用するだけで基本的には何もしませんが、例外が投げられたときにそれをキャッチして500のステータスコードを返すようにしています。

wrap-exception-handlerは以下のテストコードで機能していることを確認することができます。

;; demo.core-test
(t/deftest wrap-exception-handler-test
  (t/testing
      "例外を投げないハンドラーが実行されたら何もせずに元の結果を返すこと"
    (let [req (mock/request :get "/err")
          handler (constantly {:status 200
                               :body "Hello, world"})
          app (sut/wrap-exception-handler handler)]
      (t/is (= {:status 200
                :body "Hello, world"}
               (app req)))))
  (t/testing
      "例外を投げるハンドラーが実行されたらステータスコード500のレスポンスを返すこと"
    (let [req (mock/request :get "/err")
          handler (fn [_] (throw (Exception. "err")))
          app (sut/wrap-exception-handler handler)]
      (t/is (= {:status 500
                :body "Internal Server Error!!"
                :headers {}}
               (app req))))))

例外によってレスポンスを変更する

例外をハンドルすることができたので、今度は例外の種類によって返すレスポンスを変化させてみます。既に想像がついていると思いますが、try~catchで掴む例外を複数用意すれば簡単にレスポンスを変えることができます。やってみましょう。

以下ではIllegalArgumentExceptionのときに500clojure.lang.ExceptionInfoのときはex-data:exception/typeの値によってステータスコードを変えています。また、対応できていない例外に対しては500を返しつつも、標準出力に"Unhandled Exception:"と流すようにしています。

(t/deftest wrap-exception-handler-test
  ;; "例外を投げないハンドラーが実行されたら何もせずに元の結果を返すこと"

  (t/testing
      "例外を投げるハンドラーが実行されたら適切なエラーコードを返すこと"

    (t/testing "IllegalArgumentExceptionのとき500を返す"
      (let [req (mock/request :get "/err")
            handler (fn [_] (throw (IllegalArgumentException. "err")))
            app (sut/wrap-exception-handler handler)]
        (t/is (= {:status 500
                  :body "Internal Server Error!!"
                  :headers {}}
                 (app req)))))

    (t/testing "対応できていない例外は500を返す"
      (let [req (mock/request :get "/err")
            handler (fn [_] (throw (NullPointerException. "err")))
            app (sut/wrap-exception-handler handler)]
        (t/is (= {:status 500
                  :body "Internal Server Error!!"
                  :headers {}}
                 (app req)))
        (t/is (str/starts-with?
               (with-out-str (app req))
               "Unhandled Exception: java.lang.NullPointerException"))))

    (t/testing "ExceptionInfo"
      (t/testing ":exception/typeが:server-errorのとき500を返す"
        (let [req (mock/request :get "/err")
              handler (fn [_] (throw (ex-info "err" {:exception/type :server-error})))
              app (sut/wrap-exception-handler handler)]
          (t/is (= {:status 500
                    :body "Internal Server Error!!"
                    :headers {}}
                   (app req)))))

      (t/testing ":exception/typeが:not-foundのとき400を返す"
        (let [req (mock/request :get "/err")
              handler (fn [_] (throw (ex-info "err" {:exception/type :not-found})))
              app (sut/wrap-exception-handler handler)]
          (t/is (= {:status 404
                    :body "Not Found!!"
                    :headers {}}
                   (app req))))))))

こうなるようにwrap-exception-handlerを実装すると、以下のようになります。

(defn- internal-server-error-response []
  (-> (response/response "Internal Server Error!!")
      (response/status 500)))

(defn- not-found-response []
  (-> (response/response "Not Found!!")
      (response/status 404)))

(defprotocol ExceptionToResponse
  (->response [e]))

(extend-protocol ExceptionToResponse
  Exception
  (->response [e]
    (println "Unhandled Exception:" (type e))
    (clojure.stacktrace/print-stack-trace e)
    (internal-server-error-response))

  IllegalArgumentException
  (->response [e]
    (internal-server-error-response))

  clojure.lang.ExceptionInfo
  (->response [e]
    (let [{t :exception/type} (ex-data e)]
      (case t
        :server-error
        (internal-server-error-response)
        :not-found
        (not-found-response)))))

(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (->response e)))))

500404のレスポンスだけの関数(internal-server-error-response, not-found-response)を作りました。また実際に例外をふりわけるところはExceptionToResponseというプロトコルに任せて、それぞれの例外ごとに実装をできるようにしています。これによってwrap-exception-handlerでやっていることは非常に明快になりました。

このようにRingミドルウェアを応用することで、例外ごとに任意のレスポンスを返すことができました。

Ductアプリケーションへの適用方法

上記のwrap-exception-handlerを適用するDuctアプリケーションに適用するのは非常に簡単です。まずはプロジェクトを用意するところから。次のコマンドを使ってDuctプロジェクトの雛形を作ります。

$ lein new duct demo --to-dir exception-handling-api -- +api +ataraxy

今回はAtaraxyというデータでルーティングを記述できるライブラリを利用します。これはDuctで既にモジュール化されているため、簡単に使い始めることができます。

次に先程のwrap-exception-handlerを使って、Ductから使えるコンポーネントを用意します。

(ns demo.middleware.exception-handler
  (:require [ring.util.response :as response]
            [integrant.core :as ig]))

;; 中略

(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (->response e)))))

(defmethod ig/init-key :demo.middleware/exception-handler [_ _]
  wrap-exception-handler)

wrap-exception-handlerの実装については、既に説明したものをそのまま利用します。そして、ミドルウェアのコンポーネント:demo.middleware/exception-handlerでは、単にwrap-exception-handlerを返すようにします。

次に、このミドルウェアを適用するには、config.ednに以下のような記述をします。大事なところは:duct.module/ataraxyのキーに対応するマップです。具体的な記法の説明については、Duct module.ataraxyのREADMEに譲りますが、Ataraxyのシンタックスでいうresult部に対してメタ情報として任意のミドルウェアを指定することで先程のミドルウェアのコンポーネントを適用することができます。

{:duct.profile/base
 {:duct.core/project-ns demo

  ;; Middlewares
  :demo.middleware/exception-handler {}

  ;; Handlers
  :demo.handler.throw-exception/not-found {}
  :demo.handler.throw-exception/server-error {}}

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/ataraxy
 {"/" ^:exception-handler
  {"not-found" [:throw-exception/not-found]
   "server-error" [:throw-exception/server-error]}}

 :duct.module/logging {}
 :duct.module.web/api
 {}}

まとめ

ClojureでWebアプリケーションを作っても、簡単に例外のハンドリングはできるよ!

【kubernetes / Helm】大量のCronJobに悩む貴方に送るプラクティス

はじめに

こんにちは! UZABASE SPEEDA SRE teamの生賀id:skikkh(@skikkh)です。

最近あった嬉しかったことは、自分が翻訳した日本語がkubernetesのCronJob - Kubernetesページに反映されていたことです。

閑話休題、弊社SPEEDAサービスでは大量のバッチジョブがHinemosを起点としてVM上で動作しています。 SREチームではこのようなジョブ群を徐々にサーバから切り離して、コンテナライズを進めています。

そんな大量にあるジョブですが、環境変数だけが異なっているものも多数あり、実行環境 x 環境変数と環境が異なると掛け算式に増えていきます。 これをkubernetesのCronJobでyamlハードコーディングすると容易に1000行を超えてしまい、管理上のコストも含め現実的ではありません。1

そこで、kubernetesパッケージマネージャーを使用することにしました。その選択肢としてはHelmKustomizeなどがあります。 これらを利用することで環境毎に異なる設定値のリソースが作成できたり、複雑な依存関係を持つkubernetesリソース群をChartという一つのフォーマットにまとめることができます。

今回、要件を満たすエントリが見当たらなかった為、利用事例として投稿させていだきます。

目次

Kustomizeの場合

まずはKustomizeで同様の利用を想定した際の構成を見てみましょう。

下記ディレクトリ構成は公式のKustomizeのGithubの、サンプルディレクトリ構成から拝借しました。

Kustomizeのサンプルディレクトリ構成

~/someApp
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── development
    │   ├── cpu_count.yaml
    │   ├── kustomization.yaml
    │   └── replica_count.yaml
    └── production
        ├── cpu_count.yaml
        ├── kustomization.yaml
        └── replica_count.yaml

staging, develop, production環境といった環境差分をoverlaysで表現するのはKustomizeの方がいいかもしれませんが、Kustomizeの基本的なユースケースに照らし合わせてジョブを作成するとなると、次の例ようにCronJob毎にディレクトリを切る必要があり、管理コストが嵩んでしまいます。

Kustomizeで多数CronJobを作成する

例えば24個のAジョブと24個のBジョブ計48個作成する場合、overlays下に48個のディレクトリ構成ができてしまいます。以下がサンプルになります。

~/someApp
├── base
│   ├── cronjob.yaml
│   └── kustomization.yaml
└── overlays
    ├── 0101-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    ├── 0102-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    ├── 0103-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    │      ︙ # 増えるだけ用意しないといけなくなる
    └── 0224-job-b
        ├── caterory.yaml
        ├── kustomization.yaml
        └── table.yaml

バッチジョブが20を超えてくるとなるとディレクトリで分割するのは現実的に厳しいと思います。

参考:Kustomize で CronJob を同一テンプレートからスケジュール毎に生成する

以上の理由から、KustomizeではなくHelmを利用することを決めました。

Helmの場合

Helmではアクションと呼ばれる制御構造によってリストをループ処理することができます。したがって、今回のケースではこちらを採用することにしました。

Helm, Tillerのバージョンはv2.14.3を使用しています。

今回の要件として、

  • 同一のテンプレートをベースとして、
  • DBのテーブル毎に含まれる、
  • 複数の地域情報を取り出し処理ができる2

バッチジョブを作成する必要がありました。

これを実現するためには先述したループ処理を行う必要があります。

実現方法としては「ループ処理をネストすれば可能」というのが答えなのですが、少しだけハマりどころがあったので、それも合わせてお話できればと思います。

通常のCronJobをループ処理する場合、values.yamlに回したい変数のリストを作成し、rangeを入れればループ処理が可能です。

単一のリストを利用してCronJobをループ処理する

まずはスケジュール毎にCronJobを作成したい場合を想定してみましょう。 以下の実行例ではスケジュール毎にスケジュールのインデックスをechoで出力するという設定をyamlで行います。動作確認したい場合はhelm testを利用しましょう。

test-schedule-cj.yaml

{{- range $index, $schedule := .Values.global.schedules }}
--- # 複数作成の為に必須
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $index | add1 }} # CronJob毎に一意になるような名前をつける必要がある
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: test
            # テストで検証するのであれば、busyboxイメージでechoを出力します。  
            args:
            - /bin/sh
            - -c
            - date; echo Index count is $(INDEX).
            env:
              name: INDEX
              value: {{ $index | add1 }}
          restartPolicy: OnFailure
{{- end }}

因みにyamlの可読性の為、最初に以下のような代入を行っています。(以降、省略)

test-schedule-cj.yaml

# global
{{- $namespace := .Values.global.namespace }}
{{- $serviceAccount := .Values.global.serviceAccount }}
{{- $imageRepository := .Values.global.imageRepository }}
{{- $imageTag := .Values.globa.imageTag }}
# chart
{{- $chartName := .Chart.Name }}
{{- $chartVersion := .Chart.Version }}
{{- $releaseName := .Release.Name }}
{{- $releaseService := .Release.Service }}

values.yamlの設定は以下のようになります。

vaules.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # image  
  imageRepository: busybox
  imageTag: latest

  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

特に難しい部分はなく、rangeアクションで作成したいCronJobを囲むだけです。 ただ一つ注意しないといけない点として、CronJobの名前は一意になるようにしなければいけません。 その為、全ての名前が一意になるように命名規則を考えてつけましょう。 基本的にはループで回す変数名をつけるようにすれば大丈夫だと思います。3

作成後、以下のhelmコマンドを実行します。

$ helm upgrade --install job01 .

これでテーブルの数だけジョブを回すことができるChartのリリースができますね。

複数のリストを利用してCronJobをループ処理する

一つの変数の条件でループ処理ができたので、複数の変数のリストを使用してループする場合を考えてみましょう。 スケジュールのループ処理内にテーブルのループ処理をネストするだけで作成できます。 記述例としては以下のようになります。

test-schedule-table-cj.yaml

{{- range $index, $schedule := .Values.global.schedules }}
{{- range $table := .Values.global.tables }}
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $index | add1 }}
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: test
            args:
            - /bin/sh
            - -c
            - date; echo Index count is $(INDEX). Table name is $(TABLE_NAME).
            env:
              name: INDEX
              value: {{ $index | add1 }}
              name: TABLE_NAME
              value: {{ $tablel.name }}
          restartPolicy: OnFailure
{{- end }}
{{- end }}

values.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # image  
  imageRepository: busybox
  imageTag: latest

  # schedule info
  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

  # table info
  tables: 
    - name: XxxTest
      category: walk
    - name: YyyTest
      category: walk
    - name: ZzzTest
      category: run

しかし、例のようにスケジュール×テーブルという変数でジョブを回そうとすると失敗してしまいます。 以下が失敗の際に出力されるエラーになります。

UPGRADE FAILED
Error: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/test-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {}
Error: UPGRADE FAILED: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/est-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {}

失敗の原因は.のカレントスコープが「.Values.global.schedules」を向いているためで、.Values.global.regionが習得できません。

回避策としては、ループの処理の際に、「常にグローバルスコープを持つ$を使用する」ことでこのエラーを回避できます。

したがって、以下のdiffの変更点のように最初の記述でループしたい変数を別の変数に代入することでスコープを変えないままネストしたループ処理ができるようになります。 また、CronJobのリソース名も一意にするため、テーブルの名前を更新しておきましょう。

test-schedule-table-cj.yaml

+ {{- $regions := .Values.global.tables }}

- {{- range $index, $schedule := .Values.global.schedules }}
- {{- range $table := .Values.global.tables }}
+ {{- range $index, $schedule := .Values.global.schedules }}
+ {{- range $table := $tables }} 

-   name: test-{{ $index | add1 }} 
+   name: test-{{ $index | add1 }}-{{ $table.name }} # 名前を一意にするため

しかし、これでもまだ十分ではありません。あくまでスケジュールのindexがほしいのではなく、スケジュール(cron)毎に習得される地域の変数をCronJobのmetadataや環境変数に代入したいのです。

スケジュールの時間毎に、異なる地域のバッチジョブを実行する

golangのSprig libraryを利用してリストを取得するようにしています。 このような形にしたのは、複数のバッチジョブを後述するsubchartに記述する際、values.yamlをDRYにするためです。 全てのリストを1つづつ取得する関数がなかったので、次のような形で再現しています。

test-region-table-cj.yaml

{{- $regions := .Values.global.tables }}

{{- range $index, $schedule := $schedules }}
{{- range $table := $tables }}

# スケジュール毎に地域のリストを取得する
{{- $region := slice $regions $index ( $index | add1 ) | first }}

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $region }}-{{ $table.name }} # indexではなく地域別で名前をつけている
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: {{ $name }}
            args:
            - /bin/sh
            - -c
            - date; echo This chart has region $(REGION) and table $(TABLE_NAME).
            env:
              - name: TABLE_NAME
                value: "{{ $table.name }}"
              - name: TABLE_CATEGORY
                value: "{{ $table.category }}"
              - name: REGION
                value: {{ $region | quote }}
          restartPolicy: OnFailure
{{- end }}
{{- end }}

values.yamlにはglobal.regionsを、scheduleのリスト長と同一のリストを加えましょう。4

values.yaml

+  regions: [japan, us, uk, china]

これで、地域別のDBのテーブルの情報が取得できるようになりましたね。

サブチャートを作成して、一連のワークフローを一つのChartで再現する

実際には上記のようなバッチジョブが後続に存在しているので、これらをひとまとまりにして扱う必要があります。 最後に、これらバッチジョブをひまとまりにして処理する方法を学びましょう。

複数のjob(Dockerイメージが別)を扱うためにsubchartを採用しました。 ServiceAccount関連のリソースやNetworkPolicyやNamespaceのようなグローバルなリソースは大本のChartにまとめ、各ジョブをsubchartに入れるという形を取ります。

values.yamlの項目で、globalの変数と、そうでないsubchart毎のローカル変数を使いわけることで実現できます。

以下にサンプルのディレクトリ構成を挙げます。

.
├── Chart.yaml
├── charts
│   ├── 0101-job
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── _helpers.tpl
│   │       ├── xxx-cronjob.yaml
│   │       └── tests
│   │           └── test-xxx.yaml
│   ├── 0102-job
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── _helpers.tpl
│   │       ├── yyy-cronjob.yaml
│   │       └── tests
│   │           └── test-yyy.yaml
│   └── 0103-job
│       ├── Chart.yaml
│       └── templates
│           ├── _helpers.tpl
│           └── zzz-cronjob.yaml
│           └── tests
│               └── test-zzz.yaml
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── namespace.yaml
│   ├── networkpolicy.yaml
│   ├── secrets
│   │             ︙
│   └── serviceaccount.yaml
└── values.yaml

個別のyaml設定の注意点

各サブチャート毎に分化されたschedulesはグローバルの設定ではなく、個別のジョブ毎のローカルの設定になるため、各subchart内では、.Values.global.schedulesから.Values.schedulesのようにグローバルのスコープをローカルのスコープに変更しておきましょう。

+ {{- $category := .Values.schedules }}
- {{- $tables := .Values.global.schedules }}

フラグで実行するジョブバッチを選択する

後でsubhart毎にテストをするには、個別のsubchartだけを実行する必要があります。 そのため、enabledを各subchartに記述します。

以下のように各CronJobリソースを条件式{{- if .Values.enabled }}で挟んで、

test-subchart-cj.yaml

{{- if .Values.enabled }}
{{- range $index, $schedule := $schedules }}
{{- range $table := $tables }}
---
apiVersion: batch/v1beta1
︙
{{- end}}
{{- end}}
{{- end}}

values.yamlに以下の設定を入れます。

values.yaml

0101-job:
  enabled: true # 全てtrueにしておく
  imageRepository: xxx
︙

実行時に不必要なジョブに対してfalseのフラグを立てることで個別実行が実現できます。

$ helm upgrade --install jobs --set 0101-job.enabled=true --set 0102-job.enabled=false --set 0103-job.enabled=false .

上記は0101-jobだけがChartだけがデプロイされる例になります。

テストに関して1つ注意点があります。 テストを行う毎はテストの実行ジョブの順番が変わるのでご注意下さい。5

完成したvalues.yaml

最終的なvalues.yamlは次のようになります。

values.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # table info
  tables: 
    - name: XxxTest
      category: walk
    - name: YyyTest
      category: walk
    - name: ZzzTest
      category: run
     

  regions: [japan, us, uk, china]

  # test config
  tests:

    tables: 
      - name: XxxTest
        category: walk

    regions: [japan, china]

0101-job:
  enabled: true
  imageRepository: xxx
  imageTag: 1.0.0
  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

0102-job:
  enabled: true
  imageRepository: yyy
  imageTag: 1.0.0
  schedule: '0 1 * * *'

0103-job:
  enabled: true
  imageRepository: zzz
  imageTag: 0.1.0

  schedules: [
    '0 2 * * *', '10 2 * * *',
    '20 2 * * *', '30 2 * * *'
    ]

おわりに

今回ループ処理化したCronJob Aが成功したらCronJob Bを実行すると言ったワークフローは単純なスケジュール(cron)でHelm Chart化しました。

その他ハマりどころとしてはvalues.yamlのsubchart名と一致しないChart名、ディレクトリ名になっているとチャートがデプロイされないという事例がありました。案外、見落とします。 名前を変更した際には確認するようにしましょう。

最後に注意点で、今回のエントリでは実運用で想定するようなTLSの暗号化通信やSecurityContext、Compute Resourcesなど省略しているので、それぞれの環境に合わせて設定していただればと思います。

以上!

仲間募集

ユーザベースのSPEEDA SREチームは、No Challenge, No SRE, No SPEEDA を掲げて業務に取り組んでいます。
「挑戦しなければ、SREではないし、SREがなければ、SPEEDAもない」という意識で、日々ユーザベースのMissionである、「経済情報で、世界をかえる」の実現に向けて邁進しています。

少しでも興味を持ってくださった方はこちらまで


  1. いわゆるwall of YAML

  2. 割り当てたい変数(今回でいうと地域)毎にスケジュール(cron)を生成します

  3. 63文字の制限があります

  4. 本来は順序が逆ですが、説明の便宜上このように書いています

  5. 恐らく前回の実行のリストから+1されています

XP祭りの裏側 〜大規模イベントの運営ってどんなことやるの?〜

こんにちは!SPEEDA開発チームの斎藤です。

先月9月21(土)に開催された開発者向けイベント「XP祭り」は皆様ご存知でしょうか。
今回は運営メンバーとして「XP祭り」に関わり、思いきりお祭りを堪能した私の視点から

  • 運営ってどんなことしてたの?(大変なの?)
  • 運営側だとどんな楽しいことがあるの?

といったことについて少しだけ語らせていただき、
イベントを創る側の魅力や楽しさが伝えられればと思います。

XP祭りとは?

毎年9月ごろに開催されるソフトウェア開発者向けの技術イベントです。
XP(エクストリームプログラミング)やアジャイル、スクラムなどのテーマを中心に、
講演者が培った知見、体験を元にした魅力的な講演、ワークショップが多数行われます。
参加費用は無料!今年は18回目で、来場者数220人と大盛況でした。

f:id:saito86:20190927210953j:plain

XP祭りの運営ってどんなことやるの?

運営側がやることを大きく分けると、
イベント当日までの準備と、当日の運営の2つに大別されます。

①イベント当日までの準備(毎月一回開かれる運営会議で進めていく)

  • イベントの開催日時、会場を決める
  • イベント開催の告知とwebサイトを用意する
  • 講演者を公募し、イベント当日のタイムテーブルを作る
  • 参加者を募集する
  • 各出版社様にお願いし、技術本を寄贈していただく(※) etc...

XP祭りでは毎年、各出版社様から技術本を多数寄贈していただいております。
これらは全て参加者および講演者の方に無料で配布させていただいております。 f:id:saito86:20190927210708j:plain

②イベント当日の運営

  • 会場のセッティング
  • 受付
  • 各講演の司会
  • 後片付け
  • 懇親会 etc...

色々やることがあって大変に見えるかもしれませんね。でも大丈夫です。
XP祭りは今年で18回目なので、既にどんなことをどうやるかの大筋が決まっています。

例えば、イベント会場は早稲田大学教授の鷲崎先生のご厚意で無償でお借りしています。
また、講演者の公募や当日のタイムテーブル作成も、今まで積み重ねてきたものをベースにみんなでワイワイ進めるので、 運営をやるのが初めての私でも特に大変だと感じることはありませんでした。

「運営になるべく負担がかからないようにする」
「運営がお祭りを楽しめるようにする」

ということを意識されているそうで、長くイベントを続ける秘訣を教えていただいたなあと思いました。

XP祭りの運営ってどんな楽しいことがあるの?

これは色々ありますが、大きく分けると2つあると感じました。

①お祭りの準備、開催の楽しさと達成感
学校の文化祭のようなイメージといって伝わるのか自信がありませんが、 お祭りの準備や運営って、参加してくれる方が笑って満足してくれると嬉しいですよね。 自分たちが携わったイベントに200人以上も参加者が集まってくれて、 「XP祭り楽しかった」という感想を聞いたりすると「やって良かった」と思いますね。
もちろん、当日は参加者でもあるため、自分の気になる講演やワークショップに参加しちゃってOKです。

②知り合いが増えて、コミュニティの輪が広がる
運営メンバーを中心に、講演者の方や参加者の方まで、 たくさんの方々とつながりができました。 やっぱり同じようなことに挑戦している仲間や、偉大な先人の方と話ができるのはとても心強いです。
困ったときに相談したり、良い知見が得られたときに共有したりと、コミュニティの強さって本当に素晴らしいなと思います。

f:id:saito86:20190927212902j:plain
お祭りの後はやっぱり懇親会!

終わりに

ほんの少しXP祭りの裏側を書かせていただきましたが、いかがでしたでしょうか。

  • 大規模イベントの運営でも実はそんなに大変じゃない
  • 運営として裏側からもお祭りを楽しめた

という2点が伝わって、少しでも興味を持っていただけたのなら幸いです。
(後はほんの少し勇気を出して飛び込むだけです。来年のXP祭り運営委員の会でお会いしましょう!)

謝辞

XP祭りの運営の皆様、ならびに講演、参加者の皆様、ありがとうございました。
皆様のおかげで、XP祭りは今年も最高に楽しいイベントになったのではないか思います。 また来年もどうぞよろしくお願いいたします!

【Clojure】Ductで始めるWebAPI開発

こんにちは!!SPEEDA開発チームの岡村です!!

私たちの開発チームでは、先日チームメンバの野口が書いたこちらの記事に書かれているように、チームメンバーの入れ替えが頻繁に行われます。

かく言う私も一ヶ月前に行われたチームシャッフルで、ClojureでDuctを使って開発を行うチームに移動しました。(Clojureほぼ未経験)

私がClojure初心者だからこそ、これからClojureとDuctを始めて見ようかなと思っている方に対し、お伝えできることがあると思い、今回はDuctでWebAPIを作成する方法をご紹介しようと思います。

Ductとは

Ductはライフサイクル管理を行うライブラリ、Integrantをベースとしたサーバサイドのフレームワークであり、アプリケーション構造は、Integrantのコンフィグレーションマップによって定義されます。

DuctとIntegrantの関係に関しては、弊社の中村がこちらの記事でも一度ご紹介しています。

作成物

CRUDの題材として定番な、TODO管理を下記リクエストで行えるAPIを作成していきます。

リクエストメソッド パス 内容
GET /todos 一覧を取得する
GET /todos/:id 指定したデータを取得する
POST /todos データを登録する
PUT /todos/:id 指定したデータを更新する
DELETE /todos/:id 指定したデータを削除する

プロジェクト作成

DuctプロジェクトはLeiningenを使って作成することができ、プロファイルを指定すると、
それに合わせプロジェクト構成や設定ファイルを自動で生成してくれます。

$ lein new duct todo +api +ataraxy +postgres

今回はAPIを作成し、ルーティングライブラリにAtaraxy、TodoデータをPostgresで管理するため、
+api +ataraxy +postgresと指定しました。

Todoを管理するデータベースも作成しておきましょう。
今回はDockerコンテナで作成しました。

docker run --name todo-db -p 5432:5432 -e POSTGRES_DB=todo-db -e POSTGRES_USER=username01 -e POSTGRES_PASSWORD=password01 -d postgres:11.5 

システムの起動

DuctはREPLを利用して開発を進めていくため、先ほど作成したプロジェクトに移動し、REPLを起動しましょう!

$ lein repl
nREPL server started on port 50077 on host 127.0.0.1 - nrepl://127.0.0.1:50077
REPL-y 0.4.3, nREPL 0.6.0
Clojure 1.10.0
OpenJDK 64-Bit Server VM 11.0.2+9
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

REPLが起動したら、(dev)と入力し開発環境をロードします。

user=> (dev)
:loaded

その後、(go)とコマンドを入力するとシステムを起動することができるのですが、
DBへの接続情報をまだ記述していないため、下記の様なエラーが発生し、起動することができません。

dev=> (go)
:duct.server.http.jetty/stopping-server
Execution error (PSQLException) at org.postgresql.core.v3.ConnectionFactoryImpl/doAuthentication (ConnectionFactoryImpl.java:534).
サーバはパスワード・ベースの認証を要求しましたが、パスワードが渡されませんでした。
dev=> 

システムを起動させるため、DB接続情報を記述しましょう!
/todo/dev/resources配下にある、dev.ednファイルを以下の様に変更します。
このファイルはファイル名の通り、開発環境での設定を記述するファイルです。

{:duct.database/sql
 {:connection-uri "jdbc:postgresql://localhost:5432/todo-db?user=username01&password=password01"}
 }

Ductではednファイルにコンフィグレーションマップを書いていくことで、
アプリケーションの構造を表現することができます。

ここではduct.database/sqlというコンポーネントの初期化時に、データベースの接続情報が渡される用に定義しています。

書き換えた後に、再度システムを起動させてみましょう。

dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated

上記の様に表示されていれば、システムが起動されています。 curlでリクエストを送ってみましょう。

$ curl http://localhost:3000
{"error":"not-found"}

errorが帰ってきますが、起動していることがわかります。

マイグレーション

ductのmoduleでマイグレーションツールとして提供されている、
migrator.ragtime を利用して、システムの起動時にテーブルの作成と、テストデータの投入が行われるようにしましょう。

先ほど編集したdev.ednを下記のように編集します。

{:duct.database/sql
 {:connection-uri "jdbc:postgresql://localhost:5432/todo-db?user=username01&password=password01"}

 :duct.migrator/ragtime
 {:migrations [#ig/ref :todo.migration/create-todos]}

 [:duct.migrator.ragtime/sql :todo.migration/create-todos]
 {:up ["CREATE TABLE todos (id SERIAL  PRIMARY KEY, title TEXT)"
       "INSERT INTO todos (title) values('test1')"
       "INSERT INTO todos (title) values('test2')"]
  :down ["DROP TABLE todos"]}
 }

システムを再起動させてみましょう。
REPLで(reset)と入力すると、再起動することができます。

dev=> (reset)
:reloading ()
:duct.migrator.ragtime/applying :todo.migration/create-todos#2cd2c3f9
:resumed

これでデータベースにテーブルが作成され、データが投入されました。

一覧取得処理の実装

まずはTodo一覧を取得する処理を実装していきましょう。
/todo/resources/todo/に存在する、config.ednを下記のように編集します。

{:duct.profile/base
 {:duct.core/project-ns todo

  :duct.router/ataraxy
  {:routes
   {[:get "/todos"] [:todo.handler.todos/list]}
   }

  ;; Handlers
  :todo.handler.todos/list {}

  }

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

:duct.router/ataraxyコンポーネントに、 {[:get "/todos"] [:todo.handler.todos/list]}という記述を追加しました。
これは、/todosにGETリクエストが来た時、:todo.handler.todos/listコンポーネントで処理をするということを表しています。

:todo.handler.todos/list {}という記述も追加しました。これは:todo.handler.todos/listコンポーネントの初期化時に空のマップを渡すということを示しています。

ハンドラーの実装

続いてハンドラーを作成していきます。

/todo/src/todo/handlerに、todos.cljというファイルを作成し、以下を記述します。

(ns todo.handler.todos
  (:require [ataraxy.response :as response]
            [integrant.core :as ig]))

(defmethod ig/init-key ::list [_ _]
  (fn [_]
    [::response/ok {:message "OK!!!"}]))

Integrantでマルチメソッドとして定義されている、ig/init-keyを実装することにより、コンポーネントを作成することができます。

::listというのは、todo.handler.todos/listと同じ意味になります。
これにより、先ほどconfig.ednで書いた設定と処理を紐づけることができます。

戻り値は、Ataraxyを利用しているのでベクタで記述することができます。
一先ずリクエストが来たら :message "OK!!!"というマップを返すようにしました。

一旦システムを再起動し、実際にリクエストをしてみましょう!

dev> (reset)
:reloading (todo.handler.todos)
;;=> :resumed
$ curl  http://localhost:3000/todos
{"message":"OK!!!"}

レスポンスが帰ってくることを確認できました!

それでは、実際にデータベースからTodoリストを取得し、返すように実装を変えていきます。

/todo/resources/todo/config.ednを下記のように変更します。

{:duct.profile/base
 {:duct.core/project-ns todo

  :duct.router/ataraxy
  {:routes
   {[:get "/todos"] [:todo.handler.todos/list]}
   }

  ;;Handlers
  :todo.handler.todos/list {:db #ig/ref :duct.database/sql}

  }

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

:todo.handler.todos/list {}と記述していた箇所を、
:todo.handler.todos/list {:db #ig/ref :duct.database/sql}に変更しました。

#ig/ref関数を利用し、:todo.handler.todos/listコンポーネントは、:duct.database/sqlコンポーネントに依存しているということを示しています。

:duct.database/sqlは、最初にdev.ednで記述したコンポーネントのことです。

これにより、:todo.handler.todos/listコンポーネントの初期化時に、
初期化済の:duct.database/sqlコンポーネントを受け取ります。

次に先ほど作成した、todos.cljを以下のように変更します。

(ns todo.handler.todos
  (:require [ataraxy.response :as response]
            [integrant.core :as ig]
            [todo.boundary.todos :as todos]))

(defmethod ig/init-key ::list [_ {:keys [db]}]
  (fn [_]
    (let [todos (todos/get-todos db)]
      [::response/ok todos])))

第二引数に、先ほどconfig.ednで定義したデータベースの接続情報を受け取り、
後で作成する、データベース接続を行うBoundaryにDB接続情報を渡して結果を返すようにしました。

init-keyは、第一引数にコンポーネントのキー(:todo.handler.todos/list)が、

第二引数にはコンフィグマップのコンポーネントに対応する以下のようなバリューが渡ってきます。

{:db #duct.database.sql.Boundary
 {:spec
  {:datasource #object[net.ttddyy.dsproxy.support.ProxyDataSource 0x11e3a8d net.ttddyy.dsproxy.support.ProxyDataSource@11e3a8d]}}}

バリューは:todo.handler.todos/list {:db #ig/ref :duct.database/sql}{:db #ig/ref :duct.database/sql}のことです。
#ig/refと記述しているため、初期化された:duct.database/sqlコンポーネントが渡ってきています。

データアクセス層の実装

続いて、実際にデータベースへの接続を行う層として、Boundaryを作成していきましょう!

/todo/src/todo/boundaryにtodos.cljというファイルを作成し、以下を記述します。

(ns todo.boundary.todos
  (:require
   [clojure.java.jdbc :as jdbc]))

(defprotocol Todos
  (get-todos [db]))

(extend-protocol Todos
  duct.database.sql.Boundary

  (get-todos [{:keys [spec]}]
    (jdbc/query spec ["SELECT * FROM todos"]))
  )

DBへの接続情報を受け取り、clojure.java.jdbcを利用してデータベースにアクセスしています。

システムを再起動し、リクエストを送ってみましょう!

curl http://localhost:3000/todos
[{"id":1,"title":"test1"},{"id":2,"title":"test2"}]

データベースから値を取得することができました!

登録処理の実装

次にデータベースへの登録処理を実装していきましょう。
流れは一覧取得処理を実装した時とほとんど同じです。

config.ednに/todosにPOSTでリクエストが送られてきた時の定義を記述します。

{:duct.profile/base
 {:duct.core/project-ns todo

  :duct.router/ataraxy
  {:routes
   {[:get "/todos"] [:todo.handler.todos/list]
    [:post "/todos" {body :body-params}] [:todo.handler.todos/create body]}
   }

  :todo.handler.todos/list {:db #ig/ref :duct.database/sql}
  :todo.handler.todos/create {:db #ig/ref :duct.database/sql}

  }

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

先ほどと違うところは、HTTPリクエストからパラメータを取得しているというところです。
HTTPリクエストの情報はマップで渡ってきており、そこから登録に必要な:body-paramsの値を取り出しています。

ハンドラーの実装

次にhandlerを下記のように変更します。

(ns todo.handler.todos
  (:require [ataraxy.response :as response]
            [integrant.core :as ig]
            [todo.boundary.todos :as todos]))

(defmethod ig/init-key ::list [_ {:keys [db]}]
  (fn [_]
    (let [todos (todos/get-todos db)]
      [::response/ok todos])))

(defmethod ig/init-key ::create [_ {:keys [db]}]
  (fn [{[_ params] :ataraxy/result}]
    (let [result (todos/create-todo db params)
          id (:id (first result))]
      [::response/created (str "/todos/" id)])))

無名関数の引数が一覧検索の時と異なっています。
この無名関数の引数には下記のようなHTTPリクエスト情報のマップが渡ってきます。

{:ssl-client-cert nil, 
 :protocol HTTP/1.1, 
 :remote-addr 0:0:0:0:0:0:0:1, 
 :params {}, 
 :body-params {:title test3}, 
 :route-params {}, 
 :headers {user-agent curl/7.54.0, host localhost:3000, accept */*, content-length 18, content-type application/json}, 
 :server-port 3000, 
 :muuntaja/request #FormatAndCharset{:format application/json, :charset utf-8, :raw-format application/json}, 
 :ataraxy/result [:todo.handler.todos/create {:title test3}], 
 :content-length 18, :form-params {}, 
 :query-params {}, 
 :content-type application/json, 
 :character-encoding UTF-8, 
 :uri /todoss 
 :server-name localhost, 
 :query-string nil, 
 :muuntaja/response #FormatAndCharset{:format application/json, :charset utf-8, :raw-format */*}, :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x74981d49 HttpInputOverHTTP@74981d49], 
:scheme :http, 
:request-method :post}

そのため:ataraxy/resultを分配束縛させています。

データアクセス層の実装

続いてBoundaryを編集しましょう!

(ns todo.boundary.todos
  (:require
   [clojure.java.jdbc :as jdbc]))

(defprotocol Todos
  (get-todos [db])
  (create-todo [db params]))

(extend-protocol Todos
  duct.database.sql.Boundary

  (get-todos [{:keys [spec]}]
    (jdbc/query spec ["SELECT * FROM todos"]))

  (create-todo [{:keys [spec]} params]
    (jdbc/insert! spec :todos {:title (:title params)}))

  )

jdbc/queryを使っても、もちろん実装できるのですが、jdbc/insert!を利用してみました。

これで登録処理の実装は完了です!

システムを再起動してリクエストを送ってみましょう!

curl -i -X POST http://localhost:3000/todos -d '{"title": "test3"}'  --header "Content-Type: application/json" 
HTTP/1.1 201 Created
Date: Thu, 29 Aug 2019 05:14:01 GMT
Location: http://localhost:3000/todos/3
Content-Type: application/octet-stream
Content-Length: 0
Server: Jetty(9.2.21.v20170120)

登録処理を行うことができました!

更新、削除、一件取得処理の実装

更新、削除、一件取得処理もこれまでと同様にして実装することができます!
そのため説明は割愛させていただきますが、最終的に実装は下記のようになります。

todo/resources/todo/config.edn

{:duct.profile/base
 {:duct.core/project-ns todo

  :duct.router/ataraxy
  {:routes
   {[:get "/todos"] [:todo.handler.todos/list]
    [:get "/todos/" id] [:todo.handler.todos/fetch ^int id]
    [:post "/todos" {body :body-params}] [:todo.handler.todos/create body]
    [:put "/todos/" id {body :body-params}] [:todo.handler.todos/update ^int id body]
    [:delete "/todos/" id] [:todo.handler.todos/delete ^int id]
    }}

  ;; Handler
  :todo.handler.todos/list
  {:db #ig/ref :duct.database/sql}
  :todo.handler.todos/fetch
  {:db #ig/ref :duct.database/sql}
  :todo.handler.todos/create
  {:db #ig/ref :duct.database/sql}
  :todo.handler.todos/update
  {:db #ig/ref :duct.database/sql}
  :todo.handler.todos/delete
  {:db #ig/ref :duct.database/sql}
  }

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

todo/src/todo/handler/todos.clj

(ns todo.handler.todos
  (:require [ataraxy.response :as response]
            [integrant.core :as ig]
            [todo.boundary.todos :as todos]))

(defmethod ig/init-key ::list [_ {:keys [db]}]
  (fn [_]
    (let [todos (todos/get-todos db)]
      [::response/ok todos])))

(defmethod ig/init-key ::fetch [_ {:keys [db]}]
  (fn [{[_ id] :ataraxy/result}]
    (let [todo (todos/fetch-todo db id)]
      [::response/ok todo])))

(defmethod ig/init-key ::create [_ {:keys [db]}]
  (fn [{[_ params] :ataraxy/result}]
    (let [result (todos/create-todo db params)
          id (:id (first result))]
      [::response/created (str "/todos/" id)])))

(defmethod ig/init-key ::update [_ {:keys [db]}]
  (fn [{[_ id params] :ataraxy/result}]
    (todos/update-todo db id params)
    [::response/no-content]))

(defmethod ig/init-key ::delete [_ {:keys [db]}]
  (fn [{[_ id] :ataraxy/result}]
    (todos/delete-todo db id)
    [::response/no-content]))

todo/src/todo/boundary/todos.clj

(ns todo.boundary.todos
  (:require
   [clojure.java.jdbc :as jdbc]))

(defprotocol Todos
  (get-todos [db])
  (fetch-todo [db id])
  (create-todo [db params])
  (update-todo [db id params])
  (delete-todo [db id]))

(extend-protocol Todos
  duct.database.sql.Boundary

  (get-todos [{:keys [spec]}]
    (jdbc/query spec ["SELECT * FROM todos"]))

  (fetch-todo [{:keys [spec]} id]
    (jdbc/query spec [(format "SELECT * FROM todos WHERE id = '%s'" id)]))

  (create-todo [{:keys [spec]} params]
    (jdbc/insert! spec :todos {:title (:title params)}))

  (update-todo [{:keys [spec]} id params]
    (jdbc/update! spec :todos {:title (:title params)} ["id = ?" id]))

  (delete-todo [{:keys [spec]} id]
    (jdbc/delete! spec :todos ["id = ?" id]))

  )

まとめ

Integrantの「アプリケーションの構造と実装を、明確に分けて開発を行っていける」という機能を利用して作成されたDuctは、ednファイルで各コンポーネント間の依存関係を明示でき、「何が何に依存しているのかednファイルを見ればわかる」といったとこや、提供されているモジュールを利用することにより、実装を手軽に行えるということに魅力を感じました。

Rustで非同期リクエストでハマったこと

こんにちは!SPEEDAプロダクト開発チームの成です。

去年の秋ごろからRustを触り始め、徐々にRustの魅力に惚れられました。 最初は日々コンパイラーにボコボコにされていましたが、 The Book *1 を読みながら、Rustを少しずつ理解していくと、段々コンパイラーと仲良くなってきて、Rustを書くのも楽しくなりました。

小さいな作業効率化のツールから、Rest Api Server、色んな処理を並列化するBatchなどをRustで作ってきました。最近プロダクトのマイクロサービスの極一部もRustで作っており、幸せ感満喫です!!

本日は、HTTPリクエストを並列化するときハマったことをお話したいと思います。 今日の内容はある程度Rustの知識が必要ですが、初めての方は上の The Book の日本語版 から参考できます。

Rustは独特なメモリ管理仕組みを持ち、安全かつRuntimeで発生したら世界が止まるFullGC的なものも一切ないし、高速な並列処理などが特徴です。めちゃくちゃカッコイイ言語と思いますので、本当に広がってほしいです。なので、初めての方々も是非書いてみてください!

はい、宣伝は以上です。本題に入ります。

今日はサンプルコードを介してハマった点を説明させていただきます。

  • 環境

OS rustバージョン
macOS 10.14.6 rustc 1.39.0-nightly
  • 使ったライブラリ

[dependencies]
reqwest = "0.9.20"
tokio-threadpool = "0.1.15"
futures01 = "0.1.28"
futures-preview = { version = "0.3.0-alpha.18", features = ["io-compat"] }
failure = "0.1.5"

今回は tokio-threadpoolfuturesの01系 を使って並列処理を実現します。
HTTPリクエストのライブラリは reqwest を使います。
記事の最後にはRust公式の新しい非同期シンタックス async / await を試したことを共有したいので、 futuresの03系 も入れました。

  • 同期リクエストの実現

1000回リクエストを順番に実行されるサンプルコードです。

use failure::Fail;
use reqwest::{Client, Url};
use std::thread;
use std::thread::ThreadId;
use std::time::{Duration, SystemTime};

pub type ResultWithError<T> = std::result::Result<T, ErrorWrapper>;

#[derive(Fail, Debug)]
pub enum ErrorWrapper {
    #[fail(display = "http request error: {:?}", error)]
    HttpRequestError { error: reqwest::Error },
}

impl From<reqwest::Error> for ErrorWrapper {
    fn from(error: reqwest::Error) -> Self {
        ErrorWrapper::HttpRequestError { error }
    }
}

fn main() {
    request_with_main_thread();
}

fn request_with_main_thread() {
    output1("START SINGLE THREAD");
    let start_at = SystemTime::now();

    let client = Client::new();

    (0..1000)
        .collect::<Vec<i32>>()
        .iter()
        .for_each(|_| match send_request(&client) {
            Ok((thread_id, text)) => output2(thread_id, text.as_str()),
            Err(error) => output1(format!("{:?}", error).as_str()),
        });

    let spent_time = start_at.elapsed().unwrap_or(Duration::new(0, 0));
    output1(format!("END: {}", spent_time.as_millis()).as_str())
}

fn send_request(client: &Client) -> ResultWithError<(ThreadId, String)> {
    let mut response = client
        .get(Url::parse("http://localhost:9000/timestamp").unwrap())
        .send()?;
    Ok((thread::current().id(), response.text()?))
}

fn output2(thread_id: ThreadId, text: &str) {
    println!("[{:?}] => {}", thread_id, text);
}

fn output1(text: &str) {
    output2(thread::current().id(), text);
}
  • main() 関数以前のコードはエラーハンドリングに関するものなので、説明は割愛します。

  • send_request() 関数ではローカルで別で立ち上がっているRestApiServerにリクエストを投げてシステム時間を返してくれます。

  • 実行結果
[ThreadId(1)] => START SINGLE THREAD
[ThreadId(1)] => 2019-09-01T18:50:22.706987Z
[ThreadId(1)] => 2019-09-01T18:50:22.710136Z
[ThreadId(1)] => 2019-09-01T18:50:22.713299Z
......
[ThreadId(1)] => 2019-09-01T18:50:27.398560Z
[ThreadId(1)] => 2019-09-01T18:50:27.404358Z
[ThreadId(1)] => 2019-09-01T18:50:27.407224Z
[ThreadId(1)] => END: 4711

当たり前ですが、リクエスト処理は全部メインスレッド ThreadId(1) で順番に実行されてます。1000回リクエスは5秒近くかかりました。

  • 非同期リクエストの実現

1000回リクエストを10スレッドに捌けて実行されるサンプルコードです。

use failure::Fail;
use futures01::{future, Future};
use reqwest::r#async::Client as AsyncClient;
use reqwest::Url;
use std::thread;
use std::thread::ThreadId;
use std::time::{Duration, SystemTime};
use tokio_threadpool::{Builder, SpawnHandle};

fn main() {
    request_with_multi_thread();
}

fn request_with_multi_thread() {
    output1("START MULTI THREAD");
    let start_at = SystemTime::now();

    let client = AsyncClient::new();
    let thread_pool_size = 10;
    let thread_pool = Builder::new().pool_size(thread_pool_size).build();

    let mut handles = Vec::<SpawnHandle<(ThreadId, String), ErrorWrapper>>::new();
    while handles.iter().count() <= 1000 {
        let cloned_client = client.clone();
        handles.push(thread_pool.spawn_handle(future::lazy(move || {
            send_request_for_future(&cloned_client)
        })));
    }

    handles.iter_mut().for_each(|handle| match handle.wait() {
        Ok((thread_id, text)) => output2(thread_id, text.as_str()),
        Err(error) => output1(format!("{:?}", error).as_str()),
    });

    thread_pool.shutdown_now();

    let spent_time = start_at.elapsed().unwrap_or(Duration::new(0, 0));
    output1(format!("END: {}", spent_time.as_millis()).as_str())
}

fn send_request_for_future(
    client: &AsyncClient,
) -> impl Future<Item = (ThreadId, String), Error = ErrorWrapper> {
    client
        .get(Url::parse("http://localhost:9000/timestamp").unwrap())
        .send()
        .and_then(|mut response| response.text())
        .map(|text| (thread::current().id(), text))
        .from_err()
}

fn output2(thread_id: ThreadId, text: &str) {
    println!("[{:?}] => {}", thread_id, text);
}

fn output1(text: &str) {
    output2(thread::current().id(), text);
}
  • エラーハンドリングに関するコードは省略しました。

  • このサンプルでは 10スレッド で 1000回リクエストを捌けようとしてます。

  • タスク Future(send_request_for_futureの戻り値) をキューに溜まって、スレッドプール tokio_threadpool にアイドルなスレッドがあれば、キューからタスクを取り出して実行する。
    (行:22~28)

  • その結果達 SpawnHandle をメインスレッドでまとめる。
    (行:30~33)。

  • 実行結果
[ThreadId(1)] => START MULTI THREAD
[ThreadId(10)] => 2019-09-01T19:02:53.924756Z
[ThreadId(6)] => 2019-09-01T19:02:53.930478Z
[ThreadId(11)] => 2019-09-01T19:02:53.929056Z
[ThreadId(11)] => 2019-09-01T19:02:53.934684Z
[ThreadId(13)] => 2019-09-01T19:02:53.932217Z
[ThreadId(7)] => 2019-09-01T19:02:53.918978Z
[ThreadId(7)] => 2019-09-01T19:02:53.921761Z
......
[ThreadId(10)] => 2019-09-01T19:02:54.454774Z
[ThreadId(14)] => 2019-09-01T19:02:54.456831Z
[ThreadId(15)] => 2019-09-01T19:02:54.458395Z
[ThreadId(15)] => 2019-09-01T19:02:54.459974Z
[ThreadId(1)] => END: 593

リクエスト処理は全部メインスレッド以外のスレッドで行われて、かかった時間もほぼ直列の1/8です。

  • ハマったこと

1. HttpClientが違います。

reqwest::Client をFutureの中で使うと下記のエラーが発生します。

[ThreadId(1)] => HttpRequestError { error: Error(BlockingClientInFutureContext, "http://localhost:9000/timestamp") }

Futureの中では reqwest::r#async::Client を使わないといけないです。

2. reqwest::r#async::Client をマルチスレッド間で共有しないと、大量リクエストが発生したらリソースが枯渇してしまいます。
        let cloned_client = client.clone();
        handles.push(thread_pool.spawn_handle(future::lazy(move || {
            send_request_for_future(&cloned_client)
        })));

この部分を下記のように変えたら

        handles.push(thread_pool.spawn_handle(future::lazy(move || {
            send_request_for_future(&AsyncClient::new())
        })));

下記のエラーが大量に出てきます。(リクエストが全部失敗ではないですが)

[ThreadId(1)] => HttpRequestError { error: Error(Hyper(Error(Io, Os { code: 54, kind: ConnectionReset, message: "Connection reset by peer" })), "http://localhost:9000/timestamp") }

リクエスト先のサーバからコネクションを勝手に切断したためエラーになったと思います。
reqwest::r#async::Client のソースを見てみると、確かにClientには connection pool を持っています。
並列で数多くClientを生成すると、余計にリクエスト先のサーバとコネクションを確立し過ぎで、途中で切断されることになります。

/// An asynchronous `Client` to make Requests with.
///
/// The Client has various configuration values to tweak, but the defaults
/// are set to what is usually the most commonly desired value. To configure a
/// `Client`, use `Client::builder()`.
///
/// The `Client` holds a connection pool internally, so it is advised that
/// you create one and **reuse** it.
#[derive(Clone)]
pub struct Client {
    inner: Arc<ClientRef>,
}
3. Future.wait() を呼び出すときは、そのFutureは 別のスレッドで実行される保証 があるかどうかを考えないといけないです。

最初 send_request_for_future() は下記のように実装していました。

fn send_request_for_future(client: &AsyncClient) -> ResultWithError<(ThreadId, String)> {
    Ok((
        thread::current().id(),
        client
            .get(Url::parse("http://localhost:9000/timestamp").unwrap())
            .send()
            .wait()?
            .text()
            .wait()?,
    ))
}

reqwest::r#async::Client は使っていますが、Futureを返せずに実際の処理結果を返そうとしていました。.send().text() はそれぞれFutureを返すので、実際の処理結果を取り出すために .wait()? を2回呼び出しました。その結果、実行した直後に [ThreadId(1)] => START MULTI THREAD だけが出力され、その後は一切処理が進まなかったです。
原因は send_request_for_future() はthread_pool中のスレッドから実行されますが、そこで呼び出している2回の .wait()? は更に他のスレッドに実行してくれないと、処理が進まないです。thread_pool中のスレッドが全部 send_request_for_future() の実行に使ってしまったら、処理全体が進まなくなります。もしthread_pool_sizeがリクエスト回数より多い場合(この例だと let thread_pool_size = 1001; )はすべてのリクエストが正常に終わります。
send_request_for_future() がFutureを返すし、メインスレッドで SpawnHandle.wait() を呼び出すように実装したら、処理全体が正常に終えました。

  • まとめ

ほぼどの技術のハマりも同じだと思います。本質を理解したら当たり前のように見えますが、理解する前には詳しい人に聞いたり、ググったりしてバタバタしても中々解決しないとイライラしてしまいますね。今回のハマリポイント、特に3点目は色々ググっても類似のサンプルソースがなかったので、困りました。もし同じ問題に困っている方がいらっしゃったら、この記事が参考にできるならば嬉しいです。
ここで使っているサンプルソースは下記のプロジェクトからcloneできます。

https://github.com/dimmy82/rust-concurrency-request

  • (おまけ)async / await

今年の7月ぐらい、新しい非同期シンタックス async / await がリリースされて、ちょっと触ってみたので共有させていただきます。もし間違っているところがあればご指摘ください。

use failure::Fail;
use futures::executor::block_on;
use reqwest::{Client, Url};
use std::thread;
use std::thread::ThreadId;
use std::time::{Duration, SystemTime};

fn main() {
    block_on(request_with_async_await()); // this is not a parallel request example
}

async fn request_with_async_await() {
    output1("START ASYNC AWAIT");
    let start_at = SystemTime::now();

    let client = Client::new();
    let future1 = send_request_async(&client);
    let future2 = send_request_async(&client);
    match future1.await {
        Ok((thread_id, text)) => output2(thread_id, text.as_str()),
        Err(error) => output1(format!("{:?}", error).as_str()),
    };
    match future2.await {
        Ok((thread_id, text)) => output2(thread_id, text.as_str()),
        Err(error) => output1(format!("{:?}", error).as_str()),
    };

    let spent_time = start_at.elapsed().unwrap_or(Duration::new(0, 0));
    output1(format!("END: {}", spent_time.as_millis()).as_str())
}

async fn send_request_async(client: &Client) -> ResultWithError<(ThreadId, String)> {
    let mut response = client
        .get(Url::parse("http://localhost:9000/timestamp").unwrap())
        .send()?;
    Ok((thread::current().id(), response.text()?))
}

fn output2(thread_id: ThreadId, text: &str) {
    println!("[{:?}] => {}", thread_id, text);
}

fn output1(text: &str) {
    output2(thread::current().id(), text);
}

結論から言うと、上記サンプルのように

let future1 = /* async 関数を呼び出す */;
let future2 = /* async 関数を呼び出す */;
future1.await;
future2.await;

という書き方は非同期にならず、同期処理になります。 async 関数は await で呼び出されたタイミングで実行される からです。

  • 実行結果
[ThreadId(1)] => START ASYNC AWAIT
[ThreadId(1)] => 2019-09-01T21:23:12.791994Z
[ThreadId(1)] => 2019-09-01T21:23:12.794254Z
[ThreadId(1)] => END: 8

async 関数は処理結果(今回の例だと ResultWithError<(ThreadId, String)> )を返すように実装しますが、実際future1の型は impl core::future::Future<Output=ResultWithError<(ThreadId, String)>> になります。なので、もし非同期処理にしたければ、future1, future2を thread_pool.spawn_handle 的な関数に渡して実行してもらうじゃないかなと思います。
ただ、ここの core::future::Future は最近stdに追加されたFuture Traitですが、 futures01::Future とは全然違うものです。なので、使っているthread_poolのバージョンはまだ対応していないようです。
今後 core::future::Future に関する使い方が分かったらまた更新します。

以上、よろしくお願いいたします。

*1:The Book: Rustの創始者を含むコニュニティの方々が執筆するRust仕様の本です。去年までは日本語版が古くて、google先生に助けもらいながら無理やり英語版を読んでましたが、最近翻訳の皆さんのおかげて英語版とほぼ同期する 日本語版 が出てきましたので、Rustの勉強ならばそっちが一番おすすめです。

SPEEDA開発チームをブーストするふりかえりのカルチャー

こんにちは!SPEEDA開発チームの岩見です。
この記事では私たちSPEEDA開発チームの中でも特徴的な文化のひとつとなっている、ふりかえりについてご紹介します。

以下のような方々のお役に立つことを願っています。

  • 自分たちのチームでもふりかえりをやってみたい方
  • ふりかえりはやってるけどなんだかマンネリ化している方
  • 色々なふりかえりの運用、実例が知りたい方

SPEEDA開発チームにおけるふりかえり

私たちSPEEDA開発チームでは、プロジェクト単位の少人数のチームに分かれ、それぞれがエクストリームプログラミング(通称XP)をベースとしたアジャイル開発を行なっています。
私たちは短い開発サイクル(イテレーション)を継続的に繰り返す中で、プロダクトとチームでの開発をインクリメンタルに改善、洗練させていくことを目指しています。
こうした中で、私たちはイテレーション(多くの場合1週間が1イテレーションに相当します)の終わりに、ふりかえりを必ず実施しています。

ふりかえりの中では、イテレーションの開発を振り返って、次のイテレーションでの自分たちの開発を改善する為のアクションを一つ設定します。
アクションの数を絞っているのは、数が多すぎても一つ一つのアクションに対する精度が落ちてしまうリスクがあるためです。
短いイテレーションの中でより本質的なアクションを実施する為に、ふりかえりの時間の中で問題の洗い出し、深掘りを集中的に行うようにしています。

こうして決まったアクションをチームで実行しながら、私たちはプロダクトとチームでの開発プロセスを日々進化させています。

SPEEDA開発チームのふりかえりの特徴

私たちがふりかえりのためのミーティングを行う際には、一つの決め事が存在します。
それは、自分たちの所属しているチームの外側から、一人のメンバーにファシリテーターとして参加してもらうことです。
例えばイテレーションのふりかえりであれば、自分たちのプロジェクトチーム以外のエンジニアにファシリテーターとして参加してもらいます。
このエンジニアの中には、Webアプリケーションエンジニア、テストエンジニア、データサイエンティストといった多様なロールのメンバーが含まれています。
また、ファシリテーターの選出には、偏りや重複が出ないように、シフトを組んで回す形をとっています。
そのため、チーム内のエンジニアは、おおよそ1ヶ月に一度、どこかのチームのふりかえりをファシリテーションすることになっています。

SPEEDA開発チームにおけるファシリテーターの役割

ではファシリテーターは何をしているのか、というと、以下のような役割を担っています。

議論しやすい場の設定

ユーザベースの六本木オフィスにはラウンジと呼ばれる社内ミーティング用の広めのスペースがあります。
ファシリテーターはそこにホワイトボードと人数分の椅子をセットし、参加者が議論ができるようなスペースを作ります。
その際には、椅子の配置をメンバーの顔が全員見えるように、かつホワイトボードが見やすいように、ホワイトボードを囲むような形で椅子を設営します。

議論しやすい場作りは、ふりかえりの最中にも行われます。
あるメンバーがあまり議論に参加できていなければ、ファシリテーターがそのメンバーに発言を促したり、議論が脇道にそれてきた際には、議論の進行をメンバー全員で確認したり、と言ったことを行います。
こうした場作りによって、参加者がより議論をしやすい場面を作るのが、ファシリテーターの役割の一つです。

アクティビティの選定を含めた全体の進行

ファシリテーターは、ふりかえりたいテーマに合わせてアクティビティの選定や、タイムスケジュールの設定、タイムキープを行います。
SPEEDA開発チームのふりかえりでは、KPTとよばれるメジャーな手法以外にも様々なアクティビティが用いられています。
例えばイテレーションで起こったことを満遍なくふりかえりたい場合にはタイムライン*1、各メンバーが思っていることや感情に着目してみたいときにはチームレーダー*2や喜怒哀*3、と言った具合に選出を変えていきます。

ファシリテーターの好みとセンスに応じてどのような手法を用いるかが決められるので、参加者は毎回新鮮な気持ちでふりかえりに臨むことができています。

アクティビティは、アジャイル・レトロスペクティブズFun Retrospectivesを参考にして選んだりしています。
特にアジャイル・レトロスペクティブズは、私たち開発チームのメンバーの教科書のようなものになっており、私もファシリテーションを行う際に読み返したりしています。

ファシリテーターを置くメリット

チームの外側からファシリテーターを招くことで、以下のようなメリットを実感しています。

ファシリテーターが他のチームへの理解を深めることができる

SPEEDA開発チームでは、定期的にプロジェクトチーム間での入れ替えを行なっています。
これは、プロジェクトに関する知識、技術を特定個人に偏らせないようにするための施策です。

ファシリテーターとして他のチームのふりかえりに参加することで、他のチームの現在の状況を高い解像度で知ることができます。
結果、いつでもチームメンバーの入れ替えを行うことができるようになっている状況を作り出す一因となっています。

ファシリテーターが問題を構造的・俯瞰的にとらえる能力が高まる

ファシリテーターには、チームで議論されている問題を俯瞰してとらえることが求められます。
議論の進行に合わせて、参加者に様々なアクションを促し、より良い解決策に導く必要があるためです。
ファシリテーションは問題を構造的、俯瞰的にとらえる絶好の機会となっています。

チームのメンバーが議論に集中することができる

これはチームメンバーの視点から見たメリットなのですが、ファシリテーターを別途置くことで、参加者が残り時間などに気を取られず、議論に集中することができるようになります。
結果、短い時間の中でも、腰を据えた議論ができるようになり、より効果的なアクションが打てるようになります。

さいごに

私たちはふりかえりを定期的に実施していますが、同時に「ふりかえりに甘えてはいけない」ということもチーム内で話しています。
これは、ふりかえりの場を設定しているからと言って、チームの問題や改善策について議論する場をそこだけに限定してはいけない、という意図からきているものです。
私達のチームでは直接的な対話を重視して仕事をしており、何かチームで議論、改善するべき事項が出た際には、そのタイミングで議論をするようにしています。
この文化とふりかえりの両輪で、チームとしてより良い姿を目指して日々開発を行なっています。

私たちが目指すチームの姿は、CTO林の以下の記事で紹介しています。
journal.uzabase.com
興味を持たれた方は、是非以下までご連絡ください!
Uzabase, Inc. - Jobs: SPEEDA ソフトウェアエンジニア(サーバー/フロント) - Apply online


それでは良いふりかえりライフを!

*1:時系列に沿って、チームで起こったことや、チームメンバーが感じたことを付箋に書き、貼り出すアクティビティです。一定期間内での時間、因果関係に沿ったデータを収集するのに適しています。

*2:チームで大切にしている価値観(XP Valuesを用いたことがあります。)をレーダー上で表し、各チームメンバーがどのように評価するかを5点満点などでプロットします。その後、プロットされた点数を元に、何故そう思ったかをディスカッションしていきます。

*3:その名の通り、「喜ばしいこと」「怒りを覚えたこと」「哀しかったこと」という軸でデータを収集します。チームメンバーの感情面に着目するアクティビティです。

appiumを使ってモバイルアプリのテストを自動化する

こんにちは!SPEEDA プロダクト開発チームの板倉です。
前回は、テスト環境(appium + gauge + kotlin)のセットアップについて書きました。

appiumを使ってモバイルアプリのテストを自動化する ~環境構築まで~ - UZABASE Tech Blog

今回は、実際にアプリのテストをどう書くのかについて書いていこうと思います。

テスト対象のアプリ

今回はAndroid StudioでBasic Activityを選択してプロジェクトを作成したアプリを例にテストを書いていきます。

f:id:diskit:20190829081840p:plain

f:id:diskit:20190826075829p:plain:w300

今回はプロジェクト作成時に作られたコードをそのまま使用しています。

テストを書く

Spec, Scenario, Step

まずは、画面表示までScenarioを書いていこうと思います。
アプリを起動すると何が表示されるのかをStepに記述します。

# ホーム画面

## ホーム画面を表示する
* ヘッダにタイトル"example"が表示される
* ヘッダにアクションボタンが表示される
* コンテンツにメッセージ"Hello World!"が表示される
* フローティングアクションボタンが表示される

GaugeのSpecの書き方として、
# は Spec
## はScenario
* はStep
となります。

Stepの実装を書いていく

Stepに記述した文章はアノテーションに記述します。
ダブルクオートで囲んだ部分はパラメータになりますので、メソッドに引数として定義します。
1つ目のStepを例に実装すると以下のような形になります。

@Step("ヘッダにタイトル<title>が表示される")
fun ヘッダにタイトルが表示される(title: String) {
}

メソッドの中に操作や検証を記述していきます。
操作や検証をするにはセレクタを使用して要素を取得して行います。
Specに記載した内容を実装すると以下のようになります。

@Step("ヘッダにタイトル<title>が表示される")
fun ヘッダにタイトルが表示される(title: String) {
  val header = driver.findElementById("toolbar")
  header.findElement(By.className("android.widget.TextView")).text shouldEqual title
}


@Step("ヘッダにアクションボタンが表示される")
fun ヘッダにアクションボタンが表示される() {
    val header = driver.findElementById("toolbar")
    header.findElement(By.className("android.widget.ImageView")).isDisplayed shouldBe true
}

@Step("コンテンツにメッセージ<message>が表示される")
fun コンテンツにメッセージが表示される(message: String) {
    // xpathは避けたほうがいいです。
    // そのままのコードを使ったので、開発する際はidを設定するなど工夫が必要だと思います。
    val content = driver.findElement(By.xpath("//android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup"))
    content.findElement(By.className("android.widget.TextView")).text shouldEqual message
}

@Step("フローティングアクションボタンが表示される")
fun フローティングアクションボタンが表示される() {
    val button = driver.findElementById("fab")
    button.isDisplayed shouldBe true
}

この実装をする上で、ヘッダのタイトルのセレクタを記述する際に困りました。
そういったときに使えるツールを紹介します。

Appium Desktop

AppiumはGUIツールが公開されています。
このツールはインスペクターだけでなく、Appiumサーバーの起動も行えます。

github.com

アプリのダウンロードはこちら

確認してみる

今回困ったヘッダのタイトルの要素を見てみると、

f:id:diskit:20190829080347p:plain

左側に表示しているアプリ上でヘッダのタイトルをクリックすると、中央のAppSourceに表示されたソースから要素がハイライトされ、右側には要素の情報を見ることができます。
今回はツールバーをid指定で取得し、その要素からclassNameを指定して取得するセレクタを使用してタイトルの要素を取得することにしました。

さいごに

AppiumからSelenium Driver(AndroidDriver, IOSDriverなど)が提供されているので、思ってた以上に簡単にテストを書くことができました。
今回試したのはアプリに表示されている要素の取得と検証のみだったので、ネイティブ特有の機能を使う場合どのような実装が必要なのかは調べてみようと思います。

appiumを使ってモバイルアプリのテストを自動化する ~環境構築まで~

こんにちは!SPEEDA プロダクト開発チームの板倉です。
前回書いた時はNewsPicksの開発をしていましたが、今はSPEEDAの開発をしてます。

SPEEDAのプロダクトチームでは、以下の記事のようにE2EをGaugeとKotlinを使って書くことが多いです。

Gauge Test Automation Toolとアジャイル開発 - UZABASE Tech Blog

GaugeのConceptを用いてテストシナリオをより仕様書のように記述する - UZABASE Tech Blog

GaugeのParameterを使いこなす - UZABASE Tech Blog

今回は、NewsPicksでアプリ開発をやっていたこともあり、モバイルアプリではE2Eをどうやるのかなということで調べてみました。

環境構築

appiumのインストールとテストプロジェクトの準備について説明します。
※ アプリ開発に関する環境構築については割愛します。

appium

npmを使ってインストールします。

npm install -g appium

プロジェクト

MavenやGradleプロジェクトを作成したのち、以下の依存関係を追加します。

Maven Repository: com.thoughtworks.gauge » gauge-java
Maven Repository: io.appium » java-client

作成したプロジェクトでコマンドを実行してgaugeプロジェクトに必要なファイルを展開します。

gauge init java

実行後のプロジェクトは以下のようなディレクトリ構造になります。

<PROJECT_ROOT>
├── build.gradle
├── env
│   └── default
│       ├── default.properties
│       └── java.properties
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── libs
├── manifest.json
├── settings.gradle
├── specs
│   └── example.spec
└── src
    ├── main
    │   ├── java
    │   ├── kotlin
    │   └── resources
    └── test
        ├── java
        │   └── StepImplementation.java
        ├── kotlin
        └── resources

あとはIDEの機能を使って test/java/StepImplementation.java をkotlinに変換し、test/kotlin 以下に移動します。
これでプロジェクトのセットアップは完了です。

appiumのセットアップ

appiumを使ってアプリを操作する際は、SeleniumのRemoteDriverを通して行うことになります。
DriverにCapabilitiesを指定する必要があり、Androidでは最低限以下の2項目を設定することでテストを行うことができます。

キー 設定内容
deviceName テストを実行する端末の名前 Android Emulator
app テストをするアプリ(apk)の絶対パス /Users/user/workspace/project/test-app.apk

こちらにもあるようにAndroidではdeviceNameが設定しても無視されるようです(2019.7.31時点)。

On Android this capability is currently ignored, though it remains required.

Desired Capabilities - Appium

コードで書くとこんな感じです。

fun <T: WebElement> driver(): AppiumDriver<T> {
  val app = File("example.apk")
  val capabilities = DesiredCapabilities().apply {
    setCapability("deviceName", "Android Emulator")
    setCapability("app", app.absolutePath)
  }
  return AndroidDriver(capabilities)
}

このコードだけでも実行すると appium server が実行され、指定した端末にアプリをインストールし起動するまでが行われます!
※ エミュレータは起動しておく必要があります。

次回は

今回は環境構築まで書いてみました。
次回は実際にアプリをテストするコードを書いてみたいと思います!

さいごに

プロダクトチームは一緒にSPEEDAを開発する仲間を募集しています!
プロダクトチームってどんな感じなの?という疑問に対しては、以下の記事が参考になるかと思います!

journal.uzabase.com

少しでも興味のある方はご連絡お待ちしております!!!

使う技術は自分たちで決める。XPを極め技術を追い求めるエンジニア募集! - 株式会社ユーザベースのWeb エンジニア中途の求人 - Wantedly

「ここではすべてが流れている!」SPEEDA の開発チームに入って驚いた 3 つのこと

7 月から SPEEDA 開発チームに参加しました、野口です!

SPEEDA 開発チームでは、XP のプラクティスを大きく取り入れて日々の開発を進めています。
私は入社前から XP やスクラムのようなアジャイル開発手法とその考え方には慣れ親しんでいたのですが、SPEEDA 開発チームに参加してみて、ユニークだなと感じたことがいくつもありました。

この記事では、SPEEDA 開発チームで私が特にユニークだと感じた 3 つのことについて紹介します。

おことわり

この記事の目的は、SPEEDA 開発チームのユニークな文化を紹介することです。
これらの取り組みには、SPEEDA 開発チームの現在のフェーズに固有の部分も少なからずあると思っており、これを読まれた方の組織やチームで、ここに書かれているようなことをただちに実践することを勧めるものではありません。

私自身、チームに入ってからしばらく経ちますが、まだ十分には消化しきれていない部分もあり、これからも実践しながら考え続けていきたいと思っています :)

その 1: チームのメンバーを「安定させない」

SPEEDA 開発チームは、開発を担う機能エリアごとに、数名程度の小さなチームに分かれています。(以後、この「小さなチーム」のことを単に「チーム」と呼ぶことにします)

日々の開発(スタンドアップや見積もり、ふりかえり等を含む)はこのチームごとに行うのですが、このチームのメンバーは、数ヶ月に一度入れ替えを行っています。実際、私が入社してからも一度入れ替えがあり、7 名いたメンバーのうち 2 名が交代しました。 *1

個人的には、チームのメンバーがお互いの個性やチームの文化・慣習を知り、チームとして安定してパフォーマンスを出せるようになるには時間がかかるため、頻繁な入れ替えは避ける方がよいと考えていました。

では、なぜあえてチームメンバーを安定させず、メンバーが「流れる」ようにするのでしょうか?
今のところ、私は以下の理由があると考えています。

  • やる理由: 製品全体についての知識、および文化を共同所有するため。
    • 個々のチームに知識を閉じず、開発チーム全体で製品全体を共同的に理解し、開発・保守・運用できるようになることが目的。また、文化についても同様。
  • できる理由: 「さまざまなものが流れる」文化があるため。
    • チームメンバーの入れ替えに限らず、SPEEDA の開発チームでは多くのものを安定させず、常に流れる状態に置いている。「流れる」ことが定常状態であるため、メンバーの入れ替えもそのうちの一つにすぎず、チーム・メンバーのいずれも入れ替えにすばやく適応できる。

よく見ると、「やる理由」と「できる理由」は表裏一体になっています。メンバーを入れ替えることによって知識や文化が撹拌され、それによってさらに入れ替えがやりやすくなる、という正のフィードバックの関係です。

その 2: 属人化を防ぐためにドキュメントを「残さない」

SPEEDA 開発チームでは、ドキュメントを用いることがかなり稀です。

アジャイルソフトウェア開発宣言では「包括的なドキュメントよりも個人と対話を」価値とする、と言われていますが、これほどまで対話(特に口頭での会話)に重きを置くソフトウェア開発組織はかなり珍しいのではないかと思います。

アジャイルソフトウェア開発宣言でも「ドキュメントは不要」とまでは言っていない、とはよく言われることで、個人的にも、安定して開発を進めるためには必要十分な量のドキュメントを書いた方がよいと考えていました。

では、なぜドキュメントを「最少限」と言い切れるほどのレベルにまで減らすのでしょうか?
現在の私の理解では、以下の理由があると考えています。

  • やる理由: ドキュメントを減らせば、会話せざるを得ないため。
    • やや本末転倒にも見えるが、知識が「流れる」ために必要な仕掛けと言える。
  • できる理由: 知識は撹拌されており、また常に会話する文化があるため。
    • 「その 1」で紹介したように、SPEEDA の製品についての知識や文化は常に流動し、撹拌されている。また、常時ペアプログラミングを行なっていてペア間の会話は絶えないほか、他ペアや他チームからの割り込みも奨励されており、「質問されたらすぐに答える」ことが徹底されている。

おそらくお気付きのように、ここにも正のフィードバックがあります。ドキュメントを減らせば会話が増え、会話が増えれば、さらにドキュメントを減らせるようになります。
よく「属人化を防ぐためにドキュメントを残す」と言われますが、私たちのチームでは、いわば知識が「チーム全体に属人化(属チーム化?)」したような状態といえます。

その 3: 担当者を「明確にしない」

SPEEDA 開発チームでは、「担当者を明確にしない」ことがよくあります。

たとえば、チームでのふりかえりを行なった際は次週に取り組むアクションを決めますが、私が初めてチームでのふりかえりに参加したとき、アクションの担当者を決めずにふりかえりが終わったことに驚きました。「ちゃんと決めておかないと、みんな忘れてしまったらどうするの?」と思ったのですね。

しかしこれは杞憂でした。当たり前といえば当たり前ですが、誰かが覚えているのですね。
アクションは次週にきちんと行われました。

とはいえ、一般論としてはアクションの担当者を決める方が確実です。私が初めて会ったチームのふりかえりのファシリテーターだったら、「誰か担当者になってくれますか?」と聞くと思います。

他にも、SPEEDA 開発チームでは、普通なら担当者を決めるであろう場面で、あえて決めない、とすることが多くあります。

これはなぜでしょうか?
私の考えでは、以下が理由です。

  • やる理由: 「活動の最小単位」をチームとし、何らかのタスクが個人に帰属することを防ぐため。
    • 何かを個人のタスクとしてしまうと、そのプロセスについての知識と、責任が個人に紐づいてしまう。そうならないよう、万事を個人ではなくチームの関心事にとどめるために、担当者を決めない。
  • できる理由: いつでも活動の最小単位をチームとしており、そうすることに慣れているから。
    • つまり、普段からそうする訓練をしているから。

「やる理由」についてはともかく、「できる理由」については「やっていたらできるようになった」という理由しか思いつきませんでした :)
チーム全体でこれを意識していると、不思議と補い合うようになります。失敗することも時にはありますが、その都度会話して対応します(ただし、できる限り「担当者を決める」以外の方法で)。

「その 1」と「その 2」では「知識」や「文化」が流れていましたが、ここでは「担当」が流れています。*2

そうやってどこへ行きたいのか、そしてこれから

ここまで紹介してきたように、SPEEDA の開発チームでは多くのものが「流れて」います。

私の個人的な感覚では、「ほとんど全てが流れている」と言ってもいいように思います。他にも、今回は紹介しなかったペアプログラミングのやり方や、ミーティングの進め方、製品のデザイン等々、多くの場面が「流れ」の中にあるように感じます。

では、「流れ」それ自体が目的なのでしょうか?
おそらく、Yes でもあり No でもある、と言えます。

ソフトウェア開発という営み自体が絶え間ない流れの中にあり、SPEEDA はソフトウェアサービスなので、その開発チームにあって流れは重要です。そのため、「流れ」はそれ自体として目的の一つになりえます。

一方で、「流れ」は究極の目的ではないはずです。
ユーザベースには「経済情報で、世界を変える」というミッションがあり、それを支える SPEEDA プロダクトチーム(開発チームもここに属する)のミッションは「技術力で、ビジネスをリードする」です。だとすれば、この「流れ」の文化はそれを支えるものであるはずだし、そうあるべきです。

たとえば、以下のインタビュー記事で CTO の林が掲げている「最高の開発チームをつくる」は、「流れ」によって支えられる大きな目標の一つなのでしょう。

journal.uzabase.com

私はまだチームに参加して 2 ヶ月足らずですが、これからもチームで仕事をしていく中でこの流れの文化の究極の意義を見つけ出し、発展させていくことを楽しみにしています!

*1:ちなみに入れ替えを行うこと自体は半ばルール化されていますが、誰が入れ替わるかはメンバー自身の意思によって決まります

*2:もしかしたら、チームの中を「漂っている」と言った方がより的確かもしれません。もっとも「チーム」という単位で見れば、担当は「定まっている」とも言えますが、そのチームのメンバーでさえ入れ替わるので、やはり流れている、漂っているという表現の方が的確に思えます