Completed
Push — master ( b06c47...4a45af )
by Tomasz
04:32
created

ZooKeeperConfig::compareMachineInfo()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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