Skip to content

Instantly share code, notes, and snippets.

@shimabox
Last active October 4, 2018 23:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shimabox/0615ee11912a0cb3a9b335a1006a19b2 to your computer and use it in GitHub Desktop.
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/
<?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;
}
}
<?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