MySQLユーザーがPostgreSQLを触ってみたメモ

最近なぜか MySQL を使う Ruby アプリを PostgreSQL に対応する羽目になっているのですが、今までほとんど MySQL 以外の RDBMS を触ってなかったので、色々ハマったりしたのでメモっときます。

なお PostgreSQL 歴が浅いので間違ってること書いてるかもしれません。

API

プログラムから MySQL にアクセスするには Ruby/MySQL を使っていたのですが、PostgreSQL 用の API を新たに覚えるのは面倒だったので、Sequel を使って書き直しました。

mysql.query("select col1, col2 from table where col3='xxx'")
↓
db[:table].where(col3: 'xxx').select(:col1, :col2)

…みたいな感じです。

今までプログラム中に突然 SQL が現れていて読みにくかったのが、Ruby プログラムとして読みやすくなるという効果もありました。

Sequel については前に記事を書いたので興味があれば見てください。

ユーザーとデータベース

MySQL はデータベースを指定せずにサーバーに接続することができます。その場合データベース未選択状態になります。接続後に use を使用してデータベースを選択してからクエリを発行します。また、カレントのデータベースとは異なるデータベースのテーブルを指定することもできます。

PostgreSQL の場合はデータベースを指定しないで接続することはできません。psql コマンドはデータベースを省略すると、ユーザー名と同じ名前のデータベースに接続しようとします。また、接続後はデータベースを変更することはできません。

Ubuntu の場合は postgres という名前のデータベースが最初から用意されています。 誰でも使用できるので MySQL の test データベースのようなものでしょうか。

ユーザー名と認証

MySQL では UNIX ドメインソケット経由での接続であってもパスワードは必要です。パスワードを設定しないこともできますが、その場合は誰でもそのユーザーでログインできてしまいます。また OS のユーザー名と MySQL 上のユーザー名はほとんど関係ありません。mysql コマンドでユーザー名省略時に OS のユーザー名が使用される程度です。

PostgreSQL の場合は UNIX ドメインソケット経由の場合は、OS のユーザー名と PostgreSQL 上のユーザー名が一致していればパスワードを聞かれません。また OS のユーザー名と異なるユーザー名を指定しても接続できません。ネットワーク経由の場合はユーザー名とパスワードが必要です。

Ubuntu の場合はあらかじめ postgres というユーザーがスーパーユーザーとして用意されています。MySQL の root と同じようなもんだと思います。

認証まわりの設定は pg_hba.conf ファイルで行います。どのクライアントからの接続で、どの認証方式を使用するかを指定することができます。

UNIX ドメインソケットの場合にパスワードを聞かれないのは、pg_hba.conf ファイル中に次の行があるためです。

# TYPE  DATABASE        USER            ADDRESS                 METHOD
# "local" is for Unix domain socket connections only
local   all             all                                     peer

ユーザー作成

PostgreSQL に接続するユーザーを作るには、PostgreSQL のスーパーユーザーで createuser コマンドを実行します。

Ubuntu の場合は postgres ユーザーがスーパーユーザーです。

% sudo -u postgres -i
[sudo] password for tommy: 
postgres$ createuser -P hoge
Enter password for new role: 
Enter it again: 

この例では hoge ユーザーをパスワードつきで作成しています。 localhost からしか使用しない場合はパスワードをつけなくてもいいと思います。

データベース作成

PostgreSQL のスーパーユーザーで createdb コマンドを実行します。

postgres$ createdb hoge

接続

クライアントからの接続は MySQL に比べると PostgreSQL は重いです。

MySQL でコネクションプールを使わずに接続切断を繰り返してもなんとかなっていた場合でも、PostgreSQL ではコネクションプールを使わないといけなくなるかもしれません。

MySQL は1プロセスで動作し接続毎にスレッドを生成するのに対し、PostgreSQL は接続毎にプロセスを生成するためだと思います。

調べてませんが、もしかすると認証プロトコル自体の処理も関係あるのかもしれません。

自動変換等

MySQL は型が一致しなくてもテキトーに変換して処理してくれるのですが(かなり余計なお世話)、PostgreSQL は厳密に型をチェックするので、MySQL でエラーにならなかったクエリがエラーになることがあります。

以下は MySQL ではエラーになりませんが、PostgreSQL でエラーになる例です。

  • 数値カラムへの文字列の登録
tommy=> insert into t (i) values ('hoge');
ERROR:  invalid input syntax for integer: "hoge"
LINE 1: insert into t (i) values ('hoge');
                                  ^
  • 数値カラムと文字列の比較
