Last active
October 4, 2018 23:43
-
-
Save shimabox/0615ee11912a0cb3a9b335a1006a19b2 to your computer and use it in GitHub Desktop.
APIの実行回数を呼び出し元で制限する-サンプル2 @see https://blog.shimabox.net/2018/10/04/restrict_the_execution_count_of_api_by_caller/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sample2; | |
/** | |
* 当クラスでは更新命令を監視し(実際には教えてもらうが) | |
* | |
* - 最初の命令から閾値(マイクロ秒)以下で処理が実行されている場合、 | |
* 閾値 - 前回の処理から今回の処理までの実行間隔 の値で usleep を挟む | |
* | |
* 処理を行う。 | |
* | |
* 閾値は、制限時間 / 制限時間内許可実行数 で求める。<br> | |
* 1秒間に100回という制約の場合、 1 / 100 なので 0.01 となる。 | |
* | |
* 監視している処理の実行間隔が上記の 0.01秒 以下で終わっている場合、<br> | |
* 1秒間に100回以上の処理が走ってしまう計算になる。 | |
* | |
* その為、前回の処理から今回の処理までの実行間隔が0.01秒 以下で終わっている場合は、<br> | |
* 閾値 - 前回の処理から今回の処理までの実行間隔 の値で usleepをかける。<br> | |
* ※ 閾値が 0.01, 前回の処理から今回の処理までの実行間隔が 0.009 だとしたら 0.001秒 sleep をかける | |
* | |
* 要は "x秒間にy回までの実行回数" にする為、均等に処理が実行されるということになる。<br> | |
* ※ 1秒間に100回という制約の場合、処理間隔が 0.01 秒で収まる ※ベストエフォート | |
* ※ 100回を1秒間で均等に行うというイメージ | |
* | |
* 1秒間に1回という制限を設けた際に、処理が0.01秒で終わるものでも10回監視した場合は<br> | |
* 9秒以上処理に時間がかかるという事になる。 | |
* | |
* インスタンス生成後、監視対象となる処理が実行される前に notify() を呼んで使う。 | |
* | |
* <code> | |
* // インスタンス生成<br> | |
* $executionController = new ExecutionController(); | |
* | |
* //----- 監視したい処理の前に呼ぶ ----- | |
* | |
* // notify() で通知する<br> | |
* $executionController->notify(); | |
* | |
* // 監視する処理<br> | |
* $target->exec(); | |
* </code> | |
*/ | |
class ExecutionController | |
{ | |
/** | |
* マイクロ秒単位 | |
* | |
* マイクロ秒とは、1秒の100万分の1 1秒は 1000000 | |
* | |
* @var int | |
*/ | |
const UNIT_OF_MICROSECONDS = 1000000; | |
/** | |
* デフォルトの制限時間 x秒間約y回までの xの部分 | |
* @var int|float e.g) 1, 0.1 単位は秒 | |
*/ | |
const DEFAULT_DEFINED_TIME_LIMIT = 1; | |
/** | |
* デフォルトの閾値内(決められた時間)内に許可する実行数 x秒間y回数発行可能の yの値 | |
* @var int | |
*/ | |
const DEFAULT_ALLOW_EXEC_CNT = 100; | |
/** | |
* 制限時間 x秒間約y回までの xの部分 | |
* @var int|float e.g) 1, 0.1 単位は秒 | |
*/ | |
private $definedTimeLimit = self::DEFAULT_DEFINED_TIME_LIMIT; | |
/** | |
* 制限時間内許可実行数 xx秒間〇〇回数発行可能の ○○の値 | |
* @var int | |
*/ | |
private $allowExecCnt = self::DEFAULT_ALLOW_EXEC_CNT; | |
/** | |
* 閾値 制限時間 / 制限時間内許可実行数 の値 | |
* @var int|float | |
*/ | |
private $threshold = 0; | |
/** | |
* 基準時間 | |
* @var string microtime() | |
*/ | |
private $referenceTime = null; | |
/** | |
* コンストラクタ | |
* | |
* 監視に必要な値を設定 | |
*/ | |
public function __construct() | |
{ | |
$this->setupForMonitoring(); | |
} | |
/** | |
* 通知 | |
* | |
* 監視させたい処理の前で呼ぶこと | |
* | |
* @return void | |
*/ | |
public function notify() | |
{ | |
// 基準時間がセットされていない場合、取得/set だけしておいて終了 | |
// 一番最初の呼び出しが該当する | |
if ($this->referenceTime === null) { | |
$this->referenceTime = microtime(); | |
return; | |
} | |
// 前回実行時点との実行間隔 | |
$elapsedTime = $this->elapsedTime(microtime(), $this->referenceTime); | |
/* | |
|---------------------------------------------------------------------- | |
| 前回の実行から閾値以下で実行されているかどうか | |
|---------------------------------------------------------------------- | |
| 例えば、1秒間に100回までと決められている場合、監視している処理の実行間隔が | |
| 0.01秒以下で終わっていると1秒間に100回以上実行されてしまうことになる。 | |
| そのため、実行間隔が閾値以下になってるかを判定する。 | |
| | |
| true であれば実行間隔が閾値以下で実行されていることになる | |
*/ | |
$isOverSpeed = $elapsedTime < $this->threshold; | |
// 実行間隔が閾値以下で実行されていた場合、閾値の間隔で処理が実行されるようにsleepを挟む | |
if ($isOverSpeed) { | |
// sleep | |
$this->_sleep((int)(($this->threshold - $elapsedTime) * self::UNIT_OF_MICROSECONDS)); | |
} | |
// 監視が終わったら基準時間を設定し直す | |
$this->referenceTime = microtime(); | |
} | |
/** | |
* sleep(usleep) 実行 | |
* @param int $microSeconds | |
* @return void | |
*/ | |
protected function _sleep($microSeconds) | |
{ | |
usleep($microSeconds); | |
} | |
/** | |
* 経過時間(マイクロ秒単位)を返す | |
* | |
* 当関数は microtime() の結果を引数で受け取る。 | |
* microtime(true)での計算を行うと計測精度が落ちるので、<br> | |
* なるべく正確に差分を測るために microtime() の結果から 整数部と小数部を取得し経過時間を計算している。 | |
* | |
* @link https://tgws.plus/prog/phpmicrotime/ | |
* @param string $now microtime()の結果 "msec sec" | |
* @param string $reference microtime()の結果 "msec sec" | |
* @return float | |
*/ | |
private function elapsedTime($now, $reference) | |
{ | |
list($nowMsec, $nowSec) = explode(' ', $now); | |
list($referenceMsec, $referenceSec) = explode(' ', $reference); | |
return ((float)$nowMsec - (float)$referenceMsec) + ((float)$nowSec - (float)$referenceSec); | |
} | |
/** | |
* 内部変数のリセット | |
* | |
* 内部変数の値をこのクラスが持つ定数の値でリセットします | |
* @return void | |
*/ | |
public function reset() | |
{ | |
$this->definedTimeLimit = self::DEFAULT_DEFINED_TIME_LIMIT; | |
$this->allowExecCnt = self::DEFAULT_ALLOW_EXEC_CNT; | |
$this->threshold = $this->definedTimeLimit / $this->allowExecCnt; | |
$this->referenceTime = null; | |
} | |
/** | |
* 制限時間を上書きして返す | |
* @param int|float $definedTimeLimit | |
* @return ExecutionController | |
*/ | |
public function overrideDefinedTimeLimit($definedTimeLimit) | |
{ | |
$this->definedTimeLimit = $definedTimeLimit; | |
return $this; | |
} | |
/** | |
* 制限時間内許可実行数を上書きして返す | |
* | |
* @param int $allowExecCnt | |
* @return ExecutionController | |
* @throws \InvalidArgumentExcepti | |
*/ | |
public function overrideAllowExecCnt($allowExecCnt) | |
{ | |
if ((int)$allowExecCnt <= 0) { | |
throw new \InvalidArgumentException('制限時間内許可実行数は0より大きい整数を設定してください'); | |
} | |
$this->allowExecCnt = (int)$allowExecCnt; | |
return $this; | |
} | |
/** | |
* 監視に必要な値を設定 | |
* @return ExecutionController | |
*/ | |
public function setupForMonitoring() | |
{ | |
// 閾値の決定 | |
$this->threshold = $this->definedTimeLimit / $this->allowExecCnt; | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sample2Test; | |
require_once 'ExecutionController.php'; | |
use Sample2\ExecutionController; | |
/** | |
* Test Of Sample2\ExecutionController | |
*/ | |
class ExecutionControllerTest extends \PHPUnit_Framework_TestCase | |
{ | |
/** | |
* チェック対象クラス | |
* | |
* @var ExecutionController | |
*/ | |
protected $target = null; | |
/** | |
* Setup | |
* | |
* {@inheritDoc} | |
*/ | |
protected function setUp() | |
{ | |
parent::setUp(); | |
$this->target = new ExecutionController(); | |
} | |
/** | |
* tearDown | |
* | |
* {@inheritDoc} | |
*/ | |
protected function tearDown() | |
{ | |
parent::tearDown(); | |
} | |
/** | |
* @test | |
*/ | |
public function 最初の通知で基準時間が設定されている() | |
{ | |
// インスタンス生成直後はnull | |
$this->assertNull($this->_getByReflection($this->target, 'referenceTime')); | |
// 通知する | |
$this->target->notify(); | |
// 通知した後は何かしらセットされている | |
$this->assertNotNull($this->_getByReflection($this->target, 'referenceTime')); | |
} | |
/** | |
* @test | |
*/ | |
public function 閾値を超えた場合sleepされる() | |
{ | |
$mock = $this->_buildMock(['_sleep']); | |
// 制限時間を 1秒 に | |
$mock->overrideDefinedTimeLimit(1); | |
// 閾値内に実行出来る回数を 2 に | |
$mock->overrideAllowExecCnt(2); | |
// 再セット 閾値は 0.5 | |
$mock->setupForMonitoring(); | |
// 4回中2回は閾値以下の実行速度で終わるはずなので 2回 sleepが走るし、 | |
// 1回目は ≒(0.5 - 0.1) 秒 くらいのsleep | |
// 2回目は ≒(0.5 - 0.3) 秒 くらいのsleep | |
// が走る | |
$mock | |
->expects($this->exactly(2)) | |
->method('_sleep') | |
->withConsecutive( | |
[ | |
$this->callback(function($microSeconds) { | |
return $microSeconds <= 400000 && $microSeconds > 300000; | |
}) | |
], | |
[ | |
$this->callback(function($microSeconds) { | |
return $microSeconds <= 200000 && $microSeconds > 100000; | |
}) | |
] | |
) | |
; | |
$mock->notify(); | |
$this->_doSomething('usleep', 100000); // 0.1秒待機 | |
$mock->notify(); // 1回目のsleep | |
$this->_doSomething('usleep', 500000); // 0.5秒待機 | |
$mock->notify(); // 閾値以上の時間がかかっているので ここでは _sleepされない | |
$this->_doSomething('usleep', 300000); // 0.3秒待機 | |
$mock->notify(); // 2回目のsleep | |
} | |
/** | |
* @test | |
*/ | |
public function 閾値を超えなければsleepされない() | |
{ | |
$mock = $this->_buildMock(['_sleep']); | |
// 制限時間を 0.01秒 に | |
$mock->overrideDefinedTimeLimit(0.01); | |
// 閾値内に実行出来る回数を 100 に | |
$mock->overrideAllowExecCnt(100); | |
// 再セット 閾値は 0.0001 | |
$mock->setupForMonitoring(); | |
// 閾値以上の秒数で行われるので_sleepは呼ばれない | |
$mock->expects($this->never())->method('_sleep'); | |
$mock->notify(); | |
$this->_doSomething('usleep', 1000); // 0.0001秒待機 | |
$mock->notify(); | |
$this->_doSomething('usleep', 1000); // 0.0001秒待機 | |
$mock->notify(); | |
} | |
/** | |
* @test | |
*/ | |
public function resetできる() | |
{ | |
// 適当に上書きしておく | |
$this->target->overrideDefinedTimeLimit(99999); | |
$this->target->overrideAllowExecCnt(99999); | |
// 実行回数を増やしておく | |
$this->target->notify(); | |
$this->target->notify(); | |
// reset | |
$this->target->reset(); | |
$definedTimeLimit = $this->_getByReflection($this->target, 'definedTimeLimit'); | |
$this->assertSame( | |
ExecutionController::DEFAULT_DEFINED_TIME_LIMIT, | |
$definedTimeLimit | |
); | |
$allowExecCnt = $this->_getByReflection($this->target, 'allowExecCnt'); | |
$this->assertSame( | |
ExecutionController::DEFAULT_ALLOW_EXEC_CNT, | |
$allowExecCnt | |
); | |
$threshold = $this->_getByReflection($this->target, 'threshold'); | |
$this->assertSame( | |
$definedTimeLimit / $allowExecCnt, | |
$threshold | |
); | |
$referenceTime = $this->_getByReflection($this->target, 'referenceTime'); | |
$this->assertNull($referenceTime); | |
} | |
/** | |
* @test | |
*/ | |
public function overrideDefinedTimeLimit_制限時間を上書きできる() | |
{ | |
$this->target->overrideDefinedTimeLimit(99); | |
$expected = 99; | |
$actual = $this->_getByReflection($this->target, 'definedTimeLimit'); | |
$this->assertSame($expected, $actual); | |
} | |
/** | |
* @test | |
*/ | |
public function overrideAllowExecCnt_決められた時間内に実行出来る数を上書きできる() | |
{ | |
$this->target->overrideAllowExecCnt(99); | |
$expected = 99; | |
$actual = $this->_getByReflection($this->target, 'allowExecCnt'); | |
$this->assertSame($expected, $actual); | |
} | |
/** | |
* @test | |
* @expectedException \InvalidArgumentException | |
* @expectedExceptionMessage 制限時間内許可実行数は0より大きい整数を設定してください | |
* @dataProvider overrideAllowExecCntProvider | |
*/ | |
public function overrideAllowExecCnt_1以下の値が設定されたらInvalidArgumentException($arg) | |
{ | |
$this->target->overrideAllowExecCnt($arg); | |
} | |
/** | |
* overrideAllowExecCntProvider | |
*/ | |
public function overrideAllowExecCntProvider() | |
{ | |
return [ | |
[0], | |
[0.00001], | |
[-1], | |
['hoge'] | |
]; | |
} | |
/** | |
* @test | |
*/ | |
public function setupForMonitoring_監視に必要な値を設定できる() | |
{ | |
// 適当に上書きしておく | |
$this->target->overrideDefinedTimeLimit(1); | |
$this->target->overrideAllowExecCnt(2); | |
// 監視に必要な値を設定 | |
$this->target->setupForMonitoring(); | |
// 1 / 2 なので 0.5 | |
$this->assertSame(0.5, $this->_getByReflection($this->target, 'threshold')); | |
} | |
/* | |
|-------------------------------------------------------------------------- | |
| helper | |
|-------------------------------------------------------------------------- | |
*/ | |
/** | |
* アクセス不能なプロパティの値を取得する | |
* @param mixed $target 対象インスタンス | |
* @param string $propertyName プロパティ名 | |
* @return mixed 取得したプロパティの値 | |
*/ | |
private function _getByReflection($target, $propertyName) | |
{ | |
$refClass = new \ReflectionClass($target); | |
$refProperty = $refClass->getProperty($propertyName); | |
$refProperty->setAccessible(true); | |
return $refProperty->getValue($target); | |
} | |
/** | |
* ExecutionControllerのモック | |
* @param array $methods | |
* @return \PHPUnit_Framework_MockObject_MockObject | |
*/ | |
private function _buildMock(array $methods) | |
{ | |
$mock = $this->getMockBuilder('Sample2\ExecutionController') | |
->disableOriginalConstructor() | |
->setMethods($methods) | |
->getMock(); | |
return $mock; | |
} | |
/** | |
* dummyで何かやる | |
* @param callable $func | |
* @param mixed $arg | |
* @return void | |
*/ | |
private function _doSomething(callable $func=null, $arg=null) | |
{ | |
if ($func === null) { | |
return; | |
} | |
call_user_func($func, $arg); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment