Productive Outs

価値ある凡打を積み重ねる

elasticsearchのrefreshに関して

はじめに

elasticsearchにはRefresh APIなるものが存在する。
ドキュメントをインデックスしたら即検索できるようになると考えていた自分は、このrefreshを意識せずelasticsearchを使用していたため、elasticsearchの絡むテストを書くときにハマりかけた。今回はrefreshに関する備忘録を残しておく。

なぜrefreshが必要なのか?

前提として、elasticsearchではドキュメントをインデックスしただけで検索可能な状態になるわけではない。追加されるドキュメントは一旦In-memory bufferという領域に溜め込まれた上で、まとめて検索対象にとして反映されるようになっている。
このIn-memory bufferに溜め込まれたドキュメントを検索対象として反映する処理がrefreshと呼ばれている。

Near real-time search | Elasticsearch Guide [8.5] | Elastic

普段はなぜrefreshを意識せずに使えるのか?

elasticsearchでは上記で述べたrefreshを1秒間隔で自動的に実行している。1秒という短い間隔で追加したインデックスが検索可能の状態になるため、通常の利用時はrefereshを意識する必要がない。 (elasticsearchが「"ほぼリアルタイム"の検索プラットフォーム」とされている理由は、厳密に言うと検索できるようになるのが1秒後だからだと思われる。)

テスト時に困ったこと

「通常の利用時はrefereshを意識する必要がない」と前述したが、テスト時はそのようには行かない。
例えば、FactoryBotでレコードを作成して、検索によって作成したレコードが表示できるかを検証するspecを書くとする。
FactoryBotでレコードが作成されてからアサーションが実行されるまでの間でrefreshが実行される保証はないため、目的のレコードのドキュメントが検索可能ではない状態でアサーションが実行されてしまいテストが落ちるということが発生し得る。自分の参加している案件では、これが原因でテストがflakyとなってしまっていた。
FactoryBotでレコードを作成し、アサーションが実行される前に明示的にrefreshを行う必要があった。

明示的なrefreshを行う

ドキュメント単位でのrefreshの場合はこのRefresh APIを使用する。 今回の案件ではelasticsearch-railsを使用していたため、以下のようにしてindex_documentにrefreshオプションを渡すことで明示的なrefreshを行った。これにより、アサーションが実行される際にドキュメントが検索可能な状態になっていることが担保できるようになり、flakyテストを解消することができた。

factorybot_created_record.__elasticsearch__.index_document(refresh: :wait_for)

refreshに渡すことができる引数は以下。

true:デフォルトでは1秒間隔で実行されるrefreshを即時で実行させる。
:wait_for:次のrefreshが実行され、documentが検索可能になるまで待つ。trueとは違い、強制的にrefreshを実行させるわけではない。
false(default):refreshに関連するアクションは行わない。デフォルトの1秒間隔でrefreshが行われる。

疑問点

最初はrefresh: trueに設定していたがflakyテストは解消せず、refresh: :wait_forに変更したところテストが落ちなくなった。refresh: trueは即時refresh、refresh: :wait_forはタイミングは変えずに定期refreshを待つという認識だったので、refresh: trueでflakyテストが解消しなかった理由がわからん。
refresh: :wait_forでは「可視化」を待つという点が重要だったりするのか・・・?)

Wait for the changes made by the request to be made visible by a refresh before replying.

Rails6.1にアップグレードしたらwice_gridでFrozenErrorが出るようになった話

仕事でRailsのアップグレードを進めていたときに出会ったエラーの内容をゆるくまとめておく。

Rails6.1+wice_gridでFrozenErrorが発生

