DurationLimiter.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php
  2. namespace Illuminate\Redis\Limiters;
  3. use Illuminate\Contracts\Redis\LimiterTimeoutException;
  4. use Illuminate\Support\Sleep;
  5. class DurationLimiter
  6. {
  7. /**
  8. * The Redis factory implementation.
  9. *
  10. * @var \Illuminate\Redis\Connections\Connection
  11. */
  12. private $redis;
  13. /**
  14. * The unique name of the lock.
  15. *
  16. * @var string
  17. */
  18. private $name;
  19. /**
  20. * The allowed number of concurrent tasks.
  21. *
  22. * @var int
  23. */
  24. private $maxLocks;
  25. /**
  26. * The number of seconds a slot should be maintained.
  27. *
  28. * @var int
  29. */
  30. private $decay;
  31. /**
  32. * The timestamp of the end of the current duration.
  33. *
  34. * @var int
  35. */
  36. public $decaysAt;
  37. /**
  38. * The number of remaining slots.
  39. *
  40. * @var int
  41. */
  42. public $remaining;
  43. /**
  44. * Create a new duration limiter instance.
  45. *
  46. * @param \Illuminate\Redis\Connections\Connection $redis
  47. * @param string $name
  48. * @param int $maxLocks
  49. * @param int $decay
  50. */
  51. public function __construct($redis, $name, $maxLocks, $decay)
  52. {
  53. $this->name = $name;
  54. $this->decay = $decay;
  55. $this->redis = $redis;
  56. $this->maxLocks = $maxLocks;
  57. }
  58. /**
  59. * Attempt to acquire the lock for the given number of seconds.
  60. *
  61. * @param int $timeout
  62. * @param callable|null $callback
  63. * @param int $sleep
  64. * @return mixed
  65. *
  66. * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
  67. */
  68. public function block($timeout, $callback = null, $sleep = 750)
  69. {
  70. $starting = time();
  71. while (! $this->acquire()) {
  72. if (time() - $timeout >= $starting) {
  73. throw new LimiterTimeoutException;
  74. }
  75. Sleep::usleep($sleep * 1000);
  76. }
  77. if (is_callable($callback)) {
  78. return $callback();
  79. }
  80. return true;
  81. }
  82. /**
  83. * Attempt to acquire the lock.
  84. *
  85. * @return bool
  86. */
  87. public function acquire()
  88. {
  89. $results = $this->redis->eval(
  90. $this->luaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks
  91. );
  92. $this->decaysAt = $results[1];
  93. $this->remaining = max(0, $results[2]);
  94. return (bool) $results[0];
  95. }
  96. /**
  97. * Determine if the key has been "accessed" too many times.
  98. *
  99. * @return bool
  100. */
  101. public function tooManyAttempts()
  102. {
  103. [$this->decaysAt, $this->remaining] = $this->redis->eval(
  104. $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks
  105. );
  106. return $this->remaining <= 0;
  107. }
  108. /**
  109. * Clear the limiter.
  110. *
  111. * @return void
  112. */
  113. public function clear()
  114. {
  115. $this->redis->del($this->name);
  116. }
  117. /**
  118. * Get the Lua script for acquiring a lock.
  119. *
  120. * KEYS[1] - The limiter name
  121. * ARGV[1] - Current time in microseconds
  122. * ARGV[2] - Current time in seconds
  123. * ARGV[3] - Duration of the bucket
  124. * ARGV[4] - Allowed number of tasks
  125. *
  126. * @return string
  127. */
  128. protected function luaScript()
  129. {
  130. return <<<'LUA'
  131. local function reset()
  132. redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
  133. return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
  134. end
  135. if redis.call('EXISTS', KEYS[1]) == 0 then
  136. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  137. end
  138. if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
  139. return {
  140. tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
  141. redis.call('HGET', KEYS[1], 'end'),
  142. ARGV[4] - redis.call('HGET', KEYS[1], 'count')
  143. }
  144. end
  145. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  146. LUA;
  147. }
  148. /**
  149. * Get the Lua script to determine if the key has been "accessed" too many times.
  150. *
  151. * KEYS[1] - The limiter name
  152. * ARGV[1] - Current time in microseconds
  153. * ARGV[2] - Current time in seconds
  154. * ARGV[3] - Duration of the bucket
  155. * ARGV[4] - Allowed number of tasks
  156. *
  157. * @return string
  158. */
  159. protected function tooManyAttemptsLuaScript()
  160. {
  161. return <<<'LUA'
  162. if redis.call('EXISTS', KEYS[1]) == 0 then
  163. return {0, ARGV[2] + ARGV[3]}
  164. end
  165. if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
  166. return {
  167. redis.call('HGET', KEYS[1], 'end'),
  168. ARGV[4] - redis.call('HGET', KEYS[1], 'count')
  169. }
  170. end
  171. return {0, ARGV[2] + ARGV[3]}
  172. LUA;
  173. }
  174. }