GitHub Actionsで重複ビルドを排除する

最近のTravisCIの残念なムーブもあり、GitHub Actionsへの移行を積極的に行なっているのですが、個人的に1つ気になっているのがブランチへのプッシュとプルリクエストに対するアクションでビルドが重複してしまうケースがあることです。

まず、GitHub上で標準で用意されているテンプレートからGitHub Actionsの設定を作成すると以下のような感じになります。

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

この場合、masterにpushしたコミット、およびmasterに対するプルリクエストに対してビルドが実行されます。ただ、TravisCIやCircleCIではmasterブランチ以外のブランチに対するコミットやプルリクエストでもビルドが走っていたのでこれと同じ動作を実現するために以下のように設定を変更します。

on: [push, pull_request]

すると、このリポジトリに作成したブランチからプルリクエストを作成した場合に、以下のようにブランチのpushに対するビルドとプルリクエストに対するビルドで同じコミットに対して2回ビルドが走ってしまいます。

f:id:takezoe:20201204162745p:plain

まあ問題ないといえばないのですが、同じコミットに対して二回ビルドするのは無駄ですし、ジョブが多いとプルリクエストに大量のジョブ実行結果が二重に表示されてしまうので、できれば避けたいところです(ちなみにTravisCIでは同様に重複実行されるのですが、CircleCIでは重複実行されないようになっているようです)。調べてみたところ、以下のように設定されているオープンソースプロジェクトを見かけました。

on:
  push:
    branches: [ master ]
  pull_request:

この設定はコードの変更時は必ずプルリクエストを作成し、マージされたらmasterブランチをビルドするというワークフローであればうまく機能します。とはいうものの、個人のリポジトリでは毎回プルリクエストを作成するのも面倒ですし、逆に業務では複数のリリースブランチを運用したい(masterブランチ以外へのpushでもビルドを走らせたい)というケースも多いのではないかと思います。

どうやらGitHub Actionsで完全にTravisCIやCircleCIと同じ挙動にすることは難しそうな気配なので、次善の策として以下のSkip Duplicate Actionsというアクションを試してみました。

github.com

READMEにも書いてあるように、以下のような感じでpre_jobという重複をチェックするジョブを追加し、もしビルドが重複している場合はmain_jobでifを使ってジョブをスキップするようにします。

jobs:
  pre_job:
    runs-on: ubuntu-latest
    # Map a step output to a job output
    outputs:
      should_skip: ${{ steps.skip_check.outputs.should_skip }}
    steps:
      - id: skip_check
        uses: fkirc/skip-duplicate-actions@master
        with:
          github_token: ${{ github.token }}

  main_job:
    needs: pre_job
    if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Running slow tests..." && sleep 30

確かにこのアクションによってmain_jobをスキップすることは可能でした。しかし結果的にスキップはされるものの、ビルドはトリガーされてしまうので、プルリクエストには以下のように複数のビルドが表示されてしまいます。

f:id:takezoe:20201204162808p:plain

というわけでGitHub ActionsでTravisCIやCircleCIと同じ動作をさせつつ重複ビルドを排除するという目的はいまだ達成できていないのでした。何か方法はあるのでしょうか…。

Presto Conference Tokyo 2020でトレジャーデータにおけるPrestoサービスのテスト戦略について発表させていただきました

今年はリモート開催となったPresto Conference Tokyo 2020で、トレジャーデータのPrestoサービスのテスト戦略について発表させていただきました。

スライドはこちらです。

www2.slideshare.net

社内ではpresto-query-simulatorと呼んでいるテストシステムなのですが、基本的な考え方としては現行バージョンとテスト対象のバージョンの2つのPrestoクラスタで実際にプロダクション環境で実行された過去一定期間のクエリを流し、結果を比較するというものです。

2年前に私が入社した時点ですでにこのテストの仕組みは存在していたのですが、さらなる効率・カバレッジ改善と共に、テスト結果の分析に非常に手間がかかる上に分析が個人のナレッジに依存しがちで、結果的にテストを実施しているにも関わらず問題を検出できていないケースもあったため、最近になって結果の自動解析とレポートの自動生成機能を追加し、テスト結果を迅速かつ定量的に評価できるようになりました。

発表では他社さんの類似のテスト事例についても軽く紹介させていただいたのですが、SnowflakeではSnowtrailという内製のテスティングフレームワークが使われているようです。以下のペーパーを読むと同じSaaSならではの苦労やテストの難しさなどかなり共感できる部分があります。

resources.snowflake.com

Snowflakeはプラットフォームとしてタイムトラベル機能をサポートしているので、テストでも実際にクエリが実行された時点でのデータを使ってテストを行うことができているようです。また、SnowflakeSQLでのデータ書き込みができないはずなので、Writeのパフォーマンスや、クエリエンジンによるWriteを駆使したトリッキーなユースケースについて考えなくてもよいであろう点は若干羨ましいところですw

また、TiDBを開発しているPingCAPではKubernetes上でFault Injectionまで含んだテストを行なっているようです。さらなるサービスの安全性と安定性のためにはこういった異常系のテストについても充実させていく必要があるかと思います。

pingcap.com

それからカンファレンス後にTwitterで教えていただいたのですが、Googleでも同様のアプローチでクエリエンジンのデグレードを検出するためのテストを行なっているようです。こちらはVLDB 2020で発表があったようなのですが見落としていました。後で目を通してみようと思います。

なお、今回のPresto Conference Tokyo 2020全体については弊社のサポートチームのKammyさんが執筆された以下のブログ記事に大変よくまとまっていますので、こちらをご参照いただければと思います。

td-support.hatenablog.com

参加者の皆さん、登壇者の皆さん、お疲れ様でした。またいつの日か昨年同様、Prestoクリエーターのお三方やBrianさんもお招きしてオフラインでPresto Conference Tokyoが開催できる日が再びやってくることを願っています。

mockito-scalaについて調べてみた

ScalaTestでは以前からMockitoSugarというトレイトが提供されており、ScalaからMockitoをちょっとだけ便利に使用することができるようになっていたのですが、元々それほど大した機能もなかった上に現在は別ライブラリに切り出されてしまったこともあり、使うモチベーションがだいぶ薄れてしまったのではないかと思います。実際のところ、ScalaTestのMockitoSugarを使わずに直接Mockitoを使っても大差ないのですが、やはりScalaからMockitoを使っていると不便に感じる点があったりします。

Mockito本家ではmockito-scalaというScala向けのライブラリが開発されており、こちらはScala向けにかなり作り込まれているようなので軽く試してみました。

github.com

このライブラリを使うにはbuild.sbtに以下の依存関係を追加します。Mockito本体はmockito-scalaの依存関係で引っ張ってくるので明示的に追加する必要はありません。

libraryDependencies ++= Seq(
  "org.mockito" %% "mockito-scala" % "1.16.0" % "test"
)

基本的な使い方はMockitoSugarArgumentMatchersSugarというトレイトをテストケースにミックスインします。

import org.scalatest.funsuite.AnyFunSuite
import org.mockito.{ ArgumentMatchersSugar, MockitoSugar }

class MyTest extends AnyFunSuite 
    with MockitoSugar 
    with ArgumentMatchersSugar {
  ...
}

もしくはmockito-scalaではトレイトに対応するシングルトンオブジェクトも用意されており、トレイトをミックスインする代わりにシングルトンオブジェクトのメンバをインポートすることで同じように使うことができます。

import org.scalatest.funsuite.AnyFunSuite
import org.mockito.ArgumentMatchersSugar._
import org.mockito.MockitoSugar._

class MyTest extends AnyFunSuite {
  ...
}

