MCP (MultiCore Parser)を使ってみる

http://www.scutum.jp/information/waf_tech_blog/2013/02/waf-blog-019.html
https://github.com/Kanatoko/MCP


↑の、Javaを用いて手軽にマルチコアを生かしたデータ処理を可能にするためのフレームワーク MCP (Multi Core Parser)というものを見つけたので使ってみました。

何かのデータ、特にログなどをHadoopで処理する前にそもそもマルチコアで処理したいなと思いました。意外と一つのファイルをマルチコアで処理するのは難しく、うまく出来ませんでした。シングルコアでの処理なら簡単なんですが、8コアマシンで1コアだけ使って待っているってアホらしいですね。一方でHadoopを持ち出すのも大仰ですし。。。

まずは使い方です。私はJavaに疎いのではじめから書きます。

# まず例として、以下の様なファイルを作ります。
# parseというメソッドに、テキストの一行一行が入るようです。
$ vi UpperCase.java
$ cat UpperCase.java 
public class UpperCase
implements net.jumperz.io.multicore.MParser
{
	//--------------------------------------------------------------------------------
	public String parse( String s )
	{
		return s.toUpperCase();
	}
	//--------------------------------------------------------------------------------
}

# mcp.jar をダウンロードします
$ wget http://www.jumperz.net/tools/mcp.jar

# コンパイルします
$ javac -cp mcp.jar UpperCase.java

# 使ってみます
$ echo -e 'foo\nbar\nbaz' | java -cp mcp.jar net.jumperz.app.MCP.MCP UpperCase
FOO
BAR
BAZ

というわけで無事に動かせました。次は集計などをやってみたいと思います。

MySQLで論理削除と一意性制約の両立

追記@2015-03-25

素直にMySQLのバックアップを取っていれば(さらに消すときにログを残しておけば)、特に論理削除せず普通に削除すればいい気もしてきました。いろいろと見通しがよくなりますし。とは言えTwitterみたいにアカウント削除しても30日は復活できる、みたいなことをしたい場合必要だと思います。要件次第ですね。


論理削除にはいろいろなメリットがあります。行削除のように関連する他テーブルへ削除が波及しないこと、エントリ復活ができること、障害時にデータ変更の経緯を追いやすくなることなどなど。

ところが論理削除の方針でDBを組んでいて困ったことはありませんか?
「 メールアドレスは一意性(UNIQUE)制約をかけたいのに、それだと削除済みのユーザと同じメールアドレスが使えないことになる 」

論理削除と一意性制約、両立はできないのか?
できないと思っている方、多いと思います。実はちゃんとできます。DB製品によって実現方法がちょっと違ってくるだけで。

論理削除と一意性制約を両立させる方法・DB製品別


↑を読んでなるほどと思ったのですが、MySQLでの実現方法が載っていなかったので、書いてみました。MySQLではCHECK制約が無視されますのでどうしようかなと思ったのですが、要はstatusの値も含めて一意かどうか判別すれば良さそうです。

今回行ったことは、

– 二つのカラム(usernameとemailを想定します)に対する一意性制約と論理削除の両立@MySQL

です。

ともかくやってみます。MySQLのコンソールに入って作業することを想定しています。「#」はコメントです。

# 以下の様なテーブルを作成します
> CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `username` varchar(19) NOT NULL,
  `state` tinyint(4) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email_state` (`email`,`state`),
  UNIQUE KEY `username_state` (`username`,`state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 3つほど値をいれてみます
 > insert into users (email, username) values ("foo@example.com", "foo"),("bar@example.com", "bar"),("fizz@example.com", "fizz");
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

# 状態を確認してみます
> select * from users;
+----+------------------+----------+-------+
| id | email            | username | state |
+----+------------------+----------+-------+
|  1 | foo@example.com  | foo      |     0 |
|  2 | bar@example.com  | bar      |     0 |
|  3 | fizz@example.com | fizz     |     0 |
+----+------------------+----------+-------+
3 rows in set (0.00 sec)

# emailとusernameの一意性が保たれているか確かめてみます
# emailが同じだったりusernameが同じだったりするものを入れてみます

# 同じものをいれてみる→一意性制約によりエラー
> insert into users (email, username) values ("foo@example.com", "foo");
ERROR 1062 (23000): Duplicate entry 'foo@example.com-0' for key 'email_state'

# emailが異なり、usernameが同じものを入れてみる→一意性制約によりエラー 
> insert into users (email, username) values ("foo1@example.com", "foo");
ERROR 1062 (23000): Duplicate entry 'foo-0' for key 'username_state'

