Passed
Push — master ( 69b6a3...2b67eb )
by Dispositif
02:36
created

Stats::sqliteExecWriteOrWait()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 9
c 1
b 1
f 0
dl 0
loc 14
rs 9.9666
cc 3
nc 3
nop 2
1
<?php
2
/*
3
 * This file is part of dispositif/wikibot application (@github)
4
 * 2019-2023 © Philippe M./Irønie  <[email protected]>
5
 * For the full copyright and MIT license information, view the license file.
6
 */
7
8
declare(strict_types=1);
9
10
namespace App\Infrastructure\Monitor;
11
12
use Exception;
13
use SQLite3;
14
15
/**
16
 * SQLite3 base: stats.db
17
 * tables: tagnum, tagnum_daily, tagnum_monthly
18
 */
19
class Stats
20
{
21
    protected const DEFAULT_FILEPATH = __DIR__ . '/../../../log/stats.db';
22
    protected const MAX_TAG_LENGTH = 100;
23
24
    protected SQLite3 $db;
25
26
    public function __construct()
27
    {
28
        if (!class_exists('SQLite3')) {
29
            throw new Exception('Stats ERROR : SQLite 3 NOT supported.');
30
        }
31
        $this->db = new SQLite3(getenv('STATS_FILEPATH') ?: self::DEFAULT_FILEPATH);
32
        $this->createTableIfNotExists();
33
    }
34
35
    protected function createTableIfNotExists(): bool
36
    {
37
        // note : Sqlite VARCHAR(X) do not truncate to the supposed X max chars. VARCHAR is TEXT.
38
        try {
39
            $this->db->exec('CREATE TABLE if not exists tagnum_monthly (
40
                tag TEXT NOT NULL PRIMARY KEY,
41
                num int(11) NOT NULL DEFAULT 1
42
            )');
43
44
            $this->db->exec('CREATE TABLE if not exists tagnum_daily (
45
                tag TEXT NOT NULL PRIMARY KEY,
46
                num int(11) NOT NULL DEFAULT 1
47
            )');
48
49
            return $this->db->exec('CREATE TABLE if not exists tagnum (
50
                tag TEXT NOT NULL PRIMARY KEY,
51
                num int(11) NOT NULL DEFAULT 1
52
            )');
53
        } catch (Exception $e) {
54
            return false;
55
        }
56
    }
57
58
    public function increment(string $tag): bool
59
    {
60
        $tag = $this->formatTag($tag);
61
62
        $this->upsertTag(sprintf('%s.%s', date('Ymd'), $tag), 'tagnum_daily');
63
        $this->upsertTag(sprintf('%s.%s', date('Ym'), $tag), 'tagnum_monthly');
64
65
        return $this->upsertTag($tag);
66
    }
67
68
    private function formatTag(string $tag)
69
    {
70
        return mb_substr($tag, 0, self::MAX_TAG_LENGTH);
71
    }
72
73
    protected function upsertTag(string $tag, string $table = 'tagnum'): bool
74
    {
75
        // `num` default value is 1 so insert is enough to set num=1
76
        try {
77
            // upsert :)
78
            return $this->sqliteExecWriteOrWait(
79
                'INSERT INTO ' . $table . ' (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num+1'
80
            );
81
        } catch (Exception $e) {
82
            return false;
83
        }
84
    }
85
86
    /**
87
     * Sqlite do not allow concurrent write from different process.
88
     * So wait a little and retry.
89
     */
90
    protected function sqliteExecWriteOrWait(string $query, int $maxRetry = 10): bool
91
    {
92
        $retry = 0;
93
        while ($retry < $maxRetry) {
94
            $success = $this->db->exec($query);
95
            if ($success) {
96
                return true;
97
            }
98
            echo "DEBUG: Sqlite retry : wait 100000 µs...\n"; // todo remove
99
            $retry++;
100
            usleep(100000); // 100000 µs = 0.1 s
101
        }
102
103
        return false;
104
    }
105
106
    public function set(string $tag, int $num): bool
107
    {
108
        $tag = $this->formatTag($tag);
109
        try {
110
            return $this->sqliteExecWriteOrWait(
111
                'INSERT OR REPLACE INTO tagnum (tag,num) VALUES("' . $tag . '", ' . $num . ')'
112
            );
113
        } catch (Exception $e) {
114
            return false;
115
        }
116
    }
117
118
    public function decrement(string $tag): bool
119
    {
120
        $tag = $this->formatTag($tag);
121
        try {
122
            return $this->sqliteExecWriteOrWait(
123
                'INSERT INTO tagnum (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num-1'
124
            );
125
        } catch (Exception $e) {
126
            return false;
127
        }
128
    }
129
130
    public function select(string $tag): ?int
131
    {
132
        $tag = $this->formatTag($tag);
133
        try {
134
            $stmt = $this->db->prepare('SELECT tag,num FROM tagnum WHERE tag LIKE :tag');
135
            $stmt->bindValue(':tag', $tag, SQLITE3_TEXT);
136
            $result = $stmt->execute();
137
138
            return $result ? $result->fetchArray(SQLITE3_ASSOC)['num'] : null;
0 ignored issues
show
introduced by
$result is of type SQLite3Result, thus it always evaluated to true.
Loading history...
139
        } catch (Exception $e) {
140
            return null;
141
        }
142
    }
143
}