Ruby の with_index を追う

f:id:masm11:20210122225424p:plain

こんにちは、masm11 です。

先日、Ruby の with_index メソッドを知り、衝撃を受けました。 今回は使い方を簡単に紹介し、更に with_index の実装に迫りたいと思います。

with_index の使い方

Ruby の Array には、Array#eachメソッドがありますね。

array.each do |item|
  puts item.to_s
end

のように使えば、各要素について処理することができます。

また、その添字も欲しいことがあります。そういう場合は、

array.each_with_index do |item, i|
  puts "#{i} #{item}"
end

のように、each の代わりに each_with_index を使えば、添字を同時に受け取ることができます。

しかし限界があります。Array には each_with_object というメソッドがありますが、 これの with_index 版 (要素と object と index の3つを渡してくれるもの) はありません。 つまり、

r = array.each_with_object_with_index('') do |item, obj, i|
  obj << "[#{i}:#{item}]"
end

なんてことはできないわけです。

そこで with_index の出番です。

r = array.each_with_object('').with_index do |(item, obj), i|
  obj << "[#{i}:#{item}]"
end

こうすれば添字も受け取ることができます。

with_index の実装

では、何がどうなって実現できているのでしょうか?

Array#each_with_object はブロックが与えられていない場合には Enumerator を返します。 つまり with_index は Enumerator のメソッドなのです。 each_with_object が繰り返すのではなく、each_with_object は Enumerator を返し、 Enumerator の with_index が繰り返しているのですね。

ソースコードを覗いてみましょう。

Enumerator は Ruby のソースコードの enumerator.c で定義されています。

https://github.com/ruby/ruby/blob/v3_0_0/enumerator.c#L4011

rb_cEnumerator = rb_define_class("Enumerator", rb_cObject);

ここから Enumerator クラスの定義が始まります。

そして、

https://github.com/ruby/ruby/blob/v3_0_0/enumerator.c#L4020

rb_define_method(rb_cEnumerator, "with_index", enumerator_with_index, -1);

これが with_index メソッドの定義です。実体は enumerator_with_index という関数にあるようです。

https://github.com/ruby/ruby/blob/v3_0_0/enumerator.c#L653-L662

static VALUE
enumerator_with_index(int argc, VALUE *argv, VALUE obj)
{
    VALUE memo;

    rb_check_arity(argc, 0, 1);
    RETURN_SIZED_ENUMERATOR(obj, argc, argv, enumerator_enum_size);
    memo = (!argc || NIL_P(memo = argv[0])) ? INT2FIX(0) : rb_to_int(memo);
    return enumerator_block_call(obj, enumerator_with_index_i, (VALUE)MEMO_NEW(memo, 0, 0));
}

これがその実体です。引数 argc, argv は with_index メソッドに対する引数の個数と引数そのもの、 引数 obj は Enumerator オブジェクトそのものを指すのでしょう。

    memo = (!argc || NIL_P(memo = argv[0])) ? INT2FIX(0) : rb_to_int(memo);

ここは、with_index メソッドの引数の処理のようです。 with_index は引数なしで呼び出すと添字は 0 から始まりますが、 引数でいくつから始めるかを指定することもできます。

よく見ていきます。

(!argc || NIL_P(memo = argv[0]))

引数がない場合、または最初の引数が nil の場合。

INT2FIX(0)

0 を Ruby の型に変換したものなのでしょう。

rb_to_int(memo)

こちらは memo を Ruby の整数に変換しているようです。 なお、memo は先程 NIL_P(memo = argv[0]) で代入されていて、つまり最初の引数です。

これで、引数に応じて開始の値が決まりました。

    return enumerator_block_call(obj, enumerator_with_index_i, (VALUE)MEMO_NEW(memo, 0, 0));

このあたりから Ruby の言語処理系に深く入っていくので挫折してしまったのですが、 各要素について enumerator_with_index_i が呼ばれるようです。 enumerator_with_index_i のコードは以下にあります。

https://github.com/ruby/ruby/blob/v3_0_0/enumerator.c#L619-L630

