Completed
Push — master ( b6560c...9b1dc4 )
by Tomasz
02:38
created

ConsulConfig::heartbeat()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 26
ccs 13
cts 14
cp 0.9286
rs 6.7272
cc 7
eloc 15
nc 7
nop 0
crap 7.0178
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
 * Description of ConsulConfig
15
 *
16
 * @author Tomasz Struczyński <[email protected]>
17
 */
18
class ConsulConfig implements ConfigInterface
19
{
20
    const DEFAULT_KV_PREFIX = 'service/CruftFlake/machines/';
21
    
22
    /**
23
     * CURL requestor.
24
     * 
25
     * @var ConsulCurl
26
     */
27
    private $curl;
28
    
29
    private $kvPrefix = self::DEFAULT_KV_PREFIX;
30
    
31
    /**
32
     * Consul session ID.
33
     * 
34
     * @var string
35
     */
36
    private $sessionId = "";
37
    
38
    private $sessionTTL;
39
    
40
    /**
41
     * Last successfull check.
42
     * 
43
     * @var integer|null
44
     */
45
    private $lastSuccessfullCheck = null;
46
    
47
    /**
48
     * Machine ID.
49
     * 
50
     * @var integer
51
     */
52
    private $machineId;
53
    
54 7
    function __construct(ConsulCurl $curl, $sessionTTL = 600, $kvPrefix = self::DEFAULT_KV_PREFIX)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
55
    {
56 7
        $this->curl = $curl;
57 7
        $this->kvPrefix = $kvPrefix;
58 7
        $this->sessionTTL = (int)$sessionTTL;
59
        //If we cannot connect to Consul on start, we have a problem.
60 7
        $this->createSession();
61 7
        $this->lastSuccessfullCheck = time();
62 7
    }
63
64
    /**
65
     * On object destruction, we have to destroy session.
66
     */
67 7
    public function __destruct()
68
    {
69 7
        $this->destroySession();
70 7
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 3
    public function getMachine()
76
    {
77 3
        if (!$this->machineId) {
78 3
            $this->machineId = $this->acquireMachineId();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->acquireMachineId() can also be of type string. However, the property $machineId is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
79 2
        }
80 2
        return $this->machineId;
81
    }
82
83
    /**
84
     * Configuration heartbeat. 
85
     * 
86
     * Heartbeat connects periodically to Consul to renew session and check its validity.
87
     * 
88
     * @return bool True, if configuration data had been changed during heartbeat.
89
     * 
90
     * @throws RuntimeException Thrown, when we could not create new session and it was needed.
91
     */
92 4
    public function heartbeat()
93
    {
94
        //If we have last successfull check recently new, we don't have to do anything
95 4
        if ($this->lastSuccessfullCheck && time() - $this->lastSuccessfullCheck < $this->sessionTTL / 2 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lastSuccessfullCheck of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
96 1
            return false;
97
        }
98
        //If session reneval succeedes, update last successfull check.
99 3
        if ($this->curl->performPutRequest("/session/renew/".$this->sessionId)) {
100 1
            $this->lastSuccessfullCheck = time();
101 1
            return false;
102
        }
103
        //Ok, we don't have a valid session. We have to create new one and signal update.
104
        try {
105 2
            $this->createSession();
106 1
            $this->lastSuccessfullCheck = time();
107 1
            $this->machineId = null;
108 1
            return true;
109 1
        } catch (RuntimeException $e) {
110
            //We could not create new session. We can work for some time in 'detached' mode,
111
            //but if our TTL time runs out, we have to throw an exception.
112 1
            if (!$this->lastSuccessfullCheck || time() - $this->lastSuccessfullCheck >= $this->sessionTTL) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lastSuccessfullCheck of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
113 1
                throw $e;
114
            }
115
            return false;
116
        }
117
    }
118
    
119 3
    private function acquireMachineId()
120
    {
121
        //Check, if we don't have existing value for the session
122 3
        $currentValue = $this->curl->performGetRequest('/kv/'.$this->kvPrefix.$this->sessionId);
123 3
        if (!empty($currentValue['Value'])) {
124
            return base64_decode($currentValue['Value']);
125
        }
126
        //Lock main key to block concurrent checks
127 3
        $this->lockKey();
128
        //Get currently locked machine IDs to check, if we can get a new one. If yes, save it.
129 3
        $currentValues = $this->curl->performGetRequest('/kv/'.$this->kvPrefix.'?recurse');
130 3
        if (!is_array($currentValues)) {
131 1
            $currentValues = array();
132 1
        }
133 3
        $machineId = $this->computePossibleMachineId($currentValues);
134 2
        if (!$this->curl->performPutRequest('/kv/'.$this->kvPrefix.$this->sessionId.'?acquire='.$this->sessionId, $machineId)) {
135
            throw new RuntimeException("Could not register machine ID on consul.");
136
        }
137
        //Release the lock on the main key and return machine ID.
138 2
        $this->releaseKey();
139 2
        return $machineId;
140
    }
141
    
142 3
    private function computePossibleMachineId(array $currentValues)
143
    {
144 3
        $usedIds = array();
145 3
        foreach ($currentValues as $currentValue) {
146 2
            if ($currentValue['Key'] == $this->kvPrefix) {
147
                continue;
148 2
            } elseif ($currentValue['Key'] == $this->sessionId) {
149
                return base64_decode($currentValue['Value']);
150
            }
151
            else {
152 2
                $usedIds[] = base64_decode($currentValue['Value']);
153
            }
154 3
        }
155 3
        for ($k = 0; $k < 1024; $k++) {
156 3
            if (!in_array($k, $usedIds)) {
157 2
                return $k;
158
            }
159 2
        }
160 1
        throw new RuntimeException("Cannot acquire machine ID - all machine IDs are used up");
161
    }
162
    
163 3
    private function lockKey()
164
    {
165
        //try to acquire the lock on prefix during whole operation.
166
        do {
167 3
            $acquired = $this->curl->performPutRequest('/kv/'.$this->kvPrefix.'?acquire='.$this->sessionId, $this->sessionId);
168 3
            if (!$acquired) {
169
                sleep(1);
170
            }
171 3
        } while (!$acquired);
172 3
    }
173
    
174 2
    private function releaseKey()
175
    {
176 2
        $this->curl->performPutRequest('/kv/'.$this->kvPrefix.'?release='.$this->sessionId, $this->sessionId);
177 2
    }
178
179 7
    private function createSession()
180
    {
181 7
        $url ='/session/create';
182
        //We create new session with given TTL and with lock delay equal to half of TTL.
183
        $payload = array(
184 7
            'TTL' => $this->sessionTTL.'s',
185 7
            "Behavior" => "delete",
186 7
            'LockDelay' => floor($this->sessionTTL/2).'s',
187 7
        );
188 7
        $returnData = $this->curl->performPutRequest($url, json_encode($payload));
189 7
        if (empty($returnData['ID'])) {
190 1
            throw new RuntimeException("Cannot create session");
191
        }
192 7
        $this->sessionId = $returnData['ID'];
193 7
    }
194
    
195 7
    private function destroySession()
196
    {
197 7
        if ($this->sessionId) {
198 7
            $this->curl->performPutRequest("/session/destroy/".$this->sessionId);
199 7
        }
200 7
    }
201
}
202