kikeda1104's blog

備忘録・技術に関することを書いています。(webエンジニア)

Unicornの参照先が変わらないケース(Capistrano)

前提条件

問題

デプロイした後にWeb + Appサーバにアクセスすると、release versionが古いバージョンにアクセスしている(しかもviewsのみ)そのrelease versionは削除されているので、templateがないと言われてAppサーバがエラーを吐いていた。

#{app_path}/currentが、シンボリックリンクで、更新されていないことでApplication Errorが起きた。unicornプロセスも古いコードを参照したままになっていた。

暫定対応

unicornUSR2シグナルによる再起動ではなく、QUITシグナルで殺してから、unicornプロセスを起動し直す。

kill -s QUIT `pid`
bundle exec unicorn -c /var/www/app_name/config/unicorn.rb -E development -D

これで解決した。

対応

以前からデプロイを繰り返していたにも関わらず今回、初めて起きた事象だったので、前提がおそらく違うのだが、対応はほぼ同じになる。

# config/deploy.rb set :bundle_binstubs, -> { shared_path.join('bin') } # config/unicorn.rb app_path = '/var/www/app' Unicorn::HttpServer::START_CTX[0] = File.join(app_path, 'shared/bin/unicorn')

unicorn + capistrano 構成で、古いリリースの実行パスを参照し続けてしまう問題 - scramble cadenza

今後

capstranoを継続利用せず、Docker + ECRに寄せていければ嬉しいので提案は検討。ただそれ自体がプロダクトに価値があるのか微妙なところだ。

参考

mgi.hatenablog.com

GitHub - capistrano/bundler: Bundler support for Capistrano 3.x

GitHub - defunkt/unicorn: Unofficial Unicorn Mirror.

unicorn/http_server.rb at master · defunkt/unicorn · GitHub

独り言 - 内容と関係ないです。

備忘録と言う名目もあるので、簡単なコードは引用しますが、それ以外は自分で形を変えてをコードを取ってくるつもりです。 (参考記事を見ればその辺は把握できる人たちが読者だと想定して)

sshの接続切れ対応(Broken pipe)

以前から利用しているサーバに対してクライアントには設定(.ssh/config)していたが、サーバに始めて接続する際に、オプションを設定していなかった。 サーバ接続中に起きたので、再設定して、sshした。

接続状態を維持したいので、ServerAliveIntervalとServerAliveCountMaxを設定する。(TCPKeepAliveはデフォルト値がyes)

.ssh/config
host lvh.me
  user ec2-user
  port 22
  IdentityFile ~/.ssh/lvhme.pem
  ServerAliveInterval 15
  ServerAliveCountMax 100

以上。

参考

ssh_config(5) - OpenBSD manual pages

man ssh_config

TCPKeepAlive
Specifies whether the system should send TCP keepalive messages to the other side.  If they are sent, death of the connection or crash of one of the machines will be properly noticed.  However, this means that connections will die if the route is down temporarily, and some people find it annoying.
The default is yes (to send TCP keepalive messages), and the client will notice if the network goes down or the remote host dies.  This is important in scripts, and many users want it too.
To disable TCP keepalive messages, the value should be set to no.

ServerAliveCountMax
Sets the number of server alive messages (see below) which may be sent without ssh(1) receiving any messages back from the server.  If this threshold is reached while server alive messages are being sent, ssh will disconnect from the server, terminating the session.  It is important to note that the use of server alive messages is very different from TCPKeepAlive (below).  The server alive messages are sent through the encrypted channel and therefore will not be spoofable.  The TCP keepalive option enabled by TCPKeepAlive is spoofable.  The server alive mechanism is valuable when the client or server depend on knowing when a connection has become inactive.

The default value is 3.  If, for example, ServerAliveInterval (see below) is set to 15 and ServerAliveCountMax is left at the default, if the server becomes unresponsive,
ssh will disconnect after approximately 45 seconds.

ServerAliveInterval
Sets a timeout interval in seconds after which if no data has been received from the server, ssh(1) will send a message through the encrypted channel to request a
response from the server.  The default is 0, indicating that these messages will not be sent to the server.

BSD                              June 23, 2018                             BSD

