最近、参照したり共有したりすることが多いURL

soudai.hatenablog.com

gihyo.jp

scrapbox.io

scrapbox.io

よく参照するならまとめておけばいいじゃない。

ラインナップからフェーズ的なものについては察してください。

ガッチガチに「こうあるべき」というような話というよりも、モデリングやテーブル設計においての固定観念*1を崩すためのキッカケに使っていることが多い気がします。

ちなみに私は、十数年、散々失敗した挙句にこのような有益なURLを有益だと理解して参照したりしているわけで、最初から(もしくは短期間に)ここに行き着ける人ってすごいなと尊敬しています。

追記

そーだいさんから有益情報を教えてもらったので並べておきます。

*1:「(Webフレームワークの)モデルとデータベーステーブルを1:1にして終わり」とか

APIテスティングツールに必要なのはテストケースごとのIDなのではないか #cicd_test_night

先日、機会をいただきましてCI/CD Test Night #7でAPIテスティングツール*1を開発している中で考えていることをお話しさせていただきました。

testnight.connpass.com

詳細はスライドをご覧ください。

speakerdeck.com

APIテストはスモールテストと比べて効果は大きいがコストも大きい

私はAPIテストの一番の課題はこれだと思っています。

APIテストは、「APIに対してリクエストを投げてそのままテストする」というわかりやすさがあります。

また、テストあたりにおけるカバレッジの広さというメリットもあります。

なので、できればデメリットであるコストの大きさをなんとかしたいわけです。

この課題の解決には2つの方法があります。

  1. コストを小さくする
  2. 範囲を広げて効果を大きくする

先に、範囲を広げる方法について考えると

  • 作ったテストをそのまま負荷テストに使えるように
  • 作ったテストをそのまま本番環境のリグレッションテストにも使えるように
  • APIテストしていたらAPIスキーマとの合致も検証するように

というような方向性、言ってみれば一石N鳥にすることによって効果を大きくするというものになります。効果が大きければコストが気にならなくなるだろうという寸法です。

では、コストを小さくするにはどのようにするかというと「テスト自体を書きやすくする(実装コストを小さくする)」などがあるのですが、結局は実行コストの大きさ、特に実行時間の長さが際立ってきてこれを解決しないといけなくなります。

まず最初に解決する手段として挙げられるのは「テストの並列実行」だと思います。

しかし、並列実行にも限界があります。

その次に取れる手段は何でしょう?

インテリジェントにテストを実行する

もう全てを愚直に実行するのはあきらめて、効率よく実行していく必要がありそうです。

「効率よく」というのは、例えば、

  • 失敗しやすいテストから先に実行する
  • 失敗しないテストは通常は実行しない
  • 実行時間が平均的になるように分けて並列実行する

などです。

ちなみに、このようなアプローチは私が考えたわけではありません。

私は、Launchable社の方たちの発表やエントリを見て知りました。

そして、APIテストという実行時間の長いテストに向き合うようになって、このアプローチの効果に期待しはじめるようになりました。

APIテスティングツールに必要なのはテストケースごとのIDなのではないか

そして、タイトル回収です。

先述した「インテリジェントなテスト実行」を実現するためにはまず何が必要か?というと、「テストケースの一意な識別」です。

それぞれのテストケースを一意に識別できなければ「どのテストが失敗したのか」がわかりません。 識別できなければ特定のテストだけを実行することもできないし、指定の順番に実行するということもできません。

テストケースに識別子=IDがあれば、特定テストの実行や、任意の順番でのテスト実行も可能になります。

インテリジェントなテストを実現するための第一歩が「ID」だと考えます。

APIテスティングツールに必要なのは(まず)テストケースごとのIDなのではないでしょうか*2

IDによる識別や、IDを使った柔軟なテストの実行ができることで、コストを抑えたインテリジェントなテスト実行への道が開けます。

ちなみに、runnにおけるIDの利用については次のエントリをご覧ください。

zenn.dev

この考えに至るまでの話

実は、きっかけはこのXのポストです。

ここからテスト実行時間について意識して考えるようになり、今に至っています。

今回の発表やこのエントリは @zoncoen さんのポストへの私なりの回答となります。

