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

Generator::heartbeat()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
 * Like the Twitter one.
5
 * 
6
 * 64 bits:
7
 * 
8
 * time - 41 bits (millisecond precision w/ a custom epoch gives us 69 years)
9
 * configured machine id - 10 bits - gives us up to 1024 machines
10
 * sequence number - 12 bits - rolls over every 4096 per machine (with protection to avoid rollover in the same ms)
11
 * 
12
 * 32 bits + 9 = 41 bits of time
13
 * 2199023255552 < milliseconds = 2199023255 seconds
14
 *                                2147483647 < max 31 bit int (signed)
15
 *
16
 * @author @davegardnerisme
17
 */
18
19
namespace Gendoria\CruftFlake\Generator;
20
21
use Gendoria\CruftFlake\Config\ConfigInterface;
22
use Gendoria\CruftFlake\Timer\TimerInterface;
23
use InvalidArgumentException;
24
use OverflowException;
25
use UnexpectedValueException;
26
27
class Generator
28
{
29
    /**
30
     * Max timestamp.
31
     */
32
    const MAX_ADJUSTED_TIMESTAMP = 2199023255551;
33
34
    /**
35
     * Hexdec lookup.
36
     * 
37
     * @staticvar array
38
     */
39
    private static $hexdec = array(
40
        '0' => 0,
41
        '1' => 1,
42
        '2' => 2,
43
        '3' => 3,
44
        '4' => 4,
45
        '5' => 5,
46
        '6' => 6,
47
        '7' => 7,
48
        '8' => 8,
49
        '9' => 9,
50
        'a' => 10,
51
        'b' => 11,
52
        'c' => 12,
53
        'd' => 13,
54
        'e' => 14,
55
        'f' => 15,
56
    );
57
58
    /**
59
     * Timer.
60
     * 
61
     * @var TimerInterface
62
     */
63
    private $timer;
64
65
    /**
66
     * Configured machine ID - 10 bits (dec 0 -> 1023).
67
     *
68
     * @var int
69
     */
70
    private $machine;
71
72
    /**
73
     * Epoch - in UTC millisecond timestamp.
74
     *
75
     * @var int
76
     */
77
    private $epoch = 1325376000000;
78
79
    /**
80
     * Sequence number - 12 bits, we auto-increment for same-millisecond collisions.
81
     *
82
     * @var int
83
     */
84
    private $sequence = 1;
85
86
    /**
87
     * The most recent millisecond time window encountered.
88
     *
89
     * @var int
90
     */
91
    private $lastTime = null;
92
93
    /**
94
     * Config.
95
     * 
96
     * @var ConfigInterface
97
     */
98
    private $config;
99
100
    /**
101
     * Constructor.
102
     * 
103
     * @param @inject ConfigInterface $config
104
     * @param @inject TimerInterface  $timer
105
     */
106 22
    public function __construct(ConfigInterface $config, TimerInterface $timer)
107
    {
108 22
        $this->config = $config;
109 22
        $this->machine = $config->getMachine();
110 22
        if (!is_int($this->machine) || $this->machine < 0 || $this->machine > 1023) {
111 4
            throw new InvalidArgumentException(
112
            'Machine identifier invalid -- must be 10 bit integer (0 to 1023)'
113 4
            );
114
        }
115 18
        $this->timer = $timer;
116 18
    }
117
118
    /**
119
     * Generate ID.
120
     *
121
     * @return string A 64 bit integer as a string of numbers (so we can deal
122
     *                with this on 32 bit platforms) 
123
     */
124 16
    public function generate()
125
    {
126 16
        $t = (int) floor($this->timer->getUnixTimestamp() - $this->epoch);
127 16
        if ($t !== $this->lastTime) {
128 16
            $this->generateWithDifferentTime($t);
129 14
        } else {
130 9
            $this->generateWithSameTime();
131
        }
132
133 14
        if ($this->is32Bit()) {
134 1
            return $this->mintId32($t, $this->machine, $this->sequence);
135
        } else {
136 13
            return $this->mintId64($t, $this->machine, $this->sequence);
137
        }
138
    }
139
140
    /**
141
     * Return true, if we are on 32 bit platform.
142
     * 
143
     * @return bool
144
     */
145 13
    protected function is32Bit()
146
    {
147 13
        return PHP_INT_SIZE === 4;
148
    }
149
150
    /**
151
     * Generate new ID with different time.
152
     * 
153
     * @param int $t
154
     *
155
     * @throws UnexpectedValueException
156
     * @throws OverflowException
157
     */
158 16
    private function generateWithDifferentTime($t)
159
    {
160 16
        if ($t < $this->lastTime) {
161 1
            throw new UnexpectedValueException(
162
            'Time moved backwards. We cannot generate IDs for '
163 1
            .($this->lastTime - $t).' milliseconds'
164 1
            );
165 16
        } elseif ($t < 0) {
166 1
            throw new UnexpectedValueException(
167
            'Time is currently set before our epoch - unable '
168 1
            .'to generate IDs for '.(-$t).' milliseconds'
169 1
            );
170 15
        } elseif ($t > self::MAX_ADJUSTED_TIMESTAMP) {
171 1
            throw new OverflowException(
172
            'Timestamp overflow (past end of lifespan) - unable to generate any more IDs'
173 1
            );
174
        }
175 14
        $this->sequence = 0;
176 14
        $this->lastTime = $t;
177 14
    }
178
179
    /**
180
     * Generate new ID with same time.
181
     * 
182
     * @throws OverflowException
183
     */
184 9
    private function generateWithSameTime()
185
    {
186 9
        ++$this->sequence;
187 9
        if ($this->sequence > 4095) {
188 1
            throw new OverflowException(
189
            'Sequence overflow (too many IDs generated) - unable to generate IDs for 1 milliseconds'
190 1
            );
191
        }
192 9
    }
193
194
    /**
195
     * Get stats.
196
     *
197
     * @return GeneratorStatus
198
     */
199 1
    public function status()
200
    {
201 1
        return new GeneratorStatus($this->machine, $this->lastTime,
202 1
            $this->sequence, (PHP_INT_SIZE === 4));
203
    }
204
205
    /**
206
     * Perform configuration heartbeat.
207
     * 
208
     * This refreshes the configuration and may eventually result in obtaining new machine ID.
209
     * It may be usefull, when we want to perform garbage collection for stalled machine IDs
210
     * in some configuration mechanisms.
211
     */
212 1
    public function heartbeat()
213
    {
214 1
        if ($this->config->heartbeat()) {
215 1
            $this->machine = $this->config->getMachine();
216
            //Just to be sure, sleep 1 microsecond to reset sequence
217 1
            usleep(1);
218 1
        }
219 1
    }
220
221 1
    private function mintId32($timestamp, $machine, $sequence)
222
    {
223 1
        $hi = (int) ($timestamp / pow(2, 10));
224 1
        $lo = (int) ($timestamp * pow(2, 22));
225
226
        // stick in the machine + sequence to the low bit
227 1
        $lo = $lo | ($machine << 12) | $sequence;
228
229
        // reconstruct into a string of numbers
230 1
        $hex = pack('N2', $hi, $lo);
231 1
        $unpacked = unpack('H*', $hex);
232 1
        $value = $this->hexdec($unpacked[1]);
233
234 1
        return (string) $value;
235
    }
236
237 13
    private function mintId64($timestamp, $machine, $sequence)
238
    {
239 13
        $timestamp = (int) $timestamp;
240 13
        $value = ($timestamp << 22) | ($machine << 12) | $sequence;
241
242 13
        return (string) $value;
243
    }
244
245 1
    private function hexdec($hex)
246
    {
247 1
        $dec = 0;
248 1
        for ($i = strlen($hex) - 1, $e = 1; $i >= 0; $i--, $e = bcmul($e, 16)) {
249 1
            $factor = self::$hexdec[$hex[$i]];
250 1
            $dec = bcadd($dec, bcmul($factor, $e));
251 1
        }
252
253 1
        return $dec;
254
    }
255
}
256