scramble cadenza

技術ネタのガラクタ置き場

VPC についてまとめ

イントロ

VPC 周りの知識が曖昧なままここまで来てしまったので、整理する。
なんとなく知っていた用語や単語について、一般的な概要、及び AWS ではどういう特徴をもっているか、を大雑把にまとめたもの。

VPC

概要

Virtual Private Cloud の略。Vrtial Private Cloud とは、クラウド環境に構築された仮想的なプライベートネットワークのこと。
VPC 自体が 1 つの IP アドレスを持っていて、VPC は沢山の private ip を持っている。
そんなインターネット上のでっかい領域を指す。

AWSでは

  • VPC は CIDR = x.x.x.x/16 ~ x.x.x.x/28 の間で作成可能
  • デフォルトの VPC というものが存在する
  • CIDR は一度作成したら変更不可
    • 後からレンジを変えられないので注意
  • VPC 削除のタイミングで、関連のAWS リソースは削除されてしまう
    • subnet とか Internet Gateway とか色々。
    • 削除して作りなおすと、全て作り直しになってしまうので、CIDR は余裕を持たせるのが基本

VPC の作成方法

VPC とサブネット - Amazon Virtual Private Cloud

インターネットゲートウェイ

インターネットゲートウェイ - Amazon Virtual Private Cloud

  • VPC のルーティングを各サブネットのルーティングテーブルに追加できる
  • NAT 変換を行い、public IP を持つ instance がインターネットに接続する

という機能を持つ。

AWS では

  • デフォルトの VPC というものが存在する。
  • インターネットゲートウェイがデフォルトの VPC にアタッチされている
  • デフォルトのサブネットの中で起動したインスタンスは、public IP が付与される
    • public の IP が付与されると、インターネットゲートウェイが NAT 変換を行える。
    • つまりインターネットに接続できる

というわけで、何も考えずに EC2 インスタンスをポチると、外部と通信することができるようになるのは、AWS の素晴らしい挙動によるもの。

インターネットゲートウェイの作成

インターネットゲートウェイ - Amazon Virtual Private Cloud

ルートテーブル

ルートテーブル - Amazon Virtual Private Cloud

インターネットトラフィックがどこからどう流れていくか、を記した経路のこと。
どこからどこにトラフィックを流す、という対応表で表現される。

AWS では

ルートテーブル - Amazon Virtual Private Cloud

にまとめられてる。全部重要だけど最低限の機能としては次の通り。

  • VPC を作成すると、ルートテーブルが自動で割り振られて、インターネットゲートウェイへのルートが追加されている。これを メインルートテーブル と呼ぶ
  • サブネットを特定のルートテーブルに紐付けないと、メインルートテーブルに紐付けられる。
    • つまり作成したサブネットはデフォルトでインターネットに通信できる。
  • 新たに追加する場合、ルートは CIDR で指定する

ルートテーブルを操作する

ルートテーブル - Amazon Virtual Private Cloud

サブネット

VPC の中で作成するグループのこと。 VPC が持つ IP address の範囲を超えない程度に、自由にグループ化可能。

VPC都道府県とするなら、サブネットは市区町村みたいな立ち位置。

サブネットの性質上、ルートテーブルに関連付けることにより、初めてサブネット内にトラフィックを流すことができる。

{public,private} サブネット

public サブネット

インターネットゲートウェイにルーティングされているサブネットのこと。 public サブネット内に作成された EC2 インスタンスが public ip もしくは elastic ip を持っている場合、インターネットに接続することができる。

private サブネット

インターネットゲートウェイに routing されていないサブネットのこと。 このサブネット内に EC2 インスタンスを作成しても、通常はインターネットに接続できない。 インターネットに接続するためには、NAT gateway を構築したり、インターネットゲートウェイをルートテーブルに追加する必要がある。

予約された ip address

  • 10.0.0.0
    • ネットワークアドレス。ネットワーク自身を表すアドレス
  • 10.0.0.255
    • ネットワークブロードキャストアドレス。ローカルネットワークのホスト全てに通信するためのアドレス

これ以外にも AWS が独自に予約しているアドレスが存在する。

AWS では

  • AZ(Availability zone) にまたがってサブネットを構築することはできない

AWS が予約している ip address

参考

まとめ

AWS のドキュメントは入門書として優秀。 勿論周辺情報を理解するためには、別の文献や本で知識を補わないとダメだけど、AWS なら実際に手を動かしながら試せる環境があるし、概要の把握という意味では理解が進みやすい。

本当はもっと書きたかったのだけど、一旦ここまで。

ActiveRecord::Base.connection#table_exists? と ActiveRecord::Base#table_exists? の違い

イントロ

#

似ているメソッドで、微妙に挙動が異なり、ちょっとハマったのでメモ

違い

引数、結果をキャッシュするかどうかが違う

引数

  • ActiveRecord::Base.connection#table_exists?
    • 引数を一つ取る
ActiveRecord::Base.connection.table_exists?("users")
#=> 引数で受け取ったテーブルが存在していれば true, なかったら false
  • ActiveRecord::Base.table_exists?
    • 引数を取らない
class User < ActiveRecord::Base
end

user = User.first
user.class.table_exists?
#=> モデルに対応したテーブルが存在していれば true, なかったら false

キャッシュするかどうか

  • ActiveRecord::Base.connection#table_exists?
    • キャッシュしない
    • 毎回 database とやり取りして、テーブルが存在するかどうかを確認するメソッド
ActiveRecord::Base.connection.table_exists?('users')
#=> true

ActiveRecord::Base.connection.drop_table('users')

ActiveRecord::Base.connection.table_exists?('users')
#=> false
  • ActiveRecord::Base.table_exists?
    • 一度呼びだされたらキャッシュされる
      • キャッシュ先は ActiveRecord::Base.connection
user = User.first

user.class.table_exists?
#=> true

ActiveRecord::Base.connection.drop_table('users')

user.class.table_exists?
# => true
# DROP TABLE `users` したのに true が返る!

ActiveRecord::Base.connection.schema_cache.table_exists?('users')
# => true
#
# * `ActiveRecord::ConnectionAdapters::SchemaCache` によってメソッド呼び出し結果が保存されている。
# * `ActiveRecord::ConnectionAdapters::SchemaCache` インスタンスは `ActiveRecord::Base.connection` のインスタンスが保持している

結論

上にも書いたとおり、引数と、メソッド呼び出しの結果をキャッシュするかどうかが違う。

  • ActiveRecord::Base.table_exists? は通常 ActiveRecord::Base 継承したモデル(例だとUser)が対象となるので、引数を取らない
  • また、table の存在判定は、一般的には変更されにくい値であるので、一度 table_exits? を呼び出したらキャッシュされる

と覚えれば良い。

私は動的テーブルを作ったり消したりしていてハマってしまいました。
ActiveRecord を使って、頻繁にテーブルを作成したり削除したりする場合は注意が必要。かも。

# 表記に迷ったのですが ActiveRecord::Base.connection の戻り値は、ActiveRecord::ConnectionAdapters::XxxAdapterインスタンスが返るので、 ActiveRecord::Base.connection#table_exists? という表記は正確ではありません。可読性を考えて敢えてそのままにしてます。(rails 触ったことある人に馴染み深そうな表記のほうがわかりやすいのでは、と考えてのこと)

Mac で apery を compile する

イントロ

最近、職場の後輩に将棋で負けたことをきっかけに、将棋がマイブーム。

apery って?

将棋プログラムの一種。2014 年の世界大会で優勝したとかなんとか。
一部界隈では、あの ponanza 先生をも凌ぐとかなんとか言われている?らしく、とにかくすごく強いソフト。

Apery - Wikipedia

で、そんな世界大会で優勝した将棋エンジンが github 公開されているのを知ったので、compile してみました。

HiraokaTakuya/apery

そのままやると...

すさまじい警告が出て、失敗する

$ make
24 warnings generated.
g++ -std=c++11 -fno-exceptions -fno-rtti -Wextra -Ofast -MMD -MP -fopenmp -march=native  -o ../obj/common.o -c common.cpp
g++ -std=c++11 -fno-exceptions -fno-rtti -Wextra -Ofast -MMD -MP -fopenmp -march=native  -o ../obj/pieceScore.o -c pieceScore.cpp
g++ -o apery ../obj/main.o ../obj/bitboard.o ../obj/init.o ../obj/mt64bit.o ../obj/position.o ../obj/evalList.o ../obj/move.o ../obj/movePicker.o ../obj/square.o ../obj/usi.o ../obj/generateMoves.o ../obj/evaluate.o ../obj/search.o ../obj/hand.o ../obj/tt.o ../obj/timeManager.o ../obj/book.o ../obj/benchmark.o ../obj/thread.o ../obj/common.o ../obj/pieceScore.o -lpthread  -std=c++11 -fno-exceptions -fno-rtti -Wextra -Ofast -MMD -MP -fopenmp -march=native
ld: library not found for -lgomp
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [apery] Error 1

gomp というライブラリが無いと言われているようだ。

gomp とは

GOMP — An OpenMP implementation for GCC - GNU Project - Free Software Foundation (FSF)

openMP で実装している GNU project らしい。

openMP とは

複数CPU を効率的に計算するライブラリのこと。所謂スレッド。
http://www.cc.u-tokyo.ac.jp/support/kosyu/03/kosyu-openmp_c.pdf

計算を早く行うためにマルチスレッドで処理しているのだろう、と想像。

それと URI protocol の仕様として、計算中でも入力を受け取れるようにしなければならないので、そういうところでもスレッドの出番はありそう。

Mac でどう compile するか

g++-4.9 で compile する

環境

  • OSX 10.11.2
$ brew install gcc49
$ g++-4.9 --version
g++-4.9 (Homebrew gcc49 4.9.3) 4.9.3
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,4 +1,4 @@
-COMPILER = g++
+COMPILER = g++-4.9
 #COMPILER = mpicxx
 CFLAGS   = -std=c++11 -fno-exceptions -fno-rtti -Wextra -Ofast -MMD -MP -fopenmp
 CFLAGS   += -march=native

結果