mockito-scalaでは以下のようにMockitoをScalaから使う際に便利な様々な機能が提供されています。

  • ScalaTestのMockitoSugarと同じようにmock[T]でモックを生成できる
  • 同様に例外スローや引数のキャプチャもdoThrow[T]Captor[T]のように記述できる
  • mock[MyClass with MyTrait]のようにモックの生成時にトレイトをミックスインできる
  • Mockitoのオーバーロードされた可変長引数メソッドの呼び出しが簡単にできる(doReturn(value, Nil: _*)Java同様doReturn(value)と記述できる)
  • spyLambdaで関数のspyが可能
  • verifyでデフォルト引数や名前渡し(遅延評価)の引数も適切に扱うことができる
  • eqがScalaTestのMatcherと衝突してしまう問題が解消されている(予めeqToが用意されている)
  • Matcherの型変数や括弧を省略可能(any[String]anyStringの代わりにanyと記述できる)
  • null撲滅のためにw Null matcherに警告が出る
  • 引数なしの関数のためにfunction0 matcherが用意されている
  • Value ClassのためにeqToVal matcherが用意されている
  • ArgumentCaptorの代わりにシンプルかつValue ClassをサポートしたArgCaptorが提供されている
  • MockitoのStrict Stubsのサポート(Idiomatic Syntaxではデフォルト)
  • ScalaTest用の便利トレイト(MockitoFixtureResetMocksAfterEachTestなど)
  • Answerを関数で記述できるようになっており、Invocationから引数を抽出する代わりに関数の引数として受け取ることができる
  • Idiomatic SyntaxやExpect DSLというマクロを活用したDSLが提供されている

最後のIdiomatic Syntaxですが、たとえば通常のMockitoを使用した次のようなコードがあるとします。

when(aMock.bar) thenReturn "mocked!"
when(aMock.baz(any)) thenReturn "mocked!"

verify(aMock, times(6)).bar
verify(aMock, atLeast(6)).baz(any)  

Idiomatic Syntaxではこのコードを次のような感じで記述することができるというものです。語順が異なるだけでなく、anyの代わりに*が使えたりします。

aMock.bar returns "mocked!"
aMock.baz(*) returns "mocked!"

aMock.bar wasCalled 6.times
aMock.baz(*) wasCalled atLeast(6.times)

Expect DSLはこのIdiomatic Syntaxをさらに変形させたようなもので、Idiomatic Syntaxで次のように記述するところを

aMock.bar wasCalled 6.times
aMock.baz(*) wasCalled atLeast(6.times)

expectから始まるDSLで置き換えることができます。

expect exactly 6.calls to aMock.bar
expect atLeast 6.calls to aMock.baz(*)

先頭にexpectが来るのでverifyしていることがわかりやすいということのようですが、それなら元のMockitoのverifyでいいのではという気がしなくも…。

その他、機能の細かい部分についてはmockito-scalaのREADMEや、mockito-scalaの作者であるBruno Bonannoさんのブログでも開設されています。

medium.com

mockito-scalaはかなり機能豊富でScalaからMockitoを使う際に便利になっている部分も多いのですが、Idiomatic SyntaxやExpect DSLは若干やりすぎ感がなくもない感じがします。Mockitoというより別のモックライブラリを使っている感じというか…。とはいえMockitoの知識なしで使えるかというとそれも微妙な感じなので、結局学習コストが二重にかかるだけなのではという懸念があります。また、マクロが活用されているということもあり、もし今後万が一mockito-scalaのメンテが止まってしまった場合の対応もなかなか大変そうな予感がします。

それ以外の部分については、特にValue Classやデフォルト引数、名前渡し引数のサポートやAnswerを関数と書けるようになっているあたりはなかなかポイント高いのではないかと思うので、独自DSLを使わず基本機能だけ使うのもありなのではないかと思いました。

sbt-native-imageプラグインによるScalaアプリケーションのネイティブイメージの生成

