日頃の行い

個人的な日頃の行いをつらつら書いてます\\\\ ٩( 'ω' )و ////

mongooseのconnection周りでちょっとハマったからメモる

スクレイピングのストレージにMongoDBをなんとなく使っていて、その時にnodejsからmongooseを使ってデータを突っ込んでます。
その時になぜか固まってしまったので、原因を追ってみました。

Mongoose ODM v4.3.4

事象

connection生成して、MongoDBにデータ突っ込もうとしたら止まりました。
試したコマンドとコードは下記の通りです。

$npm i --save mongoose
...

$node -v
v4.1.0

$node src/main.js
mongo connection opened
# ここで止まります
  • src/main.js
var mongoose= require("mongoose");
var logger  = console;
var db      = mongoose.createConnection();

db.on('open', function() {
    logger.info("mongo connection opened")
});

db.on('close', function() {
    logger.info("mongo connection closed")
});

db.open("mongodb://localhost/test");
var schema = new mongoose.Schema({
    id: String
});
mongoose.model("Test", schema);

var Model = mongoose.model("Test"); // 結論ここが問題
var m = new Model({
    id: "hogehoge"
});
// console.logとかでどこで止まってるかを見るとここで止まります。
m.save(function(e1) {
    e1 && logger.error(e1);

    Model.count({}, function(e2, count) {
        e2 && logger.error(e2);
        logger.info(count);
        mongoose.disconnect();
    });
});

原因(?)と対策

その1

コネクションを作った場合、そのコネクションからモデルを取得しないといけないようです。
var Model = mongoose.model("Test"); ではなく var Model = db.model("Test"); にします。

  • src/main.js
var mongoose= require("mongoose");
var logger  = console;
var db      = mongoose.createConnection();

db.on('open', function() {
    logger.info("mongo connection opened")
});

db.on('close', function() {
    logger.info("mongo connection closed")
});

db.open("mongodb://localhost/test");
var schema = new mongoose.Schema({
    id: String
});
mongoose.model("Test", schema);

// var Model = mongoose.model("Test"); // 結論ここが問題
var Model = db.model("Test");
var m = new Model({
    id: "hogehoge"
});
m.save(function(e1) {
    e1 && logger.error(e1);

    Model.count({}, function(e2, count) {
        e2 && logger.error(e2);
        logger.info(count);
        mongoose.disconnect();
    });
});
  • 実行結果
$node src/main.js
mongo connection opened
9
mongo connection closed

その2

そもそもコネクションはmongooseオブジェクトが持っているようなので、それを使いましょう。
var db = mongoose.createConnection();var db = mongoose.connection; に変えます。

var mongoose= require("mongoose");
var logger  = console;
//var db      = mongoose.createConnection();
var db      = mongoose.connection;

db.on('open', function() {
    logger.info("mongo connection opened")
});

db.on('close', function() {
    logger.info("mongo connection closed")
});

db.open("mongodb://localhost/test");
var schema = new mongoose.Schema({
    id: String
});
mongoose.model("Test", schema);

var Model = mongoose.model("Test"); // 結論ここが問題
var m = new Model({
    id: "hogehoge"
});
m.save(function(e1) {
    e1 && logger.error(e1);

    Model.count({}, function(e2, count) {
        e2 && logger.error(e2);
        logger.info(count);
        mongoose.disconnect();
    });
});
  • 実行結果
$node src/main.js
mongo connection opened
10
mongo connection closed

動いたみたい。
そもそもMongoやめようかなみたいな気持ち。

サークルの雑務をしてくれるマネージャーbotが欲しくなりますよね。

このブログはVOYAGE GROUPのアドベントカレンダー9日目として書かれています。techlog.voyagegroup.com


こんにちは @ara_ta3 です。
突然ですが、VOYAGE GROUPにはサークルという制度(?)があります。
詳しくはこちらとか。culture.voyagegroup.com

私は現在バスケットボールサークルの代表をしているのですが、サークルの代表って往々にして雑務に追われると思うんですね。
仕事の振り方が下手とか言わないでください。

バスケットボールサークルの場合、体育館の抽選・予約が主に重要な雑務です。
その他には大会予約や会計等ありますがまあ・・・うん・・・許容範囲内です。
体育館の抽選・予約にはいくつかしなければいけないことがあります。

  1. まず抽選・予約というタスクを忘れない
  2. Web上にある予約システムから抽選または予約を行う
  3. 抽選結果を確認する
  4. (Optional)先払いで体育館代を支払いに行く(物理)

