DurationLimiter.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. * @return void
  51. */
  52. public function __construct($redis, $name, $maxLocks, $decay)
  53. {
  54. $this->name = $name;
  55. $this->decay = $decay;
  56. $this->redis = $redis;
  57. $this->maxLocks = $maxLocks;
  58. }
  59. /**
  60. * Attempt to acquire the lock for the given number of seconds.
  61. *
  62. * @param int $timeout
  63. * @param callable|null $callback
  64. * @param int $sleep
  65. * @return mixed
  66. *
  67. * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
  68. */
  69. public function block($timeout, $callback = null, $sleep = 750)
  70. {
  71. $starting = time();
  72. while (! $this->acquire()) {
  73. if (time() - $timeout >= $starting) {
  74. throw new LimiterTimeoutException;
  75. }
  76. Sleep::usleep($sleep * 1000);
  77. }
  78. if (is_callable($callback)) {
  79. return $callback();
  80. }
  81. return true;
  82. }
  83. /**
  84. * Attempt to acquire the lock.
  85. *
  86. * @return bool
  87. */
  88. public function acquire()
  89. {
  90. $results = $this->redis->eval(
  91. $this->luaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks
  92. );
  93. $this->decaysAt = $results[1];
  94. $this->remaining = max(0, $results[2]);
  95. return (bool) $results[0];
  96. }
  97. /**
  98. * Determine if the key has been "accessed" too many times.
  99. *
  100. * @return bool
  101. */
  102. public function tooManyAttempts()
  103. {
  104. [$this->decaysAt, $this->remaining] = $this->redis->eval(
  105. $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks
  106. );
  107. return $this->remaining <= 0;
  108. }
  109. /**
  110. * Clear the limiter.
  111. *
  112. * @return void
  113. */
  114. public function clear()
  115. {
  116. $this->redis->del($this->name);
  117. }
  118. /**
  119. * Get the Lua script for acquiring a lock.
  120. *
  121. * KEYS[1] - The limiter name
  122. * ARGV[1] - Current time in microseconds
  123. * ARGV[2] - Current time in seconds
  124. * ARGV[3] - Duration of the bucket
  125. * ARGV[4] - Allowed number of tasks
  126. *
  127. * @return string
  128. */
  129. protected function luaScript()
  130. {
  131. return <<<'LUA'
  132. local function reset()
  133. redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
  134. return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
  135. end
  136. if redis.call('EXISTS', KEYS[1]) == 0 then
  137. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  138. end
  139. if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
  140. return {
  141. tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
  142. redis.call('HGET', KEYS[1], 'end'),
  143. ARGV[4] - redis.call('HGET', KEYS[1], 'count')
  144. }
  145. end
  146. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  147. LUA;
  148. }
  149. /**
  150. * Get the Lua script to determine if the key has been "accessed" too many times.
  151. *
  152. * KEYS[1] - The limiter name
  153. * ARGV[1] - Current time in microseconds
  154. * ARGV[2] - Current time in seconds
  155. * ARGV[3] - Duration of the bucket
  156. * ARGV[4] - Allowed number of tasks
  157. *
  158. * @return string
  159. */
  160. protected function tooManyAttemptsLuaScript()
  161. {
  162. return <<<'LUA'
  163. if redis.call('EXISTS', KEYS[1]) == 0 then
  164. return {0, ARGV[2] + ARGV[3]}
  165. end
  166. if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
  167. return {
  168. redis.call('HGET', KEYS[1], 'end'),
  169. ARGV[4] - redis.call('HGET', KEYS[1], 'count')
  170. }
  171. end
  172. return {0, ARGV[2] + ARGV[3]}
  173. LUA;
  174. }
  175. }