Sinon.JSのuseFakeTimersを教えてもらった

ということがあって、HipChat上に投げたら、sinon.useFakeTimersつかってないの?というツッコミがあったので、そんなのあるんだと。ちょっと使ってみました。 あら便利

http://sinonjs.org/ を見てみると JavaScript test spies, stubs and mocks.らしい。ちなみにバージョンは1.14.1で動かしてます。

var sinon = require('sinon');
describe('test', function() {
  describe('sub-test', function() {
    var sandbox;
    beforeEach(function(done) {
      sandbox = sinon.sandbox.create(); // sandbox でまとめて管理
      sandbox.useFakeTimers(new Date(2000, 0, 1).getTime());
      done();
    });

    it('call new Date', function(done) {
      console.log(new Date());
      done();
    });

    it('call new Date2', function(done) {
      console.log(new Date());
      done();
    });

    afterEach(function(done) {
      sandbox.restore();
      console.log('afterEach', new Date());
      done();
    });
  });
});

で動かすと、

  test
    sub-test
Sat Jan 01 2000 00:00:00 GMT+0900 (JST)
      ✓ call new Date
afterEach Thu May 21 2015 23:02:23 GMT+0900 (JST)
Sat Jan 01 2000 00:00:00 GMT+0900 (JST)
      ✓ call new Date2
afterEach Thu May 21 2015 23:02:23 GMT+0900 (JST)


  2 passing (14ms)

TypeScriptでNodeのWebアプリを書き換え中

今年頭からNode.jsを本格的に初めてみて、JavaScriptの柔軟性の便利さと怖さに四苦八苦してしてます。と言っても、1/3ぐらいは負荷試験してたり、細々と色々なことしてたので、そこまで書いてたわけではないけど。

オブジェクトにどんどんプロパティを追加して柔軟にできたり、ちょこっと関数追加できたりと嬉しい恩恵もあるのですが、その一方でこの関数の引数/戻り値の型がわからなかったりして、TypeErrorを連発してしまって、Java1.4時代のClassCastExeceptionの時代に戻ってしまった思いをしてます。 自分はIDEを使ってるのですが、その恩恵が100%得られてないというお金的に損した気分。

で、まぁ自分のベースはJavaなので型がある言語だと嬉しいよね。ということで、ちょっと前からTypeScriptの勉強を始めてます。型以外にもES6ぽい書き方もできるし、TypeScriptが出力するJSコードを見つつ、JSの勉強もするという色々なことに使ってます。

GW中にガッツリ書き書き換えて久しぶりにコード書いて徹夜して、自分でもまだまだいけるんだと自己満足に浸っております

通称わかめ本を読みつつ、以前Nodeを勉強するときに作ったWebアプリをTypeScriptで書き換え中。大規模開発になってるので実際の新規プロダクションで入れられたらいいなー

今はまだ書き換えてる途中なので、所々お作法に沿ってなかったりします。 [TypeScript版] https://github.com/hachi-eiji/79school-2/tree/typescript [通常のJavaScript版] https://github.com/hachi-eiji/79school-2/tree/master

まぁ一番間違っているのは二重コールバックのコードをよく書いてしまってることだけどw

TypeScriptリファレンス Ver.1.0対応

TypeScriptリファレンス Ver.1.0対応

自分用のメモにもslackがいいのかもしれない

仕事用のメモで何かいいのないのかなーと色々試している途中

ユースケース

  • メモはほぼ毎日書く
  • 自分用のミーティング議事メモや、軽いプログラム(集計SQLとかコピペで実行するシェルとか)、コード書いてて気づいたことを書く。
  • 小さなタスクも書く
  • ホワイトボードをカメラで撮ることはある

必須要件

  • プロジェクト情報とか入るので、自分以外には見せたくない
  • マシンが複数台(会社と家に障害対応用マシン)あるので共有はしたい
  • メモの書き出しには日付入れてわかりやすくしたい

あったら嬉しい

  • 過去に書いたものを検索する
  • 可能な限りテキストベースでコピーとかしたい
  • vimコマンドでショートカットできると嬉しい
  • インターネットにつながってなくても使えると嬉しい
  • markdown でかけると助かる

やらなくていいこと

  • スマホからのアクセス
  • 自分用のメモなので細かいバージョン管理
