直径1.5メートル

ひよっこエンジニアのちょっとしたメモ。主に備忘録。

【Rails】omniauth-yahoojpのエラー対応

omniauth-yahoojpを使ってYahoo!IDでの認証を実装しているサービスで、いつのまにかエラーを吐き出すようになっていました。
さっとググって出てくるものと、ささっとググるだけでは出て来ないものがあったので、対応メモ。

invalid_redirect_url

まずはこちらのエラー。

ERROR -- omniauth: (yahoojp) Authentication failure!
  invalid_credentials: OAuth2::Error, invalid_redirect_uri: redirect_uri is invalid.

調べてみるとyahoojpではなく、omniauth-oauth21.4.0以降のエラーらしく、「omniauth-oauth2のバージョンを1.3.1にすれば解決できるよ」な記事をよく見かけます。
もしくはomniauth-oauth2のcallback_urlメソッドを上書きでも良いようです。
今回はomniauth-oauth2のバージョンを1.3.1に下げることで対応。

参考:
Gem omniauth-oauth2 バージョン1.4.0のエラー - Kntmrkm.new OmniAuth OAuth2 1.4.0 以降で `Invalid Credentials` エラー - Qiita

invalid_request: the parameter is invalid. [“client_id”]

この時点で、最初に出ていた「invalid_redirect_url」は解消されたように見えます。
が、今度はまた別のエラーが発生していました。

ERROR -- omniauth: (yahoojp) Authentication failure!
  invalid_credentials: OAuth2::Error, invalid_request: the parameter is invalid. ["client_id"]
{"error":"invalid_request","error_description":"the parameter is invalid. [\"client_id\"]","error_code":"102"}

こちらも調べてみると、今度はoauth2のv1.3.0で変更のあったパラメータを、yahoojpのGemにも追加しないといけないようです。

すでにmikanmarusan/omniauth-yahoojp#4の PRで取り込まれているようなので、この修正を反映すべく、omniauth-yahoojpのGemをアプデします。

$ bundle update omniauth-yahoojp

v0.1.4へのアプデが完了したところで再度確認すると、無事、Yahoo!IDでの認証ができるようになりました。

【Redis】メモリ周りの確認

Redisのメモリを使い切った時に「OOM command not allowed when used memory > ‘maxmemory’.」というエラーが発生することがあります。
「maxmemory言われてるし…」とメモリ周りを確認することになるので、ちらっと調べたことのメモ書き。

現時点でのメモリ使用量を調べる

RedisのinfoコマンドでRedisの情報を確認。
「メモリ情報だけ確認したい!」という時はinfo memoryで確認できます。

> redis-cli -h xxx
xxx:6379> info memory

# Memory
used_memory:1346349472
used_memory_human:1.25G
used_memory_rss:1322913792
used_memory_peak:1347333880
used_memory_peak_human:1.25G
used_memory_lua:36864
mem_fragmentation_ratio:0.98
mem_allocator:jemalloc-3.6.0

used_memoryが使用量(_humanで単位付き)なので、上記の例だと1.25Gのメモリを使用していることになります。

参考:
INFO – Redis

maxmemory-policyの確認

ざっくり言うと、「メモリいっぱいになったらどうする?」の設定。

  • noeviction
    メモリ制限に達しているのに、さらにメモリを使用するようなコマンド(ほぼ書き込み。DELやいくつか例外あり)を実行しようとしているときにエラーを返す。
  • allkeys-lru
    新しいデータの領域を確保するため、あまり使われていないキー(LRU)を先に削除しようとする。
  • volatile-lru
    新しいデータの領域を確保するため、あまり使われていないキー(LRU)を先に削除しようとする。有効期限が設定されているキーのみ対象。
  • allkeys-random
    新しいデータの領域を確保するため、キーをランダムに削除する。
  • volatile-random
    新しいデータの領域を確保するため、キーをランダムに削除する。有効期限が設定されているキーのみ対象。
  • volatile-ttl
    新しいデータの領域を確保するため、有効期限が設定されたキーの中で、より短い有効期間(TTL)のキーを削除する。

