denisov_2023
@denisov_2023 (デニソフ)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

sidekiqでの投稿がDBに保存されない

□解決したいこと

投稿に時間がかかるため、sidekiqで非同期処理をしたいと考えております。

投稿がデータベースに保存されないため、アドバイスをいただきたく、投稿いたしました。

sidekiq導入前のコントローラーの記述は以下でした

def create
    @tip = TipTag.new(tip_params)
    tag_list = params[:tip][:name].split(',')
    if @tip.valid?
      @tip.save(tag_list)
      redirect_to root_path
    else
      render :new
    end
  end

保存に時間がかかるため、@tip.saveをsidekiqで非同期処理したいと思っておりました。

しかし、sidekiqを導入するよう記述を変更し、実行した際に、DBに投稿が保存されておりまでした。

投稿した際のsidekiqのログを確認すると一部にNoMethodErrorが現れましたので、当該部分がDBに保存されない原因かと想定しております。

以下、sidekiq導入のコードの内容

tips_controller.rb

def create
    @tip = TipTag.new(tip_params)
    @tag_list = params[:tip][:name].split(',')
    if @tip.valid?
      TestWorker.perform_async(@tip, @tag_list)
      redirect_to root_path
    else
      render :new
    end
  end
test_worker.rb

class TestWorker < ApplicationController
  include Sidekiq::Worker
  def perform(tip, tag_list)
    tip.save(tag_list)
  end
end
siidekiq.yml

:verbose: false
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 10
:queues:
  - default
  - test
tiptag.rb

class TipTag
  include ActiveModel::Model
  attr_accessor :title, :category_id, :description, :user_id, :image, :name, :id, :_method, :authenticity_token, :commit, :tip

  validates :category_id, numericality: { other_than: 1 }
  with_options presence: true do
    validates :title, :description, presence: true
  end

  delegate :persisted?, to: :tip

  def initialize(attributes = nil, tip: Tip.new)
    @tip = tip
    attributes ||= default_attributes
    super(attributes)
  end

  def save(tag_list)
    ActiveRecord::Base.transaction do
      @tip.update(title: title, category_id: category_id, description: description, image: image, user_id: user_id)
      @tip.tip_tag_relations.each do |tag|
        tag.delete
      end

      tag_list.each do |tag_name|
        tag = Tag.where(name: tag_name).first_or_initialize
        tag.save
        tip_tag = TipTagRelation.where(tip_id: @tip.id, tag_id: tag.id).first_or_initialize
        tip_tag.update(tip_id: @tip.id, tag_id: tag.id)
      end
    end
  end

  def to_model
    tip
  end

  private

  def default_attributes
    {
      title: tip.title,
      category_id: tip.category_id,
      description: tip.description,
      image: tip.image,
      name: tip.tags.pluck(:name).join(','),
    }
  end
end

以下、エラーと想定される部分のログ

% bundle exec sidekiq -C config/sidekiq.yml