$ cd src
$ make
g++-4.9 -o apery ../obj/main.o ../obj/bitboard.o ../obj/init.o ../obj/mt64bit.o ../obj/position.o ../obj/evalList.o ../obj/move.o ../obj/movePicker.o ../obj/square.o ../obj/usi.o ../obj/generateMoves.o ../obj/evaluate.o ../obj/search.o ../obj/hand.o ../obj/tt.o ../obj/timeManager.o ../obj/book.o ../obj/benchmark.o ../obj/thread.o ../obj/common.o ../obj/pieceScore.o -lpthread  -std=c++11 -fno-exceptions -fno-rtti -Wextra -Ofast -MMD -MP -fopenmp -march=native

というわけでうまくいった。

うまくいったけど、どういうことなのか

HiraokaTakuya/apery の README.md より。

Linux のディストリビューションによっては、Makefile に記述されている '-lpthread' を '-pthread' にしなければ、
実行時にエラーになってしまう場合があります。
Linux, Windows で G++ 4.8 以上のバージョンで動作確認をしています。
Clang では正しくビルド出来ているか確認出来ていません。
Visual Studio でビルドすることは現状では出来ません。
Windows でビルドする場合は、MinGW64 をお使い下さい

というわけで、g++4.9 を使うとうまくいく、と。
mac のデフォルトでは、gcc コマンドが clang コマンドの alias になるという罠があり、明示的に 4.9 以上の gcc を指定する必要があった、というオチ。
普段の生活では gcc に馴染みがないので、少しハマった。

起動を早くする

初回起動時は評価関数生成のため、起動が非常に遅い。
ので、以下を行って評価関数のみ生成しておく。

linux,macでのコンパイルと使い方 · HiraokaTakuya/apery Wiki

$ git submodule init
$ git submodule update --depth=1

# コンパイル済みの apery を bin 以下に移動させる
$ cp ../src/apery . 

$ ls bin
20151105                  Readme.txt                apery                     book                      make_synthesized_eval.bat make_synthesized_eval.sh

# bin の directory 上で `make_synthesized_eval.sh` を実行する。
$ cd bin
./make_synthesized_eval.sh
info string start setting eval table
info string end setting eval table

で成功。
make_synthesized_eval.sh が、bin 以下で実行されること前提の実装になっていたのが難しかった。

動かしてみる

# 入力待ちになる
$ ./apery

# 標準入力で通信開始の合図を入れる
usi

# apery が標準出力でレスポンスを返してくれる
id name Apery Debug Build
id author Hiraoka Takuya

option name Best_Book_Move type check default false
option name Book_File type string default book/20150503/book.bin
option name Byoyomi_Margin type spin default 500 min 0 max 2147483647
option name Clear_Hash type button
option name Emergency_Base_Time type spin default 200 min 0 max 30000
option name Emergency_Move_Horizon type spin default 40 min 0 max 50
option name Emergency_Move_Time type spin default 70 min 0 max 5000
option name Eval_Dir type string default 20151105
option name Max_Book_Ply type spin default 32767 min 0 max 32767
option name Max_Random_Score_Diff type spin default 0 min 0 max 32600
option name Max_Random_Score_Diff_Ply type spin default 40 min 0 max 32767
option name Max_Threads_per_Split_Point type spin default 5 min 4 max 8
option name Min_Book_Ply type spin default 32767 min 0 max 32767
option name Min_Book_Score type spin default -180 min -32601 max 32601
option name Minimum_Thinking_Time type spin default 1500 min 0 max 2147483647
option name MultiPV type spin default 1 min 1 max 594
option name OwnBook type check default true
option name Skill_Level type spin default 20 min 0 max 20
option name Slow_Mover type spin default 100 min 10 max 1000
option name Threads type spin default 4 min 1 max 64
option name Use_Sleeping_Threads type check default false
option name USI_Hash type spin default 256 min 1 max 65536
option name USI_Ponder type check default true
option name Write_Synthesized_Eval type check default false
usiok

というわけで無事動かせた。
これで世界レベルの将棋ソフトが動かせる!!!

参考

homebrew の install path を変更して、install 済みパッケージを入れなおすの巻

イントロ

年末になったので、自宅 PC を EL Captain にしました。 噂には聞いてたが、/usr/local 周りの権限問題により、普通に install すると躓くところが出てくる。困る。

おまけに解決方法が sudo chown -R $(whoami):admin /usr/local ってどうことだよ、と。 こんな理不尽な方法があっていいのか。

homebrew/El_Capitan_and_Homebrew.md at master · Homebrew/homebrew

自分以外 mac 使わないし、一度は面倒さに屈して、上記のコマンドをしぶしぶ受け入れましたが もう我慢ならぬ ということで、install path を変更して homebrew を再 install しました。

brew bundle

まずは落ち着いて brew bundle を入れる。

% brew bundle
==> Tapping homebrew/bundle
Cloning into '/Users/argerich/.homebrew/Library/Taps/homebrew/homebrew-bundle'...
remote: Counting objects: 49, done.
remote: Compressing objects: 100% (46/46), done.
remote: Total 49 (delta 2), reused 12 (delta 0), pack-reused 0
Unpacking objects: 100% (49/49), done.
Checking connectivity... done.
Tapped 0 formulae (111 files, 472K)

とやると、勝手に tap してくれる。 引き続き、今 install している package を dump する。

% brew bundle dump

Brewfile が作成されていることを確認する。ついでに中身もチェックしておく。

install path を決める