# emailは同じで、usernameが異なるものを入れてみる→一意性制約によりエラー
> insert into users (email, username) values ("foo@example.com", "foo1");
ERROR 1062 (23000): Duplicate entry 'foo@example.com-0' for key 'email_state'

# emailは同じで、usernameを大文字にしてみる→一意性制約によりエラー
# (これはいわゆるケースセンシティブでない例です。Collation: utf8_general_ciと
# 設定しておく必要があると思います)
> insert into users (email, username) values ("foo@example.com", "FOO");
ERROR 1062 (23000): Duplicate entry 'foo@example.com-0' for key 'email_state'

# ↑上記のように、一意性制約がきちんと働いていることが分かりました。

# 次に、論理削除をしてみます。id:3を論理削除します。
> update users set state = -1 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# ↓のようにstateが-1となっています
> select * from users;
+----+------------------+----------+-------+
| id | email            | username | state |
+----+------------------+----------+-------+
|  1 | foo@example.com  | foo      |     0 |
|  2 | bar@example.com  | bar      |     0 |
|  3 | fizz@example.com | fizz     |    -1 |
+----+------------------+----------+-------+
3 rows in set (0.00 sec)

# さて、この状態において、emailのfizz@example.comや、usernameのfizzは、
# 論理削除されているので、同じものが追加されても受け付けるはずです。

# まったく同じものを挿入してみました。無事、挿入できました。
> insert into users (email, username) values ("fizz@example.com", "fizz");
Query OK, 1 row affected (0.00 sec)

# 状態を確認すると↓のようになっています。
# 一件、fizz@example.comとfizzが二つあっておかしな感じですが、
# あくまで一意性制約はemail+state, username+stateに掛かっているので、
# 正常です。
# 蛇足ですが、新しいレコードのidが8となっているのは、
# MySQLのトランザクションとロールバックの関係のようです。
# 参考: http://stackoverflow.com/questions/2787910/why-does-mysql-autoincrement-increase-on-failed-inserts
 > select * from users;
+----+------------------+----------+-------+
| id | email            | username | state |
+----+------------------+----------+-------+
|  1 | foo@example.com  | foo      |     0 |
|  2 | bar@example.com  | bar      |     0 |
|  3 | fizz@example.com | fizz     |    -1 |
|  8 | fizz@example.com | fizz     |     0 |
+----+------------------+----------+-------+
4 rows in set (0.00 sec)

# 検索するときは、stateの条件も付与する必要があります

# OKな例↓
select * from users where username like "fizz" and state = 0;
+----+------------------+----------+-------+
| id | email            | username | state |
+----+------------------+----------+-------+
|  8 | fizz@example.com | fizz     |     0 |
+----+------------------+----------+-------+
1 row in set (0.02 sec)

# だめな例↓(2つのレコードがでてくる)
> select * from users where username like "fizz";
+----+------------------+----------+-------+
| id | email            | username | state |
+----+------------------+----------+-------+
|  3 | fizz@example.com | fizz     |    -1 |
|  8 | fizz@example.com | fizz     |     0 |
+----+------------------+----------+-------+
2 rows in set (0.00 sec)

というわけで無事、MySQLでもできました。

Mailgunに登録してPHPでメール送信をしてみる

追記@2015-03-27
もしかしたら、アプリケーション内でhttpsによる外部APIを叩いているサーバのメモリ使用量が増加し続ける件について が起きるかもです。
素直にMailgunが提供しているPHP向けSDKを使ったほうが良いかもです。


標題のことをやってみました。Mailgunが用意したSDK for PHPは存在するのですが、そのままHTTPで叩きたいなと思ったので、やってみました。

Mailgunに登録すると、

curl -s --user 'api:key-xxxxxx' \
    https://api.mailgun.net/v3/sandboxxxxxxx.mailgun.org/messages \
    -F from='Mailgun Sandbox <postmaster@sandboxxxxxxx.mailgun.org>' \
    -F to='John Doe <john.doe@example.com>'\
    -F subject='Hello John Doe' \
    -F text='Congratulations John Doe, you just sent an email with Mailgun!  You are truly awesome! 

You can see a record of this email in your logs: https://mailgun.com/cp/log 

You can send up to 300 emails/day from this sandbox server. Next, you should add your own domain so you can send 10,000 emails/month for free.'

↑てな感じのコマンドを渡されます。これをPHPで再現したのが↓です。