考えるきっかけをくれた @zoncoen さん、発表する機会をくれた @ponkio_o さんありがとうございました。

*1:ここでいう「API」とはWeb APIやgRPCを指します

*2:多くのテスティングツールにおいては備えているものでもあります。ただ、私が開発しているrunnには最初なかったのです。ちなみに提案してくださったのは同じrunn開発者の @katzchum さんです

PHPerKaigi 2024に参加した #phperkaigi

今年も参加してきました。

phperkaigi.jp

聴講した発表へのフィードバックも終わり、このエントリで私のPHPerKaigi 2024はフィニッシュです。

#runn開発者会議

最近恒例の廊下での会議ですが、テストランナーのシンタックスの改善の方向性についてどうしてもしめじさん( @smeghead さん)から直接フィードバックをもらいたく、3日間「いつか会えるだろう」とフラフラしていました。 気にかけていただいた皆さんありがとうございました。無事邂逅できました!

runnのランブックの一級言語がYAML(?)なのでYAMLシンタックスをうまく使ってかつニーズを満たす構文が思いつけばいいのですが、もう一歩アイデアが降ってこない。。。というところまででした。しめじさんにも言われたのですが「一度作ってしまうとなかなか変えられない」ものなのでどうにか降りてきてほしいっ!!!

あとはカンファレンス駆動で、runnで使っているOpenAPIパーサパッケージの差し替えとか、変数展開に使っているデリミタのエスケープができるようにするなどして、とうとうv0.100.0になりました。

v1...

github.com github.com

キャッシュとCSRF

Day2のお昼は話したい人が目の前にいたので声かけた結果なかなかなさそうなメンツで行くことに。

CSRF対策としてのOriginヘッダの話で、 @hiro_y さんの発表と全く同じことを(おそらく発表を聞いていない上で)言っている @catatsuy さんがいて、「あーこれは古来からのCSRFトークンではない対策もアリだなあ」と思ったのでした。

「そーだいさんのキャッシュ話面白かった」というところからキャッシュ全般の話になり「最近のJSフレームワークやGraphQLクライアントは勝手に(?)キャッシュをしてくるのでムズカシイ(かなり意訳)」と、なるほどと聞いていました。

プロキシキャッシュの話ももう少し聞けば良かった...!

そんな話をしながら「普通に衣上下に縦に食べられない厚さのカツ」を体験できました。おいしかった。@ttabtt3 さんチョイスありがとうございました。

発表

今回出したプロポーザルはCLIツールのデザインについての話でした。発表内容は資料や動画を見ていただくとして、多くの皆さんに質問や反応をしてもらって良かったです。

speakerdeck.com

CLIツール、小さく作れるし、しっかり考えて作ると勉強になるし、Webアプリケーション開発にも通じるものが多いのでおすすめです。

コミュニケーション

時間制限をして思いつく限りを書いてみました

無事きんじょうさん( @o0h_ さん)を拉致(?)して懇親会に行けたのは良かったです。 私はきんじょうさんを知識蓄積型、広範囲学習型とみているのですが、いつも発表の内容が明快でかつ多彩で気になっていた方でした。 今回も「多分何聞いても回答が返ってきそう」と思って、きんじょうさんに色々質問していました。 ただ、すでに私にお酒が入っていたので、質問しきれたかというとそうでもないので、次回はカンペを用意していこうと思います。

@kaz_29 + @keita__Max のお二人の話も面白かったです。どこかのカンファレンスでネタになるかもしれないのであまり話せませんが、良いシナジーだなあと思いました。 あと、これも私からは詳細は書けませんが、みなさん @keita__Max さん見かけたら話しかけてみると良いと思います。めちゃくちゃすごかったし面白かった!

すごいといえば @katzchum さんと @tyamahori さんの「あるあるですが〜」から話していた全然あるあるではない「フィーチャーフラグ Lv100(命名 きんじょうさん)」は驚愕だった。たぶんこれもいつかカンファレンスで出てくると予想しています。

@77web さんと @KentarouTakeda さんに凸って聞いた「SymfonyとLaravel」話もめちゃくちゃ興味深かったです。一部 @77web さんがブログエントリに書かれていたので是非。書けない話もありますよね。

