MilkyStepの最近のブログ記事

たま~にforkして子プロセスを起動する処理を呼んだとき、親プロセスで上記の「Lost connection to MySQL server during query」エラーや、httpヘッダエラーが発生していた。

特に決済まわりで使っているわけでもないし、トランザクションが絶対必要なわけでもないし、子プロセス内の処理はとりあえず最後まで完了してるし、いままで「ま、いっか」って感じで放置していたが、最近頻発するようになってきたので、ようやく真面目に取り組んでみる。

DBIに接続したときのステートメントハンドルの扱い方が悪いんじゃないかっていいうのはうすうす気づいてたが、下記のような資料を発見。ありがたや。
http://d.hatena.ne.jp/hiratara/20110122/1295711939
http://nihen.hatenablog.com/entry/2011/11/17/102557


これの真似して、子プロセス生成直後に
$dbh->{InactiveDestroy} = 1;
$dbh = $dbh->clone({InactiveDestroy => 0});
を呼んだらなんとあっさり解決。

以前にこんなこと↓
http://www.igreks.jp/dev/2010/06/forkmysql.html
書いてたけど、やっぱ親プロセスと同じコネクションつかっちゃまずいわけね。
Apacheのせいにしてごめんなさいって感じですね。


例えば以下のようなテーブルがあったとき。

user_tbl(会員テーブル)
id name address
1 山田 東京都
3 鈴木 千葉県
6 高橋 北海道
12 佐藤 大阪府

shitumon_tbl(質問テーブル)
id title answer_type
1 あなたの身長 テキスト入力
2 あなたの体重 テキスト入力
3 住居形態 択一選択

sentakushi_tbl(質問の選択肢テーブル)
id shitsumon_id sentakushi_name
1 3 一戸建て
2 3 マンション
3 3 アパート
4 3 ダンボール

user_info_tbl(会員情報テーブル)
id user_id shitsumon_id answer
(テキスト入力の時はその値、
択一選択の時は選択肢ID)
9 1(山田) 1(身長) 175(cm)
10 1(山田) 2(体重) 68(kg)
11 1(山田) 3(住居形態) 3(アパート)
12 3(鈴木) 1(身長) 168(cm)
13 3(鈴木) 3(住居形態) 1(一戸建て)
14 6(高橋) 2(体重) 74(kg)
15 6(高橋) 3(住居形態) 2(マンション)
16 12(佐藤) 1(身長) 182(cm)
17 12(佐藤) 3(住居形態) 3(アパート)


上記のとき、
※「身長」が「170」cm以上で、 ※「住居形態」が「アパート」の会員(条件数=2)の「会員テーブル」だけを表示する。

SQLの例
-----------------------------------------------------------------------------------------
SELECT `user_tbl` . *
FROM `user_tbl`

LEFT JOIN INNER JOIN (
# 副問い合わせ

# まず質問IDが1(身長)で、答えが170(cm)以上のuser_idを抽出
(
SELECT `user_info_tbl`.user_id
FROM `user_info_tbl`
WHERE
`user_info_tbl`.question_id = 1
AND
`user_info_tbl`.answer >= 170
)

# 次に質問IDが3(住居形態)で、答えが3(アパート)のレコードをUNION ALL で結合
# ※「UNION ALL」にすることで、わざと重複させる。
UNION ALL (
SELECT `user_info_tbl`.user_id
FROM `user_info_tbl`
WHERE
`user_info_tbl`.question_id = 3
AND
`user_info_tbl`.answer = 3
)

) AS uni
# ↑副問い合わせでUNIONするときは固有名をつけないと、「ERROR 1248 (42000): Every derived table must have its own alias」になる。

# ユーザIDが同じものを外部内部結合
ON user_tbl.id = `uni`.user_id

# 重複数が2(=条件数)のレコードのみ表示
GROUP BY `user_tbl`.id
HAVING COUNT( `user_tbl`.id ) = 2
-----------------------------------------------------------------------------------------
結果
id name address
1 山田 東京都
12 佐藤 大阪府


