しゅがー

技術ネタとか書いていけたらな…

【Rails】MongoDBとMySQLそれぞれへのindexの貼り方とか

Overview

いつも仕事でメインはMongoDB、サブはMySQLみたいな使い方をしていて、MySQLってどうやるんだっけ?と忘れることがあるのでその備忘。

MySQL

みなさんご存知のRDBです。
migrateを実行してDBに各種定義を反映していきます。

table & column作成

bundle exec rails g migration AddHogehogeTable 上記で作成されたmigrateファイルを編集します。

class AddHogehogeTable < ActiveRecord::Migration[5.X]
  def change
      create_table :hogehoges do |t|
         t.string :fugafuga
      end
  end
end

index作成

class AddHogehogeTable < ActiveRecord::Migration[5.X]
  def change
      create_table :hogehoges do |t|
         t.string :fugafuga
      end

      add_index :hogehoges, :fugafuga ← NEW
  end
end

migrate実行

ステータスの確認をしてupになっていれば問題なし。
あとはrails consoleからデータを作成できるかも確認しましょう。
bundle exec rake db:migrate
bundle exec rake db:migrate:status

MongoDB

NoSQL Databaseです。
基本モデルと連動しているので、再読み込みさせることによって各種定義が反映されます。

table(collection)作成

rails g model Hogehoge

column(field)作成

hogehogeファイルを開いて、下記を記載するだけ。
includeしているモジュールはmongoid(ORM)経由でMongoDBに定義するため必要なものになります。

class Hogehoge
  include Mongoid::Document

  field :fugafuga
end

index作成

# ターミナルにてrails consoleに入ります
$ bundle exec rails console -e development
略
pry(main)> Hogehoge.create_indexs # index作成
pry(main)> Hogehoge.collection.indexes.to_a # 作成されたindexが確認できる

まとめ

MongoDBはschema定義がそのままモデルに記載されているので、MySQLより簡単にDB定義を変更できます。
ですが、トランザクションがないため整合性を重要視するプロダクトには向かないかもです。(一応MongoDB4.0からトランザクション機能が増えたので、かなりよくなっているかもですが)
MySQLはしっかりトランザクションがあるので、整合性に関しても安心できそうです。
schema定義はそのままschema.rbにあるので見やすいですが、ある程度MongoDBに慣れてしまうとmigrate実行漏れがあったりするので気をつけましょう。

ResqueのFailed JobsをCLIで一気にrequeueする方法

TL; DR

障害などでResqueジョブがコケてしまい一気に再実行したい人向けのスクリプト
resque_webからだと1個ずつちまちま実行していかなければならない。
つまりここ

Failureジョブの取得方法について

ApplicationJobが継承されていない古いジョブもあったりするので、ActiveJob::QueueAdapters::ResqueAdapter::JobWrapperで分岐させています。
基本的にpayload出力結果が変わっていたり、エラー表示のさせかたが違います。

最終的には失敗ジョブ名 => 件数として出力される

