Completed
Push — master ( 04281c...63ec8d )
by Dmitry
11:56
created

FileMutex::acquireLock()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 51
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 11.8345

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 51
ccs 14
cts 23
cp 0.6087
rs 6.5978
cc 8
eloc 23
nc 10
nop 2
crap 11.8345

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\mutex;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\helpers\FileHelper;
13
14
/**
15
 * FileMutex implements mutex "lock" mechanism via local file system files.
16
 * This component relies on PHP `flock()` function.
17
 *
18
 * Application configuration example:
19
 *
20
 * ```php
21
 * [
22
 *     'components' => [
23
 *         'mutex' => [
24
 *             'class' => 'yii\mutex\FileMutex'
25
 *         ],
26
 *     ],
27
 * ]
28
 * ```
29
 *
30
 * Note: this component can maintain the locks only for the single web server,
31
 * it probably will not suffice to your in case you are using cloud server solution.
32
 *
33
 * Warning: due to `flock()` function nature this component is unreliable when
34
 * using a multithreaded server API like ISAPI.
35
 *
36
 * @see Mutex
37
 *
38
 * @author resurtm <[email protected]>
39
 * @since 2.0
40
 */
41
class FileMutex extends Mutex
42
{
43
    /**
44
     * @var string the directory to store mutex files. You may use [path alias](guide:concept-aliases) here.
45
     * Defaults to the "mutex" subdirectory under the application runtime path.
46
     */
47
    public $mutexPath = '@runtime/mutex';
48
    /**
49
     * @var int the permission to be set for newly created mutex files.
50
     * This value will be used by PHP chmod() function. No umask will be applied.
51
     * If not set, the permission will be determined by the current environment.
52
     */
53
    public $fileMode;
54
    /**
55
     * @var int the permission to be set for newly created directories.
56
     * This value will be used by PHP chmod() function. No umask will be applied.
57
     * Defaults to 0775, meaning the directory is read-writable by owner and group,
58
     * but read-only for other users.
59
     */
60
    public $dirMode = 0775;
61
62
    /**
63
     * @var resource[] stores all opened lock files. Keys are lock names and values are file handles.
64
     */
65
    private $_files = [];
66
67
68
    /**
69
     * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like
70
     * operating systems.
71
     * @throws InvalidConfigException
72
     */
73 3
    public function init()
74
    {
75 3
        parent::init();
76 3
        $this->mutexPath = Yii::getAlias($this->mutexPath);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Yii::getAlias($this->mutexPath) can also be of type boolean. However, the property $mutexPath is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
77 3
        if (!is_dir($this->mutexPath)) {
78 1
            FileHelper::createDirectory($this->mutexPath, $this->dirMode, true);
79
        }
80 3
    }
81
82
    /**
83
     * Acquires lock by given name.
84
     * @param string $name of the lock to be acquired.
85
     * @param int $timeout to wait for lock to become released.
86
     * @return bool acquiring result.
87
     */
88 3
    protected function acquireLock($name, $timeout = 0)
89
    {
90 3
        $filePath = $this->getLockFilePath($name);
91 3
        $waitTime = 0;
92
93 3
        while (true) {
94 3
            $file = fopen($filePath, 'w+');
95
96 3
            if ($file === false) {
97
                return false;
98
            }
99
100 3
            if ($this->fileMode !== null) {
101
                @chmod($filePath, $this->fileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
102
            }
103
104 3
            if (!flock($file, LOCK_EX | LOCK_NB)) {
105 1
                fclose($file);
106
107 1
                if (++$waitTime > $timeout) {
108 1
                    return false;
109
                }
110
111
                sleep(1);
112
                continue;
113
            }
114
115
            // Under unix we delete the lock file before releasing the related handle. Thus it's possible that we've acquired a lock on
116
            // a non-existing file here (race condition). We must compare the inode of the lock file handle with the inode of the actual lock file.
117
            // If they do not match we simply continue the loop since we can assume the inodes will be equal on the next try.
118
            // Example of race condition without inode-comparison:
119
            // Script A: locks file
120
            // Script B: opens file
121
            // Script A: unlinks and unlocks file
122
            // Script B: locks handle of *unlinked* file
123
            // Script C: opens and locks *new* file
124
            // In this case we would have acquired two locks for the same file path.
125 3
            if (DIRECTORY_SEPARATOR !== '\\' && fstat($file)['ino'] !== @fileinode($filePath)) {
126
                clearstatcache(true, $filePath);
127
                flock($file, LOCK_UN);
128
                fclose($file);
129
                continue;
130
            }
131
132 3
            $this->_files[$name] = $file;
133 3
            return true;
134
        }
135
136
        // Should not be reached normally.
137
        return false;
138
    }
139
140
    /**
141
     * Releases lock by given name.
142
     * @param string $name of the lock to be released.
143
     * @return bool release result.
144
     */
145 3
    protected function releaseLock($name)
146
    {
147 3
        if (!isset($this->_files[$name])) {
148
            return false;
149
        }
150
151 3
        if (DIRECTORY_SEPARATOR === '\\') {
152
            // Under windows it's not possible to delete a file opened via fopen (either by own or other process).
153
            // That's why we must first unlock and close the handle and then *try* to delete the lock file.
154
            flock($this->_files[$name], LOCK_UN);
155
            fclose($this->_files[$name]);
156
            @unlink($this->getLockFilePath($name));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
157
        } else {
158
            // Under unix it's possible to delete a file opened via fopen (either by own or other process).
159
            // That's why we must unlink (the currently locked) lock file first and then unlock and close the handle.
160 3
            unlink($this->getLockFilePath($name));
161 3
            flock($this->_files[$name], LOCK_UN);
162 3
            fclose($this->_files[$name]);
163
        }
164
165 3
        unset($this->_files[$name]);
166 3
        return true;
167
    }
168
169
    /**
170
     * Generate path for lock file.
171
     * @param string $name
172
     * @return string
173
     * @since 2.0.10
174
     */
175 3
    protected function getLockFilePath($name)
176
    {
177 3
        return $this->mutexPath . DIRECTORY_SEPARATOR . md5($name) . '.lock';
178
    }
179
}
180