これだと、会員・質問・選択肢を自由に追加・削除しても、情報の整合性がとれる。
会員の自由項目情報を扱ったりするときに便利。
(もちろん削除時、変更時にはそれに対応する各々のテーブルデータを書き換える必要はあるが・・・)


※追記
「LEFT JOIN」だと条件が1つのときにuser_tbl他のレコードも一緒にくっついてきてしまってまずいので「INNER JOIN」が正解。

まあ、択一選択にしたければ、最初からラジオボタンかセレクトボックスでフォームパーツを作ればいいのだが、場合によってはチェックボックスで択一選択のフォームを作りたいこともある。(多分そうそう機会はないと思うが)

例えばパラメタの都合上、各inputのname属性の値を別々にしたいときとか。
ラジオボタンでも、name属性の値が違えば、どちらもチェックできてしまう。

以下のようなチェックボックスのフォームが合った場合、

----------------------------------------------------------------------------------
<div class='parts_list'>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_A' value='1'> 選択肢A
    </td>
   </tr>
  </table>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_B' value='1'> 選択肢B
    </td>
   </tr>
  </table>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_C' value='1> 選択肢C
    </td>
   </tr>
  </table>
</div>
----------------------------------------------------------------------------------

これだと、そのままでは全てのチェックボックスにチェックを入れることができてしまうので、これをjQeryで制御する。
簡単にいえば、チェックされた以外のチェックボックスのチェックを外す。

以下が改良版。

----------------------------------------------------------------------------------
<script type="text/javascript" src='jquery.js'></script>

<script type="text/javascript">
 $(function(){

  $("div.parts_list table").each(
   $("input:checkbox",this).click(
    // チェックボックスをチェックした時のイベント開始
    function(){
     // 親要素(table)のセレクタ取得
     var parent = $(this).closest("table");
     // これより前のtableに対する処理
     parent.prevAll().each(
      function(){
       // チェックを外す
       $("input:checkbox",this).attr("checked",false);
      }
     );
     // これより後ろのtableに対する処理
     parent.nextAll().each(
      function(){
       // チェックを外す
       $("input:checkbox",this).attr("checked",false);
      }
     );
     // チェックボックスをチェックした時のイベント終了
    }
   ) // click閉じ
  ) // each閉じ

 )); // function閉じ
</script>


<div class='parts_list'>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_A' value='1'> 選択肢A
    </td>
   </tr>
  </table>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_B' value='1'> 選択肢B
    </td>
   </tr>
  </table>
  <table>
   <tr>
    <td>
     <input type='checkbox' name='check_C' value='1> 選択肢C
    </td>
   </tr>
  </table>
</div>
----------------------------------------------------------------------------------


以上。

jqueryは便利だなーやっぱり。

スクリプト側の配列をテンプレートに反映させるとき、すべての値をそのまま出力するならいいのだが、値が1のときは「checked」、0の時は空白文字列を出力したい時などがある。

例えば、以下のようなテンプレートがあったとき。

■test.tpl
--------------------------------------------------------------------------------------

<TMPL_IF NAME="users">
 <ul>
  <TMPL_LOOP NAME="users">
   <li>
    名前:<TMPL_VAR NAME="user_name">
   </li>
   <li>
    <input type='checkbox' name='pub_name_on' value='1' <TMPL_VAR NAME="pub_name"> /> 名前を公開する
   </li>
  </TMPL_LOOP>
 </ul>
<TMPL_ELSE>
 <p>要素がありません</p>
</TMPL_IF>

--------------------------------------------------------------------------------------


スクリプト側をオーソドックスに書くとこんな感じ

■test.cgi
--------------------------------------------------------------------------------------

use strict;
use HTML::Template;

# オブジェクト生成
my $tpl = HTML::Template->new( filename => 'test.tpl');

#ユーザ情報を2次元配列にセット
my @users = (
 ['山田 太郎', 0], # 「0」は名前非公開
 ['鈴木 一郎', 1] # 「1」は名前公開
);

#ループ格納用配列作成
my @lood_data = ();