適当に決める。 Homebrew のインストール先を変更する - Qiita を見ると /opt/homebrew とあるが、私は ~/.homebrew とした。

理由はルートディレクトリのすぐ近くに、ユーザー固有の権限を持つディレクトリを掘りたくなかったから。/usr/local の二の舞い感がする。

どうせユーザー固有の権限を持つディレクトリを掘るなら、ホームディレクトリでいいかなと考えた。本当に適当です。

決めたら先に path を通しておく。 一時的にとはいえ、brew を uninstall すると色々使えなくなるので、やっておくのが無難。

export PATH=$HOME/.homebrew/bin:$PATH

uninstall するその前に

zsh

homebrew で install した zsh を使っている場合は、shell が起動できなくて詰みそうになるので、デフォルトの shell を変えておく。

もし変更しないとターミナルが起動しなくなり、コマンドを打たせてもらえない。
(クリーンインストールをマジで考えた)

この場合は冷静に mac 標準のターミナルから起動する shell を指定し対処する。
Terminal - Mavericksにしたらターミナルが - Qiita

こういう状態にならないためにも、予め適当に変えておくと良いと思う

echo $SHELL
# => /usr/local/bin/zsh なら homebrew で install した zsh を使っている
chsh -s /bin/bash # デフォルトシェルを bash に変更

git

alias git="hub" とやっているそこの君! 安易の homwebrew を uninstall すると、git clone すらできないので、気をつけよう。

% cd ~ && git clone https://github.com/Homebrew/homebrew.git .homebrew
zsh: command not found: hub
_direnv_hook:1: command not found: direnv

uninstall

ようやく homebrew を消し去る。 homebrew/FAQ.md at master · Homebrew/homebrew に方法が書いてある。

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

勿論いきなりこれを実行するのではなく、curl 先の ruby スクリプトを念のため読む。 良さそうだと思ったら、勇気を出して実行する。

/usr/local/bin/
/usr/local/etc/
/usr/local/lib/
/usr/local/share/
/usr/local/var/

You may consider to remove them by yourself.
You may want to restore /usr/local's original permissions
  sudo chmod 0755 /usr/local
  sudo chgrp wheel /usr/local

_direnv_hook:1: command not found: direnv # direnv 様がお怒りに….

ゴミが残ってしまった。これは適当に掃除する。
(/usr/local って 0755 だっけ...?)

再 install

消し去ったら homebrew を 再 install する。

ここで注意するのが、tarball を使った install を行わないこと。

homebrew/Installation.md at master · Homebrew/homebrew

tarball なので、.git ディレクトリが含まれてないからです。.git がないと、brew bundle を install できない。

なので普通に clone する。

% cd ~
% /usr/bin/git clone https://github.com/Homebrew/homebrew.git .homebrew

ちゃんと install 先の path が変わっているか確認する HOMEBREW_PREFIX まわりを見れば良さそう。

% brew —config
HOMEBREW_VERSION: 0.9.5
ORIGIN: https://github.com/Homebrew/homebrew.git
HEAD: 8eda6f6417c09a03486353f1c4565bfd2bdbc84b
Last commit: 13 minutes ago
HOMEBREW_PREFIX: /Users/argerich/.homebrew
HOMEBREW_REPOSITORY: /Users/argerich/.homebrew
HOMEBREW_CELLAR: /Users/argerich/.homebrew/Cellar
HOMEBREW_BOTTLE_DOMAIN: https://homebrew.bintray.com
CPU: quad-core 64-bit haswell
OS X: 10.11.2-x86_64
Xcode: 7.2
CLT: 7.2.0.0.1.1447826929
Clang: 7.0 build 700
X11: N/A
System Ruby: 2.0.0-p645
Perl: /usr/bin/perl
Python: /Users/argerich/.pyenv/shims/python => /Users/argerich
Ruby: /usr/bin/ruby => /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby
Java: N/A
_direnv_hook:1: command not found: direnv

再度 brew bundle

Brewfile が置かれているディレクトリに移動して以下を実行。

% brew bundle
Succeeded in tapping benswift/extempore
Succeeded in tapping caskroom/cask
Succeeded in tapping homebrew/binary
Succeeded in tapping homebrew/boneyard
Succeeded in tapping homebrew/bundle
...

後は待つだけ。

まとめ

brew bundle と組み合わせれば、sudo chown -R $(whoami):admin /usr/local に屈しないで済む。
package を install すると環境がパワーアップして便利だけど、こういうぬるま湯に浸かってしまうと、イザという時に普段やってることが何もできなくて、とても困ることになる。

そういう時に備えて、せめて vi は素で使えるように訓練したい(実際している)と常々思う。
幸い普段は emacs 使ってるので、vi は何もカスタマイズせずとも普段の生活に支障はない。

みんなも emacs 使おう。

babelify7.0.2 で jsx の compile に失敗する件

現象

  • babelify 7.0.2
  • browserify 12.0.1
  • react 0.14.1
var React = require('react');

var Container = React.createClass({
  render: function() {
    <div>test</div>
  }
});

React.render(
  <Container />,
  document.getElementById('container')
);
% browserify -t babelify src/app.jsx -o build/bundle.js
SyntaxError: /Users/horowitz/dev/poochie/src/app.jsx: Unexpected token (9:2)
   7 |
   8 | React.render(
>  9 |   <Container />,
     |   ^
  10 |   document.getElementById('container')
  11 | );
  12 |

