Test Failed
Pull Request — master (#17)
by Evgeniy
08:59
created

MysqlMutex::acquire()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
dl 0
loc 13
rs 10
c 1
b 0
f 0
cc 2
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Mutex\Mysql;
6
7
use InvalidArgumentException;
8
use PDO;
9
use RuntimeException;
10
use Yiisoft\Mutex\MutexInterface;
11
use Yiisoft\Mutex\RetryAcquireTrait;
12
13
use function sha1;
14
15
/**
16
 * MysqlMutex implements mutex "lock" mechanism via MySQL locks.
17
 */
18
final class MysqlMutex implements MutexInterface
19
{
20
    use RetryAcquireTrait;
21
22
    private string $name;
23
    private PDO $connection;
24
25
    /**
26
     * @param string $name Mutex name.
27
     * @param PDO $connection PDO connection instance to use.
28
     */
29
    public function __construct(string $name, PDO $connection)
30
    {
31
        $this->name = $name;
32
        $this->connection = $connection;
33
34
        /** @var string $driverName */
35
        $driverName = $connection->getAttribute(PDO::ATTR_DRIVER_NAME);
36
37
        if ($driverName !== 'mysql') {
38
            throw new InvalidArgumentException("MySQL connection instance should be passed. Got $driverName.");
39
        }
40
    }
41
42
    public function __destruct()
43
    {
44
        $this->release();
45
    }
46
47
    /**
48
     * {@inheritdoc}
49
     *
50
     * @see https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_get-lock
51
     * @see https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_is-free-lock
52
     */
53
    public function acquire(int $timeout = 0): bool
54
    {
55
        return $this->retryAcquire($timeout, function () use ($timeout): bool {
56
            if (!$this->isFreeLock()) {
57
                return false;
58
            }
59
60
            $statement = $this->connection->prepare('SELECT GET_LOCK(:name, :timeout)');
61
            $statement->bindValue(':name', $this->hashLockName());
62
            $statement->bindValue(':timeout', $timeout);
63
            $statement->execute();
64
65
            return (bool) $statement->fetchColumn();
66
        });
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     *
72
     * @see https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_release-lock
73
     * @see https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_is-free-lock
74
     */
75
    public function release(): void
76
    {
77
        if ($this->isFreeLock()) {
78
            return;
79
        }
80
81
        $statement = $this->connection->prepare('SELECT RELEASE_LOCK(:name)');
82
        $statement->bindValue(':name', $this->hashLockName());
83
        $statement->execute();
84
85
        if (!$statement->fetchColumn()) {
86
            throw new RuntimeException("Unable to release lock \"$this->name\".");
87
        }
88
    }
89
90
    /**
91
     * Generates hash for the lock name to avoid exceeding lock name length limit.
92
     *
93
     * @return string The generated hash for the lock name.
94
     *
95
     * @see https://github.com/yiisoft/yii2/pull/16836
96
     */
97
    private function hashLockName(): string
98
    {
99
        return sha1($this->name);
100
    }
101
102
    /**
103
     * Checks whether the lock is free to use (that is, not locked).
104
     *
105
     * @return bool Whether the lock is free to use.
106
     *
107
     * @see https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_is-free-lock
108
     */
109
    private function isFreeLock(): bool
110
    {
111
        $statement = $this->connection->prepare('SELECT IS_FREE_LOCK(:name)');
112
        $statement->bindValue(':name', $this->hashLockName());
113
        $statement->execute();
114
115
        return (bool) $statement->fetchColumn();
116
    }
117
}
118