while (@users) {

 # 各行のハッシュ作成
 my %row_data = ();

 # 配列データを格納
 $row_data{'user_name'} = $$_[0];
 $row_data{'pub_name'} = $$_[1];

 # $row_data{'pub_name'} が1だったら「checked」に変更
 $row_data{'pub_name'} = $row_data{'pub_name'} == 1 ? 'checked' : '';

 # リファレンスでループ用配列に追加
 push(@loop_data, \%row_data);

}

# paramメソッド実行
$tpl->param('users' => \@loop_data);

#出力
$tpl->output(print_to => \*STDOUT);

--------------------------------------------------------------------------------------


しかしこれだと、各値をいちいちハッシュにしたり面倒くさいので、下記のようにmapを用いてさらにその中で条件分岐も済ませると楽ちん。

■test2.cgi
--------------------------------------------------------------------------------------

use strict;
use HTML::Template;

# オブジェクト生成
my $tpl = HTML::Template->new( filename => 'test.tpl');

#ユーザ情報を2次元配列にセット
my @users = (
['山田 太郎', 0], # 「0」は名前非公開
['鈴木 一郎', 1] # 「1」は名前公開
);

# mapを使ってパラメータを一気に作成
my %params = ('users' => [
 map { +{
  user_name => $$_[0],
  pub_name => $$_[1] == 1 ? 'checked' : '' # ←ここで条件分岐
 }} @users
]);


# paramメソッド実行
$tpl->param(%params);

#出力
$tpl->output(print_to => \*STDOUT);

--------------------------------------------------------------------------------------

こんなかんじです。
MilkyStepのコミュニティでコアサーバを使ったとき、メルマガのデフォルト登録完了画面が文字化けする(日本語部分が???になる)との報告を受け、一時期かなり調査を行ったが解決せず。
それから約2カ月後、他のユーザから同様の現象が報告された。

開発環境では全くそのような現象が起こらないので、仕方なく、コアサーバーを実際に借りてMilkyStepを設置し徹底的に調査することにした。

表示させるデフォルト画面のHTMLソースは、ユーザが自由に編集できるようになっているのだが、そのソースはMySQLに保存される。

そこからデータを引っ張って、少し置換処理してからプログラムで出力する。

確かにコアサーバで実際に試してみると、5回中4回くらいの割合で日本語が「?」になる。
(成功する場合もある)

MySQLの文字セットはutf8、照合順序もutf8_general_ci、さらにSQL発行時に毎回「SET NAMES utf8」(MySQL4.1以降で有効)しているので、データ通信間で化けるとは考えにくい。
もちろんスクリプトのファイルもUTF8。

とりあえず、ブラウザの文字コードの誤判定臭いので、日本語を多めに入れたり、EUCでいうところの「美乳テーブル」である、郵便くんマーク(〠←これ)を冒頭に入れたりしてみたが、変わらず。

次に、Perlでutf8扱う時によくある、utf8フラグあるなしの問題かとも思い、「use utf8」したりEncode::encode/decodeしたりいろいろやったが、どうやら関係ないっぽい。

というか、なんかMySQLから参照した時点ですでに化けてるっぽい。

試しに、SELECT時に、以下のような感じで、明示的に照合順序を指定してみる。

まず、
--------------------------------------------------------------------------------
SELECT column_name COLLATE utf8_general_ci FROM ・・・・・
--------------------------------------------------------------------------------

・・・変化なし。

次に、
--------------------------------------------------------------------------------
SELECT COERCIBILITY(column_name COLLATE utf8_general_ci) FROM ・・・
--------------------------------------------------------------------------------

お、1回目はうまくいった。
もう1回。

--------------------------------------------------------------------------------
Software error:

COLLATION 'utf8' is not valid for CHARACTER SET 'latin1_swedish_ci' at ......
--------------------------------------------------------------------------------

あれあれ????

「utf8_general_ciでの照合はCHARACTER SET 'latin1'では有効ではありません」

latin1??なんで勝手に文字セット変わってんだよ!
utf8で固定だろーが!

