Completed
Pull Request — master (#14)
by Chad
01:45
created

ProcessRegistry::add()   D

Complexity

Conditions 17
Paths 197

Size

Total Lines 84
Code Lines 47

Duplication

Lines 7
Ratio 8.33 %

Importance

Changes 0
Metric Value
dl 7
loc 84
rs 4.5416
c 0
b 0
f 0
cc 17
eloc 47
nc 197
nop 4

How to fix   Long Method    Complexity   

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
 * Defines the ProcessRegistry class which uses MongoDB as a backend.
4
 */
5
6
namespace DominionEnterprises\Cronus;
7
8
use MongoDB\BSON\ObjectID;
9
use MongoDB\Collection;
10
11
/**
12
 * Class that adds/removes from a process registry.
13
 */
14
final class ProcessRegistry implements ProcessRegistryInterface
15
{
16
    /** example doc:
17
     * {
18
     *     '_id': 'a unique id',
19
     *     'hosts': {
20
     *         'a hostname' : {
21
     *             'a pid': expire timestamp,
22
     *             ...
23
     *         },
24
     *         ...
25
     *     },
26
     *     'version' => ObjectID(an id),
27
     * }
28
     */
29
30
    const MONGO_INT32_MAX = 2147483647;//2147483648 can overflow in php mongo without using the MongoInt64
31
32
    /**
33
     * MongoDB collection containing the process information.
34
     *
35
     * @var Collection
36
     */
37
    private $collection;
38
39
    /**
40
     * Construct a new instance of the registry.
41
     *
42
     * @param Collection $collection The MongoDB collection containing the process information.
43
     */
44
    public function __construct(Collection $collection)
45
    {
46
        $this->collection = $collection;
47
    }
48
49
    /**
50
     * Add to process registry. Adds based on $maxGlobalProcesses and $maxHostProcesses after a process registry
51
     * cleaning.
52
     *
53
     * @param string  $id                 A unique id.
54
     * @param integer $minsBeforeExpire   Number of minutes before a process is considered expired.
55
     * @param integer $maxGlobalProcesses Max processes of an id allowed to run across all hosts.
56
     * @param integer $maxHostProcesses   Max processes of an id allowed to run across a single host.
57
     *
58
     * @return boolean true if the process was added, false if not or there is too much concurrency at the moment.
59
     */
60
    public function add(
0 ignored issues
show
Coding Style introduced by
Function's nesting level (4) exceeds 2; consider refactoring the function
Loading history...
Coding Style introduced by
Unknown type hint "string" found for $id
Loading history...
Coding Style introduced by
Unknown type hint "int" found for $minsBeforeExpire
Loading history...
Coding Style introduced by
Unknown type hint "int" found for $maxGlobalProcesses
Loading history...
Coding Style introduced by
Unknown type hint "int" found for $maxHostProcesses
Loading history...
61
        string $id,
62
        int $minsBeforeExpire = PHP_INT_MAX,
63
        int $maxGlobalProcesses = 1,
64
        int $maxHostProcesses = 1
65
    ) : bool {
66
        $thisHostName = self::_getEncodedHostname();
67
        $thisPid = getmypid();
68
69
        //loop in case the update fails its optimistic concurrency check
70
        for ($i = 0; $i < 5; ++$i) {
71
            $this->collection->findOneAndUpdate(
72
                ['_id' => $id],
73
                ['$setOnInsert' => ['hosts' => [], 'version' => new ObjectID()]],
74
                ['upsert' => true]
75
            );
76
            $existing = $this->collection->findOne(
77
                ['_id' => $id],
78
                ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]
79
            );
80
81
            $replacement = $existing;
82
            $replacement['version'] = new ObjectID();
83
84
            //clean $replacement based on their pids and expire times
85
            foreach ($existing['hosts'] as $hostname => $pids) {
86
                foreach ($pids as $pid => $expires) {
87
                    //our machine and not running
88
                    //the task expired
89
                    //our machine and pid is recycled (should rarely happen)
90
                    if (($hostname === $thisHostName && !file_exists("/proc/{$pid}"))
91
                        || time() > $expires
92
                        || ($hostname === $thisHostName && $pid === $thisPid)
93
                    ) {
94
                        unset($replacement['hosts'][$hostname][$pid]);
95
                    }
96
                }
97
98
                if (empty($replacement['hosts'][$hostname])) {
99
                    unset($replacement['hosts'][$hostname]);
100
                }
101
            }
102
103
            $totalPidCount = 0;
104
            foreach ($replacement['hosts'] as $hostname => $pids) {
105
                $totalPidCount += count($pids);
106
            }
107
108
            $thisHostPids = array_key_exists($thisHostName, $replacement['hosts']) ? $replacement['hosts'][$thisHostName] : [];
109
110
            if ($totalPidCount >= $maxGlobalProcesses || count($thisHostPids) >= $maxHostProcesses) {
111
                return false;
112
            }
113
114
            // add our process
115
            $expireSecs = time() + $minsBeforeExpire * 60;
116 View Code Duplication
            if (!is_int($expireSecs)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
117
                if ($minsBeforeExpire > 0) {
118
                    $expireSecs = self::MONGO_INT32_MAX;
119
                } else {
120
                    $expireSecs = 0;
121
                }
122
            }
123
124
            $thisHostPids[$thisPid] = $expireSecs;
125
            $replacement['hosts'][$thisHostName] = $thisHostPids;
126
127
            $status = $this->collection->replaceOne(
128
                ['_id' => $existing['_id'], 'version' => $existing['version']],
129
                $replacement,
130
                ['writeConcern' => new \MongoDB\Driver\WriteConcern(1, 100, true)]
131
            );
132
            if ($status->getMatchedCount() === 1) {
133
                return true;
134
            }
135
136
            //@codeCoverageIgnoreStart
137
            //hard to test the optimistic concurrency check
138
        }
139
140
        //too much concurrency at the moment, return false to signify not added.
141
        return false;
142
        //@codeCoverageIgnoreEnd
143
    }
144
145
    /**
146
     * Removes from process registry. Does not do anything needed for use of the add() method. Most will only use at the
147
     * end of their script so the mongo collection is up to date.
148
     *
149
     * @param string $id A unique id.
150
     *
151
     * @return void
152
     */
153
    public function remove(string $id)
0 ignored issues
show
Coding Style introduced by
Unknown type hint "string" found for $id
Loading history...
154
    {
155
        $thisHostName = self::_getEncodedHostname();
156
        $thisPid = getmypid();
157
158
        $this->collection->updateOne(
159
            ['_id' => $id],
160
            ['$unset' => ["hosts.{$thisHostName}.{$thisPid}" => ''], '$set' => ['version' => new ObjectID()]]
161
        );
162
    }
163
164
    /**
165
     * Reset a process expire time in the registry.
166
     *
167
     * @param string  $id               A unique id.
168
     * @param integer $minsBeforeExpire Number of minutes before a process is considered expired.
169
     *
170
     * @return void
171
     */
172
    public function reset(string $id, int $minsBeforeExpire)
0 ignored issues
show
Coding Style introduced by
Unknown type hint "string" found for $id
Loading history...
Coding Style introduced by
Unknown type hint "int" found for $minsBeforeExpire
Loading history...
173
    {
174
        $expireSecs = time() + $minsBeforeExpire * 60;
175 View Code Duplication
        if (!is_int($expireSecs)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
            if ($minsBeforeExpire > 0) {
177
                $expireSecs = self::MONGO_INT32_MAX;
178
            } else {
179
                $expireSecs = 0;
180
            }
181
        }
182
183
        $thisHostName = self::_getEncodedHostname();
184
        $thisPid = getmypid();
185
186
        $this->collection->updateOne(
187
            ['_id' => $id],
188
            [
189
                '$set' => [
190
                    "hosts.{$thisHostName}.{$thisPid}" => $expireSecs,
191
                    'version' => new ObjectID(),
192
                ],
193
            ]
194
        );
195
    }
196
197
    /**
198
     * Encodes '.' and '$' to be used as a mongo field name.
199
     *
200
     * @return string the encoded hostname from gethostname().
201
     */
202
    private static function _getEncodedHostname() : string
203
    {
204
        return str_replace(['.', '$'], ['_DOT_', '_DOLLAR_'], gethostname());
205
    }
206
}
207