static VALUE
enumerator_with_index_i(RB_BLOCK_CALL_FUNC_ARGLIST(val, m))
{
    struct MEMO *memo = (struct MEMO *)m;
    VALUE idx = memo->v1;
    MEMO_V1_SET(memo, rb_int_succ(idx));

    if (argc <= 1)
        return rb_yield_values(2, val, idx);

    return rb_yield_values(2, rb_ary_new4(argc, argv), idx);
}

冒頭の部分はまずは飛ばして、

    if (argc <= 1)
        return rb_yield_values(2, val, idx);

この部分は、ブロック引数がもともと1つまでの場合の処理です。 その場合は、その引数と添字の2つを引数にして yield しているようです。

また、

    return rb_yield_values(2, rb_ary_new4(argc, argv), idx);

こちらの部分は、ブロック引数がもともと2つ以上の場合で、 その場合は引数を配列にまとめたものと添字の2つを引数にして yield しているようです。

で、冒頭の部分は何かというと、先程の

    return enumerator_block_call(obj, enumerator_with_index_i, (VALUE)MEMO_NEW(memo, 0, 0));

の引数に渡した (VALUE)MEMO_NEW(memo, 0, 0)

static VALUE
enumerator_with_index_i(RB_BLOCK_CALL_FUNC_ARGLIST(val, m))

m に渡ってきていて、

    struct MEMO *memo = (struct MEMO *)m;
    VALUE idx = memo->v1;
    MEMO_V1_SET(memo, rb_int_succ(idx));

と処理されています。1行目で型変換をして、2行目で v1 を取り出して、 3行目でそれに +1 したものを v1 にセットしているんですね。 v1 に添字が格納されている、というわけです。

取り出した値は先程見たように yield に渡されていました。

まとめ

Enumerator#with_index を紹介し、その実装を見てみました。

ふと思ったのですが、このコードは何故 C で書かれているのでしょうか? Ruby で書けるコードは Ruby で書いてしまった方がメンテしやすいと思うのですが。 ただ、毎回クラスを読み込むと起動に時間がかかるので、読み込んだ状態を dump しておく、 といった Emacs のような手法は必要になります。それを避けたいのかもしれませんね。 もしくは単にスピードを追求するためか。

ではまた!

Docker Composeを使って複数のコンテナを管理する

f:id:ingnis:20201120144028j:plain

こんにちは、にしむらです。

今回は複数のコンテナを定義してアプリケーションを開発する方法です。 Docker Composeによって、Rubyとデータベース(MySQL)のコンテナを連携させて開発ができるようにしていきます。

続きを読む

grape で URI パラメータとしてメールアドレスを受け取る

f:id:masm11:20210109214927p:plain

明けましておめでとうございます。masm11 です。今年もよろしくお願いします!

grape をご存知でしょうか? Rails で API を作る時に便利ですね。

今回は、grape を使った API で URI パラメータとしてメールアドレスを受け取ろうと してハマったので、ご紹介します。

環境を用意する

まず Rails 環境を用意します。

gem install rails
mkdir t2
cd t2
rails new .
echo "gem 'grape'" >> Gemfile
bundle install --path=vendor/bundle
bundle exec rails webpacker:install

grape を組み込んでいきます。

まずはルーティングから。

vi config/routes.rb
Rails.application.routes.draw do
  mount Test::API => '/'
end

API の実装を作成します。

vi app/api/test/api.rb
class Test::API < Grape::API
  version 'v1', using: :path
  format :json
  content_type :json, 'application/json'
  prefix :api

  resource '/test' do
    get '/:id' do
      { value: params[:id] }
    end
  end
end

ここではとりあえず URI パラメータとして :id を受け取るようにしてあります。

api の大文字が Api でなく API になるように設定します。

vi config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end

では起動してみます。

bundle exec rails s

API のエンドポイントは http://localhost:3000/api/v1/test/:id です。 ここにアクセスしてみます。

luna:~ % curl http://localhost:3000/api/v1/test/123
{"value":"123"}
luna:~ %

:id が受け取れていますね。

メールアドレスを受け取る

ではメールアドレスを受け取るように改造します。

vi app/api/test/api.rb
class Test::API < Grape::API
  version 'v1', using: :path
  format :json
  content_type :json, 'application/json'
  prefix :api

  resource '/test' do
    get '/:email' do
      { value: params[:email] }
    end
  end