これまでScalaでGraalVMのネイティブイメージ生成機能を使う場合はsbt-native-packagerプラグインを使用していたのですが、最近新たにsbt-native-imageというプラグインがリリースされたとのことで実際に試してみました。

まずは例によってproject/plugins.sbtプラグインを追加します。

addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.2.1")

次にbuild.sbtに以下の記述を追加してプラグインを有効にします。

enablePlugins(NativeImagePlugin)

最低限の準備はこれで完了です。sbt nativeImageを実行するとtarget/native-imageディレクトリに実行可能ファイルが生成されます。

指定可能なタスクやオプションはREADMEで説明されています。たとえばJVMにフォールバックしないネイティブイメージを生成するのであればbuils.sbtに以下の設定を追加します。

nativeImageOptions ++= List(
  "--initialize-at-build-time",
  "--no-fallback",
  "--no-server"
)

sbt-native-packagerと比較して、できることに大きな差はないのですが、やはりGraalVMを事前にインストールしておく必要がないという点は大きなアドバンテージです。また、GraalVMのバージョンをsbtの設定で指定することができるのもビルド環境の差異によるトラブルを減らすのに役立つのではないかと思います。

以前作ったScalaによるネイティブCLIアプリケーション作成用のgiter8テンプレートもsbt-native-imageプラグインを使うように修正しておきました。

github.com

Learning Spark: Lightning-Fast Data Analytics 2nd Edition

Spark 3.0に対応したLearning Sparkの2nd Edition、しばらく前にDatabricks社のWebサイトから無料でダウンロードできるものを入手していたのですが、最近ようやく一通り目を通すことができました。

Learning Spark: Lightning-Fast Data Analytics

Learning Spark: Lightning-Fast Data Analytics

Sparkの基礎からStructured Streamingによるストリーム処理、パフォーマンスチューニング、MLlibによる機械学習、さらにMLflowによる機械学習パイプライン、Delta Lakeによるデータレイクなど最新のトピックまでカバーされ、サンプルコードも基本的にScalaPythonの両方で同一のコードが記述されているという網羅度の高いSparkの入門書です。

個人的にはややわかりづらいStructured Streamingの挙動について図も多用して説明されていたのが好印象でした。また、Sparkは開発が非常に活発であるということもあり、APIにも複数の種類やレイヤーがあるのも入門者にはわかりづらい部分なのではないかと思うのですが、そのあたりも歴史を追いつつ説明されており、Sparkを使い始めるのであればとりあえずこれを読んでおけば間違いないという内容になっていると思います。構築・運用に関する部分は弱いかなと思うのものの、環境依存なところもありますし、マネージドなクラウドサービスを使うという選択肢もあるので妥当なところではないでしょうか。

Spark 3.0での新機能については最後の章にまとめられているのですが、各章でも該当箇所で個別に触れられています。1st Editionは読んだことがないので比較はできないのですが、単にSpark 3.0の章を付け足しただけというわけではなく、全編丁寧にアップデートされている印象です。

なお、冒頭でも触れたとおり、この書籍はDatabricks社のWebサイトから無料でダウンロードすることができます(メールアドレスの登録が必要)。

databricks.com

以前紹介したPresto: The Definitive Guideもそうでしたが、オライリーでは最近こういうパターンが増えていますね。ノベルティなどでオライリー製作の小冊子が配布されているのもよく見かけます。技術書の出版はグローバルで見ても難しい商売だとは思うのですが、オープンソースプロダクトを支援している企業としては広告宣伝活動の一環としてコストを投下できますし、読者としても無料でまとまった書籍の形で情報を得ることができる、オライリーのブランドを活かした興味深い取り組みだと思います。

GitBucket 4.34.0をリリースしました

f:id:takezoe:20200726033019p:plain

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.34.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.34.0

2020年初、7ヶ月ぶりのリリースとなります。色々変更が溜まっていたり、プラグインのアップデートも必要だったりしたのでいつリリース作業をしようかと思っていたのですが、4連休を使ってリリース作業を行うことができたのでよかったです。次のリリースはいつできるのか…。