https://77web.hatenablog.com/entry/2024/03/11/200747

そういえば、そーだいさんの近くにいるといつも良い話を聞けるので良いです。ただ、いつもそうなので、良い話をするそーだいさん「以外」がいるのか気になっています。

あと、P山さんには「謎の世界的なオヤマ」推しをされてとても困りました。

ただ、印象に残したのは私のようです。

daisuki.nichiyoubi.land

これまたk1LoWさんとの交流によってもたらされたやつですが、「P山さんの凄さ」について聴いたのがすっっっごい心に残ってます。

こちらの凄い人を体験したい方はこちらへ

PHPerKaigi

会期中のタイムラインでも、廊下でも話題になった話なのですが、どのカンファレンスもいつも楽しいのですが、PHPerKaigiの懐の深さと楽しさは本当に素晴らしいと思います。

「PHPer」と冠していますが、もっとひろくエンジニアの人々に開かれているので、PHPerだったときからPHPerではない?ように感じる今でもとても居心地が良いです。

また来年も1年の集大成をぶつけられれば良いなあと思っています。

スタッフの皆さん、PHPerKaigiコミュニティのみなさんありがとうございました!

contextのキャンセル時にtesting.T.Cleanupのようにクリーンアップ処理を実行できるパッケージdonegroupを作った

contextのキャンセルとそれに対応したクリーンアップ処理

Goにおいてcontextはさまざまな値を伝搬させるために使用します。その1つがキャンセル信号です。

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

contextを通じてキャンセル信号を送ることによって、そのcontextが伝播した関係するある種のcontextグループでキャンセルに対応した処理を行うことができます。

このcontextのキャンセルに対応した処理を、このエントリでは「クリーンアップ処理」と呼ぶことにします。

クリーンアップ処理の具体例: Graceful shutdown

あるミドルウェア(プロキシサーバでもデータベースサーバでもよい)があったとして、そのミドルウェアにはメインの機能以外に「ログやメトリクスを定期的にまとめて外部サーバに転送する機能(ログ転送機能)」があるとします。

ミドルウェアのプロセスを停止する際に、ログ転送機能が転送中であれば転送完了までプロセスの停止を待って欲しいですし、次の起動時のためにどこまで転送完了したかの記録するまでプロセスの停止を待って欲しいと考えます。

このような、いわゆる「Graceful shutdown」の実現に必要な後始末処理は、本エントリにおける「クリーンアップ処理」の1つの具体例になります。

cancel() はキャンセル信号の伝搬しかしない

キャンセル処理の伝搬は大抵 context.WithCancel() で作成した cancel() を使って行います。

cancel() を実行することでcontextや、そのcontextから派生した子contextにキャンセル信号が伝搬され、その信号は context.Done() から補足できます(実体は <-chan struct{})。

contextを引数で受け取っている各処理は、context.Done() から信号を受け取ったときにクリーンアップ処理を行うようにする(処理を書く)のですが、このクリーンアップ処理の実行の完遂は context.WithCancel()cancel() で実現しているキャンセル信号の伝搬の仕組みでは保証されません。

次のGo Playgroundに簡単な例を置きました。クリーンアップ処理が完了する前に main() が終了してしまい、クリーンアップ処理が完了していないことがわかると思います。

https://go.dev/play/p/F-4qnF5L9bf

クリーンアップ処理の実行を保証したい

少し複雑なGoのプログラムを書くと context はさまざまな箇所に渡されます。「(終了処理実現のために)関数の第一引数にcontextを渡す」という慣習もありますし、その深さ(どれくらい関数をまたいでcontextが渡されてきているか)は意図せず深くなっていきます。

また、goroutineの存在により並行処理もよく活用します。開始した並行処理は終了処理を書く必要があり、必要であればクリーンアップ処理を書く必要があります。そもそも context を渡しているのはgoroutineによる並行処理の終了処理のためでもあります。

終了処理のトリガーにはcontextのキャンセルを検知できる context.Done() を使います。

では、クリーンアップ処理はどう実装したら良いでしょう?クリーンアップ処理の完了を待つための仕組みが必要です。

testing.T.Cleanup

