Completed
Push — master ( 4a45af...693b46 )
by Tomasz
03:04
created

ZooKeeperConfig::getMachineInfo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 2
Metric Value
c 5
b 0
f 2
dl 0
loc 13
rs 9.4285
cc 2
eloc 8
nc 2
nop 0
1
<?php
2
3
/**
4
 * ZooKeeper-based configuration.
5
 *
6
 * Couple of points:
7
 *
8
 *  1. We coordinate via ZK on launch - hence ZK must be available at launch
9
 *     time
10
 *  2. We create permanent nodes (not ephmeral) so that if we get disconnected
11
 *     ZK still knows about us running
12
 *  3. There is a danger that point 2 will mean that we run out of machine IDs
13
 *     if host name change and we don't manually clean up.
14
 *  4. All of your machines have to have unique host names.
15
 *
16
 * @author @davegardnerisme
17
 */
18
19
namespace Gendoria\CruftFlake\Config;
20
21
use BadMethodCallException;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerInterface;
24
use Psr\Log\NullLogger;
25
use RuntimeException;
26
27
class ZooKeeperConfig implements ConfigInterface, LoggerAwareInterface
28
{
29
    /**
30
     * Parent path.
31
     *
32
     * @var string
33
     */
34
    private $parentPath;
35
36
    /**
37
     * ZK.
38
     *
39
     * @var \Zookeeper
40
     */
41
    private $zk;
42
43
    /**
44
     * Process ID for a multi-process-single-machine setup.
45
     * 
46
     * @var int
47
     */
48
    private $procesId = 1;
49
50
    /**
51
     * Logger.
52
     * 
53
     * @var LoggerInterface
54
     */
55
    private $logger;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param string          $hostnames A comma separated list of hostnames (including
61
     *                                   port)
62
     * @param int             $processId If you want to run multiple server processes on a single machine, 
63
     *                                   you have to provide each one an unique ID,
64
     *                                   so the zookeeper knows, which machine ID belongs to which process.
65
     * @param string          $zkPath    The ZK path we look to find other machines under
66
     * @param LoggerInterface $logger    Logger class
67
     */
68
    public function __construct($hostnames, $processId = 1, $zkPath = '/cruftflake',
69
        LoggerInterface $logger = null)
70
    {
71
        if (!class_exists('\Zookeeper')) {
72
            $this->logger->critical('Zookeeper not present');
73
            throw new BadMethodCallException('ZooKeeper extension not installed. Try hitting PECL.');
74
        }
75
        $this->procesId = $processId;
76
        $this->zk = new \Zookeeper($hostnames);
77
        $this->parentPath = $zkPath;
78
        if ($logger) {
79
            $this->logger = $logger;
80
        } else {
81
            $this->logger = new NullLogger();
82
        }
83
    }
84
85
    /**
86
     * Get machine identifier.
87
     *
88
     * @return int Should be a 10-bit int (decimal 0 to 1023)
89
     *
90
     * @throws RuntimeException Thrown, when obtaining machine ID has failed.
91
     */
92
    public function getMachine()
93
    {
94
        $machineId = null;
95
96
        $this->createParentIfNeeded($this->parentPath);
97
98
        // get info about _this_ machine
99
        $machineInfo = $this->getMachineInfo();
100
101
        // get current machine list
102
        $children = $this->zk->getChildren($this->parentPath);
103
104
        //Find existing machine info
105
        foreach ($children as $child) {
106
            $info = $this->zk->get("{$this->parentPath}/$child");
107
            $info = json_decode($info, true);
108
            if ($this->compareMachineInfo($info, $machineInfo)) {
109
                $machineId = (int) $child;
110
                break; //We don't have to check further
111
            }
112
        }
113
114
        //Machine info not found, attempt to create one
115
        if ($machineId === null) {
116
            $machineId = $this->createMachineInfo($children, $machineInfo);
117
        }
118
119
        $this->logger->debug('Obtained machine ID '.$machineId.' through ZooKeeper configuration');
120
121
        return (int) $machineId;
122
    }
123
124
    /**
125
     * Compare found machine information with expected values.
126
     * 
127
     * @param array $found
128
     * @param array $expected
129
     *
130
     * @return bool
131
     */
132
    private function compareMachineInfo(array $found, array $expected)
133
    {
134
        if (!isset($found['hostname']) || !isset($found['processId'])) {
135
            return false;
136
        }
137
138
        return $found['hostname'] === $expected['hostname'] && $found['processId'] === $expected['processId'];
139
    }
140
141
    /**
142
     * Attempt to claim and create new machine ID in Zookeeper.
143
     * 
144
     * @param array $children
145
     * @param array $machineInfo
146
     *
147
     * @return int Machine ID.
148
     *
149
     * @throws RuntimeException Thrown, when creation of machine ID has failed.
150
     */
151
    private function createMachineInfo(array $children, array $machineInfo)
152
    {
153
        // find an unused machine number
154
        for ($i = 0; $i < 1024; ++$i) {
155
            $machineNode = $this->machineToNode($i);
156
            if (in_array($machineNode, $children)) {
157
                continue; // already used
158
            }
159
160
            // attempt to claim
161
            $created = $this->zk->create(
162
                "{$this->parentPath}/{$machineNode}", json_encode($machineInfo),
163
                array(array(// acl
164
                    'perms' => \Zookeeper::PERM_ALL,
165
                    'scheme' => 'world',
166
                    'id' => 'anyone',
167
                ))
168
            );
169
            if ($created !== null) {
170
                return $i;
171
            }
172
        }
173
174
        //Creating machine ID failed, throw an error
175
        $this->logger->critical('Cannot locate and claim a free machine ID via ZK', array($this));
176
        throw new RuntimeException('Cannot locate and claim a free machine ID via ZK');
177
    }
178
179
    /**
180
     * Get mac address and hostname.
181
     *
182
     * @return array "hostname","processId" keys
183
     */
184
    private function getMachineInfo()
185
    {
186
        $info = array();
187
        $info['hostname'] = php_uname('n');
188
189
        if (empty($info['hostname'])) {
190
            $this->logger->critical('Unable to identify machine hostname', array($this));
191
            throw new RuntimeException('Unable to identify machine hostname');
192
        }
193
        $info['processId'] = $this->procesId;
194
195
        return $info;
196
    }
197
198
    /**
199
     * Create parent node, if needed.
200
     *
201
     * @param string $nodePath
202
     */
203
    private function createParentIfNeeded($nodePath)
204
    {
205
        if (!$this->zk->exists($nodePath)) {
206
            $this->zk->create(
207
                $nodePath, 'Cruftflake machines',
208
                array(array(// acl
209
                    'perms' => \Zookeeper::PERM_ALL,
210
                    'scheme' => 'world',
211
                    'id' => 'anyone',
212
                ))
213
            );
214
        }
215
    }
216
217
    /**
218
     * Machine ID to ZK node.
219
     *
220
     * @param int $id
221
     *
222
     * @return string The node path to use in ZK
223
     */
224
    private function machineToNode($id)
225
    {
226
        return str_pad($id, 4, '0', STR_PAD_LEFT);
227
    }
228
229
    /**
230
     * Set logger.
231
     * 
232
     * @param LoggerInterface $logger
233
     */
234
    public function setLogger(LoggerInterface $logger)
235
    {
236
        $this->logger = $logger;
237
    }
238
}
239