行ってきた
今は会社でメルカリチャンネルというサービスのチームにいるのだが、そこでFirebaseを利用している。
その関係で出張として行かせてもらえることになった。ありがとうございます弊社。
技術関連は会社ブログに書くのでその他雑感だけ。
続きを読む今年は前職の先輩と現職の先輩と組んで参加しました。
予選突破の20万台には遠く及ばず…
点数のブレが大きすぎてどの対策が効いたか全然わからなかったけど時系列でやったこととかやりたかったけど諦めたことをメモる。
私の担当はアプリケーションだった。
(user.name)
(message.id, message.channel_id)
(haveread.user_id, haveread.channel_id)
SELECT *
の撲滅get_channel_list_info
の置き換え
COUNT(*)
をCOUNT(1)
にCOUNT
せずにSELECT
するだけでよくなったxdebug.so
, お前のことやでN+1
をいくつか潰す他のお二人には
とか諸々やってもらった。感謝…
/register
, /login
ページを静的コンテンツに
select * from user
してるのとかLIMIT
, OFFSET
殺す他にも色々アイディアはあって
なんて案もあったけどとにもかくにもボトルネックが/icons
から他に移らなくて死んだ。
とにもかくにも/icons
のリクエストを捌けて無くてしんどかった。
一番最初はDBが完全にサチっててその壁は一瞬で越えられたけどその後すぐに画像が死んでることが分かってなるほどという感じだった。
画像の脱DB化をしてもそこまでスループットが上がらず、なかなか苦しかった。
今回はそこを抜けたチームが10万の壁を越えていったのではという推測。
スギャブロエックスチームと同じ会場で解いてたので効いたところCache-Control
ヘッダあたりをいじって2回目以降リクエストさせないようにしてたらしい。ぐぬぬ…
一昨年、昨年参加して今年3回目だけど計測してボトルネック特定して確実に芽をつぶしていく力はついていると感じてて、それゆえに結構悔しい。
あとはぱっと直したコードにバグがあることが多くて結構時間を取られた。
今回はほぼ何も準備できなかったけど来年は手元で簡単にアプリが動かせるような環境を作る秘伝のタレを用意してそこでデバッグしたい。
来年こそ100万円使って温泉行きたいです。
次は東京Node学園2017に登壇する予定です。
JavaScript系で大きなカンファレンスは初めてなので頑張るぞ〜〜〜
例えばこんなクラスコードがあるとする。
<?php namespace App; use Psr\Log\LoggerInterface; use App\Util\DB; use App\Exception\DbException; class BlogService { /** @var DB */ protected $db; /** @var LoggerInterface */ protected $logger; public function __construct(DB $db, LoggerInterface $logger) { $this->db = $db; $this->logger = $logger; } public function createNewPost(int $userId, string $title) { $this->db->begin(); try { $postRow = $this->db->insert('posts', [ 'title' => $title, ]); $this->db->insert('post_user_relations', [ 'user_id' => $userId, 'post_id' => $row['post_id'], ]); $this->db->commit(); } catch (DbException $e) { $this->logger->warning($e->getMessage()); $this->db->rollback(); throw $e; } return $postRow['post_id']; } }
ブログを投稿する簡単なクラス。
INSERT文を2つ発行するのでトランザクションを貼って、もしデータベース例外が発生したらloggingした上で例外を投げ直すというよくあるコード。
これに対してテストコードをPHPUnitで書いてみる。
スタンダードに以下のようなテストケースを書いてみる。
上記のクラスはきれいにDIパターンで実装されているのでいずれのテストもモックを注入し、振る舞いを指定することでロジックの保証をするテストが書ける。
後者のテストを実装するならこんな感じ。
<?php namespace App; use Mockery as m; use Psr\Log\LoggerInterface; use App\Util\DB; use App\Exception\DbException; class BlogServiceTest { /** @var DB */ protected $db; /** @var LoggerInterface */ protected $logger; public function setUp() { $this->db = m::mock(DB::class); $this->logger = m::mock(LoggerInterface::class); } /** * @expectedException DbException */ public function testCreateNewPostSholdThrowExceptionWithLogging() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $this->db->shouldReceive('rollback')->once(); // Rollbackすることをテスト $this->logger->shouldReceive('warning')->once(); // Loggingすることをテスト $SUT = new BlogService($this->db, $this->logger); $SUT->createNewPost(1235, 'title'); } }
色々はしょってるけどだいたいこんな感じになると思う。
ただ、現実のサービスはこんな単純な仕様であることはほぼない。
例えばここに追加仕様で「例外を投げたらloggingし、rollbackし、エラーをテキストファイルに吐き出すようにする」みたいなものが来たとする。(いい例えが思いつかなかっただけで、現実的にはもっと別のケースがあるでしょう)
こんなコード。
<?php /** ~ */ public function createNewPost(int $userId, string $title) { $this->db->begin(); try { $postRow = $this->db->insert('posts', [ 'title' => $title, ]); $this->db->insert('post_user_relations', [ 'user_id' => $userId, 'post_id' => $row['post_id'], ]); $this->db->commit(); } catch (DbException $e) { $this->logger->warning($e->getMessage()); $this->db->rollback(); // ここが追加 file_put_contents('path/to/file.txt', json_encode($e)); throw $e; } return $postRow['post_id']; }
これに対して先程のテストコードだと「エラーをテキストファイルに吐き出す」ことの確認ができない。
しかしファイル書き込みロジックは外部注入していないのでモックすることもできない。
そこでこんなテストを書いてみるとする。
<?php namespace App; use Mockery as m; use Psr\Log\LoggerInterface; use App\Util\DB; use App\Exception\DbException; class BlogServiceTest { /** @var DB */ protected $db; /** @var LoggerInterface */ protected $logger; public function setUp() { $this->db = m::mock(DB::class); $this->logger = m::mock(LoggerInterface::class); } public function testCreateNewPostSholdThrowExceptionWithLogging() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $this->db->shouldReceive('rollback')->once(); // Rollbackすることをテスト $this->logger->shouldReceive('warning')->once(); // Loggingすることをテスト $SUT = new BlogService($this->db, $this->logger); // 例外投げられるとチェックできないからcatchする try { $SUT->createNewPost(1235, 'title'); } catch (DbException $e) { $this->assertTrue(file_exists('path/to/file.txt')); return; } $this->fail(); } }
例外を投げられた後だとプログラムが続行できないので例外をcatchしてassertionする。
また、例外をcatchできなかったらテストをコケるようにして例外を投げることを保証する。
これで一件落着、と思いきやこの書き方には2つ問題がある。
前者に関しては以下のようなテストの書き方をしてると起きる。
<?php // 例外投げられるとチェックできないからcatchする try { $SUT->createNewPost(1235, 'title'); } catch (\Exception $e) { $this->assertInstanceOf(DbException::class, $e); $this->assertTrue(file_exists('path/to/file.txt')); return; }
こんな書き方しなきゃいいじゃんって話なんだけど、時たまやるしテストやロジックが複雑化していけばなおさら起きる。
とはいえこういうことは往々にして起きるのでもう少し違うアプローチを考えることにした。
シンプルにテストを分けてコケたときのエラートラッキングを分けるようにする。
具体的には「意図した例外が投げられること」と「例外が投げられた後の状態が望みどおりであること」の2ケースに分ける。
イメージ的にはこんな感じ。
<?php namespace App; use Mockery as m; use Psr\Log\LoggerInterface; use App\Util\DB; use App\Exception\DbException; class BlogServiceTest { /** @var DB */ protected $db; /** @var LoggerInterface */ protected $logger; public function setUp() { $this->db = m::mock(DB::class); $this->logger = m::mock(LoggerInterface::class); } /** * @expectedException DbException */ public function testCreateNewPostSholdThrowExceptionWithLogging() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $this->db->shouldReceive('rollback')->once(); // Rollbackすることをテスト $this->logger->shouldReceive('warning')->once(); // Loggingすることをテスト $SUT = new BlogService($this->db, $this->logger); $SUT->createNewPost(1235, 'title'); } public function testCreateNewPostSholdPutFileAfterThrowException() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $SUT = new BlogService($this->db, $this->logger); try { $SUT->createNewPost(1235, 'title'); } catch (DbException $e) { // 例外をthrow後の状態をテストする $this->assertTrue(file_exists('path/to/file.txt')); } } }
こうすることで意図しない例外が発生しても原因の切り分けが多少やりやすくなる。
また、もう少し改善する点としてこの2ケースは前者がコケた時点で後者をテストする必要がない。
そこで@dependsアノテーションを利用して前者がコケたら後者がコケるようにする。
<?php namespace App; use Mockery as m; use Psr\Log\LoggerInterface; use App\Util\DB; use App\Exception\DbException; class BlogServiceTest { /** @var DB */ protected $db; /** @var LoggerInterface */ protected $logger; public function setUp() { $this->db = m::mock(DB::class); $this->logger = m::mock(LoggerInterface::class); } /** * @expectedException DbException */ public function testCreateNewPostSholdThrowExceptionWithLogging() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $this->db->shouldReceive('rollback')->once(); // Rollbackすることをテスト $this->logger->shouldReceive('warning')->once(); // Loggingすることをテスト $SUT = new BlogService($this->db, $this->logger); $SUT->createNewPost(1235, 'title'); } /** * @depends testCreateNewPostSholdThrowExceptionWithLogging */ public function testCreateNewPostSholdPutFileAfterThrowException() { $this->db->shouldReceive('insert')->andThrow(new DbException()); $SUT = new BlogService($this->db, $this->logger); try { $SUT->createNewPost(1235, 'title'); } catch (DbException $e) { // 例外をthrow後の状態をテストする $this->assertTrue(file_exists('path/to/file.txt')); } } }
これで完璧!!
テストを書くのは楽しいね。