人生の主軸がサークルの活動ではないので、普段これらを常に気にし続けるストレスフルな生活をしたくはありません。必要なタイミングで気がつけるようにしたくなりますね。
あぁ・・・マネージャーが欲しい。

そこで・・・Hubotですよッ!!
Hubot https://hubot.github.com/

社内でのコミュニケーションツールとしてSlackを利用しているため、連携も容易です。
ではマネージャーの錬成しましょう。時間と等価交換です。
その名も
_人人人人人人_
> harukosan <
 ̄Y^Y^Y^Y^Y ̄
名前に聞き覚えがある?気のせいかと思われます。
まず錬成の前にそれぞれの仕事を自動化する機能を実装します。

1. まず抽選・予約というタスクを忘れない

抽選を忘れるとほぼ体育館の予約は不可能となり、活動自体が行えなくなってしまいます。
アラートレベルはCRITICALです。
それほど東京での体育館の抽選は熾烈です。つらい。
なので、毎月あるタイミングで通知してくれる機能が欲しくなります。
そこで利用したのがこちら。

matsukaz/hubot-schedule · GitHub

できれば、抽選登録をやってなければ締め切り前に通知を投げてくれる等の機能も欲しいですが、今回は割愛します。
インストールは簡単。

$npm install hubot-schedule --save

そして、external-scripts.json に追加すれば反映されます。

[
  "hubot-help",
  "hubot-redis-brain",
  "hubot-schedule"
]

hubotのbrainという機能を使わないと再起動時にスケジュールのデータが消えてしまうので、hubot-redis-brainを利用してデータの永続化をしています。
残るはスケジュールの登録ですね。cronの文法でスケジュールを登録できます。
毎月1日から抽選の応募を開始し、4日あたりに締め切りが来るので、毎月1日の9:30に通知が来れば良さそうですね。
なのでharukosan起動後に下記のように伝えれば問題ないですね。

harukosan schedule add "30 9 1 * *" バスケットはお好きですか?

2. Web上にある予約システムから抽選または予約を行う

これができれば、1. で設定したものもいらなくなるので最高です。
また、抽選に落ちたとしても低い確率で開いている日程が存在するので、それにも気が付きたいところです。
今回はとりあえず第一歩として、認証がいらない体育館の予約が空いている日を教えてくれる機能を実装しようと思いました。
そう、あの時の私はそう思っていたんだ・・・

How

下記のライブラリたちを利用してスクレイピングしました。

cheeriojs/cheerio · GitHub
jQuery Likeに使えるHTMLパーサ

request/request · GitHub
HTTP Client

bnoordhuis/node-iconv · GitHub
Node.js用の文字コード変換ツール

erikdubbelboer/node-sleep · GitHub
Node.js用のスリープライブラリ

caolan/async · GitHub
同期処理を行えるようにするライブラリ

予約が空いているかどうかはログインしていなくてもできるので、単なるGETリクエストのオンパレードになると思っていました。
現実は厳しく、全ての遷移がPOSTリクエストで文字コードはShift-Jisでした。
あひるの空の九頭龍高校の監督の言葉がよぎりましたが、私は限界です。


スクレイピングのコード

とはいえちょっとは頑張ったので軽くどうやったか書こうと思います。

var req = require('request');
var cheerio = require('cheerio');
var iconv = require("iconv");
var async = require("async");
var sleep = require("sleep");

// 遷移していくページに必要なPOSTパラメータを返す無名関数の配列
var pageOptions = [
    function() {
        return {
            uri:"...",
            form:{
                TOKEN_KEY:"",  //あと7個ぐらいのパラメータ
                ...
            },
            encoding:null
        };
    },
    function(token) {
        return {
            uri:"...",
            form:{
                TOKEN_KEY:token,
            }
        };
    },
    ...
];

// 今訪れたページのHTMLから次のページ遷移に必要なTOKENを取得する手続き
var getToken = function(topPageSrc) {
    var $ = cheerio.load(topPageSrc);
    return $('input[name=TOKEN_KEY]').val();
};

// 今訪れたページに来たときにセットされたCookieをClientにセットする手続き
var setCookie = function(topPageResponse) {
    var setCookies = topPageResponse.caseless.dict['set-cookie'];
    for (var i=0; i < setCookies.length; ++i) {
        req.cookie(setCookies[i]);
    }
};

