1. tanaken on Rails
  2. #014: ActiveRecord::Transact..
2024-04-06 20:01

#014: ActiveRecord::Transaction, ActiveJob enqueue_after_transaction_commit, queries count in rendering log

00:01
はい、こんにちは。第14回のTanaken on Rails始めます。今週は3つのプレリクエストを紹介します。
1つは、Allow to register transaction callbacks outside of a recordというプレリクエストです。
こちらは、アクティブレコードに関する変更です。
アクティブレコードのトランザクションは、普段お仕事でもよく使っているというか、これなしではなかなか成り立たないような感じになっていると思いますが、
今回のプレリクエストでは、ActiveRecord://base.transactionに渡すブロックの引数に、
ActiveRecord://transactionのオブジェクトを受け取れるようになりました。
そういう変更があります。
受け取ったブロックの引数、そのトランザクションのオブジェクトに、
こいつに対してコールバックを登録できるようになっています。
具体例を見てみます。
例えば、ArticleというモデルがあるRailsアプリケーションで、
この実装でやりたいことが、Articleというオブジェクトがあって、
そいつをアップデートすると、Published True、公開しますと、
公開というフラグをTrueに更新すると言って、
その後、更新したよというお知らせのメールを、
非同期処理、非同期メールジョブで送ると、
そういう処理を今やろうとしているとしましょう。
トランザクションで囲って、
ArticleModel.TransactionDoEndで、
そのブロックの中で、記事の更新処理と、
メールの通知処理をやるという前提でいきましょう。
もう一度、Article.TransactionDoで、
そのブロックの引数としてトランザクションを受け取れると。
ブロックの中では、Article.Update括弧Published True、
その後、ブロックの引数で受け取ったトランザクションを使って、
Transaction.AfterCommitDoEndという形で、
そのAfterCommitメソッドに渡すブロックの中に、
Published Notification Mailer、
Publishedしたお知らせメールを送るよという形で、
Deliberator、メール処理を非同期で送るよという処理を書くと。
03:02
そんな感じで、
Article.Transactionのブロックに渡すブロックの引数、
こいつをそのブロックの中で使えて、
その引数はトランザクションのオブジェクトですよと。
で、AfterCommitというメソッドで、
このトランザクションがコミットされた後に、
メールを送信しますと。
そんな処理が書けるようになりましたよと。
便利そうですよね。
またさらにこのプレリクエストでは、
ActiveRecord://base.current__transactionというメソッドも追加されています。
Current Transactionというその名の通り、
そのメソッドの呼び出し時点におけるトランザクションのオブジェクトを扱えるようになっています。
なので先ほどの例は、
ArticleModel.TransactionDoで、
そのブロックの中にブロックの引数として、
トランザクションを受け取って処理するというようなことを書いたんですけど、
今回のCurrent Transactionというメソッドを使えば、
トランザクションというブロックの引数を受け取らずとも、
Article.CurrentTransactionとかってやれば、
現在のトランザクションが取れるので、
さっきと同じようにそこにメソッドチェーンして、
AfterCommitメソッドを呼び出してあげれば、
このトランザクションがコミットされた後に、
そのDoEndでそのAfterCommitに渡すブロックの中の処理を実行できるよと。
そんな感じになりますよというところですね。
なので、このCurrent Transactionというのも便利ですね。
Current Transactionが便利なのは、
わざわざ引数としてトランザクションを渡したりとかしなくて済むので、
今のトランザクションを教えてよという感じでできるので、
例えば、メールの送信処理のところだけプライベートメソッドに切り出してみたりとかするときに、
引数にトランザクションとか渡さなくても、
今の現在のトランザクションの中で、
AfterCommitでメール送信処理をしてよみたいなことが書けるというのが便利なのかなというふうに思いましたね。
なので、現在のトランザクションを教えてくれるという便利なメソッドになっています。
さらにこのブリリクエストでは、
ActiveRecord.AfterAllTransactionsCommitというメソッドも追加されています。
このメソッドもブロックを受け取るメソッドで、
このブロックの中の処理は全てのトランザクションがコミットされた後に実行されるという感じになっています。
06:11
なので、とにかくこのAfterAllTransactionsCommitというブロックにブロックを渡してあげれば、
そのブロックの中はとにかく全部のトランザクションがコミットされた後に動くということができますね。
さっきの記事を更新してメールを送るという処理も、
これ自体も一つのメソッドに例えば切り出してあげて、
例えばPublishArticleで引数にアーティクルを受け取るようなそういうメソッドに切り出すこともできるでしょうと。
ただそういうメソッドに切り出すと、このメソッドがどこで追われるのかはわからない。
呼び出し元次第になるんですよね。
トランザクションの中で呼ばれるかもしれないし、そうではないところで呼ばれるかもしれないというところがあって、
メソッドに切り出すとその呼び出し元でどう呼ばれるかは制御ができないですよね。
そんなときにこのメソッドの中でアーティクルのアップデートをしつつ、
今紹介したActiveRecord.AfterAllTransactionsCommitDoEndでその中にメール送信処理を書いてあげるというようなことをやると、
どこで呼ばれるかわからないが、最終的にはトランザクションが全部コミットされた後に、
このブロックの中の処理が呼ばれる、つまりメール送信処理を呼べるということになるので、
どこで呼ばれるかとかあまり気にせずに、とにかくこいつは一番最後に、メール送信は一番最後なんじゃっていうような処理が実装できるというので、
これは便利だねという感じですね。
そんなところで一つ目のプレリクエストでは、
アクティブレコードのトランザクション周りでコミット、アフターコミット、コミットの後の処理を明示的に指定したりできるようになっているというところですね。
あとは純粋にトランザクションに渡すブロックの引数でトランザクションを受け取れると、
現在のトランザクションを受け取れるというところも便利ですよね。
というふうになっていますというのが一つ目でした。
ちょっと1個目が長かったんですが、2個目に行きます。
2個目は、Automatically Delay Active Job Enqueues to After Commitというプレリクエストです。
こちらはアクティブジョブに関する変更です。
このアクティブジョブに関する変更なんですが、今長々と先ほどまで紹介していたアクティブレコードトランザクションに関する実装とすごく関わりのあるプレリクエストです。
09:01
というのもプレリクエストを出したオーサーの方が同じ方ですね。
何をやっているかというと、アクティブジョブってよくある落ち入りやすいミスとして、
DBトランザクションの中でジョブを延期してしまって、
そのトランザクションがコミットされる前にジョブが実行されちゃう。
それが意図しない結果を生むということがよくありがちなミスとしてあるんですよね。
さっきも紹介していた記事、アーティクルを公開してお知らせメールを送るという例でちょっと考えてみましょうと。
アーティクルモデルのクラスメソッドとしてアーティクル.トランザクションドゥエンドで括って、
その中でアーティクル.アップデート、パブリッシュトゥルー、
その後すぐにパブリッシュノーティフィケーションメイラー、デリバレーターでメールを送ると。
これも全部トランザクションの中ですね。
トランザクションの中でまだ後続の処理があるとして、
例えばちょっといろんな処理があるかもしれないんですけど、
例えば今回は3秒待つという処理を書いておくことにしましょうと。
そうすると記事が更新されてすぐに記事の更新処理としてのクエリがトランザクションの中で用意されて、
パブリッシュノーティフィケーションメイラーのジョブが積まれて、
積まれた途端にすぐ別のプロセスでメール送信処理が実行が始まったとしましょうと。
呼び出し元のトランザクションの方では今3秒間待っている状態ですと。
その3秒間待っている間にメール送信処理が動いちゃったという状況を想定してみましょうと。
そうすると記事の更新、そのパブリッシュドトゥルーというのはまだ反映されていない、
DBにコミットされていないので、コミットされる前の段階でメールが送られちゃっているということになると。
そうすると少なくともその3秒間はメールはまだ非公開なのに公開されましたよという、
メールじゃない記事、記事は非公開なのに公開されましたよというメール通知が届いてしまうということになりますよね。
そうするとその3秒間の間にメール受け取って記事を開いたら、
あれ非公開だからまだ見れないじゃんみたいな感じになったりするわけなんですよね。
またあるいは今3秒待っているという前提ですけど、その3秒間でまた別の違う処理があってエラーになりましたと。
12:02
そうするとトランザクションはロールバックされるので、記事の公開するという処理もなくなって、
非公開のまま、永続的に非公開のままになっちゃいますと。
にも関わらずメール送信のプロセスはもう完了していて、メールは送っちゃいましたってなると、
記事はずっと非公開なのにリンクがユーザーにメールで届いていて、
クリックしてみようとするとエラーになるみたいな。
なんかそんなことが起きたりしますよねと。
なのでこういう状況を回避するためには本来は、
トランザクションの外でメール送信のジョブを積むとか、
あるいはさっきの1個目のプレリクエストで紹介した、
アクティブレコード.アフターオールトランザクションコミットを使うなどして、
全部のDB更新のトランザクションがコミットが完了した後に、
ジョブが実行されるというような対応をしておく必要が本来あるんですよと。
だけれどもこれって結構落ち入りがちだし、
毎回チェックするのは大変ではあるんですよね。
という課題感があって、今回の2つ目のプレリクエストでは、
自動的にトランザクションがコミットされるまでは、
ジョブの延期を遅延するように実装がされています。
なので今まで話していたようなよくあるミスみたいなものを意識して、
対応する必要がなくなったってことですね。
だから何も気にせずにトランザクションの中で、
メール、メーラー、デリバレーターとかやっても、
最終的にアクティブジョブの方で、
吉田に考えてくれて、お膳立てしてくれて、
トランザクションがコミットされるまではジョブを延期しないと。
というような対処理に変わってくれたということですね。
これはありがたい話ではありますね。
どんどんアプリケーションエンジニアが、
この辺を意識せずにコードを書いてしまうことに、
どんどんなっていくなという感じではあるけど、
Nailsは何も考えずに書けるみたいなのが強みでもあるんでね。
なかなかうーんと思いつつも便利ではありますよね。
ただ一方で今回の変更によって、
トランザクションのコミットを待たずに、
ジョブを延期したいんだっていう人がいたときに困っちゃいますよね。
全部がトランザクションのコミットまで、
15:00
ジョブの実行を遅延するようになっちゃったんで、
そうすると、いやいや、俺は知っててわざと、
トランザクションのコミットを待たずに、
ジョブを実行したい理由があるんだみたいな、
そういうこともあるでしょうと。
あんまりない気もしますけど、
あるでしょうと。
今までできてたことなんでね。
できなくなっちゃうと困っちゃうんで。
そういう場合に対応するために、
今回の実装では、
各ジョブのクラスで、
延期アフタートランザクションコミットという、
クラスアトリビュートを設定できるようになっています。
このアトリビュート、
延期アフタートランザクションコミットというアトリビュートに、
フォルスを指定することで、
つまりトランザクションがコミットした後に延期するよという、
フラグを落とすんですね。
フォルスにすることで、
コミットを待たずに、
トランザクションのコミットを待たずに、
すぐにジョブを延期できるということになります。
なので、例えば、
HooverJobという独自の、
それぞれのアプリケーションであるでしょう。
何らかのジョブクラスがあると思うんですけど、
そのアクティブジョブを継承した、
HooverJobみたいなジョブの中で、
セルフ.延期アフタートランザクションコミットイコールフォルス
とかって書いておいてあげると、
こいつは遅延されずに、
すぐにジョブを延期できるという実装になってますよということですね。
という形で、
ちゃんと広報互換性も考慮しつつの実装になっているということでございます。
最後は簡単なお話ですね。
3つ目が、Adopt Queries Count to Template Rendering Instrumentationですね。
こちらはアクションビューに関する変更です。
チェンジログを見ると、
アクションビュー配下のチェンジログに更新の記録が書いてあるんですけど、
実装の中身を見ると、
アクティブレコードの変更にはなっているかなという感じですね。
やっていることはシンプルで、
例えばアクション単位でレールズのコントローラーのあるアクションに
リクエストが来たときにログが出ると思うんですよね。
コンプリーティットとか200OKでしたとか何秒かかりました、
例えば何ミリセカンドかかりましたとかっていうのがあって、
その構成要素としてビューズのレンダリングに何ミリセカンドかかりましたと、
18:05
アクティブレコードに何ミリセックかかりましたみたいなログが出るようになっているわけなんですよ。
そのログのアクティブレコードに関するログをちょっと充実させましたということで、
何が増えたかというと、アクティブレコード何ミリセックって書いてある横に
かっこ書きで何個のクエリが呼ばれたよと、
何個のクエリはすでにキャッシュドなクエリが呼ばれたよみたいな
そういう件数の表示がされるようになりましたと。
例えば1個のアクションですごくいくつかのクエリを呼んで
レンダリングするみたいなことはあると思うんですけど、
その時に何個のクエリが呼ばれているのか、
100個も1000個もクエリが呼ばれていたら何かおかしいよねって気づけたりするわけなんですよね。
それは例えばNプラ1問題が起きているのかどうかとかね、
そういうのが分かったりするんで、
そういう開発者向けのヒントとして、
クエリの件数が出ていると便利なんじゃないかということで、
今回の3つ目のプレリクエストが出されたという感じのようですね。
そんなところで、今日ちょっと長くなっちゃいましたが、
3つのプレリクエストを紹介しました。
1つ目がアクティブレコードのトランザクションに関する便利なる変更というのと、
2つ目がアクティブジョブで、
トランザクションがコミットした後にジョブが延期されるようになったよという話と、
最後はアクションビューでログが充実して便利になったよと、
そんなお話でございました。
そんなところで、今週の田中のレースを終わりにしたいと思います。
ではまた来週。バイバイ。
20:01

コメント

スクロール