tommy=> select * from t where i='hoge';
ERROR:  invalid input syntax for integer: "hoge"
LINE 1: select * from t where i='hoge';
                                ^
  • 文字列カラムと数値の比較
tommy=> select * from t where s=123;
ERROR:  operator does not exist: character = integer
LINE 1: select * from t where s=123;
                               ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
  • INSERT 時にデフォルト値を持たない NOT NULL カラムを省略
tommy=> insert into t (col1) values (123);
ERROR:  null value in column "col2" violates not-null constraint
DETAIL:  Failing row contains (123, null).
  • SELECT に指定していないカラムを GROUP BY に指定
tommy=> select col1 from t group by col2;
ERROR:  column "t.col1" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: select col1 from t group by col2;
               ^
  • CHAR カラムにサイズ超過文字列を登録
tommy=> insert into t (col1) values ('0123456789a');
ERROR:  value too long for type character(10)
  • DATE カラムに 0000-00-00 を登録
tommy=> insert into t (d) values ('0000-00-00');
ERROR:  date/time field value out of range: "0000-00-00"
LINE 1: insert into t (d) values ('0000-00-00');
                                  ^
  • DATE カラムに不正な日付を登録
tommy=> insert into t (d) values ('2014-10-32');
ERROR:  date/time field value out of range: "2014-10-32"
LINE 1: insert into t (d) values ('2014-10-32');
                                  ^
  • 0 除算
tommy=> select 1/0;
ERROR:  division by zero

MySQL でも sql_mode を設定することで、これらのうちのいくつかをエラーにすることができます。

mysql> set sql_mode='STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY';

なお、ERROR_FOR_DIVISION_BY_ZERO を指定することで、UPDATE,INSERT 時のゼロ除算はエラーになりますが、SELECT の時はエラーではなくワーニングになります。

UNSIGNED

MySQL の整数型には UNSIGNED を指定することができますが、PostgreSQL には UNSIGNED 型はありません。

大文字小文字

MySQL の文字列型はデフォルトでは大文字小文字を区別しませんが、PostgreSQL は大文字小文字を区別します。

PostgreSQL で大文字小文字を区別しないようにするには、citext 拡張をデータベースに導入して、文字列型カラム作成時に char, varchar ではなく citext を使うのが良いようです。

tommy=> create table t (s varchar);
CREATE TABLE
tommy=> insert into t (s) values ('abc');
INSERT 0 1
tommy=> select * from t where s='ABC';
 s 
---
(0 rows)

citext 拡張を導入するにはスーパーユーザーでないとできません。

tommy=# create extension citext;
CREATE EXTENSION
tommy=> create table t (s citext);
CREATE TABLE
tommy=> insert into t (s) values ('abc');
INSERT 0 1
tommy=> select * from t where s='ABC';
  s  
-----
 abc
(1 row)

LIKE も大文字小文字を区別します。大文字小文字を区別したくない場合は ILIKE を使います。

tommy=> select 'abc' like 'ABC';
 ?column? 
----------
 f
(1 row)

tommy=> select 'abc' ilike 'ABC';
 ?column? 
----------
 t
(1 row)

スキーマ

MySQL には無いものですが、PostgreSQL にはデータベースとテーブルの間にスキーマというものがあります。 データベース内でテーブルの名前空間をわけられるようです。 特に指定しない場合はデフォルトのスキーマが使われるので意識しなくても問題ないです。

MySQL のデータベースは PostgreSQL のデータベースとスキーマの両方が混ざったものなのかもしれません。

トランザクション

PostgreSQL はトランザクション中で何かエラーが発生した場合は、以降のクエリはロールバックするまですべてエラーになります。

この状態でコミットするとエラーにはなりませんが、実際にはロールバックされます。これはちょっと罠っぽいです。

tommy=> begin;
BEGIN
tommy=> insert into t (i) values (123);
INSERT 0 1
tommy=> hoge;
ERROR:  syntax error at or near "hoge"
LINE 1: hoge;
        ^
tommy=> select * from t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block
tommy=> commit;
ROLLBACK
tommy=> select * from t;
 i 
---
(0 rows)

MySQL と違い、PostgreSQL はトランザクション中で CREATE TABLE や DROP TABLE しても、ロールバックすると無かったことになります。これは結構嬉しいです。MySQL はトランザクション中で CREATE TABLE, DROP TABLE すると、その時点で勝手にコミットされてしまうという罠があるので…。

その他の構文

MySQL は文字列リテラルの表記は「'」でも「"」でも良いですが、PostgreSQL では「'」だけです。「"」はテーブルやカラムの識別子リテラルを表します(MySQL での「`」に相当)。