// ・・・
var toShiftjis = function(v) {
    var i = new iconv.Iconv('UTF-8//TRANSLIT//IGNORE', 'shift-jis');
    return i.convert(v).toString();
}

// ・・・
var toUtf = function(body) {
    var conv = new iconv.Iconv('shift-jis','UTF-8//TRANSLIT//IGNORE');
    return conv.convert(body).toString();
};

var next, token = "", res = "",body = "", first = true;
async.series(pageOptions.map(function(v) {
// async.seriesにcallback関数を第一引数とした無名関数の配列を渡します。
    return function(callback) {
        next = v(token);
        if( !first ) {
            sleep.sleep(1);
        }
        first = false;
        req.post(next, function(e, r, b) {
            body = toUtf(b);
            token = getToken(body);
            setCookie(r);
            callback(); // callback関数が呼ばれるとasync.seriesの次の関数が呼ばれます。
        });
    };
}), function(err, result) {
    console.log(body);
});

pageOptionsの配列の順序に沿ってページ遷移していくプログラムが出来ました。
実際に実行したらこんな感じになりました。

<table width="500" border="0" cellpadding="0" cellspacing="0" class="txt16">
    <tr>
        <td style="color:#CC0000;font-weight:bold;">ソスVソスXソスeソスソスソスGソスソスソス[ソスソスソスソスソスソスソスソスソスワゑソスソスソスソスB</td>
    </tr>

    <TR>
        <TD class="MBATR" align="left">ソス¥ソスソスソスゑソスソスワゑソスソスェ、ソスナ擾ソスソスソスソス迹ソスソスソスソスソスソスソス闥シソスソスソストゑソスソスソスソスソスソスソスソスB</TD>
    </TR>

    <TR>
        <TD class="MBATR" align="left">ソスuソスソスソスEソスUソスフ「ソス゚ゑソスソスvソス{ソス^ソスソスソスソスソスgソスpソスソスソス黷スソス鼾ソスAソスソスソスフソスソスbソスZソス[ソスwソスソスソス¥ソスソスソスソスソスソスソスソスソス鼾ソスソスソスソスソスソスソスワゑソスソスB</TD>
    </TR>

    <TR>
        <TD class="MBATR" align="left">ソスuソスソスソスEソスUソスフ「ソス゚ゑソスソスvソスヘ使ソスpソスソスソスネゑソスソス謔、ソスノゑソスソス閧「ソスソスソスソスソスソスソスワゑソスソスB</TD>
    </TR>

    <TR>
        <TD height="80" align="center"><p>&nbsp;</p><BR></TD>

    </TR>
    <TR>
        <TD align="center">ソス゚ゑソスソス{ソス^ソスソスソスソスソスNソスソスソスbソスNソスソスソスソスソスニソスソスCソスソスソスソスソスjソスソスソス[ソスノ戻ゑソスソスワゑソスソスB</TD>
    </TR>
</table>
雑感

_人人人人人_
> ソスソスソス <
 ̄Y^Y^Y^Y ̄
ソスソスソスってなんだよ!▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ
スクレイピングなんてするもんじゃないですね。
予約システムもWebAPI用意とかしてくれないかな・・・

3. 抽選結果を確認する

次に体育館の抽選結果の話です。
抽選結果が出ると、登録してある私の個人メールアドレスにメールが届くのですが、いちいちメールを開きないのでSlackに通知させます。

hubotでgmailのメールを取得したりパースしたりするのめんどいのでIFTTTを利用しました。(完全にbot関係ない)
そしたら文字化けしたので、ZapierというIFTTTに似たサービスを利用しました。
く・・・文字コードめ。

zapier.com

4 (Optional)先払いで体育館代を支払いに行く(物理)

特定の区だと、現地まで行かなければ支払いができません。
なぜ銀行振込とかができn・・・げふんげふん。
仕方ないので下記のようなサービスを利用しましょう。

https://www.uber.com/ja/cities/tokyo

錬成

最後に 起動 錬成です。
コマンドとしては下記のようなコマンドを用意してます。
Makefileインターフェイスとしてとても使いやすいです。
起動は↓のようなコマンドで行えます。

$make start credential={path to credential}
node=$(shell which node)
npm=$(shell which npm)
credential=./credentials/development

install:
	$(npm) install

start:
	./bin/hubot-slack $(credential)


run:
	$(node) ./resources/main.js
  • bin/hubot-slack
#! /bin/sh

trap 'kill $(jobs -p)' EXIT
file=${1:- "./credentials/sample"}
. $file; \
    export HUBOT_SLACK_TOKEN; \
    export HUBOT_SLACK_TEAM; \
    export HUBOT_SLACK_BOTNAME;

./bin/hubot --adapter slack
  • credential/sample
HUBOT_SLACK_TOKEN={slack token}
HUBOT_SLACK_TEAM={slack team name}
HUBOT_SLACK_BOTNAME=harukosan


接続が切れた時などにプロセスが突然死を遂げることがあるので、私はSupervisorを利用していたりします。
その辺りは他の記事書いたのでもしよかったら見てください。

arata.hatenadiary.com

これでサークルの雑務をしてくれるマネージャーbotが錬成されました。
楽になると良いなぁ・・・

まとめ

  • バスケサークルは外部の方の参加も募集してたりします。参加されたい方は是非Twitter等でお声がけください。
  • 人がやらなくても良い仕事はbotに任せ、誰しもが気軽に代表を出来るようにしましょう。
  • この後Chrome拡張に逃げる気持ちになったのですが、その話はまた今度。
  • 予約システムのスクレイピングには断固たる決意が必要なんだ。
  • スクレイピング対策としてセッションに全ての状態をもたせ、POSTパラメータを大量にしましょう。安西先生も諦めてくれると思います。
  • 私は、あひるの空だと里見西高校が一番好きです。
  • あと、あひるの空の言葉だとニノの「名前は轟くもんやろ」が一番好きです。

明日のアドベントカレンダーは同期の @karahiyo_n による記事です。
乞うご期待。

Darkを支える技術 #3 テスト編

この記事は Dark - Developers at Real Kommunity Advent Calendar 2015 - Adventar の8日目として書かれています。

こんにちは。たなあら (@ara_ta3) | Twitter です。
もうちっとだけ続くんじゃ。

Darkというコミュニティアドベントカレンダーとして書いています。
前置きは #1 の日記を参考に・・・\(^o^)/arata.hatenadiary.com

DarkではSlackを利用していて、そこにはDarkというHubotが存在します。
今日はテスト周りについて話そうと思います。
Dark botは公開していて、だれでもPR投げれるんですが(たぶん)、テストがコケてるのにマージしてしまって動かなくなってしまうと、dark poemを打った返事がなくなってしまい心が廃れてしまいます。
だからテストがコケてる状態でマージできないように設定するのはとても大事です。github.com

※dark poemについてはこちらのちょっとふざけてる記事を御覧ください。

Q. じゃあどうやってるの?
A. テストの実行をTravis CI上で行い、github上でTravis CIがコケてるとマージできないように設定しています。

Travis CIの設定

設定している内容はこの程度です。

language: node_js
node_js: 
    - 5.1.0

Travis CIでlanguage: node_jsの設定を行うと、デフォルトでテスト時にnpm installnpm testが走ります。
設定はこちらを参考に。
Building a Node.js project - Travis CI

そしてnpm test実行時にどんなコマンドが走るかの設定はpackage.jsonで指定することができます。
dark-botでは make install && make test のように設定しています。

make install && make test ??

dark-botではMakefileでnpm等のコマンドをラップしています。

Makefile

token=
team=
name=

npm=$(shell which npm)
mocha=./node_modules/.bin/mocha
lint=./node_modules/.bin/coffeelint
gulp=./node_modules/.bin/gulp
monitoring-code=local
credential=./credentials/development

.PHONY:test

all: install

install:
	$(npm) install
	test -f settings/poems.json || cp settings/poems.json.sample settings/poems.json
	test -f settings/relayblog.json || cp settings/relayblog.json.sample settings/relayblog.json

start:
	./bin/hubot-slack $(credential) --monitoring-code=$(monitoring-code)

start-local:
	./bin/hubot

test-watch:
	$(gulp) watch

test: lint config-check
	$(mocha) --compilers coffee:coffee-script/register --recursive -R spec
	test -f settings/hello.json
	test -f settings/poems.json
	test -f settings/relayblog.json

lint:
	$(lint) scripts -f lintconfig.json

config-check:
	./bin/hubot --config-check

run-new-channels:
	./bin/start-new-channels $(credential)

これによってスクリプトのLINTとhubotの設定確認とmochaによるテストとファイル存在チェックが走ります。
Makefileはいずれかのコマンドの終了コードが1になると途中で失敗として終了してくれるので便利です。

Githubの設定

Github上でTravis CIのテストがこけたらMerge出来ないように設定します。
コードが置いてあるところのSettingsからBranchesを選び、Choose Branchesから守りたいブランチを選ぶと下記のようになります。
ここで、Protect this branchRequire status checks to pass before mergingを選び、status checkの方法としてTravis CIを選べば、PRマージのタイミングでTravis CIが通ってないとマージできないように設定することが出来ます。
べんり!

f:id:arata3da4:20151207204130p:plain

まとめ

  • githubレポジトリを守るためにテスト大事
  • Travis CIの設定かなり楽
  • そろそろネタ切れ
  • 次回はたぶんデプロイ周りの話
  • 明日のDark AdventCalendarはanoworl 氏による記事です。乞うご期待!

Darkを支える技術 #2 Hubotの死活管理について

この記事は Dark - Developers at Real Kommunity Advent Calendar 2015 - Adventar の7日目として書かれています。

こんにちは。たなあら (@ara_ta3) | Twitter です。
Darkのアドベントカレンダーに絶対に空きは作りません。
酔っ払った勢いで書ける唯一のアドベントカレンダーとして書き続けます。
こう言い続けられるのも後何日でしょうか。


Darkというコミュニティアドベントカレンダーとして書いています。
前置きは #1 の日記を参考に・・・warata.hatenadiary.com


DarkではSlackを利用していて、そこにはDarkというHubotが存在します。
今日はその死活管理について話そうと思います。
Dark botはさくらVPSで動かしてるのですが、たまに接続が切れてプロセスがそのまま死ぬことがあります。つらい。
Dark botはpoemを詠んでくれたりするのですが、dark poemと打ったのに返事がないと心がさらに廃れてしまいます。
dark poemについてはこちらのちょっとふざけてる記事を御覧ください。arata.hatenadiary.com

なので、Dark botの死活管理はとても重要です(たぶん

Q. じゃあなにやってるの?
A. やっていることは2つです。

  • supervisord によるプロセス管理
  • mackerel によるプロセス監視

supervisord によるプロセス管理

supervisordのインストールは省きますが、設定はとても簡単です。

$cat /etc/supervisord/darkbot.conf
[program:dark]
command=make -C /path/to/dark-bot start credential=/credentials/dark-bot-for-ngineerxiv monitoring-code=dark-bot-ngineerxiv
stdout_logfile=/var/log/hubot/dark-hubot.log
stderr_logfile=/var/log/hubot/dark-hubot.log.err
autorestart=true
autostart=true
numprocs=1

commandのところに起動スクリプトのコマンドを書いています。
これを書いてsupervisordのserviceを起動すれば、死んでも勝手に蘇る闇が・・・ごほんごほん、Dark botが出来ます。
人が頑張らなくても勝手に起動するので楽でいいですね。
monitoring-codeというオプション(?)はmackerelで監視するようのものとして付け加えています。

mackerel によるプロセス監視

こちらもとても簡単です。

$cat /etc/mackerel-agent/mackerel-agent.conf

# Configure
pidfile = "/var/run/mackerel-agent.pid"
root = "/var/lib/mackerel-agent"
verbose = true
apikey = "hogehoge"
diagnostic = true

[host_status]
on_start = "working"
on_stop  = "poweroff"

[plugin.checks.dark-ngineerxiv]
command = "/usr/local/bin/check-procs --pattern dark-bot-ngineerxiv -C 1"
...

設定についてはヘルプがしっかりしているのでそちらがわかりやすかったです。

help-ja.mackerel.io

mackerelはSlackへの通知もしてくれるので便利ですね。
New Relicとかもできるのかな。

help-ja.mackerel.io

これで万が一、supervisordが変な感じになってたとしてもそれに気がつけそうです。
supervisordもmackerelも死んでたらもう仕方ないですね。
そこまでのSLAはいいかなって思ってます。

まとめ

  • プロセス管理もプロセス監視もなんとなく簡単にできてよかった。
  • 明日の投稿者いないけどどうしようwポエムでも書こうかな。
  • 少なくとも明後日の投稿者は anoworl - Adventar 氏による記事です。乞うご期待。