offset = 0
limit = -1 # 全件取得の場合
Resque::Failure.all(offset, limit).
  map {|f| f.dig("payload", "class") == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper" ? f.dig("payload", "args", 0, "job_class") : f.dig("payload", "class")}.
  each_with_object(Hash.new(0)) {|v, o| o[v] += 1}

{
    "HogehogeJob" => 100,
    "FugaFugaJob" => 100,
}

特定期間にスコープを絞る場合

failed_at という失敗日時が入るフィールドが存在するので、from toを用いてスコープを絞ることもできます。

offset = 0
limit = -1 # 全件取得の場合
from_failed_at = "20XX/XX/XX 00:00:00 JST"
to_failed_at = "2020/XX/XX 00:00:00 JST"
Resque::Failure.all(offset, limit).
  select {|f| from_failed_at < f.dig("failed_at") && f.dig("failed_at") < to_failed_at}.
  map {|f| f.dig("payload", "class") == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper" ? f.dig("payload", "args", 0, "job_class") : f.dig("payload", "class")}.
  each_with_object(Hash.new(0)) {|v, o| o[v] += 1}

{
    "HogehogeJob" => 50,
    "FugaFugaJob" => 50,
}

対象ジョブの再実行

上記までで何が何件失敗したか把握できたと思います。
最後はどのジョブを再実行させるかです。

not_run_job_class = %w[FugaFugaJob] # 実行したくないクラスを指定

offset = 0
limit = -1 # 全件取得の場合
from_failed_at = "20XX/XX/XX 00:00:00 JST"
to_failed_at = "2020/XX/XX 00:00:00 JST"

# 対象のjobを取得
failed_jobs = Resque::Failure.all(offset, limit).
  each_with_object({}).with_index do |(f, h), i|
    job_class = f.dig("payload", "class") == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper" ? f.dig("payload", "args", 0, "job_class") : f.dig("payload", "class")
    next if not_run_job_class.include?(job_class)
    next unless from_failed_at < f.dig("failed_at") && f.dig("failed_at") < to_failed_at

    h[i] = job_class
  end

{
    1 => "HogehogeJob",
    2 => "HogehogeJob",
    3 => "HogehogeJob"
}

対象ジョブをfailed_jobsに放り込んだので、ここからは実行に問題ないかチェック

# 再実行件数確認
failed_jobs.count
=> 50

# 実行されるジョブ確認
failed_jobs.values.uniq
[
    [0] "HogehogeJob"
]

# 内訳
failed_jobs.values.each_with_object(Hash.new(0)) {|v, o| o[v] += 1}

問題なければ再実行
実行結果の確認用に一番最後 + 1のindexを取得しておく。

from_index = Resque::Failure.count
failed_jobs.keys.each do |i|
  Resque::Failure.requeue(i)
end

実行結果
空のhashが返ってくれば問題なく再実行 or キューに放り込まれているはず。

offset = from_index
limit = -1 # 全件取得の場合

Resque::Failure.all(offset, limit).
  map {|f| f.dig("payload", "class") == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper" ? f.dig("payload", "args", 0, "job_class") : f.dig("payload", "class")}.
  each_with_object(Hash.new(0)) {|v, o| o[v] += 1}

=> {}

最後に

こんな感じで実行すれば障害発生時に慌てずジョブの再実行ができます。
Sidekiqの方が色々便利なので、できれば移行した方がよさそうですね。

【MongoDB】特定のcollectionをdumpしてrestoreする方法

特定のcollectionのみをdump

下記コマンドを実行するとカレントディレクトリにdumpフォルダが生成される。

mongodump -d hogehoge_db -c hogehoge_collection1 --out dump
mongodump -d hogehoge_db -c hogehoge_collection2 --out dump
mongodump -d hogehoge_db -c hogehoge_collection3 --out dump

こんな感じ

current directory
├── dump
   ├── hogehoge_db
      ├── hogehoge_collection1
      ├── hogehoge_collection2
      ├── hogehoge_collection3

既存collectionを完全に入れ替えたい場合は、--dropオプションを追記する。
詳細は公式リファレンスを参照

mongorestore --drop dump

すると既存のcollectionデータは消えて、新しいデータでrestoreされる。

補足

dockerコンテナ上で動作しているMongoDBにデータを取り込む方法 ホストからコンテナへdumpしたデータをコピー

docker cp dump コンテナID:/dump

参考 https://qiita.com/gologo13/items/7e4e404af80377b48fd5

Elasticsearch 6.8.X に上げるときの注意点

最近書いてなかったので、久しぶりに投稿

TL; DR

  • ヒープサイズのデフォルト値が2GBから1GBに変更になりました。
    • itamaeでjvm.optionsを管理している方は要注意。
    • デフォルト値の指定で書き換えを行っていると、書き換えされずデフォルト値のままElasticsearchが起動されます。
  • like_textからlikeへの変更
  • elasticsearch-railsのmongoidのimport処理で障害になった
    • importo処理を実行する
    • countクエリが走る
    • メタ情報で帰ってこないから素直にカウントしにいく

はじめに

EOLを向かえた5.X系から6.X系の最新までバージョンを上げました。 その時の注意点やクラスタの入れ替え方法をまとめました。

変更概要

ヒープサイズがデフォルト1GBに変更

Elasticsearch 5系のインストール時のデフォルトサイズは2GBとなっています。 しかし6系からデフォルト値は1GBに変更されています。

6系のリファレンスだとこんな感じです。

By default, Elasticsearch tells the JVM to use a heap with a minimum and maximum size of 1 GB. When moving to production, it is important to configure heap size to ensure that Elasticsearch has enough heap available.

基本的にクラスタを組んで一括で管理している方が多数だと思います。(itamaeやchefなどで) そういった方でデフォルト値から書き換えている方は注意してください。 sedで置き換えておりましたが、変更されていることに気づかず1台1GBのクラスタを組んでしまいました。

more_like_thislike_textがサポートされなくなった

シンプルにlike_textがなくなり、likeになりました。

[400] {"error":{"root_cause":[{"type":"parsing_exception","reason":"[mlt] query does not support [like_text]","line":1,"col":96}],"type":"parsing_exception","reason":"[mlt] query does not support [like_text]","line":1,"col":96},"status":400}

公式リファレンスでは見つけられず、そのままクラスタの入れ替えをしてしまいました。 後々GitHub上のIssueで発見しました。(ググっても見つけられなかった、というかページが見つからなかった)

resqueで非同期処理を実行していて、そちらで処理がコケていました。 また、残念なことにエラー検知システムの方でアラートが上がらず、発見したのが翌日でした。 幸いなことに致命的な処理ではなかったので、再実行すれば問題ありませんでした。

elasticsearch-railsのmongoidのimport処理で障害になった

シンプルにこんな処理を流してElasticsearchクラスタにDBにデータを流し込む処理を実行しました。

Hogehoge.__elasticsearch__.import

すると、数分の間にslow queryカウントが急激に増加し、webサーバが処理しきれなくなりサービスが完全ダウンしかけました。

scope.no_timeout.each_slice(batch_size) do |items|
  yield (preprocess ? self.__send__(preprocess, items) : items)
end

このeach_sliceってカウントクエリが走ります。 そのため、メタ情報でカウントが帰ってこない数え方だと純粋に数えに行きレスポンスが帰ってこなくなります。 データ量もそこそこあるcollectionだったので、余計遅くなりサービスダウンの障害となりました。

items = []
scope.no_timeout.each do |item|
  items << item
  if items.count >= batch_size
  yield (preprocess ? self.__send__(preprocess, items) : items)
    items.clear
  end
end

yield (preprocess ? self.__send__(preprocess, items) : items) if items.present?

最終的にはこんな感じのモンキーパッチを当てて対応しました。 バッチのデフォルトサイズ1000件まで配列に追加して、そのままyieldに投げ込むやり方に変更しました。

最後に

Beaking Changeは読みましょう!(読んでいたけど・・) どこで障害に起こっているかわからないため、エラー検知だけを信じず見れるところは見ましょう。

Rails3 + Passenger について

今更ながらRails3 + Passenger + Apacheの環境について調べました。
Railsは3.0.1のためassets pipelineがありません。
そのため、publicディレクトリに置いてあるJavaScriptCSSApacheモジュールのPassengerが圧縮しているみたいです。

Passenger

環境

  • production
  • staging
  • development

基本的に環境はhttpd.confに依存しています。
デフォルトではproductionで動作するようになっているため、production以外の環境はそれぞれ下記のような環境変数を指定してあげるとよさそうです。

httpd.conf

<Directory "/var/www/html">
    Order allow,deny
    Allow from all
    RailsEnv development #ここ
</Directory>

圧縮をかけるコンテンツフィルターはこんな感じで指定できます。

httpd.conf

# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 gzip-only-text/html

# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4\.0[678] no-gzip

# MSIE masquerades as Netscape, but it is fine
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

FilterDeclare Compression CONTENT_SET
FilterProvider Compression DEFLATE Content-Type $text/plain
FilterProvider Compression DEFLATE Content-Type $text/css
FilterProvider Compression DEFLATE Content-Type $application/xhtml
FilterProvider Compression DEFLATE Content-Type $application/xml
FilterProvider Compression DEFLATE Content-Type $application/xhtml+xml
FilterProvider Compression DEFLATE Content-Type $application/rss+xml
FilterProvider Compression DEFLATE Content-Type $application/atom+xml
FilterProvider Compression DEFLATE Content-Type $application/x-javascript
FilterProvider Compression DEFLATE Content-Type $image/svg+xml
FilterProvider Compression DEFLATE Content-Type $text/html
FilterProvider Compression DEFLATE Content-Type $application/javascript
FilterChain Compression

あまりにもローカル環境が遅いためPassengerを利用して開発してみようと思い調べてみました。
ローカル環境でシンプルに開発するならやっぱりでWEBrickがいいのかな。
もしくはDockerで各種コンテナを用意して、開発した方が楽そうだな・・

参考

Rails3 事始め: Passenger で development モードで動かす
【Apache】サイトのコンテンツ全てをgzip圧縮する | blog.remora.cx
Apache 2.2 mod_filterを使いこなす – AddOutputFilterByType DEFLATE text/htmlを書き換えてみる – cyano

sessionの有効期限について

使用ツール

Rails 5.1.5
devise 4.4.1

概要

sessionが切れる時の対処法

  • config/initializers/devise.rb
    config.timeout_inの時間を任意の時間に変更

before

  # ==> Configuration for :timeoutable
  # The time you want to timeout the user session without activity. After this
  # time the user will be asked for credentials again. Default is 30 minutes.
  # config.timeout_in = 30.minutes

after

  # ==> Configuration for :timeoutable
  # The time you want to timeout the user session without activity. After this
  # time the user will be asked for credentials again. Default is 30 minutes.
  config.timeout_in = 1.month
  • config/initializers/session_store.rb
    expire_afterを任意の時間で設定する

before

Rails.application.config.session_store :active_record_store, :key => '_app_session'

after

Rails.application.config.session_store :active_record_store, :key => '_app_session', :expire_after => 1.month

参考

Deviseでセッションのタイムアウトまでの時間を延ばす方法 - Qiita

Rails carrierwave fog rmagickで画像アップロード

プロダクト開発でアイコンやバックグラウドイメージをアップロードできるようにしたいので試しに使用してみました。
すでに色々な方々が試しているみたいですが、自分なりの備忘として残しておきます。
環境変数はハードコーディングせずにgem:dotenvを使用して実装しています。

carrierwave

CarrierWave.configure do |config|
  config.fog_credentials = {
    provider: 'AWS',
    aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
    region: ENV['AWS_REGION'],
    path_style: true
  }
  config.fog_public = true
  config.fog_attributes = {'Cache-Control' => 'public, max-age=86400'}
  config.remove_previously_stored_files_after_update = false
  config.cache_storage = :fog

  case Rails.env
  when 'production'
    config.fog_directory  = ENV['AWS_S3_BUCKET']
  end
end
# 日本語の文字化けを防ぐ
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

Uploader

class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::RMagick

  storage :fog
  def store_dir
    "images/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  version :thumb do
    process resize_to_fit: [100, 100]
  end

  def extension_whitelist
    %w(jpg jpeg png)
  end

  def filename
    "image_#{model.id}.#{file.extension}" if original_filename
  end
end

Model

class User < ApplicationRecord
  mount_uploader :icon, ImageUploader
end

参考

Rails4.2+carrierwave+fog+rmagickでS3に画像アップロードした話 - Qiita
CarrierWave + Rails 5.1で画像アップローダー | 酒と涙とRubyとRailsと
GitHub - carrierwaveuploader/carrierwave: Classier solution for file uploads for Rails, Sinatra and other Ruby web frameworks