TECH PLAY

キャディ株式会社

キャディ株式会社 の技術ブログ

234

CTO自ら コードゴルフ のキャディをやってみた こんにちは。キャディCTOの小橋です。キャディは特注部品の調達支援などを行っている製造業スタートアップです。8月に約80億円のシリーズBの資金調達を受けて 今後のキャディについての記事 を書かせていただきました。   そんなキャディですが社名の「キャディ」にはどんな意味が込められているか、皆さんご存知ですか? 実は、社名の「キャディ」という名前は、製造業に向き合う皆さまにとっての「プロゴルフにおけるキャディ的な存在」となり、メーカー様や加工会社様の事業に伴走し、ともに改善や成長を続けるという意味が込められています。(でも、他の意味もあります)   そして、ソフトウェアエンジニアにとってのゴルフといえば、 そう、 コードゴルフ ですね!  ということで、ソフトウェアエンジニアの皆さんにとってのキャディとなるべく、私自ら コードゴルフ のキャディをやってみることにしました。   コードゴルフ とは…? コードゴルフ はコンピュータプログラミング・コンテストの一種。参加者は与えられた アルゴリズム を、可能な限りもっとも短い ソースコード で記述することを競う。バイナリサイズではなく、 ソースコード の文字数がスコアとなる。「 Perl golf」など、トーナメントで使用される プログラミング言語 の名前がつけられることもある。ショートコーディング、コードパズル等とも呼ばれる。 ( WikiPedia より抜粋)   競技プログラミング や コードゴルフ のご経験がない方もいらっしゃると思います。でも、大丈夫です!  私がキャディとして、皆さんの コードゴルフ に伴走します!   ゴルファー同士がオンラインでコードの文字数を競い合う形ではありませんが、クリア基準となる文字数を設けることで、一人用 コードゴルフ 的なことができるものをこの記事には組み込んであります。(そういう意味で本来の コードゴルフ とはちょっと異なることをご容赦ください)   なお、本記事ではpaiza.io API を利用してコードを実行しています。 paiza.io paiza.ioの各言語のバージョンや制約について   コードゴルフ の解き方は、文字数を競うという点を除けば 競技プログラミング とだいたい同じです。標準入力にテストケースが与えられるので、標準出力に処理結果を出力するコードを実装します。   例えば、python3での入出力を行うだけのコードはこんな感じになります。 hoge = input() answer = hoge # なにか処理 print(answer) もし、こういったコードの実装に慣れていなければ「 競技プログラミング 標準入出力 使いたい言語名」などで検索すると良いでしょう。   ただ、 コードゴルフ では文字数をできるだけ少なくする必要がありますから、可読性は無視して、使う変数の数を減らしたり、 シンタックス シュガーをフル活用したりなど、短い書き方を追求していくことになります。   最初の問題はクリア基準も緩めにしてありますから心配ありません。(後半は結構難しいかも) それでは、各ホールを回っていきましょう! 1番ホール「板金」 まずは簡単な問題から コードゴルフ をはじめましょう。「板金」に関する問題です。     「板金」とは薄い金属の板のことで、板金加工は箱型やプレート型の部品を作るのに使われるごく一般的な金属加工です。上記画像で私が持っているのはキャディで製作したRustマークの板金加工品です(笑)。キャディは今でこそ切削や製罐など様々な金属加工品を対応していますが、2017年の創業当初は板金加工だけでした。そういう意味でキャディの原点とも言えますね。 問題 縦Hセンチ横Wセンチの長方形の形をした板金があります。この板金から正方形の板金を1つ切り出します。切り出される板金の面積が最大になるように切り出すとき、その面積は何平方センチメートルになるか求めて、それを出力してください。 制約 \\ 0 \le H, W \le 10^5 \\ 入力形式 H W 入力例 10 5 出力例 25 クリア基準 180文字   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> 長方形ないし正方形から正方形を切り出すには、短い方の辺に合わせて切り出せば良いですね。   ところで、「キャディ」という社名に込めた意味は、冒頭で少し触れた通り、製造業に向き合う皆さまにとっての「プロゴルフにおけるキャディ的な存在」という意味があることはお伝えした通りです。 しかし、それだけではなく、 実はもう一つの意味があったりします。   「キャディ」に込めたもう一つの意味は… 1番ホールをクリアした皆さんだけにお伝えしましょう。   ギブアップする(コードゴルフを解かなくても続きが表示されます) 社名「キャディ」に込められたもう一つの意味とは…?   キャディをアルファベット表記すると CADDi になりますが、これは CAD Di rect の略であったりもします。設計ツールである CAD からダイレクトに製作の発注ができること。これはキャディが目指す理想の一つであり、創業期にも 3D CAD データを使った自動見積の仮説検証を行っていたりしました。 また、そういった理想を目指すために現在も図面データの情報抽出に関する研究開発を進めていたりもします。   CAD Direct という社名の由来に込められた理想は、 「物理とデジタルの橋渡し」が高度に実現された状態 と言えるのではないでしょうか。残念ながら、ありとあらゆる部品を CAD から即座に発注できるような仕組みはまだ少し時間がかかりそうですが、そういったものを実現していくために必要な物理とデジタルの連携を、キャディでは少しずつ、けれど確実に進めています。 製造業で物理とデジタルの橋渡しをするために 製造業を設計、調達、製造、販売といった形でジャンルを分けた場合、設計は 3D CAD や CAE 、製造はロボット化をはじめとするファクトリオートメーション、販売については 機械学習 や統計解析を使った マーケティング の高度化など、様々な DX や イノベーション が行われてきたと思いますが、調達の分野においてはまだまだ改善の余地があると考えています。   製造業は物理的なものを扱う業種です。製造業の調達の分野こそ、設計情報という最初はデジタルデータとなっている情報を、実際に実物に変換するところまでを 一気通貫 で見れる、まさに物理とデジタルの橋渡しが必要な領域だと考えています。   キャディにおいても、調達領域への支援として特注金属加工部品の受発注を進める上で、以下のようなシステムを内製開発することで、物理とデジタルの橋渡しを進めています。   KLEIN: サプライチェーン 情報 QUIPU: 原価計算 情報 HERODOTUS: 設計図面情報 パートナーポータル: 加工パートナーとの連携システム   それぞれ詳しくは こちらの記事 も是非ご確認ください。   それ以外にも、キャディでは関東関西に検査拠点兼物流拠点を設けていますが、そこでは バーコードリーダ ーを使ってリアルタイムに部品の検査ステータス等を把握するなど、様々な技術を現場に導入して検証を繰り返しています。   将来的にも様々なテーマがあります。製品の在庫管理の高度化や、検査プロセスにおける寸法計測やキズ検知の自動化など、あらゆる受発注の工程にあたって圧倒的な品質や速度をお届けできるように挑み続けます。   私は、リアルのモノを扱っている事業会社のCTOとして、物理とデジタルの世界の橋渡しをすることを通じ、今までは想像もできなかった未来を実現していきたいと思っています。そのために人と技術が協力して事業を推進していきたいですね。   それでは、そろそろゴルフに戻りましょう。次の問題は少し難しいと思う人もいるかもしれません。 2番ホール「シャフト」   次の問題は「シャフト」に関する問題です。シャフトとは金属の棒状の部品のことで、上記画像で私の手のひらに浮いているのもシャフトの一種ですね。   シャフトは、主に回転機構を持つ様々な機械に使われている部品で、ある程度は量産されている汎用品もありますが、キャディで請け負っている特注品のシャフトは、産業機械の動力部分にあたる場合もあり高い加工精度が求められることもあります。 問題 2つのシャフトはジョイントを使って連結することができます。長さXのシャフトと長さYのシャフトを連結すると長さX+Yの連結シャフトになります。   強度の関係で3本以上のシャフトを連結することはできません。   シャフトがN本あります。i番目のシャフトの長さはA_iです。   K個のジョイントがあります。これらを全て使って、N本のシャフトの内いくつかを連結すると、連結シャフトと連結していないシャフトが合わせてN-K個できます。このN-K個の全ての組み合わせの中で、N-K個のうち最も長さが短いものが、他の組み合わせの場合よりも最も長くなる場合の長さを求めてください。 制約 \\ 0 入力形式 N K A_1 A_2 ... A_N 入力例 20 10 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 14 14 出力例 19 クリア基準 300文字   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> 2K=Nの場合はどうなりますか?   この問題は、 競技プログラミング や コードゴルフ に慣れていない方だと少し苦労するかもしれません。   ギブアップする(コードゴルフを解かなくても続きが表示されます) 2番ホールクリアおめでとう! だが、残念だったな!   私は悪のEM(Engineering Manager)の平岩だ!  残念だったな、この記事は私が乗っ取った!   2番ホールをクリアしたみんなには、EMの平岩に延々とキャディへの入社を口説かれる、 カジュアル面談と言いながらうんざりするほど採用色たっぷりのカジュアル面談 をする権利をやろう!   平岩がなぜキャディに入社したか にはじまり、日々のキャディでの業務、キャディ本社(蔵前)まわりのランチ事情、代表加藤の 黒歴史 、平岩が個人的に最近ハマっていることまで、聞いてもいないどうでもいいことを延々と数十分聞かせてやるのだ!     どうだ嬉しいか? 嬉しいだろう! ガハハハハハハハハハハハハ……   <ナレーター> なんてことだ!! この記事は面接の時は圧強めで怖いと噂(実話)の悪のEM平岩によって乗っ取られてしまった! 今こそ本当のキャディパワー(謎)を集め「モノづくり産業のポテンシャルを解放し」悪のEM平岩を打倒しなければ!! モノづくり産業のポテンシャルを解放する 3番ホール「CAD Direct」 ありがとうございます! 皆さんのキャディパワー(謎)により、モノづくり産業のポテンシャルが解放されかけていますが、まだ足りません。     そして、伝説の「CAD Direct」の封印が解かれようとしています!!   ソフトウェアエンジニアの皆さん! 今こそ最後の問題を解いて「CAD Direct」の封印を解き、 モノづくり産業のポテンシャルを解放するのです! 問題 C,A,Dの3種類の文字から構成される長さNの文字列Sが与えられます。文字列Sに対して 以下の操作を可能な限り繰り返します。   操作:文字列Sから文字C,文字A,文字Dをちょうど1つずつ削除する。   1回操作をするたびにX点獲得できます。また、取り出したC,A,Dがこの順に連続していたらCAD DirectボーナスでさらにY点獲得できます。   獲得できる点数が最大になるよう操作を行ったときに、獲得できる点数を出力してください。 制約 \\ 0 入力形式 N x y S 入力例 48 850 33 CDDDDDACCCCCDAAAAACACCADCADCADADCCADACADDCADCADD 出力例 13963 クリア基準 文字数制限なし、解けたらOK!(もはや コードゴルフ じゃない)   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> ヒントはありません!   ギブアップする(コードゴルフを解かなくても続きが表示されます) 伝説の「CAD Direct」の封印が解かれた!   皆さんありがとうございます。皆さんのお力により「CAD Direct」の封印が解かれ、キャディの平和は守られました! エンジニア募集中! キャディでは各種エンジニア絶賛募集中です! こちらのサイトで求人をご確認いただけます! @paiza転職へ 謝辞 問題作成協力:Ken Ogura クリエイティブ作成協力:Minami Yamada powered by paiza.io 実行中 .hidden { display: none; } pre.input_form { margin-left: 30px; padding: 10px; background-color: #eee; } pre.error { margin-top: 15px; padding: 10px; color: red; background-color: #f0f0f0; font-size: 12px; } .code { padding: 10px; margin: 10px; background-color: #eef; } .indicator { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: blue; color: white; padding: 10px; animation: flash 1s linear infinite; } @keyframes flash { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } button.giveup { background-color: red; font-size: 18px; } button.release { background-color: yellow; color: blue; font-size: 24px; } a.info { display: block; font-size: 12px; margin: 20px 0; } .katex { font-size: 1em; } (function() { var apiKey = "oem:caddi:ceawnzx3s8xdmnev"; var buttonDisabled = false; function showIndicator(text) { buttonDisabled = true; if (text) { $("#indicator").text(text); } $("#indicator").removeClass("hidden"); } function hideIndicator() { $("#indicator").addClass("hidden"); buttonDisabled = false; } function executeTests(code, language, originalInputs, originalOutputs, allClear, showRuntimeError) { var testNum = originalInputs.length; var inputs = originalInputs.concat(); var outputs = originalOutputs.concat(); function onError(req, status, error) { alert("エラーが発生しました" + " status:" + status + " error:" + error); hideIndicator(); } function watchRunner(id, onComplete) { setTimeout(function() { $.ajax({ cache: false, type: "GET", url: "https://api.paiza.io/runners/get_status", data: { id: id, api_key: apiKey }, success: function(data) { if (data && data.status !== "completed") { watchRunner(id, onComplete); } else if (data && data.status === "completed") { onComplete(); } else { onError(null, "N/A", "watch runner failed"); } }, error: onError }); }, 500); } function getRunnerResult(id, resultHandler) { $.ajax({ cache: false, type: "GET", url: "https://api.paiza.io/runners/get_details", data: { id: id, api_key: apiKey }, success: function(data) { resultHandler(data); }, error: onError }); } function test(input, resultHandler) { $.ajax({ cache: false, type: "POST", url: "https://api.paiza.io/runners/create", data: { source_code: code, language: language, input: input, api_key: apiKey }, success: function(data) { if (data && data.id) { watchRunner(data.id, function() { getRunnerResult(data.id, resultHandler); }); } else { onError(null, "N/A", "create runner failed"); } }, error: onError }); } function executeTest(input, output) { showIndicator("テストケース " + (testNum - inputs.length) + " 実行中"); test(input, function(data) { if (data && data.stdout && data.stdout.trim() === output) { if (inputs.length === 0) { hideIndicator(); allClear(); } else { executeTest(inputs.shift(), outputs.shift()); } } else { hideIndicator(); var err = ""; if (data.build_stderr) { err += "[build stderr]\n" + data.build_stderr; } if (data.stderr) { if (err !== "") { err += "\n\n"; } err += "[stderr]\n" + data.stderr; } if (err === "") { alert("回答コードが正しくありません"); } else { alert("実行中にエラーが発生しました"); showRuntimeError(err); } } }); } showIndicator("コード実行準備中"); executeTest(inputs.shift(), outputs.shift()); } function startRunner(textAreaId, selectId, errorId, inputs, outputs, allClear) { var code = $("#" + textAreaId).val(); var language = $("#" + selectId).val(); var error = $("#" + errorId); if (code.trim() === "") { alert("回答コードを入力してください"); } else { error.text(""); error.addClass("hidden"); executeTests(code, language, inputs, outputs, allClear, function(errMes) { error.text(errMes); error.removeClass("hidden"); }); } } function watchAnswer(lengthId, answerId) { var f = function(event){ var len = $(event.target).val().length; $("#" + lengthId).text((len > 0) ? "(" + len + "文字)" : ""); }; $("#" + answerId).keyup(f); $("#" + answerId).blur(f); } function onload() { watchAnswer("length_1", "answer_1"); $("#button_1").click(function() { if (buttonDisabled) { return; } var threshold = +$("#threshold_1").text().replace(/[^0-9]/g, ""); startRunner("answer_1", "language_1", "error_1", testCase1In, testCase1Out, function() { var score = $("#answer_1").val().length; if (score
はじめに 今までの歩み ドメインモデル構築フェーズ 必要なデータの取扱及び蓄積フェーズ KLEIN QUIPU HERODOTUS パートナーポータル 個人の力を組織の力に 今後への期待と投資 物理とデジタルの橋渡し データを活用するフェーズ プラットフォームチームの立ち上げ 事業のグローバル展開 新規事業の立ち上げ こんな人ウェルカム! もしご興味ありましたら、是非ご連絡下さい はじめに こんにちは。CTOの小橋です。先程、シリーズBの資金調達を発表させていただきました。ビジネスモデルや事業自体に関しては コーポレートサイト をご覧頂ければと思いますが、今日は拡大する事業を支えるテクノロジーを生み出している組織のお話を出来ればと思います。 急拡大していくとともに、組織的課題も技術的課題も急増してきておりまして、折角の機会なので、これまでの歩みと今後への投資を、技術者としてご紹介出来ればと思います。 今までの歩み ドメインモデル構築フェーズ 2017年末の創業期は社員合計3名。会社経営経験無し。金属加工実経験無し。正直分からない事が多すぎて大変でした。事業的にも知識不足でしたが、ソフトウェアの領域に関しても業界的な流通ファイルフォーマットすら分かっていない状況でスタートしました。当然ですが、適切なOSSのライブラリ選定も出来ず手戻りも発生したり、学びの多い日々でした。 取り扱っているものが特注金属加工品だったので、注文毎に完全オーダーメイドの設計や材料の指定があり、多品種少量の難しさを肌で感じていました。当然ですが知識不足や考慮漏れで不良を起こしてしまったり、納期遅延で慌てて再制作に挑むなど、お客様にご迷惑を掛けてしまった日々の反省を、今でも痛烈に覚えています。 もちろん事業を軌道に乗せるフェーズではありましたが、同時に持続する事業を作る上で製造業のドメインをソフトウェアで表現できるように試行錯誤するフェーズでもあり、全て破壊する覚悟で開発を進め、殆どの時間をデータ設計で溶かしていました。塗装の色もシステム的には「色」という項目をRGBで表現すれば良いかなと高を括っていたら、業界的にはマンセル値という別の表現があったり、そもそも色指定がデジタルな文字列でもなく、物理的な見本が郵送される場面もあったりで、ひたすらドメインモデルの更新やリファクタリングを繰り返していました。 業界に寄り添いつつも、この情報の嵐を構造的に表現していくのがキャディのエンジニアリングでドメイン駆動設計(DDD)を活用している起点でもあります。お作法的な部分もありますが、リアルの世界とソフトウェアの世界を往復する上の心構えとして大切にしてます。ソフトウェアの技術者としてその目標に向けて適切な技術やフレームワークを活用し、継続的にドメインの進化にキャッチアップできる仕組み作りを目指しています。 必要なデータの取扱及び蓄積フェーズ 現在キャディは受発注を進める上で大きく3種類のデータを扱っており、それぞれが別システムとして存在します。具体的な画面のキャプチャや概要を紹介できればと思いますが、名前の由来に関しては面談やイベントなどで聞いて下さい! KLEIN: サプライチェーン情報 QUIPU: 原価計算情報 HERODOTUS: 設計図面情報 パートナーポータル: 加工パートナーとの連携システム データ自体は System of Record 的にデータモデルの境界でシステムや開発チームが分かれているものの、システム間参照できるようにキー情報を通じてデータが繋がっている事が重要なポイントです。データを最大限活用するのには管理会計から設計図まで一気通貫で情報が連動している仕組みが不可欠で、そのためにキャディのビジネスモデルの根幹を支える内製開発を進めています。 KLEIN サプライチェーンを全て構築するので、グラフ構造の物流を表現し、それを最終的にキャディの検査拠点で検査して発送するところまで見届けるシステムです。技術的にも複雑なデータ構造を操作するevent sourcingライブラリを開発したり、疎結合な外部システム連携のためにRabbitMQを活用したり、相当モダンな設計が求められているシステムです。 QUIPU 属人化しやすい見積もりを仕組みで担保するための原価計算エンジンです。設計図は作りたいものを表現した、いわば「お絵描き」で、求められている結果を製造するための工程を逆算して価格に変換する、ドメイン知識が山盛りのシステムです。重量や体積等複数の単位を扱うため、Rustの型システムで単位の整合性を担保しています。 HERODOTUS 設計図面を取り扱うシステムで、スキャンされた図面のバージョン管理含む、情報整合性担保の役割も果たしています。製造業に特化した内製画像解析アルゴリズムを活用して、画像データから受発注に必要な情報抽出もできるように機能開発を続けています。 後でも触れる通り、これまで蓄積された/今後も更に蓄積され続けるデータを基に、コンピュータビジョンから深層学習など幅広い技術を活用して、顧客や加工パートナー、そして社内メンバーがこれまで想像もつかなかった機能を複数実現するフェーズに入っていきます。 パートナーポータル 社内で利用しているKLEIN+QUIPU+HERODOTUSのシステムに溜まっているデータを構造型データとしてそのまま加工パートナーに連携するシステムです。文字数のわりに情報量が少なく、自由自在にエクセルとPDFが添付されるメールと違って、共通フォーマットでの情報のやり取りに挑戦しています。社長から事務スタッフまで幅広い方々に利用されるため、ビジュアルのデザインとUXにも重点を置いて作っています。 個人の力を組織の力に 創業からの4年弱で、エンジニア組織も1人から40人規模まで成長してきました。10人くらいまでは個人の力に依存し、全員が単一障害点になっているリスクを抱えつつ、事業を作り上げてきました。当然ですが、人数的にもチーム間の連携強化やタスクの分解と割り振りが重要になり、チーム会議のファシリテーションや交通整備も必要になってきました。お互いの依存関係も増えO(N 2 )でコミュニケーションが増えるのが目に見えていたので、情報集約とプロダクトの方針を担うプロダクトマネージャーの採用を通じてカオスを何とか整理。エンジニアとして個人の力も大切ですが、チームメンバーが全員最大限活躍できるような組織的フレームワークの重要性を実感したのが社員10人くらいのときでした。 20人を超えると、意図的に個人の力を組織の力に繋げる事がより重要に。システムの複雑化と同時に仲間がどんどん入ってきてある程度オンボードも型化しないと効率が悪く感じるようになってきました。昔は「開発ドキュメント読んでSlackフォローして」でしたが、今では新入社員用の「Welcome Document」もでき上がっています。 多様なバックグラウンドの方々がいる中で、組織自体の開発及びそこに属する人材開発も積極的に取り組むフェーズに入り、エンジニアマネジメントというロールも設けて、技術を事業で活用しつつ、それ自体を支える仕組みづくりにも力を入れるようになったのが20人を超えた一年半前です。まだ出来ていない事や後手後手な部分もたくさんあり、これからも更に変わっていくとは思いますが、それはその時のお楽しみという事でまずは事業を伸ばすことに専念していこうと思います。 今後への期待と投資 物理とデジタルの橋渡し これだけ社内で貴重なデータが溜まっている一方、製造業は当然物理的なものを扱っているため決してデジタルの世界で全てが完結しません。パートナー加工会社はデジタル設計情報を実物に変換していますし、我々の検査拠点では制作物を開梱してノギスやハイトゲージで確認していますし、受発注の全ては最終的に実世界の実態とデジタル予定情報の突き合わせに収束します。今後はより物理とデジタルの世界の往復に挑戦し、データを片方の世界に滞留させない事が operational excellence とも言えるかと思います。 それに向けた要素技術としてはバーコードやRFID含むたくさんの技術がありますが、これらを現場に落とし込んで日々運用できている状態まで持っていくのがリアルビジネスの難しいところで、キャディではソフトウェア技術を活用して自社の現場オペレーションにのせるところまで裁量を持てるからこそ、最終的な事業価値まで見れるからこそ、やりがいがあります。 キャディも検査拠点兼物流拠点を関東関西に設けてリアルタイムに部品の検査ステータス等を把握してオペレーション全体の最適化するために、常に新しい技術を現場に導入して検証を繰り返しています。今後も在庫管理や寸法計測、キズ検知含めてあらゆる受発注の工程に当たって、圧倒的な品質や速度をお届けできるように挑み続けます。 リアルのモノを扱っている事業として、デジタルトランスフォーメーション(DX)というのは物理とデジタルの世界の橋渡しする事を通じて、今までは想像もできなかった世界を描くことだと思います。その世界を実現するために人と技術が協力して事業を推進出来ればと思っています。 データを活用するフェーズ 基本的なビジネスインテリジェンスはもちろん、設計図面等の画像情報の解析にも挑戦しています。伝統的なコンピュータビジョンから深層学習まで幅広い手法の活用を通じて、データを資産として扱える世界を描いています。 キャディの物量とデータと解析技術を活用すれば、非構造型データも構造化できますし、保険会社がリスクモデルを作るのと同様に不具合のリスク予測もできるようになります。大量生産ではパラメータの数が少なく、同じ作業を繰り返すためデータドリブンなアプローチが利用できますが、我々の向き合っている多品種少量生産になるとパラメータが数百倍増えるため伝統的な分析方法では難しくなってきています。だからこそ機械学習含む多次元での推論技術を利用して、商流のど真ん中にいるからこそ取得出来るデータを分析し、世界初の多品種少量生産のリスクモデルに挑戦したいと思ってます。 これを実現するために新たに仲間を探したくデータグループの立ち上げを真面目に検討してます。創業から4年近く蓄積してきた知見とデータを今後の投資としてもフル活用していく上で、データを中心に取り扱う仕組みを作るデータエンジニアリングとその分析を推進するデータサイエンスを深堀っていきます。 プラットフォームチームの立ち上げ 今年の7月にPlatform Engineeringチームを立ち上げましたが、それまでは組織構造としてプロジェクト単位に分かれていて、各プロジェクトでインフラ構築や監視からアプリケーション開発まで一気通貫で持っている状況でした。大体のプロジェクトがKubernetesを使っているんですが、学習コストも一定かかって気軽に新しいプロジェクトも始めにくい課題がありました。複数プロジェクトが利用している内製のミドルウェアもあるものの、明確なメンテナンスもない状態でした。 個人的な意見ですが、「インフラチーム」「基盤チーム」「SREチーム」は縁の下の力持ちという存在になり、「何かよしなにに色々やってくれる人たち」「夜オンコールしてくれる人たち」という感覚になりやすいと思います。そもそもプラットフォームチームのミッション自体がインフラに絞られているわけでもないので、そういう空気にはしたくはありませんでした。 プラットフォームチームのミッションは、開発組織のアジリティ向上とイノベーション推進だと思っています。共通化、標準化、自動化を通じて開発者の時間を生み出し、それをまた仕組みや挑戦に投資する事で一定リスクを背負いながらも、より高い目標を目指せると信じています。詳しくは こちらのブログ記事 を是非見て頂ければと思いますが、中期的な投資目線でなかなかリソースを割くのに苦労しているものの、ここにリソースを貼ることが開発組織の更なる成長にはクリティカルになってくると考えています。 デザインシステムを作りたい人、インフラが大好きな人、DevOpsをやってみたい人、nodejsエコシステムのエキスパート等々、とにかく開発組織全体の力になりたい方に助けて頂きたいです。興味ある方はお気軽にTwitterで @caddi.tech までDM下さい! 事業のグローバル展開 モノづくりは幸い言語や文化を問わず価値が伝わる業界で、日本の製造業は世界的にもトップクラスです。海外でも「Made in Japan」は「高品質」と同等に解釈される場面も多く、日本企業としてキャディが受発注網をグローバルに展開するのは創業期から視野に入れていました。 開発しているシステムも日本人ユーザから海外ユーザへと拡大する事で、ローカリゼーションもそうですし、通貨の取扱等ドメインモデルを大きく更新する必要が出てくる事を想定しています。また、物流や納期管理の難度が一気に上がり、よりデータドリブンなリスク管理を通じて、今まで実現不可能だったQCD(品質・価格・納期)を提供出来る事も期待しています。どこまで既存の外部会計システムを利用し、どこまでを内製開発するかまだ決まっていないフェーズですが、このような実世界の複雑な実態をソフトウェアで表現するチャレンジ自体がやりがいなのかなと思います。 新規事業の立ち上げ 私たちは祖業である、受発注プラットフォームの事業を通じていくつかの点に気づきました。 - 一つ目は、受発注の出し手でもあり受け手でもあるわたしたちが自分たちの事業のために内製してきた技術は、同様に受発注をしているメーカーや加工会社にも提供できる可能性があるということです。 - 二つ目は、図面という製造業において非常に重要なドキュメントの管理やデータ活用が、まだこの業界でほとんど解決されていないということです。 - そして一方で(これが三つ目ですが)、この図面というものを解析したり分析したりといった部分に、機械学習など近年で発展が大きい分野の技術が応用しやすいというポテンシャルが存在します。 私たちの新しい試みとして、図面データを軸にした 新しいプロダクト を始めます。既存事業が、受発注プラットフォームという仕組みを通じて最終的には製品(モノ)を納めるのに対して、今回はソフトウェアプロダクトそのものを提供するという形になると思います。そして、既存事業のデータや技術基盤との間に、強いシナジーが生まれる事業になります。 この新しい試みにとてもわくわくしています。 こんな人ウェルカム! 実際やりたい事は山ほどあります。普段これを求人という形で表現していますが、今回は折角なので別の切り口で紹介させてください。以下、全くMECEではないし、大分かぶりあると思いますがもし興味あれば是非! 技術が好きな人 最先端を攻めたい 誰もやったこと無いことを実現したい 裁量持って技術領域広げたい チーム開発が好きな人 お互い尊重してチームで課題を解決したい 周りが働きやすくする事でやりがいを感じる チームの結束や連携を良くすることで嬉しくなる 組織や人が好きな人 人を育てて成長支援するのが好き 事業の事情に合わせた人事制度を作ってみたい 広報活動や技術系のイベントが好き OSSやコミュニティ活動が楽しい 事業が好きな人 ユーザを喜ばし定性的なフィードバックが好き 技術を攻めなくてもしっかりと活用してインパクト出したい 業界問わず自分の腕っぷりで社会貢献したい ものづくり好きな人 自作PCやキーボード等ものづくりが好き 卓上旋盤やフライスでの加工が楽しい PLCやCNCの制御が面白いと思う もしご興味ありましたら、是非ご連絡下さい まずはカジュアルに話を聞いてみたいという方は カジュアル面談フォーム からご連絡ください 個人的に話をしてみたいという方はTwitterで @caddi.tech までDM頂いても構いません。 是非選考を受けてみたいという方は 求人ページ からエントリーください。
こんにちは。 SWE の飯迫 ( @minato128 ) です。 7月1日、CADDi で初めての Tech 組織横断チームとして、山田( @kei711_ ) と一緒に Platform チームを立ち上げました。一般的に Platform チーム/エンジニアの役割は、SRE ほど型化されておらず会社によってやることが異なりますが、過去 2 社の経験や他社事例をみると大きな技術的投資が必要になってくるタイミングで組成されることが多いようです。そこで、CADDi の Platform チームがどういう目的で何をやるチームなのか紹介します。 背景 現在開発チームは 5 つあり、各チームが責任を持ってプロダクトを開発運用しています。これまでは、SRE チームも Platform チームもなく、開発チーム間でよしなに連携して進めてきました。それでも一定なんとかなってはいたのですが、片手間で取り組むには大きすぎる課題はどうしても劣後せざるを得ませんでした。そこで、開発組織もプロダクトもスケールしていく中でも、開発速度や品質を落とさず Whole Product としてユーザーに価値を届けるための技術的投資ができるように、Platform チームを立ち上げることになりました。 ※抽象化した図なのでチーム数やプロダクト数は実際とは異なります 開発チーム それぞれのプロダクトの開発運用に責任をもつ Embeded SRE は存在しないが、それぞれのチーム内では実際その Role に近いことをやっている人もいる Platform チーム 全社共通プロダクトの開発運用に責任をもつ 開発チームと協調して、プロダクトに閉じない課題を解決する CADDi の Platform チームとは 戦略的に「技術課題を解く」ことを目的とする 戦略的に 短期的視点だけでなく、中長期的視点で価値のあることに投資する 4半期単位でできることだけにとらわれない 技術課題を解く インフラ共通基盤 アプリケーション共通基盤 社内標準化(属人性排除) 生産性 RASIS Observability などの課題を見つけて解いていく 直近の具体例をあげると、 認証認可基盤の設計・構築・運用 新規プロダクトの アーキテクチャ 設計・構築 ArgoCD 管理の全社展開 などがあります。 行動指針 技術課題を解くにフォーカスする インフラレイヤーでもアプリケーションレイヤーでもなんでもやる 安易にこぼれ落ちたボール拾って対応しない 柔軟に横断的に動く 組織的事業的に価値を発揮できるならなんでもやる 開発チームに一時的に入るとかもありえる 大きな課題は、横断的なプロジェクトチームの立ち上げてリードする 必要ならなんでも Learning / Unlearning 過去の成功体験、 固定観念 に縛られない 常にパラメーターが違うので、再現性があるか今やることに価値があるかを見極める Have Backbone; Disagree and Commit 信念を持ち、正しいと考えていることを言い続ける そして、決まったことにはコミットする 想定カバー範囲 やること 上の図の通り、カバーする範囲は広めに定義しています やらないこと 情シス的な業務 プロダクトの運用全般 各プロダクトの運用は開発チームが責任を持つ アラートの一次受けも開発チーム 認証認可基盤などの全社共通プロダクトは例外 Frotend, Algorithm, Data Engineering の Core Design/Implementation CADDi にはエキスパートがいるのでこれらは任せる 全体 アーキテクチャ 設計時に協調することはありえる 関わる人 プロジェクトごとに多少差異はあるかもしれませんが、基本的にはチーム内で優先度を決定し、そのプロジェクトの ステークホルダー と合意をとって進めます。 開発チームメンバー CTO PdM EM 開発の進め方 1 スプリントは 1 週間として、 スクラム で開発を進めています。また、タスク管理は Jira を利用し、直近だとこの 3 軸をメインにタスク化していっています。 中長期的にあるべき姿を描いてブレイクダウン アーキテクチャ や構成管理、アクセスコン トロール など 新規のプロダクト 立ち上げのための全体 アーキテクチャ の設計、構築など 開発チームから要望 DX 改善など おわりに CADDi では過去 1 年で見てもかなりのスピードで事業とプロダクトが動いています。だからこそ、まだできたばかりで少人数のチームではありますが、常に柔軟に、ときには中長期視点でエンジニアリングを進めていく必要があります。将来的に組織やプロダクトがスケールしていくと mercari さんの Platform チームのように分業化が進むのかなと想像していますが、今は自分たちのカバー範囲を定義しすぎず、コトに向かってなんでもやっていこうと考えています。 We’re hiring 一緒に課題に向き合ってくれる仲間を募集中です! https://corp.caddi.jp/recruit/
こんにちは😉 @ryokotmng です。 今日は社内ドキュメントの、Rust初心者向けのクックブックを公開しようと思います。 私自身コードを書くのに四苦八苦していた頃にとても助けられたので、Rustをはじめたばかりの方の参考になれば嬉しいです。 目次 [ toc ] はじめに この記事では、 The Book に記載されている知識を前提としています。 Rustを全く書いたことがない方は、先に読んでみることをお勧めします。 サンプルコードが結構長いこと、実行環境があった方が良い内容も多いことから、サンプルコードは大体Rust Playgroundのリンクとなっています。 ぜひご自身で修正して遊んでみてください。 単位つきの計算を型で厳格に縛る 例えば複数の長さの単位 (mm, cm, mなど) を扱う場合に、単位が合っていない長さ同士の計算をする場合、単位を揃える必要がありますね。 この時、最終的に欲しいのは1つの「長さ」、つまりプリミティブな数字のデータになるでしょう。 計算を行うとき、最終的に得たい「長さ」の単位は1つになるので、コード上では単位を比較して異なる場合はエラーを返す、もしくは、異なる単位の長さ同士の計算の場合はある単位に換算したうえで計算できる状態にする、のどちらかの処理が必要になります。 しかし、単位を型として定義すると、計算の実装をする際に単位をチェックするようなコードを書くことなく、異なる単位の長さ同士の計算を実装しようとしたら コンパイル エラーを出すことで、意図しない挙動を防いでくれます。 また、下記のサンプルコードのように、traitを使って単位の変換処理を実装することもできます。 サンプルコード 上記のサンプルコードには、型の書き方の他にも、以下のような多くの知識が詰め込まれています。 Rustの enum が、 JavaScript でいうUnion型のような使い方もできること ジェネリクス PhantomData (幽霊型) これらの概念を知らなくてもさらっと読んで雰囲気を掴むこともできますが、よく使うテクニックのはずなので、慣れていない方はそのような概念をひとつずつ調べながら読むことをお勧めします。 The Book にも記載されているので、ぜひ読んでみてください。 参考 (The Book): Enumを定義する 、 ジェネリクス 、 幽霊型パラメータ 参考 (外部ブログ): Rust で Phantom Type (幽霊型) なお、RustベースのWebエンジンである Servoの内部実装 でも、このパターンが使われています。よければ参考にしてみてください。 エラーハンドリング Rustは基本的に、 f() -> Result<T, E> 型でエラーを伴う処理を表します。 T が成功した場合の値, E がエラーだった場合の型です。 Result<T, E> 型は、パターンマッチで T と E のどちらに値が入っているのか判定できるので、以下のように使うことができます。 match f() { Err(e) => //エラー処理(eはE型の値), Ok(r) => // 成功したときの処理(rはT型の値), } これを利用してエラーハンドリングすると、以下のような処理を書くことが出来ます。 サンプルコード 1 更にRustでは、 ? オペレーターを利用して以下のように書くことも出来ます。 サンプルコード 2 ? を利用するために下準備で必要となるコード量が多いため、実際には以下のcrateを利用したりして使いやすくすることができます。弊社では errer crateを利用しています。 errer errer_derive OptionとResultに対する処理にcombinatorを使う Option と Result について処理を行う場合、match式で場合分けを行いながら処理を進めることも出来ますが、combinatorを使うと短く書くことができて便利です。 なお、Productionでは、 unwrap() , expect() は処理が失敗した場合にpanicを返すので原則使わない方が良いでしょう。 (テストやサンプルコードで使うのは問題ありません。 また紛らわしいですが、 unwrap_or***() 系のpanicを出さないものは問題なく使えます。) サンプルコード Productionコードでは、 Result を返す小さい関数を、 and_then() などのcombinatorを使って合成し、大きい処理を表したりするのに使います。 また単純なエラーハンドリングの場合は、combinatorで頑張らなくても、上記に挙げた ? オペレーターで処理もできるので、読みやすいように適宜調整すると良さそうです。 Rustでは厳密には Monad はありませんが、考え方は使えるので以下の記事などで少しでも理解しておくと理解しやすいでしょう。 参考: 箱で考えるFunctor、ApplicativeそしてMonad なお弊社では、 anyhow クレート を使用しています。エラーにコンテキスト情報を含める機能 ( with_context ) や便利なマクロ ( bail! など),独自のエラー型などを提供しています。 collect() で Vec<Result > と Result<Vec > を相互に変換できる イテレータ の処理で collect() を実行し、返り値として Result<Vec< >> がほしいと仮定します。 単純に collect() を実行した結果、 Vec<Result< >> が返り値となった場合でも、その逆の形に簡単に変換することができます。 以下引用 fn main() { // 全てSomeならSome(配列)を返し、どれかがNoneなら全体もNoneになる assert_eq!([Some(1), Some(2)].iter().cloned().collect::<Option<Vec<_>>>(), Some(vec![1, 2])); assert_eq!([None, Some(2)].iter().cloned().collect::<Option<Vec<_>>>(), None); } 引用元: RustでOptionやResultの配列ができてしまったときの一般的なテク4つ このテクニックを知らないままVecの複雑な処理に直面すると絶望的な気持ちになるので、すぐには使う場面がなくても、「こんなことができるんだな」くらいに覚えておく価値はあると思います。 _ の表す意味 変数名に使う => 変数が未使用であることを宣言する 型の一部として使う => 型推論 してねとRustにお願いする サンプルコード コードの公開範囲(public/private) Rustで定義したものはデフォルトでprivateで定義されます。定義されたモジュールの外で利用する場合は pub キーワードで公開することを宣言する必要があります。 サンプルコード また、pub(crate)などと指定することにより、公開する範囲を限定することができます。 参考: Visibility and Privacy 注意点 タプルも同様に、デフォルトの公開範囲はprivateです。 // Sampleタプルは外に公開されている // この場合、タプル内部のStringは非公開 pub Sample(String); // このように内部にpubをつけることで公開すことができる pub Sample(pub String); PRを出す前にやっておきたいcargoコマンド プッシュする前に、下記のコマンドを実行し、エラーがないことを確認しておきましょう。 cargo build アプリケーションをビルド cargo test テストコードを実行 cargo clippy Linterで構文チェック rust-lang/rust-clippy cargo fmt formatterにかける rust-lang/rustfmt 副作用のある処理をMock化して、実装を切り替えられるようにする 弊社では、 ビジネスロジック ( ドメイン 層など)などは、クリーン アーキテクチャ で言う外側の層に影響されないように記述しています。 例えば、 ビジネスロジック の 単体テスト を行うのに、DBや API など外部のシステムと連携したテストを作成するのは、環境構築等色々な前工程を行う必要が生じるため辛いことになります。 このため、外部リソースを使って計算を行うロジックはロジック部分と外部の連携部分を切り分けたくなります。 以下のサンプルコードでは、実際の ビジネスロジック は、その外部リソースを扱う処理に直接依存するのではなく、「外部リソースの扱い方を定義したtraitに依存するように記述する」ことで処理の分離を実現しています。 サンプルコード turbofish(::<>) turbofishとは型注釈の一種で、型を引数のように関数に対して与える表現方法です。 例えば、strに対する parse() メソッドの型定義は以下の通りです。 pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err> where F: FromStr, 出所: Primitive Type str ここで F は、 FromStr を実装している型となるように抽象化されています。 つまり、実際に使う時には コンパイラ が F の型を推定できないと、 str 型を何に変換すればいいか特定できないでしょう。 この時、推定できるように記述する方法が2つあります。 // 1. 型が決まるように束縛するxに対して型注釈をつける // なお、xの型注釈は Result<i32, _> のように省略可能 // => 理由は以下のturbofishの例に記述 let x: Result<i32, ParseIntError> = "10".parse() // 2. turbofishを使う // この際turbofishとしてはi32になることが特定できれば // エラー型はFromStrの定義からError型の具体型が特定できる let x = "10".parse::<i32>() 参考: Rustのturbofishを理解する (おまけ) cargoの独自コマンドを作る cargo bookに、「$PATHに cargo-XXX というバイナリが入っていたら cargo XXX でcargoのサブコマンドのように実行できる」との記述がありますが、これはバイナリに限った話ではなく実行権限がついていれば大丈夫です。なので、簡単なshellを登録しておいてcargoから実行することも可能です。 (例) #!/bin/sh rg -l todo: 上記のshellを$PATHの通る場所に置いておくと、 cargo todolist で todo: のあるファイルを探してくれます。出力がPATHになるので、弊社では、 VSCode のターミナルで実行 -> PathをCtrl+クリックで該当ファイルに飛ぶなどに利用している人もいます。 shell自体をPATHにあるところに置いて呼び出せばいいじゃんという声が聞こえてきそうですが、 cargo --list で利用できるサブコマンドの一覧を取れるところが強みです。 いかがでしたでしょうか? Rustはすらすら書けるようになるまでが難しい言語だと思います。私の場合は、Rustを書き始めて2ヶ月くらいの間は、他の言語ではスラスラと書けたようなロジックでも全然書けなくて、とても悲しい気持ちになりました。 ですが、書けるようになってみるとやっぱり良いところもたくさんありますし、勉強すればするほどその強力さがわかって楽しくなってくるなと感じています。 最後に、弊社でRustを長く書いているエンジニアに勉強のコツを聞いてみたのですが、「 The Book は論理から非常によくまとまっているため、何度も読むと良い」というアド バイス をいただきました。本記事のようなテクニック的なところではなく、Rustのコード自体読んでもよくわからないと言う方は、 The Book を読み直すと良いかもしれません。何度読んでも学びがある内容なので、損はないと思います。 この記事を読んでいる皆様が、Rustを楽しんでくれることを願っております! We’re hiring!!! キャディでは、エンジニアを含め全職種積極採用中です! Rustを使って開発がしたい方、会社に興味を持ってくださった方、気になるから話を聞いてみたいという方、ぜひ面談にお越しください。 弊社がRustを採用している背景や、実際に開発してみてのメリットデメリットなどは、 「Rust についてカジュアル面談で頻繁に訊かれる質問と、それに対する個人的な回答」 をご参考ください。 ご応募は こちら カジュアル面談のお申し込みは こちら 募集職種一覧 長文お読みいただき、ありがとうございました!
エンジニアリングマネージャーの村上 (@mura_mi) です。採用関連で面談に出ることが多いのですが、大体7割くらいの確率で 「なんで Rust 使ってるのですか?」「Rust 使っててどうですか」と聞かれるので先回りして書いておこうと思った記事です。 なんで Rust を選んだの Rust をエンジニアリングチームの武器の中心に据える意思決定がされたのは私の入社前ですが、伝え聞いている話しと自分の解釈を混ぜ合わせた話を書きます。 「データ指向アプリケーションを堅牢に作るのに必要な型システムを求めたこと」と、「キャディがもともと C++ の会社だったこと」の2つが、キャディが Rust を使っていることの背景にあったのだと理解しています。 後述しますが、キャディが 原価計算システム やサプライチェーン・マネジメントシステム を Rust を使って開発しはじめたのは、2019年の中頃だったと伝え聞いています。これらのシステムは、「キャディがどんなものを、いくら費やして、どのように製作し、いくらで販売するか」という、キャディのビジネスの根幹となるデータを扱った データ指向アプリケーション です。 扱うデータの複雑さや、常に変化の可能性の下にあるビジネスルールに立ち向かう手段はいくつか考えられるでしょうが、データストアを担う物理層だけでなく、アプリケーション層でデータ形式の制限をすることは常套手段のひとつでしょう。 静的型付けのないプログラミング言語を否定するつもりは毛頭ありませんが、具体的な技術選定に口出しをしない CTO も、”テストをたくさん書くことに時間を使わず、型を書いてバリエーションを自明にしてラクをしよう” とは, しばしば口にします。 ではなぜ、そこで Rust なのか?選択肢に挙がった言語たちをなぜ見送ったのかの具体的な理由を記述することは割愛しますが、選択肢の中に Rust があった背景には、CADDi がもともと C++ の会社だったことが挙げられます。 「CADDi」という社名の由来のひとつには “CAD から Direct” という意味がありました。この記事の執筆時点では、キャディがお客様からいただく設計図の情報は二次元図面がほとんどですが、創業当初のキャディのビジネスの中心にはCADデータの自動解析アルゴリズムがありました。この頃 CAD データの解析に使っていたのが C++ で、江添亮さんには一時期テクニカルアドバイザーになってもらっていた事もありました。 C++ のプログラマから見て、パフォーマンスを犠牲にせずに安全性を手に入れることができるのが Rust です。当時社内で Rust を推した一人である いなむさんのnote を読むと、少しだけ当時の雰囲気が垣間見えるかもしれません。 数多のテック企業が採用候補者を奪いあう戦国時代の中で、採用市場で「え、Rust を業務システムに使っているの?」でアテンションを惹けることは、キャディにとって良い副産物 でした。そもそも C++ と一口に言っても C++17 を 2018 年の段階で実戦投入したりしていた というのもあり、最新の技術を使っている組織だというイメージは強化されたのかなと思います。そういえば、筆者自身も「は?Rust でサービス作るの?」と釣り針に引っかかってカジュアル面談の話を聞きに行った一人でした。 Rust の用途とメリット 主に3つのエリアで使っていたり、使おうとしています。 1つは前述した、原価計算システムやサプライチェーン管理を行う「基幹システム」の開発です。 tonic (一部、まだ tower-grpc を使ってる部分もあります…😫 ) を用いて gRPC API サーバーを建てています。リレーショナルデータベースを接続してのデータ CRUD には、 diesel 上に構築した、データレコードの更新履歴も保存するフレームワークを独自に開発して利用しています。 ブラウザからユーザーがアクセスしたり、RDB のみならず Redis やメッセージキューなど多種多様なミドルウェアと接続されたシステムなので日々様々なトラブルは起きるのですが、Rust 自体が不明瞭な挙動を引き起こしたり、パフォーマンス劣化に悩まされる事象は見た記憶がありません。 (ビジネスルールの設計や実装に起因する問題だったり、データベースに異常な負荷を掛けてしまうケースが多い) 2つ目は、現在のキャディのビジネスの中心を流れる 「二次元図面」の解析を行うアルゴリズム の開発です。これに関しては、図面解析チーム (orama) のテックリードを務める寺田の記事 がとても良いので読んでほしい… のですが、Rust に関して彼の言っていることの抜粋が以下です。 Rust は目新しさがあるかもしれません。Rust は安全性とスピードを兼ね備えているので、パフォーマンスが求められるアルゴリズムの開発には本当に適していると実感しています。Python + OpenCV だけで賄えない理由は、ベクターデータの処理が必要になるからです。上で紹介した表の罫線認識では、まず画像をベクターデータに変換してから様々なアルゴリズムを組み込んでいます。ベクター化アルゴリズムを含めた各種のアルゴリズムの実装に、Rust が活躍しています それ以外に、まだ実戦投入していませんが、WASM を用いたブラウザアプリケーションの開発に利用できないかとフロントエンドエンジニア陣が試行錯誤をしています。ブラウザ上で2次元図面のデータを表示したり、その図面への注記の追加をブラウザ上でできるようにする (社内では 図面版Figma と呼ばれていたりします) ようなアプリケーションの開発が視野にあるのですが、このようなアプリケーションの開発にはどうしてもパフォーマンスを追求したくなるケースが出てくると思っており、そのときに備えて様々な技術調査をしています。 Rust を仕事で使って、「頑張らないといけない」ところ 現場で Rust をこれからも使い続けていくに際し、組織的に頑張らないといけない点もいくつかあります。 まず思い当たるのが ビルドや CI に時間が掛かる点。開発者にパワフルなマシンを貸与するという “札束勝負” もしつつ、並行ビルドが効くような工夫 も頑張っています。 独特な書き味であったり、ライフタイム、所有権など他の言語では馴染みのない概念も多く、新規参入メンバーに各種知識のキャッチアップをしてもらうのはどうしても大変です。 しかし、私達のチームは「誰もが Rust のキャッチアップを頑張る道を通った経験がある」ことが強みであると思っていて、より一層「学習の高速道路」を整備しなきゃいけないなと思っています。 言語仕様や性質から来る制約という観点では、外部通信のような比較的大きい副作用が絡むテストに於いて テストダブルを差し込むことは可能なのですが、かなりの労力が必要になる印象を持っています。これは、Rust の場合コンパイル時に Dependency Injection される具体的なデータ型が指定されている必要があることに由来しています。 (キャディでは CakePattern を使った DI を実装しています) 同様の理由で、The Clean Architecture の「domain のレイヤをピュアに保つ」という方針を徹底しきれないな、と思うこともしばしばあります。 とはいえ、長所と短所 を天秤に乗せても、ビジネスの基幹となる情報システムを、硬く・速く動くようにするという意味では良い選択肢だと思っているので、もっとうまく使いたいと思っているところです。 キャディの人って元から Rust 書ける人ばっかりなんですか? 否。 入社前から Rust を経験していたメンバーもいますが、 「がっつりチームで Rust 使った開発してた!」「ウェブアプリケーションの API サーバーを Rust で開発・運用していた」という人はいない はずです。皆それぞれのバックグラウンドをもって入社しています。先述したように、誰もがキャディに入社してから Rust 周辺についてキャッチアップをしてきた経験値を持っていることが、チームの強みなんじゃないかと思っています。 実際、 採用活動の中で候補者の方とお話する際には、Rust の経験があればもちろん良いのですが、それ以外にどういう経験してきたかを重視しています。サーバーサイドアプリケーションの開発をメインの領域とする自分からすると、しっかりとドメインモデリングに向き合ってきた経験や、他の言語で Clean Architecture をイチから書いたり、理解して使ってきた経験の方が重要だなーと思っています。 一方で、赤裸々なことを書くと、実際に社内の Rust のコードを読んでいても、API の設計をする際に参照と実態の区別をしっかりつけられていない (よって無用な .clone() の必要が生じる) ケースも結構見かけ、まだまだ Rustacean の集団として成熟しないといけないなぁと思うこともあります。 We’re hiring てなわけで、社内で書くコードが Rust オンリーというわけではないけど、キャディは「書きたい」と言えば十中八九 Rust を書ける環境だと思います。そんなCADDi の仲間になってくれる方を募集しています。「すぐ転職する感じじゃないんだけど Rust の話は聞きたい!」「Rust 書いたことないけど興味あります!」みたいな方でもお気軽にカジュアル面談からお申し込みください。 応募はコチラ https://caddi-careers.studio.site/jobs-tech-backend からどうぞ。 The post Rust についてカジュアル面談で頻繁に訊かれる質問と、それに対する個人的な回答 appeared first on CADDi Tech Blog.
はじめに こんにちは。キャディで原価計算システムの開発を担当しております、高橋です。 この記事は キャディ Advent Calendar 2020 の23日目です。前日は朱さんの 「【開発カルチャー発信 vol.1】原価計算システム開発チームの開発理念を大公開!」でした! さて本日は掲題の通り、私がスキルアップを兼ねて趣味的に取り組んでいる、コストモデル可視化システムの開発について紹介させていただきます。 目次 課題意識 弊社のビジネスの核は、コストモデル コストモデルは、名前の通り「コスト」の計算を「モデル」化したことで、原価計算という作業を弊社内で民主化しました。「正しい原価」を誰でもすばやく計算できるということです。 これが無くては弊社のビジネスがスケールアウトすることは不可能であり、スケールアウトしなければ受発注プラットフォームは作れません。従って、弊社のビジネスの核は1にも2にもコストモデルなのです。 コストモデルは生き物 しかしこの「正しい原価」というのが曲者です。正しいの定義は時々刻々と変わります。従って、コストモデルはこの正しさに常に追従する必要があります。 例えば材料費が高騰したら、原価は変わります。また、弊社が今まで対応していなかった新しい加工方法をコストモデルで取り扱えるように拡張しないと正しい原価が出せない場合もあります。このような事情で、コストモデルは常に改訂・拡張を繰り返しながら、あるべき原価を求めてさまよい続けています。 しかし、コストモデルは見えにくい このように、弊社ビジネスの核でありながらも常に動き続けるコストモデルですが、現状はRustのソースコードで実装されています。従って、コストモデルを常日頃からメンテナンスしている立場でない限り、具体的にコストモデルの定義がどうなっているのか把握しにくいというデメリットがあります。 コストモデルを作るのではなく扱う立場であっても、コストモデルが前提とするコストの構造・概念を理解していることは重要なのですが、ここで「コストモデルが見えにくい」ということが障壁になっていると私は考えています。 コストモデルをどうやって可視化するか 可視化したいものがコードの中にしか無い 上記の通り、現状ではコストモデルの定義はRustでハードコーディングされています。 こんな感じのものがたくさんコーディングされています。 コストモデルの定義の例(体積計算): 原価 = 加工時間 × 時間当たり原価 加工時間 = 加工工程 XXX の時間 + 加工工程 YYY の時間 時間辺り原価 = とある定数 加工工程 XXX の時間 = XXX加工単体の時間 × XXX加工の数 加工工程 YYY の時間 = XXX加工単体の時間 × XXX加工の数の2倍 抽象的に書いていますが、例えば穴あけ加工が1個1分かかってそれが2箇所、1分あたり100円なら、概算で200円、みたいなことを考えるとわかると思います(実際にはこんな単純ではなく、もっと複雑です)。 ここから可視化しようと思うと、Rustのコードをパースしてコストモデルの定義を抽出してくるような仕組みが必要ですが、あまり現実的ではありません。 コストモデルの定義をデータとして分離する コストモデルは数式のグラフ 上記の例から分かるように、コストモデルは数式の集合であり、それらは依存関係を持つことから、数式や定数(引数を持たない数式)をノードとする非循環有向グラフ(DAG)を考えることができます。これを数式グラフと呼ぶことにしましょう。 上記の定義を数式グラフとして表現した例: document.write("graph TD;\n原価-->加工時間;\n原価-->時間当たり原価;\n加工時間-->加工工程XXXの時間;\n加工時間-->加工工程YYYの時間;\n加工工程XXXの時間-->XXX加工単体の時間;\n加工工程XXXの時間-->XXX加工の数;\n加工工程YYYの時間-->XXX加工単体の時間;\n加工工程YYYの時間-->XXX加工の数;\n"); 例えば上図のように表現された数式グラフを計算する場合は、依存の階層の一番下から順番に計算していくと、最終的に原価を求めることができます。コストモデルの定義がどのようなものであれ、計算可能な関数と依存関係の集合であれば可能なことです。 グラフ構造をデータとして計算処理から分離できる さてこのように考えていくと、コストモデルの定義は数式グラフの中にしか登場せず、数式グラフの計算処理には登場しません。したがって、現状のようにプログラム中にコストモデルの定義をハードコーディングせずに、どこかにデータとして保存された数式グラフを計算実行処理に外側から注入して結果を得る、というやり方ができそうです。要は、コストモデルの定義を数式のグラフ構造のデータとして、計算処理から切り離すということです。 このやり方であれば、グラフ構造の保存・編集・可視化ができれば、コストモデルを可視化できると言えそうです。Rustのプログラムをパースしてコストモデルの定義を解析するよりも、大分現実味があります。 技術選定とシステム構成 上記のやり方を試すために、Frontend, backend, db を直列につないだ極めてシンプルな構成でシステムを組んでみることにしました。 component 仕様技術、ライブラリなど 役割 Frontend React + Typescript , G6.js, typed-rest-client 数式グラフと計算結果の表示 Backend Java + Spring boot + Spring Data Neo4j, mxParser 数式グラフのロードと計算 DB Neo4j 数式グラフの保存 まず、数式グラフを保存する手立てとして、グラフ構造をそのまま扱えるグラフ指向データベースを使うことにしました。とりあえず今回は、一番有名っぽいNeo4jにしました。 すると、Neo4jとエンティティ定義のマッパー(ObjectGraphMapping)に対応したフレームワークであるSpring Data Neo4j を使うのが一番楽につくれそうなので、バックエンドはJavaに決定。 バックエンドでは数式グラフを計算するので、Neo4jに文字列として保存された数式(簡単のために算術演算に限定)を動的にパースして計算する処理が必要になるのですが、そのようなライブラリをJavaから探した結果、mxParserというのがあるので使ってみました。「キャディのエンジニアなんだからパーサーくらい自分で書け」と言われそうですが、今はサクサク作って動かしたいので、あるものは最大限活用します。 UIは、業務上のスキルアップも兼ねてReact + Typescript を使うことにしました。グラフ構造を描画するUIライブラリとしては、G6.js を使ってみました。弊社で使用実績はなさそうなものの、MITライセンス・機能が豊富・公式ドキュメントが充実の3点で決めました。バックエンドのAPIを叩くクライアントは、本家が作ってて信頼できそうなのでtyped-rest-clientに(適当)。 実装 今回は簡単のため、UIからのコストモデルへのパラメータの入力は受け付けないものとします。 また、コストモデルの編集は実装が多いので、ここでは割愛します。 データベースに予め数式の定義と入力が保存されている状況から、計算と表示ができるところまでを紹介します。 特に難しいことはしていないので、同じものはこの記事を読みながらどなたでも作れると思います。 Backend ここでは、この記事のために「数式グラフのロード」「数式グラフの計算」の2つのAPIを用意してみます。 Entity定義 ノードに数式をもたせて、ノード間のエッジに、数式同士の依存関係と、依存先の数式が対応する依存元の数式中の変数名を保持させます。 SpringDataNeo4j のおかげで、クラスにアノテーションを付けるだけでグラフの構成要素として設定できます。 @NodeEntity public class ExprNode { @Id @GeneratedValue public Long id; public String name; public String expr; @Relationship(type = "SUBEXPR", direction = Relationship.OUTGOING) public ArrayList subExpressions = new ArrayList (); // 依存先の数式を定義する関数 // 第二引数で、数式中のどの変数に第一引数の数式の評価結果を割り当てるかを // 指定している public boolean setSubexpr(ExprNode node, String token) { Expression expr = new Expression(this.expr); this.subExpressions.add(new Edge(this, node, token)); } // 自身の値を計算する関数。 // 関数の引数の値が別の関数で求まる場合、再帰的に潜っていって計算する。 // Expression は mxParser が提供する数式型。 public Double evaluate() { // ここで文字列の数式をパースして関数を生成すると同時に、関数の引数に依存先の関数の評価結果を割り当てる Expression expression = new Expression(this.expr, this.subExpressions.stream() .map(edge -> new Argument(edge.startToken, edge.end.evaluate())).toArray(Argument[]::new)); return expression.calculate(); } } @RelationshipEntity(type = "SUBEXPR") public class Edge { @Id @GeneratedValue public Long id; @StartNode public ExprNode start; @EndNode public ExprNode end; // startnode の数式のどの 変数名に endnodeが対応するかを保存するフィールド public String startToken; } Repository Neo4jにアクセスするインターフェースを定義し、数式グラフをロードしてくる関数を定義します。 これもSpring DataNeo4jの恩恵を受けることが出来て、interface さえ定義すれば実装はSpringが勝手に作ってくれます。 public interface ExpressionRepository extends Neo4jRepository { // cyper query を直接書くこともできる @Query("MATCH p=(n:ExprNode)-[:SUBEXPR *]->(:ExprNode) RETURN nodes(p), relationships(p)") ArrayList getAllExprNodesWithEdges(); // query を書かない場合、実装は関数名から自動で定義される // デフォルトではグラフの深さ1までしか取ってきてくれないので、アノテーションで指定する @Depth(value=4) Option findByName(String name); } Service 作りたいAPIには、DBへの数式グラフのシード、数式グラフの取得、数式グラフ計算の3つの処理が必要なので、それをここで用意します。 @Service @Transactional @EnableNeo4jRepositories(basePackageClasses=ExpressionRepository.class) public class ExpressionService{ @Autowired ExpressionRepository expressionRepository; // 数式全体の取得 // 戻り値は適当に用意したResponse型 public ExpressionsResponse getAllExpressions() { ArrayList nodes = expressionRepository.getAllExprNodesWithEdges(); ArrayList nodeResponses = nodes .stream() .map(node -> { return new NodeResponse(node.id, node.name, node.expr); }).collect(Collectors.toCollection(ArrayList::new)); ArrayList edgeResponses = nodes .stream() .flatMap(node -> { return node .subExpressions .stream() .map(edge->{ return new EdgeResponse(edge.id, edge.start.id, edge.end.id, edge.startToken); }); }).collect(Collectors.toCollection(ArrayList::new)); return new ExpressionsResponse(nodeResponses, edgeResponses); } // 架空の体積計算を表す数式グラフを保存してみる public String seedExpressions() { // 数式ノード定義 ExprNode volume = new ExprNode("volume", "x * y * z"); ExprNode x = new ExprNode("x", "10"); ExprNode y = new ExprNode("y", "10"); ExprNode z = new ExprNode("z", "a + b"); ExprNode a = new ExprNode("a", "20"); // 定数 ExprNode b = new ExprNode("b", "30"); // 定数 // 数式中の変数に別の数式を割り当てる。 volume.setSubexpr(x, "x"); volume.setSubexpr(y, "y"); volume.setSubexpr(z, "z"); z.setSubexpr(a, "a"); z.setSubexpr(b, "b"); expressionRepository.save(volume); return volume.name; } // 数式グラフ中の指定した数式ノードの値を計算する public double calculateExpression(String name){ ExprNode rootNode = expressionRepository .findByName(name) .orElseThrow(() -> new RuntimeException()); return rootNode.evaluate(); } } Controller RestAPI経由でそれぞれのビジネスロジックを呼んでResponseを返すようにします。 @RestController @RequestMapping("/") // UI は yarn start で立てるので、そのアドレスをcorsに設定 @CrossOrigin(origins = "http://localhost:3000") @ResponseBody public class Controller { @Autowired ExpressionService expressionService; // 数式グラフをシードしてロード @RequestMapping(value = "/expressions", method = RequestMethod.GET) public ExpressionsResponse readExpressions(@PathVariable String version) { expressionService.seedExpressions(version); return expressionService.getAllExpressions(version); } // 数式グラフの計算 @RequestMapping(value = "/calculate", method = RequestMethod.GET) public String calculateSeededNode(@PathVariable String version) { // DB初期化 String rootNodeName = expressionService.seedExpressions(version); return String.format("calclation finished: %f", expressionService.calculateExpression(rootNodeName, version)); } }; UI こんな感じで、Reactのコンポーネント内でG6.jsのグラフオブジェクトを初期化した後、バックエンドからグラフ取得したグラフのデータを流し込んで描画します。(長いので一部省略しています) G6.js は pureJS ライブラリなので、Reactに組み込むのがちょっと手間です。公式のサンプルを元に実装しましたが、そのままだと警告が出るので一部手を加えました。 function GraphView() { const ref = React.useRef(null); const graph = React.useRef (null); useEffect(() => { if (!graph.current) { graph.current = new G6.Graph({ container: ReactDOM.findDOMNode(ref.current) as HTMLElement, layout: { type: 'dagre', // 有向グラフを階層的にレイアウトするためのアルゴリズム rankdir: 'LR', // レイアウトの向きを指定 ranksep: 70, // レイアウト方向のノード間隔を指定 }, // グラフ上で描画されるノードのデフォルト設定 defaultNode: { type: 'modelRect', anchorPoints: [ [0, 0.5], // source [1, 0.5], // target ], // ...その他設定は省略 }, // グラフ上で描画されるエッジのデフォルト設定 defaultEdge: { // ノードのアンカーポイントのどれとどれをつなぐかを // defaultNode の anchorPointsのindex指定で設定 sourceAnchor: 1, targetAnchor: 0, }, }); } let rest: trc.RestClient = new trc.RestClient('test', 'http://localhost:8080/'); rest .get (`expressions`) .then((res: trc.IRestResponse ) => { let data: GraphData = { nodes: res.result!.nodes.map(/* G6.js のノードの形式に変換*/), edges: res.result!.edges.map(/* G6.js のエッジの形式に変換*/), }; graph.current!.data(data); graph.current!.render(); }); return () => { graph.current!.destroy(); graph.current = null; } }, []); return ; } 起動 以上作ったものをローカルで起動させてみました。 まず、DBは公式ドキュメントに従うとDockerコマンド一発で立ち上がります。 $ docker run -p7474:7474 -p7687:7687 -e NEO4J_AUTH=neo4j/s3cr3t neo4j backend と Frontend はそれぞれ、gradle bootRun, yarn start として起動しました。 起動してみるとこんな感じです。簡素な画面ではありますが、数式のグラフを可視化できています。 上のコードでは省略していますが、G6.jsのプラグインで、画面左上に計算処理を叩くボタンを用意して、押すと計算結果を alertするようにしてみました。 volume = 10 * 10 * (20 + 30) = 5000 なので、確かにちゃんと計算できています。 これで一応、数式グラフの保存・ロード・表示まではできたことになります。 今後はもう少し作り込んで、あわよくば仕事につながったら面白そうに思っています。 おわりに 今回は、コストモデルの背景や課題に加え、それをReactとNeo4jを用いて可視化する試みをご紹介しました。 私は元々CADアルゴリズムグループとして採用されたので、Webエンジニアは初めてまだ1年程です。しかし、この記事のような小さなシステムを自分で一から一通り作ってみると、中々いい勉強になりました。 また、ここに書いた実装は私1人では達成し得なかったことで、色々な方のアドバイスを経ながらできたものです。弊社にはこのように一緒に技術を楽しんでくれるメンバーが揃っておりますので、興味を持っていただいた方は、ぜひご連絡いただければと思います。 The post React + Neo4j によるコストモデル可視化の取り組み紹介 appeared first on CADDi Tech Blog.
こんにちは、キャディでバックエンドエンジニアをしている朱です。 この記事は CADDi Advent Calendar 22 日目の記事です。昨日は、高藤さんによる「 tracing crateを利用したRustのlogging方法について 」でした! 今回は、私が所属している原価計算システム開発チームの開発理念についてお話ししようと思います。 この記事を通して キャディの開発チームに息づくカルチャーの一端を感じ取っていただければ 幸いです! (タイトルを勝手に vol.1 としていますが、反響が良ければきっと企画化されるはずでしょう…) [toc] 開発理念を定めた経緯 はじめに、私たちのチームで開発理念を定めるに至った経緯について簡単に触れておきます。 原価計算システム開発チームは現在、総勢9名(内 PM が1人、デザイナーが1人、残りがエンジニア)で社内で最大規模の開発チームです。 今年6月のプロダクトリリースを経て運用体制が一定周り始めたタイミングかつ、メンバーのアサイン変更や新たな仲間を迎えるにあたって、 これまでのチームのスタンスを保つ/発展させていくために最低限の明文化を図った というのが策定の背景です。 また、PM が他部署との兼任になったというのも理由の1つでした。 それまではユーザーとのコミュニケーションやそこから得られた示唆をもとにした意思決定は PM が主体となって行っていましたが、チームとして開発のスピードを落とさないためにも、なるべくエンジニア主導でのコミュニケーションや意思決定の比率を上げていく必要性がありました。明文化された理念は、こういった行動を取る際のある種の拠り所としても機能しています。 5つの理念 さて、ここからはチームに存在する5つの開発理念をそれぞれ深掘っていきたいと思います。 (社内外の)ユーザーからの信頼、期待値を毀損せず常に高めるべく努力すること 5つの中で最も重要かつ守るのが難しいのがこの一文だと感じています。 信頼、期待値というのは得てして積み上げるのは簡単ですが崩れ去るのは一瞬だからです。 プロダクトにとっての期待値の表れの1つに、ユーザーからの「こういう機能が欲しい!」という要望があります。 大前提として、 いただいたフィードバックは全て資産であり、チームとしては可能な限り要望に答えてプロダクトをより使いやすくしたい と考えています。(5つ目の理念が正にそれを明文化したものです) しかし、現実には開発リソースは有限なので中にはすぐには答えられない要望もあります。だからこそ、そのような要望をくれたユーザー1人ひとりにリスペクトを持って 「なぜ今その機能の開発に取り組まないのか/いつになればその負は解消されるのか」を丁寧に説明する ように心掛けています。 その上で、待望の機能をリリースした暁には Wow を届ける = 期待を上回る価値を提供して初めて、ユーザーからの信頼は保たれるのだと思うのです。 ユーザーに利便性を生むこと。利便性とはなにかをやらなくて良くなること 2つ目の理念は非常にシンプルで分かりやすいですね。 10X の矢本さんが 同じような趣旨のツイート をされていましたが、弊社のような toB の領域でプロダクトを提供している会社では特に意識する必要がある考え方だと思っています。 我々が挑んでいる課題は必ずしもプロダクト (= ここでは狭義のプロダクトでシステムの意) だけでは解決し得ないもの であり、両輪のもう片側である"ヒト"がより本質的な業務に時間を使えるようにするためにも、 プロダクトは出来る限り「やらなくて済むこと」を増やすのに心血を注ぐべき なのだ、と解釈しています。 何かをリリースする、変える上での説明は丁寧すぎるレベルでやること 私たちが作っているのは社内のオペレーションで用いる業務システムであり、 プロダクトの変更がすなわちオペレーションの変更に繋がります 。 もし、変更内容が社内に伝わりきらずにオペレーションが古いまま運用されてしまうと、我々にとってのエンドユーザーであるキャディの顧客・SPにまで間違った情報が渡ってしまうというリスクがあります。実際に、過去にはリリース内容の周知が不足していたことでバグの検知が遅くなり、顧客に間違った価格が流出しかけたという事象がありました。こうした背景を踏まえて、3つ目の理念は付け加えられています。 また、直近ではプロダクトの機能拡張によりステークホルダーが社内外に徐々に広がっており、今までよりも一層この理念を意識する必要が出てきています。長くなりがちなリリース内容をなるべくスムーズに認知してもらうための取り組みとして、 告知文に絵文字を用いることで関心を持ってもらおうというUXライティング的な動き も草の根的に始まっています。 Quipu というのが原価計算システムの名前で、インカ帝国で使われたそろばんのようなものが由来です(実は本邦初公開!?) リリースした後はリアルタイムにユーザーのそばでフォローすること。そして自分たちの目と耳で課題を見つけること 3つ目とも関連しますが、リリース時の周知の徹底だけでなくリリース後のフォローも大切な要素です。 これを実践するために、私たちのチームでは 週に一度ユーザーの業務を見学する会を行っています 。 見学会では、原価計算システムとその他の業務システムを用いた一連のオペレーションの様子を画面で共有してもらい、主にUX面での課題がないかを観察しています。やはり実際にプロダクトが使われているのを目にすると、思いもよらぬところで躓いてしまったり本来不要なはずの作業を繰り返してしまっていたり、当初のUX設計では考慮できていなかった点があぶり出されることが多いです。 このように一次情報を絶えず取得できる仕組みがあるのは開発側としては願ったり叶ったりの状態ですが、ユーザーにとっては負担にもなり得る取り組みを受け入れてもらっているのは、ひとえに日頃の信頼関係が築けているからこそだと思っています。 加えて、大きなリリースがあった場合はアドホックに説明会を開いたり、課題がないか個別でヒアリングしたり、 サポート体制には積極的に工数を割くようにしています 。 もらったフィードバックすべてに感謝しリスペクトをもって扱うこと。そして基本なるはやで対応すること。しかし、そのまま鵜呑みにして対応するのではなく、本質的に必要な対応を行うこと 最後の理念もユーザーからのフィードバックにまつわるものです。 フィードバックに対するチームのスタンスについては先述した通りなので、ここではもう少し具体的な話をしようと思います。 試しに直近1ヶ月のユーザーからのフィードバック件数を数えてみたところ、平均すると週に6-7件ほど要望が来ていることが分かりました。これは 対ユーザー数比率で言うと全体の約3割の方が週に一度はフィードバックをくださっている 計算になります。 また解決までのリードタイムについては、いただいた要望の内軽微な修正でUX向上が図れるものの多くは次のSprintでの解消を、中期的に取り組む必要があるものについても長くて1ヶ月程度のスパンで解消してきています。(中にはいち新機能として開発が必要になる要望も含まれるので、さすがにそれは本腰を入れて長期的に対応します) では、ユーザーの要望を取り入れ続けていわゆるキメラ的な開発を続けているのか?と言われると、そうではないと思っています。実際にユーザーの本質的な課題を特定して解決した事例として、先月行った とある機能を廃止した事例 を取り上げてみましょう。 原価計算システムには入力した諸々のデータを確定させ以降は編集できなくする、いわゆる「ロック機能」が存在しました。これは機能を設計した当初には製造業のバリューチェーンをウォーターフォール的に捉えていたために、後工程にデータを受け渡す際には情報を確定させることで不確実性を下げることを意図したものでした。 しかし、蓋を空けてみると実際のオペレーションではロックを行ったあとにも情報を修正することは度々行われており、本来はよりアジャイル的な情報の扱いができるUXを指向すべきだったことが明らかになります。もちろん、ロック後にもデータを修正できる手段としてバージョン管理の機能もセットで提供していたのですが、キャディとして取り扱う案件の規模が拡大するにつれて修正にかかるインタラクションコストが無視できないものになってきました。 ユーザーからはバージョンアップにかかる手間を削減できないか?という問い合わせが繰り返し寄せられましたが、チームで「元々この機能で解決したかった課題は何なんだっけ?」と議論を重ね、結果としてロックのタイミングの後倒しと改修が完了するまでの一時的な機能の停止を決定しました。 以上のように、 UXを毀損しているようなケースではたとえそれが時間をかけて開発した機能だとしても躊躇なく捨てる意思決定が出来ること が「本質的に必要な対応」なのだと考えています。 一緒にチームのカルチャーを育んでいく仲間を募集中! いかがだったでしょうか?冒頭でも述べましたが、この記事を読んでキャディの開発チームのカルチャーに興味を持っていただけた方が少しでもいらっしゃれば嬉しい限りです。 今回取り上げた開発理念はまだまだ発展途上のもので、 これからも既存のチームメンバーや今後キャディにJoinしてくださる皆さまの手によって育まれていくもの だと思っています。控えめに言っても最高のチームが集まっているので、まずは話しを聞いてみたいという方も Twitter などで気軽にご連絡いただければ幸いです! 明日はそんな原価計算システム開発チームの高橋さんによる「React + Neo4j によるコストモデル可視化の取り組み紹介」です。お楽しみに! キャディではエンジニア・デザイナー・PMなど幅広い職種の皆さまを募集しています! エンジニア採用サイトは こちら からどうぞ!
こんにちは。CADDi でバックエンドエンジニアをしている 高藤 です。 この記事は CADDi Advent Calendar 21日目の記事です。昨日は、寺田さんによる RustでRAMの動作原理をシミュレートする でした! 今回はRustのtracintg crateについて紹介したいと思います。 目次 はじめに キャディではバックエンドのAPIをgRPCを使って実装しています。 実装にはtonicというRustでは比較的新しいcrateを使っています。使いやすいこともあり比較的使ってみたなどの記事は散見されるのですが、今回は本番環境で運用するのに大事なloggingの観点で説明をしたいと思っています。 gRPCサーバの実装 まずはtonicのサンプルを紹介します。以下のコードはtonic にあるexamples/src/helloworld/server.rsをもとに説明を行います。 まずコードを見てみましょう。非常にシンプルなサーバです。Requestに名前を含めると返事をしてくれるそれだけのサーバですが、まずはこのコードを使っていくつか検証をして行こうと思います。 use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result > { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {}", addr); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } 挙動を確認するためにこちらのServerをまずは実行してみます。 ❯ cargo run --bin helloworld-server` サーバを起動すると GreeterServer listening on [::1]:50051 このように表示されてサーバが起動します。 このままRequestを送るためgrpcurlを使って見ます ❯ grpcurl -plaintext -d '{"name": "foo"}' -proto proto/helloworld/helloworld.proto -import-path ./proto localhost:50051 helloworld.Greeter/SayHello { "message": "Hello foo!" } サーバ側 Got a request from Some([::1]:58752) ちゃんと動いていますね。 あくまでサンプルですが、このコードにDBとの接続処理やロジックを記述していくことでサービスを提供できそうです。ですが、本番でちゃんと運用するにはログをちゃんと出力しないと難しいです。 上記の例では標準出力にprintln!を使ってRequestが来たことは出力されていますがtimestampもなくいつ処理されたものなのかもわかりません。 本番環境での運用を考えてgRPCサーバのloggingについて考えて見ようと思います。 env_loggerの利用 Rustではログの出力を行うためログ出力機能が抽象化されたlogcrateとその実装crateが存在しています。crates.ioでも上位にあるenv_loggerを使ってログの出力を行ってみます。 まず、Cargo.tomlに対して依存するcrateの追加を行います。dependenciesに以下の2つのcrateを追加します。 env_logger = "0.8.2" log = "0.4.11" env_loggerの初期化 main関数部分でenv_loggerの初期化処理を追加します。あわせてprintln!を使って標準出力を行っている部分をlog::info!に書き換えて見ましょう #[tokio::main] async fn main() -> Result > { // ここを追加 env_logger::init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!!("GreeterServer listening on {}", addr); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } こちらを改めて起動して確認をします。 起動直後 [2020-11-06T16:25:56Z INFO helloworld_server] GreeterServer listening on [::1]:50051 Requestの送信時 [2020-11-06T16:26:43Z INFO helloworld_server] Got a request from Some([::1]:34238) 無事timestampやlogレベルをあわせて出力することが出来ました。 ただし、これだけだと処理の内容がわからずlogを出力する意味があまりない状態なので処理の終了時にRequestの内容と処理が終わった旨を出力するように修正してみます。 #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", request.get_ref().name), }; log::info!("Request: {:?}, Done", request); Ok(Response::new(reply)) } } 実行結果 [2020-11-06T16:35:39Z INFO helloworld_server] GreeterServer listening on [::1]:50051 [2020-11-06T16:35:40Z INFO helloworld_server] Request: Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }, Done これで意図したとおりに処理が終わった旨の出力とRequestの内容が表示されるようになりました。 もう少し実用的なアプリケーションを想定して処理を追加してみます 冒頭のimport宣言にCodeを追加します。 use tonic::{transport::Server, Code, Request, Response, Status}; 何かしらの処理を行う関数some_logic()の追加とそれを利用するようにsay_hello()メソッドの修正を行います。 #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; log::info!("Request: {:?}, Done", request); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { log::info!("run some logic"); match name { "foo" => { log::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } Requestに含む名前によってはエラーを出力するように修正を行いました。サーバを起動し、先程と同様にgrpcurlでRequestを投げると以下のような出力を得ることが出来ます。 [2020-11-07T00:39:25Z INFO helloworld_server] GreeterServer listening on [::1]:50051 [2020-11-07T00:39:36Z INFO helloworld_server] run some logic [2020-11-07T00:39:36Z ERROR helloworld_server] Failed some_logic 想定している通り失敗した時にERRORログが出力されることが確認できました。 しかしこの方法だと問題があります。 ERRORログにRequestの情報がないので複数のRequestを受けている時にどのRequestがエラーになったのか判断出来ない 今回の処理は全てasync fnにより非同期に実行されるため、ログの出力に1つのRequestからなる処理の内容がが混ざって表示される 愚直に問題を解決させるなら、RequestやRequestヘッダーにRequesを識別できるIdを含めてそれをsome_logic()関数に渡すことで解消はできます。 async fn some_logic(request_id: RequestId, name: &str) -> Result ただしこのやり方では更にlogicが複雑になったときなどに全てのlogicに対してRequestや識別子を持ち回す事を行わないと実現することが出来ません。 このような問題を解決するためにtracing crateを利用することが出来ます。 tracing crateの利用 Cargo.tomlにはすでにtracingの依存が含まれている状態なので、修正はserver.rsのみとなります。 #[derive(Default, Debug)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument] async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::info!("Done"); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { log::info!("run some logic"); match name { "foo" => { tracing::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } #[tokio::main] async fn main() -> Result > { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!("GreeterServer listening on {}", addr); Server::builder() .trace_fn(|_| tracing::info_span!("gRPC server")) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } いくつかの修正を行っているため、修正点を列挙します。 MyGreeterにDebug traitを実装 say_helloメソッドに#[tracing::instrument]を追加 log::info, log::errorとしている部分をそれぞれtracing::info, tracing::errorとなるように修正 main関数で初期化していたenv_loggerの初期化処理を削除し、tracing_subscriberの初期化処理を追加 このコードを実行し先程のエラーが起きるRequestを送信するとログの出力が以下のようになります Nov 07 09:57:42.891 INFO helloworld_server: GreeterServer listening on [::1]:50051 Nov 07 10:02:51.585 INFO gRPC server:say_hello{self=MyGreeter request=Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }}: helloworld_server: run some logic Nov 07 10:02:51.585 ERROR gRPC server:say_hello{self=MyGreeter request=Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }}: helloworld_server: Failed some_logic 実際にログを出力しているsome_logic()関数内にはRequestの情報は渡していないにも関わらずログの出力にRequestの情報など付与されるようになりました。 どのような仕組みになっているのか少し説明をします。 tracing crateは In−Process Tracing機能を提供するcrateとなります。Microservice等の分散処理システムの文脈ではJaeger、Zipkinを始めとする分散トレーシングという技術を利用してどこのサービスからどのサービスへ通信がされたか、その処理時間はなどメトリクスを取得することが出来ます。tracing crateも同様にプロセス内部の処理を追跡できるような形で記録する仕組みを提供しています。 仕組みを理解する上で重要になるのが以下の3つの要素となります。 Span 処理を記録する期間を表します 名前やあわせて記録しておきたい情報を保持することができる Event Spanに記録するトレースしたい事象を表します 発生した事象を記録したい情報とあわせて保持することが出来ます Subscriber Spanや紐付いたEventを収集するための処理を表します 今回の例を上記3つの要素を明確に使ってsay_hello()メソッドの部分を書き直すと以下のようになります。 async fn say_hello( &self, request: Request , ) -> Result , Status> { let args = format!("{:?}", request); let span = tracing::span!(tracing::Level::INFO, "say_hello", request = args.as_str()); let _enter = span.enter(); let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::event!(tracing::Level::INFO, "Done"); Ok(Response::new(reply)) } 処理の冒頭でSpanを定義します 定義内容 名前: say_hello Spanに含める情報 = RequestをDebug traitを使って文字列にした情報 Span.enter()を行いSpanの中に入る事を表す。(enter()はRAIIガードオブジェクトを返し、DropされたタイミングでSpanを閉じます) event!()マクロを使って記録する内容を記述します。 上記の例からわかるとおり、#[tracing::instrument]の処理ではSpanの定義とSpan::enter()の処理を自動的に生成しています。また、tracing::info!()やtracing::error!()はEventの生成をlog crateと同様のI/Fで定義できるように作られています。 注意公式のドキュメントにも記述されていますが非同期処理内でのSpan::enter()の処理は慎重に利用するか避けることが明記されています。非同期関数の場合は#[tracing::instrument]を使った場合にただしく生成できるとドキュメントに書かれているように#[tracing::instrument]を利用することを推奨します。 tonicとの統合 すでに実装例で示していますが、tonicのServer::Builderにはtrace_fn()メソッドが用意されており、ここでRequest毎のSpanを生成しています。次の例ではRequest全体の情報をSpanに含めず、Request Headerにtrace_idという文字列の情報を出力するように変更しています。(もちろんClient側でRequestする際にIdをヘッダーに入れる必要があります) #[derive(Default, Debug)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument(skip(self, request))] async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::info!("Done"); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { tracing::info!("run some logic"); match name { "foo" => { tracing::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } #[tokio::main] async fn main() -> Result > { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!("GreeterServer listening on {}", addr); Server::builder() .trace_fn(|header| { let trace_id = header .get("trace_id") .map(|value| value.to_str().unwrap_or("Unknown")) .unwrap_or("Unknown"); tracing::info_span!("gRPC server", trace_id = trace_id) }) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } 修正点 #[tracing::instrument(skip(self, request))] instrumentを使ってSpanを生成する場合、引数を全てSpanに含める挙動になりますが、今回はtraice_idのみを出力するために引数をSpanに含めないようにしています trace_fn(|header| ....) trace_fn()メソッドは引数にHeaderMap型をとり、Requestに含まれるHeaderの情報を取得することができます 実行結果 Nov 07 12:02:03.233 INFO helloworld_server: GreeterServer listening on [::1]:50051 Nov 07 12:02:06.875 INFO gRPC server{trace_id="Unknown"}:say_hello: helloworld_server: run some logic Nov 07 12:02:06.875 ERROR gRPC server{trace_id="Unknown"}:say_hello: helloworld_server: Failed some_logic Nov 07 12:02:12.284 INFO gRPC server{trace_id="xxxxxxxxxxxxxxxxxxxx"}:say_hello: helloworld_server: run some logic Nov 07 12:02:12.284 ERROR gRPC server{trace_id="xxxxxxxxxxxxxxxxxxxx"}:say_hello: helloworld_server: Failed some_logic 出力結果にHeaderから取得したtrace_idを含めることができました。 このようにtracing crateを利用することで非同期に実行される処理に対してContextを含めたログの出力を行うことが出来ます。 おわりに 今回取り上げたtracingにはこの処理機構を使って様々な処理を拡張するためのcrateが存在しており1つのecosystemが形成されて来ています。今回は単純にログを出力するだけのFmtSubscriberを利用しましたが、tracing-opentelemetry crateなどを利用すると前述した分散トレーシングシステムに対して出力することも可能です。 こちらは依存するopentelemetry crateの変更が激しく、今回割愛していますが興味がある方は試してみると面白いと思います。私が検証した内容では tracing-opentelemetry = "0.7" opentelemetry-jaeger = "0.7.0" opentelemetry = "0.8" 上記のような依存関係だとうまく実装が出来ましたが、すでにopentelemetry crateは0.10.0がリリースされている状態なので、本番への適用はもう少し様子を見たほうが良いかもしれません。 この記事がどこかの誰かの役に立つ日がくれば幸いです。 The post tracing crateを利用したRustのlogging方法について appeared first on CADDi Tech Blog.
頭おかしいタイトルですね。何を言っているんだお前は。 本記事は CADDi とは何の関係もありませんし、実用的価値も一切ありません。その点はご了承を。 あ、Rust が分からないからといって帰る必要はありません。この記事はほとんどRustと無関係です。なんらかのプログラム言語に親しんでいる方であれば雰囲気で読める程度の機能しか使っていないのでご安心ください。 nand2tetris 先日、こちらの記事が話題になっていました。Nand2Tetris(コンピュータシステムの理論と実装)でCPUからOSまで一気通貫で作るのが最高に楽しかった話 この記事にあるように、O’Reilly Japan – コンピュータシステムの理論と実装 、またの名を nand2tetris と呼ばれる本があります。NAND素子を出発点として簡単なゲームを作るまで(何故か作るのは名前に反してテトリスではない…)を一気通貫に説明してくれる本です。 上の記事の方は完走されたそうで、すごいですね。私は根気が続かず、途中でやめてしまいました…。お恥ずかしい。 しかしながら、やはりこの本が最高に楽しいのは前半のハードウェアのところではないかと思っています。私は本書に沿って、ハードウェアの動作をシミュレートするプログラムをRustで書いてみました。 やったのはずいぶん昔なのですが、上記の記事で思い出したので紹介してみます。キッカケを作ってくれた記事に感謝です。 NANDとフリップフロップ NAND pub fn nand(a: bool, b: bool) -> bool { !(a && b) } 言わずとしれたNAND素子です。 論理ゲートの入出力は電圧による 0/1 ですから、これを bool 型でシミュレートすることにします。この形式の関数で、2つの入力と1つの出力を持つ論理ゲートがシミュレートできることがわかると思います。 実装には && や ! といった演算子が使われていますね。こういった「高度な」演算子を使うのはここだけで、他の箇所では一切使いません。NAND素子を最もプリミティブな要素として、それを組み合わせて && のような論理演算をシミュレートしていくというのが目的なのですから、こういった演算子を使ってしまっては意味がありません。一方、nand() 関数だけはブラックボックスとして与えられるプリミティブな素子ですから、この実装だけはズルをするしかない、というわけです。 フリップフロップ nand2tetrisの紹介で「NAND素子を出発点として」と書きましたが、実はもうひとつ「フリップフロップ」も所与のものとして与えます。 コンピュータの状態遷移はクロック信号によって駆動されます。従って、コンピュータやそれを構成する部品は、次のようなクロック信号のループに駆動されて動くというモデルで考えていきましょう。 loop { hardware.clock(...); } これを踏まえて、フリップフロップを次のようなコードでモデル化します。 pub struct Flipflop { bit: bool } impl Flipflop { pub fn new() -> Self { Self { bit: false } } pub fn out(&self) -> bool { self.bit } pub fn clock(&mut self, a: bool) { self.bit = a; } } フリップフロップは1bitの状態を持っています。 入力 in が変化しても、すぐには出力 out には反映されず、内部で持っている bit の値を出力し続けます。そしてカシャッと clock が入力されたタイミングで、入力の値が内部に取り込まれます。 これをRustでシミュレートしたものが上記のコードです。out() 関数は単に self.bit を返す関数であり、clock() 関数によって内部状態を入力値に置き換えます。 clock() の実装において、値(状態)の代入という「高度な」操作が使われています。しかしこれ以降、Flipflop の内部以外では一切、mutable な状態変数への代入という操作は行いません。コンピュータは状態遷移機械であり、その「状態」を保持するための最もプリミティブな機構がこのフリップフロップです。それをシミュレートするのが目的ですから、Rust言語が備えている状態保持の機能を使ってしまっては意味がありません。フリップロップだけは、NAND素子と同様にブラックボックスとして与えられるものですから、その実装では「ズル」をしています。しかしこれ以降は、「状態」はすべてフリップフロップを組み合わせて表現していくことになります。 1bit レジスタを作る ではいよいよ、レジスタを作っていきましょう。まずは最も簡単な、1bitだけを保持するレジスタです。こんな形をしています。 フリップフロップと比較すると、load という入力が増えていることが分かります。clock のタイミングで内部の状態が遷移するという点はフリップフロップと同じです。 この構造をRustのコードにすると、次のようになります。 pub struct BitRegister { flipflop: Flipflop } impl BitRegister { pub fn new() -> Self { Self { flipflop: Flipflop::new() } } pub fn out(&self) -> bool { self.flipflop.out() } pub fn clock(&mut self, input: bool, load: bool) { ... } } BitRegister 型は、内部にフリップフロップを1つ保持しています。そして out() はフリップフロップの out() をそのまま返しています。 問題は clock() の実装です。この関数で、input と load という2つの入力に応じて内部の状態が遷移します。 実現したいのは次のような動きです。 impl BitRegister { pub fn clock(&mut self, input: bool, load: bool) { self.flipflop.clock(if load { input } else { self.out() }) } } 要するに load が true の場合のみ input が取り込まれて、load が false の時には状態は遷移しない、というわけですね。 しかし、上記は if 式を利用しています。これはズルです。物理デバイスに if を直接実現するものはありません。ですから、NANDを組み合わせて if に相当する回路を組まなくてはなりません。if すら使ってはいけないプログラミング、相当頭おかしい感じがしますが、やっていきましょう。 1bit レジスタを実現する回路は、下図のようなものです。(※ DFF と書かれているのは Data Flipflop の略で、要するに上で定義した Flipflop 型です。) Mux という素子が登場しています。これは multiplexor と呼ばれる素子で、if に相当する機能を担うものです。Rust で表現すると次のような動作をします。 pub fn mux(a: bool, b: bool, sel: bool) -> bool { if sel { b } else { a } } Mux は a, b, sel の3つの入力を持ち、sel の値に応じて a または b を出力します。上のコードは if を使ってズルをした実装になっていますが、これはあとで直すとして、まずはこの mux() を使って BitRegister::clock() の実装を書き換えてみましょう。 impl BitRegister { pub fn clock(&mut self, input: bool, load: bool) { // self.flipflop.clock(if load { input } else { self.out() }) self.flipflop.clock(mux(self.out(), input, load)) } } 上の回路図と見比べると、きちんと対応していることが分かるでしょう? これで BitRegister から if を取り除くことが出来ました。あとは mux() の実装のズルを取り除いて、全てNANDの組み合わせで実現できれば完了です。 mux() は次のように書き換えることが出来ます。 pub fn mux(a: bool, b: bool, sel: bool) -> bool { // if sel { b } else { a } (a && !sel) || (b && sel) } あとは &&, ||, ! という3つの論理演算子を nand() で表現できればOKです。 どん!答えは下記のとおりです。 pub fn not(a: bool) -> bool { nand(a, a) } pub fn and(a: bool, b: bool) -> bool { not(nand(a, b)) } pub fn or(a: bool, b: bool) -> bool { nand(not(a), not(b)) } pub fn mux(a: bool, b: bool, sel: bool) -> bool { or(and(a, not(sel)), and(b, sel)) } これで1bitのレジスタの完成です。 それにしても load という入力はどう役に立つのでしょうか? それは後ほどのお楽しみ。 16bit レジスタを作る 16bit を 1 word とするレジスタを作りましょう。まず Word を次のように定義しておきます。 pub type Word = [bool; 16]; [bool; 16] というのは長さ16(固定長)のboolの配列型を意味しています。 ところで、「配列」を使うのはズルではないのでしょうか。我々は if や && すら使ってはいけないプログラミングに取り組んでいます。「配列」は使ってはいけない「高度な」機能ではないのでしょうか。 心配は無用です。16本の導線を束にすれば、ハードウェアで [bool; 16] を実現することが出来ます。もちろん可変長の配列を使うことは出来ませんが(ハードウェアで動的に導線が増減したら怖い)、固定長なら問題ありません。 というわけで、16bit レジスタは下記のコードになります。 pub struct Register { bits: [BitRegister; 16] } impl Register { pub fn new() -> Self { Self { bits: [ BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), ] } } pub fn out(&self) -> Word { [ self.bits[ 0].out(), self.bits[ 1].out(), self.bits[ 2].out(), self.bits[ 3].out(), self.bits[ 4].out(), self.bits[ 5].out(), self.bits[ 6].out(), self.bits[ 7].out(), self.bits[ 8].out(), self.bits[ 9].out(), self.bits[10].out(), self.bits[11].out(), self.bits[12].out(), self.bits[13].out(), self.bits[14].out(), self.bits[15].out(), ] } pub fn clock(&mut self, input: Word, load: bool) { self.bits[ 0].clock(input[ 0], load); self.bits[ 1].clock(input[ 1], load); self.bits[ 2].clock(input[ 2], load); self.bits[ 3].clock(input[ 3], load); self.bits[ 4].clock(input[ 4], load); self.bits[ 5].clock(input[ 5], load); self.bits[ 6].clock(input[ 6], load); self.bits[ 7].clock(input[ 7], load); self.bits[ 8].clock(input[ 8], load); self.bits[ 9].clock(input[ 9], load); self.bits[10].clock(input[10], load); self.bits[11].clock(input[11], load); self.bits[12].clock(input[12], load); self.bits[13].clock(input[13], load); self.bits[14].clock(input[14], load); self.bits[15].clock(input[15], load); } } 単に BitRegister を16個並べたものが Register です。動作は上のコードを読めばすぐに分かるでしょう。 それにしても Register::clock() の実装、これはひどいですね。for ループ使えや!と言いたくなります。 が、「ハードウェアに for ループはない!」という強い信念の元(?)、あえてループは使わずに実装しました。こうやってベタッと書いたほうが、回路図が透けて見える気がしませんか? 8ワード(16バイト)のRAMを作る Register を8個並べてRAMを作りましょう。骨組みは下記のようなコードになります。 pub struct RAM8 { registers: [Register; 8] } impl RAM8 { pub fn new() -> Self { Self { registers: [ Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), ]} } pub fn out(&self, address: [bool; 3]) -> Word { ... } pub fn clock(&mut self, address: [bool; 3], input: Word, load: bool) { ... } } Register とよく似ていますが、よく見ると out() と clock() に address: [bool; 3] という引数が新たに加わっています。address は要するに、ポインタです。レジスタは8個ですから、3bit のアドレスで一意に指定することが出来ます。out() は address で指定されたアドレスのレジスタを読み取りますし、clock() は address で指定されたレジスタの値を書き換えるというわけです。 RAM8::out() 実現したいのはこういう動作です。 impl RAM8 { pub fn out(&self, address: [bool; 3]) -> Word { match address { [false, false, false] => self.registers[0].out(), [true, false, false] => self.registers[1].out(), [false, true, false] => self.registers[2].out(), ... [true, true, true ] => self.registers[7].out(), } } } もちろん match 式は「ズル」ですから、これを使わずに論理回路で分岐を実現しなくてはなりません。 1bit レジスタのときには、if 式を mux() に置き換えたのでした。ここでも mux() を組み合わせて拡張していきます。 まず次のような動作をする mux16() というものを作ります。 pub fn mux16(a: Word, b: Word, sel: bool) -> Word { if sel { b } else { a } } ほとんど mux() と同じに見えますが、入出力が bool から Word (16bit)に拡張されていることに注意してください。 これは、次のように mux() をひたすら16個ならべることで実装できます。 pub fn mux16(a: Word, b: Word, sel: bool) -> Word { [ mux(a[ 0], b[ 0], sel), mux(a[ 1], b[ 1], sel), mux(a[ 2], b[ 2], sel), mux(a[ 3], b[ 3], sel), mux(a[ 4], b[ 4], sel), mux(a[ 5], b[ 5], sel), mux(a[ 6], b[ 6], sel), mux(a[ 7], b[ 7], sel), mux(a[ 8], b[ 8], sel), mux(a[ 9], b[ 9], sel), mux(a[10], b[10], sel), mux(a[11], b[11], sel), mux(a[12], b[12], sel), mux(a[13], b[13], sel), mux(a[14], b[14], sel), mux(a[15], b[15], sel), ] } 続いて、次のような動作をする mux4way16() というものを作ります。 pub fn mux4way16(a: Word, b: Word, c: Word, d: Word, sel: [bool; 2]) -> Word { if sel[1] { mux16(c, d, sel[0]) } else { mux16(a, b, sel[0]) } /* 次のコードと等価 match sel { [false, false] => a, [true, false] => b, [false, true ] => c, [true, true ] => d, } */ } これは下記の実装で実現できることがすぐ分かるでしょう。 pub fn mux4way16(a: Word, b: Word, c: Word, d: Word, sel: [bool; 2]) -> Word { mux16(mux16(a, b, sel[0]), mux16(c, d, sel[0]), sel[1]) } 同様にして mux8way16() を作ることが出来ます。 pub fn mux8way16( a: Word, b: Word, c: Word, d: Word, e: Word, f: Word, g: Word, h: Word, sel: [bool; 3] ) -> Word { mux16( mux4way16(a, b, c, d, [sel[0], sel[1]]), mux4way16(e, f, g, h, [sel[0], sel[1]]), sel[2] ) } これを使って、RAM8::out() は次のように実装できます。 impl RAM8 { pub fn out(&self, address: [bool; 3]) -> Word { mux8way16( self.registers[0].out(), self.registers[1].out(), self.registers[2].out(), self.registers[3].out(), self.registers[4].out(), self.registers[5].out(), self.registers[6].out(), self.registers[7].out(), address) } } 以上で、指定された address のレジスタを読み取る回路が作れました。 RAM8::clock() address からの読み取りは出来ましたから、今度は address への書き込みを実装しましょう。実現したいのはこういう動作です。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input:Word, load: bool) -> Word { match address { [false, false, false] => self.registers[0].clock(address, input, load), [true, false, false] => self.registers[1].clock(address, input, load), [false, true, false] => self.registers[2].clock(address, input, load), ... [true, true, true ] => self.registers[7].clock(address, input, load), } } } しかし、ちょっとこれは無理があります。このコードは address の値に応じてクロック信号を入力するレジスタを切り替える書き方になっていますが、クロック信号は常に全ての素子に入力し続けなくてはなりません。 ですので、こんなふうな方針に切り替えます。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input:Word, load: bool) -> Word { let load8: [bool; 8] = match address { [false, false, false] => [load, false, false, false, false, false, false, false], [true, false, false] => [false, load, false, false, false, false, false, false], [false, true, false] => [false, false, load, false, false, false, false, false], ... [true, true, true ] => [false, false, false, false, false, false, false, load], }; self.registers[0].clock(input, load8[0]); self.registers[1].clock(input, load8[1]); self.registers[2].clock(input, load8[2]); self.registers[3].clock(input, load8[3]); self.registers[4].clock(input, load8[4]); self.registers[5].clock(input, load8[5]); self.registers[6].clock(input, load8[6]); self.registers[7].clock(input, load8[7]); } } 常に全てのレジスタにクロック信号が入力されていることが一目瞭然ですね。input も常に全てのレジスタに入力されています。 ではどうやって指定された address だけに書き込む制御をしているかというと、ここで load ビットが活躍します。指定された address のレジスタだけ load に true を入力することで、この制御をしています。BitRegister で仕込んだ load 入力の伏線を、ようやくここで回収することが出来ました。 では今までと同様に、if や match を使っている箇所(load8 を求めている箇所)を論理回路に置き換えていきましょう。 まず DMux (Demultiplexor)という素子を作ります。これは次のような動作をするものです。 pub fn dmux(input: bool, sel: bool) -> [bool; 2] { match sel { false => [input, false], true => [false, input], } } これは次のような論理回路で実現できます。 pub fn dmux(input: bool, sel: bool) -> [bool; 2] { [and(input, not(sel)), and(input, sel)] } これを組み合わせて、次の動作仕様の dmux4way() dmux8way() を作ります。 pub fn dmux4way(input: bool, sel: [bool; 2]) -> [bool; 4] { match sel { [false, false] => [input, false, false, false], [true, false] => [false, input, false, false], [false, true ] => [false, false, input, false], [true, true ] => [false, false, false, input], } } pub fn dmux8way(input: bool, sel: [bool; 3]) -> [bool; 8] { match sel { [false, false, false] => [input, false, false, false, false, false, false, false], [true, false, false] => [false, input, false, false, false, false, false, false], [false, true, false] => [false, false, input, false, false, false, false, false], ... [true, true, true ] => [false, false, false, false, false, false, false, input], } } これらから match 式を除去して論理回路としてどう実装できるか、考えてみて下さい。 これを使うと、RAM8::clock() は次のように実装できます。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input: Word, load: bool) { let load = dmux8way(load, address); self.registers[0].clock(input, load[0]); self.registers[1].clock(input, load[1]); self.registers[2].clock(input, load[2]); self.registers[3].clock(input, load[3]); self.registers[4].clock(input, load[4]); self.registers[5].clock(input, load[5]); self.registers[6].clock(input, load[6]); self.registers[7].clock(input, load[7]); } } おわりに このあとは、RAM8 を8個並べて組み合わせて RAM64 を作り、RAM64 を8個並べて RAM512 を作り…、と続けてRAMを大きくしていきます。 そして、CPUを論理ゲートとレジスタの組み合わせから構成し、CPUとRAMを繋げて、ROMから機械語コードを読み出して実行するようにしていきます。 これが組み上がって動いたとき、何とも言えない感動を覚えたものです。特にレジスタやRAM周りの仕組みにワクワクしました。コンピュータというのは、クロック信号でカチカチと動いていく壮大なピタゴラ装置なんだということが実感できました。 書いたコードはここに置いてあります。ドヤァ!https://github.com/u1roh/nand2tetris …と思ったら、あれ?これ動かないっすね…。 ディスプレイをシミュレートするところを glium という OpenGL ラッパーで作ったのですが、久しぶりに動かそうとしたら動かない…。 今ちょっと原因を調べる時間も取れないので、すんません、ダサい感じの終わり方になりましたが、以上です。 The post RustでRAMの動作原理をシミュレートする appeared first on CADDi Tech Blog.
こんにちは。テクノロジー本部バックエンド開発グループの江良です。 この記事は CADDi Advent Calendar 19 日目の記事です。昨日は、狭間さんによる「GraphQL PaginationのNestJSでの実装」でした! 「バックエンド開発グループの〜」と自己紹介したばかりで恐縮なのですが、今日はフロントエンドの話をします。 はじめに これはなに まえがき 使用したライブラリのバージョン Apollo Client について Apollo Client のキャッシュとは Apollo Client 3.0 の新機能 ページネーションの設計 オフセットベース カーソルベース 閑話休題 サンプルコードで学ぶ Apollo Client 3.0 導入 概要 Offset-based なページネーションを実装する Cursor-based なページネーションを実装する 動作確認 おわりに はじめに これはなに Apollo Client の 3.0 で追加されたキャッシュ周りの新機能を試してみた記事です offsetLimitPagination と relayStylePagination について触れています 実際に手元で動かせるコードを使って、ステップ・バイ・ステップで説明します 能書きはいいからコードを見せてくれ、という人はこちらをご覧ください。 gushernobindsme/apollo-client-v3-practice まえがき 私が所属する原価計算システムの開発チームでは、 バックエンド BFF フロントエンド という構成でシステムを提供しています。 バックエンド・BFF 間は gRPC、BFF・フロントエンド間は GraphQL で通信しています。 フロントエンドから BFF の GraphQL サーバにアクセスする際に使用しているのが、今回お話する Apollo Client というライブラリです。 弊チームでは、現在 Apollo Client のバージョン 2.6.9 を使用しているのですが、 3.0 以降で登場したキャッシュ周りの機能がなかなか便利そうだったので、今後のバージョンアップに備えて試してみたことをまとめてみます。 使用したライブラリのバージョン 検証には以下のバージョンを使用しました。 @apollo/client: 3.3.4 graphql: 15.4.0 Apollo Client について Apollo Client のキャッシュとは Apollo Client は、GraphQL クエリの結果をインメモリのキャッシュに保存します。 クエリの結果は正規化して保存され、 InMemoryCache というクラスから簡単に操作できます。 InMemoryCache は 公式ガイド にも記載の通り、簡単に使い始められます。 import { InMemoryCache, ApolloClient } from '@apollo/client'; const client = new ApolloClient({ // ...other arguments... cache: new InMemoryCache(options) }); 保存されたキャッシュには InMemoryCache の以下のメソッドを使うことでアクセスできます。 readQuery readFragment writeQuery writeFragment また、Apollo Client 3.0 からはキャッシュ内の個々のフィールドを更新するために modify というメソッドが追加されています。「mutation を実行した後、その結果をキャッシュに書き戻したい」といったユースケースで便利です。 詳細は 公式ガイド のほか、弊社フロントエンドエンジニアの桐生さんの記事にも詳しく書かれていますので、気になる方は読んでみてください。 Apollo Client 3.0 の新機能 Apollo Client 3.0 ではいくつもの新機能が追加されています。 詳細は Apollo の公式ブログ と マイグレーションガイド に譲りますが、その中でも特にパワフルなのが Pagination helpers の追加です。 これは文字通りページネーションの実装を助ける便利なヘルパ機能になります。 ページネーションの設計 さて、ここでちょっと脱線してページネーションを実現する API の設計方針について考えてみましょう。 ページネーションの設計は数あれど、大まかなパターンとしては以下の二種類に整理できるかと思います。 オフセットベース(Offset-based pagination) カーソルベース(Cursor-based pagination) オフセットベース オフセットベースはいわゆる offset と limit を使ってページングを行うやり方です。 offset にデータの取得開始位置を指定し、 limit に取得するデータ件数を指定します(SQL を書いたことのある人には馴染みのあるアレですね)。 例えばこんな風に指定すると、 SELECT * FROM transactions LIMIT 10 OFFSET 20; 先頭の 20 行目から 10 件のデータを取得してください、という意味になります。 カーソルベース カーソルベースはデータの取得を開始する位置をインデックスではなく、トークンで指定するやり方です。 この方式では first にデータ件数、 after にデータの取得開始位置を表す base64 エンコードされたカーソルを指定します。 { user { id name friends(first: 10, after: "opaqueCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } } この方式は GraphQL のサイトにて ベストプラクティス として紹介されているほか、GraphQL クライアントの Relay でも紹介されています。 GraphQL Cursor Connections Specification 閑話休題 さて、話を Apollo Client に戻します。 Apollo Client 3.0 では、上述した二種類の API のページング処理をいい感じにしてくれる便利な機能を提供しています。 Pagination helpers は InMemoryCache に対するオプションとして設定できます。 先ほど紹介した InMemoryCache のインスタンスを生成するコードを思い出してみましょう。 公式ガイド によると、ここに offsetLimitPagination を指定するとオフセットベースの API のページング処理がいい感じになります。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { comments: offsetLimitPagination(), }, }, }, }); また公式ガイドの このページ によれば、ここに relayStylePagination を指定するとカーソルベースの API のページング処理もいい感じになるそうです。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { comments: relayStylePagination(), }, }, }, }); 本当に、そんなうまい話があるのでしょうか? サンプルコードで学ぶ Apollo Client 3.0 導入 ということで、早速コードを書いて検証してみます。 やってみたことは以下の通りです。 オフセットベースのレスポンスを返す GraphQL のエンドポイントを実装する カーソルベースのレスポンスを返す GraphQL のエンドポイントを実装する Apollo Client 3.0 を組み込んだフロントエンドを実装し、Pagination helpers を設定する ここでは、ページングの動作を検証するためのシンプルな CRUD アプリケーションを実装してみます。 概要 ということで完成したのがこちらのリポジトリです。 gushernobindsme/apollo-client-v3-practice backend ディレクトリに NestJS 製の Graph サーバを実装 frontend ディレクトリに React 製のフロントエンドを実装 という構成になっています。 ページネーション以外の話題については、本記事では省略します。NestJS を使ったバックエンドの実装については、前日の狭間さんの記事に詳しく書いてありますので、是非読んでみてください! Offset-based なページネーションを実装する まず、オフセットベースの GraphQL の定義を用意します。 (バックエンド側の実装については割愛します。) type Query { sharks(offset: Int, limit: Int): [Shark] } type Shark { id: Int originalTitle: String japaneseTitle: String rate: Int } 次に、 offsetLimitPagination を設定した InMemoryCache を用意します。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { sharks: offsetLimitPagination(), } } } }) const client = new ApolloClient({ // other settings cache }); GraphQL のドキュメント定義を用意して、 const GET_SHARKS = gql` query getSharks($offset: Int, $limit: Int) { sharks(offset: $offset, limit: $limit) { id originalTitle japaneseTitle rate } } `; 戻り値の型を用意して、 interface SharksModel { sharks: Shark[]; } useQuery の hooks を実装します。 const { loading, error, data, fetchMore } = useQuery<SharksModel>( GET_SHARKS, { variables: { offset: 0, limit: 10 }, }, ); 最後に hooks を呼び出す component を実装して完成です。 // ... 略 <table> // ... 略 <tbody> {data && data.sharks.map((shark) => { return ( <tr key={shark.id}> <th>{shark.id}</th> <td>{shark.originalTitle}</td> <td>{shark.japaneseTitle}</td> <td> {shark.id && ( <Ratings id={shark.id} rate={shark.rate || 0} mutation={updateShark} /> )} </td> </tr> ); })} </tbody> </table> // ... 略 次の 10 件をフェッチするためのボタンも設置します。 次のデータの取得は fetchMore メソッドを呼ぶことで簡単に実装できます。 Core pagination API - Client (React) - Apollo GraphQL Docs <Button onClick={async () => { await fetchMore({ variables: { offset: data?.sharks.length, }, }); }} > fetch more </Button> Cursor-based なページネーションを実装する 次にカーソルベースの GraphQL の定義を用意します。 お作法にしたがって connection に edges と pageInfo を、 edges に node を定義してみます。 type Query { sharks(first: Int!, after: String): SharkConnection } type SharkConnection { edges: [SharkEdge] pageInfo: PageInfo } type SharkEdge { node: Shark cursor: String } type PageInfo { endCursor: String hasNextPage: Boolean } 次に、 relayStylePagination を設定した InMemoryCache を用意します。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { sharks: relayStylePagination(), } } } }) const client = new ApolloClient({ // other settings cache }); GraphQL のドキュメント定義を用意して、 export const GET_SHARKS = gql` query getSharks($cursor: String) { sharks(first: 10, after: $cursor) { edges { cursor node { id originalTitle japaneseTitle rate } } pageInfo { endCursor hasNextPage } } } `; 戻り値の型を用意して、 interface SharksModel { sharks: SharkConnection; } useQuery の hooks を実装します。 const { loading, error, data, fetchMore } = useQuery<SharksModel>( GET_SHARKS, { variables: { cursor: '' }, }, ); 最後に hooks を呼び出す component を実装して完成です。 // ... 略 <table> // ... 略 <tbody> {data && data.sharks && data.sharks.edges && data.sharks.edges.map((shark) => { const node = shark.node; return ( <tr key={node?.id}> <th>{node?.id}</th> <td>{node?.originalTitle}</td> <td>{node?.japaneseTitle}</td> <td> {node?.id && ( <Ratings id={node.id} rate={node.rate || 0} mutation={updateShark} /> )} </td> </tr> ); })} </tbody> </table> // ... 略 次の 10 件をフェッチするためのボタンはこんな感じです。 {data && data.sharks.pageInfo?.hasNextPage && ( <Button onClick={async () => { await fetchMore({ variables: { cursor: data?.sharks?.pageInfo?.endCursor, }, }); }} > fetch more </Button> )} 動作確認 実装が一通り書けたのでさっそく動かしてみましょう。 「fetch more」ボタンを押すと次の 10 件が表示されます。 さっそくデータを追加してみましょう。 追加できました。 ☆アイコンを押して評価をつけることもできます。 ( フランケンジョーズ は CG が本当にひどいので☆ 1 つです。) こちらも無事更新できました。 (ちょっとわかりにくいのですが)実際にうまくキャッシュが動作している様子は、先ほどご紹介したリポジトリをクローンして起動することでも検証できます。是非お手元で動かしてみてください。 おわりに ということで Apollo Client 3.0 で追加された新機能 Pagination helpers のご紹介でした。 明日は、寺田さんによる「RustでRAMの動作原理をシミュレートする」です。お楽しみに!
はじめに テオ。弾薬が尽きた。このまま突入する、さらばだ。ヴァルハラで会おう。Wir sehen uns in Walhalla! これは第二次世界大戦時のドイツ空軍のエースパイロット、ハインリヒ・エールラーの最後の言葉です。 北欧神話で戦死した人がラグナロク(終末の日)に備える場所とされるヴァルハラ。今日はそれにちなんで命名された解析ツールValgrindについて見ていきましょう。 申し遅れました。わたくしCADDiでコスト計算システムのバックエンドを担当している @kimu_di といいます。 こちら キャディAdvent Calendar 2020 17日目の記事となります。昨日は @yskeee000 の「 KleinというProductについて 」でした。 改めて、今回はValgrindという強力なコード解析・メモリデバッグツールについてお話します。 ここまで強力な解析ツールを使う必要はないことも多いですが、RustやC++を書く上で最後の最後のパフォーマンスの追い込みに力を発揮してくれること請け合いです。 改めてValgrindとは 普通、C++やRustで書いたプログラムはプロセッサに依存した実行可能ファイルへコンパイルされますね。Valgrindはそれを中間表現に変換し、それを再び機械語に翻訳し直して実行させるという手間を踏みます。パフォーマンスは低下しますが、中間表現を経由することで計測・解析・デバッグが容易になります。 ここにコードを追加することで様々なツールを機能させることが出来るようになっています。 よく使うツールをさらっとご紹介しましょう。 Memcheck 初期化されていない・あるいは解放されたはずのメモリの使用、境界外への読み書き、メモリリークを検出するツール Cachegrind キャッシュのプロファイラ。キャッシュのヒット率などを計測できる Callgrind コールグラフの作成、ブランチ予測などのツール Helgrind マルチスレッドのエラー検出ツール Massif ヒーププロファイラ 導入 Ubuntuでは $ sudo apt install valgrind のように導入します。 ビジュアライザも入れましょう。 $ sudo apt install kcachegrind massif-visualizer このあたりがあると幸せになれそうです。 kcachegrindでコールグラフを読み解く まずはコールグラフを見てみましょう。 プログラムをvalgrindに食べさせながら実行します。 $ valgrind --tool=callgrind プログラム名 引数 Rustで開発している途中ならこうなるでしょう。 $ cargo build --relase $ valgrind --tool=callgrind target/release/binary_name まさか本番環境でdebug版を動かしていることはないでしょうから、リリースビルドを選ぶのを忘れないようにします。 そのあとvalgrindにプログラムを渡して実行ですね。 実行するたびに callgrind.out.12345 のような名前でファイルが出力されます。 ls コマンドで確認できると思います。 このファイルの中身をビジュアライザで見てみましょう。 $ kcachegrind callgrind.out.12345 これでツールが起動します。 このような画面が表示されましたね。 メニューバーには真新しいものはありませんが、View -> Cycle DetectionだけはOFFにしておくことをおすすめします。 この機能は再帰的な相互呼び出しをヒューリスティックに解析してコストを表示してくれるのですが、ONになっていると実際の関数が <Cycle1> のような計測結果に覆い隠されてしまいます。関数別の呼び出しコストを見たいので、OFFで良いでしょう。 左側がflat profileと呼ばれる領域で、ここにコールスタックのようなものを表示してくれます。実際にこのようなコールスタックがあるわけではないですが、現在の関数から見て呼び出す関数/呼び出される関数を上下に並べてコストが高い順に表示されます。 右側には視覚化された「ツリーマップ」やコールグラフ、呼び出し/呼び出される関数のリストが並びます。 Caller というのは、今扱っている関数を呼び出している親玉のこと、 Callee は今扱っている関数が呼び出す子分のことを指します。 また、 CallGraph を選ぶと呼び出し階層をグラフ構造で表示してくれます。 この例ではripgrepというRust製の検索ツールをvalgrindで実行した結果を表示しています。 コールマップを見るとmain関数の中でばっくりと2つ大きな関数が占めていて、残りが小さめの関数で占められている事がわかります。 マップをクリックすると、それぞれの色分けされている領域がどの関数に対応しているかがわかります。 試しに押してみましょう。左側の大きな領域をクリックするとハイライトされますね。それは ignore::walk::WalkParallel::visit という関数で、すべての呼び出しのうち38%のコストを占めている事がわかります。 ダブルクリックするとコールマップが変わりました。 その関数の中に絞って表示してくれるというわけです。 このツールのソースコードを見れば、きっと重い実装であることが想像できますね。 crossbeam ってなんでしょうと思って見てみると、並行プログラミングのライブラリです。つまり、これ自体にあまり意味はないですね。 なので、さらにその内側を見るとエイホ・コラシックという関数が見えます。これは文字列探索アルゴリズムの名前です。きっとこのあたりが処理の中核だと当たりをつけ、どれくらいのコストが掛かっているか、何を呼び出しているかがわかるでしょう。 massifでヒーププロファイリングを覗いてみる ヒープとはご存知の通り、実行時に任意のタイミングで確保や解放ができるメモリ領域のことですね。 このプログラムがどれくらいのメモリを使うか探ってみましょう。 今度はmassifを使います。 $ valgrind --tool=massif --time-unit=B --stacks=yes プログラム名 引数 massif.out.23456 のようなファイルが出力されるので、これをビジュアライザに掛けます。 $ massif-visualizer massif.out.34567 例によってripgrepのメモリ消費を見てみました。 一目瞭然ですね。 右側でメモリ確保/解放の詳細を見ることができます。 今回はたいへん明確にメモリを使ったあと終了と同時に解放する右肩上がりのグラフになっていますね。 実務にて 業務にて、以上2つのツールを活用してコスト計算システムのコアのプロファイリングを行いました。 計算量の多く複雑なロジックを内包するコンポーネントだったのでボトルネックになっていることが予想されていましたが、意外にも、さほどメモリを消費せず、コールグラフを見るとメモリアロケーションが大半のコストを占めていたことが判明しました。 (残念ながらその結果は公開できないですが……。) ボトルネックを探る手段としてたいへん強力な手段になった一方で、使い方がやや難しいといいますか、タバコに火を付けるのに火炎放射器を使うこともないですから、まずは簡単にミリ秒単位での出力から始めると良いでしょう(と、ボスからアドバイスを受けました) 通常のバックエンド開発としてはAPI単位で時間計測していくほうが遥かに簡単で効果的ですが、難解なアルゴリズムを用いたソフトウェア開発で力を発揮するかもしれません。 主にC++を書く方へ valgrindの目玉ツールとして memcheck があります。 $ valgrind --leak-check=full プログラム名 本来valgrindと言ったらコレがまっさきに出てくる機能です。主にメモリリークの検出に使われるものなのですが、Rustを普通に書いているとまず遭遇しないので……。まあ見ていきましょう。 メモリ解放忘れ 以下のようなコードで発生させてみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *p << std::endl; } これをmemcheckに掛けると、 definitely lost: 確実に解放漏れがある、と怒られます。 delete p を呼んでいないからですね。 未初期化 今度は *p=0 を消してみました。 #include <iostream> int main() { int *p = new int; std::cout << *p << std::endl; delete p; } Use of uninitialised value of ...と怒られますね。 二重解放 解放済みのポインタをもう一度解放してみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *p << std::endl; delete p; delete p; } Invalid free() / delete / delete[] / realloc() と怒られました。そのとおり。 不正な読み込み ポインタをずらして読んでみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *(p + 1) << std::endl; delete p; } 4つ分後ろ見てるよ、と怒られています。 不正な書き込み 最後です。初期化に続けて違う領域に書き込んでみます。 #include <iostream> int main() { int *p = new int; *p = 0; *(p + 1) = 0; std::cout << *p << std::endl; delete p; } Invalid write of ...と怒られますね。 memcheckまとめ このように、厳格にメモリの使い方を指摘してくれますのでC++erの各位におかれましてはぜひお手元のプログラムを一度memcheckに掛けてみることをオススメします。 そして何よりも、メモリ安全なRustに切り替えることを考えると良いでしょう! おわりに Valgrindについて見てきました。 これであなたもプロファイリングマスターです。バイナリにビルドされているプログラムならたいてい解析できますので、お試しに身近なコマンドやプログラムを覗いてみてはいかがでしょうか。
えこんにちは!CADDiでプロダクトマネージャーとしている @yskeee000 です。 もうそろそろ仕事納めしたい 本記事は、キャディ Advent Calendar 2020 – Qiita の16日目の記事です。昨日の記事は松田さんの「 Rust入門者がrust-analyzerへのコントリビュートを達成するまで 」でした。 この記事では、私がプロダクトマネージャーとしてCADDiで携わっているプロダクトについて素朴に紹介したいと思います。 私はCADDiに入社して約1年半となりますが、ほぼ一貫して本記事で紹介する Klein というプロダクト開発に、スクラッチの段階から関わってきました。 現状では、内部に閉じたProductであり外部の方になかなか知られることがないですが、CADDiの根幹をなす重要なProductですので、出来るだけ平易に紹介できればと思います。 プロダクトの背景となるCADDiのビジネスプロセス ①顧客からの受注 ②パートナーへの発注 ③サプライチェーンの設計 ④製造~納品までの実行と実績の管理 KleinというProduct Kleinの持つ役割 果たす価値 これからの発展 名前の由来 P.S. 【余談】もう一つの名前の由来 プロダクトの背景となるCADDiのビジネスプロセス Kleinというプロダクトはいくつかの側面を持っていますが、 受発注管理 生産管理 サプライチェーン管理 物流管理 といったキーワードが内包する概念として近いでしょうか。 こういったビジネスプロセスが発生する、ということがこのプロダクトの価値の前提なのでまずは簡単にそこから出発したいと思います。 ここでは特に関わる範囲のプロセスに絞ってご紹介します。(全体像については以前noteに投稿した こちら ) ①顧客からの受注 顧客から、金属加工品の製造を受注します。 例えば、工場やプラントの中で使う産業装置の部品となる以下のようなものです。 CADDiでは 特注品 と言われるものを扱っておりますので、カタログに規格化された部品ではなく、顧客それぞれが装置ごとのために設計した図面を元にオーダーメイドのものとして受注します。 大きな案件では一案件につき、数千点にのぼる部品数になります。 それぞれについてどういう前提で、いくらで受注したかという情報が発生します。 ②パートナーへの発注 CADDiは自社で製造能力を持たない事業構造のため、パートナーに発注を行ってファブレスにこれらの部品の製造を進めます。 (※ パートナー: CADDiではアラインアンスを結んだサプライヤー(加工会社)をこのように読んでいます。) この時、それぞれのパートナーの強み(=価格・品質などの面で競争力が高い)に合わせて、特性にあった部品を発注していきます。 強みは 鉄/ステンレス/アルミといった材質 板金/旋盤/フライスなどの加工の種類 どれだけのロット数を作るか 加工物のサイズ 求められる品質や精度 などの様々な切り口によって別れていきます。 また、この時の発注の仕分けは部品ごとの粒度ではありません。 例えば、金属自体を切削などによって削る部分はパートナーA、塗装やメッキなど表面への加工はパートナーBのようにその部品に施すべき 工程 というより小さい粒度によります。 結果として、一つの案件において数十のパートナーに発注が分割されることもあります。 ③サプライチェーンの設計 製品は最終的には顧客に納品されなければなりません。 それまでに、例えばパートナーから次の加工のパートナーにわたる、東西にあるCADDiの物流拠点において検品を行う、顧客に送るなどの物流の情報が発生します。 いつ/どこに/何を/どれだけ送るか、というものの流れの情報を、誰が/どの工程を担当するかの情報に加えて管理する必要があります。 大量生産品のように、ものが量産されていく一つの大きなサプライチェーンを確立するのではなく、案件ごとに最適なサプライチェーンをその時ごとに設計して管理する必要があります。 弊社の顧客となる装置メーカー様などでは、部品を納品した後に組み立てる工程が待っており、そのペースに合わせて納品スケジュールを分割していくことが必要な場合も多いので、より複雑になり得る要素も含んでいます。 ④製造~納品までの実行と実績の管理 ③でサプライチェーンの設計ができたとして、製造と輸送が進むにつれて実際にいつどこに何が届いたか、何を納品したか、という実績も管理していく必要が出てきます。 望ましくはないことですが、時に不良や納期遅延が発生するので、それらをきちんと記録として残しつつ、そういったイレギュラーに対応しながらトラブルシューティングしていくことも求められます。 KleinというProduct その中において、顧客とパートナーを繋ぐリボンモデルを繋ぐ存在として業務プロセスに伴走するのがKleinというProductです。 リボンモデルとはいうものの、よく弊社のビジネスそのものが勘違いされるのですが、単なるマッチングではなく、納品・品質責任を持つ存在として、オペレーションを遂行するという責任を弊社は追っています。 顧客はキャディに発注し、パートナーはキャディという顧客から受注するということです。 この大きな違いがあるためにこのProductは要請されます。 Kleinの持つ役割 こういったプロセスの中で、KleinというProductが持つ役割はどのようなものでしょうか? 現状から一部を抜粋すると例えば以下のようなものです。 受発注管理 受注した製品とそれに求められる工程(=発注して手配すべきもの)の情報が管理できます それらの製品に対応する図面のデータを管理することができます どのパートナーにどの工程を依頼するかの判断を助けることができます それを元にして、パートナーに対して工程を簡単に割り振り、発注データを作ることができます 発注もれや二重発注をチェックすることができます 帳票として発注データを元にして発注書の発行ができます サプライチェーン管理 どこから/どこに/何を/どれだけ運ぶかという物流の情報を管理できます 結果として形成されたサプライチェーンのグラフ構造を直感的に図解することができます サプライチェーンのものの流れが整合的かをチェックすることができます あまり多くの情報を記載することができませんが、例えばサプライチェーン全体を管理するのは以下のようなUIを通じて行うなどします。 果たす価値 結果として、このプロダクトが果たしている役割はどんなものでしょうか。 受発注業務のプロセスに伴走するシステムとして、業務プロセスの 標準化 を促進する 自動化プロセスを組み込んでいくことにより 業務工数を削減 する 直感性的UXやデータ/ロジックによって、 人間の正しく迅速な判断をアシスト する 受発注/サプライチェーンにおいて発生するデータを正しい形で蓄積することで データドリブンな 業務/システム改善、また データによって生み出される新たな価値 の土台となる こういった点があげられると思います。 さらに、個人的な観点でいうと、よりマクロで見た時に サプライチェーンを構築・管理する工数を圧倒的に下げることにより、より小さい粒度、例えば部品単位ではなく工程単位での流動性を高めることで、受発注のトラフィックをより強みに合わせて最適に調整する ということが価値であると考えています。 流量をより細分化しても低負荷で最適化できる水路のようなイメージです。 ソフトウェア技術によって、ビジネスにおけるバックエンド情報をより複雑な場合でもリーンに扱うことができるようにすることによって、取引の単位をより小さい単位で、より最適にマッチング ~ 実行することができるようになっていくのです。 これからの発展 KleinはCADDiという受発注プラットフォームにおいて、扇の要となるようなプロダクトと考えています。 実際に使う一次ユーザー以外にも、他のプロダクトやデータを活用するユーザーにも価値を伝播させていくことを前提に土台から設計しており、進化の方向性は複雑ですが多方面にわたります。 今現段階での展望にすぎませんが、以下のような9つのベクトルを意識して、個別の具体的な実装の補助線としています。 名前の由来 最後に、Kleinという名前の由来ですが、これはクラインの壺 ( wiki ) というものに由来します。 ちょっときもいですね。 この壺は表面を辿っていくと表と裏が入れ替わるという構造を持っています。 (メビウスの輪の3次元版のイメージです) CADDiは顧客から受注し、パートナーに発注するという受注側/発注側のどちらのロールも内包しています。 このことから得られるソフトウェアとそれを前提にしたオペレーションのドッグフーディングが、将来的により直接的に顧客/パートナーへのテクノロジーによる価値提供に繋がるというvisionから名付けました。 パートナーへの発注者としてのナレッジの形式知化が、同じ発注という業務を行う顧客への転用につながっていくと考えており、その逆も然りということです。 まだまだやることは無限に山積みですが、一緒に遠くへこのプロダクトを連れて行ってくださる方をまだまだ募集しています! P.S. 【余談】もう一つの名前の由来 Kleinというのは一つのProductですが、そのシステムのなかの一つのコンポーネントとして、 h20e というコンポーネントがあります。 このコンポーネントは案件管理という役割を担っているのですが、Kleinのリポジトリなどにはよくこの h20e というワードが登場します。 Kleinの立ち上げ当時、上記の業務プロセスを統括していたのは創業メンバーの一人でもある 幸松大喜 という人物です。 仕事ができてリーダシップがあるばかりでなく、なんと縁起のいい名前なのでしょうか。 幸せで、最上で(松竹梅)、大きな喜びなのです。結婚式ではぜひ乾杯を頼みたい。 この縁起の良さにあやかるべく、 happy pine great pleasure -> h20e という名前を授けました。 これから長い間このproductが使われていく中で、この魂も脈々と受け継がれていくということでしょう。
はじめに はじめまして、テクノロジー本部バックエンド開発グループの松田です。 この記事は、 CADDi Advent Calendar 15日目の記事です。昨日は、和田さんによる「 Rust と nalgebra で MLP を実装した話 」でした! CADDiでは、バックエンドの主要な開発言語としてRustを採用していますが、エンジニアの多くはRust未経験の状態でCADDiに入社し、入社前後にRustの勉強を開始しています。11月に入社した私も、Rustを本格的に利用するのはCADDiが初めてで、入社が決まって以降、Rustの勉強を行っていました。 また、これは個人的な話ですが、これまでにOSSへのコントリビュートを一度も行ったことがなく、いつかはOSSコントリビュートを行ってみたいと思っていました。 今回、Rust入門とOSSコントリビュート達成という2つの目標を同時に追求するため、「何でもいいからRustのOSSのIssueをひとつ選んでコントリビュートする」ということを行ってみました。 その模様を、毎週火曜日の午前に行っているCADDiの社内勉強会である「STUDDi」で発表しましたので、ダイジェストをここにまとめたいと思います。 伝えたいこと OSSへのコントリビュートに興味があるがコントリビュート経験がない読者様に対し、 手頃なIssueを探せば、その言語や技術の熟練者でなくてもOSSにコントリビュートすることは可能 簡単に見えるIssueであっても、取り組んでみると勉強になる ことをお伝えすることがこの記事の目的です。 逆に、既にOSSへのコントリビュートの経験がある方にとっては、あまり得られるものがないかもしれません。 いろいろなコントリビュート OSSへのコントリビュートを行うきっかけとしてよくあるのは、「あるOSSを自プロダクトで利用したいが、必要な機能が存在しない」という状況だと思います。この場合、どのOSSに対してどのようなコントリビュートを行うべきかは明確です。 一方、私の場合、身も蓋もなく言えば、「何でもいいからRust製OSSへのコントリビュートをしてみたい」というモチベーションだったため、何から手を付ければ良いか分かりませんでした。 このため、私はまず、Rustの公式ウェブサイトの コントリビュータ向けページ に飛びました。 すると、一口に「コントリビュート」といっても以下のような様々な方法があることが紹介されていました。 問題を捜し、仕分け、修正する ドキュメント コミュニティの形成 ツール、IDE、インフラ ライブラリ 言語、コンパイラ、標準ライブラリ 国際化 このように、「コントリビュート」には、ソースコードを書いてPRを提出することだけでなく、ドキュメントの整備や、コミュニティの形成といった取り組みも含まれます。 ただし、私のモチベーションとしては、とりあえず何らかRustのソースコードに手を入れてみたい気持ちがありました。そのため、上記のうち、「問題を捜し、仕分け、修正する」の方法に取り組むことにしました。 初心者向けのIssueを探そう さて、問題を捜し、仕分け、修正すると言っても、私はRust入門者ですから、いきなり複雑度の高いIssueに取り組むのは困難です。幸い、Rustのコントリビュータ向けページでは、初心者向けのIssueの探し方が紹介されていました。 それは、E-easyまたはE-mentorというラベルが付いたIssueを探すことです。 E-easyは文字通り、難易度が低いことを示すラベルです。一方、E-mentorは、そのIssueに関連するコードや、修正の方針について相談に乗ってくれるメンターがアサインされていることを示します。 また、上記の他、ソースコードの該当箇所や、修正の方向性などが分かりやすくブレークダウンして記載されていることを示すE-has-instructionsタグや、そのOSSに初めてコントリビュートする人向けのIssueであることを示すgood first issueラベルなども存在します。 good first issueを求めて 私は、とりあえず上記のラベルが付けられたIssue(例えば、 good first issueタグが付いたIssueの一覧 )を片っ端から見て回り、自分でも取り組めそうなIssueを探すことにしました。 ここで直面したのは、手軽に取り組めそうなIssueほど、既に他の開発者が着手してしまっているという問題です。 業務のプログラミングであれば、新規メンバー向けの手頃なタスクを先輩メンバーが用意してくれたりするものですが、OSSでは誰かがタスクをあてがってくれるわけではないので、この点はどうしようもありません。 私は、自分が取り組めそうで、かつ、他の開発者が着手していないIssueを手に入れるため、 rust-lang/rust , rust-lang/rustfmt , rust-analyzer/rust-analyzer など、思いついたRust製OSSのリポジトリのIssueをひたすら探してまわりました。 誰も着手していないIssueを発見 その結果、発見したのが、rust-analyzerの このIssue です。 これは、rust-analyzerの機能のうち、Assistと呼ばれる入力補助機能群の一つに関するテストについて、冗長になっているものを最小化せよというIssueです。 具体的には、テスト対象の関数内に分岐Aと分岐Bが存在して、それぞれM通りとN通りのパスに分かれているとき、分岐A内の処理と分岐B内の処理の内容が独立であればテストケースの数はM+N個で済むはずのところ、MxN個のテストケースが書かれてしまっていることが問題となっていました。 整数リテラルの基数を変換するAssist機能 テスト対象のAssist機能は、具体的には、エディタ上に入力された整数リテラルの基数を変換するという機能です。 rust-analyzerをインストールしたエディタ上で整数リテラルを入力し、その上にカーソルを合わせると、電球のマーク(VSCodeの場合)が表示されると思います。この電球のマークをクリックすると、 Convert integer base という選択肢が表示されます。これを選択することにより、 100 を 0b1100100 に、あるいは 100 を 0x64 に、といった具合に基数を変換することができます。 テストの肥大化 今回のAssist機能の場合、 基数は2, 8, 10, 16の4通り 整数リテラルがunderscoreで区切られているかどうかで2通り Rustでは、 100_000 のようにunderscoreで整数リテラルを区切ることができます 型名がsuffixとして付いているかどうかで2通り Rustでは、 100i64 のように型名をsuffixとして付けることができます という分岐が存在するところ、 変換元の基数 * underscoreの有無 * suffixの有無 の組み合わせ全パターンのテストが存在する状態となっていました。 rust-analyzerのコード規約においても、テストは最小化することとされており、これは コード規約 にも反している状態でした。 Issueを完遂できるか検証する 簡単そうなタスクだと思ったら、依存関係が複雑に広がっていて想定の何倍も手がかかった、という経験は日常業務においても誰しもしていることだと思います。 OSSの場合、休日に趣味として取り組んでいるわけで、軽い気持ちで手を出してみたもののなかなか完遂できず、しかし一度着手したものを投げ出すのも……という気持ちででダラダラと作業を続ける、という状態になってしまうと精神的に辛そうです。 そこで、このIssueが本当に自分の想定の範囲内で完遂できるかどうか、事前に検証することにしました。 幸い、 修正対象のコード の位置は、Issueから貼られたリンクにより示されていました。初心者向けのタグが付けられたIssueの場合、このようにコードの該当箇所へのリンクが示されている場合が多いようです。 また、 docs/dev/ 以下に置かれたドキュメントを参照しつつ、rust-analyzer全体のアーキテクチャを確認しました。その結果、今回の対象となるAssist機能は、rust-analyzerのコアな部分から疎結合な設計となっており、rust-analyzer全体を理解しなくても着手できそうだということが分かりました。 以上を踏まえ、休日の1日の間に完遂できそう、と判断しました。 手を挙げる やるときめたらすぐに手を挙げておかないと、他の開発者が同じIssueに着手してしまいます。 そこで、すぐに手を挙げました。 すると、10分も経たないうちにメンテナの方から返信があり、このIssueは私が担当することが決まりました。 テスト対象機能内の各分岐は独立 晴れてIssueを獲得したので、さっそく修正に着手します。 テスト対象の機能においては、変換元の基数、underscoreの有無、suffixの有無に応じて分岐が生じていることを上述しましたが、これらの分岐が相互に独立していると言えるのかどうか、改めて検証してみました。 その結果、これら3つの分岐は相互に独立していることが確認できました。 具体的には、テスト対象の機能においては、以下のような順序で処理が行われていました。 まず、underscoreとsuffixを除去する その際、suffixについては、その有無と内容をOption型の変数に保持 underscoreとsuffixが除去された整数リテラルの基数を変換 suffixが付いていた場合、変換後の整数リテラルに変換前と同じsuffixを付加 したがって、underscore有りの場合およびsuffix有りの場合については、どれか1つの基数の場合についてのみテストを行えば足りるということが分かりました。 実際のコードは以下のとおりです。それぞれの処理は、他の分岐でどのパスを通ったかに影響されないことが分かるかと思います。 underscoreの除去 let buf; if text.contains("_") { buf = text.replace('_', ""); // underscoreの除去 text = buf.as_str(); }; let value = u128::from_str_radix(text, radix as u32).ok()?; Some(value) 基数の変換とsuffixの付け直し // 変換先の基数に応じて変換 let mut converted = match target_radix { Radix::Binary => format!("0b{:b}", value), Radix::Octal => format!("0o{:o}", value), Radix::Decimal => value.to_string(), Radix::Hexadecimal => format!("0x{:X}", value), }; let label = format!("Convert {} to {}{}", literal, converted, suffix.unwrap_or_default()); if let Some(suffix) = suffix { converted.push_str(suffix); // suffixを付け直す } 不要なテストケースをひたすら消していく あとは、不要なテストケースをひたすら消していくだけのお仕事です。 具体的には、underscore有りの場合およびsuffix有りの場合については、変換元の基数が10進数の場合のテストのみを残し、それ以外の基数のテストは削除しました。 また、これまでの説明では省略しましたが、テスト対象の機能について、私が作業を行う1週間ほど前にリファクタリングが行われていたところ、それによって不要になったはずのエッジケースに対応するテストなども残存していたので、これらもまとめてお掃除しました。 リポジトリをforkしてPR 通常の社内リポジトリの場合、作業対象のリポジトリ内で feature ブランチなどを切り、 master/main ブランチなどに対してPRを送るのが一般的だと思います。 これに対し、OSSの場合、作業対象リポジトリを自分のGitHubアカウント内にforkし、自分のアカウント内で作業を行ったものをfork元リポジトリに対してPRする、という手順が採られることが多いようです。 このようにするメリットとして、PRがマージされない限り、作業対象リポジトリに対して一切影響を与えないことが保証されるという点が挙げられます。 完成したPR 完成したPR は以下のとおりです。 rust-analyzerの場合、PRやコミットメッセージの書き方に明確なルールはないようだったので、よしなに書きました。 なお、私の場合、英語でのコミュニケーションにあまり自信がありません。そのため、PRの内容に細かい質問をされて英語でのディスカッションが始まると辛いなと思ったので、初めから丁寧な説明を書いて、それを読んで全てを理解してもらう、という戦略を採りました。 diffは以下のような感じです。 ほとんど削除しただけなので真っ赤ですね。 Merged! PR提出後、ドキドキしながら晩ごはんを食べていたのですが、30分も経たないうちにメンテナの方によりマージされました。 削除するだけの簡単な内容ですが、Perfectと言ってもらえると嬉しいですね。 まとめ 初心者でもOSSへのコントリビュートは可能 OSS、それも、rust-analyzerのようにある程度複雑で規模の大きいOSSへのコントリビュートというと、その言語を初めたばかりの初心者には難しいのではないかと思ってしまいがちです。 しかしながら、自分の力量にあったIssueを探せば、初心者でもOSSへのコントリビュートは十分に可能と感じました。 逆に言えば、そのような手頃なIssueを探すことが最も大変だとも言えます。簡単なIssueほどすぐに着手されがちであり、空いていることが少ないからです。 この点については、初心者向けのタグが付いたIssueを片っ端から当たっていくしかないのではないかと思います。 簡単なIssueであっても、やってみると勉強になる 漠然とコードリーディングをするのと、1行でも変更するつもりで読むのとでは、当事者意識が大きく違ってくると感じました。 今回の場合、やったことはテストを削除しただけですが、あるテストケースを削除して良いかどうか判断するためには、テスト対象の処理を注意深く読む必要があり、その結果として、テスト対象の処理そのものについても理解を深めることができました。 自分のPRがマージされたときの達成感は大きい 自分のPRが国籍も所属会社も異なる海外のメンテナによって評価され、マージされるという体験は、日常業務とはまた異なる達成感がありました。とても楽しいので今後も挑戦していきたいです。 最後に CADDiでは、フロントエンド,バックエンド,アルゴリズム,SREと幅広くエンジニアを募集しております.興味をお持ちいただけた方は, こちら からご連絡ください! 明日は、 @yskeee000 さんが、CADDiのサプライチェーン管理システムについてご説明する予定です!
1. Rust Christmas, I gave you my heart はじめに 折角クリスマスなので、楽しい近未来感のある技術を検証してみようと思います。お仕事で使っているRust、 Kubernetes 、そして個人的に興味のあるWebAssemblyをガッチャンコ出来ないか考えてみた。技術者としては常に勉強をする事が重要だと思っているので勉強兼ねて 「highly experimental」 と注意書きのある技術の紹介をしたいと思います。 本日は会社の アドベントカレンダー ということもあり仕事しているふりをする必要があるため、ポエムも混ぜながら Kubernetes 上でWebAssemblyのモジュールを実行するKrustletを使ってみたお話をします。 Krustletとは? GitHubのページ 曰く: Krustlet is a tool to run WebAssembly workloads natively on Kubernetes . Krustlet acts like a node in your Kubernetes cluster. 流行りの言葉を並べた感じになりますが、今年の4月に Microsoft Azure部門で始まった Krustlet プロジェクト。WebAssemblyモジュールを直接 Kubernetes クラスタ ー上でPod同様に実行する Kubernetes のNode同等のプログラム。 通常の Kubernetes クラスタ ーはマスターと、複数のノードによって構成され、各ノード内のコンテナをKubeletがマスターからの指示の元手配する。新しくコンテナを クラスタ ー上で動かしたい場合マスターに依頼を送れば、後はマスターが勝手に適切なノード上で動かしてくれます。 環境構築と設定 まずは Kubernetes の クラスタ ーが必要です。今回はminikubeで立ち上げました。こちらの図で描かれているように、既存のコンテナ実行するNodeと、新たにKrustletを利用してWasm実行するNodeの2つを並行で利用します。 $ minikube start 立ち上がったら動作確認しましょう。minikubeは勝手にkubectlの設定もしてくれるので、そのまま試せるはずです: $ minikube status host: Running kubelet: Running apiserver: Running kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100 $ kubectl cluster-info Kubernetes master is running at https://192.168.99.100:8443 KubeDNS is running at https://192.168.99.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. Kubernetes dashboard も立ち上げることが出来ます。 kubectl で取得できる情報ばかりですが、見やすいので一応裏で立ち上げておきます。 $ minikube dashboard 🔌 Enabling dashboard ... 🤔 Verifying dashboard health ... 🚀 Launching proxy ... 🤔 Verifying proxy health ... 🎉 Opening http://127.0.0.1:36305/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/ in your default browser... ブラウザー 開くとこんな感じの ダッシュ ボード。 Kubernetes dashboard 手元で Kubernetes クラスタ ーが動いているので、続いてkrustletを手元で実行します。 GitHub からバイナリーを直接ダウンロード出来ます。 wasi 版と wascc 版がありますが、今回は wasi で行きます。違いをすごく雑に纏めると、Wasmのモジュールが外部環境とインタフェースする上で必要な接続 プロトコール が2種類あり、それに適したバイナリーを利用する必要がある。 こちらのバイナリーは、kubelet同様 Kubernetes のNode上でPod実行出来る環境を用意するものです。普段だとNode上でコンテナが実行されますが、Wasmの場合はKrustletがWasmを実行する形になります。 Kubernetes のcontrol planeにアクセスして、「該当するPodは実行出来るので待っています」という登録をする必要があり、こちら少し分かりにくいんですがささっと進めていきます。 まずはBootstrapから始めます。詳細はこちらを参考にしてください。 $ bash <(curl https://raw.githubusercontent.com/deislabs/krustlet/master/docs/howto/assets/bootstrap.sh) 裏ではまずKrustletが Kubernetes のマスターと通信出来るように 秘密鍵 等を整備している。こちらの情報が最終的には bootstrap.conf に書き出され、これを参考にして krustlet は立ち上がる。 $ export KUBECONFIG=~/.krustlet/config/kubeconfig $ krustlet-wasi --node-ip 192.168.99.100 --cert-file=~/.krustlet/config/krustlet.crt --private-key-file=~/.krustlet/config/krustlet.key --bootstrap-file=~/.krustlet/config/bootstrap.conf BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve silicon-tls 最後に、Krustletが Kubernetes マスターと通信した上で、 CSR (certificate service request)を手動で承認する必要があります。別ターミナルで指示通り クラスタ ーの CSR 承認しましょう $ kubectl certificate approve silicon-tls certificatesigningrequest.certificates.k8s.io/silicon-tls approved するとついにkrustletが稼動します! BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve silicon-tls BOOTSTRAP: received TLS certificate approval: continuing [2020-12-14T14:51:51Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:51:56Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:52:01Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:52:06Z ERROR wasi_provider::states::registered] Cannot run kube-proxy こちらエラー出ているのは一旦無視して大丈夫です。ちなみに、krustlet-wasi実行時にはほぼログメッセージが出ないので、気になる方はRustのロギング 環境変数 を設定して RUST_LOG=info で実行してみて下さい。 最後確認のため、ノードとして登録されているか確認しましょう。 $ kubectl get nodes -o=wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME minikube Ready master 29h v1.14.0 10.0.2.15 <none> Buildroot 2018.05 4.15.0 docker://18.6.2 silicon Ready <none> 29h 0.5.0 192.168.99.100 <none> <unknown> <unknown> mvp いいですね、 silicon という別ノードが立ち上がっています。少し詳細を見てみましょう。 $ kubectl describe node silicon Name: silicon Roles: <none> Labels: beta.kubernetes.io/arch=wasm32-wasi beta.kubernetes.io/os=linux kubernetes.io/arch=wasm32-wasi kubernetes.io/hostname=silicon kubernetes.io/os=linux type=krustlet アーキテクチャ としても wasm32-wasi 対応されている事が分かります。 wasmを kubernetes 上で実行 色々と整備できたので、待ちに待ったデプロイです。krustletのデモをそのまま使ってみました。実際に自分でも コンパイル してみたんですが、OCIコンテナにパッケージする必要もあるため省略します。サンプルの Yaml ファイルはkrustletのレポジトリにあります: https://github.com/deislabs/krustlet/tree/master/demos/wasi/simpleserver $ kubectl apply -f simpleserver.yaml YAML ファイルの中には Tolerations がありますが、こちらはある意味 Kubernetes のハック。 wasm32-wasi の アノテーション があるPodに関しては、 NoSchedule, NoExecute ということで、普通のkubeletは実行しないようにする指示が入っています。この Tolerantions が入っていないとコンテナでも無いWasmをコンテナ扱いしてしまい、実行が失敗してしまいます。 Kubernetes はコンテナの オーケストレーション するものなので、今回のようにwasmを無理やりコンテナに突っ込んだものを扱う仕様は恐らく想定されないはずですから、krustletの開発者はちょっとした工夫をしたんですね。 ここで、先程スケジュールしたPodのログを取りに行きましょう。 $ kubectl logs simpleserver Hello World! Hello World! Hello World! Hello World! Hello World! という訳で、wasmモジュールの出力が kubernetes のログとして見れました!ただの Hello World ですが、これが実現出来ている背景には Kubernetes の素晴らしい設計と、多大なる努力があることを感じた。 waSCCでウェブサーバー動かそう WASIではネットワーキング対応出来ていませんが、折角なのでwaSCCを試してみましょう。krustlet-wasccを同じオプションで実行して: $ ~/Downloads/krustlet-v0.5.0/krustlet-wascc --node-ip 192.168.99.1 --cert-file=~/.krustlet/config/krustlet.crt --private-key-file=~/.krustlet/config/krustlet.key --bootstrap-file=~/.krustlet/config/bootstrap.conf 別ターミナルでサンプルの yaml ファイルをデプロイ:  $ kubectl apply -f uppercase-wascc.yaml pod/uppercase created $ kubectl get pods NAME READY STATUS RESTARTS AGE uppercase 0/1 ImagePull 0 2s こちらのステータスが ImagePull から Running になったらリク エス トを送ってみましょう: $ curl localhost:8080/?hello-world {"original":"hello-world","uppercased":"HELLO-WORLD"}a そしてwasmのログの方を確認すると: $ kubectl logs uppercase 11:47:47 [ INFO] [MDFC3LZ2YAGPTI452SEKDZ3D5D6QJD62R5KDPPJVPDL5B6DFFQKM3B62] Query String: hello-world サンプルの ソースコード を見てみましょう。 fn uppercase(r: codec::http::Request) -> HandlerResult<codec::http::Response> { info!("Query String: {}", r.query_string); let upper = UppercaseResponse { original: r.query_string.to_string(), uppercased: r.query_string.to_ascii_uppercase(), }; Ok(codec::http::Response::json(upper, 200, "OK")) } ウェブアプリケーション になってるじゃん!サンプルの ソースコード は本当に簡単なんですが、これがWebAssemblyになって Kubernetes 上で動いているって、ワクワクしますよね。 2. But the very next day, you gave it away Krustletの実用性 最初は楽しく Krustlet とはお付き合いさせて頂きましたが、やはり highly experimental で実は動かすだけで数日かかりました。相当細かい Kubernetes 内部の仕様も勉強した上で挑むべきだったと反省はしていますが、セキュリティ周り含めて幅広い知見が無いと躓いた瞬間に時間が溶けます。 日々の業務で Kubernetes 使っているので、そちらで利用しようとした結果、色々と苦労して諦めてREADMEどおりにminikubeで立ち上げました(笑) READMEには本番環境では使うなと記載もありますし、WASIの規格が決まっていない時点で本番運用は論外だと思いますが、サーバアプリケーションをWasmに コンパイル してそれを既存のインフラ上で実行させる世界感が見えてきましたね。。。 WebAssembly と Runtime 達 WASI 規格は未完成ですし現時点でもネットワーク通信出来ないし、実用性のあるアプリケーション作ることは出来ない。これからの API の安定やエコシステムの拡大に期待すべきでしょう。今回はネットワーキングサポートの有る waSCC ランタイムを利用して簡単な ウェブアプリケーション を立ち上げたが、こちらもまだデータベースへアクセスも出来ない。 時間注ぎ込んだ結果すぐには実らないかもしれない。クリスマスは、おとなしくコード書くべきだったのかもしれない。 3. Maybe next year, I’ll give it to someone special それでも勉強し続ける アプリやウェブ上のプロダクトを作る時って、ユーザさんには「アプリが使いやすい」とか「サービスすごく便利」と言われますが、「インフラすごく格好いい?」なんて褒めてもらえないですよね。「今日もホームページ動いている、拍手!」なんて言われたら逆に期待値の低さを感じてしまう。インフラのユーザ インパク トはどちらかというと減点方式であり、改善に向けた努力の結果がどうしてもユーザには理解してもらえない部分はある。 インフラの仕事はUI作るのと違い因果関係が見えにくく、長い時間軸で価値発揮する仕事だと思っている。ボタン押してすぐ結果が出ないので、ある程度自分の行動がユーザの為になっている事を自信持って周りとコミュニケーション出来ないと、周りの信頼を失いやすい。デプロイ速度を上げたり、セキュリティ強化したり一般人には通じないが非常に大切な縁の下の力持であることは覚えておきたい。 そんなインフラ開発者に似ているのが日本の製造業に関わっているメーカーや町工場さん。 自作PC 作りながら「このPCのケース、キズ無くていいね!」なんて思わないですよね。「何かネジが入らないんだけど!」という不具合が記憶に残りやすいが、このようなミスを極力下げるために日々プロセス改善を続け、加工技術を磨いてらっしゃる製造業を支えるために弊社ではアプリケーション開発をしております。
こんにちは! @ryokotmng です🙋🏻‍♀️ 本記事は、 キャディ Advent Calendar 2020 – Qiita の13日目の記事です。昨日の記事は @kuwana-kb さんの、 DDD のパターンを Rust で表現する ~ Entity 編 ~ でした。 今日は主に、rstestというcrateを使い、Rustでパラメーター化テストをするときに便利な書き方について説明します。 パラメーター化テストとは パラメーター化テストとは、検証対象のソースコードが想定した挙動になることを保証できるような入力パラメーターの値と結果を設定し、同じロジックのテストをそれぞれのパラメーターに対して行うテストのことです。通常、関数やメソッドに対して行われます。 ある関数をテストするとき、定義域 (インプットの取り得る値の範囲) も値域 (アウトプットが取り得る値の範囲) も非常に限定されているという場合もありますが、抽象度が高いオブジェクトを扱うソースコードになってくると、定義域も値域も広範囲に渡り、複雑な条件によってアウトプットが変化するようなものが増えてきます。 パラメーター化テストは境界テストに適しているため、そのようなある程度複雑化したアプリケーションにおいて、最もよく見られるタイプのテストのひとつだと思います。 一方で、パラメーター化テストはプロパティベース・テストと違い、網羅性を保証するものではありません。定義域と値域の特徴を表す境界値を明記することでソースコードの挙動の特徴を表すものであり、そのためエンジニアにとってドキュメントのような役割も果たします。 rstestというcrateについて crates.ioの説明を拝借すると、 rstest は、フィクスチャベースのテストフレームワークで、フィクスチャとテーブルベースのテストを書くためのツールです。 フィクスチャとは、テストを実行、成功させるために必要な状態や前提条件の集合のことを言います。 また、テーブルベースのテストとは、一般的にテーブル駆動テストと呼ばれるもので、テストの入力と期待値をテーブルの行に記載し、テーブルを走査しながら実行していくテストのことです。 Goの公式ドキュメントで言及されていることから、Goコミュニティではよく使われますよね。 rstest の使い方については、主に こちらのドキュメント を参考に書きました。 このドキュメントは非常にわかりやすく書かれているため、このセクションはドキュメントの和訳のような内容になっている箇所も多くあります。 個人的にはこのcrateはかなり使いやすく、テストの可読性を高めコード量を減らすことに貢献すると思っているので、テストに悩む人の目に止まったら良いなと思って取り上げた次第です。 2020年12月10日時点の情報として、crates.ioでのダウンロード数は合計11万程度、GithubのStarは145です。 rstest のバージョンは0.6.4を用います。 (他の大多数のcrate同様、rstestもバージョン1に至っていません😇) パラメーター化テスト 既存のフィボナッチ数を返す関数に対してテストを書くと仮定します。 まず、Rustでテストを書く際、The Bookにもあるように、このような書き方をするのが一般的かと思います。 普通のテスト #[test] fn fibonacci_test() { assert_eq!(fibonacci(0), 0); assert_eq!(fibonacci(1), 1); assert_eq!(fibonacci(2), 1); assert_eq!(fibonacci(3), 2); assert_eq!(fibonacci(4), 3); assert_ne!(fibonacci(0), 1); // not equalのテスト } これが、 rstest を使うと、このように書くことができます。 rstestを使ったテスト use rstest::rstest; #[rstest(input, expected, case(0, 0), case(1, 1), case(2, 1), case(3, 2), case(4, 3), #[should_panic] case(0, 1), // not equalのテスト )] fn fibonacci_test(input: u32, expected: u32) { assert_eq!(expected, fibonacci(input)); } 上記のソースコードは、 input と expected という変数を定義し、 case の引数をそれぞれ代入してテストを実行してくれます。 この程度の簡単なロジックだとありがたみがわかりづらいかもしれませんが、例えば、もし input と expected の値が複数の型を取り得るものだった場合などを考えると、 fibonacchi_test 関数の中で様々な値の初期化を行う必要があり、あっという間にテストコードが読みづらくなることが想像できます。後ほど、これよりはやや複雑なコードのテストを例示します。 テーブル形式でパラメーターを書くと、インプットとインプットの値がひと目でわかるため、「どのようなテストパターンを行っていて、どのようなテストパターンをやっていないか」がテスト内のロジックから切り離されて記述され、理解しやすいですね。 上記のテストを実行すると、下記の通り、指定した case の数だけテストが実行されます。 running 5 tests test fibonacci_test::case_1 ... ok test fibonacci_test::case_2 ... ok test fibonacci_test::case_3 ... ok test fibonacci_test::case_4 ... ok test fibonacci_test::case_5 ... ok test fibonacci_test::case_6 ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 使い方はとても簡単ですね。 このパラメータ化の機能を使うことで、テストケースの意図がわかりやすくなります。 小ネタですが、 trace アトリビュートを追加することで、テストが失敗した際にインプットの変数を全て表示してくれます。 #[rstest( number, name, tuple, case(42, "FortyTwo", ("minus twelve", -12)), case(24, "TwentyFour", ("minus twentyfour", -24)) ::trace // このアトリビュートを追加すると、トレースできます )] fn should_fail(number: u32, name: &str, tuple: (&str, i32)) { assert!(false); // テストが失敗した時のみ標準出力に値が表示されます } 上記のテストはどちらも失敗し、出力はこのようになります。 running 2 tests test should_fail::case_1 ... FAILED test should_fail::case_2 ... FAILED failures: ---- should_fail::case_1 stdout ---- ------------ TEST ARGUMENTS ------------ number = 42 name = "FortyTwo" tuple = ("minus twelve", -12) -------------- TEST START -------------- thread 'should_fail::case_1' panicked at 'assertion failed: false', src/main.rs:64:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. ---- should_fail::case_2 stdout ---- ------------ TEST ARGUMENTS ------------ number = 24 name = "TwentyFour" tuple = ("minus twentyfour", -24) -------------- TEST START -------------- thread 'should_fail::case_2' panicked at 'assertion failed: false', src/main.rs:64:5 failures: should_fail::case_1 should_fail::case_2 test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out 境界値のテストの例 やや実践的な例でテストを書いてみます。例として、以下2つの型を作るファイルにテストを書きます。 1. コンストラクタで国番号と電話番号を受け取り、海外の場合は国際番号付きの電話番号を返してくれる PhoneNumber 型 2. ユーザーの連絡先を表す、 Contact 型。文字列をラップしており、日本国内のユーザーで電話番号を持つ場合は電話番号を、電話番号のないユーザーまたは国外のユーザーの場合はメールアドレスを返す テストは下記のようになります。オブジェクトの関数が実装されていませんので、このコードを動かすことはできません。書き方の参考に読んでみてください。 // 電話番号が国番号を付与された状態で生成されることのテスト #[rstest(country_code, number, expected, // パラメーターテーブルには型を用いたオブジェクトを入れることもできます case(Country::Japan, "08031704919", "08031704919"), case(Country::US, "99999999", "+199999999"), )] fn phone_number_test(country_code: String, number: &str, expected: &str) { let phone_number = PhoneNumber::new(country_code, number).to_str(); assert_eq!(phone_number, expected); } // コンタクト情報が正しく生成されることのテスト // 1. 日本の場合は電話番号を返す // 2. 電話番号の登録がない場合、もしくは海外の場合はメールアドレスを返す #[rstest(country_code, number, email, expected, case(Country::Japan, "08031704919", "jp@gmail.com", "08031704919"), case(Country::Japan, "", "test@gmail.com", "test@gmail.com"), case(Country::US, "+499999999", "us@gmail.com", "us@gmail.com"), )] fn primary_contact_test(country_code: String, number: &str, email: &str, expected: &str) { let phone_number = PhoneNumber::new(number); // primary_contactメソッドでContact型の値を返すと想定しています let contact_str = Contact::new(country_code, phone_number, email).primary_contact().to_str(); assert_eq!(contact_str, expected); } マトリックス・テスト 複数の引数を掛け合わせて検証するマトリクス・テストをするときは、このように書きます。 #[rstest( first => [1, 3, 5], second => [2, 4, 6] )] fn equal_test(first: u32, second: u32) { assert_eq!(first, second) } 上記のテストは、 assert_eq! で引数 first と second の値が同じであるかどうかを検証しています。 rstest を使って first と second という変数を指定していて、テストを実行すると first と second それぞれの配列から一つずつ値を取り出し、組み合わせてくれます。 first と second は奇数と偶数の配列であるため、それぞれの要素に等しいものはなく、テストは全て失敗します。組み合わせなので、テストは9回実行されます。 running 9 tests equal_test::first_2::second_3 ... FAILED equal_test::first_1::second_3 ... FAILED equal_test::first_3::second_2 ... FAILED equal_test::first_1::second_1 ... FAILED equal_test::first_2::second_2 ... FAILED equal_test::first_3::second_1 ... FAILED equal_test::first_3::second_3 ... FAILED equal_test::first_2::second_1 ... FAILED equal_test::first_1::second_2 ... FAILED フィクスチャを作る #[fixture] をつけることによって、フィクスチャを作成し、複数のテストで使い回すことができます。 依存関係のあるオブジェクトをたくさん生成しないとテストできない場合などに便利です。 use rstest::*; #[fixture] pub fn fixture() -> u32 { 42 } #[rstest] fn should_success(fixture: u32) { assert_eq!(fixture, 42); } #[rstest] fn should_fail(fixture: u32) { assert_ne!(fixture, 42); } フィクスチャにデフォルト値を持たせることもできます。 #[fixture(name="Alice", age=22)] fn user(name: &str, age: u8) -> User { User::new(name, age) } #[rstest] fn is_alice(user: User) { assert_eq!(user.name(), "Alice") } #[rstest] fn is_22(user: User) { assert_eq!(user.age(), 22) } #[rstest(user("Bob"))] fn is_bob(user: User) { assert_eq!(user.name(), "Bob") } #[rstest(user("", 42))] fn is_42(user: User) { assert_eq!(user.age(), 42) } Ruby on RailsでのFactoryBotに近い感じに見えますが、パラメーターテーブル内ではデフォルト値が使えないなど、使い勝手に関してはもう少しかなと思うところもあります。そこまでの柔軟性を求めるのも酷な気はするので全然いいのですが。 test_reuseクレートを併用し、テストのインプット/アウトプット値を複数のテストで使い回すこともできます。この機能については使い道がまだよくわかっていません。 use rstest::rstest; use rstest_reuse::{self, *}; #[template] #[rstest(a, b, case(2, 2), case(4/2, 2), ) ] // ここではシグネチャだけを定義 fn two_simple_cases(a: u32, b: u32) {} // テンプレートに指定したcaseを用いてテストが実行される #[apply(two_simple_cases)] fn it_works(a: u32, b: u32) { assert!(a == b); } rstest に関する説明は以上になります。 どうでしょうか、結構便利そうだなと思っていただけましたか? Rustにおけるテストの難しさ ここまでは課題の解決というよりはややテクニカルな話をしましたが、最後にRustでテストを書くことの難しさについて、特に感じていることを書いてみます。 まず前提として、Rustにおいて、単体テストと結合テストは記載される場所が異なり、単体テストはテスト対象モジュールの中で記述されるか、テスト用のモジュールを作成して記載します。 そのため、単体テストにおいて他crateに依存するオブジェクトを必要とする際、テストと関係の薄い依存データまでテスト内で作成しなければなりません。多少なら問題ありませんが、アプリケーションが大きくなり依存関係が複雑になるほど、テストを書くことが苦痛になってきます。 このフィクスチャ問題を克服するため、私のチームでは下記3つの方法を検討しています。 テスト用のフィクスチャを作成する関数を作る テストで使うデータを作成するための関数を作り、依存先のcrateでテストを行う場合もその依存元にあるフィクスチャ作成用の関数を用いるという方法です。 この方法のデメリットとしては、他のcrateから参照できる関数を作ってしまった場合、テスト以外においてもその関数を使用できてしまうことです。プロダクションコードに突然大量のデータを作成するメソッドを使用するコードが紛れ込んでしまうのは大きなリスクと言えます。 また、テストでしか使わないはずの関数もビルド対象となってしまうことで、ビルド時間が大きく伸びてしまいます。 これら2つのデメリットがかなり致命的なことから、この選択肢は検討しないことにしました。 複雑な構造体でも、文字列からパースして作る 先ほどご紹介したフィクスチャを作る手法を活用し、単体テスト内で使いまわせる擬似データを作ってしまうこともできますが、初期化の処理を延々と書くくらいなら、JSONや文字列からパースできるようにしてしまうという手もあります。 serde_json::from_valueを使えば、指定された型に向かって文字列をデシリアライズしてくれます。 この方法は後から読んでもデータ構造もわかりやすいですし、書き方も簡単ですが、複雑な構造体を使う場合にテストに不必要なデータを大量に書かなければならないという問題が残ります。 使用するオブジェクトが文字列からパースできる状態になっていれば特別な準備が必要ないため、現状、この方法で乗り切っている箇所もあります。 フェイクデータを作成してくれるテスト用マクロを準備する Ruby on Railsでよく使用されるFactoryBotのように、その場限りで使えるフェイクデータを作ってくれるテスト用のマクロを準備するというやり方も考えられます。rstestも、内部的にはマクロを使って実装されています。 この方法は、準備がある程度大規模になることが予想されますが、作ることができれば無駄なコードを書く必要もなくなり、テストに本当に必要な情報だけを記載できるという、非常に便利なものになりそうだと考えられます。 キャディでは大事なロジックに漏れがないようテストカバレッジや書き方を意識してはいますが、満足に書けていると言える状態ではありません。 やはり、複雑なデータ構造であればあるほど結合テストに頼る形になり、部分的なコードの修正をカバーするテストを書くことが困難になってくるという問題があります。 おすすめのテストの書き方がある方、是非教えてください!! また、キャディでは一緒に素敵なプロダクトを開発してくれる仲間を探しています!ご興味をお持ちいただけた方は、是非 お話しに来てください 。お待ちしております🙌 長文お読みいただきありがとうございました😃 明日は和田さんによる、「Rust と nalgebra で MLP を実装した話」です。お楽しみに❣️ みなさま、良い年の瀬をお過ごしください〜🎄
こんにちは。CADDi でバックエンドエンジニアをしている @kuwana-kb です。 この記事は CADDi Advent Calendar 12日目の記事です。昨日は、山下さんによる GitOpsの概要と実践例 〜Kustomize + CircleCI編〜 でした! 本日は「DDD のパターンを Rust で表現する ~ Entity 編 ~」と題しまして、 Rust で DDD のパターンを表現してみたいと思います。 目次 [toc] はじめに DDDとは、 Domain-Driven Design(ドメイン駆動設計)の略です。アプリケーションの扱う業務領域に焦点をあてた設計手法であり、エリック・エヴァンスが提唱しました。詳細については、以下の記事で紹介していますので、そちらをご覧ください。 DDDのパターンをRustで表現する ~ Value Object編 ~ また、今回の記事を書く上で「ドメイン駆動設計入門」(著: 成瀬 允宣氏)という書籍を参考にさせていただきました。書籍のサンプルコードは C# でして、本記事ではそのコードを参考に Rust で DDD のパターンを表現しています。 なお、本記事に登場するコードは kuwana-kb/ddd-in-rust に格納しています。 DDD における Entity とは まず、Entity について整理しましょう。 Entity とは、一意なものを表現する概念です。人(Human)で例えてみましょう。 ここに二人の人がいます。 二人の名前はどちらも「山田太郎」さんです。では、この二人は全くの同一人物といえるでしょうか?現実世界では、同姓同名がありえます。つまり、二人は同じ名前でありながら、異なる一意な存在です。コード的な表現でいえば name という属性は同じでも、二人を区別できる必要が有ります。 これは、Entity は属性が同じであっても区別されることを示します。また、二人を区別するための識別子が必要となることも示しています。 また、人は時を経て状態が変わります。例えば、身長が伸びたり、体重が増えたりしますね。これは、ライフサイクルを通じて height や weight といった属性が変化することと同義です。つまり、Entity は可変であるといえるでしょう。 属性として登場した name, height, weight は、属性単位で見たときに値が同じであれば同一とみなせます。これらは Value Object として定義できるでしょう。一方で、Human はその構成要素である name, height, weight がすべて同一だとしても、同じ存在とは限りません。そして、ライフサイクルによって変化することがある。 これが Entity です。 実装パターンの紹介 まずはシンプルに実装してみる ここでは、 EC サイトのユーザーを例にとってみます。ユーザーの仕様は以下とします。 ユーザーには名前を設定できる 名前は 3 文字以上である必要がある Rust の型で表現すると以下のような実装になるでしょう。 use common::MyError; // User // 現時点では、可変性と同一性を持たない。 #[derive(Clone, Debug)] pub struct User { name: String, } impl User { // Userのコンストラクタ pub fn new(name: &str) -> Result { if name.chars().count() とてもシンプルですね。 User 型は属性として name を持っています。また、コンストラクタとして new() 関数を持っています。 しかし、現在の実装だと後から名前を変えたくても変える方法がありません。 可変性を与える 次に User 型に対して、 Entity のもつ特性である可変性を与えます。具体的には、 User.name 属性を変更できるようにしてみましょう。 use common::MyError; // User pub struct User { name: String, } impl User { // User のコンストラクタ pub fn new(name: &str) -> Result { let mut user = Self { name: Default::default(), }; user.change_name(name)?; Ok(user) } // ふるまいを通じて属性を変更する // 変更ロジックはメソッド内に閉じ込めている pub fn change_name(&mut self, name: &str) -> Result { if name.chars().count() User 型に対して名前を変更できる change_name() メソッドを追加しました。これで名前を変更できるようになりましたね。 さて、 change_name() メソッドには、冒頭に述べた「名前は 3 文字以上である必要がある」という仕様が含まれています。この User.name の制約は名前変更時だけでなく、 User を作成するときにも満たす必要が有ります。したがって、 new() 関数の処理は、 User インスタンスを生成してから change_name() メソッドを通じて引数である name を注入しています。なお、 change_name() はバリデーションによって失敗する可能性がある(= Result 型を返す)ため、 new() の返り値も Result になっています。 この実装でも仕様は満たせているのですが、 名前の制約を満たすために毎回 change_name() を呼ぶのは面倒です。また、 new() の処理で user.change_name(name)? のような形で制約を満たすようにしていますが、フィールドが追加される度に user.change_***() といったメソッド呼び出しを追加する必要がでてきそうな点も気になります。 そこで、名前の持つ制約を型に閉じ込めてしまいましょう。具体的には、 Name 型を定義して「名前は 3 文字以上である必要がある」という仕様は Name 型が持つ形にします。 use common::MyError; // User pub struct User { // name 属性は、Name 型を持つ形に変更 name: Name, } impl User { // input の型も String 型 から Name 型に変更 pub fn new(name: Name) -> Self { Self { name: name } } // バリデーションのロジックは Name 型に移譲している pub fn change_name(&mut self, name: Name) { self.name = name; } } // ユーザー名 // // 制約として、3文字以上である必要がある // Name 型を新たに Value Object として定義した pub struct Name(String); impl Name { pub fn new(s: &str) -> Result { if s.chars().count() Name 型を新たに定義しました。 「名前は 3 文字以上である必要がある」という仕様は、 Name::new() に移譲されています。 Name 型のインスタンスが生成できた時点で、上記の仕様を満たした状態であるということが保証されます。 Name 型を定義したことで、 User 型にも変更を加えています。これまで User.name 属性や引数の型は String 型でしたが、 Name 型になっていますね。 Name 型のインスタンスを生成するタイミングでバリデーションが入るため、 User::new() と change_name() の返り値の型から Result 型 が消えています。 さて、今回はドメイン上の仕様をメソッド( change_name() )ではなく、型( Name 型)に移譲してみました。 // name は String 型 // どのような値が入っているかはパット見ではわからない pub struct User { name: String, } // name は Name 型 // Name 型特有の値であることがひと目でわかる // 型の定義に飛べばその制約も知ることができる pub struct User { name: Name, } 私としては、型で表現した方がオブジェクトの持つ特性がより伝わりやすいと感じます。 同一性を与える 次は、 User 型に対して Entity の特性である同一性を与えたいと思います。名前が一緒だからといって、同じユーザーとは限りません(少なくとも今回の例においては)。したがって、 User.name 属性が同じだったとしても、別々の User であると識別できるようにしたいと思います。 use derive_getters::Getters; use common::MyError; /// Userモデルに対して可変性と同一性を与えた #[derive(Clone, Debug, Getters)] pub struct User { id: UserId, name: Name, } impl User { pub fn new(id: UserId, name: Name) -> Self { Self { id, name } } // nameフィールドは可変性を持つ pub fn change_username(&mut self, name: Name) { self.name = name; } } さて、今回は User に対して id 属性を追加しました。 User.id 属性の型は UserId 型です。 簡単ですが、 UserId 型は以下のような文字列を受け取る型とします。 /// ユーザーID #[derive(Clone, Debug, PartialEq, Eq)] pub struct UserId(String); impl UserId { pub fn new(s: &str) -> Self { Self(s.to_string()) } } 次に User 型の同一性をふるまいとして実装します。同値関係をあらわすには、 Eq , PartialEq という Trait を利用します。コード的な表現でいうと x user_a のような形で、同一かどうか検証できるようになります。この時、 User が同一であるかは User.id のみを比較対象とします。 User.name は比較対象にしません。 // trait PartialEq は半同値関係をの性質を表す impl PartialEq for User { fn eq(&self, other: &Self) -> bool { // 今回の実装は、idの値が同一か検証するようにしている self.id == other.id } } // trait Eq は同値関係の性質を表す impl Eq for User {} これで、 User に対して同一性の特性を与えることができました。実装だけだとわかりづらいと思うので、簡単な使用例をみてましょう。 /// 名前 #[test] fn test_user_eq() { // User インスタンスを生成する let user_before = User::new(UserId::new("DummyId1"), Name::new("Hoge").unwrap()); // User インスタンスをコピーし, before と after で2つにする let mut user_after = user_before.clone(); // after のインスタンスの名前を変更する user_after.change_username(Name::new("Fuga").unwrap()); // User が同一であるかを検証する // User の名前を変更しても同一性は同じままである assert_eq!(user_before, user_after); // Ok } 名前を変更する前と後で User が同一であるかを検証しました。User は可変性と同一性の性質をもったオブジェクトであり、 Entity の性質を満たしています。 まとめ 今回は、Rust で Entity をどのように表現できるか、についてご紹介しました。みなさんに Rust の魅力が少しでも伝われば幸いです。 CADDiでは「モノづくり産業のポテンシャルを解放する」ために仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。 少しでも興味を持って頂けましたら、リニューアルされたばかりの 採用サイト をご覧ください。 また、カジュアル面談も行っていますので、実際にエンジニアに会ってみたい方は こちら から、 どうぞ宜しくお願いいたします。
こんにちは。テクノロジー本部バックエンド開発グループの山下です。 この記事は キャディ Advent Calendar 2020 の11日目です。 前日は大原さんの 図面を管理するために図面版 figma を開発している話 について でした。 今回は 以前のKustomizeの記事 に続き、 「GitOps概要と実践例 〜Kustomize + CircleCI 編〜」と題して、 GitOpsの概要を説明した上で、KustomizeとCirlceCIを利用したk8s上でのGitOpsの実践例について 書いていこうと思います。 [toc] GitOpsとは 概要 WeaveWorksの Guide To GitOps より引用 GitOpsはきちんと説明するだけでもかなりの分量になるので、かなり思い切って要約すると 「Kubernetesクラスターの構成をコード化してGitで管理し、その情報だけを正として環境を展開するもの」です。 え、それもうやってるよ? Terraformでk8sクラスターなどインフラ構築して、イメージをkubectlとかでデプロイしているよ? という方も多いかと思います。 ただ、ここで重要なのは、コード化されたもの 「だけ」 を正とする、ということです。 つまり、コンテナイメージなど動的に生成されうるものなども含めて全てを静的に管理する、ということです。 この前提で継続的デリバリーを構築しようとすると面倒な事実に気付きます。 例えば、リリース対象のアプリケーションをビルドすると当たり前ですがイメージが新しく作られます。 ですが、そのイメージをそのままクラスターにデプロイすると、コードだけを正とするGitOpsの概念から外れます。 何故なら、そのイメージを一意に特定する情報がインフラ側のコードにはないため、同じ状態を再現できないためです。 正しくGitOpsを行おうとすると、コンテナをプッシュするのではなく、 何らかの手段で、ビルドしたイメージの情報をインフラコードに反映させなければなりません。 そうすると、反映されたインフラコードに合わせて実際の環境が収束していきます。 こうすることで、常に定義した内容と現実が一致するようになるのです。 GitOpsのメリット ここまで聞くと、そんな面倒なことしなくても、ビルドしたイメージをデプロイしちゃえば良いじゃない、と思われるかと思います。 ですが、GitOpsにはいくつかのメリットがあり、この面倒なことをする理由になります。 そのメリットの中で、キャディで採用した際に重視した点は大きく三つです。 信頼性と復元力 一つ目が、信頼性と復元力、つまり 必ずある時点での状態に戻すことが出来る ことです。 GitOpsでは必ずコードで定義されている状態に収束します。 つまりアプリケーションのイメージをタグなどを用いて定義してあれば、 必ずクラスターで展開されるイメージ全てが、その時々で一意に決まります。 加えて、キャディでは、Helmなどを利用して各種ミドルウェアの設定などを行っていますが、 その時のバージョンや設定なども合わせて管理されているため、 リリースした当時、バグが発生した当時の状況を確実に再現することができます。 (DBなどState部分に関しては出来ませんが) これによって、バグ発生当時の状況の再現やロールバックなどの動作が容易になります。 関心の分離による生産性の向上 二つ目が、 CIとCDの分離による関心の分離と、それに伴う生産性の向上 です。 キャディでは稼働中のプロダクトが4つ存在しますが、 内部的には、さらに細かいサービスに別れて存在しています。 このサービス毎に、どうやってデプロイしよう、と考えたり 実際にCDの処理を書いたりするのは非常に面倒ですし、 デプロイ先のクラスターのことを理解していないといけないのは開発者にとって負担が大きいです。 しかし、GitOpsでは、イメージをビルドさえ出来れば、 その情報(タグなど)をインフラ側に反映させるだけでよく、 その書き換え処理の共通化も容易です。 (キャディではOrbを使ってCD処理などを共通化しています。以前書いたOrbの記事は こちら です) そのためGitOpsでは、 アプリケーション開発チームはコードを書いてイメージが出来上がるまでに集中すればよく、 後のインフラ構成やインフラエンジニアの稼働状況などを気にせず、 開発を進めることが出来るようになりDX(開発者体験)が高まります。 もし、これが従来のE2Eのパイプラインによるデプロイ形式だと、 実際にアプリケーション側のCI/CDパイプラインでデプロイが完了するまで気にしなくてはいけませんが、 GitOpsであればイメージのタグが書き換わりさえすれば、そのあとはデプロイが何らかの理由で失敗しても 既にコードは変更してあるので、再度デプロイをしたり原因調査をインフラ担当者が行えばよくなります。 これはアプリケーション開発だとPubSubモデルに近く、 実際に処理が完了するまでをPublisher側が気にしなくてもよくなるのに近いです。 高い効率性とセキュリティを保つ運用体制 最後に三つ目が、 高い効率性とセキュリティを保つ運用体制が構築できること です。 これはGitOpsでは、全ての変更やリリースがGitで管理され、その変更に応じて環境が変わるので、 誰が、何を変更したことによって、このリリースを行われたかを確実に分かるためです。 これによって、問題の発覚から原因の特定・対策までをスムーズに行うことが出来ますし、 許可されていないユーザーからの変更も防ぐことが出来ます。 更に、CIとCDを分離したことで、CI側にクラスターを変更する権限を渡すことなくリリースが行えるため、 セキュリティ的にも安全性が高まります。 GitOpsをより詳しく知るには 以上、大胆にGitOpsの定義を削って簡単に説明しましたが、 GitOpsには他にも様々な要素があり学びも大きいです。 詳細を知りたい方は、是非、この考えを提唱したWeaveWorksの 公式資料 をご覧ください。 キャディでの実践例 前置きが長くなりましたが、ここから実際にGitOpsを実現する方法を具体的に書いていきます。 前置きで色々書いていたり、公式資料にも小難しいこと書いているな、と思われるかもしれませんが、 GitOpsを実現する際に考えることは実は大きく三つだけです。 どのような形式でイメージの情報を管理するか(≒構成管理) どのタイミングでイメージの情報を書き換えるか(≒ブランチ戦略) どうやってイメージの情報を書き換えるか(≒CDの実装) どのような形式でイメージの情報を管理するか ここでいうイメージの情報とはImageのtagやdigestなどで、 それをどう保存して管理するか、つまり構成管理の方法を最初に決める必要があります。 キャディではKustomizeを使って構成管理を行っています。 詳しくは こちらの記事 で書いているので詳細は省略しますが、 基本はk8sのPodを定義するbaseのファイルと具体的なタグや設定値などが書いてあるファイルでoverlay つまり、上書きする、という方式を使っています。 具体的なサンプルで説明すると baseのファイル(path: base/services/**/deployment.yaml)が下記だとすると apiVersion: apps/v1 kind: Deployment metadata: name: sample-app spec: replicas: 1 selector: matchLabels: app: sample-app template: metadata: labels: app: sample-app spec: containers: - name: sample-app image: sample-app-image overlayするファイル(path: overlays/${env}/kutomization.yaml)が下記になります。 apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base images: - name: sample-app newName: gcr.io/bucket-name/image-name newTag: 91c8cee4b05a0ab1642d63198969fac9df5f62ae この二つを元にkustomize buildすると下記のようなmanifestが生成されます。 apiVersion: apps/v1 kind: Deployment metadata: name: sample-app spec: replicas: 1 selector: matchLabels: app: sample-app template: metadata: labels: app: sample-app spec: containers: - name: sample-app image: gcr.io/bucket-name/image-name:91c8cee4b05a0ab1642d63198969fac9df5f62ae つまり、上記のbaseファイルでImage名だけで指定していたものを overlay側のkustomization.yamlでtagを指定して上書きすることで、 その時点でのイメージ情報を確定できます。 overlayのファイルは各環境毎に設定するので環境毎に設定することができます。 (詳細なディレクトリー構成は こちら ) どのタイミングでイメージの情報を書き換えるか CIを定義している各サービスのレポジトリーから インフラのレポジトリーで定義しているイメージ情報を変更する訳ですが、 それをどのタイミングにやるのか、を決める必要があります。 これはイコール、ブランチ戦略を決めることでもあります。 よく使われているパターンだとGitHubフローとGitフローがありますが、 どちらの場合でも下記の二つのアクションをどのブランチやタグに結びつけるか、 が変わるだけになります。 いつイメージをビルドするか いつ、そのイメージ情報をインフラコード側に反映するか 対応表 この結びつきをGitHubフローのバージョンで対応表にすると下記のようになります。 Appブランチ イメージビルド インフラコード変更 デプロイされる環境 feature/* ビルドなし 変更なし なし master ビルド developブランチを変更 Dev環境 masterブランチでtag打ち(vx.y.z) ビルドなし masterブランチを変更 Stg環境 深掘って説明すると、 最初の、featureブランチでは開発しているだけなので特にインフラの変更がないです。 次に、masterブランチでは、実際にbuildが走り、インフラのdevelopが変更されDev環境にデプロイされます。 最後は、GitHubのリリース機能を利用して、masterブランチにtagを打つことで処理が走ります。 tagが打たれたタイミングで、インフラのmasterブランチが変更され、Stg環境にデプロイされます。 イメージビルドなし、が気になるかと思いますが、masterブランチでのbuild時点でイメージタグをcommit hashにしているため、tagを打たれたとしても、あくまで、その時点のhash値を使ってイメージを指定すれば良いのでビルドの必要がないのです。 どうやってイメージの情報を書き換えるか 上記でいつ、どのように保存するかが決まりました。 最後にどのように書き換えるのか、ということですが、 こちらもKustomizeの機能を使って書き換えます。 具体的に利用するのは、KustomizeのEdit機能です。 ( 以前の記事の参考箇所 ) この機能で各アプリケーションサービスのCI後に、 インフラコードにある overlays/${env}/kustomization.yamlを対象の環境分だけ書き換えます (対象の環境は、developブランチであればdevだけ、masterブランチであれば、stgとprod) コマンドとしては下記のようになります。 $ kustomize edit set image image_name:tag_name ただ、このコマンドだけだとイメージがつきにくいと思うので、 キャディでこの書き換え処理を共通化しているOrbの設定を一部抜粋・変更して掲載すると description: kustomization.yamlのimageのtagを変更する処理です parameters: fingerprint: type: string gcr_host_name: type: string default: gcr.io github_group: type: string default: caddijp infra_repo_name: type: string project_name: type: string default: ${CIRCLE_PROJECT_REPONAME} description: the name of the project (usually the same as the repo and image name) steps: - add_ssh_keys: fingerprints: - << parameters.fingerprint >> - run: name: Avoid hosts unknown for github command: echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - run: name: git clone infra code in specific branch decided by CIRCLE_BRANCH of CircleCI environment variables command: | git clone -b ${INFRA_BRANCH} git@github.com:<< parameters.github_group >>/<< parameters.infra_repo_name >>.git - run: name: rewrite image tag for release command: | if [[ ${INFRA_BRANCH} = develop ]]; then TARGET_ENVS=("dev") IMAGE_TAG="dev-${CIRCLE_SHA1}" elif [[ ${INFRA_BRANCH} = master ]]; then TARGET_ENVS=("stg" "prod") IMAGE_TAG=${CIRCLE_SHA1} else echo "This step failed because branch name is not suitable" echo "We have to set branch name from master, feature/*" exit 1 fi for ENV in ${TARGET_ENVS[@]} do cd ~/infra/<< parameters.infra_repo_name >>/k8s/overlays/${ENV} kustomize edit set image << parameters.project_name >>=<< parameters.gcr_host_name >>/${GOOGLE_PROJECT_ID}/<< parameters.project_name >>:${IMAGE_TAG} done 下記のStep毎に順を追って説明します。 1. SSH keyの追加 2. Hostの追加 3. インフラコードのClone 4. 実際の書き換え処理 SSH Keyの追加 アプリケーション側のCIコンテナ上からインフラコードをCloneする際に必要です。 Hostの追加 CircleCIで対象のレポジトリー以外のレポジトリーからコードを取得する際に必要な設定になります。 インフラコードのClone こちらは、書き換えたいインフラレポジトリーのブランチを ブランチ戦略に合わせて選択してCloneします。 実際の書き換え処理 キャディではインフラレポジトリーは release, master, develop, feature/* でブランチを運用しています。 そのブランチのコードを先ほどのブランチ戦略に対応表に合わせて変更しているのがこの部分になります。 変数の ${INFRA_BRANCH} が対象となるブランチですが、これをブランチ戦略に合わせて処理しているOrbのコマンドを一部変更・抜粋したものが下記になります。 description: ビルド対象のブランチを元に対象となるinfraコードのブランチを設定します steps: - run: name: Set infra branch name with CIRCLE_BRANCH in CircleCI environment variables command: | if [[ ${CIRCLE_TAG} =~ ^v ]]; then INFRA_BRANCH="master" elif [[ ${CIRCLE_BRANCH} = master ]]; then INFRA_BRANCH="develop" elif [[ ${CIRCLE_BRANCH} =~ ^(feature)/ ]]; then echo "In build of feature branch, this step is skipped" exit 0 else echo "This step failed because branch name is not suitable" echo "We have to set branch name from master and feature/*" exit 1 fi echo "${INFRA_BRANCH}" echo "export INFRA_BRANCH=${INFRA_BRANCH}" >> $BASH_ENV ブランチが決まれば、あとは対象のkustomizaton.yamlを書き換えるだけです。 developブランチであれば、 overlays/dev/kustomization.yaml を masterブランチであれば、 overlays/stg/kustomization.yaml と overlays/prod/kustomization.yaml を書き換えます。 stgとprodを同じタイミングで書き換えるのは、stgからprodへの展開する際の手順を出来るだけ最小限にするためです。 (releaseブランチにmasterブランチをマージするだけで本番にリリース出来るようにすることで誤操作などが入らないようにしている) 応用編: GitOpsのPush型とPull型 以上で、キャディで実践しているGitOpsの実践例の紹介は終了となりますが、 ここまでで、GitOpsを理解されている方だと、何か違和感があるな、と思われる方もいらっしゃるかもしれません。 それは、実は、イメージ情報を変更する際や、インフラコードの変更をクラスターに反映する際の方法がPush型になっているからです。 最初のメリットのところで、GitOpsがPubSubのようなものだと書きましたが、 PubSubにPush型とPull型があるように、実はGitOpsにもPush型とPull型があります。 そして、PubSubがそうであるように、GitOpsに関しても同様にPull型の方が理想的だと言われています。 具体的には、ImageRegistryの変更を検知して、インフラコードをConfig Updaterが変更、 (現在ですとCI側でPushして変更していますが、PubSubでいうSubscriberが書き換える方がより適切に分離されることになる) その変更内容をk8s側でwatchしておいて(pubsub的で例えるならsubscribeして)、 インフラコードに合わせて実態を収束させていく、というのが理想的な形になります。 WeaveWorksの Guide To GitOps より引用 ただ、PubSubでPull型で実装するのに、少し手間がかかるのと同様に、GitOpsでもPull型で実装するのは更に追加の対応が必要になります。 そのため、どれぐらいの工数を自分達のチームはかけることが出来るかに応じて、方法を選択してみるのも良いかもしれません。 Pull型で実装する場合は、一から実装するのは工数がかかりすぎるので、 GitOps系のCDツールとして最有力で開発が活発に続いている ArgoCD や GitOpsを最初に提唱したWeaveWorksが開発している Flux(ver2) などを試してみるのも良いかもしれません。 (因みに弊社の飯迫さんがArgo RolloutsでBlueGreenの実装を行った話を 記事 にされていますので、ご興味のある方はどうぞご覧ください) 最後に 長文になりましたが、KustomizeとCircleCI Orbのみで シンプルにGitOpsの考えを実現する方法をご紹介してみました。 応用編でも少し書きましたが、GitOpsはその考えが広まる中で、 それに関連する様々なツールやサービスが出ていますが、 間違いなく、これがデファクトスタンダードだ!と言えるツールやサービスがあるわけではありません。 その中で、今回の方法ですと、依存するものを最小限にしながらGitOpsの考えを取り入れることができ、結果的に学習コストも下がります。 加えて、最後に取り上げたArgoCDも Kustomize対応 をしていますし、 Fluxも、 Kustomizeを使ったサンプル もあったりと、 今後、次の一手を打つ際にも柔軟さを保てるのもメリットです。 GitOpsの考えは良いけど、実際に適応するのをどうすれば良いんだ!?と悩んでいた昔の私に、 こういう理由でこういう選択をしたけど、どう?っという情報があったら、 もっと効果的な判断が出来たのではないかと思い、こちらの記事を書いてみました。 これからGitOpsを実現しよう、という方のお力になれましたら幸いです。 CADDiでは「モノづくり産業のポテンシャルを解放する」ために仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。 少しでも興味を持って頂けましたら、リニューアルされたばかりの 採用サイト をご覧ください。 また、カジュアル面談も行っていますので、実際にエンジニアに会ってみたい方は こちら から、 どうぞ宜しくお願いいたします。
こんにちは。キャディでソフトウェアエンジニアをしている @sottar です。 この記事は キャディ Advent Calendar 2020 の10日目です。前日は @catupper による async/awaitで躓いて学んだ、「オレは雰囲気でRustをしている!」からの脱し方。 でした。 今日は私がチームで開発してる、図面を管理するためのツールについて紹介します。 図面とは そもそも図面とはなんでしょうか? 作りたい製品の素材やサイズといった仕様が書かれたものだということはみなさんご存知かと思います。 その図面を管理するためのアプリケーションというと一見簡単そうに思えるかもしれませんが、実際に弊社で行っているオペレーションに即して管理を行おうとすると、いくつか考慮しないといけない点があります。 取引先からpdfファイルとしてメールで送られてくることが多い 図面には id のような一意に決まるものがない(図番や品番などはあるが業界として統一されているわけではない) 手書きで文字が書かれていたり、スキャンされたPDFであることがある(テキストを機械的に読み取るのが困難) 途中で仕様(素材やサイズ)が変わることがある 電話でその仕様変更が伝えられることもある 社内では見積もりを行う際に図面に書かれている内容を社内の見積もりシステムに入力する必要があったり、サイズや素材などの変更があった場合にその変更を管理し社内メンバーに周知する必要などがあります。 よりスケーラブルな組織をつくるためにはこのオペレーションをできる限り自動化したいところですが、そのためにはこれらの課題を解決する必要があります。 弊社ではこれらを解決するアプリケーションの開発も行っており、今回はその一部として Figma のようなUIを持つアプリケーション(以下、図面版 Figma)の紹介をします。 図面版 Figma とは 図面版 Figma を開発するに当たっての解決したい課題は主にこの二つです。 1. 図面の拡大/縮小をスムーズに行える 2. 仕様変更があった際にそのアプリ上でやりとり/管理することで窓口を統一することができる ただ単に図面をwebで表示するだけではなく、文字が小さかったり手書きで書かれた文字もあるため拡大をスムーズに行う機能や取引先とのやりとりの窓口を一元化するためにチャットの機能を盛り込みます。 検証としてフロントエンドのみ実装して netlify にあげています。 今回はその検証で使用した GitHub のリポジトリ での実装を参考に進めていきます。 技術 stack 今回は主に以下の技術を使って実装しました。 TypeScript React styled-components PixiJS / react-pixi 状態管理, view のライブラリとして React と styled-components そして図面を表示して拡大縮小を滑らかに行うために WebGL のラッパーである PixiJS とそれを React から使いやすくした react-pixi を使用しました。 PixiJS PixiJS とは 2D のグラフィックス処理を実現するための JavaScrip ライブラリです。 似たような 2D グラフィクス向けのライブラリとしては konvajs や EaselJS などがありますが、それらのライブラリとは異なり WebGL を使ってレンダリングをおこなう点が特徴です。 また、同じく WebGL を利用しているライブラリとして three.js がありますが、three.js は 3D のグラフィック表現を得意としておりそれぞれ得意な領域が異なります。 今回はよりWebGLを使って滑らかに拡大縮小を実現するためにこの PixiJS を使用しました。 セットアップ 必要な npm のインストールを行なっていきます。 (lint や prettier, webpack などは割愛します。詳しくはリポジトリの package.json を参考にしてください) $ npm i --save @inlet/react-pixi pixi.js react react-dom styled-components $ npm i --save-dev @types/react @types/react-dom @types/styled-components typescript App.tsx (簡略化のためスタイルなどのコードは省略しています) アプリケーションのルートとなる App.tsx では PixiJS をマウントするためコンポーネントと、メッセージのやりとりをするコンポーネントをマウントします。 // App.tsx const App = () => { ... return ( <Wrapper> <Pixi /> <Chat /> </Wrapper> ) ... } 図面表示エリアの実装 App コンポーネントからマウントされている Pixi コンポーネントで PixiJS のコードを書いていきます。 PixiJS の基本的な使い方はほかのグラフィックライブラリと似ていて、 Stage をつくりその中に表示するオブジェクト( Container , Sprite など)を記述します。 Container Container は子供の要素を保持する汎用的なオブジェクトで、Graphic や Sprite など、他のオブジェクトのコンテナとして機能するすべてのディスプレイオブジェクトの基底クラス Sprite 画面にレンダリングされるすべてのテクスチャオブジェクトのベースのオブジェクト Stage オブジェクトではグラフィックを表示させるための root の表示域を設定します。 <Stage width={canvasWidth} height={canvasHeight} id="canvas"> ... </Stage> canvasWidth, canvasHeight はそれぞれ windowSize から取得して設定します。 そして Container を定義し、その中に図面を表示させるため Sprite オブジェクトを展開します。 Sprite オブジェクトでは表示する image を指定する他に、画像のサイズや拡大・縮小の割合 scale 、表示する位置 x , y などを設定することができます。 そのため、表示している画像の拡大縮小や位置を変更するにはこの scale や x , y を React の state で管理し、ユーザーの入力に応じて値を変更すれば良さそうです。 その図面の表示に関する state をそれぞれPixi.tsx 内に定義します。 // Pixi.tsx const [scale, setScale] = useState(0); const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); そしてその値を Sprite に設定します // Pixi.tsx <Container> <Sprite image="./images/sample.jpg" anchor={0.5} x={position.x + canvasWidth * anchor} y={position.y + canvasHeight * anchor} scale={scale} interactive={true} /> ... </Sprite> </Container> anchor は設定している Sprite の中心の値を設定します。ここでは 0.5 に設定しており WebGL の表示エリアのちょうど中心に設定しています。 x , y もこの anchor と state で管理している x , y の値で設定をします。 拡大縮小 ここまでで図面を表示するための実装ができました。次にスクロールやピンチイン/アウトに応じて図面を拡大縮小させるための関数を書いていきます。 スクロールやピンチイン/アウトにを行うためのイベントとして onWheel という JavaScript のイベントがあります。 このイベントを Pixi.tsx 内で useEffect を使ってイベントの登録を行います。 // Pixi.tsx const wheelHandler = (e: WheelEvent) => { e.preventDefault(); if (!Number.isInteger(e.deltaY)) { setScale(currentState => Math.min(Math.max(0.05, currentState + e.deltaY * -0.001), 1)); return; } setPosition(currentState => ({ x: currentState.x - e.deltaX, y: currentState.y - e.deltaY })); }; useEffect(() => { const el = document.getElementsByTagName('canvas')[0]; el.onwheel = e => { wheelHandler(e); }; return () => { el.onwheel = null; }; }, []); !Number.isInteger(e.deltaY) ここでは現在行われているスクロールが小数点を含んでいるか(整数か)どうかを見ています。 スクロールイベントでは単純な x and/or y 軸方向のスクロールでは deltaY の値は整数になり、ピンチイン/アウトでは小数付きの数字になります。 単純なスクロールの場合は表示している図面の位置を変更し、ピンチイン/アウトでは図面の倍率を変更したいためこの deltaY の値で条件分岐を行い、それぞれのケースで state の値を更新しています。 ここまでで図面の表示とスクロールによる図面の拡大/縮小機能の実装を行なってきました。次にチャット機能を実装していきます。 チャット機能 チャット機能は Figma と同じように図面の上にピンを立てるのと、右カラムに入力されたチャットを表示します。そのため、 App.tsx に必要な state を定義します。 // App.tsx const [chatList, setChatList] = useState< { messages: { id: string; author: string; createdAt: string; message: string }[]; inputValue: string; pin: { xRatio: number; yRatio: number }; // 0 ~ 1 }[] >([]); const [activeChatIndex, setActiveChatIndex] = useState(0); 入力されたチャットと入力された場所を保持するための state と active になっているチャットを保持するための state を定義し、それぞれ子コンポーネントに渡します。 右カラムに表示するチャット欄はいわゆる普通の React アプリなため説明は省略します。 Pixi.tsx では App.tsx から受け取った chatList state の pin の位置を展開します。 // Pixi.tsx <Container> ... {props.pins.map((p, i) => { const positionX = originalImageSize.x * scale * p.xRatio + (canvasWidth - originalImageSize.x * scale) / 2 + position.x; const positionY = originalImageSize.y * scale * p.yRatio + (canvasHeight - originalImageSize.y * scale) / 2 + position.y; return ( <React.Fragment key={`${p.xRatio} ${p.yRatio}`}> <Sprite image="./images/pin.svg" anchor={anchor} x={positionX} y={positionY - 10} scale={0.23} click={() => props.clickPinHandler(i)} interactive={true} cursor="pointer" /> <Text text={String(i + 1)} x={i < 9 ? positionX - 2 : positionX - 5} y={positionY - 18} scale={0.3} style={numberStyle} /> </React.Fragment> ); })} </Container> pinの位置は、拡大/縮小に対応するために絶対値ではなく比率で保存し、それを展開しています。 Pinの画像は図面と同様に Sprite コンポーネントで表示し、ピンの上に表示する番号は Text コンポーネントを使って表示します。 また図面をクリックされた際に新しくピンを表示するために、図面を表示している Sprite に pointerdown 属性を追加し、ピンを追加する関数を実装します。 ここで画像のサイズからピンを表示する位置(比率)を取得し、 App.tsx の state の値を更新しています。 // Pixi.tsx <Sprite image="./images/sample.jpg" anchor={anchor} x={position.x + canvasWidth * anchor} y={position.y + canvasHeight * anchor} scale={scale} interactive={true} pointerdown={(e: PIXI.InteractionEvent) => { const imageSize = { x: originalImageSize.x * scale, y: originalImageSize.y * scale }; const xRatio = (e.data.global.x - ((canvasWidth - imageSize.x) / 2 + position.x)) / imageSize.x; const yRatio = (e.data.global.y - ((canvasHeight - imageSize.y) / 2 + position.y)) / imageSize.y; props.addPin({ xRatio, yRatio, }); }} /> まとめ ここまでwebアプリケーションに WebGL を用いて画像を表示し、クリックされた箇所でピンを表示してチャットを行えるアプリケーションの開発を行ってきました。 ピンをクリックした時にフォーカスを合わせる実装など今回の記事では一部省略した部分もありますが、全体のコードは こちらのリポジトリ にあるので是非参考にしてみてください。 弊社ではこの図面管理のアプリケーション以外にも上記に挙げた課題を解決するために、送られてきた図面の内容を解析するためのアプリなど製造業の課題を解決するためのアプリケーションの開発を行っています。 少しでも興味ある方は是非一度お話ししましょう! カジュアル面談のお申込み
こんにちは。テクノロジー本部バックエンド開発グループの小倉です。 この記事は キャディ Advent Calendar 2020 の9日目です。前日は山田さんの protobuf v3 の optional について でした。 Rustはイカしたエコシステムが充実していて、型が厳しい言語の割にはコンパイラやリンターに従うだけで動くコードが書けたりします。その結果「オレは雰囲気でRustをしている!」という気持ちになったりするんですよね〜。 本記事では、巷にあふれる入門記事を読んで実務に挑み、その結果でてきたよくわからない点を調べた結果得た知識を紹介します。 巷にあふれているRustのasync/awaitの記事。 これらを読みました Rustの非同期プログラミングをマスターする 2019 年の非同期 Rust の動向調査 Rustのasync/awaitとスケジューラの話 / rust-async-await Rustのasync/awaitをスムーズに使うためのテクニック DIFFERENT LEVELS OF ASYNC IN RUST Async Book 読んだ感想 async関数は「あとでやっておきますよオブジェクト」を返すもの。 「あとでやっておきますよオブジェクト」は正しくは ステートマシン(State Machine) という 厳密には「 あとでやっておく」+「言われたら出来るところまで進めておく」。 すなわち「まだ途中で今ここ」「もう終わった」などのステート(状態)を持つマシン(計算機) 残りの処理の内容も含めてステートマシンが持っているので、途中の状態のステートマシンをスレッド間で渡し合うことで、柔軟に並列処理できる(便利!) RustではFutureオブジェクトがステートマシンの実装の一例。かつてFutureはデファクトなクレートだったが、Rust1.39で標準ライブラリに入った。 Future::poll を呼ぶことが「出来るところまで進めるように言う」に相当する。 ステートマシンとは別に「ステートマシン走らせる人」がいて初めて非同期処理が出来る。 「ステートマシン走らせる人」を ランタイム(Runtime) と呼ぶ Rust文脈だとFuture::pollをうまく呼ぶ人 ランタイムはステートマシンの様子を見ながら適宜「出来るところまでやるように言う」。 Runtimeの例:tokio 他にも色々 https://qiita.com/benki/items/0792444cd6c2a162fb56 block_on(非同期タスク)ってやればとりあえず実行できる、、、? 非同期関数を作るときは、awaitのタイミングを意識しないと非同期処理の恩恵が受けられない awaitを付けるとFutureが終わるまで待たせる awaitを付ける行為はステートマシンの「途中まで」の「途中」を増やすだけ。それだけでは並列処理にはならない。 昔作ったfutureをあとでawaitすると並列化した気になるけれど、実はawaitのタイミングまでfutureの中身の処理は走らない。 並列化したかったらspawnして独立したタスクとして切り出すか、joinやselectなど「複数のFutureを渡してよしなに全部終わらせてくれるやつ」を使う必要がある。 実務に挑んだ結果 案外なんとかなる。 同時に「オレは雰囲気でasync/awaitしている!」という気持ちにもなる。 よくわからないけど、コンパイラやリンターに言われたとおりにすればコンパイル通った、という事例がしばしば moveを付けたり付けなかったり Sync, Sendを付けたり付けなかったり Cloneを付けたり付けなかったり つらかったこと Iterator::mapとかで、async関数を渡すとコンパイルが通らない。 身に覚えのないpanic 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like block_on) attempted\ to block the current thread while the thread is being used to drive asynchronous tasks.' 脱雰囲気のためのナレッジ整理 moveはいつ必要? move はasyncに限らずclosureの文脈で使われるキーワードである。 クロージャー内では引数でもらっていない値を使うことが出来る。 クロージャーが定義されるタイミングの環境にある変数を キャプチャ しているのである。 fn main(){ let x = 3; let is_x = |a| a == x; println!("{}", is_x(3)); } キャプチャは3種類のやり方がある。 - 環境にある変数を所有権ごと奪う。 - 環境にある変数の可変参照を持つ。 - 環境にある変数を(可変でない)参照を持つ。 所有権を奪いたい場合は fn main(){ let x = 3; let is_x = move |a| a == x;//xの所有権はクロージャーに移動 println!("{}", is_x(3)); } のようにすれば良い。 asyncに置けるmoveも同様である。asyncクロージャーやasyncスコープはFutureを返すが、そのFutureは環境のキャプチャを保持している。 #[tokio::main] async fn main(){ let x = 3; //asyncブロック let my_future = async{ x * 2 }; println!("{}", my_future.await); } 以下は、筆者がかつてasyncクロージャのつもりで書いたがコンパイルエラーになるコードである。 #[tokio::main] async fn main(){ let my_closure = |y| async{ 3 + y }; let x = 10; let my_future = my_closure(x); println!("{}", my_future.await); } 筆者が犯した間違いは2つ: - 上記コードの my_closure はasyncクロージャではなく、asyncブロックを返す普通のクロージャである。 - yの所有権が何やら良くない。 asyncクロージャはまだunstableであり、Nightlyでなければ使えないので、普通のクロージャのまま続けるとして、どのようにすれば上記コードのコンパイルが通るだろうか? まず、 y のライフタイムは my_closure クロージャの内部である。 y はasyncブロックの中で参照され、そのasyncブロックは my_closure の戻り値として、 my_closure の外側に放り出される。 このasyncブロックが、すなわちFutureが、awaitされるまで y は生きている必要がある。よって以下のようにすることで解決する。 #[tokio::main] async fn main(){ let my_closure = |y| async move { 3 + y }; println!("{}", my_closure(10).await); } ようするに、 Futureに所有権を移したい時にmoveをつければ良い 。とある変数 x の所有権をFutureに移したくなるのは - Futureが x のスコープを飛び出るとき である、これはスレッド間通信があるときはもちろん、スレッドをまたがなくてもよくある話である。 Sendはいつ必要? Send/Syncはスレッド間で値をやりとり/共有したいときに付けるTraitである。具体的な構造体にSendやSyncをimplするのは非推奨であり、通常はトレイト境界などの文脈で登場するのみである。 非同期タスクはRuntimeによっていくつかのスレッドに振り分けながら実行されていることを思い出すと、async関数の引数や戻り値はSendやSyncをimplしていなければならない気になる。これは事実そうであるが、必ずしもすべてにSendやSyncをimplしなければならないわけではない。ここではそれを詳細に説明する。 tokioのスレッド戦略 詳細な説明の前に、 tokio のスレッド戦略を説明する。 tokioのRuntimeは、1コア内に、少しの コアスレッド と、必要な数の ブロッキングスレッド を建て、それらのスレッドを駆使しながら処理を実行していく。 コアスレッド は非同期タスクをやるためのスレッドで、デフォルトでは1つしか建てない。複数建てるとしても少数である。 コアスレッドではやる準備ができたFutureオブジェクトに対してpollを行い、出来るところまで進めてもらう。複数のFutureを少しずつ進めながらやる場合も、コアスレッドで走らせるタスクを入れ替えながら行っている。 ブロッキングスレッド は重たい処理を行うためのスレッドである。必要に応じてスレッドを建てるので、膨大な数になることもある。 終わるまで時間がかかる処理をコアスレッドで行うと、他の実行可能な処理を待たせることになり非効率である。そのため、時間がかかりそうなタスクは同期非同期かかわらずブロッキングスレッドに「逃がす」運用が想定されている。 重い処理の逃がし方 重い処理を逃がすためには、 spawn_blocking と block_in_place の2つの方法がある。 spawn_blocking use tokio::task; #[tokio::async] async fn main(){ let res = task::spawn_blocking(move || { // めっちゃ重い処理をここでやる "done computing" }).await?; } 上記コードのようにspawn_blockingに重いタスクを渡すと、別のブロッキングスレッドを建てて、そこで重いタスクを実行してくれる。 このときスレッド間の移動が発生するため、 同期タスク と その戻り値 がSend+'staticを実装している必要がある。 'staticを付けているのは、本質的に参照や参照を含むオブジェクトを受け渡しできないようにするためである。('staticの参照などはそれ自体は'staticではないがSendをimplするので受け渡しできる) よもやま:昔は Send:'static だったらしい、 https://rust-lang.github.io/rfcs/0458-send-improvements.html block_in_place use tokio::task; #[tokio::async] async fn main(){ let res = task::block_in_place(move || { // めっちゃ重い処理をここでやる "done computing" }).await?; } 上記コードのようにblock_in_placeに重いタスクを渡すと、今のスレッドをブロッキングスレッドに変更し、別に新たなコアスレッドを建てる。 このときスレッド間移動が発生しないため、 Sendはいらない 。 使い分け Send+'staticが実装されていれば、spawn_blocking, 実装されていなければblock_in_placeを使えば良い。 Syncはいつ必要? 実務でSyncを要請されたのは、実はasync/awaitの文脈ではなく、tower-grpcが原因でした。 tower-grpc上ではApiはSyncをtrait境界にしています。 Cloneはいつ必要か? 実務でCloneを要請されたのは、以下のような箇所( IsContext (仮名)は独自のTraitです) pub async fn my_function< T: 'static + Clone + Send + IsContext, >( ctx: &T, arg: i32, ) -> Result<()> { let ctx = ctx.clone(); spawn_blocking(move || other_function(&ctx, arg)).await? } spawn_blockingのために ctx の参照(&T)を別スレッドにSendしたい。 spawn_blockingで渡すクロージャーは'staticである必要があるため、参照をキャプチャできない。 よって、ctxをクローンして、それをキャプチャさせてSendしている。 ちなみに、T: Syncであれば &T: Sendである。どのみち'staticではないのでTにSyncをimplしようと、Cloneは必要になる。 for_eachとかmapとかfilterをasync文脈で呼び出したいときはどうすればいい? Stream を使いましょう。 futuresトレイトで提供されています: https://docs.rs/futures/0.3.5/futures/stream/index.html これは非同期版Iteratorで、Iteratorでできそうなことはひととおり実装されています。 非同期な関数をmapの引数に入れたいとき このときは Stream::mapではなくStream::thenを使うと良い。 use futures::stream::{self, StreamExt}; async fn twice(x: i32)->i32{ x * 2 } #[tokio::main] async fn main(){ let v = vec![1, 2, 3]; let twiced_v = stream::iter(v).then(twice).collect::<Vec<i32>>().await; assert_eq!(vec![2, 4, 6], twiced_v); } なんでblock_onの中でblock_onしたらダメなの? block_onが呼ばれると、そのスレッドがブロックされて、中身の実行が終わるまで他のことができなくなるから。(ここでいうブロックはOSのAPIを呼んで行われるOSのスレッドのブロック)。 すなわち、block_onの中では非同期タスクが、よしなに走らされているはずで、そのなかでコアスレッドをブロックされると、非同期タスクを非同期に実行できなくなってしまうので、本意から外れることになる。 同様の理由でロックを取る処理も非同期タスクの中で うかつには やってはいけない。非同期タスク内でもロックが取れる機構(例: https://docs.rs/tokio/0.2.22/tokio/sync/struct.RwLock.html )が用意されているので、そちらを使用すれば問題ない。 r2d2-postgresは、非同期に使われる想定がされていなかったため、中ではスレッドをブロックするタイプのロック機構が使われている。よって、非同期タスク内で呼び出すとpanicが起きる。これが冒頭の「身に覚えのないpanic」の正体である。 というのが前回までのあらすじ 水平線で囲まれた、常体(だ、である調)で書かれている範囲が前回までのあらすじです。 上で書いたようなことをまとめて、社内向けに公開したら好評でした。私も鼻高々でした。 問題はその後で、moveとかSend/Syncとかの話は RustBookを読んだら全部書いてあるんですよね 。 びっくりしましたねえ。私は普段塾講師もしていて、中高生相手に情報科学を教えています。口を酸っぱくして「なにごとも、詳しいことが知りたくなったら、まずは公式ドキュメントをあたろう!」と教えてきたのに、、、これはとんだ医者の不養生でございました😌 まあ、最近進展があったasync/awaitあたりはもちろんRustBookには書いてないので、巷の記事をあさらないといけませんでしたね。AsyncBookがありますが、 今は絶賛成長中です 。 Futureを扱うトレートが乱立していたときの話とか、標準ライブラリに取り込まれるまでの歴史とかはBook系ではあまり深く踏み込まないので、そういうのが学びたければ巷の記事をあさるのもいいですね。歴史系は古い記事ほど良いソースです。 巷にあふれている良い記事を読んで思うのは、Rustの難しい部分と向き合いやすくなる「気持ち」を詳しく説明してくれているものが多いことですね。これはRustBookとかだと省かれていがちで、理解度を1から10に上げるときに役に立ちます。逆にRustBookに書かれていることは気持ちというよりは詳細な事実といった感じで、理解度を0から1に上げる部分に該当すると思います。 私が思うに理解度を0から1に上げる資料を読む前に理解度を1から10に上げるための資料を読んでRustに立ち向かうと「雰囲気でRustをしている!」という気持ちになるのでしょう。万物を完全に理解する必要はないので、大抵のものは「雰囲気で○○する」くらいがちょうどいいのだと思います。 ただ、タスクがスレッドを飛び越えまくるコードを書くともっとちゃんと理解しないと不安になってくる部分もあるかと思うので、そういうときはRustBookに帰ってきて、脱雰囲気をすると良いでしょう。 簡にして要を得た結論 まず、巷に溢れた記事を読んで気持ちを知ろう。すると雰囲気でRustができるようになる。 雰囲気でRustをする状態から脱したかったら、RustBookや仕様書に帰ってこよう。 おわりに この記事もまた、巷にあふれる記事の仲間入りをするのでしょう。この記事では私の「気持ち」のフレーバーがかかっております。皆さんのお口にあったのであれば幸いです。そうでなければ、残念。でもまたどこか別の場所で良い「気持ち」に出会えることを願います。 CADDiでは「モノづくり産業のポテンシャルを解放する」ための仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。 少しでも興味を持っていただけましたたら、リニューアルされたばかりの 採用サイト をご覧ください。(わかりやすく、ヘッダの仕掛けもかっこいいので是非!) また、 カジュアル面談 も行っていますので、実際にエンジニアに会ってみたい方は こちら からどうぞ。
こんにちは。テクノロジー本部バックエンド開発グループの山田です。 この記事は キャディ Advent Calendar 2020 の8日目です。前日は桐生さんの tailwindcss のコンセプトとメリットについての考察 でした。 今回は 以前のgRPCの記事 に引き続き、システム開発の BFF や BE 間の通信で利用している gRPC & protocol buffers (以下 protobuf ) に関する記事です。 開発/運用していく中で protobuf 上での Optional な値の扱いをどうするかという点で何度も困らせられた苦い思い出をもとに、 proto3 で実験的に復活した optional について調査・検証していきます。 検証に利用したコードは こちら にあります。 時間のない方は最下部のまとめ欄をご覧ください。 [toc] optionalの定義方法と実体 久しぶりに protobuf と戯れることにしたため、まずは公式から情報を得ることとします。 公式に記述されている通り、v3.12.0より実験的機能として復活し、内部的には oneof のフィールドとして扱われる実装となっているようです。 protobuf/field_presence.md at master · protocolbuffers/protobuf · GitHub protobuf/implementing_proto3_presence.md at master · protocolbuffers/protobuf · GitHub optionalの検証 実際に触りながら差分を確認していくために、今回は以下のようなprotoの定義を利用して、検証していきます。 syntax = "proto3"; package pingpong; service PingPong { rpc SendPing(Ping) returns (Pong) {} } message Ping { string type = 1; optional string payload = 2; } message Pong { optional string payload = 1; } 今回はキャディのシステム開発で利用している Rust / JS / TS に関して検証することとします。 まずRustでの対応状況を確認していきます。 Rust Option に変換されていい感じに使えるのではないか、と期待していたものの、protobuf の有名所 crate である stepancheg/rust-protobuf と danburkert/prost はどちらも issue がたてられているだけで、まだ実装されていませんでした。 ■ 該当 issue Support Field Presence (proto3) · Issue #513 · stepancheg/rust-protobuf · GitHub Support proto3 Field Presence · Issue #351 · danburkert/prost · GitHub rust-protobuf は version 3 に組み込まれる予定になっているようなので、動向が気になるところです。 rust-protobuf version 3 · Issue #518 · stepancheg/rust-protobuf · GitHub 出落ちのような形で残念ですが、続いて JS / TS での状況を確認してみます。 JavaScript / TypeScript あらかじめ、JS / TS の検証で protoc の呼び出しをするために利用している grpc-tools を導入しておきます。 ※ yarn 及び typescript を導入済みであることを前提にしています yarn add -D grpc-tools 以前のgRPCの記事 でもまとめましたが、 protobufの扱い方はいくつかあるため、複数のライブラリで検証をすすめてみます。 protoc の JS 出力を使う方法 もともと提供されている、protoc の JS 出力を利用することで生成はできました。 しかし残念ながら 出力された JS に関連する TS 向けの型定義ファイルの出力を行う protoc のプラグインは対応されているものを見つけることはできませんでした。 そのため、今回は protoc で生成された JS の確認のみにとどめます。 以下は生成するためのコマンドと、生成された protobuf 向けコードの抜粋です。 生成コードを確認してみると、今回 optional で定義した payload というフィールドに対して、以下のように oneof と同等のコードが生成されていました。 ■ ライブラリ導入 yarn add google-protobuf ■ コード生成 shell の一部 DEST_DIR=./generated "$(yarn bin)"/grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${DEST_DIR} \ --experimental_allow_proto3_optional \ -I ../ ../pingpong.proto ■ 生成されたコードの一部 /** * Clears the field making it undefined. * @return {!proto.pingpong.Ping} returns this */ proto.pingpong.Ping.prototype.clearPayload = function() { return jspb.Message.setField(this, 2, undefined); }; /** * Returns whether this field is set. * @return {boolean} */ proto.pingpong.Ping.prototype.hasPayload = function() { return jspb.Message.getField(this, 2) != null; }; 各型での optional については、 公式のテストコード (protobuf/proto3_test.js) にて網羅的に記述されていたので確認してみてください。 続いて protoc の JS 出力を使わない方法 の検証をしていきます。 protocの JS 出力以外の方法 キャディの開発では以前検証した情報をもとに protobuf.js を利用しているため、リポジトリの Issue や PR を覗いてみましたが、残念なことに特に動きがないことがわかりました。 ■関連 issue Support for required fields in proto3 via options · Issue #1468 · protobufjs/protobuf.js · GitHub Implement Field Presence · Issue #1406 · protobufjs/protobuf.js · GitHub しかし、上記 Issue にリンクされている Issue をたどってみると、 stephenh/ts-proto というレポジトリたどりつきました。 たどりついた Issue の中には、なんと 2020年9月に公開された v1.32.0 にて optional に対応したと書かれています。 (対応しているライブラリがほぼ見つけられていない状態だったので、この記事のコンセプトが崩れずにすんでよかったです) ということで、ここから ts-proto で optional の検証をしていきます。 ts-proto の README 等をみてみると、proto ファイルをもとに protobuf.js の encode / decode を利用するようにラップされたコードを生成してくれる protoc のプラグインだということがわかりました。 ここから検証コードを作って生成後のコードをみていきます。 ■ ライブラリ導入 yarn add -D ts-proto yarn add protobufjs ■ コード生成 shell の一部 DEST_DIR=./generated "$(yarn bin)"/grpc_tools_node_protoc \ --plugin="$(yarn bin)/protoc-gen-ts_proto" \ --ts_proto_out="${DEST_DIR}" \ --ts_proto_opt="lowerCaseServiceMethods=true" \ --experimental_allow_proto3_optional \ -I ../ ../pingpong.proto ■ 生成されたコードの一部 export interface Ping { type: string; payload?: string | undefined; } export interface Pong { payload?: string | undefined; } 生成されたコードでは、TS 上で Optional フィールドとして定義されており、oneof とは異なる定義となっています。 ( oneof を利用する場合には union 型で出力するオプションなどがありました) ts-proto の考察 少し脱線しますが、上記確認をするなかで ts-proto が非常に便利そうだったため、今後の開発の検討のために良い点と気になる点をまとめておくことにします。 良い点 optional の対応 protoc の js 出力でできなく、protobuf.js には定義されていたオブジェクトからの代入と同等の実装がある(fromObject) oneof を union 型で出力できる gRPC のサービス実装で注目されている Twirp , grpc-web , NestJS 向けの実装がある 内部の encode / decode に protobuf.js の処理を利用してるため高速(protobuf.jsのベンチマークを信じると) 気になる点 出力されるコードの内部に any が利用されている message に対応するモデルの定義が、 同名の interface と const で指定されており、型が少し扱いにくい grpc-loader 等と組み合わせて利用する場合に、 Service の定義がシンプル(Promiseを利用した引数一つの定義しか出力されない) Service の 第2引数で利用する metadata 対応が nestjs オプションを有効化したときにしかきかない 今回の調査をきっかけに、今後 gRPC 関連の処理を見直す際に検討したいと思える、良いライブラリと出会えてよかったです。 まとめ 今回の調査・検証結果をまとめると以下のとおりです。 protobuf v3.12 から optional が実験的に復活し、内部的に oneof として扱われている protobuf のコード生成ライブラリ Rust で field presence の対応が入った crate は現時点では見つけられず JavaScript で field presence を使う場合は公式の protoc の js_out で対応されている TypeScript で field presence を使う場合は ts-proto で対応されている protoc の TS 向けプラグインには対応されているものはなかった 参考 以下は調査をする中で見つけたサイトの紹介です。 NestJSでgRPC API作るならコード生成はts-protoで決まり ライブラリの調査しているときに見つけた、ts-proto + NestJS で gRPC サーバーを作る記事です。 ありがたいことに 以前のgRPCの記事 へリンクしていただけてました。感謝です。 https://qiita.com/vol1003/items/326a074fb1a605651750 protobuf の optional な値の扱い方とField Presence について チームで検討していたことが簡潔にまとまっており、proto3 の optional についても詳しく書かれていました。 細かく仕様を見てみようと思った矢先に見つけたので、今回の記事では大幅に端折りました。いいまとめをありがとうございます。 https://note.com/dd_techblog/n/n95e4331a8eea おわりに 日頃の開発をするなかで、protobuf での optional な値の扱い方をどうするかを定期的に話し合ってきていましたが、調査・検討をするタイミングを作ることができずにいたため、いい機会になりました。 また、この記事が optional な値の扱い方に困っている方へ多少の手助けになると幸いです。 CADDiでは「モノづくり産業のポテンシャルを解放する」ための仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。 少しでも興味を持っていただけましたたら、リニューアルされたばかりの 採用サイト をご覧ください。(わかりやすく、ヘッダの仕掛けもかっこいいので是非!) また、 カジュアル面談 も行っていますので、実際にエンジニアに会ってみたい方は こちら からどうぞ。