ところで、testing.T.Cleanup() はテストのクリーンアップ処理を保証するための仕組みです。t.Cleanupでクリーンアップ処理を登録しておくことによって、テスト終了時にそれらのクリーンアップ処理を実行してくれます。

私はcontextの文脈でもこの testing.T.Cleanup() のような機能が欲しいと考えました。

donegroup

上記のようなモチベーションで作成したパッケージが donegroup です。

github.com

donegroupはcontextのキャンセル時にtesting.T.Cleanupのようにクリーンアップ処理を実行できる仕組みを提供するパッケージです。

準備

まず context.WithCancel() の代わりに donegroup.WithCancel() を使ってcontextとcancel()の対を作成します。

そして、cancel() 実行後にクリーンアップ処理を待つため、donegroup.Wait() を実行します。

これだけで準備は完了です。

具体的には

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

ctx, cancel := donegroup.WithCancel(context.Background())
defer func() {
    cancel()
    if err := donegroup.Wait(ctx); err != nil {
        log.Fatal(err)
    }
}()

に書き換えるだけです。

クリーンアップ処理の登録( donegroup.Cleanup )

クリーンアップ処理の登録も簡単です。任意の場所で donegroup.Cleanup() を使ってクリーンアップ処理を登録するだけです。

err := donegroup.Cleanup(ctx, func(_ context.Context) error {
    // 何かしらのクリーンアップ処理
    fmt.Println("cleanup")
    return nil
})

これだけで、contextのキャンセル時にクリーンアップ処理の実行をしてくれます。クリーンアップ処理はいくつでも登録できます。testing.T.Cleanup() との違いはクリーアップ処理の実行順序の保証がないということだけです。

関数実行の保証( donegroup.Awaiter / donegroup.Awaitable )

donegroupは、次のように「実行に時間がかかる処理の実行を待つ(保証する)」こともできます。 この場合は donegroup.Awaiter() を使います。completed() が実行されるまで donegroup.Wait() は待ってくれます。

ctx, cancel := donegroup.WithCancel(context.Background())

go func() {
    completed, err := donegroup.Awaiter(ctx)
    if err != nil {
        log.Fatal(err)
        return
    }
    // 実行に時間がかかる処理
    time.Sleep(100 * time.Second)
    fmt.Println("do something")
    completed()
}()

// メインの処理
fmt.Println("main")
time.Sleep(10 * time.Millisecond)

cancel()
if err := donegroup.Wait(ctx); err != nil {
    log.Fatal(err)
}

fmt.Println("finish")

// Output:
// main
// do something
// finish

「この関数ブロックの実行を保証したい」というような場合は defer donegroup.Awaitable(ctx)() が使いやすいです。

go func() {
    defer donegroup.Awaiter(ctx)() // この関数ブロックの実行を保証する。
    if err != nil {
        log.Fatal(err)
        return
    }
    // 実行に時間がかかる処理
    time.Sleep(100 * time.Second)
    fmt.Println("do something")
}()

クリーンアップ処理の実行待ちにタイムアウトを設定したい

donegroup.Wait() の代わりに donegroup.WaitWithTimeout() を使うことでクリーンアップ処理待ちにタイムアウトを設定することができます。

この場合、donegroup.Cleanup()donegroup.Awaiter() donegroup.Awaitable()タイムアウトが伝播します。

具体的には donegroup.Cleanup() に渡すクリーンアップ処理 func(ctx context.Context) error の ctx にキャンセル信号が届くので ctx.Done() を使って適切にキャンセル処理を書きましょう。

donegroupのメリット

donegroupのメリットは、contextさえ渡しておけば関数シグネチャなどを変えずにいつでもクリーンアップ処理を追加実装できる点です。

また、クリーンアップ処理のエラーもまとめて受け取れます。

実装の深い場所でクリーンアップ処理が必要な並行処理を実装する場合も安心です。

というわけで

散らばっている並行処理の後始末に是非使ってみてください。

octocov-action v1 をリリースしました

追記 with.workdir:with.work-dir: に変更になっています

octocov-action v1 をリリースしました。octocov-actionはoctocovGitHub Actionsで使う際に使用するActionです。

github.com

今回、何が変わったかを紹介します。