MySQL の「||」は論理和を表しますが、PostgreSQL では「||」は文字列結合です。

これらも MySQL 側で sql_mode を指定することで PostgreSQL に合わせることができますが、Sequel を使ってれば構文の差異はある程度吸収してくれるので、あまり気にしませんでした。

おわりに

はじめは「MySQLユーザーのためのPostgreSQLガイド」というタイトルにしようと思ったのですが、そんな大層なことは書けなかったので、「MySQLユーザーがPostgreSQLを触ってみたメモ」にしました。

MySQL と PostgreSQL は色々違いがありますが、構文まわりは MySQL の方が特殊だと思いました。勝手に余計な変換はしない方がみんな幸せになれると思います。

MySQL から PostgreSQL に移行するには、まず sql_mode を設定して、その状態で MySQL でエラーにならないようにしてから、PostgreSQL に移行するのがいいと思います。

自分の場合は、数値と文字列の比較とか、数値カラムに数字文字列入れようとしてたりとか、SELECT で指定してないカラムを GROUP BY に指定してたりとか色々ありました。

あと、最近 MySQL も sql_mode のデフォルト値を厳し目にするようになってるので、PostgreSQL とは関係なく、可能であれば厳し目に設定しておいた方が何かといいと思います。

RSpec をやめて Test::Unit に戻る

最近の RSpec は、それまで

obj.stub(hoge: value)

と書けたものが、

allow(obj).to receive(:hoge).and_return value

と書かないといけなくなったりとか、正気の沙汰とは思えないような変更をしたりするので、何年かぶりに Test::Unit を使ってみようとリハビリ中です。

RSpec は、テストケースを入れ子にできたり、テストケースや example がクラスやメソッドではなく、文字列で自由に書くことができたりしたのが良かったのですが、最近の Test::Unit ではそれもできるようになっています。

[ruby-list:48926] [ANN] test-unit 2.5.2

このリリースはとみたさんに使ってもらえるように改良したリリー スです。新しく追加した--locationはRSpecの--line_numberと似た 機能です。sub_test_caseはshoulda-contextとRSpecのcontextと似 た機能です。これらの機能があればtest-unit 2に乗り換えてくれ ると聞いたので実装しました。

2年もたってやっと使い始めました。すいません…。

準備

% gem install test-unit

テスト

RSpec で次のように書いてたテストは、

describe Array do
  context '空の場合' do
    before do
      @array = []
    end
    it 'size が 0' do
      expect(@array.size).to eq 0
    end
  end
  context '要素が1つの場合' do
    before do
      @array = ['hoge']
    end
    it 'size が 1' do
      expect(@array.size).to eq 1
    end
  end
end

Test::Unit だと次のように書けます。

require 'test/unit'

class TestArray < Test::Unit::TestCase
  sub_test_case '空の場合' do
    setup do
      @array = []
    end
    test 'size が 0' do
      assert_equal 0, @array.size
    end
  end
  sub_test_case '要素が1つの場合' do
    setup do
      @array = ['hoge']
    end
    test 'size が 1' do
      assert_equal 1, @array.size
    end
  end
end

一番外側は Test::Unit::TestCase を継承してクラスを作成する必要がありますが、そのクラスの中は sub_test_case を使って任意の文字列でテストケースを記述できます。

RSpec の describe, context と同様に sub_test_case は入れ子にもできます。 ただし、test に指定する文字列がまったく同じだと後で定義した方は無視されてしまうようなので注意しましょう。

デスクトップ通知

gem で test-unit-notify をインストールして、スクリプトの先頭で

require 'test/unit/notify'

と書いておくと、デスクトップ通知で結果を知らせてくれて便利です。

f:id:tmtms:20141012233534p:plain

stub

stub は、test-unit-rr をインストールして、スクリプトの先頭で

require 'test/unit/rr'

としておき、次のように記述することができます。

stub(obj).hoge{value}   # obj の hoge メソッドが value を返す

RSpec よりもシンプルに書けて良いです。

Rubyのシグナルハンドラ

toRuby & guRuby 出張版 でシグナルについてやってたので、関連して書いてみます。

どのような時にシグナルハンドラを定義するのか

どのような時にシグナルハンドラを定義するのかという話がありました。

UNIXのデーモンプログラムは、何が由来なのかわかりませんが、SIGHUP で設定ファイルの再読み込みを行うのが慣習になっています。 SIGHUP はデフォルト動作ではプログラムを終了させてしまうだけなので、SIGHUP で特別な処理を行いたいプログラムはシグナルハンドラを定義しています。