volatile-lruvolatile-randomvolatile-ttlを設定していても、有効期限を設定しているものが無い場合はnoevictionと同じ挙動になるので、メモリを使い切った時には例の「maxmemory」エラーが出てしまうようです。

参考:
Using Redis as an LRU cache – Redis

対策

有効期限を設定できるのであればそれが無難なような感じもしますが、時と場合によっては期限を設定したく無い時も。
そうなるとメモリの増量が有力候補に踊り出るかと思います。
AWSのElastiCacheの場合はノードタイプを変更することでメモリを増やすこともできますが、一時的に読み・書きができなくなるので、「アクセスできなくなったらまずいんだよなぁ…」な場合は、気持ち慎重に対応する必要があります。

参考;
単一ノード Redis のクラスターのスケールアップ - Amazon ElastiCache Redis 本番障害から学んだコードレビューの勘所 - Qiita Redisのメモリ使用量がmaxを超えた場合の挙動 - Gaishimo

【Serverspec】sidekiqのプロセスを確認する

Serverspecをちょこちょこ書いていて、sidekiqのプロセス確認をするコードを書いた時に
「sidekiqのプロセスも、リソースタイプのprocessで一発!」
と思ったら一発で仕留められなかったので、メモ。

processでsidekiqのプロセスを確認

processを使ってsidekiqの確認をした時のコード。

# sidekiqのプロセス確認
describe process('sidekiq') do
  it { should be_running }
end

これを実行すると、下記のようなエラーになりました。

