ConcurrencyLimiter.php 2.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. <?php
  2. namespace Illuminate\Redis\Limiters;
  3. use Illuminate\Contracts\Redis\LimiterTimeoutException;
  4. class ConcurrencyLimiter
  5. {
  6. /**
  7. * The Redis factory implementation.
  8. *
  9. * @var \Illuminate\Redis\Connections\Connection
  10. */
  11. protected $redis;
  12. /**
  13. * The name of the limiter.
  14. *
  15. * @var string
  16. */
  17. protected $name;
  18. /**
  19. * The allowed number of concurrent tasks.
  20. *
  21. * @var int
  22. */
  23. protected $maxLocks;
  24. /**
  25. * The number of seconds a slot should be maintained.
  26. *
  27. * @var int
  28. */
  29. protected $releaseAfter;
  30. /**
  31. * Create a new concurrency limiter instance.
  32. *
  33. * @param \Illuminate\Redis\Connections\Connection $redis
  34. * @param string $name
  35. * @param int $maxLocks
  36. * @param int $releaseAfter
  37. * @return void
  38. */
  39. public function __construct($redis, $name, $maxLocks, $releaseAfter)
  40. {
  41. $this->name = $name;
  42. $this->redis = $redis;
  43. $this->maxLocks = $maxLocks;
  44. $this->releaseAfter = $releaseAfter;
  45. }
  46. /**
  47. * Attempt to acquire the lock for the given number of seconds.
  48. *
  49. * @param int $timeout
  50. * @param callable|null $callback
  51. * @return bool
  52. * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
  53. */
  54. public function block($timeout, $callback = null)
  55. {
  56. $starting = time();
  57. while (! $slot = $this->acquire()) {
  58. if (time() - $timeout >= $starting) {
  59. throw new LimiterTimeoutException;
  60. }
  61. usleep(250 * 1000);
  62. }
  63. if (is_callable($callback)) {
  64. return tap($callback(), function () use ($slot) {
  65. $this->release($slot);
  66. });
  67. }
  68. return true;
  69. }
  70. /**
  71. * Attempt to acquire the lock.
  72. *
  73. * @return mixed
  74. */
  75. protected function acquire()
  76. {
  77. $slots = array_map(function ($i) {
  78. return $this->name.$i;
  79. }, range(1, $this->maxLocks));
  80. return $this->redis->eval($this->luaScript(), count($slots),
  81. ...array_merge($slots, [$this->name, $this->releaseAfter])
  82. );
  83. }
  84. /**
  85. * Get the Lua script for acquiring a lock.
  86. *
  87. * KEYS - The keys that represent available slots
  88. * ARGV[1] - The limiter name
  89. * ARGV[2] - The number of seconds the slot should be reserved
  90. *
  91. * @return string
  92. */
  93. protected function luaScript()
  94. {
  95. return <<<'LUA'
  96. for index, value in pairs(redis.call('mget', unpack(KEYS))) do
  97. if not value then
  98. redis.call('set', ARGV[1]..index, "1", "EX", ARGV[2])
  99. return ARGV[1]..index
  100. end
  101. end
  102. LUA;
  103. }
  104. /**
  105. * Release the lock.
  106. *
  107. * @param string $key
  108. * @return void
  109. */
  110. protected function release($key)
  111. {
  112. $this->redis->del($key);
  113. }
  114. }