DoctrineConfig::heartbeat()   B
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 34
ccs 21
cts 21
cp 1
rs 8.439
cc 6
eloc 22
nc 9
nop 0
crap 6
1
<?php
2
3
/**
4
 * Doctrine configuration.
5
 * 
6
 * This is designed to be used where each machine uses Doctrine DBAL backend
7
 * to ask and store machine IDs.
8
 * 
9
 * @author Tomasz Struczyński <[email protected]>
10
 */
11
12
namespace Gendoria\CruftFlake\Config;
13
14
use Doctrine\DBAL\Connection;
15
use Doctrine\DBAL\Schema\Schema;
16
use Doctrine\DBAL\Types\Type;
17
use Exception;
18
use RuntimeException;
19
20
/**
21
 * Configuration using doctrine DBAL.
22
 *
23
 * @author Tomasz Struczyński <[email protected]>
24
 */
25
class DoctrineConfig implements ConfigInterface
26
{
27
28
    /**
29
     * Default table name.
30
     * 
31
     * @var string
32
     */
33
    const DEFAULT_TABLE_NAME = "gendoria_cruftflake_id";
34
35
    /**
36
     * Doctrine connection.
37
     * 
38
     * @var Connection
39
     */
40
    private $connection;
41
    
42
    /**
43
     * Session TTL.
44
     * 
45
     * @var integer
46
     */
47
    private $sessionTTL;
48
49
    /**
50
     * Last successfull check.
51
     * 
52
     * @var integer|null
53
     */
54
    private $lastSuccessfullCheck = null;
55
56
    /**
57
     * Database table name.
58
     * 
59
     * @var string
60
     */
61
    private $tableName = self::DEFAULT_TABLE_NAME;
62
    
63
    /**
64
     * Machine ID.
65
     * 
66
     * @var integer
67
     */
68
    private $machineId;    
69
70 11
    public function __construct(Connection $connection, $sessionTTL = 600, $tableName = self::DEFAULT_TABLE_NAME)
71
    {
72 11
        $this->connection = $connection;
73 11
        $this->sessionTTL = $sessionTTL;
74 11
        $this->tableName = $tableName;
75 11
    }
76
77
    /**
78
     * Class destructor. 
79
     * 
80
     * Clears session in DBAL.
81
     */
82 11
    public function __destruct()
83
    {
84 11
        $this->destroySession();
85 11
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 10
    public function getMachine()
91
    {
92 10
        if ($this->machineId === null) {
93
            try {
94 10
                $this->machineId = $this->acquireMachineId();
95 10
            } catch (\RuntimeException $e) {
96 1
                throw $e;
97 1
            } catch (\Exception $e) {
98 1
                throw new \RuntimeException("Cannot acquire machine ID", 500, $e);
99
            }
100 9
        }
101 9
        return $this->machineId;
102
    }
103
104
    /**
105
     * Configuration heartbeat. 
106
     * 
107
     * Heartbeat connects periodically to database to renew session and check its validity.
108
     * 
109
     * @return bool True, if configuration data had been changed during heartbeat.
110
     * 
111
     * @throws RuntimeException Thrown, when we could not create new session and it was needed.
112
     */
113 4
    public function heartbeat()
114
    {
115
        //If we have last successfull check recently new, we don't have to do anything
116 4
        if ($this->lastSuccessfullCheck !== null && time() - $this->lastSuccessfullCheck < $this->sessionTTL / 2) {
117 1
            return false;
118
        }
119
        
120
        //If we don't yet have machine ID, nothing happens.
121 4
        if ($this->machineId === null) {
122 1
            return false;
123
        }
124
        
125
        try {
126 3
            $tmpSuccessfullCheck = time();
127 3
            $qb = $this->connection->createQueryBuilder();
128 3
            $qb->update($this->tableName)
129 3
                ->set('last_access', $tmpSuccessfullCheck)
130 3
                ->where('machine_id = ?')
131 3
                ->setParameter(0, $this->machineId)
132
                ;
133 3
            $rows = $qb->execute();
134
            //Perform garbage collection
135 2
            $this->gc();
136 2
            if ($rows == 0) {
137 1
                $this->machineId = null;
138 1
                $this->lastSuccessfullCheck = null;
139 1
                return true;
140
            }
141 1
            $this->lastSuccessfullCheck = $tmpSuccessfullCheck;
142 1
            return false;
143 1
        } catch (Exception $e) {
144 1
            throw new RuntimeException("Counld not connect to database", 500, $e);
145
        }
146
    }
147
148
    /**
149
     * Return machine ID from DBAL.
150
     * 
151
     * @return integer
152
     * @throws RuntimeException
153
     */
154 10
    private function acquireMachineId()
155
    {
156 10
        $this->gc();
157
158 9
        $time = time();
159 9
        $possibleMachineId = $this->acquireDbId();
160
        
161 9
        if ($possibleMachineId > 1023) {
162 1
            throw new \RuntimeException("Cannot acquire machine ID - too many machines present");
163
        } else {
164 9
            $this->connection->insert($this->tableName, array(
165 9
                'machine_id' => $possibleMachineId,
166 9
                'last_access' => $time,
167 9
            ));
168 9
            return $possibleMachineId;
169
        }
170
    }
171
    
172
    /**
173
     * Acquire next ID from database.
174
     * 
175
     * @return integer
176
     */
177 9
    private function acquireDbId()
178
    {
179 9
        $qbFirst = $this->connection->createQueryBuilder();
180 9
        $qbFirst->select('c1.machine_id AS machine_id')
181 9
            ->from($this->tableName, 'c1')
182 9
            ->where('c1.machine_id=0')
183 9
            ->orderBy('c1.machine_id', 'ASC')
184 9
            ->setMaxResults(1)
185
            ;
186
        //Either the table is empty, or it does not have first ID present.
187 9
        if ($qbFirst->execute()->fetchColumn() === false) {
188 9
            return 0;
189
        }
190
        
191
        //We have at least one ID in database, we should find next one.
192 4
        $qb = $this->connection->createQueryBuilder();
193 4
        $qb->select('c1.machine_id+1 AS machine_id')
194 4
            ->from($this->tableName, 'c1')
195 4
            ->leftJoin('c1', $this->tableName, 'c2', 'c1.machine_id+1 = c2.machine_id')
196 4
            ->leftJoin('c1', $this->tableName, 'c3', 'c1.machine_id-1 = c2.machine_id')
197 4
            ->where('c2.machine_id IS NULL')
198
            //->orWhere('c3.machine_id IS NULL AND c1.machine_id=1')
199 4
            ->orderBy('c1.machine_id', 'ASC')
200 4
            ->setMaxResults(1)
201
            ;
202
        
203 4
        $id = $qb->execute()->fetchColumn();
204 4
        return $id;
205
    }
206
207
    /**
208
     * Destroy session.
209
     */
210 11
    private function destroySession()
211
    {
212
        //Nothing to destroy
213 11
        if ($this->machineId === null) {
214 4
            return;
215
        }
216
        
217
        try {
218 8
            $qb = $this->connection->createQueryBuilder()
219 8
                ->delete($this->tableName)
220 8
                ->where('machine_id = ?')
221 8
                ->setParameter(0, $this->machineId);
222 8
            $qb->execute();
223 8
        } catch (\Exception $e) {
224
            //Nothing can be done here, we'll fail silently
225
        }
226 8
    }
227
228
    /**
229
     * Create database table.
230
     */
231 14
    public static function createTable(Connection $connection, $tableName = self::DEFAULT_TABLE_NAME)
232
    {
233 14
        $schema = new Schema();
234 14
        $myTable = $schema->createTable($tableName);
235 14
        $myTable->addColumn("machine_id", Type::INTEGER, array("unsigned" => true));
236 14
        $myTable->addColumn("last_access", Type::BIGINT, array("unsigned" => true));
237 14
        $myTable->setPrimaryKey(array("machine_id"));
238 14
        $sql = $schema->toSql($connection->getDatabasePlatform());
239 14
        foreach ($sql as $statement) {
240 14
            $connection->exec($statement);
241 14
        }
242 14
    }
243
    
244
    /**
245
     * Garbage collector: remove unused sessions.
246
     */
247 10
    private function gc()
248
    {
249 10
        $lastAccess = time() - $this->sessionTTL;
250 10
        $qb = $this->connection->createQueryBuilder();
251 10
        $qb->delete($this->tableName)
252 10
            ->where('last_access < ?')
253 10
            ->setParameter(0, $lastAccess);
254 10
        $qb->execute();
255
    }
256
}