SyntaxError になってしまう。

方法

2つほど。

1. babelify の version 6.4.0 に戻す

戻すと上手く動いた。
世の中に出回ってる記事のサンプルも動く。

2. presets を指定する

要は追加のオプションを付ける必要があるとのこと。

コマンドラインから実行する場合は、以下のような感じ

npm install --save-dev babel-preset-react
echo '{"presets":["react"]}' > .babelrc
browserify -t babelify src/app.jsx -o build/bundle.js

gulp の場合は configure から presets を渡す

     debug: true
   })
-    .transform(babelify)
+    .transform(babelify.configure({
+      presets: ["react"]
+    }))
     .bundle()
     .pipe(source('bundle.js'))
     .pipe(gulp.dest('build'));

関係ないけど es6 でコードを書いた場合は、babel-preset-es2015 の install と presets: ["es2015"] の指定が必要になるみたい。

まとめ

js 初学者にはキツイお仕置きでした。
babelify7 から babel6 を内部で使っていて、babel6 から新しく option 付ける必要があるらしい、という雑な理解をしています。

参考

respond_to vs routes 指定の format

イントロ

例えばAPI サーバーを作るときに「この URL にアクセスする時は、常に json でアクセスしてほしい」みたいな事を実現したい。

それを実現する具体的な方法としては namespace :api, format: 'json' と routes を書けばいいと思ってました。
(デフォルトの format が json になるので)

ただ

  • それ以外の format を渡した時にどう動くか
  • 似たような事ができる実装とどう違うのか

がよくわかってなかったので、その時にした実験をもう一度やってみたメモ。

パターン1

class Api::AccountsController < ApplicationController
  def index
    @accounts = Account.all

    respond_to do |format|
      format.html { render }
      format.json { render }
    end
  end
end
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resources :accounts
  end
end
  • controller 側では respond_to複数 format を受けつける準備ができている
  • namespace では json を指定

結果

% curl -I http://localhost:3000/api/accounts
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 01d34bba-60c5-4185-9e96-e343d179c45b
X-Runtime: 0.014599
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Content-Length: 10
Connection: Keep-Alive
% curl -I http://localhost:3000/api/accounts.html
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
Content-Length: 41119
X-Web-Console-Session-Id: 939cd9fb2834ea8353d1332a62d97e15
X-Request-Id: a77ea27e-f5c0-4766-b357-09b964188e55
X-Runtime: 6.597595
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Connection: Keep-Alive
  • controller で受け取る formatjson になる
  • response の Content-Typeapplication/json
  • json 以外の format の request が投げられたら、404 Not found
    • controller 側では html をの時は html の render をする準備ができているのにも関わらず
    • routes 側で 404 になってしまうので、controller に到達しない
  • controller 側に到達した時には、常に json 形式
    • (書いてしまってるけど) 実質 renspond_to複数 format 対応は 不要

パターン2

class Api::AccountsController < ApplicationController
  def index
    @accounts = Account.all

    respond_to do |format|
      format.html { render }
      format.json { render }
    end
  end
end
Rails.application.routes.draw do
  namespace :api do
    resources :accounts
  end
end
  • format 指定を削除
  • controller 側は変わらず

結果

% curl -I http://localhost:3000/api/accounts
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8  # text/html に注目!
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 4db51f09-53cc-42ca-a6a0-49e4080b5030
X-Runtime: 0.492789
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Content-Length: 1715
Connection: Keep-Alive
% curl -I http://localhost:3000/api/accounts.html
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 7d446561-2768-4f20-8832-4d6476115d45
X-Runtime: 0.050431
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Content-Length: 1715
Connection: Keep-Alive
  • json が通るか確かめる
% curl -I -X GET http://localhost:3000/api/accounts.json
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: c3c4a0fb-847c-4240-9470-606695c6b935
X-Runtime: 0.023534
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Content-Length: 10
Connection: Keep-Alive
  • respond_to での指定以外の format
% curl -I http://localhost:3000/api/accounts.xml
HTTP/1.1 406 Not Acceptable
Content-Type: text/html; charset=utf-8
Content-Length: 115323
X-Web-Console-Session-Id: bfb96347bd3ab4058a468ebb5762595f
X-Request-Id: 74ee3306-aa22-4615-a72d-c1a7602ef434
X-Runtime: 0.640820
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Connection: Keep-Alive
  • respond_to do ~ end で指定する format の内、 最初に書いてある方が default になる
    • これは controller のコードで、優先順位が決定するということ
    • 今回は format.html { render } を先に書いたので、html が default になった
      • json を上にすると、format 指定なしの状態では json が返る
  • respond_to ブロックで指定している format 以外の request が投げられたら 406 Not acceptable

responders

class Api::AccountsController < ApplicationController
  respond_to :json
  def index
    @accounts = Account.all
    respond_with @account
  end
end
Rails.application.routes.draw do
  namespace :api do
    resources :accounts
  end
end

結果