<?php
// ↓の3つは自分の値に書き換える!
define('MAILGUN_USER', 'api:key-xxxxxx');
define('MAILGUN_URI', 'https://api.mailgun.net/v3/sandboxxxxxxx.mailgun.org/messages');
define('MAILGUN_FROM', 'Mailgun Sandbox <postmaster@sandboxxxxxxx.mailgun.org>');

$sendTo = 'John Doe <john.doe@example.com>';
$subject = 'Hello John Doe';
$text = 'Congratulations John Doe, you just sent an email with Mailgun!  You are truly awesome! 

You can see a record of this email in your logs: https://mailgun.com/cp/log 

You can send up to 300 emails/day from this sandbox server. Next, you should add your own domain so you can send 10,000 emails/month for free.';

$rep = sendEmailByMailgun($sendTo, $subject, $text);

var_dump($rep);

function sendEmailByMailgun($sendTo, $subject, $text) {
	$data = http_build_query(array(
		"from" => MAILGUN_FROM,
		"to" => $sendTo,
		"subject" => $subject,
		"text" => $text,
		), "", "&");
	$header = array(
		"Authorization: Basic " . base64_encode(MAILGUN_USER),
		"User-Agent: PHP",
		"Host: api.mailgun.net",
		"Accept: */*",
		"Content-Length: " . strlen($data),
		"Expect: 100-continue",
		"Content-Type: application/x-www-form-urlencoded",
	);
	$stream_context = array(
		"http" => array(
			"method" => "POST",
			"header" => implode("\r\n", $header),
			"content" => $data,
		)
	);
	return file_get_contents(MAILGUN_URI, false, stream_context_create($stream_context));
}


実行結果↓

string(129) "{
  "message": "Queued. Thank you.",
  "id": "<20150320184311.70040.17235@sandboxxxxxx.mailgun.org>"
}"

シェルスクリプト(Bash)の引数を正規表現で確認する方法

ときどきやりたくなり、そのたびに忘れるので書き残しておきます。

たとえば年の確認をしたい場合は、次のように書きます。
引数にダブルクオーテーションつけたり、正規表現の方にはダブルクオーテーションいらなかったりで混乱しがちですね。。。(他の言語だと正規表現は「/」で囲んだりしていますが。。。)

year.sh

#!/usr/bin/env bash

if [[ "$1" =~ ^20[0-9]{2}$ ]]; then
        echo "year"
else
        echo "not year"
fi

↑を実行してみます。

$ sh year.sh 2015
year

$ sh year.sh piyo
not year

$ sh year.sh 2015piyo
not year

良さそうな感じですね。

【PHP】pearで「Could not open input file: /opt/local/lib/php54/pear/pearcmd.php」とでるとき【macports】

環境はmacです。

macportsでphp56を入れて、それをデフォルトにしてみました。

$ sudo port install php56
$ sudo port select --set php php56
Selecting 'php56' for 'php' failed: symlink: /opt/local/etc/select/php/current -> php56: file already exists
$ php -v
PHP 5.6.6 (cli) (built: Feb 21 2015 09:42:01) 
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies

そうしますと、pearやpeclが動かなくなりました。

$ pear
Could not open input file: /opt/local/lib/php54/pear/pearcmd.php

↑こんな感じで何も出来ません。いろいろと直し方はあるようですが、Pearの公式からpharを落としてきて実行したら直りました。

$ wget http://pear.php.net/go-pear.phar
$ sudo php go-pear.phar 

Below is a suggested file layout for your new PEAR installation.  To
change individual locations, type the number in front of the
directory.  Type 'all' to change all of them or simply press Enter to
accept these locations.

 1. Installation base ($prefix)                   : /opt/local
 2. Temporary directory for processing            : /tmp/pear/install
 3. Temporary directory for downloads             : /tmp/pear/install
 4. Binaries directory                            : /opt/local/bin
 5. PHP code directory ($php_dir)                 : /opt/local/lib/php
 6. Documentation directory                       : /opt/local/docs
 7. Data directory                                : /opt/local/data
 8. User-modifiable configuration files directory : /opt/local/cfg
 9. Public Web Files directory                    : /opt/local/www
10. Tests directory                               : /opt/local/tests
11. Name of configuration file                    : /opt/local/etc/pear.conf

