Интеграционные тесты на дедлоки и одновременные запросы

Если вы так же озабочены тестированием как и я, то вы возможно сталкивались с проблемами дедлоков при транзакциях. Транзакции в БД дело хорошее, особенно на все REST-запросы, т.к. он становится атомарной операцией. Однако если вы вместе с этим затрагиваете часто используемую таблицу или операция происходит на файлах или с другими сетевыми запросами, то вы можете вызвать дедлок.

MySQL/InnoDB дедлок возникает из-за двух транзакций пытающихся в разном порядке изменить одни и те же данные. Если вы используете триггеры, то вам в коде даже не надо явно объявлять о транзакции — любой UPDATE с триггером потенциально опасен дедлоком. При этом уровень изоляции транзакции не влияет на вероятность его получения

Детали в базе можно увидеть так:

SHOW ENGINE InnoDB STATUS;
------------------------
LATEST DETECTED DEADLOCK
------------------------
2016-10-21 08:21:41 0x7f772b726700
*** (1) TRANSACTION:
TRANSACTION 9651, ACTIVE 0 sec starting index read
mysql tables in use 3, locked 3
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 1052, OS thread handle 140149807552256, query id 128292 localhost 127.0.0.1 tactic updating
UPDATE folder c SET (...)
*** (2) TRANSACTION:
TRANSACTION 9650, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
8 lock struct(s), heap size 1136, 183 row lock(s), undo log entries 1
MySQL thread id 1048, OS thread handle 140149806753536, query id 128291 localhost 127.0.0.1 tactic Sending data
UPDATE folder c SET (...)

Обычно советуют в случае дедлока перезапускать всю операцию, но это может усложнить весь метод.

Если вы не хотите усложнять себе жизнь обработкой одновременных запросов, можно сделать лок на весь POST-запрос, скажем используя memcache.

Так или иначе, в итоге вы хотите протестировать что два или более одновременных REST-запроса на один и тот же URL не упадут используя PHPUnit. В php это реализуется методом curl_multi_init:

function TwoConcurrentSaves_ShouldNotCauseError() {
        $urls       = [
            'http://kurapov.ee/file/save',
            'http://kurapov.ee/file/save',
        ];
        $node_count = count($urls);
        $chs    = [];
        $master = curl_multi_init();
        for ($i = 0; $i < $node_count; $i++) {
            $url = $urls[$i];
            $ch  = curl_init($url);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, ['collectionID' => 13, 'quality'=> 90]);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_multi_add_handle($master, $ch);
            $chs[$i] = $ch;
        }
        do {
            curl_multi_exec($master, $running);
        } while ($running > 0);
        for ($i = 0; $i < $node_count; $i++) {
            $response = curl_multi_getcontent($chs[$i]);
            $this->assertNotContains('Serialization failure', $response);
            $this->assertNotContains('Deadlock found', $response);
        }
    }