Rails6.0からRails6.1へのアップグレードを行った際に動作確認をしていると、ソートや検索機能を備えたテーブルを表示するページでエラーが発生した。 エラー内容は FrozenError (can't modify frozen ActiveRecord::ConnectionAdapters::PostgreSQL::Column ~) で、バックトレースを確認するとどうやらwice_gridというgemの内部でエラーが発生していることが分かった。

wice_gridはテーブルにソートやフィルタリング機能などを簡単に付けられるgemで、あまり頻繁に更新はされていないのでRails6.1での動作に関しては保証されていなかった。

github.com

原因調査

さらにgem内部を見ていくと、wice_gridのtable_column_matrix.rbの以下のコードで問題が発生していることが分かった。 エラーが発生していた行は4行目のself[model].each_value { |c| c.model = model }の部分。 エラーメッセージに書かれている通り、freezeによって変更が禁止されているActiveRecord::ConnectionAdapters::PostgreSQL::Columnに対して、新たな属性を追加しようとしてエラーが起きていた。

def init_columns_of_table(model) #:nodoc:
  self[model] = HashWithIndifferentAccess.new(model.columns.index_by(&:name))
  @by_table_names[model.table_name] = self[model]
  self[model].each_value { |c| c.model = model }
end

Rails6.0と6.1で何が変わったのか

上のコードはRails6.0では問題なく動いていたため、Rails6.1でActiveRecordの何かしらの仕様変更があった可能性を疑った。それぞれの環境のRails consoleで確認をしてみると、以下のようにRails6.0ではModel.columnsで返ってくる要素はfreezeされていないのに対して、Rails6.1からは要素がfreezeされるように変わっていたということが判明。

Rails6.0

Model.columns.first.frozen?
=> false

Rails6.1

Model.columns.first.frozen?
=> true


Rails6.1のコミットを追ってみると、2020年5月に以下のコミットでcolumnsメソッドで返る要素をfreezeさせる変更がされているのを発見。

github.com

発端は以下のディスカッションで、「Model.column_namesがコピーではなく参照を返しているので"!"を付けたほうが良くない?」→「ほんならきちんとfreezeするようにしといたほうがええやろ!PR作っといたで!」みたいな流れの中で上のPRが出されたっぽい。

discuss.rubyonrails.org

どのように解決したか

この記事ではwice_gridの内部のコードにあまり言及していないため詳細は省くが、ActiveRecord::ConnectionAdapters::PostgreSQL::Columnに新たな属性を追加せずとも必要なデータの受け渡しを行う方法が他にもあったため、その変更を加えるモンキーパッチを当てて対処した。

まとめ

今回のエラーで原因の特定に苦労した理由として、Railsのアップグレードガイドやリリースノート内に上記の仕様変更が書かれていなかった点が大きかった。おそらく「推奨されない書き方が禁止されるようになった」という話なので特にリリースノートに書くまでもないと判断されたのだろうと勝手に思っている。このエラーのおかげでgem内部のコードをしっかり読んだり、モンキーパッチの作成をする機会ができたので良かった。厄介なエラーも時が経つと良い思い出になるのかもしれない…。


↓「アウトプットサボってんじゃねーよ」と言われた気がした。。。がんばります。 f:id:kyouashita18:20210821013146p:plain

人生ではじめてLT会で発表してみた

先日、参加しているFJORD BOOT CAMP(フィヨルドブートキャンプ)で行われた「初めてのLT会Vol.4」で、人生初LTをさせて頂きました。「初めてのLT会」は、LTにまだ慣れていない方でもクローズドな環境で気軽に発表の練習ができる場というコンセプトでフィヨルド内で定期的に行われる発表会です。今回は、初めてLT会で発表をしてみての感想を書いていきたいと思います。

完全に見切り発車で参加を決意

私がフィヨルドに参加してから2回ほど「初めてのLT会」は開催されていましたが、どちらも都合が付かず見る側としても参加することができませんでした。今回は参加できそうだったので、まずは見る側として参加して雰囲気でも掴めればいいかなと思っていました。

しかし、オーガナイザーの方から「登壇者がまだ集まっていないからやりませんか」と声をかけられ登壇者としての参加を決意。この時点で何をテーマにするかなどは全く見当もついておらず、完全に勢いだけで見切り発車を決めた形でした。

テーマ設定の自由度が高いので話題には困らなかった

今回のテーマは「フィヨルドブートキャンプで学んだこと」であればなんでもよいという形式でした。LT会の特徴でもあると思うのですが、テーマの自由度が高く何を話しても良いというような場合が多いので、私のように「とりあえずLTをやってみたいけどテーマ設定が難しそう・・・」と悩んでいる人でも気軽に参加できるのはとても良いと思いました。

もちろんテーマ設定には悩みましたが、話せることが何も浮かばないという悩みではなく、多くの選択肢からテーマを絞り込むことに悩んだという感じでした。なんせ今まで学んできたことに関係することであれば何でも良いので、テーマとなり得る候補はいくらでもあるわけです。(と言ってもLTとしてまとめられそうなイメージが湧いたものは限られますが・・・)

今回はフィヨルド内でのプラクティスで苦戦したOAuthをテーマにしようと決めました。

たかが5分。されど5分。

LT会のLTとはLightningTalkの略で、「稲妻のように短い間に」発表するという意味が込められているそうです。なので一般的には5分程度の時間設定で発表が行われます。今回の「初めてのLT会」では5〜10分以内という時間設定がされていました。

今回参加を決めた理由として「5分くらいのプレゼンならパパっと作れるんじゃないか?」と高をくくっていた部分も多少ありました。しかし、発表内容の作成に取り組んでみて思い知らされます。

「短くまとめるのって結構難しい」

なるべく短い間にまとめようとすると、余程限定的なテーマでない限り必ず「省かなければならない部分」が出てきてしまいます。この情報の取捨選択が難しい。OAuthに関する前提知識がない方にも分かりやすく説明しようと思うと、どうしてもあれこれ言及したくなってしまうんですね。ただ、この取捨選択を行っているうちに自分の頭が整理されて、本当に重要な部分が少しずつ見えてくるのも感じました。こういった点も短い時間で行うLTが支持されている理由なのかなと思いました。

「LTなんだから5分ちょっとで分かりやすくまとめたる❗❗」と息巻いていましたが、結局7〜8分に収めるのがやっとでした。

発表本番、緊張すれど、やっぱり楽しい

発表本番の日。私はテーマの都合上、発表順が一番最後になりました。 まさかのトリということで、他の方の発表を聞きつつ頭の半分では自分の発表のことを考えていました・・・。ただ他の登壇者の方のプレゼンが素晴らしく、そのような状態の私でも分かりやすい説明でとても良い学習になりました。LTの短時間という制限は聞く側にとっても負担が少なくて良いですね。

自分の発表では緊張はしましたが、Slackで参加者の方からの反応が沢山見られて楽しめたと思います。やっぱり自分が作ったものに対して温かい反応がもらえるのはありがたいですね。

発表資料

speakerdeck.com

見切り発車でも良いので挑戦してみて!

今回参加してみて、LT会の利点は発表者の負担が少ないという点だと感じました。短くまとめるのは難しいとは書きましたが、やはりテーマの自由度や5分程度という時間設定を考えると一般的なプレゼンと比べてハードルは 低いと思います。なので「これちょっと気になるから調べるついでにまとめてLTで話そうかな」とか「ここの理解が浅いからLTで話して自分のおさらいにしよう」といった感じに、気軽に参加しちゃって全然OKだと私は思います。

今回はフィヨルド内でのLT会でしたが、他にも初めてLTをする人向けのLT会は開催されていると思うので、「LT会やってみたい❗」という方はその勢いに任せて参加してみてください!

やらかした時に助けてくれるGitコマンド備忘録①

はじめに:ブログを書くに至った経緯

プログラミング初心者にとってGitやGitHubって慣れないうちは触るのが結構怖い・・・。

自分も「本当にこのコマンドで大丈夫?」とか「せっかく作ったコードが上書きされて消えたらどうしよう・・・」とか思いながら恐る恐る操作をしていた。

なぜ怖いのか考えてみると「取り返しのつかないミスをしたらどうしよう・・・」という気持ちが恐怖心に繋がってることに気づく。

あいにくGitは優しいので、大体のミスは取り返しがつく。

それならミスの取り返し方をまとめておけば便利ではないかと思い、ブログを書くことにした。

その1:前のコミットからコードを変更したら動かなくなった!

「コードを変更したら動かなくなった」という時には、動いていたときのコミットを指定してgit reset --hardをすれば良い。

コミットの指定はHEADを使用した相対位置指定でも良いし、コミットIDをそのまま指定しても良い。

$ git reset --hard HEAD~


その2:不要なファイルをステージしてしまった!

間違って必要ないファイルをgit addでステージしてしまったときはgit reset (--mixed)でインデックスを一旦リセットする。 作業中のファイルの状態は変更されない。

$ git reset (--mixed) HEAD~


その3:間違えてresetを実行してしまった!

もちろん、git reset自体も取り消すことができる。 git reflogを使うと以下のように、git resetを行った履歴まで確認できる。

戻したい位置のコミットIDを指定してgit resetをしてやればOK。

$ git reflog
af6a6ac (HEAD -> master) HEAD@{0}: reset: moving to HEAD^^
8a2ca57 HEAD@{1}: reset: moving to 8a2ca576c
8a2ca57 HEAD@{2}: commit: 1行目と2行目を修正
8938aed HEAD@{3}: commit: 2行目を追加
af6a6ac (HEAD -> master) HEAD@{4}: commit (initial): first commit

ちなみにこのgit reflogブランチ削除の履歴も残してくれるので「必要なブランチを削除してしまった❗」というときにも使える最強のコマンド。


その4:コミットするブランチを間違えてしまった!

ブランチを分けて作業するはずが、ずっとmasterブランチにコミットしてしまっていた!ということはよくある。 そんな時に使えるのがgit cherry-pickコマンド。

例えば以下のように、developmentブランチで行うはずだった作業を間違えてmasterブランチにcommitしてしまっていた時を考えてみる。

$ git log --oneline
f76b506 (HEAD -> master) developmentブランチで4行目を追加
2312bd3 developmentブランチで3行目を追加
8a2ca57 1行目と2行目を修正
8938aed 2行目を追加
af6a6ac first commit

もともとブランチを作る予定だった位置にブランチを作成。 git checkoutでブランチを切り替える。

$ git branch development 8a2ca57
$ git checkout development
Switched to branch 'development'

masterのログを調べて、developmentブランチに移動させたいコミットのIDを調べる。 移したいコミットのIDを指定してgit cherry-pickを実行。

これでdevelopmentブランチにコミットがコピーされた。

$ git cherry-pick 2312db3
$ git cherry-pick f76b506

あとはmasterに残っている不要なコミットを削除する。

$ git checkout master
$ git reset --hard 8a2ca57
HEAD is now at 8a2ca57 1行目と2行目を修正

確認してみると、理想とする形になったのがわかる。

$ git log --oneline --decorate --graph master development
* 9f1cc9f (development) developmentブランチで4行目を追加
* 46f25f6 developmentブランチで3行目を追加
* 8a2ca57 (HEAD -> master) 1行目と2行目を修正
* 8938aed 2行目を追加
* af6a6ac first commit


その5:コミットメッセージを間違えてしまった!

コミットした後にコミットメッセージを修正したくなることは結構ある。 単純に間違えてしまった時や、もっといい表現が思いついた時にはgit commit --amendを使うと良い。

以下のコマンドを実行するとエディタが立ち上がり、直前のコミットメッセージを書き換えることができるようになる。

$ git commit --amend

また、このコマンドは直前のコミットに変更点を追加したいときにも便利。 新たにコミットを作成するほどでもない小さな変更点が見つかったときには、対象ファイルをステージした後git commit --amendを実行すればOK。

まとめ

今回は基本的な救済コマンドをまとめたが、まだまだ自分の知らない便利なコマンドは大量にあるため定期的に備忘録としてまとめていきたい。

やらかした時の対処法を知っておけばGitの操作に抵抗がなくなり、開発の効率やコミットの質も向上すると思う。

ただ、Gitの救済措置に甘えすぎず、イージーミスを減らす努力も忘れないようにしておきたい・・・。

参考URL

git reset コマンドの使い方と、主要オプションまとめ | WWWクリエイターズ

git reset --soft, --mixed, --hard の違い - yu8mada

Gitでブランチを作るのを忘れてmasterにコミットしてしまったときの対処法 - Qiita