1-11, 'all' or Enter to continue: 
Beginning install...
Configuration written to /opt/local/etc/pear.conf...
Initialized registry...
Preparing to install...
installing phar:///Users/mk/my_repos/workspace/go-pear.phar/PEAR/go-pear-tarballs/Archive_Tar-1.3.12.tar...
installing phar:///Users/mk/my_repos/workspace/go-pear.phar/PEAR/go-pear-tarballs/Console_Getopt-1.3.1.tar...
installing phar:///Users/mk/my_repos/workspace/go-pear.phar/PEAR/go-pear-tarballs/PEAR-1.9.5.tar...
installing phar:///Users/mk/my_repos/workspace/go-pear.phar/PEAR/go-pear-tarballs/Structures_Graph-1.0.4.tar...
installing phar:///Users/mk/my_repos/workspace/go-pear.phar/PEAR/go-pear-tarballs/XML_Util-1.2.3.tar...
install ok: channel://pear.php.net/Archive_Tar-1.3.12
install ok: channel://pear.php.net/Console_Getopt-1.3.1
install ok: channel://pear.php.net/Structures_Graph-1.0.4
install ok: channel://pear.php.net/XML_Util-1.2.3
install ok: channel://pear.php.net/PEAR-1.9.5
PEAR: Optional feature webinstaller available (PEAR's web-based installer)
PEAR: Optional feature gtkinstaller available (PEAR's PHP-GTK-based installer)
PEAR: Optional feature gtk2installer available (PEAR's PHP-GTK2-based installer)
PEAR: To install optional features use "pear install pear/PEAR#featurename"

******************************************************************************
WARNING!  The include_path defined in the currently used php.ini does not
contain the PEAR PHP directory you just specified:
</opt/local/lib/php>
If the specified directory is also not in the include_path used by
your scripts, you will have problems getting any PEAR packages working.


Would you like to alter php.ini </opt/local/etc/php56/php.ini>? [Y/n] : Y

php.ini </opt/local/etc/php56/php.ini> include_path updated.

Current include path           : .;/usr/lib/php/pear
Configured directory           : /opt/local/lib/php
Currently used php.ini (guess) : /opt/local/etc/php56/php.ini
Press Enter to continue: 

The 'pear' command is now at your service at /opt/local/bin/pear

# 確認
$ pear version
PEAR Version: 1.9.5
PHP Version: 5.6.6
Zend Engine Version: 2.6.0
Running on: Darwin macbook.local 12.5.0 Darwin Kernel Version 12.5.0: Sun Sep 29 13:33:47 PDT 2013; root:xnu-2050.48.12~1/RELEASE_X86_64 x86_64

ついでにpeclもインストールします。

$ sudo pear install CodeGen_PECL
WARNING: "pear/Console_Getopt" is deprecated in favor of "pear/Console_GetoptPlus"
downloading CodeGen_PECL-1.1.3.tgz ...
Starting to download CodeGen_PECL-1.1.3.tgz (102,640 bytes)
........................done: 102,640 bytes
downloading CodeGen-1.0.7.tgz ...
Starting to download CodeGen-1.0.7.tgz (51,114 bytes)
...done: 51,114 bytes
install ok: channel://pear.php.net/CodeGen-1.0.7
install ok: channel://pear.php.net/CodeGen_PECL-1.1.3

$ pecl version
PEAR Version: 1.9.5
PHP Version: 5.6.6
Zend Engine Version: 2.6.0
Running on: Darwin macbook.local 12.5.0 Darwin Kernel Version 12.5.0: Sun Sep 29 13:33:47 PDT 2013; root:xnu-2050.48.12~1/RELEASE_X86_64 x86_64

php.iniをセットします。

$ pear config-set php_ini /opt/local/etc/php56/php.ini
$ pecl config-set php_ini /opt/local/etc/php56/php.ini

peclでredisライブラリなどが入ります。

$ sudo pecl install redis
WARNING: channel "pecl.php.net" has updated its protocols, use "pecl channel-update pecl.php.net" to update
downloading redis-2.2.7.tgz ...
Starting to download redis-2.2.7.tgz (134,340 bytes)
.............................done: 134,340 bytes
13 source files, building
running: phpize
Configuring for:
PHP Api Version:         20131106
Zend Module Api No:      20131226
Zend Extension Api No:   220131226
building in /private/tmp/pear/install/pear-build-rootwa3QZM/redis-2.2.7
running: /private/tmp/pear/install/redis/configure

# ~~~中略~~~

Build process completed successfully
Installing '/opt/local/lib/php56/extensions/no-debug-non-zts-20131226/redis.so'
install ok: channel://pecl.php.net/redis-2.2.7
Extension redis enabled in php.ini

いい感じですね〜。