end

params のキーが変わっただけですね。

先ほどと同様に curl でアクセスしてみます。

luna:~ % curl http://localhost:3000/api/v1/test/foo@example.jp
{"value":"foo@example"}
luna:~ % 

なんと .jp が消えています。

他のメールアドレスでも試してみます。

luna:~ % curl http://localhost:3000/api/v1/test/foo@example.co.jp

この場合は RoutingError になりました。

解決策

いろいろなメールアドレスで試してみたところ、拡張子として認識されている のでは、と思い至りました。 Rails は URL の末尾に .html とか .json とかを付けて、希望する レスポンス形式を指定する機能がありますよね。 あれが効いているのでは、ということです。

api.rb の get の行を以下のように変更します。

    get '/:email', requirements: { email: /.*/ } do

このように変更したところ、正しく受け取ることができました!

luna:~ % curl http://localhost:3000/api/v1/test/foo@example.co.jp
{"value":"foo@example.co.jp"}
luna:~ %

systemd - .timer 編

こんにちは、masm11 です。

systemd シリーズの第5回、最終回です。

f:id:masm11:20201202230535p:plain

初回:

前回:

今回は .timer ファイルについて説明します。

.timer ファイルとは

以前から Linux/UNIX を管理している方には、cron の代わり、と言うと 話が早いかと思います。前回比較した inetd よりご存知の方は多いのでは ないかと思います。

この unit ファイル中に指定されている日時に、指定されているサービスを 起動するのです。

.timer ファイルの書き方

[Unit] セクション

.timer ファイルも unit ファイルの一種なので、[Unit] セクションが あります。

[Unit]
Description=ZFS replication timer

こんな感じで良いかと思います。.service の場合と同様、Description は ログ出力の際などに使われます。

[Timer] セクション

.timer ファイル特有のセクションです。

OnCalendar=hourly

これで、1時間おき (毎正時) に service を起動してくれます。 hourly の他に daily や weekly なども指定できます。

でも、毎正時っていろんな処理が起動して負荷が高くなりがちですよね。 そういう時にはこれです。

AccuracySec=300

指定するのは秒数です。OnCalendar で指定した時刻から、ランダムで AccuracySec までずらしてくれます。

Persistent=true

これを true にしておくと、前回 service を起動した日時をディスクに 保存しておいて、次に timer が start した際 (OS 起動時とか) に 「1回飛ばしてる!」と思ったら、その瞬間、service を起動してくれます。

Unit=foo.service

これは起動する service 名を指定します。 指定しなかった場合は、foo.timer のデフォルトは foo.service に なりますので、大抵の場合は指定しなくて良いと思います。

[Install] セクション

このセクションを書いておくと、systemctl enable できるようになります。

[Install]
WantedBy=timers.target

こう書いておくことが多いと思います。

.timer ファイルの書き方

そして、起動するサービスも定義しておく必要があります。 サービスですので .service ファイルに書きます。

[Unit]
Description=ZFS replication service
After=network.target
Requires=network.target

[Service]
Type=oneshot
ExecStart=/etc/systemd/zfs/zfsendsnap.rb zroot/home

ExecStart にサービスのプログラムを指定します。

[Install] セクションは不要です。

あとは、

sudo systemctl start foo.timer

とするだけで、指定日時になると service が起動します。

デバッグ

service を開発中で、timer 経由でなく service を直接起動したい場合もあるでしょう。

その場合は、

systemctl start foo.service

で起動することができます。

まとめ

今回は .timer ファイルの内容について説明しました。

ここまでの5回で、

  • systemctl の使い方
  • .service の書き方
  • unit ファイルの編集方法
  • .socket の書き方
  • .timer の書き方

について、具体例として私がよく使う使い方を交えながら説明してきました。 今回でこのシリーズは終わりです。

systemd はなんとも大きなシステムでとっつきにくいですが、 このくらいでも書けると、だいぶん systemd の世界に親しみを感じるようになります。

少しでも参考にしていただけたら幸いです。

では良いお年を!

【解決】nginx で gzip 圧縮がされない問題

f:id:kizashi1122:20201222093127p:plain

