DurationLimiter.php 3.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <?php
  2. namespace Illuminate\Redis\Limiters;
  3. use Illuminate\Contracts\Redis\LimiterTimeoutException;
  4. class DurationLimiter
  5. {
  6. /**
  7. * The Redis factory implementation.
  8. *
  9. * @var \Illuminate\Redis\Connections\Connection
  10. */
  11. private $redis;
  12. /**
  13. * The unique name of the lock.
  14. *
  15. * @var string
  16. */
  17. private $name;
  18. /**
  19. * The allowed number of concurrent tasks.
  20. *
  21. * @var int
  22. */
  23. private $maxLocks;
  24. /**
  25. * The number of seconds a slot should be maintained.
  26. *
  27. * @var int
  28. */
  29. private $decay;
  30. /**
  31. * The timestamp of the end of the current duration.
  32. *
  33. * @var int
  34. */
  35. public $decaysAt;
  36. /**
  37. * The number of remaining slots.
  38. *
  39. * @var int
  40. */
  41. public $remaining;
  42. /**
  43. * Create a new duration limiter instance.
  44. *
  45. * @param \Illuminate\Redis\Connections\Connection $redis
  46. * @param string $name
  47. * @param int $maxLocks
  48. * @param int $decay
  49. * @return void
  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. * @return bool
  64. * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
  65. */
  66. public function block($timeout, $callback = null)
  67. {
  68. $starting = time();
  69. while (! $this->acquire()) {
  70. if (time() - $timeout >= $starting) {
  71. throw new LimiterTimeoutException;
  72. }
  73. usleep(750 * 1000);
  74. }
  75. if (is_callable($callback)) {
  76. $callback();
  77. }
  78. return true;
  79. }
  80. /**
  81. * Attempt to acquire the lock.
  82. *
  83. * @return bool
  84. */
  85. public function acquire()
  86. {
  87. $results = $this->redis->eval($this->luaScript(), 1,
  88. $this->name, microtime(true), time(), $this->decay, $this->maxLocks
  89. );
  90. $this->decaysAt = $results[1];
  91. $this->remaining = max(0, $results[2]);
  92. return (bool) $results[0];
  93. }
  94. /**
  95. * Get the Lua script for acquiring a lock.
  96. *
  97. * KEYS[1] - The limiter name
  98. * ARGV[1] - Current time in microseconds
  99. * ARGV[2] - Current time in seconds
  100. * ARGV[3] - Duration of the bucket
  101. * ARGV[4] - Allowed number of tasks
  102. *
  103. * @return string
  104. */
  105. protected function luaScript()
  106. {
  107. return <<<'LUA'
  108. local function reset()
  109. redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
  110. return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
  111. end
  112. if redis.call('EXISTS', KEYS[1]) == 0 then
  113. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  114. end
  115. if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
  116. return {
  117. tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
  118. redis.call('HGET', KEYS[1], 'end'),
  119. ARGV[4] - redis.call('HGET', KEYS[1], 'count')
  120. }
  121. end
  122. return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
  123. LUA;
  124. }
  125. }