試したもの 感想
テキストファイ DropboxとかGitを使って同期できる。
数年使っていたが、月ごとにファイル分けるのがだんだんめんどくさくなってくる。
Dropboxには他の大きなファイルがあるのでスペックが低い障害対応用マシンで同期するのはディスクサイズを食うので、GitHubを使ったが毎日pushするのめんどくさい
Evernote PC、スマホ用のクライアントアプリある
コピーするとHTMLタグ(?)を自動で設定するのですごい不便。
プロジェクトの仕様とかをガッツリまとめるには便利。1週間ぐらい使って止めた。
Kobito QiitaのMac版クライアントアプリ。有料プランにするとDropbox同期ができる。
markdownのリアルタイムプレビューもあるので、有料でもいいかなと思ってたが、1ヶ月(実際は20日営業日前後)書いてるとクライアントアプリが明らかに重くなった
wri.pe ノートを取るシンプルなWebサービス。markdownで記載できて、リアルタイムプレビューができる。auto saveもしてくれるのは嬉しい。
時系列に書くので、最新のものを一番上に書いているが、テキストエリア内の最初のフォーカスが一番下に来てるのがすごい惜しい。もし、フォーカスが一番下に来るのであれば、markdownプレビューも一番下に来てほしい。あとmarkdownのプレビュー文字が小さいのでもう少し大きくしてほしい。
Slack テキスト形式ではないものの投稿したら自動で時系列になる。コードスニペット書けるし、画像もアップできる。
タスクはstarで管理しておく。今のところあまり不満はない。
ちなみにChatWorkでもできるかなと思ったがSlackのほうが機能多かった。ただ、タスク機能は恐ろしく便利。社内のチャットツールがChatWorkだったら全部まとめてた。個人的にはHipChatよりChatWorkのほうが好き(期待値も込めて)

ということで、今のところSlackに落ち着いてる。もうすこし使ってみて、使い方あってなかったらwri.peにするかChatWorkにするかも。いいものがあれば教えてくれるとすごい嬉しいです

gulpを書くときのメモ

背景

今のプロジェクトでgulpの中身を見ることが何回かあって、そのたびに「こうしたらいいのになー」とか思うので、軽くまとめておく。見るたびに直したい衝動にかられるが、動いているのには理由があるし、今はそのフェーズでないのでグッと我慢する。もちろんgulpを書いてる人から見たら、全部gulpを見ればわかるとか、ファイル分割されているとか、いいところはあるんだろうけど。 当たり前のことを書いてるが、自分が忘れやすいのでメモってるだけ

今回のユースケース

実際のプロジェクトではもっとたくさんのことをやってるが今回は下記に絞る

  • ローカル環境もしくはJenkins上から呼び出す。
  • JavaScript(CSS)ファイルをminifyしたり、画像をリサイズする。

改善点メモ

  • 本番環境,開発環境でminifyする/しないをわけない。タスクをまとめた親タスクを作るか、呼び出し側で制御する。親タスク側ではminimistとかつかって、外部パラメータを渡す。開発環境は何台でも増えるし、いままでminifyしてなかった環境でminifyしたくなることもある
  • gulp-helpを使ってタスクの説明を書く。3日後の自分は他人 gulp-help
  • ファイルを分割しすぎない。gulpfile.js → ファイルA.js → ファイルB.js→ファイルA.jsに戻るみたいなことがあった。若干冗長でも2つ目ぐらいで止めておきたい

なんかこんな感じ

Sample GulpFile

$ gulp help
[00:52:46] Using gulpfile ~/development/gulp-test/gulpfile.js
[00:52:46] Starting 'help'...

Usage
  gulp [task]

Available tasks
  help       Display this help text.
  minify-js  JavaScriptをMinifyします

[00:52:46] Finished 'help' after 1.5 ms

UNIXという考え方の本はこういうときも役に立つ

UNIXという考え方―その設計思想と哲学

UNIXという考え方―その設計思想と哲学

MongoDBのindex

indexの作成

db.collection_name.ensureIndexで作成できる。この時に第2引数のoptionsにbackgroundを入れること。デフォルトは background:falseなので、index作成完了までデータベースロックがかかるため
ちなみにバックグラウンドを有効にした場合でも、シェルセッションの応答まではバックグラウンドにならない.
なお、性能はバックグラウンドで作成したほうが遅い。
バックグラウンドで作成するときは,primary終了後にsecondaryへindex作成が行われるので、primary -> secondaryの順番で作成する。
secondaryで大きなindexを作成するときは、一度secondaryを外したのちに単独でindexを作成する.再度レプリカセットに入れるときに他のセカンダリにキャッチアップできるようにしてから、stepDownでプライマリを切り替える。Build Indexes on Replica Sets — MongoDB Manual 3.0.1に書いてあったけど、変更したReplica SetのステータスがRECOVERINGで止まった。(別セッションでinsertしてるからどんどんprimaryとずれていくのはわかるけど...)
この辺は詳しい方に教えていただきたい