なぜ、コアサーバだけ、しかも特定のカラムの文字列だけ参照するときにこうなるのか・・・

MySQL4から5に強引に移行した時の忘れものか?


とにかくこの、参照時に勝手にテーブルの文字コードをLatin1と判別するわけのわからない現象と格闘すること数時間。

そもそもこの処理って、forkした後の親プロセスの中でやっていたんだが、このforkを切ったらなぜか正常になった。
ということで、この処理はforkの中から外に出すことに決定。

いまだに原因はよくわからないが、fork時は内部的に変数を子プロセスにコピーするが、その時点で(コアサーバでは)なんか問題があるらしいってことはぼんやりわかった。

サーバサイドのアプリを広く対応するのはやっぱ一苦労だな・・・

システム開発上、ユーザがブラウザからメールマガジンを作成した時に、同時にエラーメール処理用のアドレスも作成されるようにする、逆にメルマガを削除したらそのアドレスも削除される必要があったためメモ。

今回のMTAはqmailを使うとのことで、アカウントの管理は必然的にvpopmailとなる。

しかし、qmailadminを使わないで新規アカウント作成(vadduser)・削除(vdeluser)を行うためには、基本的にrootでの操作となる。

単純にスクリプト内で

system("/home/vpopmail/bin/vadduser hoge@hoge.jp hogepass");

とやっただけでは、もちろんうまくいくはずがない。

というわけで、Cと連携して上手いことやってくれるモジュール様がないかと、CPANを探してみたらありました。
その名も「vpopmail.pm」。

呼び出す関数名もまさにvpopmailコマンドとほぼ同じ。

しかし、最終リリースは2001年・・・やばいんじゃないの?

かろうじてCPAN.pmをからインストールできたものの、説明も短すぎて、当然
use vpopmail;
vadduser(引数いろいろ);
とかやれば新規にアカウントを作ってくれると思いきや全然ダメ。

vpopmailのバージョンを見てきてくれる関数だけはなぜか動いた(笑)
まあ、関数名も最近のvpopmailのコマンドといまいち合ってないし。

というわけでさんざん悩んだ結果、sudoを使うことで決定。

以下手順。

1.新規バーチャルアカウントを作成する簡単なスクリプト(vadduser.cgi)を作成しCGIの動くディレクトリに置く。

vadduser.cgi
-----------------------------------------------------------------------------------------
#!/usr/bin/perl

use strict;

# コマンド発行
`sudo -u root /home/vpopmail/bin/vadduser hoge@hoge.jp hogepass`;
exit;
1
-----------------------------------------------------------------------------------------

2.sudoの設定ファイルに許可コマンドを追記

# visudo

(以下を追記)
apache ALL=(root) NOPASSWD: /home/vpopmail/bin/vadduser,/home/vpopmail/bin/vdeluser

同時に、以下の行をコメント化

Defaults requiretty
   ↓
#Defaults requiretty

※これをコメント化しないと、最近のLinuxでは、
sudo: apache : sorry, you must have a tty to run sudo ; TTY=unknown ;......
と怒られる。

このエラーは、/var/log/secure を見ればわかる。

保存して終了
:wq


3.vadduser.cgiをブラウザから実行してみる。

4.確認

popアカウント一覧に、「hoge」が追加されているのを確認

同時に、/home/vpopmail/domains/hoge@hoge.jp 内に、ディレクトリ「hoge」が作成されているのでOK!!



ああ疲れた今回も。


実務的には、このvadduser.cgiをAPIとしてドキュメントルート外に置いて、第三者からは直接アクセスされないようにし、他のCGIからシステムコールで呼んだりした方がセキュアかなと。


参考URL:
http://hibari.2ch.net/test/read.cgi/php/1024741312/l50
http://d.hatena.ne.jp/kakurasan/20100512/p1
http://old.ikoinoba.net/index.php?UID=1188143501
http://search.cpan.org/~sscanlon/vpopmail-0.08/
空メール登録のシステム構築自体は以下の通り。
http://www.igreks.jp/dev/2010/06/postfixperl.html