id:kizashi1122 です。 2020年ももう終わりですね。今年最後のエントリです。今年も色々ありました。

その色々あった中で nginx がらみで面白いことがあったのでシェアします。

弊社サービス Re:lation では Ruby on Rails を採用しており、AWS 上で稼働させています。 フロントには ALB をおいて、nginx をはさみ、unicorn が動いています。nginx は静的ファイルへのアクセスやHTTPのリダイレクト処理、固定ヘッダの追加など静的な処理を任せています。

nginx に任せている処理の中に gzip 圧縮があります。これは動的生成コンテンツも圧縮することができ、特にレスポンスがテキストの場合などは圧縮率が高くなり転送サイズはぐっと小さくなります。

現在はほとんどのサービスで gzip 圧縮が使われてると思います。

HTTP リクエストヘッダ

ブラウザがサポートしている圧縮アルゴリズムがあればリクエストヘッダに以下のように設定されます。 br というのは、Brotli というアルゴリズムで圧縮率が高いようです。

Accept-Encoding: gzip, deflate, br

HTTP レスポンスヘッダ

そして、圧縮をサポートしているならば、サーバー側は圧縮してもよいので圧縮して送ります。

content-encoding: gzip

圧縮されたレスポンスのヘッダはこんな感じになります。

nginx の設定で gzip 圧縮を有効にしたい場合は設定ファイルにこのような記述をします(最低限)。

  gzip              on;
  gzip_types        application/json text/plain text/css application/x-javascript text/xml application/xml application/rss+xml text/javascript application/javascript image/x-icon;

text/html は指定しなくてもデフォで圧縮がかかるようです。

さて

とある環境からどうも遅く感じるという報告があり、パフォーマンスのボトルネックを探ってたときにある状況を発見しました。 (そのとある環境からの問題はまだ解決してないのですが)

レスポンスによっては gzip 圧縮が効いてないようなのです。

そのリクエストは POST でした。ははーん、 POST だと効かないのか? いやそうではないようだ。ちゃんと POST のレスポンスでも圧縮されている。

なにが違うのか?

調べていった結果、HTTPのステータスコードが 201 (Created) の場合のみ圧縮が効いてないのです。 nginx のソースをみてみます。

https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_gzip_filter_module.c#L228-L240

ここに gzip 圧縮フィルタの関数があります。 if 文をみると HTTP_OK (200) でも NGX_HTTP_FORBIDDEN (403) でも NGX_HTTP_NOT_FOUND (404) でもない場合は処理を抜けて次のフィルタ適用の処理に進んでしまうようです。

微妙だ。

解決策としては、ステータスコードを 201 から 200 に変えるだけ。 これで圧縮が効くようになりました。

めでたしめでたし。

IE11 対応をやめることで得られるメリット

f:id:ishiyu1125:20201222114120p:plain

こんにちは。石田( @ishiyu )です。

今回は、弊社サービスRe:lation のIE11対応終了を受けて、IE11対応を終了することでエンジニア視点で得られるメリットを紹介したいと思います。

1. WebP対応

個人的に一番大きいのは、これかなと感じています。

WebP は、Google が開発した新たな画像形式です。Edge, Firefox, Chrome, Safari の最新版ではすでに対応されており、従来の画像形式と比べてもかなり容量を抑えられます。

ja.wikipedia.org

また、IE11 ではPNG画像などにフォールバックする必要があり(帯域が狭い環境では)画像がなかなか表示されないなどの問題があるため、使用するユーザのメリットも大きいと思います。

2. CSS (::placeholder 擬似クラス)

CSS は「::placeholder 擬似クラス」がなかったので、一番辛く感じてました。よくカスタムプロパティがないという記事を見かけるのですが、SCSS(SASS)を使っていたため、自分は困ったことがありませんでした。
それに対して、::placeholder 擬似クラスはテキストボックスなどのプレースホルダーをデザインするために必要なセレクタで、これを代替しようと思うと JavaScript と組み合わせて実装せざるおえません。 幸い Re:lation で使用する機会はなかったのですが、もし対応しようと思うと非常に大変だったと思います。

CSS のその他の変更点はこちらです。
https://caniuse.com/?compare=ie+11,edge+87&compareCats=CSS

3. JavaScript (Promise)

JavaScript に関してはアロー関数や Array.find、fetch など他にもありますが、JavaScript の中で一番大きい影響は Promise と思っています。Promise は非同期ライブラリを入れれば解決する話ですが、わざわざ標準で対応されているものを今から入れることは、長い目で見ると技術的負債になります。

もちろん、Babel などを導入していれば、コードを記述する際にはあまり気にすることはほぼありません。 しかし、IE11対応を行うことでビルド時間や生成されるコード量がどうしても増えるので、開発中のビルド待ち時間が長くなり、スムーズな開発がしにくくなるデメリットもあります。(core-jsのメンテナの zloirock さんが無事に帰ってこれてホントよかった)

JavaScript のその他の変更点はこちらです。
https://caniuse.com/?compare=ie+11,edge+87&compareCats=JS

さいごに

色々書きましたが、ホントの一番大きいメリットはこれらを常に意識する必要がなくなるということではないでしょうか。 この運用コストはバカにならないと思っています。

IE11対応終了は、いろいろなお客様へサービスを提供するということとは相反することではありますが、新しい機能をよりスムーズに提供することで理解を得られるように、これからも頑張っていきたいと思います。

Rails 6.1 つまみ食い① : 関連付けの非同期削除

おはようございます!

公私ともに2020年を納めきれるか心配になってきた @shutooike です。

今回から数回に亘って Rails 6.1 の新機能を浅く広くつまみ食いしていこうと思います!

セットアップ

techracho.bpsinc.jp

こちらの記事を参考に dip を使って環境構築をしていきます。

$ git clone https://github.com/hachi8833/rails6_docker_quicksetup_sqlite3.git
$ mv rails6_docker_quicksetup_sqlite3/ test_rails_latest/
$ cd test_rails_latest/
$ rm -rf .git
$ git init
$ dip provision
.
.
.
Webpacker successfully installed 🎉 🍰
Creating test_rails_latest_backend_run ... done
yarn install v1.22.4
[1/4] Resolving packages...
success Already up-to-date.
Done in 2.08s.
Creating test_rails_latest_runner_run ... done
Creating test_rails_latest_backend_run ... done
$ dip rails s
Creating test_rails_latest_rails_run ... done
=> Booting Puma
=> Rails 6.1.0 application starting in development
=> Run `bin/rails server --help` for more startup options
.
.

f:id:shutooike:20201221024624p:plain
localhost:3000

Yay!Rails 6.1 の環境構築がものの10分でできました!dip 良さげ!

次に Post has many comments なモデルを scaffold で作成します。

$ dip rails g scaffold post title:string body:string
$ dip rails g scaffold comment post:references body:string
$ dip rails db:migrate
class Post < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
end

セットアップ完了です!

Destroy Associations Async

weblog.rubyonrails.org

今回試すのは「関連レコードをバックグラウンドジョブで destroy してくれる」新機能です。

アソシエーションが大量にあるレコードを destroy する場合はタイムアウトやパフォーマンスを気にして、自前でこのような実装しているところも多いと思います。

こういう機能をフレームワークがサポートしてくれるのは嬉しいですね!

では早速、先ほどの Post モデルにつけて

class Post < ApplicationRecord
  has_many :comments, dependent: :destroy_async
end

Post レコードを削除してみます。