% curl -I http://localhost:3000/api/accounts
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: f7a06375-320d-4d37-a93c-12d9d3c17cf0
X-Runtime: 0.299781
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Content-Length: 0
Connection: Keep-Alive
% curl -I http://localhost:3000/api/accounts.html
HTTP/1.1 406 Not Acceptable
Content-Type: text/html; charset=utf-8
Content-Length: 115331
X-Web-Console-Session-Id: 780f1e5e4050cb0b7e72d36f60841f4c
X-Request-Id: b0f92ee8-c6fa-4dc4-872e-f60adc942104
X-Runtime: 0.379881
Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
Connection: Keep-Alive
  • パターン2 とほぼおなじ
    • respond_to複数個 format が渡せるが、format 指定無しでリクエストされた場合、最初に指定されてある format が利用される
      • respond_to :html, :json なら、html がデフォルト値
  • 指定以外の format でのリクエストは、406 Not acceptable
  • responders の利点は、controller 毎に format の指定をしなくて済むことなのかな?
    • 一箇所にまとめられていれば、controller 毎にデフォルトの format が異なるという間違いを避けられる。
    • (一瞬触っただけなので、よくわかってない)

結論

サーバーが指定外の format で request を受けた時、どう返したいかで実装が変わる

  • 406 Not acceptable を返す場合は respond_to (あるいは responders)
    • 複数 format を受け付ける URL なら、こちらのほうが自然
  • 404 Not Found を返す場合は namespace :prefix, format: 'format名'
    • 文字列 で指定すること。(symbol じゃダメ)
    • 例えば「json しか絶対受け付けない」なら、こういうのもありかも。

参考

emacs in terminal に移行しようと思ったけど、GUI の emacs に戻った話

イントロ

今まで GUIemacs を使ってたけど、terminal の中で emacs を開くようにして、暫く使ってみた。

要するに emacs -nw です。

けど色々難しいことがあって、元に戻ったという話。
苦労したところと、経緯を残します

環境

  • emacs 24.4
  • OSX 10.10.4
  • iterm2 2.1.1

なんで terminal の中で開きたいの?

カッコイイからです!
理由はなんとなく。

やったこと

特定のキーバインドemacs に渡らない

ターミナル上のEmacsで本来使えない"Ctrlキー+何か"を使う方法 - すぎゃーんメモ を見て解決。

これで C-:, C-;, C-y 等よく使うキーバインドを機能させることができた。

コピペができない

emacskill-ringclipboard の共有ができてなくて困った。
emacs でコピーした時に、pbcopy を呼ぶことで、無理やり同期させるような elisp を書いた。

Emacs と Mac のクリップボードを共有する と似たような感じ。

-nw にした時に同期できてないことに気づいたけど、特に GUI では意識してなかったポイント。

tmux prefix key と emacsキーバインドが競合する

私は tmux の prefix key として C-q を使っていたけど、emacsquoted-insert と競合する。
悩んだ末 quoted-insert の prefix を変更した。

emacsclient の起動

# emacs -nw
function emacs () {
  EMACS_CLIENT='/Applications/Emacs.app/Contents/MacOS/bin/emacsclient'
  EMACS='/Applications/Emacs.app/Contents/MacOS/Emacs'

  if ! pgrep Emacs; then
    $EMACS --daemon
  fi

  $EMACS_CLIENT -nw -q
}

function ekill () {
  pkill Emacs
}

この辺りはまだ泥臭くやってます。 emacs が立ち上がってなかったら、 --daemon 付きで起動し、その後 emacsclient を立ち上げる、というスクリプトを使って凌いでいる。

terminal の中で起動してよかった事

太字は特に良かった(悪かった)ところ

  • カッコ良くなった
  • emacsclient の起動がかなり早くなった
    • パッと開いて、パッと閉じる事が可能になって気持ちいい
  • いい感じに emacs で完結するようになった
    • 操作性うp
    • shell-command を以前より多用するようになった
      • だけどまだまだ shell 周りは課題が多い

よくなかったこと

  • 設定で無茶をしている
    • 特に「ターミナルから特定のキーバインドが渡らない」現象を解決するのに色々苦労した
    • この環境を再現するために、iterm2 の設定をいじらないといけなくて微妙な感じがする
  • コマンド + TAB で emacs に移れない
    • emacs で完結している限りは困らないけど、時々困る(かも)
  • color-theme が好みなやつがない
    • 自分の中でコレ!っていうのが無かった
    • GUI の時と、terminal の時で、色合いが結構違う
  • elscreen-server が地味に機能してない気がする件
    • 別の emacsclient を立ち上げても tab が共有されてない
    • 色々なところで emacsclient 立ち上げると頭使うから、こういう使い方をしたくなかった

結局どうなったか

GUI に戻りました...

自分は zshemacs を両方よく使っていたのと、zsh in emacs をイメージ通りに使えなかったのが大きかったです。
なので iterm2 (zsh 使う) と emacs スイッチを コマンド + TAB で行えないのが辛かった。

これがうまく機能しないと、効率が落ちてしまいます。

けどいつかは使いこなしたいこの環境、またいつか挑戦したいと思います。

delayed_job まとめ

イントロ

collectiveidea/delayed_job

なんとなく使ってきて、なんとなくわかってるつもりの delayed_job について、改めてまとめてみたもの。
休日暇だったのでソースコード読んでみたり、step 実行したりして、何処に何が書いてあるかをざっくりとまとめたものです。