Docker container actionからComposite actionに変更

Actionの実装方式をDocker container actionからComposite actionに変更しました。

これにおける効果は大きく2つあります。

octocovのバージョン指定が可能になった

1つ目は octocov のバージョンを柔軟に指定できるようになったことです。

というのもDocker container actionで ghcr.io/k1low/octocov を使っている限りはActionの外部からoctocovのバージョンを指定することができませんでした。

バージョン指定ができないと何が困るかというと、最新のoctocovでバグや意図しない動作が発生した際に1つ前のバージョンに固定するというワークアラウンドがとれないことです。

そのため、従来は octocov のバージョンと octocov-action のバージョンを同期させることで、バージョン指定ができるようにしていました。

今回 Composite actionにして任意のoctocovを指定できるようになりました( with.version: セクション )。

Actionのセットアップ速度が向上した

2つ目の効果です。

Docker container actionの場合Docker imageのpullとコンテナの起動が必要ですが、それが必要なくなりました。

そのかわり、途中でoctocovのリリースバイナリを取得する必要があるのですがそれでも体感で速いです。

今後リリースバイナリのキャッシュなどをするようにするとより速くすることが期待できます。

octocovの実行ディレクトリを指定できるようになった

with.work-dir: セクションを指定することでoctocovの実行ディレクトリを指定できるようになりました。モノレポな環境で便利だと思います。

このような新機能が追加しやすくなったのもComposite actionに変えた効果ともいえます。

Docker container actionのActionの今後

octocovがv0.xである限りはDocker container action版のoctocov-actionもメンテナンスしていく予定です。従来通り uses: k1LoW/octocov-action@v0 で使用できます。

というわけで

octocov-action v1 をリリースしました。 uses: k1LoW/octocov-action@v1 で何も設定を変えずにすぐに使用できますので是非ご利用ください。

今後もoctocovをどうぞよろしくお願いします*1

*1:そういえば、もし活用している企業様がいらっしゃいましたらこそっとでいいので教えてください。単純に喜びます

2023年の振り返りと2024年の抱負

2023年の振り返り

2023年は私にとっていろいろ思いを巡らせる年となりました。将来とか家族とか人生とか。内容は割愛しますが、HIPHOPクラシックを1つだけ貼っておきます。

open.spotify.com

2023年の抱負は「書く」でした。はじめてZenn Bookを書いたりなどしましたが、他にはそこまで書いていない気がします。ムズカシイ。

zenn.dev

あ、でも2023年後半に久しぶりに「描く」ことはできてとても嬉しかったです。

OSS

2023年も少なくなったかなと思います。

一方で、年初の宣言通り runn の開発を継続していました。 #runn開発者会議 もオンラインオフライン問わず何回も開催することができましたし、APIテストツールとしてイベントに呼ばれたり(runn開発者以外の)企業で使ってもらったりと、広く評価してもらえるようになりました。 一緒に開発してくれている@katzchumさんありがとうございます。

また、2manymws orgもプロダクションで使うパッケージとして鍛えてもらいました。 @pyama86さんありがとうございます。

今年の趣味OSSはどうなるんだろう?

でもrunnをもう少し良くしたい。v1にできる勇気が出ると良いなあ。

発表

個人的には頑張ったと思います。

私は体系的に学んだことを軸に発表するというのが苦手です。その分、多少王道から外れていても、自分で考え、作り、感じたことを集めてそのエッセンスを共有できると良いなと思っています。 2023年はどれも「自分こそが発表するべき」と思える特徴というかクセのある内容になったと思っています。

2024年の抱負

今年はなんだろう...

漠然と「新しい気持ちになりたい」というのはあるんですけど、「何を」「どうやって」というのは思いついていないし、「"気持ち"ってなんだよ。それってあなたの感想ですよね™️」という気持ちです。

とりあえずブラックフライデーに購入して深圳から届いたものの今の今まで手をつけていないブツが机の後ろに鎮座したままなので、それにちゃんと手をつけたいです。新しい気持ちにはなりそう。

今年もよろしくお願いします。

GoのHTTPミドルウェアやその周辺パッケージを配布する新しいGitHub Orgをはじめました