上記の場合は、アイリアス設定ファイルに直接転送先を書いているが、転送用ファイル「.qmail」や「.forward」にパイプで転送先を書いて、ユーザディレクトリ直下に配置してももちろん構わない。

ここで注意したいのが、「.qmail」にパイプ処理を書くとき、空行を作ってはならないということだ。

「.qmail」ファイルは本来の用途で使うならば、転送したいメールアドレスが一行に1つずつ書いてある。
qmailはこれを上から順に処理していく。

つまり、空行が合った場合でも、「(空文字列)@ドメイン」と認識してしまう。

したがって、そんなメールボックスはありませんよ!と、Mailer-Daemonから送信元にエラーが返ってきてしまう。

最後の行を改行して終わらないと気持ちが悪いのは同感だが、この場合はやってはいけない。


.qmail

○良い例
---------------------------------------------------------
| プログラムへのパス (コマンドライン引数)(改行なし)
---------------------------------------------------------

×悪い例
---------------------------------------------------------
| プログラムへのパス (コマンドライン引数)(改行)
(空行)
---------------------------------------------------------


あと、qmailの場合も、postfix同様、パイプで渡されたメールキューは勝手に削除されてるっぽい。ログに「remove」とかいう記載はないが。

渡したプログラムで「exit;」とかしてないと、もしかしたら続いちゃうのかも。
これはGD::Barcode::QRcodeを用いたQRコード生成処理時に発生したエラー。

同じようなエラーに関しての記事が数件見つかったのでメモ。
http://xiaoxia.exblog.jp/6226189/
http://php.net/manual/ja/image.requirements.php

↓上記ページからの引用

操作できるイメージの形式は、インストールされている GDとこれらのイメージフォーマットにアクセスする ためにGDが必要とするその他のライブラリに依存し ます。
gd-1.6より前のバージョンのgdは、GIF イメージ画像をサポートし ていますが、PNG はサポートしていません。
一方、gd-1.6以降でgd-2.0.28未満の バージョンはPNGをサポートし、GIFはサポートしていません。
GIF サポートは、gd-2.0.28で再度有効になりました。



つまり、GDライブラリのバージョンによってはGIFをサポートしていないので、標題のようなエラーが発生するらしい。

GDライブラリのバージョンを変えたり、依存する各種libを変えたりすれば解決できるようだが、共用サーバだったり、インストール権限が無いユーザでは難しい。

というわけで、得意のevalを使って対処。
※gifをサポートしていなければpng、pngをサポートしていなければjpegで出力する
※すべてのGDライブラリのバージョンを調査したわけではないが、gifもpngもjpegもサポートしていなかったらダメ。多分それはありえないが。

以下サンプルコード

-----------------------------------------------------------------------------------

#!/usr/bin/perl

use strict;
use GD::Barcode::QRcode;

#オブジェクト生成
my $qr_obj = GD::Barcode::QRcode->new(
 'http://example.jp', #QR化する文字列
 { ModuleSize => 2 } # Ecc & Versionはauto select
);

my $qr_img = '';
my $ext = 'gif'; #拡張子

eval { $qr_img = $qr_obj->plot()->gif(); };

if($@){
 $ext = 'png';
 eval { $qr_img = $qr_obj->plot()->png(); };
 if($@){
  $ext = 'jpeg';
  eval { $qr_img = $qr_obj->plot()->jpeg(); };
   if($@){
    die "gif, png, jpeg are not supported!!";
   }
 }
}

#書き出し
my $fh;
open($fh,">QRcode.$ext");
 binmode $fh;
 print $fh ($qr_img);
close $fh;


-----------------------------------------------------------------------------------



ちなみにGDライブラリのインストール権限をお持ちの方は、以下から最新版のGDライブラリをダウンロードして根本解決することも可。
http://www.libgd.org/releases/


milystepはメルマガごとに配信履歴テーブル(his)とクリック測定用テーブル(cs)がある。

クリック測定テーブルには全てのURLのデータが入っていて、各レコードはどの配信履歴に対応しているか判別するためのナンバーが格納されている。