環境

  • rails (4.2.1)
  • delayed_job (4.0.6)
  • delayed_job_active_record (4.0.3)
  • daemons (1.2.3)

導入

  • Gemfile
gem "delayed_job_active_record"
  • ActiveJob

ActiveJob のバックエンドとして、delayed_job を指定する

config.active_job.queue_adapter = :delayed_job
  • command
rails generate delayed_job:active_record
rake db:migrate

基本的な仕組み

Queue

  • queue は database に保存される
  • テーブル名は delayed_jobs
  • rails generate delayed_job:active_record で自動生成される

serialize

  • ruby の object を yaml 形式で dump し、DB に保存する
    • handler カラムに保存する
  • 取り出す時に dump された yaml を load し、ruby のオブジェクトに戻す

worker

  • worker が delayed_jobs テーブルを数秒おきにポーリングする
  • レコードがあったら、yaml をロードして ruby のオブジェクトに戻し job を実行する
    • job を実行し終わったらレコードは削除される
  • worker はポーリングを続ける、の繰り返し

worker の詳細

要の worker について詳しく見てみる。

woker の起動方法

  • rake jobs:work
  • bin/delayed_job start

の二通り

共通してやっていること

両者の違いは?

  • rake jobs:work は worker を起動するだけ
    • worker プロセスは一つだけ
  • bin/delayed_job は 起動オプションを受け取り、柔軟な worker プロセスの設定ができる
    • daemons gem がないと 起動不可能

なので

  • 開発環境で軽く使う場合は rake jobs:work(もしくは rake jobs:workoff)
  • 本番環境で運用する場合は、オプションを指定して bin/delayed_job

のようなイメージ

worker の option

Delayed::Worker クラスのアクセサとして定義する。 README に書いてあるとおり、rails なら config/initializers の下に適当に書いておけばいい。

  • default_queue_name
    • queue の名前。queue カラムに入る。
  • destroy_failed_jobs
    • true だと、失敗した job を DB から削除する(デフォルト true)
  • max_run_time
    • job の timeout を設定(デフォルトは4時間)
    • この時間より長いジョブは Timeout::Error を継承した DelayedJob::WorkerTimeout をraise する
  • raise_signal_exceptions

bin/delayed_job のオプション

https://github.com/collectiveidea/delayed_job/blob/v4.0.6/lib/delayed/command.rb#L26-L71

書いてあるとおり。重要そうなものをピックアップ

  • --number_of_workers
    • 起動する worker 数
    • ただし --identifier option との併用は不可(identifier 指定とプロセスの自動連番が衝突するから)
    • stop と start の worker 数が等しくないと、bin/delayed_job stop で stop できない
  • --pid-dir
    • pid ファイルのディレクトリを指定
  • --log-dir
    • log ファイルのディレクトリを指定
  • --identifier
    • プロセス名を指定できる。指定すると delayed_job.#{@options[:identifier]} というプロセス名になる
  • --prefix
    • プロセス名を指定できる。指定すると File.join(options[:prefix], process_name) というプロセス名になる

bin/delayed_job stop で何が起こるか

起動中の delayed_job プロセスには TERM が送られる。(daemons gem の仕事) https://github.com/thuehlinger/daemons/blob/v1.2.3/lib/daemons/application.rb#L374

発生する例外

起動中のプロセスが TERM, INT を受け取った時、job の挙動はDelayed::Worker.raise_signal_exceptions の値で変わる

  • false
    • TERM を受け取った場合
      • 例外は出ない
    • INT を受け取った場合
      • 例外は出ない
  • :term
    • TERM を受け取った場合
      • SignalException を raise
    • INT を受け取った場合
      • 例外は出ない
  • true
    • TERM を受け取った場合
      • SignalException を raise
    • INT を受け取った場合
      • SignalException を raise

コードを見たほうが腑に落ちるかも https://github.com/collectiveidea/delayed_job/blob/v4.0.6/lib/delayed/worker.rb#L132-L142

例外が出た後に何が起こるか

callback について

  • error のフックとしては :before, :after が空席
  • 書き方は以下の様な感じ

    Delayed::Worker.lifecycle.before(:error) do |worker, job| # puts worker # puts job end

  • ブロック引数に run_callbacks(:error, self, job) の第二引数以降、即ち Delayed::Worker, Delayed::Job インスタンスを受け取れる

  • ただし、callback を定義する場所に注意
    • 実質 config/initializer しかない?
    • アプリ側で動的に callback を定義しても、既に起動中の worker には伝わらない
    • ワーカーに依存しない処理が書ける場所

Capistrano

https://github.com/collectiveidea/delayed_job/blob/v4.0.6/lib/delayed/recipes.rb

  • 各 host で bin/delayed_job コマンド + オプションを実行する

だけ。

つまり bin/delayed_job の章で述べたとおり、capistrano で deploy した時、実行中の job は中断し DB に戻される。 そして新しいコードを load した状態で、再度 job が実行される、という流れになる。

細かすぎて伝わらない delayed_job のネタ

実は Delayed::Job というクラスはない

正確には collectiveidea/delayed_job に定義されていない、ということ。

実体は collectiveidea/delayed_job_active_record になっていて、このクラスが include Delayed::Backend::Base している