このエントリは GMOペパボエンジニア Advent Calendar 2023 および、 Go Advent Calendar 2023 シリーズ3 の19 日目の記事です。

以下のエントリでも少し触れられていますが、現在プロキシサーバをGoで書くプロジェクトがあります。

ten-snapon.com

k1low.hatenablog.com

主な実装をしているのは @pyama86 さんで、それはもうブルドーザーのように実装が進んでいるわけですが*1、私も少しだけですが書いています。

必要そうな機能をGoの薄いHTTPミドルウェアハンドラとしてOSSとしていくつか切り出していました。

そして、本体の実装が進むにつれて足りない機能を Pull Request ベースでもらったりしていたのですが、関連OSSを作る必要が発生したりなど個人リポジトリだとまとまらないため、それらをまとめるためだけにGitHub Org を新設しました。

github.com

既に私のリポジトリからのトランスファーは済んでおり、今後はこのOrg上で開発していく予定です。

どうぞよろしくお願いします。

ごく簡単にですが、置いてあるリポジトリについて紹介したいと思います。

rl

github.com

レートリミットを実現するHTTPミドルウェアハンドラです。

Goにおけるレートリミット実装としては go-chi/httprate が有名だと思います。rlもgo-chi/httprateを参考に実装しています。

rlの特徴は interface を満たす実装をつくって注入するだけで

  • http.Requestの情報をもとに異なるレートリミットのルールを動的に適用できる
  • 複数のレートリミットのルールを動的に重ねがけできる

というものです。

「あるホストへのアクセスだけレートリミットを緩和したい」だとか「あるIPからのアクセスだけレートリミットをかけたい」というユースケースに対応するためのものです。

rp

github.com

リバースプロキシサーバです。HTTPミドルウェアハンドラではないです。

rpの特徴は interface を満たす実装をつくって注入するだけで

  • http.Requestの情報をもとに異なるUpstreamを動的に指定できる
  • tls.ClientHelloInfoの情報をもとに証明書を動的に指定できる

というものです。

マルチテナントなサービスの前段などで必要になる機能を想定しています。

rc

github.com

HTTPキャッシュミドルウェアハンドラです。

rpの特徴は interface を満たす実装をつくって注入するだけで

  • RFC 9111にしたがってキャッシュを使用してくれる

というものです。また、

  • キャッシュルールの拡張や切り替えも可能
  • http.Requestの情報をもとに異なるキャッシュルールを適用する

ということも可能です。

rcutil

github.com

rcのためのユーティリティパッケージです。

rc.Cacher の interface を満たしたディスクキャッシュ機能の提供などをしています。

mm

github.com

HTTPミドルウェアハンドラを管理するHTTPミドルウェアハンドラです。

mmの特徴は interface を満たす実装をつくって注入するだけで

  • http.Requestの情報をもとに異なるHTTPミドルウェアハンドラを動的に指定できる

というものです。

動的に切り替える仕組みがないHTTPミドルウェアハンドラに動的切り替え機能を付与したいときに使うイメージです。

ちなみに、すごく短いコードなので全部貼り付けておきます。

package mm

import (
    "net/http"
)

// Builder is a middleware builder.
type Builder interface { //nostyle:ifacenames
    Middleware(req *http.Request) (func(next http.Handler) http.Handler, bool)
}

type mMw struct {
    builders []Builder
}

func newMMw(builders []Builder) *mMw {
    return &mMw{builders: builders}
}

func (mm *mMw) Handler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        for _, b := range mm.builders {
            mw, ok := b.Middleware(r)
            if !ok {
                continue
            }
            next = mw(next)
        }
        next.ServeHTTP(w, r)
    })
}

// New returns a new middleware-middleware.
func New(builders ...Builder) func(next http.Handler) http.Handler {
    mm := newMMw(builders)
    return mm.Handler
}

以上、と思ったら、先ほど rlutils というパッケージが誕生していました。

github.com

2manymws*2、どうぞよろしくお願いします。

*1:わかる人にはわかると思います。是非間近で見てほしいので、こちらから応募してください。https://recruit.pepabo.com/

*2:名前の由来は2文字リポジトリが多かったことと、Too manyなミドルウェアを置く場所ということと、例のあれです