ConsulConfig::__destruct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
/**
4
 * To change this license header, choose License Headers in Project Properties.
5
 * To change this template file, choose Tools | Templates
6
 * and open the template in the editor.
7
 */
8
9
namespace Gendoria\CruftFlake\Config;
10
11
use RuntimeException;
12
13
/**
14
 * Configuration using consul instance.
15
 *
16
 * @author Tomasz Struczyński <[email protected]>
17
 */
18
class ConsulConfig implements ConfigInterface
19
{
20
    /**
21
     * Default KV prefix on Consul.
22
     * 
23
     * @var string
24
     */
25
    const DEFAULT_KV_PREFIX = 'service/CruftFlake/machines/';
26
    
27
    /**
28
     * CURL requestor.
29
     * 
30
     * @var ConsulCurl
31
     */
32
    private $curl;
33
    
34
    /**
35
     * Consul KV prefix.
36
     * 
37
     * @var string
38
     */
39
    private $kvPrefix = self::DEFAULT_KV_PREFIX;
40
    
41
    /**
42
     * Consul session ID.
43
     * 
44
     * @var string
45
     */
46
    private $sessionId = "";
47
    
48
    /**
49
     * Session TTL.
50
     * 
51
     * @var integer
52
     */
53
    private $sessionTTL;
54
    
55
    /**
56
     * Last successfull check.
57
     * 
58
     * @var integer|null
59
     */
60
    private $lastSuccessfullCheck = null;
61
    
62
    /**
63
     * Machine ID.
64
     * 
65
     * @var integer
66
     */
67
    private $machineId;
68
    
69
    /**
70
     * Class constructor.
71
     * 
72
     * @param ConsulCurl $curl
73
     * @param integer $sessionTTL
74
     * @param string $kvPrefix
75
     */
76 12
    public function __construct(ConsulCurl $curl, $sessionTTL = 600, $kvPrefix = self::DEFAULT_KV_PREFIX)
77
    {
78 12
        $this->curl = $curl;
79 12
        $this->kvPrefix = $kvPrefix;
80 12
        $this->sessionTTL = (int)$sessionTTL;
81
        //If we cannot connect to Consul on start, we have a problem.
82 12
        $this->createSession();
83 12
        $this->lastSuccessfullCheck = time();
84 12
    }
85
86
    /**
87
     * On object destruction, we have to destroy session.
88
     */
89 12
    public function __destruct()
90
    {
91 12
        $this->destroySession();
92 12
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 7
    public function getMachine()
98
    {
99 7
        if ($this->machineId === null) {
100 7
            $this->machineId = $this->acquireMachineId();
101 5
        }
102 5
        return $this->machineId;
103
    }
104
105
    /**
106
     * Configuration heartbeat. 
107
     * 
108
     * Heartbeat connects periodically to Consul to renew session and check its validity.
109
     * 
110
     * @return bool True, if configuration data had been changed during heartbeat.
111
     * 
112
     * @throws RuntimeException Thrown, when we could not create new session and it was needed.
113
     */
114 5
    public function heartbeat()
115
    {
116
        //If we have last successfull check recently new, we don't have to do anything
117 5
        if ($this->lastSuccessfullCheck !== null && time() - $this->lastSuccessfullCheck < $this->sessionTTL / 2 ) {
118 1
            return false;
119
        }
120
        //If session reneval succeedes, update last successfull check.
121 4
        if ($this->curl->performPutRequest("/session/renew/".$this->sessionId)) {
122 1
            $this->lastSuccessfullCheck = time();
123 1
            return false;
124
        }
125
        //Ok, we don't have a valid session. We have to create new one and signal update.
126
        try {
127 3
            $this->createSession();
128 1
            $this->lastSuccessfullCheck = time();
129 1
            $this->machineId = null;
130 1
            return true;
131 2
        } catch (RuntimeException $e) {
132
            //We could not create new session. We can work for some time in 'detached' mode,
133
            //but if our TTL time runs out, we have to throw an exception.
134 2
            if ($this->lastSuccessfullCheck === null || time() - $this->lastSuccessfullCheck >= $this->sessionTTL) {
135 1
                throw $e;
136
            }
137 1
            return false;
138
        }
139
    }
140
    
141
    /**
142
     * Return machine ID from consul queries.
143
     * 
144
     * @return integer
145
     * @throws RuntimeException
146
     */
147 7
    private function acquireMachineId()
148
    {
149
        //Check, if we don't have existing value for the session
150 7
        $currentValue = $this->curl->performGetRequest('/kv/'.$this->kvPrefix.$this->sessionId);
151 7
        if (!empty($currentValue['Value'])) {
152 1
            return (int)base64_decode($currentValue['Value']);
153
        }
154
        //Lock main key to block concurrent checks
155 6
        $this->lockKey();
156
        //Get currently locked machine IDs to check, if we can get a new one. If yes, save it.
157 6
        $currentValues = $this->curl->performGetRequest('/kv/'.$this->kvPrefix.'?recurse');
158 6
        if (!is_array($currentValues)) {
159 3
            $currentValues = array();
160 3
        }
161 6
        $machineId = $this->computePossibleMachineId($currentValues);
162 5
        if (!$this->curl->performPutRequest('/kv/'.$this->kvPrefix.$this->sessionId.'?acquire='.$this->sessionId, $machineId)) {
163 1
            throw new RuntimeException("Could not register machine ID on consul");
164
        }
165
        //Release the lock on the main key and return machine ID.
166 4
        $this->releaseKey();
167 4
        return (int)$machineId;
168
    }
169
    
170
    /**
171
     * Try to fetch machine ID.
172
     * 
173
     * @param array $currentValues
174
     * @return integer
175
     * @throws RuntimeException
176
     */
177 6
    private function computePossibleMachineId(array $currentValues)
178
    {
179 6
        $usedIds = array();
180 6
        foreach ($currentValues as $currentValue) {
181 3
            if ($currentValue['Key'] == $this->kvPrefix) {
182 2
                continue;
183 3
            } elseif ($currentValue['Key'] == $this->sessionId) {
184 1
                return (int)base64_decode($currentValue['Value']);
185
            }
186
            else {
187 2
                $usedIds[] = (int)base64_decode($currentValue['Value']);
188
            }
189 5
        }
190 5
        for ($k = 0; $k < 1024; $k++) {
191 5
            if (!in_array($k, $usedIds)) {
192 4
                return $k;
193
            }
194 2
        }
195 1
        throw new RuntimeException("Cannot acquire machine ID - all machine IDs are used up");
196
    }
197
    
198
    /**
199
     * Lock master key.
200
     */
201 6
    private function lockKey()
202
    {
203
        //try to acquire the lock on prefix during whole operation.
204 6
        $tryCount=0;
205
        do {
206 6
            $acquired = $this->curl->performPutRequest('/kv/'.$this->kvPrefix.'?acquire='.$this->sessionId."&flags=".$tryCount, $this->sessionId);
207 6
            if (!$acquired) {
208 1
                sleep(1);
209 1
            }
210 6
            $tryCount++;
211 6
        } while (!$acquired);
212 6
    }
213
    
214
    /**
215
     * Release master key.
216
     */
217 4
    private function releaseKey()
218
    {
219 4
        $this->curl->performPutRequest('/kv/'.$this->kvPrefix.'?release='.$this->sessionId, $this->sessionId);
220 4
    }
221
222
    /**
223
     * Create new session.
224
     * 
225
     * @throws RuntimeException
226
     */
227 12
    private function createSession()
228
    {
229 12
        $url ='/session/create';
230
        //We create new session with given TTL and with lock delay equal to half of TTL.
231
        $payload = array(
232 12
            'TTL' => $this->sessionTTL.'s',
233 12
            "Behavior" => "delete",
234 12
            'LockDelay' => floor($this->sessionTTL/2).'s',
235 12
        );
236 12
        $returnData = $this->curl->performPutRequest($url, json_encode($payload));
237 12
        if (empty($returnData['ID'])) {
238 2
            throw new RuntimeException("Cannot create session");
239
        }
240 12
        $this->sessionId = $returnData['ID'];
241 12
    }
242
    
243
    /**
244
     * Destroy session.
245
     */
246 12
    private function destroySession()
247
    {
248 12
        if ($this->sessionId) {
249 12
            $this->curl->performPutRequest("/session/destroy/".$this->sessionId);
250 12
        }
251 12
    }
252
}
253