Failures:

  1) Process "sidekiq" should be running
     On host `0.0.0.0'
     Failure/Error: it{ should be_running }
       expected Process "sidekiq" to be running
       sudo -p 'Password: ' /bin/sh -c ps\ -C\ sidekiq\ -o\ pid\=\ \|\ head\ -1

$ps -ef | grep sidekiqで確認する限りでは取得できていたので、「なんでかなー」と思っていたところ、

コマンド名とプロセス名が一致していない場合はリソースタイプ process を利用することが出来ない。
Serverspecでリソースタイプ process や package が使えない場合とその対処 - Qiita

とのこと。
改めて確認してみると、 $bundle exec sidekiqで起動していたので$ ps -C bundleでプロセスが確認できました。
ただ、これで確認するのもなぁ…という感じもするので、commandを使うことに。

commandでsidekiqのプロセスを確認

いつもプロセス確認する時のコマンドで確認しています。

describe command('ps -ef | grep [s]idekiq') do
  let(:disable_sudo) { true }
  its(:exit_status){ should eq 0 }
end

これを実行すると…

Command "ps -ef | grep [s]idekiq"
  exit_status
    should eq 0

無事成功。

何かと「うまくいかなかったらcommandで…」みたいに思ってしまうけれども、果たしてこれで良いのかどうか…。
悩みどころです。

『俳句の図書館』を読んで17音の意味を探る

少し前に書店で気になっていた本を買い、読み終わったところでいてもたってもいられず、とりあえずここに。

俳句の図書室 (角川文庫)

俳句の図書室 (角川文庫)

続きを読む

【nginx】ホスト名指定でのリバプロはresolverをセットで

nginxのリバースプロキシ設定で、転送先をIPではなくホスト名で指定していると、ある日突然エラーが発生する現象に遭遇しました。
ググると複数の記事が見つかったので、よくある現象?のようです。

リバースプロキシ設定

元々は↓のように設定していました。
proxy_passのところで、リクエストの転送先をホスト名で指定しています。

location ~ ^/hoge {
  proxy_pass https://example.com;
}

この状態だとnginx起動時に転送先のIPがキャッシュされ、以降はキャッシュしたIPへ転送するようになるようです。
IPが変わってもキャッシュしているIPへ転送し続けるので、AWSのELBを使っていたりすると、IPが変わった際にエラーが起きてしまうことも。
それを回避するため、proxy_passと併せてresolverを設定しておきます。

resolverを設定する

resolverを設定すると↓のような感じに。

location ~ ^/hoge {
  resolver 10.0.0.2;
  set $host_name example.com;
  proxy_pass https://$host_name;
}

resolverで指定するDNSのIPは、AWSの場合はVPCのネットワーク範囲+2とのこと。

AmazonProvidedDNS は Amazon DNS サーバーです。このオプションは、VPC のインターネットゲートウェイを介して通信する必要があるインスタンスに対して DNS を有効にします。文字列 AmazonProvidedDNS は、リザーブド IP アドレスで実行中の DNS サーバーにマップされ、VPC IPv4 ネットワークの範囲に 2 をプラスした値です。例えば、10.0.0.0/16 ネットワークの DNS サーバーの位置は 10.0.0.2 となります。
DHCP オプションセット - Amazon Virtual Private Cloud

デフォルトではTTLが切れたタイミングでDNSに問い合わせ。
resolverにvalidを設定すると、任意のタイミングで問い合わせることができるようです。
ex. valid指定

location ~ ^/hoge {
  resolver 10.0.0.2 valid=5s; # 5秒ごとに問い合わせ
  set $host_name example.com;
  proxy_pass https://$host_name;
}

nginxのバージョンによる?のか、nginx 1.6系、1.8系ではsetでホスト名を変数に入れないとエラーになるようです。
nginx 1.10.1では、setを使わずに指定しても設定できました。

参考

nginx proxy の名前解決問題、ファイナルアンサー? – 1Q77
Nginx のDNS 名前解決とS3 やELB へのリバースプロキシ :: by and for engineers
Nginxでproxy_passにホスト名を書いた時の名前解決のタイミング - (ひ)メモ

【Git】ブランチを切り替えた時にSlack通知されるようにした

開発に入ると、あとから「あれ、時間どれくらいかかったっけ」みたいになることありませんか。
自分はしょっちゅうありまして、Togglを使うようにしたものの、開始/終了忘れが続出。
作業を変更するタイミングで勝手にどこかに記録されていった方が良いなーと思って、開発中は何度も行うブランチの切り替えをタイミングにしてみることにしました。

checkoutのタイミング

「はて、checkoutのタイミングってどうやって知るんだろう」
ちょろっと調べるとすぐに出てきました。

git checkoutが正常に終了すると、post-checkoutフックが実行されます。
8.3 Git のカスタマイズ - Git フック#その他のクライアントフック

.git/hooksの中にはいくつかサンプルがあります。
その中にpost-checkoutファイルを作って処理を書いておくとcheckout後に実行されるらしいので、これを利用することにします。

ブランチ名の取得

「はて、ブランチ名ってどうやって取得するんだろう」
これもちょっと調べると出てきました。

git rev-parse --abbrev-ref HEADでできるらしい。rev-parse初めて知りました。
gitで現在のブランチ名を取得する - 技術は熱いうちに打て!

後から調べたのですが、Git1.8以降であればgit symbolic-ref --short HEADでも取得できるようです。
Gitリポジトリのカレントブランチ名を取得する | blog: takahiro okumura

現在のブランチ名がgit rev-parse --abbrev-ref HEADなら、一つ前はgit rev-parse --abbrev-ref @{-1}かな?と思って試したら、ちゃんと前のブランチ名を取得できました。
ということで、

# 現在のブランチ名を取得
$ git rev-parse --abbrev-ref HEAD
master

# 前のブランチ名を取得
$ git rev-parse --abbrev-ref @{-1}
develop

git rev-parseなかなか面白い。
git rev-parseを使いこなす - Qiita

最終的に

↓のように着地。

Slackへの通知は[10分で出来る]シェルスクリプトの結果をslackに投稿 - Qiitaを参考にしています。

実際にmaster -> developへブランチを変更すると… f:id:mr_96:20170603233338p:plain キタ(゚∀゚)!
名前(gittn)やメッセージは整えるとして、とりあえず通知するところまでは完了。
これでSlack上で切り替えた時間がわかるので、大まかにでも作業時間がわかる(はず)。

curl叩いている分、checkoutが若干遅くなってしまったのは…どうしよう…。

【Rails】prev_weekをprev_monthと同じノリで使うと「あれっ」となる

何かと日付計算は使うと思いますが、「一週間前」をどう出すかについて。
「一週間前」の計算をprev_monthと同じノリでprev_weekを利用して計算した際、意図したものにならず「あれっ?」となることがありました。ので、メモ。

例えば↓のような計算。

> Date.yesterday
#=> Sun, 30 Apr 2017

> Date.yesterday.prev_month
#=> Thu, 30 Mar 2017

> Date.yesterday.prev_week
#=> Mon, 17 Apr 2017

何も気にせずにやっていると、あれ?Date.yesterday.prev_weekは2017年4月23日じゃないの?となります。
が、よくよくソースを見ると処理が全く違うオチ。

prev_week

def prev_week(start_day = Date.beginning_of_week, same_time: false)
  result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
  same_time ? copy_time_to(result) : result
end

rails/calculations.rb at f2c6db41ba56afb1529e4732c59622fc0cf9f3ba · rails/rails · GitHub

prev_month

def prev_month
  months_ago(1)
end

rails/calculations.rb at f2c6db41ba56afb1529e4732c59622fc0cf9f3ba · rails/rails · GitHub

prev_weekの中身はまだ詳しく知りたいのでおいおい。

おとなしく「一週間前」を出そうとすると、こういうことになるのでしょうか。

> Date.yesterday.weeks_ago(1)
#=> Sun, 23 Apr 2017

> Date.yesterday - 1.week
#=> Sun, 23 Apr 2017

【Rails】アップロードしたPDFから数枚画像に切り出したい【Grim】

何気に「このPDF、数ページだけ画像にして保存したい…」という時があると思うのです。 …なかったとしても、万が一発生した時にGrimというgemが使えそうだったのでメモ。

github.com

処理の流れ

想定している処理は、
1. PDFを保存&S3にアップロード
2. 保存したPDFから3枚画像に切り出す
3. 切り出した画像を保存&S3にアップロード
です。では早速。

モデル準備

f:id:mr_96:20170214231326j:plain ↑のイメージでモデルを作ります。
DocumentはPDFを持つように、PdfImageはcapture(=PDFから切り出した画像)を持つようにします。
Documentは複数枚の画像を持つので、has_manyで。
PDFと切り出した画像のアップロードはpaperclipを使います。 今回はpaperclipは入っている前提で進めます。

必要なファイルを生成

$ rails g model PdfImage
$ rails g scaffold documents

migrationファイルに必要なフィールドを追加。

# db/migrate/201702140001_create_documents.rb
class CreateDocument < ActiveRecord::Migration[5.0]
  def change
    create_table :douments do |t|
      t.string     :name
      t.attachment :pdf

      t.timestamps
    end
  end
end

# db/migrate/201702140000_create_pdf_images.rb
class CreatePdfImage < ActiveRecord::Migration[5.0]
  def change
    create_table :pdf_images do |t|
      t.attachment :capture
      t.references :document, foreign_key: true

      t.timestamps
    end
  end
end

migrationファイルができたら、コマンド実行。

$ RAILS_ENV=development rails db:migrate

モデルにattachmentのvalidationなど、必要項目を足していきます。

# app/models/pdf_image.rb
class PdfImage < ApplicationRecord
  has_attached_file :capture
  belongs_to :document

  validates_attachment_content_type :capture, :content_type => 'image/png'
end

# app/models/document.rb
class Document < ApplicationRecord
  has_attached_file :pdf
  has_many :pdf_images, dependent: :destroy

  validates_attachment_content_type :pdf, :content_type => 'application/pdf'
end

PDFの場合、白背景が透過されてしまい、白い背景が真っ黒になる場合もあるので、その時はhas_attached_fileのオプションにImageMagickでの変換指定することで回避できます。

has_attached_file :pdf, convert_options: {all: '-flatten'}

Grim処理のworkerを作成

PDFから画像を切り出して保存&S3でアップロードまでやろうとすると時間がかかるので、Workerで処理するようにします。

class CreatePdfImagesWorker
  include Sidekiq::Worker
  sidekiq_options queue: :create_pdf_images, retry: false

  def perform(document_id, target_file)
    document = Document.find(document_id)
    old_pdf_images = document.pdf_images.presence

    pdf = Grim.reap(target_file)
    # 切り出した画像の一時的な保存先を生成
    path = FileUtils.mkdir_p(Rails.root.join "tmp", "pdfs", document_id.to_s).first

    # 切り出す枚数(デフォルト3ページ分)
    page_num = 3
    page_num = pdf.count if pdf.count < 3

    page_num.times do |page|
      pdf_file_name = document.pdf_file_name.split(".")[0]
      pdf_image_path = "#{path}/#{pdf_file_name}_#{page}.png"
      # 指定ページを画像として切り出し
      if pdf[page].save(pdf_image_path)
        file = File.open(pdf_image_path)
        PdfImage.create(image: file, document_id: document_id)
        file.close
      end
    end

    # 古い画像の削除
    old_pdf_images.each{|pdf_image| pdf_image.delete} if old_pdf_images

    # アップが完了した作業ファイル、ディレクトリを削除
    FileUtils.remove_entry_secure path
    FileUtils.remove_entry_secure target_file
  end
end

controllerでworker呼び出し

controller内でやることは2つです。
1. PDFのtmpファイルを自分の扱いやすいところにコピーする
2. workerを呼び出す

1. PDFのtmpファイルを自分の扱いやすいところにコピーする
privateメソッドとして/tmp以下にあるPDFをコピーするメソッドを作ります。

# 画像切り出し用に、/tmp以下に配置されるPDFのtmpファイルをコピーしておく
# 戻り値はコピーしたtmp fileのpath
def copy_temp_pdf
  tempfile = document_params[:pdf]&.tempfile
  return unless tempfile

  pdf_dir = FileUtils.mkdir_p(Rails.root.join "tmp", "pdfs").first
  # PDFのtmpファイルをプロジェクトルートのtmp以下にコピー
  FileUtils.cp tempfile, pdf_dir
  "#{pdf_dir}/#{File.basename(tempfile)}" # File.join(pdf_dir, File.basename(tempfile)) でも同じ
end

document = Document.new 等で生成したインスタンスをsaveすると、PDFをアップロードする際にtmpファイルも消えてしまうので、saveする前にtmpファイルをコピーする copy_temp_pdf メソッドを呼び出します。
コピーしたファイルのパスが返ってくるので、変数に入れておきます。

2. workerを呼び出す
documentのidが必要になるので、workerの呼び出しはインスタンスsave後に行います。

class DocumentsController < ApplicationController
  # --- 諸々省略 ---
  def create
    @document = Document.new(document_params)
    target_file = copy_temp_pdf

    if @document.save
      CreatePdfImagesWorker.perform_async(@document.id, target_file) if target_file
      format.html
    end
  end
  # --- 諸々省略 ---
end

ざっとこんな感じでした。 ゴリゴリ気合いでやるか、複数枚アップロードするしかないのかなと思っていた矢先にこのGem。 やることが少ない分、使いやすい&わかりやすかったです。 tempファイルごにょごにょしてるあたり、もっと上手くやれたらなー。

【Linux】ディスクフル?と思ったら

以前のエントリでも出てきたのですが、ディスクフルな状態に遭遇することがままあります。 今となっては慣れつつありますが、たまに「ええっと?」となるので、備忘録的に。

ディスクの使用量確認

主に使うのはdfコマンドとduコマンド。
参考:
Linuxコマンド集 - 【 df 】 ディスク・ドライブの使用量を表示する:ITpro Linuxコマンド集 - 【 du 】 ディレクトリ内のファイル容量を表示する:ITpro

dfコマンドで本当にディスクフルなのか、どこが容量を食っているのかを確認します。
hオプションを付けると、人間が見やすいように1Mや1Gなどの単位付きで容量が表示されます。

$ df -h
ファイルシステム    サイズ     使用    残り   使用%    マウント位置
/dev/disk1      465Gi    29Gi  436Gi     7%     /
devfs           178Ki   178Ki    0Bi   100%     /dev

コマンドを実行すると↑のように表示されます。
見たまま、使用%が100%であればディスクフルです。アウト。

ファイル/ディレクトリの容量確認

上の例はめちゃくちゃ余裕ありますが、「やれ、/が残り少ないぞ」とわかったら、次にduコマンドを使って、容量を使いまくっている場所を調べていきます。
sオプションで指定したファイル/ディレクトリの総計、hオプションで、単位付きで容量が表示されます。
/*のようにディレクトリを指定すると、/以下のディレクトリも確認することができます。

$ du -sh /*
2.6G    /private
1.0M    /sbin
4.0K    /tmp
509M    /usr

ルート権限でないと見れず「Permission denied」のエラーが出ることもあるので、その場合はsudoをつけて実行
パイプ(|)でsortコマンドを繋げることで、容量の大きい順/小さい順にソートすることもできます。
参考: Linuxコマンド集 - 【 sort 】 行を並び替える:ITpro

sortも使う際は、duコマンドにhオプションをつけているとうまくソートされないことがあるので、その場合はhオプション抜きで実行。
nオプションで先頭の数値や記号を数値と見なし、rオプションで逆順にソートします。

# hオプションあり
$ du -sh /* | sort -nr
509M    /usr
4.0K    /tmp
2.6G    /private
1.0M    /sbin

# hオプション無し
$ du -s /* | sort -nr
5433696 /private
1042688 /usr
2072    /sbin
8       /tmp

何が容量を使っているのかが判明したら、後は不要ファイルを削除していくだけです。

iノード枯渇説

「ディスク使用率はまだ余裕あるんだけどなぁ…」という時は、iノード数が原因だったということもあるようです。
過去一度だけ遭遇したことがあります。 qiita.com ノード数の確認はdfコマンドのiオプションで調べられます。

iノードについてはこちらがわかりやすかったです。
参考:iノード(inode)とは

雑感

本当はディスクフルになる前に対応できれば良いんだけどなーと思いつつ、ちゃんと監視しているものはアラートが来ても監視されていないものは気づけない現実。 ちゃんと対応できれば良いんですけどね…。

【MySQL】突然のIncorrect key file for table...エラー対応メモ

結果から書くとディスクフルだっただけでしたが、MySQL経験が浅いということもあり、初めて見るエラーに心臓止まるかと思った出来事でした。

- *** -


エラー文の「Incorrect key file for table」でググるとこちらのページがひっかかります。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 15.2.4.1 MyISAM テーブルの破損

「え?破損?」と、早速ビビリポイントが出てくるのですが、落ち着いて中を見ます。

次の状況が発生した場合、テーブルが破損するおそれがあります。
・ mysqld プロセスは、書き込みの最中に強制終了されます。
・コンピュータが予期せずシャットダウンされます (たとえば、コンピューターの電源が切られた場合など)。
・ハードウェア障害。
・サーバーが修正中のテーブルを、外部プログラム (myisamchkなど) を使用して同時に修正しています。
MySQL または MyISAM コードのソフトウェアバグです。
MyISAM テーブルのヘルスを CHECK TABLE ステートメントを利用して確認でき、破損した MyISAM テーブルを REPAIR TABLE を利用して修復できます。

プロセスやサーバ類になにも問題ないことを確認し、もしかしたら本当にテーブルぶっ壊れたかもしれない…と思いながらmysqlcheckコマンドでテーブルのヘルスチェックを試します。
mysqlcheckは下記が参考になりました。 d.hatena.ne.jp

念のため全テーブルをチェックしましたが、全てOKで返ってきました。
頭を抱えながら再度「Incorrect key file for table」でググると、こちらの記事にたどり着きます。
d.hatena.ne.jp 容量の問題かも、という文に「そんなまさか」と思いながらdfコマンドを叩くと、案の定ディスク使用量が100%でした…。

よく考えると、確かにディスクフルだと実行できないよなぁ。