本来 SIGHUP は端末が終了した時に端末上で動いていたプログラムに対してOSが発行するためのものです。

たとえば、端末エミュレータを開いて、

% sleep 9999

と実行してる状態で端末エミュレータを閉じると sleep プロセスに SIGHUP が送られます。別の端末から strace コマンドでそのプロセスを見てると SIGHUP で終了していることがわかります。

% sudo strace -p 26416
Process 26416 attached
restart_syscall(<... resuming interrupted call ...>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=26390, si_uid=1000} ---
+++ killed by SIGHUP +++

コマンドを「&」つきでバックグランドで起動していても同じです。端末が終了してもプログラムを終了させないようにするには、SIGHUP を無視するか、端末からプロセスを切り離す必要があります。ちゃんと作られているデーモンプログラムは端末から切り離すように作ってあるはずです。Ruby だとプログラム中で

Process.daemon

とすれば、それ以降は端末から切り離されて動作するようになります。

SIGTERM は kill コマンドのデフォルトシグナルなので、プロセスを殺す時に一番良く使われます。プログラムによっては終了時に特別な処理を行わないと整合性が壊れてしまうものがあります。その場合は SIGTERM のシグナルハンドラを定義して、処理を行ってから終了するようにしておく必要があります。

SIGWINCH は、端末の大きさが変更された時にプロセスにそれを知らせるためのシグナルです。端末上で次のコマンドを実行してから、端末のサイズを変更すると「WINCH」と表示されます。

% ruby -e 'trap(:WINCH){puts "WINCH"}; sleep'

Vim や less 等の端末全体を使用するアプリは SIGWINCH が通知された時に画面の再描画を行うようになっています。

シグナルハンドラ内での処理

toRuby & guRuby の場でも言いましたが、シグナルハンドラ中ではあまり複雑なことはやってはいけないです。というよりもできることはかなり限られてると思います。

Ruby の場合、シグナルハンドラ中でどのような操作ができるのかについては、たぶんドキュメントはないんじゃないかと思うのですが、少なくとも Ruby 1.9.3 では IO 処理、たとえば上の例で使った puts も安全ではありません。シグナルの例を示すのに簡単なのでよく使われがちなのですが…。

たとえば Ruby 1.9.3 で次のコマンドを実行して端末サイズを変更するとプログラムが終了します。

% ruby -e 'trap(:WINCH){puts "WINCH"}; loop{puts "main"}'
...
main
main
main
main
-e:1:in `write': deadlock; recursive locking (ThreadError)
    from -e:1:in `puts'
    from -e:1:in `puts'
    from -e:1:in `block in <main>'
    from -e:1:in `call'
...

Ruby 2.0.0 以降では発生しないので、IO はシグナルに対して安全になったのかもしれません。

上の例では Ruby がデッドロックを検出して落ちてくれるのでいいのですが、自分で作ったライブラリを使用する場合はデータが壊れないように対応する必要があります。

Mutex を使って排他制御する方法はうまく動きません。

次のスクリプトは SIGINT を受けると、排他領域で「Interrupt!」と表示します。

mutex = Mutex.new

trap :INT do
  mutex.synchronize do
    puts 'Interrupt!'
    sleep 1
  end
end

sleep

SIGINT は端末上で Ctrl-C を入力するとフォアグランドプロセスに対して通知されるシグナルです。

このスクリプトを動かして Ctrl-C を押すとエラーになります。

% ruby test.rb
^Ctest.rb:4:in `synchronize': can't be called from trap context (ThreadError)
        from test.rb:4:in `block in <main>'
        from test.rb:10:in `call'
        from test.rb:10:in `sleep'
        from test.rb:10:in `<main>'

Mutex#synchronize はシグナルハンドラ中では許されない処理のようです。

なお、Ruby 1.9.3 では Ctrl-C を2回押すと別のエラーになります。