irb(main):001:0> post = Post.first
  Post Load (6.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Post id: 1, title: "test", body: "1", created_at: "2020-12-20 17:07:49.130967000 +0000", updated_at: "2020-12-20 17:07:49.130967000 +0000">
irb(main):002:0> post.comments
  Comment Load (7.7ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? /* loading for inspect */ LIMIT ?  [["post_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, post_id: 1, body: "comment 1", created_at: "2020-12-20 17:08:23.394683000 +0000", updated_at: "2020-12-20 17:08:23.394683000 +0000">, #<Comment id: 2, post_id: 1, body: "comment 2", created_at: "2020-12-20 17:09:03.999279000 +0000", updated_at: "2020-12-20 17:09:03.999279000 +0000">]>
irb(main):003:0> post.destroy
  TRANSACTION (0.8ms)  begin transaction
  Comment Load (15.0ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 84f9208b-0e6d-46e9-a87e-17600e314c9c) to Async(default) with arguments: {:owner_model_name=>"Post", :owner_id=>1, :association_class=>"Comment", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
  Post Destroy (45.6ms)  DELETE FROM "posts" WHERE "posts"."id" = ?  [["id", 1]]
  TRANSACTION (5.0ms)  rollback transaction
Traceback (most recent call last):
        1: from (irb):5
ActiveRecord::InvalidForeignKey (SQLite3::ConstraintException: FOREIGN KEY constraint failed)
irb(main):006:0>    (0.3ms)  SELECT sqlite_version(*)
Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 84f9208b-0e6d-46e9-a87e-17600e314c9c) from Async(default) enqueued at 2020-12-20T17:12:21Z with arguments: {:owner_model_name=>"Post", :owner_id=>1, :association_class=>"Comment", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
   (1.7ms)  SELECT sqlite_version(*)
  Post Load (12.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
Error performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 84f9208b-0e6d-46e9-a87e-17600e314c9c) from Async(default) in 137.57ms: ActiveRecord::DestroyAssociationAsyncError (owner record not destroyed):
/usr/local/bundle/gems/activerecord-6.1.0/lib/active_record/destroy_association_async_job.rb:23:in `perform'
/usr/local/bundle/gems/activejob-6.1.0/lib/active_job/execution.rb:48:in `block in perform_now'
.
.

/usr/local/bundle/gems/concurrent-ruby-1.1.7/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:334:in `block in create_worker'

親の Post レコードが外部キー制約により削除できなかったため、ジョブで例外が発生しました。なんたるおバカムーブ、当たり前ですね 🤦‍♂️ 🤦‍♀️

外部キー制約を外して

class RemoveForeingKeyOnComment < ActiveRecord::Migration[6.1]
  def change
    remove_foreign_key :comments, :posts
  end
end
$ dip rails db:migrate

もう一度

irb(main):004:0> post.destroy
   (0.9ms)  SELECT sqlite_version(*)
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 8ac3ce06-aaaa-40c0-b40c-600028a99436) to Async(default) with arguments: {:owner_model_name=>"Post", :owner_id=>1, :association_class=>"Comment", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
  TRANSACTION (1.6ms)  begin transaction
  Post Destroy (46.7ms)  DELETE FROM "posts" WHERE "posts"."id" = ?  [["id", 1]]
  TRANSACTION (52.5ms)  commit transaction
=> #<Post id: 1, title: "test", body: "1", created_at: "2020-12-20 17:07:49.130967000 +0000", updated_at: "2020-12-20 17:07:49.130967000 +0000">
irb(main):010:0>    (0.2ms)  SELECT sqlite_version(*)
Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 8ac3ce06-aaaa-40c0-b40c-600028a99436) from Async(default) enqueued at 2020-12-20T17:26:22Z with arguments: {:owner_model_name=>"Post", :owner_id=>1, :association_class=>"Comment", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
   (0.3ms)  SELECT sqlite_version(*)
  Post Load (6.6ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Comment Load (36.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."id" IN (?, ?) ORDER BY "comments"."id" ASC LIMIT ?  [[nil, 1], [nil, 2], ["LIMIT", 1000]]
  TRANSACTION (3.8ms)  begin transaction
  Comment Destroy (54.9ms)  DELETE FROM "comments" WHERE "comments"."id" = ?  [["id", 1]]
  TRANSACTION (31.8ms)  commit transaction
  TRANSACTION (0.6ms)  begin transaction
  Comment Destroy (109.4ms)  DELETE FROM "comments" WHERE "comments"."id" = ?  [["id", 2]]
  TRANSACTION (37.8ms)  commit transaction
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 8ac3ce06-aaaa-40c0-b40c-600028a99436) from Async(default) in 502.18ms

今度はうまくいきました!

(ジョブでの削除が失敗したケースは追記予定)

さいごに

destroy_async 名前がわかりやすくていいですね〜

もう少し動作の把握が出来たら個人の Rails プロダクトでも使ってみようと思います。

次回は ActiveStorage の permanent link を試すつもりです!

では良いお年を!