簡単に書くとこんな感じ

csテーブル
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 no   | url    | click  | his_no(対応するhisのナンバー)  
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 1    | http://.... | 3    |  2
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 2    | http://.... | 1    |  2
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 3    | http://.... | 0    |  3
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 4    | http://.... | 12   |  3

        ・
        ・
        ・
hisテーブル
ーーーーーーーーーーーーーーーーーーーーーーー
 no  | subject   | body 
ーーーーーーーーーーーーーーーーーーーーーーー
 1   | こんにちは | ご無沙汰してます...
ーーーーーーーーーーーーーーーーーーーーーーー
 2   | 忘年会の件 | 場所が決まりました...
ーーーーーーーーーーーーーーーーーーーーーーー
 3   | ハロー   | 元気ですか?...
ーーーーーーーーーーーーーーーーーーーーーーー
 4   | 了解です  | 例の件ですが...
        ・
        ・
        ・



このとき、hisのテーブルに、csの対応するhisナンバーのクリック数(click)の合計をtotal_clickとしてSELECTしたい。

イメージとしては、こんな感じ。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 no  | subject   | body        | total_click
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 1   | こんにちは | ご無沙汰してます... | 0
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 2   | 忘年会の件 | 場所が決まりました...| 4(cs.his_noが2のclickの合計)
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 3   | ハロー   | 元気ですか?...   | 12(cs.his_noが3のclickの合計)
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 4   | 了解です  | 例の件ですが...   | 0
        ・
        ・
        ・


なので最初は、サブクエリを使って、
-------------------------------------------------------------
SELECT
 *,
 (
  SELECT
   SUM(cs.click) FROM cs
  WHERE cs.his_no = his.no
 ) AS total_click
FROM his
-------------------------------------------------------------

みたいにやってたのだが、あまりにも遅い。
300件程度の処理で明らかに体感速度が数秒違うので、これはヤバい。

というわけで、一旦テンポラリテーブルを作ってそれをLEFT JOINさせる。

-------------------------------------------------------------
CREATE TEMPORARY TABLE temp AS
 SELECT
  his_no,
  SUM(click) AS total_click
 FROM cs
 GROUP BY his_no;

SELECT
 *,
 temp.total_click
FROM his
LEFT JOIN temp ON
 his.no = temp.his_no;
-------------------------------------------------------------

これでだいぶ改善。

データが数千件になったときはどうなるかまだわからない。


参考ページ
http://homepage2.nifty.com/sak/w_sak3/doc/sysbrd/mysql_09.htm
http://oss.timedia.co.jp/show/MySQL%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%97%85/%E4%B8%80%E6%99%82%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AF%E3%81%A9%E3%81%86%E3%81%AA%E3%82%8B%E3%81%8B
http://labs.mythos-jp.com/blog/archives/246


例えば、長い文字列
「あいうえおかきくけこさしすせそ」を
「あいうえお...」みたいに、
指定の文字数以降は省略したい場合。

ただ単にバイト数で切ってしまうと、日本語の場合は余るバイトがでてくるので、そのへんをちゃんとしないとダメ。

以下、UTF8の場合の処理

----------------------------------------------------------------------------------

my #str = "あいうえおかきくけこさしすせそ"; # 元の文字列
my $view = 10; # 10文字まで表示し後は省略

print &main::round_utf8($str,$view);

sub round_utf8 {
my ($str,$view) = @_;

my $one = "[\x00-\x7F]"; # 1バイト文字
my $two = "[\xC2-\xDF][\x80-\xBF]"; # 2バイト文字
my $three = "[\xE0-\xEF][\x80-\xBF]{2}"; # 3バイト文字
my $bigger = "[\xF0-\xF7][\x80-\xBF]{3,}"; # 4バイト以上

$str =~ s/^(($one|$two|$three|$bigger){$view}).+/$1.../g;

return $str;
}

----------------------------------------------------------------------------------

EUCやSJISの場合も、サブルーチン中のバイト列の定義を変えれば同じ。