今回のバージョンでの主な変更内容は以下の通りです。

システム管理画面の強化

管理者向けのシステム管理画面を強化し、新たな設定項目を多数追加しました。

ファイルアップロードの設定

これまではファイルサイズ、タイムアウトの設定をCLIオプションや環境変数などで行うことができましたが、管理画面上から行えるようになりました。また、別途ラージファイル向けの設定も可能になっています。

f:id:takezoe:20200726032942p:plain

リポジトリ操作の制限

リポジトリのリネーム、削除、フォーク、移管といった操作を管理者のみに制限できるようになりました。

f:id:takezoe:20200726032954p:plain

ユーザ定義CSS

管理者はGitBucketの外観をカスタマイズするために任意のCSSを定義できるようになりました。定義したCSSはGitBucketのすべてのページに適用されます。

f:id:takezoe:20200726033006p:plain

リポジトリビューアの性能改善

リポジトリ情報のインメモリキャッシュや、ディレクトリ内のファイルが多い場合に詳細情報の表示をスキップするなどの対応によって巨大なGitリポジトリでのリポジトリビューアの表示速度が大幅に改善しています。

スタンドアロンモードでのHttpSessionの永続化

組み込みJettyを使用したスタンドアロンモードで使用する場合、これまでは再起動するとHttpSessionはリセットされていましたが、以下のように--save_sessionsというオプションを付けて起動することでHttpSessionをディスクに永続化できるようになりました。

$ java --save_sessions -jar gitbucket.war

Web APIのアップデート

List commits APIが新たに追加されました。また、既存のAPIGitHubとの互換性向上のためいくつか更新されています。GitBucketのユーザさんからはJenkins連携でのトラブルで質問をいただくことが多いのですが、今回のバージョンアップで状況が改善することを期待しています。

プラグインのアップデート

デフォルトでバンドルされているプラグインのうち、以下の2つのプラグインがバージョンアップしています。

今回は7ヶ月ぶりのリリースだけあり、この他にもMariaDBサポートの改善、アクティビティログの改善、サイドバーに表示されるリポジトリ一覧の制限など、様々な改善やバグ修正が行われています。詳細についてはIssueの一覧をご覧いただければと思います。

ThinkPadキーボードをMacで使うためのKalabiner Elementsの設定 最新版

過去にも同じ記事を書いたのですが、ThinkPadキーボードのモデルチェンジやKarabinar Elementsのバージョンアップで微妙に変わっている部分もあるのでThinkPad TrackPoint Keyboard IIで行った設定を自分の備忘録を兼ねてメモしておきます。

スペースキー周辺のキーはなるべくMacBook本体の配列にあわせるようこんな感じに。実際はCapsLockとControlの入れ替えはFor all devicesで全てのキーボードに適用されるようにしています。Mac側の設定で変えることもできるのでそれでもよいと思います。

f:id:takezoe:20200711010515p:plain

日本語入力の切り替えはコマンドキーの単体押しで切り替えられるように。

f:id:takezoe:20200711010635p:plain

トラックポイントのボタンの設定もカスタマイズできるよう、マウスとして認識されているデバイスにもチェックを入れておきます。

f:id:takezoe:20200711010719p:plain

そしてセンターボタンを無効に。これをやっておかないと、トラックポイントでスクロールしようとしたときにセンターボタンでリンクをクリックしてどんどん新しいウィンドウが開いてしまうという事故が多発します。まあでもこれは人によるかも。

f:id:takezoe:20200711010822p:plain

ちなみに上記はUSB接続の場合の設定で、Bluetooth接続の場合はキーボードとマウスが1デバイスとして認識されるようです。なのでトラックポイントのボタンの設定もキーボードと同じデバイスに対して行います。

f:id:takezoe:20200712033526p:plain