sidekiq + redisの構築(Rails 4系) 3

今回は、workerのテストを書いていきます。

前提環境

Redis Worker Classの場合

方針を決めます。retryの上限数を超えた場合のテストも書きたいので、(Dead Job Queueに移す前に通る

# app/workers/high_worker.rb
class HighWorker
    include Sidekiq::Worker
    sidekiq_options queue: 'high'

    sidekiq_retries_exhausted do |msg, ex|
      puts "hoge" # loggerへの記載 or 通知
    end

    def perform(args)
       raise
    end
end

max_retriesは、sidekiq.ymlでも定義することができ、各worker classでも定義できます。

# sidekiq.yml

max_retries: 1 

でリトライの上限数を変更できます。

Rspec

Dead Job Queueに移動する際のテストを書く ただし、コールバックが呼ばれるかのテストではなく、そこに書いているロジックをテストしたい。これもクラスとしてくくり出せるならそちらでテストを書くことでも回避できる。

上記テストのためにgem rspec-siedkiqを追加する。 (sidekiqにもテスト用moduleがありますが、上記のテストを行いたいので、こちらを選びました)

# Gemfile
group :test do
  gem 'rspec-sidekiq'
end
# app/workers/high_worker_spec.rb

require 'rails_helper'

Rspec.describe HighWorker do
   describe '#perform' do
      it 'should be enqueue' do
        expect do
          HighWorker.perform
        end.to change(HighWorker.jobs, :size).by(1)
      end

      it 'shold call sidekiq_retries_exhausted' do
         HighWorker.within_sidekiq_retries_exhausted_block do
         expect(HighWorker).to receive(:puts).with('hoge')
         end 
      end
   end
end

おまけ

警告文を削除したい場合

> [rspec-sidekiq] WARNING! Sidekiq will *NOT* process jobs in this environment. See https://github.com/philostler/rspec-sidekiq/wiki/FAQ-&-Troubleshooting

# rails_helper.rbに追記

RSpec::Sidekiq.configure do |config|
  # Warn when jobs are not enqueued to Redis but to a job array
  config.warn_when_jobs_not_processed_by_sidekiq = false
end

以上になります。

参考

Error Handling · mperham/sidekiq Wiki · GitHub

GitHub - philostler/rspec-sidekiq: RSpec for Sidekiq

Testing · mperham/sidekiq Wiki · GitHub

テーブル定義/データインポート(Amazon Redshift)

前回mysqlからtsvファイルとしてダウンロードしたファイルをredshiftにデータをロードする。検証目的なので、batch処理は後日。

前提環境

  • Redshift(AWS)
  • MySql(5.7系)
  • macOS 10.13.5
  • PostgreSQL 9.6.2
  • 前回の記事でtxtファイルをs3アップロードしている

Redshiftにクラスターを追加

ここは省いて、追加済みとして進めます。

redshiftにテーブルを作る。

# tables.sql
create table tables
(
  id BIGINT NOT NULL PRIMARY KEY,
  name VARCHAR(255),
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
  • ソートキーは無視します。
  • mysql側に定義したテーブルのカラム順番とredshiftのカラムの順番を一致させている。
  • 値を流し込む場合にredshiftの仕様により流し込みが失敗・エラーになるケースがあります。
    • String length exceeds DDL length Mysqlで扱うVARCHARの仕様のズレによりRedshiftではsizeを変更。 渡された数値を文字数として扱うかバイト数で扱うかの違い

postgresql clientからredshiftに接続して、テーブルを作ります。

コマンドを簡易化するためにshファイルを作っておく。

# redshift.sh
psql -Uusername poscalc --host=endpoint -p port
# redshiftのusernameに対応したpasswordを入力する
bash redshift.sh < tables.sql

 

データ投入

# import_to_redshift.sql
copy tables
from 's3://buckets/txts/tables.txt'
iam_role 'arn:aws:iam::000000000:role/RedshiftRoleForClient'
region 'ap-northeast-1'
blanksasnull
emptyasnull
csv;
bash redshift.sh < import_to_redshift.sql

データの確認

bash redshift.sh
select * from tables;

...=> 結果確認

以上になります。作るケースもありますが、redshiftと繋げるBIツールもありますのでユーザ(SQL必須)にはそちらを使って確認してもらうこともできます(tableau Desktop ... etc)

チューニングに関しては、まだ書く意欲があれば書きます。

次の課題

本稼働に向けて、batch処理化する必要がありますので、AWS DATAPIPEを利用することも検討しますし、記事内のスクリプトをcronに設定して、lambda(AWS)で流し込むことも可能です。

費用面と設計面でどちらを選択するか検討する。

回線速度の問題で、s3のアップロードで時間がかかるので、zip, gzipにしてからアップロードして、s3 + lambdaで解凍するとこの問題を迂回する。

参考

文字型 - Amazon Redshift

MySQL :: MySQL 5.7 Reference Manual :: 11.4.1 The CHAR and VARCHAR Types

tsvファイル出力(MySQL)とaws s3アップロード

RedShiftへのデータロードで、テーブルからcsvでダウンロードしてs3にアップロードします。txtファイルの出力とs3のアップロードについて書いていきます。

txtファイル

SQLファイルを作る

# dump.sql
select * from c into outfile '/tmp/table_name.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"';
...

tsvファイルをdump

mysql cliからsqlファイルを実行する。

mysql -u username -p database_name < dump.sql 

s3へのアップロード

aws認証情報を登録

awsのIAMからaccess_key, secret_access_keyを取得、なければ作成する。defaultは登録されているので、プロファイル名は別で定義する。

$ aws configure --profile profile_name
AWS Access Key ID [None]: access_key
AWS Secret Access Key [None]: secret_access_key
Default region name [None]: ap-northeast-1
Default output format [None]: json

shファイルを作る

syncコマンドを利用してアップロードします。shファイルを作ります。

# upload_to_s3.sh
aws s3 sync /tmp/txts s3://bucket_name/txts --profile profile_name --exclude '*' --include '*.txt'
....

shファイルを実行

bash upload_to_s3.sh

# 結果
move: /tmp/table_name.txt to s3://bucket_name/txts/table_name.txt # 成功
move failed: /tmp/table_name.txt to s3://bucket_name/txts/table_name.txt Could not connect to the endpoint URL: "endpoing" #失敗
....

以上になります。

参考

AWS CLI の設定 - AWS Command Line Interface

AWS Command Line Interface での高レベルの S3 コマンドの使用 - AWS Command Line Interface

sync — AWS CLI 1.15.50 Command Reference

AWS CLI S3 Configuration — AWS CLI 1.15.50 Command Reference

sidekiq + redisの構築(Rails 4系) 2

前回の続きで、sidekiqのデプロイについて書いていきます。

前提環境

capistranoを利用していたので、capistrano/sidekiqをGemに追加します。 unicorn, pumaなどの設定は無視していきます。

実装

# Gemfile
   group :development do
     gem 'capistrano-sidekiq'
   end
# config/Capfile

...
  require 'capistrano/sidekiq'
  require 'capistrano/sidekiq/monit'
...
# config/deploy.rb

...
set :pty,  true
set :sidekiq_config, -> { File.join(shared_path, 'config', 'sidekiq.yml') }
set :sidekiq_monit_conf_dir, '/etc/monit.d'
  ...

(sudo権限があるユーザを利用することしない場合は、pty, false(default)にする) default hookが追加されますので、deployに合わしてsidekiqプロセスが起動・停止します。

sidekiq_default_hooks Sidekiq will start or stop automatically during deployments. Just set sidekiq_default_hooks to false if you don't want this to happen.

sidekiq_default_hooksをfalseにすれば、hooksを無効に出来ます。

手動起動

cap env sidekiq:start

以上。

参考

GitHub - seuros/capistrano-sidekiq: Sidekiq integration for Capistrano

Home · seuros/capistrano-sidekiq Wiki · GitHubHome · seuros/capistrano-sidekiq Wiki · GitHub

sidekiq + redisの構築(Rails 4系)

非同期処理を取り入れる提案をして、採用されたので構築していました。アウトプットとして何回かに分けて記事を書いていきます。

前提環境

前提環境の背景を話すと、Rails 5系にアップデートできていないプロジェクトです。テストは随時入れていますので、工数が取れればアップデートは出来ますが今のところ予定はないです。

サーバ構成はこんな感じ AWS EC2( Application Server Rails) <=> AWS EC2(Batch Server Rails, sidekiq(worker), Batch(cron)) <=> Aws ElastiCache(Redis, クラスターモード無し)

Rails 4.2系でもActive Jobがあり、retry機能が想定しているエラーを迂回できそうになかったので、workerクラスを実装して進めています。ActiveJobを利用するとRailsがadapterをラップしていることによるメリットもあり、ユースケースを満たせれば,テストコードも楽に書けるようになり採用は出来たと思います。

まずredisのインストールは、この記事ではローカル上にインストールします。(Dockerはまた別の機会に書きます) Hometbrewを利用してインストールします。

redis

  brew install redis
  redis-server
   `/usr/local/etc/redis.conf`が保存されている。

続いてAppに実装

# Gemfile
  gem 'sidekiq'
  bundle install
 # app/workers/high_worker.rb
  class HighWorker
    include Sidekiq::Worker
    sidekiq_options queue: :high, retry: false
    
    def perform(csv_file_id)
       AbcCsvImporterService.call(csv_file_id)
       # 完了通知 or logging
    end
  end

# AbcCsvImportクラスは実装無し。クラス内の例外についても対応する。thread safeのコードにする。(gemが提供するライブラリを使う場合も注意)
# retry: falseなので、Dead Job Queueにはenqueueされません。
# config/application.rb
...
config.active_job.queue_adapter = :sidekiq
....
# config/sidekiq.rb
---
:verbose: false
:timeout: 15
:concurrency: 2
staging:
  :concurrency: 5
production:
  :concurrency: 5
:queues:
  - high
# database.yml
development:
  adapter: mysql2
  encoding: utf8
  database: my_db
  pool: 7
  username: my_user
  password: my_password
  host: localhost
....
# config/initializes/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://localhost:6379', network_timeout: 5 }
end

Sidekiq.configure_client do |config|
  config.redis = { url: `redis://localhost:6379` network_timeout: 5 }
end

ローカル上で動作することに限定するなら、設定ファイルをなくして、デフォルトにしてもいいです。上記のurlは、sidekiqのデフォルトになります。本番、ステージングなどある場合は環境別に変えましょう。network_timeoutオプションは、EC2を利用する都合timeout時間を伸ばしています。デフォルトは1です。

database poolと同値かそれ以下にconcurrencyを設定。

# csv_import_controller
class CsvImportController < ApplicationController
  before_action :set_csv_file, only: %w[new create]
  before_action :set_csv_files, only: %w[index]

  def index; end
  def new; end

  def create
     if @csv_file.save
         redirect_to new_csv_import_path, notice: '保存に成功しました'
     else
      render :new, notice: '保存に失敗しました'
     end
  end

  private
 
  def csv_file_params
     return params.require(:csv_file).permit(CsvFile.column_names) if params[:csv_file]
     {}
  end
 
  def set_csv_file
     @csv_file = Csvfile.new(csv_file_params)
  end

  def csv_files
    # index用
  end
end
# app/model/csv_file.rb

class CsvFile < ActiveRecord
   after_commit :import_data, on: :create

   def import_data
      CsvFileWorker.perform_async(id)
   end
end

viewsは省きます。CSVファイルの入力フォームのイメージです。

bundle exec sidekiq -C config/sidekiq.yml

以上です。次回、AWSの構成、テストコード、redisなどの記事を書ければ書きます。色々ノウハウ溜まっているのでそのほかにElasticsearchなど。そのうち。

参考

Home · mperham/sidekiq Wiki · GitHub

Error Handling · mperham/sidekiq Wiki · GitHub

https://github.com/mperham/sidekiq/wiki/Problems-and-Troubleshooting#threading

https://github.com/mperham/sidekiq/wiki/Problems-and-Troubleshooting#cannot-find-modelname-with-id12345

PostgreSQLサーバの起動コマンド

毎回ググっているので、自分の備忘録として残しています。

環境

## PostgreSQLサーバの起動(homebrewでインストール)
postgres -D /usr/local/var/postgres

参考

18.3. データベースサーバの起動

drop_tableの代わりにActiveRecordMigration#revert

環境

前提

初期からRailsでアプリを作成していて、tableを作成したmigrationもそのアプリが保有していることが前提になります。

modelを途中で消す場合、./bin/rails d model model_nameで消すと作成した際、migration fileも削除される。 途中で、カラムを追加・変更・削除している場合は、./bin/rails db:migrateが失敗することになり、modelを削除する際には、気をつけないと、migrateを一から実行することができなくなり、復旧することになります。

revert

rails app単体の構成であれば、drop_tableを使ってテーブルを削除するよりも、revertを使うのが良いと思います。 では、手順ですが、一度作成したmodel、testファイル、該当するコードを削除した後、tableを作成したmigrationは残しておき、新規drop用のmigration fileを準備します。

サンプル

model bread作成

f:id:kikeda1104:20170630224831p:plain

model rice作成

f:id:kikeda1104:20170630232206p:plain

migrate実行とgit commit

./bin/rails db:migrate
git add . && git commit -m "add bread and rice models"

model bread削除

./bin/rails d model bread
git checkout db/migrate
./bin/rails g migration DropBreadsTable

作成したmigrationをエディタで開く

require_relative '20170629123105_create_breads' # 追記(migrationファイル名は、適切なファイル名に直してください)

class DropBreadsTable < ActiveRecord::Migration[5.1]
  def change
    revert CreateBreads # 追記 create_breadsをキャメルケースに直してrevertの引数に渡す。
  end
end

以上です。これで、migrate, rollbackすることができます。

別サンプル

途中で、カラムを追加していた場合のケースも追加しておきます。

breadsにcolumnを追加

./bin/rails g migration AddIsSweetnessToBreads

migration file編集

class AddIsSweetnessToBreads < ActiveRecord::Migration[5.1]
  def change
    add_column :breads, :is_sweetness, :boolean, default: false
  end
end

テーブルを削除するmigration fileを作成

require_relative '20170630135043_add_is_sweetness_to_breads'
require_relative '20170630134600_create_breads'

class DropBreadsTable < ActiveRecord::Migration[5.1]
  def change
    revert AddIsSweetnessToBreads
    revert CreateBreads
  end
end

以上です。

./bin/rails db:migrate
./bin/rails db:rollback STEP=3
./bin/rails db:migrate

複数テーブルの操作をmigrationに書き込んでいる場合

このケースだと対応はできないです。 migration fileは、1テーブルの操作に制限して、プロジェクトメンバーに周知して守らせる必要がありそうです。 (上記をオススメしますが、migration fileの書き方で、dropしないテーブルに関する操作をrevert後にコピーしてくることで復旧できそうなので、 今度改めて、追記します。)

参考

http://api.rubyonrails.org/classes/ActiveRecord/Migration.html#method-i-revert

https://railsguides.jp/active_record_migrations.html#%E4%BB%A5%E5%89%8D%E3%81%AE%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E9%80%86%E8%BB%A2%E3%81%99%E3%82%8B

Capybara aタグ(target="_blank")をクリックについて

掲題の件、featureテストを書く量が日に日に増えてきており、何を書くかと書き足りないケースなどを自覚できるになり知見も日々、蓄積できていて楽しく、 クライアントと紹介してくれた友人に感謝っすね。

aタグのtarget属性(_blank)

新規ウィンドウ、タブを開いてページを表示した後、表示したページで、タイトルを確認する方法を書いていきます。

環境

サンプルコード

require 'rails_helper'

feature 'TOP画面からの操作', type: :feature do
  scenario 'ユーザがリンクをクリックして新規タブが開いている' do
    within_window(window_opened_by { first("a.new-window-link").click }) do
      expect(page).to have_content '新しいページタイトル'
    end
   end
 end

#  なお、within_window(string)は、非推奨になっているため注意。

感想

テストを書いているプロジェクトって、本当少ないですよね・・。

参考

Method: Capybara::Session#within_window — Documentation for capybara (2.14.3)