複合index

{ "item": 1, "location": 1, "stock": 1 } というindexを貼った場合は、検索条件でindexを使わない場合がある(基本MySQLと同じかな)

検索条件 index使う?
item o
item, location o
location, item o
item, location, stock o
item, stock o (ただし、item,stockでindexを貼ったほうが効果はある)
location x
stock x
location, stock x

sortでindexを使う場合

{a:1, b:1}というindexを貼った場合はクエリの条件で { a: 1, b: 1 },{ a: -1, b: -1 }とした場合にはindexは見るが、{b:1, a:1},{a:-1, b:1}の場合は見ない。検索条件とsortを同時に使用する場合は、作成したindexの並び順通りになる。(MySQLとかと同じだとおもう)

Index Intersection

1クエリで複数indexを使うようになった.複合indexとの性能の違いはあまり見つけられず...

Sparse Indexes

指定されたプロパティが存在しない時に無視されるindex。今のところ通常indexと混在すると色々面倒そうな気がするので、あまり使い道が出てこない

mongos> db.scores.save({userid:"newbie"});
WriteResult({ "nInserted" : 1 })
mongos> db.scores.save({userid:"abby", score: 82});
WriteResult({ "nInserted" : 1 })
mongos> db.scores.save({userid:"nina", score: 90});
WriteResult({ "nInserted" : 1 })
mongos> db.scores.ensureIndex({score :1}, {sparse:true});
{
	"raw" : {
		"ip-10-0-0-16.ec2.internal:27017" : {
			"createdCollectionAutomatically" : false,
			"numIndexesBefore" : 1,
			"numIndexesAfter" : 2,
			"ok" : 1
		}
	},
	"ok" : 1
}
mongos> db.scores.find( { score: { $lt: 90 } } )
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
mongos> db.scores.find().sort( { score: -1 } )
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("54a798805a355747582911e0"), "userid" : "newbie" }
mongos> db.scores.find().sort( { score: 1 } )
{ "_id" : ObjectId("54a798805a355747582911e0"), "userid" : "newbie" }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
mongos> db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }

mongos> db.scores.ensureIndex({score :1});
{
	"raw" : {
		"ip-10-0-0-16.ec2.internal:27017" : {
			"createdCollectionAutomatically" : false,
			"numIndexesBefore" : 1,
			"numIndexesAfter" : 2,
			"ok" : 1
		}
	},
	"ok" : 1
}
mongos> db.scores.find( { score: { $lt: 90 } } )
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
mongos> db.scores.find().sort( { score: -1 } )
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("54a798805a355747582911e0"), "userid" : "newbie" }
mongos> db.scores.find().sort( { score: 1 } )
{ "_id" : ObjectId("54a798805a355747582911e0"), "userid" : "newbie" }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
mongos> db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )
{ "_id" : ObjectId("54a798995a355747582911e2"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("54a7988e5a355747582911e1"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("54a798805a355747582911e0"), "userid" : "newbie" }

シャーディングの復習

シャーディングの種類

  • Range Base sharding
  • Hash Base Sharding
    • 全部のshardに分散されるので均一的にデータが入る
    • 範囲検索するとおそらくすべてのshardにアクセスするとおもうので微妙
    • データ一本釣りするときは有効かもしれない。(例:ユーザ自分自身のデータを引っ張ってくるときとか)
  • Tag Aware Sharding
    • 「タグ」をshard keyにするぽい

下2つは2.4以降に追加されたけど、今のところうまい使い道が出てこないのが残念なところ

シャーディングをくむ

基本MongoDBのSharding機能で遊ぶ - 個人的なまとめで作ってる通りです。今回は設定ファイルをyamlファイルにしたぐらい。

コレクションの初期配置のshardingを変える

sh.shardCollection でシャーディング対象のコレクションを追加した後に、moveChunkするだけ。最初のマイグレーションが走る前はprimary shardに入るので、こうやってprimary shardにデータが偏らないようにします。

mongos> sh.shardCollection("test.fuga",{id:1})
{ "collectionsharded" : "test.fuga", "ok" : 1 }

mongos> printShardingStatus()
--- Sharding Status ---
  databases:
	{  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
		test.fuga
			shard key: { "id" : 1 }
			chunks:
				shard0000	1
			{ "id" : { "$minKey" : 1 } } -->> { "id" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 0)


mongos> sh.moveChunk("test.fuga", {id: {$minKey:1}}, "shard0001");
{ "millis" : 82, "ok" : 1 }

mongos> printShardingStatus()
--- Sharding Status ---
  databases:
	{  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
		test.fuga
			shard key: { "id" : 1 }
			chunks:
				shard0001	1
			{ "id" : { "$minKey" : 1 } } -->> { "id" : { "$maxKey" : 1 } } on : shard0001 Timestamp(2, 0)

チャンクを分割したい

データが集中してチャンクを分割して移動させたいときはsh.splitFind()で中央で分割させます。sh.splitAt()もありますが、均等な分割をさせたほうがマイグレーション的に優しいです。基本、MongoDBに任せたほうがいいのですが、運用途中で分割させないといけないケースが出てるので。必要であれば、moveChunkします。

mongos> printShardingStatus()
--- Sharding Status ---
		test.test_collection
			shard key: { "user_id" : 1 }
			chunks:
				shard0002	2
				shard0000	3
				shard0001	3
			{ "user_id" : { "$minKey" : 1 } } -->> { "user_id" : 0 } on : shard0002 Timestamp(7, 1)
			{ "user_id" : 0 } -->> { "user_id" : 5999 } on : shard0000 Timestamp(8, 1)

mongos> sh.splitFind("test.test_collection", {user_id: 0});
{ "ok" : 1 }

mongos> printShardingStatus()
--- Sharding Status ---
  databases:
	{  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
		test.test_collection
			shard key: { "user_id" : 1 }
			chunks:
				shard0002	3
				shard0000	3
				shard0001	3
			{ "user_id" : { "$minKey" : 1 } } -->> { "user_id" : 0 } on : shard0002 Timestamp(7, 1)
			{ "user_id" : 0 } -->> { "user_id" : 2999 } on : shard0002 Timestamp(9, 0)
			{ "user_id" : 2999 } -->> { "user_id" : 5999 } on : shard0000 Timestamp(9, 1)

コンソール触ってて気づいたこと

  • tabキーを押すと補完が効いたけど、昔からあった??
  • sh,rsから始まるコマンドあったっけ?db.runCommandで実行してたけど、いつから入ったんだろうか?
  • ctrl+rで過去に実行したコマンド検索できるようになってる

MongoDBのバックアップ周りを実際にやってみる

2年ぶりにMongoDBを触ることになったので、リハビリがてらReplica SetとSharding周りを触ってます。
とりあえず今日はReplica Setとバックアップ周り。
なぜバックアップまわりかというと、前に触ってた時は本当に基礎部分しかやってなかったので、バックアップ方法などはあまりチェックしてなかったためです。

日付が一番新しかったのが CyberAgentにおけるMongoDB のスライドだったので、この方法でやってみようかなと。(はてなダイアリーってslide shareって埋め込みできなかったのね)
arbiterとnovoteにしている理由がはっきりしなかったのですが、わざわざTwitterで回答を頂きました。ありがとうございました。


ということで実際にやってみます。

Replica Setをくむ

ReplicaSetで遊ぶ - 個人的なまとめ で Replica Setを組む方法はすでに知ってるのですが、MongoDB 2.6.6になってから色々変わっていると思ったので、再度ドキュメントを見なおしてみました。
そしたらconfigファイルのフォーマットがYAMLになっててびっくり。でも、これってなんか意味があるんだろうかと謎。まぁ、設定が構造として表現できるから楽になるのかな?ぐらい。

サーバ構成としては、3台サーバを用意して、その中の1台にarbiter用のmongodを設定しました。設定ファイルは下記にまとめてます。
最初にmongodを立ち上げた後に、arbiterを立ち上げて、rsコマンドでarbiterを追加するところまでは順調に行ったので、これで試せる状態になりました。
バックアップ対象のmongodを落としたあとに、他のprimaryやsecondaryを落とした時にもちゃんとサービスが継続できるかというのが確認ポイントです。
早速やってみる。

初期状態(rs.status()の一部抜粋)

id 状態 備考
1 SECONDARY
2 SECONDARY バックアップ対象のサーバ
3 PRIMARY
4 ARBITER

_id2のmongodを落とす

id 状態 備考
1 SECONDARY
2 (not reachable/healthy) バックアップ対象のサーバ
3 PRIMARY
4 ARBITER

ここで_id:1のsecondaryのプロセスを落とす

id 状態 備考
1 (not reachable/healthy)
2 (not reachable/healthy) バックアップ対象のサーバ
3 SECONDARY
4 ARBITER

あれ?全部secondaryになった ( ゚д゚)ポカーン
あ、novoteの設定入れてない。vote(投票権)の全体数の過半数の票を取らないといけないのに、全体の半数になったからだ orz
rs.reconfigで設定します。(reconfigを実行するとrs.stepDownが走るので気をつけましょう)

rs:SECONDARY> rs.config()
{
	"_id" : "rs",
	"members" : [
		{ "_id" : 1, },
		{ "_id" : 2, "votes" : 0 },
		{ "_id" : 3, },
		{ "_id" : 4, "arbiterOnly" : true }
	]
}
rs:SECONDARY> rs.status();
{
	"members" : [
		{ "_id" : 1, "health" : 1, "state" : 2, "stateStr" : "SECONDARY", },
		{ "_id" : 2, "health" : 1, "state" : 1, "stateStr" : "PRIMARY", },
		{ "_id" : 3, "health" : 1, "state" : 2, "stateStr" : "SECONDARY", },
		{ "_id" : 4, "health" : 1, "state" : 7, "stateStr" : "ARBITER", }
	],
}

再度、バックアップ対象サーバのmongodを落とそうと思ったのですが、stepDownが走った時に該当サーバがprimaryになってるじゃありませんか。
ということで、バックアップ対象サーバのpriorityを0にして、primaryにならないようにしました。
最初はprimaryにしない設定をした時に悪影響出るかなと思ったのですが、どちらにしてもバックアップ対象以外で2つ落ちたら、クラスタがダウンするので、こちらのほうがシンプルかなと思いました。(あえてmongodも冗長化するのであれば、novotesかつpriorityが0ではないsecondaryを追加するとかぐらいですかね...未検証ですが)
ちなみに2.6からvoteの値は1より大きい値を設定すると警告がでます。[http://docs.mongodb.org/manual/reference/replica-configuration/#local.system.replset.members[n].votes]

rs:PRIMARY> rs.config()
{
	"members" : [
		{ "_id" : 1, },
		{ "_id" : 2, "votes" : 0, "priority" : 0 },
		{ "_id" : 3, },
		{ "_id" : 4, "arbiterOnly" : true
		}
	]
}

最終的にはこういう状態でバックアップスタート

id 状態 備考
1 SECONDARY
2 SECONDARY バックアップ対象のサーバ votes:0(投票権がない), priority:0(primaryにならない)
3 PRIMARY
4 ARBITER

バックアップ対象サーバのmongodを落とす

id 状態 備考
1 SECONDARY
2 (not reachable/healthy) バックアップ対象のサーバ votes:0(投票権がない), priority:0(primaryにならない)
3 PRIMARY
4 ARBITER

id:1(secondary)のmongodを落とす

id 状態 備考
1 (not reachable/healthy)
2 (not reachable/healthy) バックアップ対象のサーバ votes:0(投票権がない), priority:0(primaryにならない)
3 PRIMARY
4 ARBITER

id:1のmongodを立ち上げた後、id:3(primary)のmongodを落とす

id 状態 備考
1 PRIMARY
2 (not reachable/healthy) バックアップ対象のサーバ votes:0(投票権がない), priority:0(primaryにならない)
3 (not reachable/healthy)
4 ARBITER

id:3のmongodを立ち上げた後、id4(arbiter)を落とす

id 状態 備考
1 SECONDARY
2 (not reachable/healthy) バックアップ対象のサーバ votes:0(投票権がない), priority:0(primaryにならない)
3 PRIMARY
4 (not reachable/healthy)

とりあえず、バックアップの設定はできた。本当はMMS: The easiest way to run MongoDB | Cloud managed MongoDB on the infrastructure of your choiceとか使うほうがいいんだろうけどなー

設定ファイル

mongod.conf

systemLog:
  destination: file
  path: "/var/log/mongodb/mongodb_s1.log"
  logAppend: true
storage:
 journal:
   enabled: true
 smallFiles: true
 dbPath: "/data/db/s1"
processManagement:
 fork: true
net:
 port: 27017
replication:
 oplogSizeMB: 10
 replSetName: rs

arbiter.conf

systemLog:
  destination: file
  path: "/var/log/mongodb/arbiter.log"
  logAppend: true
storage:
 journal:
   enabled: false
 smallFiles: true
 dbPath: "/data/arb"
processManagement:
 fork: true
net:
 port: 30000
replication:
 oplogSizeMB: 10
 replSetName: rs