Completed
Push — master ( 956692...a443d7 )
by Tomasz
02:25
created

ZooKeeperConfig::heartbeat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
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
     * Periodically re-syncs with zookeeper, to obtain new machine ID, if necessary.
126
     * 
127
     * {@inheritdoc}
128
     */
129
    public function heartbeat()
130
    {
131
        return false;
132
    }
133
134
    /**
135
     * Compare found machine information with expected values.
136
     * 
137
     * @param array $found
138
     * @param array $expected
139
     *
140
     * @return bool
141
     */
142
    private function compareMachineInfo(array $found, array $expected)
143
    {
144
        if (!isset($found['hostname']) || !isset($found['processId'])) {
145
            return false;
146
        }
147
148
        return $found['hostname'] === $expected['hostname'] && $found['processId'] === $expected['processId'];
149
    }
150
151
    /**
152
     * Attempt to claim and create new machine ID in Zookeeper.
153
     * 
154
     * @param array $children
155
     * @param array $machineInfo
156
     *
157
     * @return int Machine ID.
158
     *
159
     * @throws RuntimeException Thrown, when creation of machine ID has failed.
160
     */
161
    private function createMachineInfo(array $children, array $machineInfo)
162
    {
163
        // find an unused machine number
164
        for ($i = 0; $i < 1024; ++$i) {
165
            $machineNode = $this->machineToNode($i);
166
            if (in_array($machineNode, $children)) {
167
                continue; // already used
168
            }
169
170
            // attempt to claim
171
            $created = $this->zk->create(
172
                "{$this->parentPath}/{$machineNode}", json_encode($machineInfo),
173
                array(array(// acl
174
                    'perms' => \Zookeeper::PERM_ALL,
175
                    'scheme' => 'world',
176
                    'id' => 'anyone',
177
                ))
178
            );
179
            if ($created !== null) {
180
                return $i;
181
            }
182
        }
183
184
        //Creating machine ID failed, throw an error
185
        $this->logger->critical('Cannot locate and claim a free machine ID via ZK', array($this));
186
        throw new RuntimeException('Cannot locate and claim a free machine ID via ZK');
187
    }
188
189
    /**
190
     * Get mac address and hostname.
191
     *
192
     * @return array "hostname","processId", "time" keys
193
     */
194
    private function getMachineInfo()
195
    {
196
        $info = array();
197
        $info['hostname'] = php_uname('n');
198
199
        if (empty($info['hostname'])) {
200
            $this->logger->critical('Unable to identify machine hostname', array($this));
201
            throw new RuntimeException('Unable to identify machine hostname');
202
        }
203
        $info['processId'] = $this->procesId;
204
        $info['time'] = (int) floor(microtime(true) * 1000);
205
206
        return $info;
207
    }
208
209
    /**
210
     * Create parent node, if needed.
211
     *
212
     * @param string $nodePath
213
     */
214
    private function createParentIfNeeded($nodePath)
215
    {
216
        if (!$this->zk->exists($nodePath)) {
217
            $this->zk->create(
218
                $nodePath, 'Cruftflake machines',
219
                array(array(// acl
220
                    'perms' => \Zookeeper::PERM_ALL,
221
                    'scheme' => 'world',
222
                    'id' => 'anyone',
223
                ))
224
            );
225
        }
226
    }
227
228
    /**
229
     * Machine ID to ZK node.
230
     *
231
     * @param int $id
232
     *
233
     * @return string The node path to use in ZK
234
     */
235
    private function machineToNode($id)
236
    {
237
        return str_pad($id, 4, '0', STR_PAD_LEFT);
238
    }
239
240
    /**
241
     * Set logger.
242
     * 
243
     * @param LoggerInterface $logger
244
     */
245
    public function setLogger(LoggerInterface $logger)
246
    {
247
        $this->logger = $logger;
248
    }
249
}
250