TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

222

こんにちは。駅奪取チームエンジニアの id:dorapon2000 です。 私達のチームでは、4月〜7月にプロダクトの負荷対策に注力しました。その結果、通信量の削減やDB負荷の低減、それに伴うインフラコストの削減などに繋がりました。負荷対策の方法は手探りながら多くのことをしたのですが、その中で今回は不要なDBのロック待ちを改善した部分に注目して、どのような方法でロック待ちを改善したかについてサンプルコードを交えてお話していきます。 ロック待ちの改善にバリエーションがあることについて持ち帰っていただけると幸いです。 環境 Amazon Aurora MySQL version 2 (MySQL 5.7) データベースエンジンはInnoDB トランザクション分離レベルはREPEATABLE READ ロックとロック待ち 詳細は他の記事にお譲りします。ここでは簡単な説明をば。 ロックをわかりやすく言うと、他の人に自分の作業領域への割り込み作業をさせない仕組みです。 もう少し具体的に言うと、DB内の指定範囲を別のトランザクションからの参照・更新を一時的に不可にさせる仕組みです。指定範囲はテーブル全体だったり、1行だったり、複数行だったりします。 特定のレコードをロックすると、他のトランザクションはロックが解除されるまで待たなくてはいけません。これをロック待ちと言います。 私のチームで起きていたロック待ちの問題 ロック待ちが瞬間的に連鎖的に発生し、そのうち大半のトランザクションがロックを獲得する前にタイムアウトによってエラーになっていました。 大量のトランザクションがタイムアウトまで負荷をかけ続ける状態です。 負荷がかかる様子はAWS RDSのPerformance Insightsで確認できます。 ロック待ちを見つける RDSのPerformance Insightsでは負荷の原因となった発行クエリは見られても、ソースコード内の場所までは特定できません。 そのため、私達は以下の方法でロック待ちが多発している箇所を特定しました。 発行クエリからソースコードの該当箇所を予想する 負荷がかかりそうな処理のソースコードをコードリーディングする クエリの発行箇所のファイル名と行数をコメントとして発行クエリに付随させる仕組みを自作する 特に3つ目の方法により、Performance Insights 上でクエリの呼び出し元が明確になりました。 修正の方針 問題のロック待ちが多発する箇所を特定したら、次は修正です。 本当にそのロックは必要か 条件で絞り込み、ロックを取る回数を減らせないか ロックを取る前に早期returnができないか そもそも、その処理は必要か 修正例① 利用されないuserロック 最も素朴な修正例です。 db->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; return if 9 割Trueになる条件; 処理A 処理B # 実はuserレコードをロックしている 処理C # userレコードのロックが必要 }); # COMMIT 以下のようにすることで無駄なロックを削除できました。 db->txn_do(sub { - $user->lock - return if 9割Trueになる条件; 処理A 処理B 処理C }); 解説 利用されないロックは削除すればいいです。しかし、言うは易く行なうは難し。 実際に利用されていないことを証明するために、userロック以降のすべての処理を目で追いかけました。 その結果、処理Bで実は重複してuserロックをしていることがわかり、それ以前でuserロックを利用する処理がないことがわかりました。 トランザクション先頭のuserロックを削除することで、10割userロックしていたコードは1割しかロックしないコードになりました。 修正例② 早期return 美しくないですが簡単で大きな効果があった修正例です。 db->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; return if $user->has_active_license ; # ほぼここで早期returnする ライセンスが切れている場合の処理 }); # COMMIT 以下のようにすることでロックの回数を減らす事ができました。 db->txn_do(sub { + return if $user->has_active_license; $user->lock return if $user->has_active_license; ライセンスが切れている場合の処理 }); 解説 修正前の問題点 has_active_licenseは、ライセンスの有効期限が切れていればFalseを、切れていなければTrueを返すメソッドです。ほとんどの場合で切れていないためTrueを返し、ソースコード上は早期returnします。 修正前のコードの問題点は、ほとんどの場合で早期returnして何もしないにも関わらず、必ずuserロックが取られてしまうことです。 ifの前にロックを取る理由 has_active_licenseの前でロックを取る理由は、最新のユーザ情報を取得したいからです。MySQLでは、SELECT FOR UPDATEをすることで、トランザクション中に別トランザクションでレコード更新が発生しても、更新後の値を読み取ることができます。つまり最新のレコード情報を取得できます。Locking Readと呼ぶようです。詳しくは以下の記事が詳しいです。 漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点 早期returnの重ねがけ さて、本題である早期returnを重ねて書くことの効果について説明します。第一にコードが冗長以外の副作用がないことは明らかです。第二にロックの回数を減らす事ができます。説明すると言ってもこれだけですが、非常に嬉しいわけです。すべてのコードを目で追いかける必要がありません。 Locking Readをしない1回目の早期returnはユーザ情報が古く、ライセンスが切れているのに切れていないと判定される可能性があります。それをLocking Readする2個目の早期returnで拾ってあげます。その逆であるパターンはロジック上存在しないため考慮しません。 修正例③ キャッシュを使う まずは修正前のコードから。 db_master->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; ログイン処理A if 1 日 1 回だけ ログイン処理B if イベント参加後に 1 日 1 回だけ ログイン処理C if API経由のアクセスを含めて 1 日 1 回だけ ログイン処理D if など }); # COMMIT ログイン情報をキャッシュに保存して、キャッシュがないときに限りトランザクションの処理を実行するようにしました。 + my $cache_key = $class->generate_key($user->id、日付、イベント参加してるかのフラグ、API経由かどうかのフラグ); + my $is_already_logged_in = cache->get($cache_key); + return if $is_already_logged_in; db_master->txn_do( sub { $user->lock ログイン処理A if 1日1回だけ ログイン処理B if イベント参加後に1日1回だけ ログイン処理C if API経由のアクセスを含めて1日1回だけ ログイン処理D if など }); + cache->set($cache_key => 1); 解説 修正前の問題点 ほとんどの場合で処理が何もされないにも関わらず、userロックを取っていることです。 userロックを取る理由 前述と同様に最新の情報がほしいためでもありますし、ログイン処理の中で並列に実行されると不具合・不整合が起きる箇所が多くあるためでもあります。 キャッシュ利用によるロック回避 修正例③の解決方法は修正例②と思想は同じです。ロックを取る前に条件に合致しないときだけ早期returnして、合致したときは処理を実行するようにしています。 今回はその条件をキャッシュから取得できるにしています。 ポイントはキャッシュに使うキー(鍵)です。ログイン処理A/B/C/Dのいずれかを実行する必要があるとき、キャッシュキーが存在していなければいいわけです。例で示します。 その日1回もアクセスしたことがない キャッシュキーはないため、早期returnされない ログイン処理ABCDが実行される login_1_20221216_false_false のような値をキーとしてキャッシュが作成される その日2回目のアクセス キャッシュキーは login_1_20221216_false_false ですでに存在し、早期returnされる ログイン処理は実行されない その日3回目のアクセス時にイベントにも参加していた キャッシュキーは login_1_20221216_true_false で存在せず、早期returnされない ログイン処理Bのみ実行される login_1_20221216_true_false のような値をキーとしてキャッシュが作成される キャッシュを使う方法は、ログイン処理全体をリファクタリングせずにロックを削減できる点が嬉しいです。 さいごに 並列実行を回避するためにuserレコードをロックしたくなりますが、ロックを剥がす労力は地道で相当だということが大きな学びでした。より影響が小さいレコードでロックできないか、そもそもロックを回避できないか。軽い気持ちでuserロックをすると将来痛い目に合うかもしれません。
アバター
エンジニアの id:toricor です。今年の初めまではサーバサイド(Perl)のタスクを中心に仕事をしていましたが、その後Android & iOS開発を担当するようになりもうすぐ1年になります。 今日はAndroidの位置情報ライブラリを題材に、インターフェースを活用してテスト用に位置情報のデータソースを差し替えやすくするAndroidのテスト例を紹介します。 play-services-location の21系ではFusedLocationProviderClientがクラスからインターフェースに変わった 位置情報取得の中心を担うライブラリ play-services-location の最新のリリースのうち、今回はFusedLocationProviderClientの変更に焦点をあてます。 アプリケーション開発者はFusedLocationProviderClientを介して位置情報を利用します。FusedLocationProviderClientは、Android端末がGPSやWifiなどから取得した位置情報について、まとめて管理して適切な位置情報を返してくれます。 さて、2022年10~11月リリースの play-services-location の21系のリリースノートによるとFusedLocationProviderClientがクラスからインターフェースになったとのことです。 21.0.1のリリースノート 21.0.0のリリースノート FusedLocationProviderClient, ActivityRecognitionClient, GeofencingClient and SettingsClient are now interfaces instead of classes, which helps enforce correct usage and improves testability. developers.google.com インターフェースになったことで improves testability テスタビリティ(テスト容易性)が向上したということです。 Androidのテストではインターフェースが共通のテスト用の偽の実装と差し替えるパターンが推奨されています が、実際にテストが容易になったのかをテストを書き実感したいと思います。 現在地を取得するgetCurrentLocationメソッドが正しい位置オブジェクトを返すかテストしたい 単純な現在地取得実装を用意しました GeoLocationRepository内でFusedLocationProviderClientの現在地取得(getCurrentLocation)メソッドを呼び出します GeoLocationRepositoryのコンストラクタはFusedLocationProviderClientを受け取り差し替え可能にします getCurrentLocationが成功すればaddOnSuccessListener、失敗すればaddOnFailureListenerで追加されたリスナーが呼ばれます // 一部省略 class GeoLocationRepository( private val locationProvider: FusedLocationProviderClient) { private val currentLocationRequest = CurrentLocationRequest.Builder().apply { setPriority(Priority.PRIORITY_HIGH_ACCURACY) setDurationMillis( 1000L ) setMaxUpdateAgeMillis( 30000L ) }.build() // テスト対象のメソッド // 取得したLocationの各値を、自前で用意したLocationPayloadに詰め替える @SuppressLint ( "MissingPermission" ) suspend fun getCurrentLocation(): LocationPayload { val def = CompletableDeferred<LocationPayload>() val cancellationTokenSource = CancellationTokenSource() val locationTask: Task<Location> = locationProvider.getCurrentLocation( currentLocationRequest, cancellationTokenSource.token ) locationTask.addOnSuccessListener { location: Location? -> def.complete( if (location == null ) { getEmptyLocationPayload() } else { buildLocationPayload(location) } ) } locationTask.addOnFailureListener { Log.d( "GeoLocationRepository" , "FailureListener @@@@@@" ) def.complete(getEmptyLocationPayload()) } return try { def.await() } finally { cancellationTokenSource.cancel() } } 本物のFusedLocationProviderClientを使うテストはセットアップと結果の制御が難しい まず本物のFusedLocationProviderClientクラスをそのまま使うテストを考えます。 FusedLocationProviderClientにはmockモードがあり、任意の位置情報を返すようにセットすることができます。 getCurrentLocationを呼び出したときにセットしておいた位置情報が取れたかどうかを確かめるテストを書けます。 しかし事前準備は少々手間がかかります。 debug/AndroidManifest.xmlに <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" tools:ignore="ProtectedPermissions" /> を与えます 「設定」->「開発者向けオプション」-> 「仮の現在地情報アプリを選択(Select mock location app)」から対象アプリを指定しておきます (または Uiautomator を利用し adb shell appops を使う方法 があります ) // setMockMode=trueの場合のテスト例 class GeoLocationRepositoryTest { private lateinit var client: FusedLocationProviderClient private val location = Location( "mock" ).apply { latitude = 35.6812362 longitude = 139.7671248 speed = 42.0F accuracy = 0.68f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } @Before fun setUp() { val context = InstrumentationRegistry.getInstrumentation().targetContext client = LocationServices.getFusedLocationProviderClient(context) client.setMockMode( true ).addOnFailureListener { throw it } } @After fun tearDown() { client.setMockMode( false ).addOnFailureListener { throw it } } @Test fun latitudeIsCorrect() { client.setMockLocation(location).addOnFailureListener { throw it } runTest { val acquiredLocation = GeoLocationRepository(client).getCurrentLocation() assertEquals( 35.6812 , acquiredLocation.latitude, 0.001 ) } } } 本物のFusedLocationProviderClientが提供するsetMockModeをtrueにすることで、getCurrentLocationが成功した場合の本物のレスポンスに近しいテストが可能です getCurrentLocationを意図的に失敗させ任意の例外を発生させるようなテストはできません FakeのFusedLocationProviderClientを使う場合はセットアップと返り値の改変が容易になる play-services-location の21系ではFusedLocationProviderClientがインターフェースとなりました。 この結果、本物のFusedLocationProviderClientの代わりに、FusedLocationProviderClientインターフェースを実装する偽のFakeFusedLocationProviderClientをGeoLocationRepositoryに渡すことができるようになりました。 // 偽のFusedLocationProviderClient // 一部省略 class FakeFusedLocationProviderClient : FusedLocationProviderClient { // テストケースごとに返り値を変化させるためのフラグ var shouldFail = false private val location = Location( "mock" ).apply { latitude = 35.6812362 longitude = 139.7671248 speed = 42.0F accuracy = 0.68f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } override fun getCurrentLocation( p0: CurrentLocationRequest, p1: CancellationToken? ): Task<Location> { // https://developers.google.com/android/reference/com/google/android/gms/tasks/Tasks return if (shouldFail) { Tasks.forException( Exception ()) } else { Tasks.forResult(location) } } // インターフェースのメンバーを省略 override fun setMockMode(p0: Boolean ): Task<Void> { TODO( "Not yet implemented" ) } } @OptIn (ExperimentalCoroutinesApi :: class ) class GeoLocationRepositoryWithFakeClientTest { private lateinit var fakeClient: FakeFusedLocationProviderClient @Before fun setupClient() { fakeClient = FakeFusedLocationProviderClient() } @After fun tearDown() { fakeClient.shouldFail = false } @Test fun latitudeIsCorrect() { runTest { // FakeのClientを渡す! val acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals( 35.6812362 , acquiredLocation.latitude, 0.0001 ) } } @Test fun zeroLatitudeIsAcquiredWhenFail() { runTest { fakeClient.shouldFail = true val acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals( 0.0 , acquiredLocation.latitude, 0.0001 ) } } } ここまでで、次の2点でテスタビリティ向上を確認できました。 テスト用の偽のFusedLocationProviderClientに置き換えることで、getCurrentLocationの返り値を自由に変更できるようになりました getCurrentLocation失敗時のFailureリスナーを呼び出しやすくなりました ACCESS_MOCK_LOCATION権限付与は不要になりセットアップが簡単になりました まとめ play-services-location の21系を使うテストでは、本物ではなく偽のFusedLocationProviderClientを使うことで、テスト対象メソッドの結果の制御がしやすくなったりセットアップが単純になったりすることでテストが容易になりました。 参考 Release Notes  |  Google Play services  |  Google Developers Use test doubles in Android  |  Android Developers Android端末の地理的位置を変更するJUnitテストルール | Y_SUZUKI's Android Log
アバター
はじめに サービスをデプロイするときはビルドしてテストしてから行うという手順はよくあります。 その時に、Google Cloud Platform (GCP) 上で CI/CD パイプラインを構築し、コードの変更をトリガーにしてビルド・テスト・デプロイが手軽にできる手法を紹介します。 使用するツール GCP Cloud Build App Engine GitHub 作成するもの Vue.js のプロジェクトで GitHub 上の main ブランチに push/merge されたら自動でビルド・テスト・デプロイを行う環境を構築します。 Cloud Build とは? 公式ドキュメント サーバーレス CI / CD プラットフォームでビルド、テスト、デプロイを行います。 構成を yaml ファイルで記述でき、実行するのはシェルスクリプトから独自で作成した Docker イメージなども活用できるので自由度の高いパイプラインを作成できます。 また、実行トリガーも GitHub 連携、Webhook など様々な場面で組み込みやすいものが用意されています。 App Engine とは? 公式ドキュメント モノリシックなサーバーサイドのレンダリングのウェブサイトを構築し、アジリティを維持します。App Engine は一般的な開発言語をサポートし、さまざまなデベロッパー ツールを提供しています。 こちらも構成を yaml ファイルで記述するだけで準備が整い、コンテンツの配信からサービスのスケーリングまでマネージメントしてくれます。 実際に作成する 上で紹介した 2 つのサービス + GitHub で実際に構築してみます。 Vue Application の作成 まずは、デプロイするサービスのセットアップです。 詳細は省きますが、今回は Vue.js Quick Start の Creating a Vue Application にそって、プロジェクトを作成しています。 基本的に例示されている設定と同じですが、Vitest の追加だけ Yes に変更し、ユニットテスト環境のセットアップを行っています。 App Engine の設定 あらかじめ、App Engine でデプロイするサービスを確認しておきます。 何も設定しない場合は default サービスにデプロイされますが、 default 以外が良い場合は app.yaml に設定が必要になります。 app.yaml の設定 デプロイするものや形式に合わせて適宜調整をしてください。 今回作成するのは Vue.js のプロジェクトでビルド成果物が配信できれば良いので、 runtime には nodejs16 ( php81 とかでも大丈夫です)を、 service にはデプロイする App Engine のサービス名を記述します。 runtime と service が記述できたら、 handler の項目でファイルと配信 URL を紐付けます。 Vue.js Quick Start の Creating a Vue Application から作成していれば、ビルド成果物が dist 以下に生成されるので、成果物を配信できるように記述します。 詳細な記述方法は こちら をご覧ください。 今回は以下のような yaml を記述しました。 service : your-service-name runtime : nodejs16 handlers : - url : / static_files : dist/index.html upload : dist/index.html secure : always - url : /(.*) static_files : dist/\1 upload : dist/(.*) secure : always App Engine で配信するものの構成を app.yaml としてリポジトリ内に置いておきます。 .gcloudignore の設定 続いて、App Engine でデプロイしないフォルダ類を指定します。 こちらもプロジェクトに合わせて適宜調整をしてください。 詳細な記述方法は こちら をご覧ください。 今回は以下のように記述して、ビルド成果物以外はデプロイしないようにしています。 * !dist/** これもリポジトリの一番上に .gcloudignore として置いておきます。 Cloud Build のトリガーを設定する 続いて、GitHub と Cloud Build の連携をします。 Cloud Build のダッシュボード から トリガー にすすみ、 トリガーを作成 を選択。連携したいソースを選択して認証、接続したいリポジトリを決めます。 リポジトリ決定後はトリガーの作成に移り、どのブランチにどのトリガーでビルドを開始するかを決めておきます。 今回は以下のようなトリガーを設定しました。 名前: TestTrigger リージョン: グローバル(非リージョン) イベント: ブランチにpushする リポジトリ: test-repo ブランチ: ^main$ 構成 形式: 自動検出 ブランチ名はサジェストが出ますが、正規表現で安全に指定しましょう。 cloudbuild.yaml の構成 最後に、Cloud Build で用いる yaml ファイルを作成します。 Vue.js のプロジェクトなので、npm でパッケージをインストールした後ビルド、テストを実行して App Engine にデプロイを行うコマンドまでを自動実行するように記述します。 yaml は、行いたいことをステップごとに書いていきます。 各ステップでは、 name で Docker イメージを指定します。指定できるのは 公式にサポートされているイメージ や、 Container Registry で管理されているイメージなどが指定できます。 node イメージは entrypoint が yarn と npm が設定できるようになっているので、必要に応じて使い分けましょう。今回は npm で設定しています。 あとは、コマンドラインで入力するような引数を args に渡してあげれば、ステップの記述は完了です。 詳細な記述方法は こちら をご覧ください。 今回は以下のような記述になりました。 steps : - name : node entrypoint : npm args : [ "install" ] - name : node entrypoint : npm args : [ "run" , "build" ] - name : node entrypoint : npm args : [ "run" , "test:unit" , "run" ] - name : "gcr.io/cloud-builders/gcloud" args : [ "app" , "deploy" , "app.yaml" , "--project" , "projectname" , "--quiet" ] このような yaml を記述し、 cloudbuild.yaml としてリポジトリ内に置いておきます。 確認 いよいよ main ブランチへ push ... の前に、ディレクトリ構造を確認しておきます。 ここまでのステップで以下のような構造になっていれば大丈夫です。 (重要じゃないところは省いています) project root L src L (Vue Application のソース) L (dist) L (なくてもOK。 手元でビルドをするとここに成果物が出てると思います。) L .gcloudignore L app.yaml L cloudbuild.yaml L package.json L package-lock.json 完成! ここまで設定をすれば、実際に GitHub で main ブランチへ push, merge を行うと、これらが自動で実行され、数分でデプロイまで完了しているのが確認できると思います。 お疲れ様でした。 まとめ Cloud Build + App Engine を使って、CI/CD パイプラインを構築しました。 これで、コードの変更だけに集中できますね! みなさんも良き CI/CD ライフを~
アバター
駅メモ!チームエンジニアの id:Eadaeda です。 みなさんシェルスクリプト書いてますか?私は時々書いています。12/2 の記事ではシェルスクリプトのテストを書いてみませんかという話を書きました。 tech.mobilefactory.jp 今回はテストではなく、linter の話です。 シェルの文法はなかなか難しいです。例えばダブルクォートで括るかどうかなどです。 # スクリプト a.sh があるとして $ cat ./a.sh #!/bin/bash echo " [ $1 ] " " [ $2 ] " " [ $3 ] " " [ $4 ] " " [ $5 ] " " [ $6 ] " # 例:引数のコマンド置換をダブルクォートで括るかどうかで動作が変わる $ ./a.bash $( date ) [ Wed ] [ Nov ] [ 30 ] [ 17:06:59 ] [ JST ] [ 2022 ] $ ./a.bash " $( date ) " [ Wed Nov 30 17:07:10 JST 2022 ] [] [] [] [] [] こういった慣れてないと陥りやすい罠はどんなものにもありますが、linter があれば先に気づくことができそうですね。 シェルスクリプトの linter 今回は ShellCheck を linter として使っていきます。例えば先の a.sh を実行するだけのスクリプト b.sh を以下のように書いたとします。 #!/bin/bash ./a.sh $( date ) これを shellckeck にかけると…。 $ shellcheck b.sh In b.bash line 3: ./a.sh $( date ) ^-----^ SC2046 ( warning ) : Quote this to prevent word splitting. For more information: https://www.shellcheck.net/wiki/SC2046 -- Quote this to prevent word splitt... こんな感じで警告してくれます。かんたんな対処方法が書かれていますね。より詳しい内容が知りたい場合は、同時に出力されている URL にアクセスするか、 SCxxxx で gg れば該当のページを探すことができます。 自分はそこそこチェックをするようにしています。意図しない罠を回避したいのもモチベですが、「ええっ!こんな罠が!?」と勉強にもなるのでハッピーです。 まとめ 今回は ShellCheck をかんたんに紹介しました。ぜひお手持ちのシェルスクリプトで試してみてくださいね。
アバター
こんにちは、エンジニアの id:yunagi_n です。 みなさんは JavaScript において、 URL をパースするとき、どの API を使用していますか? もっとも簡単なのは、 URL Interface を使用することだと思います。 今回は、その URL Interface が、 JavaScript の実行エンジンによって挙動が異なることについて書こうと思います。 事前情報 この記事の内容は、以下のバージョンにて確認を行っています。 macOS 12.5.1 Google Chrome 107.0.5304.121 Safari 15.6.1 Firefox 107.0.1 本題 URL Interface は、 各種ブラウザおよび Node.js 上で URL を扱うためのインターフェースです。適当な URL を渡すと、下のように各部分毎に分解してくれて、 URL を元に何かしたいときに便利なインターフェースです。 const url = new URL( "https://example.com" ) console.log(url.protocol) // => https: 今回は、そんな URL Interface について、ブラウザー実装毎による違いについてのお話です。 例えば、最もよく使われている Chromium 系列 (V8) の場合は、例として https://example.com/test?a=b#c のような URL を渡すと下記のような結果を返します。 const url = new URL( "https://example.com/test?a=b#c" ) console.dir(url) /* * URL { * hash: "#c", * host: "example.com", * hostname: "example.com", * href: "https://example.com/test?a=b#c", * origin: "https://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "https:" * search: "?a=b", * username: "" * } */ このような一般的な URL (ここではプロトコル部が HTTP および HTTPS であるものを指す) であれば、どのブラウザーでも同じ挙動をしてくれます。では、例えば FTP のセキュア版のプロトコルである FTPS を含んだ ftps://example.com/test を渡すとどうなるでしょうか? V8 の場合は以下のような結果を返します。 const url = new URL( "ftps://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "", * hostname: "", * href: "ftps://example.com/test", * origin: "null", * password: "", * pathname: "//example.com/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */ 通常の URL を渡した場合と挙動に違いがありますね。ちなみに Firefox (SpiderMonkey 系) でも同じ結果を返してくれます。 では WebKit 系列 (JavaScript Core) ではどうでしょう?答えは以下のようになります。 const url = new URL( "ftps://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftps://example.com/test", * origin: "ftps://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */ それぞれ、 host 部の扱いが異なっているのが特徴です。 V8 は host 部は無かったものとして扱い、 pathname にすべてを含めているのに対し、 JavaScript Core はおそらく私たちがイメージした結果と同じもの、つまりは host 部を example.com として返してくれています。 ではセキュアではない FTP 、つまりはプロトコル部が ftp: の URL を渡すとどうなるでしょうか?答えはすべてのブラウザーで次のような結果になります。 const url = new URL( "ftp://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftp://example.com/test", * origin: "ftp://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftp:" * search: "", * username: "" * } */ これから分かるように、 Chromium 系列と Firefox 系列は、プロトコル部によって、 host および hostname のパース結果が異なります。 では、挙動が異なるのがこれだけだというと、他にも異なる部分があります。 例えば、 hostname に大文字小文字の両方を含む文字列を渡した際の結果は、一般的なプロトコルの場合は以下のようになります。 const url = new URL( "http://ExAmple.COM/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "http://example.com/test", * origin: "http://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "http:" * search: "", * username: "" * } */ しかしそうでないもの、ここでは Web3 の文脈でよく使われている IPFS プロトコルの URL を渡した場合、 Chromium 系や Safari では以下のようになり、 const url = new URL( "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe" ) console.dir(url) /* * URL { hash: "", host: "", hostname: "", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", origin: "null", password: "", pathname: "//QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", port: "", protocol: "ipfs:", search: "", username: "", * } */ SpiderMonkey 系 (Firefox) ではこうなります。 const url = new URL( "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe" ) console.dir(url) /* * URL { hash: "", host: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", hostname: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe/", origin: "ipfs://qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", password: "", pathname: "/", port: "", protocol: "ipfs:", search: "", username: "", * } */ 面白いのが、 href は大文字小文字を区別していますが、 host などその他は大文字小文字を区別せず、すべて小文字で表されています。 結論としては、図にまとめると以下のような挙動をします。 Parse Result \ URL Protocol http / https / ftp ftps ipfs hash すべてのブラウザで挙動は一致 - - host すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる hostname すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる href すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 Firefox で挙動が異なる origin すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる pathname すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる protocol すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 では、すべてのブラウザで挙動を揃えるにはどのようにすればよいのでしょうか? 答えは簡単で、 URL Interface を使用していないパーサーライブラリを使用します。 例としては url-parse などが使用できます。 ただし、こちらも内部で isSpecial という関数で一部プロトコルでのみ、 origin をセットしていたりするので、プロトコルが違えば、 origin に限っては挙動が異なります。 しかし、それ以外についてはどのブラウザ、プロトコルでも共通の動作をしているので、より多くのブラウザで同一の挙動を実現するには、十分ではないでしょうか。 ということで、今回は URL Interface の挙動の違いについて、解説しました。
アバター
こんにちは。モバイルファクトリーでエンジニアをしているまえけんです。 自分の居るチームではスクラムで開発をしていて、自分はスクラムマスターとしてチーム運用をしていました。 が、プロダクトオーナーの退職と組織編成によるチーム人数の増加などにより、スクラムマスターからプロダクトオーナーへ帽子を被り直すことになりました。 そこで、初めてプロダクトオーナーをやってみて何があったのか、気づいたこと、などをまとめようと思います。 突然の別れと新チーム 自分がスクラムマスターをしていた頃、チームは全部で5人居ました。しかし1ヶ月の間に、 新プロジェクトの立ち上げ プロダクトオーナーの退職 組織編成に伴いチームメンバーが5人から9人に増加 という怒涛の変化がありました。特に当時のプロダクトオーナーとは新プロジェクトについて立ち上げから協力して計画を建てていたので、その半ばでの退職ということで突然の別れとなってしましました。 人数も増え、チームとしてはほぼ新しくなり、プロダクトオーナーも不在という状況で、調整をした結果自分がプロダクトオーナーの役割を担うことになりました。 プロダクトオーナーを実践するのは初めてなので、まず何から手を付けるべきなのか迷う状況になりました。 プロダクトオーナーの役割 スクラムについて迷ったら何はともあれ スクラムガイド を見直すのが良いと思うので、スクラムマスターとプロダクトオーナーの役割の定義について見てみました スクラムマスターについて スクラムマスターは、スクラムガイドで定義されたスクラムを確⽴させることの結果に責任を持つ。スクラムマスターは、スクラムチームと組織において、スクラムの理論とプラクティスを全員に理解してもらえるよう⽀援することで、その責任を果たす。 プロダクトオーナーについて プロダクトオーナーは、スクラムチームから⽣み出されるプロダクトの価値を最⼤化することの結果に責任を持つ。 スクラムマスターは「スクラムの確立」に責任を持ち、プロダクトオーナーは「プロダクトの価値を最大化すること」に責任を持つとあります。言い換えると、スクラムマスターは「チームを観察する人」、プロダクトオーナーは「(開発している)プロダクトを観察する人」なのかなと思います。 スクラムマスターはチームを取り巻く問題を見つけて解決するとしたら、プロダクトオーナーはプロダクトを取り巻く問題を見つけて解決(判断)する人だと思いました 。 また、元スクラムマスターとして、もし今の状況の時にプロダクトオーナーにやって欲しい事はなんだろうということも考えてみました。ここは尊敬するマスター・センセイ(アジャイルサムライ)の助言も参考にしてみました アジャイルサムライ――達人開発者への道 作者: JonathanRasmusson , 西村直人 , 角谷信太郎 オーム社 Amazon プロジェクトが新しく始まった時点では、その成功について思い描く姿は人によって大きく異なるものだ。(中略) ここでの問題は、プロジェクトの開始時点で関係者の認識が揃ってないことじゃない(むしろそれは自然なことだ)。問題は、関係者全員でプロジェクトについて話し合うよりも前にプロジェクトを始めてしまうことにある。 チームメンバーが誰も居ないところで合意したことを前提にしているから、プロジェクトがだめになるんだ。 新メンバーが入ってきたという事もあり、プロダクトオーナーとして最初にすべきは「メンバーを集めて認識を揃える」事だと思い、まずインセプションデッキの整備から始めました。 プロダクトオーナーの単一障害点 インセプションデッキも概ね完成してチームとの合意も取れてきて、第1スプリントを始めるぞというタイミングで自分が新型コロナウイルスに罹ってしまい、約1週間チームから離れる事態になりました。 プロダクトオーナーとして判断をしたり、特に新メンバーからの質問に答えるのが途絶えたことで少なからずチーム内で混乱があったみたいです。プロダクトオーナーは単一障害点になりやすいという事を聞いたことがありますが、第1スプリントからそれを実感する事になりました。 認識を揃える難しさ 数スプリントが経過した頃、レトロスペクティブの場で「作っているプロダクトの最終形が想像できない」「今作っているものがどうなるのか不明」というのが開発メンバーから意見が出てきました。個人的な視点ではすでにインセプションデッキも作ってチームと合意もとっていたので、こういう事が出てくるのは驚きでした。 深堀りしてみると、インセプションデッキによってたしかにプロダクトの「Why(なぜ作るのか)」「How(どうやって作るのか)」は可視化できましたが、「What(何を作るのか)」がまだ可視化出来てないと気づきました。特に このプロダクトの楽しさはどこか というのが伝わってない事がわかりました。自分の頭の中にプロダクトの最終形が存在していても、それを示す事も重要である事に気づきました。 まとめ プロダクトオーナーに就任してまだ数スプリントだけですが、ここまでで大事だと感じた事をまとめます。 プロダクトオーナーだけで抱え込まない プロダクトオーナーは単一障害点になりやすい存在です。どうしてもプロダクトオーナーじゃないと判断できない、答えられない事は存在すると思いますが、それ以外については例えばスクラムマスターと協同出来ることを早いうちから始めるべきだったと思います。 今回で言うと、例えば新メンバーのオンボーディングも自分がやっていたのですが、開発に関するオンボーディングは自分から棚卸しできたかもしれないです。プロダクトオーナーじゃないと出来ない仕事以外はなるべく協力していきたいと思います。 プロダクトの方向性を示し続ける インセプションデッキをチームで作った当時は概ねプロダクトの方向性を共有することが出来ましたが、作って終わりでは良くないことがわかりました。 マスター・センセイも話している通り、認識が揃わないことは自然なことで、時間経過でそれは発生し続けると思いました。プロダクトオーナーとしての仕事の1つにプロダクトの方向性を示すということがありますが、常にチームと対話すること等で方向性を 示し続ける も必要だと思いました。 現在ではプロトタイプを作ることで、まずプロダクトの最終形をざっくり作って認識を揃える(対話を促す)、新しいアイディアを試す、ということをしようと思ってます。
アバター
🎄モバイルファクトリー Advent Calendar 2022!毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします! こんにちは。駅メモ!シリーズでデザイナーをしている19卒入社の @watagisan です。 アドベントカレンダー初投稿ということで、新卒デザイナーとして、「どのように行動すると入社して早い時期からいい感じに会社に貢献できるか」について個人的に意識していた「心構え」のまとめを書きました。 こころとからだのためにたいせつなこと 睡眠はたいせつ IT関係の企業では、どちらかというと頭を使うお仕事が多いのでしっかり休まないと日常業務に支障をきたします 作業時間(残業時間)ではなく成果を自慢しよう 自分の扱いがおかしいなと感じたらすぐに誰かに相談する 上司もチームももちろん僕もあなたも、人間なのでチームメンバーとの相性が悪かったりする場合がどうしてもあります 上司やチームからの扱いが何かおかしいなと感じたら、身近な人にすぐに相談 わからなければすぐ質問 「Aさんに聞きたいけど忙しいかも…?」→忙しければそう言ってくれると思うので、聞いたほうがいいです 「まず自分で考えるべきかも…?」→自分で考えるべき疑問であればそう促すと思うので、聞いたほうがいいです 知識がまだ乏しい段階で「自分で考えるべき問題か」「質問すべき問題か」を判断するのは難しいです 上司に質問されることに怯えなくてもよい 上司や先輩からの質問は詰問や叱責の意図を伴っていない なんとなく「怒られているのかな」と勘違いしないこと 例えば「ここのストライプのデザインはどういう意図をもってストライプにしたの?」と言われた時 × ストライプじゃ変だからマシなデザインにしろ! ○ 数多あるパターンからストライプを選んだ理由はなんだろう?ストライプは適切だろうか? アウトプットをしよう ここでいうアウトプットとは、自分が知り得た知見や技術などを他の人が利用できるように社内向けのドキュメントとしてまとめておくことです 「自分の持っている知識なんてみんな知ってるだろうしアウトプットしても意味ないかも…」というのは間違いです 「自分が思いつくようなことはほとんど誰かが先に思いついている」のは正しいです ですが「思いついたことを文字や図にアウトプットしている」かどうかは別 たとえ被っているところがあったとしても、一字一句同じになることはないのでアウトプットする意味はあります 「自分が新卒で経験したことなんてみんな知ってるしドキュメントにしてまとめておく必要なんかないか…」 →あなたの経験とみんなの経験は当然異なるので、あなたの経験や感想を書き留めておくことは今後の新卒の糧になります おしごとをする上での行動の心構え 組織の人間であることを意識して責任を持つ デザインに限らずですが、社会人として・組織の人間として恥ずかしくない立ち振る舞いが求められます なにか不祥事があると会社の名前に傷がつき、それは社員全員の社会活動に影響します あなたがデザインしたものは、「会社の制作物」として世の中に認識されます 制作を進める時に、それはリリースして問題ない表現かを考えておくとよいです あなたの行動によって起きた不利益は会社が被ります 主体性を持つ お仕事を任せる時、全然やる気がなさそうな人よりも実績のある人ややる気がありそうな人に任せたくなります なので自分から手を挙げて仕事を取りに行くような姿勢だと、お仕事も成長のチャンスも多くなります 社風にもよりますが、実績がなくても新人ならお仕事を任せてもらえる風土があれば主体的になりやすいです 常に積極的に手を挙げる姿勢が早いうちに身につくと、どんどんお仕事を任せてもらえて実績も増え、スキルもどんどん向上します 当事者意識を持つ 何か問題が起きた時に、問題解決のために自分から動こうとする姿勢を見せると、周囲からの信頼は厚くなります 「自分には関係ない事だから…」とか、「新卒の自分が出る幕じゃないな…」と思う人には会社としては問題解決を任せたくないです 何か自分にできることはないかとりあえず考えるようにしておくと、自分にもできることがあった時にすぐ動けるようになります スピード感を持って動く 例えば何か作業を頼まれた時、他に特別優先すべき事項がなければ、先にその頼まれた仕事を終わらせて報告した方が周囲からの評価は上がります 他の重要なタスクや業務時間を犠牲にしてまで先にやれというわけではないですが、もし順番を前後しても大丈夫なら先に終わらせてしまった方が評価されるし信頼もされやすくなるのでのちのち自分にとって得になると思います もちろん仕事のスピードとクオリティの両方を満たすのがベストなんですが、クオリティ面で未熟な新卒のうちにチームに貢献するにはスピード重視に振り切ってしまってもよいと思います すばやく作業していろんな人に見てもらって、より多くのフィードバックをもらって修正していく方が時短になります (なるべく) 早めに呼びかけに応じる (できれば) オフィスにいるときはデスクで仕事をしていることが見えますが、リモートワークだとそれが見えないのでチームメンバーがそれぞれ何をしているのか(忙しいのか暇そうなのかすら)わかりません なので返事が遅いと「ちゃんと仕事してるのかな…?」とか「無視されちゃったのかな…」とか心配になります (これは信用していないわけではなく、関係が浅い新卒のうちはどうしても不安に感じてしまうという話です) なので呼びかけがあった時などはなるべく早く応答するようにすると、周囲は「この人には安心して仕事を任せられるな」と感じます ミーティング中だったりとっても忙しいときは「あとで確認してお返事させていただきます!」とか「ミーティングの後にお返事します!」とかとりあえず見たよ!ということを伝えてあげると声をかけた人は安心できます 業務をする上での心構え 「会社、チーム、上司が自分に期待していること」が何かを考える (新卒に限らずですが) 自分が今周囲から何を期待されているのか考えてみる機会を設けるとよさそうです 何を期待されて採用されたのかを考え、その期待になるべく応えられるように動くと評価は上がりやすいと思います 逆に何を期待されているのかの認識を間違えて、期待されてないことを頑張ってしまうとせっかく頑張ったのに評価されなかったり、仕事ができない人だと思われてしまったりしてかなしいです 考えてもわかんないよ〜!というときは直接上司やメンターの人に聞いてしまってよいと思います ミスしたらすぐに正直に報告する ミスが後から発覚して大ごとになるより、ミスをすぐに共有してみんなでリカバリできた方が会社としての損害は少なくなります なので、やらかしたら気付き次第すぐに誰かに報告しましょう 例えば… 「すみません、Googleドライブ上の○○のデータを誤って削除してしまったのですが、どうすればよいでしょうか?削除してしまったデータは○○、削除してしまったデータがあったドライブのURLは○○です」などなど わからないことはすぐに聞く 仕様、納期、要件、デザインの方向性などなど 入りたての頃はわからないことが多いのは会社の人たちもみんな知っているので、忙しいかもしれないし…とか思わず遠慮せずに聞くとよさそうです 誰に聞けばいいかわからない…というときは、「@わかる方お願いします」とかで聞いたら誰かが適切な人に繋いでくれると思います わからないまま1人で考えて業務時間を浪費するよりも、頻繁に質問して仕事を少しでも進められる方が会社としては助かります 質問するときは回答しやすいように質問する 初めのうちは何もわからないので難しいかもですが、聞かれた側がすぐに返事しやすい質問だとお返事が早く返ってきやすいと思います 例えば…「質問するときに自分なりの答えを添えて質問する」 「お疲れ様です、 自分の手持ちの作業についてです。 指示されていた画像制作の作業が終わったのですが、次に作業した方が良い作業などありますか? 自分としてはこの作業(タスク情報のURLなど添えつつ)が納期近そうかつ自分でも対応できそうなので、次はこれを作業しようかなと思いますがどうでしょうか? 」 という感じで ①なにについての質問かわかるように前置きをする(相手が忙しかった場合に緊急で返事した方がいいか後でもいいかの指標になります) ②質問したい内容を具体的に書く(必要に応じてURLなどあるとなおよいです) ③質問に対する自分の意見を添える(意見が合っていれば「それでいいよ!」で返事が済みますし、間違っていれば相手も何が勘違いの原因になっているのか把握しやすいです) 例えば…「関連情報を提示して適切な回答を引き出す」 「お疲れ様です、 こちらの画像制作に関して、制作で使用する キャラクターの素材画像を探しているのですが、どこにあるか教えていただけませんでしょうか? Googleドライブ上で検索したのですが見当たりませんでした。 こちらの画像制作要件は以下のURLです。 」 という感じで ①なにについての質問かわかるように前置きをする ②質問したい内容を具体的に書く ③質問前にどう行動したかを添える(なぜ質問をするに至ったのかがわかるので、回答する側もどんな情報を提供すればいいか把握しやすいです) ④関連する情報を示す(もしそもそもの質問が間違っていた時に指摘できるので、自分が知り得る関連情報は全て提示しておくと認識の齟齬が少なくなります) さいごに 個人的な心構えなので誰しも参考になる内容ではないと思いますが、入社して困った時に役に立ったらうれしいです。
アバター
こんにちは。駅奪取エンジニアの id:dorapon2000 です。 コード差分の大きなプルリクエスト(以下、プルリク)をコードレビューした経験は多くの方があると思います。 プルリクは小さく・単位ごとに、とは頭でわかっていても、実装している内に想定よりも大きくなってしまったり、1つのプルリクにまとめなければコンテキストが伝わらなかったり、どうしてもということはあります。本当に申し訳ないと思いつつレビュー依頼を出したり、出されたり。 今回は、巨大なプルリクを前にして、自分がどうモチベーションを保つか、どう読み解いていくか、同じ状況を避けるためにレビューイと協力できることはなにか、について自分の場合を例に紹介していきます。 プルリクは小さく ここでは巨大なプルリクは避けるべきだという前提で記事を進めていきます。 つまり、プルリクは可能な範囲で小さい方がよいという前提です。 その理由は、コードレビューに関する優良な記事が多くあるため、そちらに譲ります。 また、どれほどで巨大かは主観で大丈夫です。レビュアーの心のソウルジェムが濁り始めたら、この記事が役に立つかもしれません。 リスペクトを忘れない 自分はレビューイに対するリスペクトがコードレビューで一番大切だと考えています。それは巨大なプルリクでも同様です。 悪意があってプルリクを大きくしたわけではありませんし、実装も大変だったはずです。 よりよいコードへするために協力するという気持ちとねぎらいを忘れないようにしています。 読み解く3つの選択肢 巨大なプルリクには様々なコンテキストが埋め込まれており、レビュアーはApproveする前にそれらを理解する必要があります。 理解にかかる時間と体力が、頭を重くする理由です。 モチベーションを保つためにも、理解しやすくするためにも、自分は3つの選択肢を考えます。 プルリクを分割してもらう 会話をしながらレビューイに説明してもらう 簡単なもの⇒メイン処理の順に読む 1. プルリクを分割してもらう プルリクが大きいならば、分割することで1つあたりの負担も小さくなります。 まず一番最初に考える選択肢ですが、同じことはレビューイも考えており、たいてい分割できません。 現状から分割するには余計に実装コストがかかったり、そもそも分割するほうがレビューが大変になる場合です。 2. 会話をしながらレビューイに説明してもらう 弊社は完全リモートなため、通話をしつつ画面共有してもらいながら、プルリクの各差分の意味を説明してもらいます。 会話のメリットは、なにより心理的な負担が低いことです。自分でコードを追わずとも、何をする箇所に、どういう変更を、どういった意図で入ったのかを理解できます。 プルリクが巨大なため、疑問点も多く出てくるでしょう。それも通話であれば即座に解決します。このスピード感は非同期でするレビューにはない魅力です。 もちろん、通話している分だけレビューイの時間も必要です。しかし、通話のあるなしで両者がレビューに掛ける合計時間の差はないように感じます。 会話へ入る前に、レビューイは事前解説コメントをプルリクエストに残したり、レビュアーはプルリク全体を眺めておくことで、よりスムーズな会話になります。 また会話後も、会話の内容をプルリクエスト内に記載しておくことで、実装意図を見返す際や他のチームメンバーへ共有する際に役立ちます。 3. 簡単なもの⇒メイン処理の順に読む 3つ目の選択肢は、プルリクを地道に読みます。 レビューイと都合がつかないなどの、会話の選択肢が取れない苦肉の策だと思っています。 自分の場合、小さいプルリクでは表示されているファイルの上から順に見ます。プルリクが小さいため、それでも内容を把握できます。大きなプルリクではそうはいきません。まず、独立した簡単なものから見ていきます。これは、メインの処理にある最も複雑なコンテキストを理解する際のノイズを最初に除去するイメージです。 CSSの簡単な変更だったり、単純な変数名の変更だったりです。 ノイズの除去後は、メイン処理をプログラムの実行順に上から読んでいきます。 過去に、実装するときは上から順に、レビューするときはバラバラ、という時期がありました。しかし、レビューするときも上から順に読むほうが理解しやすいです。 将来の巨大なプルリクエストを避ける ここまででレビューは終わりです。 最後に、巨大なプルリクエストを分割する方法があったかを、レビューを通して得られたプルリクの理解をもとに考えます。 何度も言いますが、大きなプルリクエストのレビューは大変です。その体験を通して、どうすれば分割できるのかノウハウが溜まっていくはずです。 ノウハウをレビューイにも共有して、適切な粒度のプルリクエストをチームの文化にしていきます。
アバター
こんにちは、駅奪取チームエンジニアの id:kebhr です。 駅奪取チームでは Pull-Request を本番環境に反映する前に Jenkins を用いてフルテストを実行しています。 手順としては Jenkins をキックするシェルスクリプトを使い、開発環境で次のようなコマンドを実行します。 cd $PROJECT_ROOT # カレントブランチのテストを実行する ./jenkins # or # 特定のブランチのテストを実行する ./jenkins ${branch_name} しかし、このスクリプトは push 忘れのチェックを行っておらず、 ブランチ・コミットを push し忘れた状態でフルテストを走らせてしまう という問題点がありました。 push 忘れに気付いたときに push し、フルテストを走らせれば問題ありませんが、フルテストを二度走らせることになるためリードタイムが長くなります。 そこで、ローカルに push していないコミットが存在する場合は、フルテストを実行せずにエラーを表示するようにスクリプトを変更しました。 全体像 #!/bin/zsh set -ue # 中略 # 引数またはカレントブランチを取得し $BRANCH にブランチ名を格納 if ! git show-branch remotes/origin/ $BRANCH > /dev/null 2 >& 1 ; then echo " このブランチは push されていません " exit 1 fi if ! git diff remotes/origin/ $BRANCH .. $BRANCH --quiet --exit-code ; then echo " push されていないコミットがあります " exit 1 fi # Jenkins API をキック ブランチの push 忘れを判定する if ! git show-branch remotes/origin/ $BRANCH > /dev/null 2 >& 1 ; then echo " このブランチは push されていません " exit 1 fi git show-branch <branch> は、引数に与えたブランチが存在すれば終了コード 0、存在しなければ 128 を返します。 ブランチの push 忘れを判定するためには、origin にそのブランチが存在することを確認すればよいので、引数に remotes/origin/$BRANCH を与えます。 なお、この実装では、fetch していないブランチのテストを実行しようとした際に「このブランチは push されていません」と表示されエラー終了しますが、チーム内でこのような状況には直面しないため許容しています。 コミットの push 忘れを判定する if ! git diff remotes/origin/ $BRANCH .. $BRANCH --quiet --exit-code ; then echo " push されていないコミットがあります " exit 1 fi git diff A..B --quiet --exit-code は A と B の間に差分が存在すれば終了コード 1、存在しなければ 0 を返します。 コミットの push 忘れを判定するためには、リモートとローカルの差分を確認すればよいので、引数に remotes/origin/$BRANCH..$BRANCH を与えます。 git diff は > /dev/null 2>&1 といったコマンドを用いずとも、 --quiet オプションによって出力を抑制できます。 --exit-code オプションは結果を終了コードでも返すオプションです。しかし --quiet オプションを指定すると暗黙的に --exit-code オプションが有効になります。よって --exit-code オプションは省略できますが、コードの理解を容易にするために記述しています。
アバター
id:nesh です。 今回の記事では駅メモ!エンジニアで定期的に開催している社内勉強会「Denco Tech Night」について紹介したいです。 Denco Tech Night について この勉強会は 2017 年から始めました。 社内勉強会が促進されている環境であるため、その社内勉強会の 1 つとして駅メモ!エンジニアチームによる勉強会です。 開催概要 参加者は駅メモ!エンジニアが参加必須です。 また、他チームのエンジニアも任意で参加できます。(現状は駅奪取チームのエンジニアも定期的に参加しています。) 発表する内容の絞りは特になく、業務で得た学びや個人的に勉強したこと、チームでの取り込みの共有などがあります。 駅メモ!チームの人数は多いため、この勉強会のタイミングで他の人が開発したツールや、業務上の Tips などが共有されたりします。 今まであった発表のタイトル例です。 Web Component 触ってみた AstroNvim で Neovim をはじめました アクセシビリティと WAI-ARIA について Android プロジェクトのビルドを理解して速くする Perl5.36 の変更点 開催頻度は隔週で行っています。 開催を始めた時に参加するメンバーを決めて、一通り全員が発表し終わったタイミングを一区切りにして、次の開催頻度や時間帯を検討したりします。 開催目的 Denco Tech Night を開催する目的は次のとおりです。 「人前で説明する」練習 ニーズの理解と解決の訓練 技術的知見の理解・共有 ブランディング それぞれの目的について補足します。 「人前で説明する」練習 社外勉強会に参加して発表するハードルを下げるために、社内でも発表できる機会を設けて、人前で説明することを慣れてもらう目的です。 ニーズの理解と解決の訓練 開催概要にも書いたように発表する内容の決まりはありません。 各々が発表する内容を検討するとき、今のチームにとってどういった発表の需要があるかを元に発表のネタを決めることもあります。 Denco Tech Night でそういった訓練をします。 技術的知見の理解・共有 言葉通りでチーム内の他の人との技術的知見の共有をしたり、発表を通してチーム内の理解度を同レベルに持っていくための場として使えます。 ブランディング 発表した内容によって、その人は何が得意かとかがわかります。 企業や会社のブランディングの意味に近いですが、勉強会を通じて、社内環境で自分自身のブランディングができます。 勉強会を継続するための工夫 2017 年から継続して Denco Tech Night が開催できたことに対して、どういった工夫があったかを言語化してみました。 発表準備の手間を減らす Denco Tech Night の発表形式は自由です。ほとんどの人は Docbase *1 に記事を書いて、発表時はその記事を画面共有しながら発表しました。 業務時間内に勉強会の発表資料を作成できるように 発表資料作成には時間が必要です。発表資料を作成するハードルを下げるために、業務時間内で Denco Tech Night 資料を作っても良いことにしました。 本来の開発業務に状況次第ではリスケ可能 業務上で忙しいタイミング時に発表順が来たり、緊急のトラブル発生がしたりのように、さまざまな原因で発表が難しくなることもあるので、リスケはできます。 他の人と発表順を交換してみて、ダメなら発表をスキップする形でリスケされます。 まとめ 以上が駅メモ!エンジニアによる社内勉強会「Denco Tech Night」 の紹介でした。 また、勉強会を継続できるための工夫をまとめてみました。 *1 : 社内で標準利用している情報共有ツールです。
アバター
こんにちは。駅奪取チームエンジニアの id:dorapon2000 です。 よくシェルスクリプトのIF文に >/dev/null 2>&1 を書いて条件文とすることはありませんか。実行結果の成否をIF条件として利用したいのであって実際に出力したいわけではないケースです。 実は身近なコマンドでもオプションで出力を抑制できます。3つ紹介します。 検証環境 grep --quiet git diff --quiet apt list -qq aptの警告について 検証環境 Ubuntu 16.04 bash 4.3.48 grep --quiet 特定の文字列を含むときにだけif文を実行します。 まず、 >/dev/null 2>&1 を使う書き方。grepに引っかかると0を返し(True)、grepに引っかからないと1を返します(False)。 if cat /var/log/syslog | grep " Error " > /dev/null 2 >& 1 ; then echo ' Found ' fi --quiet オプションを使うことで、出力を抑制してくれます。 if cat /var/log/syslog | grep --quiet " Error "; then echo ' Found ' fi # -q でも可 if cat /var/log/syslog | grep -q " Error "; then echo ' Found ' fi 以下のサイトでは、 --quiet を使うことで高速化もするという嬉しい情報もあります。 シェルスクリプトでファイルに特定の文字が含まれているかどうかを高速に判定する方法 | ゲンゾウ用ポストイット git diff --quiet git差分があるときにだけif文を実行します。 まず、 >/dev/null 2>&1 を使う書き方。 git diff は差分のありなしに関わらず正常終了の0を返すため、ifで使うには --exit-code オプションが必要です。差分があるときに1を(False)、差分がないときに0を返すようになります(True) # ① --exit-code を使う場合 if ! git diff --exit-code > /dev/null 2 >& 1 ; then echo ' Diff ' fi あるいは --exit-code を使わず以下のように書く人もいるでしょう。 # ② [を使う if [ `git diff | wc -l` -gt 0 ]; then echo ' Diff ' fi # ③ bash構文を使う if [[ `git diff | wc -l` > 0 ]] ; then echo ' Diff ' fi さて、git diffにも --quiet オプションがあります。さらに --quiet オプションは暗黙的に --exit-code オプションを利用しており、一石二鳥です。 if ! git diff --quiet ; then echo ' Diff '; fi 非常にスッキリしましたね! apt list -qq aptで特定のパッケージをインストールしていなかったらインストールしたいことは多いです。 まずは、前述した grep --quiet を使うやり方から。 if apt list --installed sl | grep -q sl ; then sudo apt install sl -y fi これでいいじゃん!ですが、aptの -qq オプションでも同じことができるため紹介します。 aptの -qq オプションは --quiet オプションをより強力にしたもので、進捗情報を消してくれます。ログ出力のためにあるオプションのようです。完全に出力を消すものではありません。 $ apt list --installed sl 一覧表示... 完了 # <-- これが消える sl/xenial,now 3 .03-17build1 amd64 [ インストール済み ] $ apt list -qq --installed sl sl/xenial,now 3 .03-17build1 amd64 [ インストール済み ] したがって、最初のif文は以下のように書き換える事ができます。 grep ^ でパイプで渡される出力が空かどうかを判定します。 if ! apt list -qq --installed sl | grep -q ^ ; then sudo apt install sl -y fi aptの警告について 上記例を実行すると警告が表示されます。 WARNING: apt does not have a stable CLI interface. Use with caution in scripts. 書かれているとおりで、aptは出力フォーマットが安定しているわけではないため、出力結果を加工するスクリプトはあまりよろしくないようです。 可能なら別のコマンドを利用したほうがよく、 apt list --installed に関しては dpkg -l で代替できます。 if ! dpkg -l | grep -q sl ; then sudo apt install sl -y fi
アバター
こんにちは、ブロックチェーンチームのエンジニア id:charines です。 この記事ではJavaScriptにおける unhandledrejection がどのような条件で発生するのかをクイズ形式でまとめています。 unhandledrejection とは unhandledrejection はエラーハンドリングされていない Promise が拒否されたときにグローバルスコープに送られるイベントです。 developer.mozilla.org unhandledrejection が発生したときの挙動は環境によって異なりますが、例えばNodeJSではプロセスが強制終了するため、気づかぬうちに発生させないよう注意が必要です。 では具体的にどのような状況で unhandledrejection が発生するのかをクイズ形式で見ていきます。 クイズ 次のコードを実行したとき unhandledrejection は発生するでしょうか。 (各問題のコードはNode v18.12.1にて検証しています) 問1 Promise.reject(); 解答・解説 発生する Promise.reject は拒否された Promise を返す関数です。エラーハンドラがないため unhandledrejection が発生します。 問2 Promise.reject() . catch (() => {} ); 解答・解説 発生しない Promise.prototype.catch はエラーハンドラを設定する一般的な方法です。今回は空の関数を渡しているため、式全体は undefined に解決します。 問3 Promise.reject() .then(v => v) .then(v => v, () => {} ); 解答・解説 発生しない Promise.prototype.then の第二引数は Promise.prototype.catch でエラーハンドラを設定するのと同じ働きをします。 また、ハンドラはPromiseチェーンを辿って呼び出されます。 問4 const f = async () => { const p = Promise.reject(); await sleep(); p. catch (() => {} ); } ; f(); ※ sleep は一定時間後に解決する Promise を返す関数で実装は以下です。 import { setTimeout } from "timers/promises" ; const sleep = () => setTimeout(50); 解答・解説 発生する 4行目でエラーハンドラを設定していますが、2行目の時点で処理が開始されるため、 p はエラーハンドラの設定より先に拒否されてしまいます。 ハンドラは Promise の作成直後に設定するべきです。 問5 const f = async () => { await Promise.reject(); } ; f(). catch (() => {} ); 解答・解説 発生しない await で待った Promise が拒否された場合、 await 式はその値で例外を発生させ、 f() が返す Promise は拒否されることになります。4行目で f() に対してハンドラを設定してるため、この場合 unhandledrejection は発生しません。 2行目の await がない場合は問1と同じ状況なので unhandledrejection が発生します。 問6 const f = async () => { const p = Promise.reject(). catch (e => { throw e } ); await sleep(); await p; } ; f(). catch (() => {} ); 解答・解説 発生する エラーハンドラの中で例外を投げた場合、 catch はさらに拒否される Promise を返します。 また、4行目で await をしていますが、それより前に p は拒否されてしまうので unhandledrejection が発生します。 問7 const f = async () => { const p = Promise.reject(). catch (e => e); await sleep(); throw await p; } ; f(). catch (() => {} ); 解答・解説 発生しない エラーハンドラによって p は拒否されずエラーの値に解決します。 非同期関数内で例外を投げる場合は、問5の await で待った Promise が拒否されるパターンと同じで、 f() の返す Promise が拒否されます。これは6行目でエラーハンドラが設定されているので unhandledrejection は発生しません。 最後に 7問の Promise を使った簡単な実装を用いて unhandledrejection が発生する条件をまとめてみました。 特に問6のように拒否され得る Promise を作成して後から await するというパターンで unhandlerejection が発生してしまうようなケースは注意すべきだと思います。 参考文献 PromiseのUnhandled Rejectionを完全に理解する 日本語の記事でECMAScriptの仕様を読み解いています
アバター
こんにちは、エンジニアの id:kaoru-k_0106 です。 CloudFront で gzip 圧縮を有効にしたところ転送量が減ったのはもちろんですが、予想外にリクエスト数も減ったため、理由が気になって調査して記事にしてみました。 背景 駅奪取シリーズは、2022 年現在もフィーチャーフォン(ガラケー)をサポートしており、非常に機能が限られたブラウザへの対応が必要となります。 そのため、gzip 圧縮や Accept-Encoding ヘッダに対応していない端末があったときのため、当初 CloudFront の gzip 圧縮を有効にしていませんでした。 後に検証して、問題ないことがわかったため有効にしたのですが、その際、転送量が減ったのはもちろんですが、リクエスト数もなぜか減少しました。 リクエスト数も課金の対象ですのでありがたいのですが、理由を考えたところ、gzip 圧縮されたままキャッシュすることで、ディスクキャッシュに保存できるファイル数が増えたのではないかと考えたため、検証してみました。 Chrome のディスクキャッシュはどこにある? 手元の Mac の場合以下のディレクトリにありました。 ~/Library/Caches/Google/Chrome/Default/Cache/Cache_Data なお、プロファイル(= Chrome のユーザ)ごとに分かれており、 Default の部分を Profile 1 などに変更することで各プロファイルごとのキャッシュを見られます。 ファイル名はハッシュのようで中身はわからないですが、このように大量のファイルが入っています。(念の為ファイル名にはモザイクをかけています) キャッシュされてるデータを見てみる この画像がディスクキャッシュされてるようなので、キャッシュから掘り起こしてみましょう。 探し方が分からなかったので、試しに URL で grep してみることにします。 $ find . -type f -print | xargs grep 'https://static.ekidash.com/v16378249952/img/portal/pc/description.png' Binary file ./xxxxxxxxxxxxxxxx_x matches Binary file ./xxxxxxxxxxxxxxxx_x matches それらしきファイルが 2 つヒットしましたが、2 つともバイナリファイルのようなので、バイナリエディタを使って開いてみます。 今回は VS Code のバイナリエディタ拡張機能である Hex Editor で開いてみました。 PNG のファイルヘッダが見えることから、レスポンスをそのままキャッシュしている可能性が高そうです。 参考: https://ja.wikipedia.org/wiki/Portable_Network_Graphics gzip 圧縮されたファイルのキャッシュを探す 続いて gzip 圧縮されたレスポンスを探してみることにしましょう。 js ファイルは CloudFront で gzip 圧縮されるので、portal.min.js をキャッシュから探してみましょう。 $ find . -type f -print | xargs grep 'https://static.ekidash.com/v16536443652/js/dist/production/portal.min.js' Binary file ./xxxxxxxxxxxxxxxx_x matches Binary file ./xxxxxxxxxxxxxxxx_x matches 見つかったファイルを開いたところ、ファイルヘッダが gzip になっています! よって、Chrome ではキャッシュが gzip のまま保存されることがわかりました。 参考: http://openlab.ring.gr.jp/tsuneo/soft/tar32_2/tar32_2/sdk/TAR_FMT.TXT Chrome のキャッシュの最大サイズは? 続いて、ディスクキャッシュの最大サイズを調べたところ、こちらの Q&A サイトに似たような質問がありました。 https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit Chromium(Chrome のベース)だと一般的に 10% of the available disk space (使用可能なディスク容量の 10%)とのことです。 このことから、機種依存でありますが 1 ファイルあたりのサイズが小さくなると、より多くのファイルがディスクキャッシュされるようになりそうです。 しっかり検証するのであれば比較実験をしたほうが良いのですが、今回の調査はいったんここまでとします。 Safari のキャッシュについて少し調べた ブラウザエンジンが異なる Safari だとキャッシュ周りの仕様が違いそうなので、こちらも少し調べてみました。 開発者ツールで確認したところ、リロードしたときはメモリキャッシュが使われましたが、再起動した場合はディスクキャッシュが使われました。 キャッシュの有効期限はどうなってる? 今回 Cache-Control ヘッダの設定をしていなかったのですが、キャッシュされたのはなぜだったのでしょうか? 調べたところ、以下のブログ記事でこう触れられていました。 一般的には Last-Modified ヘッダの日時と Date ヘッダの日時の差の 10%の値を有効期間として定めることが多いと RFC7234 に記載されています。 引用元: Cache-Control ヘッダがないときもブラウザがキャッシュする! そのため、とくに設定しなくてもキャッシュしてくれるようですが、Cache-Control でキャッシュする日数を明示したほうが良さそうですね。 なおキャッシュする場合は、キャッシュバスティングする仕組みを入れておきましょう。 まとめ 断言はできませんが、リクエストが減った理由として、gzip 圧縮によってファイルサイズが小さくなることでキャッシュされるファイル数が増えたからだと考えられます。 そして、バイナリエディタはこんなときにも役立ちますね。
アバター
こんにちは! モバファクで採用担当として働く @overallfactory です。 毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします。今回の記事では、「良いモノ」=「良い組織」と捉えてエンジニア採用について紹介します。 具体的に紹介していくのはカジュアル面談について。 私が面談時にどのような準備を行い、どのような時間の使い方をしているのかについて記事にまとめました。 近年では、現場で働くエンジニアもカジュアル面談に関わることが増えてきているかと思います。個人の考えではありますが、少しでも参考になれば、嬉しいです。 カジュアル面談とは? カジュアル面談とは、選考の前に求職者と企業の担当者が情報を交換し合う場のことです。 企業側としては、求人の詳細や企業情報についての説明を行い、求職者に興味を持ってもらうことを目的としています。 注意しないといけないのは、選考ではないということ。決して合否を判定する場にしてはいけません。 モバファクの採用では、基本的に面接前にカジュアル面談を実施しており、会社について理解していただいた上で選考に進んでいただいております。 カジュアル面談の心構え カジュアル面談を行うに当たって、特に大切にしている心構えについて3つ紹介します。 一見、非効率的と感じるかもしれませんが、5年以上の採用経験をする中で、効率的な採用において非常に重要な考え方だと思っています。 ①カジュアル面談では興味を獲得すべし! カジュアル面談では「候補者に魅力を伝え、選考に進んでいただく」ことを第一に考えています。 モバファクは上場企業とはいえども、名前を聞いたことがないという人はとても多いです。 だからこそ、最初の面談は超重要。1時間程度の面談で「面談/面接に何度でも足を運びたい!」と思ってもらわないといけません。 面談をする限りは色々と聞いてみたいことがあると思いますが、まずは全力で宣伝を行い、「とりあえず受けてみよう!」と求職者に思ってもらいましょう。話はそこからです! ②嘘や適当な発言はしない わからないことは「わからない」と回答をすることも大切です。 私はエンジニアではないので、込み入った技術の話には答えられません。 回答が難しい質問があれば、「わからない」と正直に答えた上で、「メールで返答する」「別途エンジニアと面談の機会を作る」など真摯に対応をすることを心がけています。 その場を誤魔化そうと、知ったかぶりをしても、信頼を失うだけです。 ③会社の魅力を理解すべし 自社のどんなカルチャー/事業が他社に比べて秀でているかを常に考えましょう。 例え優れた制度やカルチャーが自社にあったとしても、求職者が見ている企業でも同様に存在するものであれば、伝えてもあまり効果はありません。 せっかく魅力を伝えるのであれば、求職者の人に「おっ!今までになかった魅力的な企業だな!」と、思ってもらいたいものです。 短い時間で効率的に自社の魅力を伝えるためにも、常に他社の状況に目を向け、自社がどういう強みを持っているのかを客観的に知ることがとても大切です。 カジュアル面談前に準備すること 「面談/面接の質は事前準備で決まる」という考えを、モバファク採用チームでは大切にしています。 面談の質を高めるために、私の場合は大きく2つのことを事前に行っています。 ①求職者の情報を知る 事前に求職者の情報をもらっている場合は、必ず丁寧に目を通すようにしています。 面談に慣れているといっても、準備がないと面談がスムーズにいかないことが多いです。 面談をスムーズに回すためにも、事前にどんな質問をするかを10個以上は書き出すようにしています。 また、求職者にとっても企業が事前に情報を読み込んできてくれることに、悪い気はしないはずです! ②カジュアル面談のストーリーを考える どのような流れで、どんな話を伝えるのかを細かくまとめます。 事前に履歴書を見ている場合は、「技術的に成長ができる環境を求めているから、勉強会制度や学習支援制度の話をしてみよう」など、どうやって魅力を訴求するかも詳細に決めておきます。 大事なのは一本槍にならないこと。仮に勉強会制度の話に共感してもらえなかった場合、社員のキャリアモデルの話をするなど、一歩先のことも考えておくと面談が円滑に進みます。 求職者の情報が事前に見れないときは、どういった話の流れで就活の軸や将来像を聞き、回答によってどんな話をするか、場合分けをしておきます。 カジュアル面談の流れ では、実際にカジュアル面談でどのような話をどのような流れで行っているのかを紹介します! 例外もありますが、私の場合以下のような流れで進めることが多いです。 (カジュアル面談は基本的には60分で実施しています。) ①アイスブレイク カジュアル面談の目的には、ミスマッチを防ぐことも含まれます。 だからこそ、お互いに本音で話し合うことが理想的。 初対面ということもあるので限界はありますが、砕けた会話を冒頭ですることで、カジュアルな雰囲気づくりを意識しています。 事前に求職者から情報をいただいている場合は、出身地の話や趣味の話などに触れることが多いです。 ②目的/流れの説明 個人的には、目的の説明が最も大事だと思っています。 目的がふわっとした面談を防ぐためには、カジュアル面談が面接の場ではなく、求職者側が企業を選ぶ場であるということを明確にすることが何よりも大切です。 具体的には、「カジュアル面談の目的は何なのか?」「この面談で何を判断していただきたいのか?」について丁寧に説明をするようにしています。 目的と流れの説明を怠ると、認識の齟齬から「会社説明だと思ったら、質問攻めにされてしまった」など、不信感を求職者に抱かせてしまう場合があるのでご注意を! ③求職者へのヒアリング 会社の説明に移る前に、必ず簡単なヒアリングを行うようにしています。 求職者が就活において求めていることがわからないと、訴求すべき点も見えてきません。 だからこそ、何に興味を持っていて、どんな軸で就職活動をしているのか、そして将来的にはどんなスキルを身につけていきたいのかは、かなり具体的に聞くようにしています。 また、これまでの経歴、開発物についてもヒアリングを行っています。 モバファクが求める人材は「プログラミングが大好きな人」。社員やカルチャーとのマッチングを知るために、どのようなモチベーションで開発を行ってきたかなどを伺います。志向性等で気になるポイントがあれば、具体的にその旨をお伝えし、認識の齟齬がないようにしています。 ④会社の説明 ヒアリングで伺った就活の軸や将来像に対して、会社の魅力を訴求していきます。 求職者によって流れはまちまちで、勉強会などの文化を求めてる人には、会社のカルチャーについて。裁量を求める人には、会社の方針や1、2年目のキャリアモデルをメインに。 サービス面に興味が強い方には、サービスの詳細や各サービスのやりがいについて説明をしていきます。 合わせて、求職者の希望に応えられそうにない点についても丁寧に説明を行います。 不信感を抱かせないためにも、できないことは「できない」と正直に伝えるようにしています。 ⑤選考の案内 面談の終わりに「会社説明を受けて、選考へ進みたいと思ってくれたか?」と聞き、「進みたい」と言ってくれた方には、選考のご案内を行います。 注意点としては、冒頭の目的の説明時に、「選考に進むか否かを最後に判断して欲しい」という旨を伝えておくこと。 事前に伝えておかないと、求職者側に強制的な印象を与えてしまうかもしれません。 ミスマッチを防ぐためにも「No!」と言えるような雰囲気を作っておきましょう。 最後に カジュアル面談について記載をさせていただきました。 個人的に意識していることなので、正解ではないと思います。 ですが、これからカジュアル面談に関わるみなさまにとって、何かしらのヒントになっていれば、嬉しいです。 最後に宣伝です! 現在モバファクでは、中途エンジニア、新卒エンジニアを募集中です。 ご興味ある方は以下をご覧ください! https://recruit.mobilefactory.jp/recruit/
アバター
駅メモチームでエンジニアをしている id:Eadaeda です。シバンは #!/usr/bin/env を使う派です。 皆さんはシェルスクリプト書いてますか? 環境構築、開発、テスト、ビルド、デプロイなどなど、一連の作業を自動化するための手段として時々出番があるんじゃないでしょうか。 ところでそのシェルスクリプト、テスト書いてますか? シェルスクリプトのテスト 「シェルスクリプトのテスト〜?」って感じですよね。殆どの場合、一度書いてしまえばあんまり壊れることはないし別に…って感じですよね。わかります。実際開発環境のために docker compose up するだけのスクリプトなら雑でもいいですよね。 でも、重要な役割をもつスクリプトならどうでしょう。例えばアプリケーションのエントリーポイントや、リリースビルド・デプロイのためのスクリプトなどが思いつきます。 こういうのはテストである程度保証されていれば安心じゃないですか?どうですか?書きたくなってきましたか?とりあえず一回書いてみませんか? Bats/ShellSpecでシェルスクリプトのテストを書いてみよう シェルスクリプトにもテストフレームワークがあります。たとえば Bats や ShellSpec などです。 今回は上記2つについて少しだけ紹介しようと思います。テスト対象のスクリプトは以下のものとします。 #!/usr/bin/env zsh if [[ " ${1} " == " en " ]] ; then echo " Hello World " elif [[ " ${1} " == "" ]] ; then echo " こんにちは 世界 " fi 第一引数に en を渡せば Hello World を、何も渡さなければ こんにちは 世界 と出力するだけのスクリプトです。これを bin/hello-world.sh として保存しておきます。 Bats Bats は10年ぐらい前からあるそこそこ定番のテストフレームワークです。簡素に書けるのがいいところかなと思っています。 test/hello-world.bats として以下の内容を保存します。 #!/usr/bin/env bats # 出力を検査するシンプルなテスト。シェルの構文っぽく書く @ test " 引数がないとき、こんにちは 世界が返されるべき " { result = " $( ./bin/hello-world.sh ) " [ " $result " = "こんにちは 世界" ] } @ test " 引数がないとき、こんにちは 世界が返されるべき - 2 " { # run を使うと $output に出力が格納される run ./bin/hello-world.sh [ " $output " = "こんにちは 世界" ] } # bats-core/bats-supportとbats-core/bats-assertを ./test/helpers 以下にcloneして読み込めば # 様々なヘルパーが使えるようになって便利 load ' helpers/bats-support/load ' load ' helpers/bats-assert/load ' @ test " 引数がenのとき、Hello Worldが返されるべき " { run ./bin/hello-world.sh en # runの出力が一致しているかを見るヘルパー assert_output ' Hello World ' } 実行は bats コマンドに渡してやればよいです $ bats ./ test /hello-world.bats hello-world.bats ✓ 引数がないとき、こんにちは 世界が返されるべき ✓ 引数がないとき、こんにちは 世界が返されるべき - 2 ✓ 引数がenのとき、Hello Worldが返されるべき 3 tests, 0 failures setup/teardownも書くことができます #!/usr/bin/env bats setup() { # ヘルパーのロードをsetupでやっちゃう load ' helpers/bats-support/load ' load ' helpers/bats-assert/load ' } @ test " 引数がenのとき、Hello Worldが返されるべき " { run ./bin/hello-world.sh en # setupでロードしてるのでヘルパーが使えちゃう assert_output ' Hello World ' } ShellSpec ShellSpec はBDDな単体テストフレームワークです。RSpecとかJestみたいな書き味でテストを書いていくことができ、機能も豊富です。 まずはプロジェクトのセットアップです。最低限 .shellspec ファイルが必要となりますが、 shellspec --init で作成できるので、これを使うのが楽です。 $ shellspec --init create /path/to/ pwd /.shellspec create /path/to/ pwd /spec/spec_helper.sh あとは spec/ 以下にテストを書いていきます。今回、READMEの Typical directory structure にならってファイル名は spec/bin/hello-world_spec.sh としました。内容は以下の通りです。なんだかプログラムっていうか普通の文章みたいになりますね。 Describe " bin/hello-world.shについて " Context " 引数がないとき " It " こんにちは 世界が出力されるべき " When call ./bin/hello-world.sh The output should eq ' こんにちは 世界 ' End End Context " 引数がenのとき " It " Hello Worldが出力されるべき " When call ./bin/hello-world.sh en The output should eq ' Hello World ' End End End テストの実行は shellspec --init したディレクトリで shellspec を実行するだけです # --shell指定なしだと /bin/sh になる $ shellspec --shell zsh Running: /bin/zsh [ zsh 5 . 8 . 1 ] .. Finished in 0 . 27 seconds ( user 0 . 04 seconds, sys 0 . 04 seconds ) 2 examples, 0 failures まとめ 今回はシェルスクリプトのテストフレームワークであるBatsとShellSpecの触りだけご紹介しました。どちらを使うかはREADMEを読んでみて決めてみてくださいね。 以上です。
アバター
BC チームでエンジニアをしている id:d-kimuson です 11月にリリースされた TypeScript 4.9 から satisfies operator が追加されました。satisfies operator が追加されたことで 「React Router でのナビゲーションを型安全にする」がやりやすくなったのでやってみました この記事で紹介するコードは TS Playground で試すことができます React Router v6.4 からオブジェクト形式でルーティングをかけるようになり、ルーティング宣言から型を拾いやすくなった React Router v6.4 から createXXXRouter のAPIが追加され、コンポーネントではなく、プレーンオブジェクトでルーティングを書けるようになりました import { createBrowserRouter } from "react-router-dom" const router = createBrowserRouter ( [ { path: "/" , element: < HomePage / >, } , ] ) な形式でルーティングを宣言できます 以前からある <BrowserRouter> <Routes> <Route path="/" component={HomePage} /> </Routes> </BrowserRouter> なコンポーネント形式のルーティングでは難しかった、「宣言から型情報を読み取る」ことができるようになりました ルーティングの宣言に型の制約を課したいが、具体な型に解決させたい ルーティングの宣言から型情報を拾えるようになったので、良い感じに拾って型安全なナビゲーションを実現したいなと考えます しかし 宣言に型の制約を課しつつ 型自体は宣言から具体な型に解決させる はちょっと実現が面倒です ルーティングオブジェクトの宣言に RouteObject[] 型の制約を課すために形注釈をつけると import type { RouteObject } from "react-router-dom" const routes: RouteObject [] = [ { path: "/" , element: < HomePage / >, } , ] 制約は課すことができますが、routes は RouteObject[] 型に解決されてしまうので、具体的なルーティング( / ) を型情報から拾うことができません 宣言に合わせた型を拾いたいなら注釈をつけずに as const を使うのが有効です const routes = [ { path: "/" , element: < HomePage / >, } , ] as const ただし、今度は routes に RouteObject[] な制約をかけられていません 結果、補完が効かなくなったり宣言ではなく使用箇所での型エラーになってしまったりで望ましくありません satisfies operator この問題が satisfies operator で解決して、「制約を書けるが具体な型に解決させる」ができるようになりました satisfies operator は型の制約をかしますが、解決される型には影響を与えません したがって import type { ReadonlyDeep } from "type-fest" // as const すると readonly 化してしまうので type RoutesDef = ReadonlyArray < ReadonlyDeep < RouteObject >> const routes = [ { path: "/" , element: < HomePage / >, } , ] as const satisfies RoutesDef で宣言することで、 RouteObject[] な制約でルーティングを宣言しつつ、routes には宣言通りの型に解決させることができるようになりました 上の routes 変数は実際に const routes2: readonly [{ readonly path: "/" ; readonly element: JSX. Element ; }] 型に解決され、制約に違反すると型エラーが出ます ※ satisfies がないと実現できないというわけではなく、Vue 関連のエコシステムでよく使われている defineXXX のパターンでも一応同じことは達成できましたが、satiesfies operator で実現しやすくなりました 遷移に制約をつける routes を具体な型に解決させられるようになったので、型演算を通じて型安全なナビゲーションを実現できます サンプルとして、以下のルーティングの宣言を用意します const routes = [ { path: "/" , element: < HomePage / >, } , { path: "/nests" , element: ( < div > < h2 > Nests route < /h2 > < Nav / > < /div > ), children: [ { path: ":nestId" , element: ( < div > < h2 > nests 20 < /h2 > < Nav / > < /div > ), } , ] , } , ] as const satisfies RoutesDef typeof routes を扱いやすい型に整形する ルーティングのネストは children で書かれていて使いにくいので、まずは使いやすい型に変換していきます type RouteConfig < T extends RoutesDef , U = ToRouteUnion < T >> = AsObjectShape < U extends { path: string } ? U : { path: string } > /** * @desc children のネストを解決して Union にする * { * readonly path: "/"; * readonly element: JSX.Element; * } | { * readonly path: "/example"; * readonly element: JSX.Element; * } | { * readonly path: "/nests"; * readonly element: JSX.Element; * readonly children: readonly [...]; * } | { * path: "/nests/:nestId"; * } */ type ToRouteUnion < T extends RoutesDef > = T extends ReadonlyArray < infer I > ? MergeChild < I > : never /** * @desc 使いやすい Object 形式に整形 * { * "/example": { * path: "/example"; * }; * "/nests": { * path: "/nests"; * }; * "/": { * path: "/"; * }; * "/nests/:nestId": { * path: "/nests/:nestId"; * } & { * params: { * nestId: string; * }; * }; * } */ type AsObjectShape < T extends { path: string } > = { [ K in T [ "path" ]] : { path: K } & ( ParsePathParams < K > extends infer Params ? keyof Params extends never ? {} : { params: Params } : never ) } type MergeChild < T > = T extends { path: string children: ReadonlyArray < infer Children extends { path: string } > } ? | ( T extends { element: JSX. Element } ? T : never ) | MergeChild < { path: ` ${ T[ "path" ] } / ${ Children[ "path" ] } ` } & ( Children extends { children: any } ? { children: Children [ "children" ] } : {} ) > : T /** * @desc リテラルなルーティング文字列からパスパラメタを抽出する * @example ParsePathParams<'/nests/:nestId'> = { nestId: string } */ type ParsePathParams < T extends string > = [ T ] extends [ ` ${ string } : ${ infer I1 } ` ] ? I1 extends ` ${ infer Param } / ${ infer I2 } ` ? Required < { [ K in Param ] : string } & ParsePathParams < I2 >> : { [ K in I1 ] : string } : {} こういうパズルを組みます ここでは詳細な説明はしませんが children にネストしていたルーティングをマージして それぞれのパスからパスパラメタを抽出して 使いやすい型に整形 をしています typeof routes を渡してあげると export type RouteConf = RouteConfig <typeof routes > これは以下に解決されます type RouteConf = { "/" : { path: "/" ; } ; "/nests" : { path: "/nests" ; } ; "/nests/:nestId" : { path: "/nests/:nestId" ; } & { params: { nestId: string ; } ; } ; } 使いやすい型を抽出することができました 型安全に遷移先のリンクを生成する 使いやすい型が手に入ったので、これを使って型安全にパスを生成できる utility を作っていきます export const pagePath = < T extends keyof RouteConf >( path: T , ...args: RouteConf [ T ] extends { params: any } ? [ RouteConf [ T ][ 'params' ]] : [] ) : string => { const [ params ] = args as [ Record < string , string > | undefined ] return params === undefined ? path : Object .entries ( params ) .reduce ( ( s: string , [ key , value ] ) => s.replace ( `: ${ key } ` , value ), path ) } これで pagePath 関数を通すことで型安全に遷移先のルーティングを書くことができるようになりました Link タグの to には pagePath の関数でパスを設定します < ul > < li > < Link to= {pagePath('/')} > Home </ Link > </ li > < li > < Link to= {pagePath('/example')} > Example </ Link > </ li > < li > < Link to= {pagePath('/nests/:nestId', { nestId: '20' })} > Nests 20 </ Link > </ li > </ ul > useNavigate からの遷移でも同様に const navigate = useNavigate () const onClick = () => { navigate ( pagePath ( '/nests/:nestId' , { nestId: '20' } )) } とすることで、型安全なナビゲーションを実現することができました その他の型安全ルーティング ということで、React Router をそのまま使いながら型安全なナビゲーションを実現できましたが、ルーティングの宣言自体型を拾うことを前提に作られてないので対応するのがそこそこ大変でした また、クエリパラメタについても React Router のルーティング宣言にはクエリの型を書くようなインタフェースがないので拾うことができません したがって、本格的に型安全を目指したい場合は 型情報を拾うことを前提にしたインタフェースでルーティングを宣言し、React Router に渡せる routes を吐き出すようなアプローチ react-router-typesafe-routes 等 型情報を拾うことを前提にしたルーティングライブラリ Rocon 等 を使うのが良いと思います 一方、ルーティングのインタフェースを変えてしまうと React Router 等のメジャーなライブラリと比較してメンテナスが滞ったときや、バージョンアップ時のマイグレーションがつらくなる側面はあります 今回紹介した「React Router のインタフェースに乗っかりながら、可能な範囲で型安全性を保証するアプローチ」の場合、インタフェース変更時のマイグレーションも公式のマイグレーションに乗っかれば良いだけなので無難な選択肢にはなるのかなと思います できればアプリケーションコードにはこの辺りを入れたくないので、願わくば、公式や著名なところからライブラリとして出てくれると嬉しいんですが... まとめ satiesfies operator と React Router v6.4 のオブジェクト形式のルーティングで標準的な記法をそのまま使って型安全なナビゲーションを実現することができるようになりました また、実際に最低限の実装例を紹介しました それでは良い型安全ライフを!
アバター
こんにちは!モバイルファクトリーでエンジニアをしている id:d-kimuson です! 今年もモバイルファクトリーの Advent Calendar をお送りします 🎉 Advent Calendar 2022 モバイルファクトリー Advent Calendar 2022 では モバイルファクトリーの社員がプロダクトで使っている技術や興味のある技術での知見、Tips 等を毎日投稿していきます! 昨年は、140 字くらいのコンパクトな記事でも OK というルールを設定して Advent Calendar を開催しました。 今年も昨年のルールを継承しつつ、コンパクトな記事から内容の濃い記事まで幅広く投稿していきますので、ぜひお楽しみください! また、今年は例年とは少し趣向を変えて 「良いモノ」を作る技術 というサブテーマを設定しています。 テックな記事に加えて、毎週土曜日の記事ではデザインや社内勉強会の設計等少し広い視点から記事をお届けしていきます! 「JS の unhandledRejection クイズ」、「シェルスクリプトのテスト」、「デザイナーが大切にしている考え方」等、幅広い内容が予定されていますので、ぜひお楽しみください! 記事の一覧 各記事へのリンクをこちらに掲載します。 随時追加していきます。 12/1 tech.mobilefactory.jp 12/2 tech.mobilefactory.jp 12/3 tech.mobilefactory.jp 12/4 tech.mobilefactory.jp 12/5 tech.mobilefactory.jp 12/6 tech.mobilefactory.jp 12/7 tech.mobilefactory.jp 12/8 tech.mobilefactory.jp 12/9 tech.mobilefactory.jp 12/10 tech.mobilefactory.jp 12/11 tech.mobilefactory.jp 12/12 tech.mobilefactory.jp 12/13 tech.mobilefactory.jp 12/14 tech.mobilefactory.jp 12/15 tech.mobilefactory.jp 12/16 tech.mobilefactory.jp 12/17 tech.mobilefactory.jp 12/18 tech.mobilefactory.jp 12/19 tech.mobilefactory.jp 12/20 tech.mobilefactory.jp 12/21 tech.mobilefactory.jp 12/22 tech.mobilefactory.jp 12/23 tech.mobilefactory.jp 12/24 tech.mobilefactory.jp 12/25 tech.mobilefactory.jp 過去のアドベントカレンダーはこちら tech.mobilefactory.jp qiita.com qiita.com qiita.com qiita.com qiita.com Twitter ( @mfactech ) でアドベントカレンダーの更新情報をお知らせしていていきますので、フォローしていただけますと投稿された記事をいち早く知ることができます! ぜひお楽しみください!
アバター
こんにちは。ブロックチェーンチームのソフトウェアエンジニアの id:odan3240 です。 tech.mobilefactory.jp 上記の記事で紹介した通りユニマ/ガレージのインフラは Terraform で管理されています。 この記事では Terraform を管理するリポジトリのディレクトリ構成とその思想について紹介します。 前提 Terraform を管理するリポジトリは2020年の1月頃に開発されたものです。 当時の最新版の Terraform のバージョンは 0.12 でした。 当時の Terraform のバージョンでのディレクトリ構成の紹介であり、現在の最新版のベストプラクティスに沿わない可能性があります。 ディレクトリ構成 リポジトリルートのディレクトリ構造は次の通りです。以降で紹介するディレクトリ構造は説明のために一部簡略化しています。 $ tree -L 1 . . ├── README.md ├── environments └── modules environments のディレクトリ構成は次の通りです。 $ tree -L 3 environments environments ├── production │ ├── components │ │ ├── app │ │ ├── base │ │ ├── log │ │ └── rds │ └── tfbackend.tfvars └── staging ├── components │ ├── app │ ├── base │ ├── log │ └── rds └── tfbackend.tfvars 環境の名前でディレクトリを作成し、その中に components というディレクトリを作成しています。各 components の中のディレクトリについては後述します。 次に modules のディレクトリ構成は次の通りです。 $ tree -L 2 modules modules ├── iam_role │ ├── main.tf │ └── variables.tf ├── security_group │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── ssm_placeholder_parameter │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── components ├── app ├── base ├── log └── rds iam_role / security_group / ssm_placeholder_parameter は汎用的なモジュールです。これらの説明は今回の記事の趣旨とはずれるので、これ以上は説明しません。 components 以下のディレクトリは environments/production/components や environments/staging/components と同じ構造です。 以上がリポジトリのディレクトリ構成です。ここからはなぜこの構成にしたかを紹介していきます。 思想 当時このディレクトリ構成を考えるのにあたって大事にしていた思想について紹介します。 Web フロントエンドの思想にある Presentational/Container Components パターンを意識する ディレクトリ構造の紹介で説明したとおり、 modules/components と environments/$ENVIRONMENT/components には対応関係があります。これは Web フロントエンドの設計の思想にある Presentational/Container Components を意識してこの形になりました。 Presentational and Container Components | by Dan Abramov | Medium Presentational/Container Components パターンをざっくり説明すると Web フロントエンドを作るコンポーネントを、ボタンやレイアウトなどの UI の形を決める Presentational Components と、API でのデータの取得を行う Container Components に2つに分類する、というものです。このパターンは提唱されてからしばらく時間が経っており、コンポーネントをさらに細分化するパターンが知られています。しかし、この「汎用的な見た目を提供するグループ」と「具体的にデータを流し込むグループ」に分けて整理する思想は、後に考えられたパターンにも影響を及ぼしています。 Terraform の環境分離の方法の1つに Module 方式が知られています。自分は Web フロントエンドに土地勘があったので、Presentational/Container Components パターンに当てはめながら設計を考えることにしました。 modules/components は Presentational Components に対応してます。このレイヤーではアプリケーションサーバが動作する Fargate のサブネットや RDS のパラメーターグループなどが定義されています。RDS のスペックやアプリケーションサーバに付与する IAM ロールに指定する S3 バケットの ARN などは、tf ファイルにハードコーディングしないようにしています。 environments/$ENVIRONMENT/components は Container Components に対応してます。このレイヤーでは modules/components で定義したインフラの構造に対して具体的な値を流し込んでいます。ALB/Fargate/RDS に付与するセキュリティグループや各種必要な ARN をこのレイヤーから variable 経由で指定できるようになっています。 tfstate の分離はライフサイクルを意識する サービス全体のインフラを1つの tfstate にまとめると、tfstate が巨大化して plan/apply が遅くなるという問題が知られています。これに対応するためには、いくつかの tfstate に細分化する必要があります。 今回紹介したディレクトリ構造だと components/app や components/rds などのコンポーネントごとに tfstate が生成されるようになっています。これはアプリケーションサーバのインフラ構成の刷新の可能性は RDS の刷新の可能性と比べて高い、というライフサイクルの違いに着目しています。アプリケーションサーバのインフラ構成の刷新があって components/app2 というコンポーネントが生えても RDS やログに関する tfstate には干渉しない仕組みになっています。 これは同じ IaC のライブラリである、CloudFormation のスタック分割のベストプラクティスでも触れられている内容です。 docs.aws.amazon.com Terraform Workspaces は環境を分けるのに使用しない 本番環境、stg 環境など環境を分離する手法で採用される手法として Terraform Workspaces が知られています。しかし今回のリポジトリではこれまでに紹介した通り Modules 方式で各環境を分けています。 この理由は最新版のドキュメントでは記述が削除されていますが、次の由来によるものです。 In the 0.9 line of Terraform releases, this concept was known as "environment". It was renamed in 0.10 based on feedback about confusion caused by the overloading of the word "environment" both within Terraform itself and within organizations that use Terraform. https://developer.hashicorp.com/terraform/language/v1.2.x/state/workspaces 元々 Workspaces は environment という名前で知られていました。しかし本番環境、stg 環境などを意味する環境との混同を避けるために名前を変更した経緯があります。これは Workspaces は環境を分離するために用意された機能ではないことを指していると考えて環境を分離するためには使用しない判断を下しました。 運用してみての感想 terraform import コマンドとの相性が良い Terraform には既存の AWS のリソースを tfstate に読み込む terraform import というコマンドがあります。 .tf ファイルの作成にはこのコマンドを使用して次の流れで行うようにしていました。 stg 環境用の AWS アカウントでリソースを作成 対応するコンポーネントが存在しないならコンポーネントを作成 (hoge とする) terraform import で stg 環境のコンポーネントの tfstate に読み込む environments/staging/components/hoge で plan の diff がなくなるまで modules/components/hoge 以下の .tf ファイルを編集 本番環境への反映は environments/production/components/hoge で apply することによって行う この流れで作業すると modules/components/hoge をベースに本番環境が作成されるため、環境ごとの差異を極力抑えることができていました。 components が数がどんどん増えていく リポジトリを作成した当初のコンポーネントの数は4つか5つでした。しかし2年以上の月日によってアプリケーションに必要なインフラが増えた結果、コンポーネントの数が16個まで増えました。 16個に増えると新しい環境を作るときに16回 apply する必要があるため、一時的な手間は増えてしまいました。 app コンポーネントの肥大化 当初の app コンポーネントは API 用のサーバを Fargate で用意する定義だけが書かれておりシンプルな内容でした。しかし開発が進むにつれて、batch 用や worker 用のサーバの構成の定義が増えたりした結果、app コンポーネントが肥大化しました。 これは plan/apply の時間の増加に繋がりつらいです。 app コンポーネントでは ALB やフロントエンドで使用する S3 + CloudFront も管理しています。app という名前だと役割が大きいので fargate コンポーネントという命名にして、ALB などの設定が別のコンポーネントに生えるようにするのが良かったかもしれません。 まとめ Terraform のリポジトリのディレクトリ構成とその思想を紹介しました。 その中でも特に、Web フロントエンドのコンポーネント分割のパターンである Presentational/Container Components パターンをベースに Modules 形式で各環境を分離する手法を紹介しました。 この記事が Terraform のディレクトリ構造に悩む人に1つのアイディアとして受け取っていただけると幸いです。
アバター
こんにちは。ブロックチェーンチームのソフトウェアエンジニアの id:odan3240 です。 下記の記事で紹介した通り、ブロックチェーンチームではバックエンドでも Node.js を使用しています。 tech.mobilefactory.jp フロントエンドとバックエンドのソースコードは同一リポジトリで管理するモノレポを採用しています。 使用しているライブラリやプラットフォームの違いもあり、フロントエンドとバックエンドでは異なる Node.js のバージョンを使用しています。複数のバージョンを使い分けるために開発環境では nodenv を導入しています。 この記事ではチームで実践している GitHub Actions で nodenv を使用して複数の Node.js のバージョンを使い分ける方法を紹介します。 公式の Actions はあるが Node.js のインストールは行われない nodenv の Organization が Actions を公開しています。 github.com これを使えばバージョンの使い分けができるように見えます。しかしこの Actions は nodenv コマンドをインストールするだけで、その先の環境の設定や指定したバージョンの Node.js のインストールは自分で行う必要があります。 setup-node だと Node.js のバージョンの切り替えができない GitHub Actions で Node.js のインストールというと setup-node を思い浮かべる人が多いでしょう。 しかしこの Actions は一時的に PATH に含まれる node コマンドを置き換えるだけです。nodenv の機能にある、都度ローカルの .node-version ファイルを読んでバージョンの違う node コマンドを実行できるようにはなりません。 そのため今回のようなディレクトリごとに Node.js のバージョンを切り替えたい要求には使用することができません。 解決策 今回は次の Actions を書いて問題を解決しました。今回のサンプルコードは GitHub に置いてあります。 重要なステップを1つずつ説明していきます。 name : CI on : push : branches : [ "main" ] pull_request : branches : [ "main" ] workflow_dispatch : jobs : build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v3 - uses : nodenv/actions/setup-nodenv@v2 - name : Install node-build run : | mkdir -p "$(nodenv root)" /plugins git clone https://github.com/nodenv/node-build.git "$(nodenv root)" /plugins/node-build - name : Install node run : nodenv install -s - name : Run nodenv init run : | echo "$(nodenv root)" /shims >> $GITHUB_PATH echo "NODENV_SHELL=bash" >> $GITHUB_ENV nodenv rehash - name : Check node version run : node -v Install node-build について nodenv だけでは Node.js のインストールができないため node-build を別途インストールする必要があります。 node-build の README で説明されている通り nodenv のプラグインとしてインストールしています。 github.com Run nodenv init について nodenv init 相当のセットアップを行っています。 開発環境に nodenv をインストールする場合、 eval "$(nodenv init -)" を実行するか .bashrc や .zshrc に eval "$(nodenv init -)" を追記する必要があります。 self-hosted not using bashrc · Discussion #25407 · community を参考に .bashrc に追記する方法も試してみましたが環境変数が設定されませんでした。 これは、GitHub Actions では変数を export しても環境変数として扱われないことが原因だと予想して、手動で設定する方針にしました。 dev.classmethod.jp GitHub Actions 上で nodenv init を実行したときの出力内容は次の通りです。 export PATH= "/home/runner/.nodenv/shims: ${PATH} " export NODENV_SHELL=bash source '/opt/hostedtoolcache/nodenv/1.3.1/x64/libexec/../completions/nodenv.bash' command nodenv rehash 2 > /dev/null nodenv() { local command command = " ${1:-} " if [ " $# " -gt 0 ]; then shift fi case " $command " in rehash| shell) eval " $( nodenv "sh- $command " " $@ " ) " ;; *) command nodenv " $command " " $@ " ;; esac } これに相当することを GitHub Actions 流の設定方法で step に記述しています。 これらの対応を行うことで、GitHub Actions でも cd すると自動的に .node-version の Node.js が実行されるようになります。 まとめ ブロックチェーンチームではモノレポでディレクトリごとに異なる Node.js のバージョンを使用している 開発環境と同様に GitHub Actions でも nodenv で Node.js を使い分けたくなった nodenv/actions/setup-nodenv や actions/setup-node では目的を達成できない nodenv init の内容に相当する処理を step に書くと、自動的に Node.js のバージョンが切り替わるようになる
アバター
駅奪取チームエンジニアの id:dorapon2000 です。 弊社の今年の技術研修についての記事が何点か投稿されています。 tech.mobilefactory.jp tech.mobilefactory.jp プロダクトで利用されているプログラミング言語、ライブラリ、RDBMSなどの技術研修を行っても、プロダクト開発を円滑に行うことは難しいです。プロダクトの仕様の理解が浅く、各機能のコードがどこにあるか把握できていないことが一因です。他にも、口頭伝承になりがちで毎年のコストになっていたことや、新機能開発に取り組んでも、プロダクト理解が浅ければ出るべき提案も出てこない問題もあります。 私達のチームでは、こういった問題を解決するため、チーム横断の技術研修とは別に「プロダクト技術研修」を4年前から実施しています。本記事では、そのプロダクト技術研修の紹介と実施にあたり大切にしていることを書きたいと思います。 何をしているのか 【遊び方】遊んでスクショを撮る 【仕様理解】フローチャート・状態遷移図を書く 【コードを追う】該当処理のコードがどこにあるのか追う 何を目指しているのか 内容は毎年アップデートしていく 個人的に気をつけていること 最後に 何をしているのか 新人にプロダクトに関する課題を与えて解いてもらいます。課題の回答をメンター(私)がレビューし、設問の目的を達成できていれば次の課題を、達成できていなければその理由を説明し新人に修正してもらいます。後述しますが、課題文通りの回答になっているかではなく、お互いに目的を達成できていると納得した状態を目指します。 実際に用意している課題には3種類の設問があります。順に説明します。 【遊び方】遊んでスクショを撮る 【仕様理解】フローチャート・状態遷移図を書く 【コードを追う】該当処理のコードがどこにあるのか追う 【遊び方】遊んでスクショを撮る プロダクトを操作しながら指定されたスクリーンショットを撮ってもらいます。 私のチームで扱うプロダクトは、駅奪取という位置情報を使った駅の陣取りゲームですが、例えば、下記のスクリーンショットのように駅の路線を制覇(路線に属するすべての駅で位置登録)したときの達成画面をスクリーンショットしてもらっています。 工夫のポイントは、ユーザーが主要機能の内容をゲーム内のどこで把握できるのか、ドキュメントとしてはどこにまとまっているのか、併せて理解してもらうことです。そうすることで、課題で扱わなかった機能についても理解したいとき、同じ要領で自ら探せます。 【仕様理解】フローチャート・状態遷移図を書く 主要機能について、ユーザーからは見えないコードレベルで細かい仕様の理解をするために、コードを読み、フローチャート・状態遷移図に落とし込んでもらいます。【遊び方】の発展と言えます。 下記は「1週間以内に20km移動せよ」といった指令機能の状態遷移図です。素朴に考えると、ユーザが取りうる状態は、指令を達成しているか否かだけと考えがちですが、それ以外にも期限切れなどの状態があることを、状態遷移図を通して理解してもらいます。 工夫している点は、手間はかかりますが、自分の手でフローチャートを書いてもらっていることです。すでにあるフローチャートを読むだけでは、プロダクトのコードとフローを紐付けて理解することが難しいと感じています。 【コードを追う】該当処理のコードがどこにあるのか追う 指定した処理のコードがどこにあるのか、練習用のブランチ上でコメントしてもらいます。 この設問の目的は2つです。 アーキテクチャ・実装の理解 自分が探している処理をソースコード上から見つけられるようになること 偶然見つけられたでは後者の目的を達成できていません。達成するために、実際にユーザーから見えるフロントエンドのコードから順に追っていき、目的のバックエンド側の処理を探してもらいます。課題文中では「トレース的に」と説明します。 例えば、ガチャの確率を決定するメソッドを探してほしい場合、ガチャを引くボタンを押して、リクエストが飛ぶエンドポイントを探し、そこからバックエンド側のメソッドの奥へ奥へと探索しながら該当コードを見つけられると目的達成です。以下は回答例です。 # 1. ここにガチャ抽選のHTTPリクエストがくる sub dispatch_draw_gacha { # 2. ガチャの抽選処理 my $result = Service::Gacha->draw( $user , $gacha ); ... } sub Service::Gacha::draw ($ self, $user, $gacha ) { # 3. ガチャに入っているアイテム my $items = $self->select_gacha_items ( $gacha ); # 4. ガチャの確率を決定する箇所【ここが課題の箇所】 my $max_rate = sum0 { $_->weight } @$items ; my $rate = Sub::Rate->new( max_rate => $max_rate ); $rate->add ( $_->weight => sub { $_ } ) for @$items ; # 5. 抽選 my $item = $rate->genereate (); ... } 実際の業務では、ユーザーからお問い合わせで不具合の報告があったとき、実際の処理がどうなっているのかコードレベルでの理解が要求されます。あるいは、既存の機能の拡張を行う際も、既存の機能を十分に理解する必要があります。そのための練習です。 何を目指しているのか 課題全体の目的は2つあります。これら2つの目的を達成することで、定常業務や新規機能開発にスムーズに参加できるようになることを期待しています。 基本的な遊び方、仕様を理解すること 各機能、処理のコードがどこにあるか自分で追えるようになること 1つ目はその通りで、自分は2つ目を大切にしています。 プロダクトの技術研修を終えた後もまだまだ未知の機能やコードがあります。それらをメンターや先輩、上司に積極的に質問して解決できることは嬉しいことではありますが、チーム全体のリソースを考えると自己解決できることも同じように大事です。2つ目の目的は課題の中で自己解決のノウハウを学んでもらうためにあります。【遊び方】で機能を理解できる場所を示したり、【コードを追う】でトレース的にコードをたどってもらったのもそのような意図でした。 内容は毎年アップデートしていく 基本機能はプロダクトの新規機能開発に伴って増えていきます。そのため、課題の内容にもアップデートが欠かせません。 しかし、内容のアップデートだけなく、課題文を洗練していくことも大切です。プロダクトに慣れているせいで初心者の視点が抜け落ちた不親切な課題文になってしまっていたり、曖昧な課題文で回答が1つに定まらない場合があります。新人が目的とはずれた場所で時間を浪費しないために、時間を浪費して自分自身を責めてしまわないように、課題文はアップデートをしていきます。 個人的に気をつけていること 去年と今年の2回、プロダクトの技術研修の担当をしました。この研修を活かすために意識していたことがあります。 まず、新人にプロダクトの技術研修の目的を知ってもらうことです。 課題が解ければいいのではないこと ユーザーからお問い合わせがあったとき、ドキュメントになっていない細かい仕様をコードを追いながら説明できるようになってほしいこと 課題的には不要だったフローチャートも目的と合致しているから残しておいていいこと インフラ起因のエラーに悩むことは目的と合致していないからすぐヘルプを出してほしいこと そして新人が質問してきたこと・躓いたところを忘れないうちにメモしておきます。それらは来年の課題のアップデートの際に必要な材料になります。 最後に プロダクトの技術研修としてプロダクト用の課題を準備していることについて紹介しました。 初回の準備は大変ですが、一度実施できれば、その課題を翌年以降にも引き続くことができます。準備された課題はチームの新人学習に対するノウハウでもあります。研修担当者が変わったとしても、同じ質で研修を実施できるでしょう。 見てくださった方の参考になれば幸いです。
アバター