% ruby test.rb
^CInterrupt!
^C<internal:prelude>:8:in `lock': deadlock; recursive locking (ThreadError)
        from <internal:prelude>:8:in `synchronize'
        from test.rb:4:in `block in <main>'
        from test.rb:6:in `call'
        from test.rb:6:in `sleep'
        from test.rb:6:in `block (2 levels) in <main>'
        from <internal:prelude>:10:in `synchronize'
        from test.rb:4:in `block in <main>'
        from test.rb:10:in `call'
        from test.rb:10:in `sleep'
        from test.rb:10:in `<main>'

1.9.3 では Mutex#synchronize は禁止されていないようですが、Mutex#synchronize ブロック実行中にまたシグナルが発生して、同じ Mutex で排他制御しようとしてデッドロックになります。

次のように Mutex#synchronize ではなく、Mutex#try_lock を使用すると動くようです。(Ctrl-C は効かないので、プログラムを停止するには Ctrl-Z して kill %1 するか、他の端末から kill してください)

mutex = Mutex.new

trap :INT do
  if mutex.try_lock
    puts 'Interrupt!'
    sleep 1
    mutex.unlock
  end
end

sleep

これを試してて気がついたのですが、Ruby 1.9.3 と 2.x ではシグナルハンドラの実行のされかたが少し異なるようです。

1.9.3 ではシグナル発生の度にハンドラが実行されますが(上のスクリプトだと if の条件を満たさないので無視される)、2.x ではハンドラ実行中に発生したシグナルは溜められていて、ハンドラブロックを抜けると通知されるようです。

上のようにハンドラ内でも Mutex#try_lock は使えるようですが、これは排他が獲得できなければシグナルを無視してしまうことになるので、あまりよくありません。

結局シグナルハンドラ内で何か複雑な処理を行おうとすることが間違いなのだと思います。

シグナルハンドラ内ではシグナルが発生したことを表すフラグを立てるくらいにしておき、プログラムの都合のいいタイミングでそのフラグを見てシグナルに応じた処理を行うのがいいと思います。

mutex = Mutex.new
sigint = false

Thread.new do
  loop do
    if sigint
      sigint = false
      mutex.synchronize do
        puts 'Interrupt!'
        sleep 1
      end
    end
    sleep 0.1
  end
end

trap :INT do
  sigint = true
end

sleep

上のプログラムは、シグナルハンドラ内では sigint 変数の値を設定しているだけで、実際の処理はスレッドの中で行っています。

この例のままだとシグナル処理中にシグナルが連続発生しても1回分しか処理されないとか、sleep 0.1 してるものの、フラグチェックがビジーループっぽいところがいまいちかもしれません。

シグナルハンドラ中で Array#push して、シグナル処理時に Array#pop すれば回数の問題はクリアできるような気がします。Array は Ruby 標準で C で書かれたクラスなのでシグナルハンドラ中で使ってもアトミックに処理できるんじゃないかと思います。

ビジーループの方は Thread.stopThread#run を使うのがいいかもしれません。これもシグナルハンドラ中で使っていいような気がします。なんとなく。

何かもっとうまい方法があれば教えてください。

RubyHiroba 2014 に行きました

去年に引き続き RubyHiroba 2014 に行ってきました。

お目当ては toRuby & guRuby 出張版 です。

渋谷の一角の北関東。

咳さん(@m_seki)とか田中さん(@tanaka_akr)とか角さん(@kdmsnr)とか角谷さん(@kakutani)とか豪華メンバーが参加してました。

guRuby - メタプログラミングRuby

メタプログラミングRuby

メタプログラミングRuby

method_missing の章の朗読&サンプルプログラムの写経でした。

第二版の翻訳予定もあるようです。

toRuby - なるほどUnixプロセス ― Rubyで学ぶUnixの基礎

シグナルの章の朗読&サンプルプログラムの写経でした。

偶然にも自分は最近 Ruby のシグナル周りでハマってるので、タイムリーな話題でした。

その場でも話したのですが、シグナルは非同期割り込みなので、シグナルハンドラはプログラムがどのような状態であっても動けるようになってないといけないので、ちゃんと作ろうとしたら結構大変です。

「なるほどUnixプロセス」ではハンドラ中で気軽に puts とかしてますけど、少なくとも Ruby 1.9.3 ではそれも安全ではなかったと思います。最近の Ruby では大丈夫かもしれません。

Rubyのシグナルハンドラ という記事を書いたので興味があれば読んでみてください。

trap(:KILL) がエラーにならないのはおかしいんじゃないかという話題があって、なかださん(@n0kada)が召喚されたりしてました。

なるほどUnixプロセスの続編の Working With TCP Sockets の翻訳もそのうち出るとこのことでした。楽しみです。

Working With TCP Sockets

Working With TCP Sockets

RubyでUnixシステムコールを理解するというと、一年ほど前に Rubyでシェルもどきを作る という記事を書いたことがありました。参考になる人もいるかもしれません。

しかし、北関東の人たちはエディタ使わずに ruby の標準入力からプログラムを入力してたんですけど、入力ミスしても修正できないので厳しいと思うのですけど、cat があればエディタなんて要らないみたいな猛者の集まりなのかもしれません。北関東こわい。

その他

サンライズ出雲で RubyWorld Conference に行こう仲間の @yancya さんが、生活発表会と LT の両方で発表してました。すごい。 Refinements はハマりどころがたくさんありそうですね…。

あとは、すとうさん(@ktou)と Rabbit の話をしたり、@yancya さんが Rabbit で PDF を作るのに四苦八苦してるのを眺めたりしながら、ずっと自作ライブラリのデバッグしてました。

受付の無限コーヒーは嬉しかったです。

RubyHiroba もいいんですけど、来年は RubyKaigi にも参加したいです。

メールアドレスの正規表現

たまにメールアドレスの形式を正規表現で表すのは不可能とかというのを目にするのですが、そんなことはありません。入れ子がなければたいていの文字列の形式は正規表現で表すことができます。

ということで、RFC5321, 5322 からメールアドレスの正規表現を書いてみました。

/\A([0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+(\.[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)*|\"([\x20\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*\")@[0-9a-z]([0-9a-z-]*[0-9a-z])?(\.[0-9a-z]([0-9a-z-]*[0-9a-z])?)*\z/i

ちょっと長いですけど、最近の Ruby だと (?<hoge>)\g<hoge> を使うことで、同じ正規表現の繰り返しを簡単に書くことができるので、それを使って書きなおしてみます。

/\A((?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)(\.\g<atom>)*|\"([\x20\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*\")@(?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)(\.\g<sub_domain>)*\z/i

\g を使えば入れ子も表現することができるみたいですが、やったことはありません。

わかりにくいので改行&インデントしてコメントを入れて見ました。正規表現リテラルに x オプションを入れるとこのような表記ができます。

\g や x オプションが Ruby 以外で使えるかどうかは知りません。

/\A
  # local-part
  (  # dot-atom
     (?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
     (\.\g<atom>)*
  |  # quoted-string
    \"([\x20\x21\x23-\x5b\x5d-\x7e]
       |\\[\x20-\x7e])*\"
  )@
  # domain
  (?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)
  (\.\g<sub_domain>)*
\z/ix

これで少しはわかりやすくなったとは思いますが、さすがに暗記するのは難しいです。

ということで、この正規表現を印刷したTシャツやマグカップなどを作ってみました。これがあればいつでもメールアドレス形式のチェックができますね!

[追記]

「入れ子がなければたいていの文字列の形式は正規表現で表すことができます」と書いたんですけど、文字数とかの制約も表せませんでした。

RFC5321 では local-part の最大長は 64, domain の最大長は 255, メールアドレス全体の最大長は 256 という制約がありますが、これは正規表現で表すことはできません。

…と思ったのですが、Twitter で教えてもらって、lookahead (?=...) を使えば文字数制限も指定することができました。

sub-domain が最大63文字というのにも対応してます。

/\A
  # 全体で256文字以下
  (?=.{,256}\z)
  # local-partは64文字以下でdomainは255文字以下
  (?=.{,64}@.{,255}\z)
  # local-part
  (  # dot-atom
     (?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
     (\.\g<atom>)*
  |  # quoted-string
    \"([\x09\x20\x21\x23-\x5b\x5d-\x7e]
       |\\[\x09\x20-\x7e])*\"
  )@
  # domain
  (?<sub_domain>[0-9a-z]([0-9a-z-]{,61}[0-9a-z])?)
  (\.\g<sub_domain>)*
\z/ix

しかし、こんなネタで何故かブクマが200超えてるんですけど、世の中何がウケるかわかりませんな。

反応している人はだいたい、

  1. ヘッダの From や To フィールドをパースしたい人
  2. メールアドレス登録時にバリデーションしたい人
  3. 日本語メールアドレスは?
  4. Tシャツいかす!

に分類できるみたいです。

ちなみに私はTシャツのネタ以上のことは考えてなかったので 4 です。

コメント(CFWS)とか表示名(display-name)が気になる人はきっと 1 なんでしょう。

ふつうにメールアドレスと言った時には、コメントや表示名は含まないと思うので、今回はそれは考慮しませんでした。

ヘッダの From や To フィールドをパースしようと思ったら流石に正規表現じゃ無理だと思います。コメントは入れ子だし、グループ(group)もありますし。

ケータイにありがちなピリオドの連続とかの変なアドレスが気になる人は 2 ですね。

私も以前は「ピリオド連続ローカルパートとか死ねばいいのに」と思ってたのですが、最近は考えが変わりました。

ローカルパートに特殊な記号とかピリオドの連続を含めたい場合は「"」で括れば可能です。でもそれはプロトコルの都合なので、人間様がそれに合わせてやる必要はありません。アプリが頑張ればいいんです。

たとえば、「@」の左側をローカルパートとみなして、ローカルパートに「"」で括らないと使えないような文字の並びがあれば、アプリ側で quoted-string 形式に変換してやればいいだけの話です。

日本語メールアドレスは RFC 6530〜6533 あたりを見ればいいんでしょうけど、まだちゃんと読んでません。まだ当分は普及しないんじゃないかと思ってます。

Tシャツとその他のグッズは suzuri で作りました。suzuri はお気楽でいいですね。suzuri についてはまた今度書きます。

Dockerイメージを作る (NSEG#54)

NSEGの第54回勉強会で「Dockerイメージを作る」という発表をしました。

(SlideShare にもアップロードしました)

スライドにも書きましたが、前回以降にやったことの自分用のまとめみたいな感じで、世間的に目新しい話題は特に無いです。

今回も会場はケイケンさんの会議室でした。前の日まで参加申し込み5人だったのですが、当日の参加者は10人でした。

他の人の発表

@hiro345さんの Frontend framework and Template

先週末の HTML5時代のフロントエンド開発入門でも思ったんですけど、最近のフロントまわりの技術にはさっぱりついていけてなくて良くないです。

@earth2001yさんの「昨日、ぺちゃくちゃで NSEG を喋りました」

定期的に開催されている ぺちゃくちゃないとNAGANOの話。 ぺちゃくちゃないとNAGANOは、スライドは20枚固定で 20秒/枚 で勝手にスライドが切り替わっていて、それに合わせてしゃべるという珍しい形式のプレゼンです。発表難しそう。

@office_krtecさんの「失敗して学ぶ炎上プロジェクトからの生還(案)」

SIerの闇の話。失敗し続けてもオフショアに発注し続ける会社と、炎上案件の火消しをし続ける office_krtec さんの話でした。

オフショアを使わないのが一番いいと誰もがわかりながらも、政治的な理由で使わざるを得ないというのが涙を誘います。もう、お金払うからオフショアの人には何もしないでもらって、こっち側でちゃんと作った方が結果的に安くつくんじゃないかとも思いました。

@_iwateさんの「日本一雲に近い Azure User Group の話」

9/27 に JAZUG4周年記念! どこよりも雲に近い長野でするクラウドの話 - Azureしなの | Doorkeeper というイベントをやるという話です。 あと隔週で水曜日にGEEKLAB. NAGANOで「Azure もくもく会」が開催されているとのこと。興味のある人は行ってみるのがいいと思います。

@nabetaroさんの「deb パッケージを再構築」

既存の deb パッケージを流用して、新しい deb パッケージを作る方法の解説でした。pdebuild 便利そうです。

次回

次回の日程は未定です。9月の中頃の土日だと思われます。

懇親会

C-one の地下の「だんまや水産」に行きました。

相手がいないのに ESTABLISHED になってる TCP ポート

最近 ParallelServer というライブラリを作ったのですが、その最中に奇妙な状態になってる TCP ポートを見つけたので、メモっておきます。

Ruby では TCP サーバーは次のような感じで作ることができます。お手軽ですね。

require 'socket'

Socket.tcp_server_loop(12345) do |socket, client_addr|
  socket.puts "Your IP address: #{client_addr.ip_address}"
  name = socket.gets
  socket.puts "Hello, #{name}"
  socket.close
end

これは 12345 ポートでクライアントからの接続を待ち、接続されたらクライアントのIPアドレスとクライアントからの入力をクライアントに送信して切断するだけの簡単なプログラムです。

~% nc -v localhost 12345
Connection to localhost 12345 port [tcp/*] succeeded!
Your IP address: 127.0.0.1
hogehoge
Hello, hogehoge
~% 

サーバープログラム起動直後の 12345 ポートの状態は次のようになります。

~% netstat -an | grep 12345
tcp        0      0 0.0.0.0:12345           0.0.0.0:*               LISTEN     
tcp6       0      0 :::12345                :::*                    LISTEN     

IPv4 と IPv6 の両方で LISTEN 状態です。

クライアントから接続すると次のように ESTABLISHED になります。

~% netstat -an | grep 12345
tcp        0      0 0.0.0.0:12345           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:12345         127.0.0.1:56863         ESTABLISHED
tcp        0      0 127.0.0.1:56863         127.0.0.1:12345         ESTABLISHED
tcp6       0      0 :::12345                :::*                    LISTEN     

サーバーの 12345 とクライアントの 56863 が接続されている状態です。 同じサーバー内で接続しているため、1つの接続について、サーバーからみた接続とクライアントから見た接続の2行出力されています。

このように普通は接続が確立したらサーバーとクライアントの両方が ESTABLISHED になります。

ここで次のクライアントプログラムを動かしてみます。

require 'socket'

sockets = []
200.times do |i|
  p i
  sockets.push TCPSocket.new("localhost", 12345)
end
sleep

サーバーに 200接続したまま何もしないプログラムです。

サーバーは最初の接続からのクライアントの入力を待つので、2個め以降の接続を処理できません。

普通のサーバープログラムは、そんなことにならないように、クライアントからの接続を受け付けた時に fork したりスレッドを作ったりするのですが、ここでは意図的にこのようにしてます。

この状態で netstat を見ると、サーバー側にいくつか SYN_RECV 状態のポートがあります。

tcp        0      0 127.0.0.1:12345         127.0.0.1:36807         ESTABLISHED
tcp        0      0 127.0.0.1:12345         127.0.0.1:36930         SYN_RECV       ← これ
tcp        0      0 127.0.0.1:12345         127.0.0.1:36953         SYN_RECV       ← これ
tcp        0      0 127.0.0.1:36807         127.0.0.1:12345         ESTABLISHED
tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

クライアントポート 36807 はちゃんと対向するサーバーが ESTABLISHED になっているのですが、36930, 36953 ポートは SYN_RECV になっています。

しばらくすると、この SYN_RECV 状態のポートはなくなります。といっても ESTABLISHED になったわけではなく、消えてなくなってしまいます。

tcp        0      0 127.0.0.1:12345         127.0.0.1:36807         ESTABLISHED
tcp        0      0 127.0.0.1:36807         127.0.0.1:12345         ESTABLISHED
tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

クライアントは ESTABLISHED だと思っているのに接続相手のサーバーはいません。

サーバープログラムを停止しても状態は変わりません。 クライアントプログラムを停止しない限り残ったままです。

tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

このように 200 くらい接続すると SYN_RECV が確認できるのですけど、どうやらこれは listen のバックログの値に関連してるようです。

Socket.tcp_server_loop は内部で listen(Socket::SOMAXCONN) しています。手元の Linux では SOMAXCONN は 128 になっていました。

もっと単純に次のようなサーバープログラムで試してみました。

require 'socket'

TCPServer.new(12345).listen(5)
sleep

このようにバックログを 5 にすると、クライアント接続が 10 くらいでも再現できます。

なお、listen(Socket::SOMAXCONN) しているのは Ruby 2.0 以降です。Ruby 1.9.3 では listen(5) としているので、すぐに発生します。

で、結局こんなことになってしまう理由はわかりませんでした。

クライアントが ESTABLISHED だと思ってるのに SYN_RECV になってるのもわからないし、相手がいないのに ESTABLISHED になってるソケットが存在してるのもわかりません。 ネットワークの向こうの別のサーバであればパケット落ちとかでこのような状況になるのもわかるのですけど。

バックログを超えたら SYN_RECV にならなくてもいいと思うんですけど…。 TCP/IP はそういうもんなんですかね。

[追記]

Facebook の方で色々教えてもらいました。

OS X では発生しないようです。うすうす感じていたのですがやはり Linux の実装の問題のようです。

教えてもらったブログ http://veithen.blogspot.jp/2014/01/how-tcp-backlog-works-in-linux.html に答えがありました。

BSD では接続用のキューが1つで、キューがいっぱいの場合はクライアントからの SYN を破棄するだけなので、クライアントが ESTABLISHED になることはありません。私の知ってる TCP の動きです。

Linux では SYN キューと accept キューの2つがあるようで、SYN キューに入った時に SYN ACK を返し SYN_RECV 状態になり、accept キューに入った時に ESTABLISHED になるようです。

サーバープログラムが accept しなければ、accept キューがいっぱいになり SYN キューも掃けません。SYN キューにたまったものは一定時間たつと削除されるようです。

SYN キューの大きさは、sysctl net.ipv4.tcp_max_syn_backlog で、accept キューの大きさはプログラムから指定する listen のバックログです。

sysctl -w net.ipv4.tcp_max_syn_backlog=0 にして試してみたら BSD の動きに近くなりました。ただし 0 に設定しても1個は SYN_RECV になってしまうようです。

また sysctl -w net.ipv4.tcp_abort_on_overflow=1 にすると accept キューがあふれた場合、クライアントからの SYN に RST を返すようです。この場合はクライアントプログラムは connect(2) に対して ECONNRESET エラーが返ります。

RST じゃなくて SYN を無視するようになれば BSD と同じになるんじゃないかと思ったのですが、そういうパラメータは無さそうでした。

まあ別に BSD と同じ動きにしたかったわけではなく、ただ疑問に思っただけだったので、それは解決したのでもういいです。