<省略>
2021-07-17T11:54:14.782Z pid=31588 tid=ovj3tn428 WARN: {"context":"Job raised exception","job":{"retry":true,"queue":"default","class":"TestWorker","args":["#<TipTag:0x00007fa3d945ce80>",[]],"jid":"a09eb53e3e3f2aa9c383daff","created_at":1626522854.658729,"enqueued_at":1626522854.659025},"jobstr":"{\"retry\":true,\"queue\":\"default\",\"class\":\"TestWorker\",\"args\":[\"#<TipTag:0x00007fa3d945ce80>\",[]],\"jid\":\"a09eb53e3e3f2aa9c383daff\",\"created_at\":1626522854.658729,\"enqueued_at\":1626522854.659025}"}
2021-07-17T11:54:14.782Z pid=31588 tid=ovj3tn428 WARN: NoMethodError: undefined method `save' for "#<TipTag:0x00007fa3d945ce80>":String
<省略>

□仮説及び調べたこと

tipのclassがstring型になっていることが問題ではないかと仮定しました。

そこでworker.rb内でbinding.pryを行い、値を取得しました。

参考

https://qiita.com/kosukeKK/items/1839d470e9472861fe6b

#以下ターミナル
4: def perform(tip, tag_list)
 => 5:   binding.pry
    6:   tip.save(tag_list)
    7: end
pry(#<TestWorker>)> tip.class
=> String

以上より、tipの型がStringになっております。

saveメソッドはString型には使用できないのが原因?ではないかと考えました。

そこで、tips_controllerに戻り、@tipがどのclassかを確認しました。

#以下ターミナル
15: def create
    16:   @tip = TipTag.new(tip_params)
    17:   @tag_list = params[:tip][:name].split(',')
 => 18:   binding.pry
    19:   if @tip.valid?
[1] pry(#<TipsController>)> @tip.class
=> TipTag
[2] pry(#<TipsController>)> TipTag.class
=> Class

TipTagは中間モデルです。
TipTagはActiveModelです。

classはClass 型となりました。

参考

https://docs.ruby-lang.org/ja/latest/method/Object/i/class.html

□わからなかったこと

test_worker.rb内でtipのclassの型がなぜString型になったかがわかりませんでした。

初心者で、全く見当違いの検討をしているかもしれませんが、ご教示いただきたくお願い申し上げます。

□環境

ruby 2.6.5

rails (6.0.3.7)

sidekiq (6.2.1)

redis (4.3.1)

0

1Answer

test_worker.rb内でtipのclassの型がなぜString型になったかがわかりませんでした。

Sidekiq のドキュメントに書いてあるとおりperform_async メソッドに渡せる引数は String や Number などの一部の型に限られています。それ以外のオブジェクトを渡すと #to_s で String に変換されます。

そうなっているのは、 Sidekiq による非同期実行の仕組みの都合で、引数を一度 Redis データベースに保存する必要があるからです。 Redis に保存できるのは基本的に文字列だけのため、 perform_async メソッドは引数を JSON 形式の文字列に変換します。ここで、 JSON に変換できるのは Ruby の String や Number など基本的な型だけです。 Sidekiq は Redis に保存した JSON 文字列を後で(つまり非同期に)取り出し、 Ruby の値に戻して、ワーカーの perform メソッドに渡しています。

ワーカーで TipTag を使いたいなら perform_async に TipTag の ID を渡してワーカーで find してください。

# tips_controller.rb
TestWorker.perform_async(@tip.id, tag_list)
# test_worker.rb
class TestWorker
  include Sidekiq::Worker
  def perform(tip_id, tag_list)
    tip = TipTag.find(tip_id)
    ...
  end
end

ちなみにご質問ではワーカーのクラス定義が class TestWorker < ApplicationController となっていますが、コントローラクラスを継承するのは間違いと言っていいです。たまにこう書いてある記事もありますが。

1Like

Comments

  1. 蛇足ですが、タグを保存するくらいのことが非同期実行が必要なほど遅くなるとはちょっと考えづらいです。他に遅さの原因がある気がします。
  2. @denisov_2023

    Questioner

    @uasiさん早速のご回答ありがとうございます。
    ドキュメントにその旨書いてあった件、ご指摘ありがとうございます。
    勉強不足でした。
    また、ワーカークラスの定義がApplicationControllerとなっていることに対するご指摘も、
    勉強になりました。

    大変申し訳ないのですが今、確認作業をすることができませんので、可能な限り早く検証させていただきます。
    ご丁寧な対応ありがとうございます。
  3. @denisov_2023

    Questioner

    検証遅くなり、大変申し訳ございません。

    上記、「TipTagは中間モデルです。」は誤りで、TIpTagはActionModelです。

    大変、失礼いたしました。

    その上で、アドバイスいただいた内容を試してみました。

    ```ruby
    def create
    @tip = TipTag.new(tip_params)
    tag_list = params[:tip][:name].split(',')
    if @tip.valid?
    TestWorker.perform_async(@tip.id, tag_list)
    redirect_to root_path
    else
    render :new
    end
    end
    ```

    ```ruby
    class TestWorker < ApplicationController
    include Sidekiq::Worker
    def perform(tip_id, tag_list)
    tip = TipTag.save(tag_list)
    tip.save(tag_list)
    end
    end
    ```

    以下のように、
    NoMethodError: undefined method `find' for TipTag:Class となりまして、
    TipTagにfindメソッドが定義されていないとのことです。
    TIpTagがActionModelであることが原因と思われます。

    ```ruby
    2021-07-20T11:49:36.138Z pid=46494 tid=oxx5nunda WARN: {"context":"Job raised exception","job":{"retry":true,"queue":"default","class":"TestWorker","args":[null,[]],"jid":"fe3f93c46f73fff010058de9","created_at":1626781776.045098,"enqueued_at":1626781776.0451431},"jobstr":"{\"retry\":true,\"queue\":\"default\",\"class\":\"TestWorker\",\"args\":[null,[]],\"jid\":\"fe3f93c46f73fff010058de9\",\"created_at\":1626781776.045098,\"enqueued_at\":1626781776.0451431}"}
    2021-07-20T11:49:36.138Z pid=46494 tid=oxx5nunda WARN: NoMethodError: undefined method `find' for TipTag:Class
    ```

    そこで、アドバイスいただいた内容をもとに私自身も検証してみました。

    sidekiqドキュメントより、ハッシュ形式であれば、引数の値として渡せるとのことですので、

    tip_paramsをハッシュに変換して引数にして渡してみようと考えました。

    ```ruby
    def create
    @tip = TipTag.new(tip_params)
    tag_list = params[:tip][:name].split(',')
    if @tip.valid?
    binding.pry
    TestWorker.perform_async(tip_params.to_h, tag_list)
    redirect_to root_path
    else
    render :new
    end
    end

    #以下ターミナル
    [1] pry(#<TipsController>)> tip_params
    => <ActionController::Parameters {"title"=>"テスト", "category_id"=>"2", "description"=>"aaa", "name"=>"", "user_id"=>1} permitted: true>
    [2] pry(#<TipsController>)> tip_params.to_h
    => {"title"=>"テスト",
    "category_id"=>"2",
    "description"=>"aaa",
    "name"=>"",
    "user_id"=>1}
    ```

    ```ruby
    class TestWorker
    include Sidekiq::Worker
    def perform(tip_params, tag_list)
    tip = TipTag.new(tip_params)
    tip.save(tag_list)
    end
    end
    ```

    すると、以下のgifような挙動となりました。

    [https://gyazo.com/a2129a6c6406cbba3757f6d18988a5d4](https://www.notion.so/a2129a6c6406cbba3757f6d18988a5d4)

    課題としては、以下があります。

    ・再読み込みをしないとが投稿が反映されない

    ・画像がアップされない

    しかし、表題の課題は解決できたと思われます。
    ご協力いただき、誠にありがとうございます。

Your answer might help someone💌