>> Delayed::Job
=> Delayed::Backend::ActiveRecord::Job

なので Delayed::Job インスタンスメソッドを調べる時は delayed_job_active_recordDelayed::Backend::Base を見ると良い。

find_available メソッドの謎

Delayed::Backend::Base.reserve に定義されている find_available メソッド、よく調べてみたけど、実は何処にも定義されていない。なので NoMethodError が発生するはず。 https://github.com/collectiveidea/delayed_job/blob/v4.0.6/lib/delayed/backend/base.rb#L43

けど上記で述べたとおり、実体は collectiveidea/delayed_job_active_record のオブジェクトになっていて、https://github.com/collectiveidea/delayed_job_active_record/blob/v4.0.6/lib/delayed/backend/active_record.rb#L43reserve を上書きしているから、実質問題は起きていない。

delayed_job バックエンドによっては find_available を上書きしているものもあるらしい

まとめ

  • delayed_job の仕組みについて調べました
    • DB に queue を溜めて、worker がそれを取り出して実行する
  • worker の起動方法は2種類ある
    • rakebin/delayed_job の2つ
  • worker の設定は Delayed::Worker クラスのアクセサとして定義
    • worker の設定は沢山あるんやで
  • worker を stop させると、worker が既に動いていても DB に一旦戻されて再実行を行う仕組みになっている
    • デフォルト

authlogic で、ユーザーの password を編集した直後に強制ログアウトされる件

イントロ

authlogic でハマったネタ第二弾。
まとめると以下の様な感じ。

  1. authlogic でログインユーザーを作成
    • よくある 名前と password で認証するユーザー
  2. ログインユーザーを編集する画面を作る
    • password を編集可能にする
  3. ログインユーザーの password を更新すると、何故か強制的にログアウト状態になる
    • ログインを要求する url に強制 redirect される

解決法

class User < ActiveRecord::Base
  acts_as_authentic do |c|
    c.session_class = UserSession
  end
end

この解決方法はあくまで一例。

原因

  1. authlogic のログイン状態は、DB に保存されている persistence_tokensession[:user_credentials] の一致により判定される。
    • user_credentials という session の key はログイン用のモデルによって変更の可能性あり
  2. authlogic で password を更新すると、強制的に persistence_token も再生成される
    • この時 session の値も再生成された文字列に更新しなければならない
  3. 設定が甘くて、password を更新した時に session の値が更新されなかったため、ログイン状態が維持されなかった
    • authlogic がログイン画面にリダイレクト

のような感じ。

肝心の session の値も再生性された文字列で上書きしなければならない 設定は以下のメソッドで行っている。
恐らくこの値は true でなければいけないが、false になっているとこんなおかしな状況になる。

https://github.com/binarylogic/authlogic/blob/8e2acfe303ea68013ad5afa5b85c5569837a290d/lib/authlogic/acts_as_authentic/session_maintenance.rb#L87-L89

def update_sessions?
  !skip_session_maintenance && session_class && session_class.activated? && self.class.maintain_sessions == true && !session_ids.blank? && persistence_token_changed?
end

今回私が遭遇したケースでは session_class が何故か nil を返すようになっていたので、session が update されず、ログアウト状態になってしまっていた。

session_class について

session_class とは

  • Authlogic::Session::Base を継承した session クラスを返すメソッド
  • 普通は自動で判定される
    • Useracts_as_authentic なら UserSession という風に

ので、普段は意識しなくていいはず。

例えば Admin モデルを作って、Admin 専用の管理画面を作ろう、となった時に Session クラスを AdministratorSession としてしまうと、session_class を指定する必要がある。

設定を忘れるとこのような目にある。

まとめ

Authlogic::ActsAsAuthentic::SessionMaintenance::Methods#update_session? メソッドを調べればよろし

authlogic で blank の password で update 出来てしまう問題

イントロ

情報が古いけど

ruby on rails - changing password with authlogic - validation not catching blank inputs - Stack Overflow
と同じ現象。

改めてまとめると、以下の様な状態。

  1. authlogic でログインユーザーを作成
    • ログインユーザーは password を form に入力する
  2. ログインユーザーを編集する画面を作る
    • 新規作成ではないので注意
    • password を編集可能にする
  3. ログインユーザーを編集するとき、パスワードを空にして保存できてしまう

解決法

class User < ActiveRecord::Base
  acts_as_authentic do |c|
    c.ignore_password_blank = false
  end
end

とする。

よく見ると

https://github.com/binarylogic/authlogic/blob/23751e3eaba4c2b09751505079d7264fd1cc8b72/lib/authlogic/acts_as_authentic/password.rb#L44-L54

のコメントに書いてある。

前提条件も含めて説明すると...

  • 編集画面において、password を最初から入力済みにすることはできない
    • DB に保存されている password を復号することはできないため(暗号化方式によるが)
    • つまり編集画面を開いた直後はどうしても空になってしまう
  • 状況として「 password は変えたくないけど、ページを遷移してしまう」ケースが存在するだろう
    • どういうケースかは明記されてないけど、例えば間違って「更新」ボタンを押してしまったりするとか?
    • そうすると、空のパスワードが保存されてしまい、セキュリティ的に甘くなる
      • そういうケースは避けたい
      • オプションで変更できる

ということらしい。(多分)