PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.2
5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / DataAccess / ArchiveWriter.php
matomo / app / core / DataAccess Last commit date
LogQueryBuilder 3 years ago Actions.php 5 years ago ArchiveSelector.php 3 years ago ArchiveTableCreator.php 5 years ago ArchiveTableDao.php 5 years ago ArchiveWriter.php 3 years ago ArchivingDbAdapter.php 4 years ago LogAggregator.php 3 years ago LogQueryBuilder.php 5 years ago LogTableTemporary.php 5 years ago Model.php 3 years ago RawLogDao.php 4 years ago TableMetadata.php 5 years ago
ArchiveWriter.php
375 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9
10 namespace Piwik\DataAccess;
11
12 use Exception;
13 use Piwik\Archive\Chunk;
14 use Piwik\ArchiveProcessor\Rules;
15 use Piwik\ArchiveProcessor;
16 use Piwik\Container\StaticContainer;
17 use Piwik\Date;
18 use Piwik\Db;
19 use Piwik\Db\BatchInsert;
20 use Psr\Log\LoggerInterface;
21
22 /**
23 * This class is used to create a new Archive.
24 * An Archive is a set of reports (numeric and data tables).
25 * New data can be inserted in the archive with insertRecord/insertBulkRecords
26 */
27 class ArchiveWriter
28 {
29 /**
30 * Flag stored at the end of the archiving
31 *
32 * @var int
33 */
34 const DONE_OK = 1;
35 /**
36 * Flag stored at the start of the archiving
37 * When requesting an Archive, we make sure that non-finished archive are not considered valid
38 *
39 * @var int
40 */
41 const DONE_ERROR = 2;
42
43 /**
44 * Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc.
45 * Archives flagged will be regularly purged from the DB.
46 *
47 * This flag is deprecated, new archives should not be written as temporary.
48 *
49 * @var int
50 * @deprecated it should not be used anymore as temporary archives have been removed. It still exists though for
51 * historical reasons.
52 */
53 const DONE_OK_TEMPORARY = 3;
54
55 /**
56 * Flag indicated that archive is done but was marked as invalid later and needs to be re-processed during next archiving process
57 *
58 * @var int
59 */
60 const DONE_INVALIDATED = 4;
61
62 /**
63 * Flag indicating that the archive is
64 *
65 * @var int
66 */
67 const DONE_PARTIAL = 5;
68
69 protected $fields = ['idarchive',
70 'idsite',
71 'date1',
72 'date2',
73 'period',
74 'ts_archived',
75 'name',
76 'value'];
77
78 private $recordsToWriteSpool = [
79 'numeric' => [],
80 'blob' => []
81 ];
82
83 const MAX_SPOOL_SIZE = 50;
84
85 /**
86 * @var int|false
87 */
88 private $idArchive;
89
90 /**
91 * @var int|null
92 */
93 private $idSite;
94
95 /**
96 * @var \Piwik\Segment
97 */
98 private $segment;
99
100 /**
101 * @var \Piwik\Period
102 */
103 private $period;
104
105 /**
106 * @var ArchiveProcessor\Parameters
107 */
108 private $parameters;
109
110 /**
111 * @var string
112 */
113 private $earliestNow;
114
115 /**
116 * ArchiveWriter constructor.
117 * @param ArchiveProcessor\Parameters $params
118 * @param bool $isArchiveTemporary Deprecated. Has no effect.
119 * @throws Exception
120 */
121 public function __construct(ArchiveProcessor\Parameters $params)
122 {
123 $this->idArchive = false;
124 $this->idSite = $params->getSite()->getId();
125 $this->segment = $params->getSegment();
126 $this->period = $params->getPeriod();
127 $this->parameters = $params;
128
129 $idSites = [$this->idSite];
130 $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin());
131
132 $this->dateStart = $this->period->getDateStart();
133 }
134
135 /**
136 * @param string $name
137 * @param string|string[] $values A blob string or an array of blob strings. If an array
138 * is used, the first element in the array will be inserted
139 * with the `$name` name. The others will be splitted into chunks. All subtables
140 * within one chunk will be serialized as an array where the index is the
141 * subtableId.
142 */
143 public function insertBlobRecord($name, $values)
144 {
145 if (is_array($values)) {
146
147 if (isset($values[0])) {
148 // we always store the root table in a single blob for fast access
149 $this->insertRecord($name, $this->compress($values[0]));
150 unset($values[0]);
151 }
152
153 if (!empty($values)) {
154 // we move all subtables into chunks
155 $chunk = new Chunk();
156 $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values);
157 foreach ($chunks as $index => $subtables) {
158 $this->insertRecord($index, $this->compress(serialize($subtables)));
159 }
160 }
161 } else {
162 $values = $this->compress($values);
163 $this->insertRecord($name, $values);
164 }
165 }
166
167 public function getIdArchive()
168 {
169 if ($this->idArchive === false) {
170 throw new Exception("Must call allocateNewArchiveId() first");
171 }
172
173 return $this->idArchive;
174 }
175
176 public function initNewArchive()
177 {
178 $idArchive = $this->allocateNewArchiveId();
179 $this->logArchiveStatusAsIncomplete();
180 return $idArchive;
181 }
182
183 public function finalizeArchive()
184 {
185 $this->flushSpools();
186
187 $numericTable = $this->getTableNumeric();
188 $idArchive = $this->getIdArchive();
189
190 $doneValue = $this->parameters->isPartialArchive() ? self::DONE_PARTIAL : self::DONE_OK;
191 $this->checkDoneValueIsOnlyPartialForPluginArchives($doneValue); // check and log
192
193 $this->getModel()->updateArchiveStatus($numericTable, $idArchive, $this->doneFlag, $doneValue);
194
195 if (!$this->parameters->isPartialArchive()
196 // sanity check, just in case nothing was inserted (the archive status should always be inserted)
197 && !empty($this->earliestNow)
198 ) {
199 $this->getModel()->deleteOlderArchives($this->parameters, $this->doneFlag, $this->earliestNow, $this->idArchive);
200 }
201 }
202
203 protected function compress($data)
204 {
205 if (Db::get()->hasBlobDataType()) {
206 return gzcompress($data);
207 }
208
209 return $data;
210 }
211
212 protected function allocateNewArchiveId()
213 {
214 $numericTable = $this->getTableNumeric();
215
216 $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable);
217 return $this->idArchive;
218 }
219
220 private function getModel()
221 {
222 return new Model();
223 }
224
225 protected function logArchiveStatusAsIncomplete()
226 {
227 $this->insertRecord($this->doneFlag, self::DONE_ERROR);
228 }
229
230 private function batchInsertSpool($valueType)
231 {
232 $records = $this->recordsToWriteSpool[$valueType];
233
234 $bindSql = $this->getInsertRecordBind();
235 $values = [];
236
237 $valueSeen = false;
238 foreach ($records as $record) {
239 // don't record zero
240 if (empty($record[1])) {
241 continue;
242 }
243
244 $bind = $bindSql;
245 $bind[] = $record[0]; // name
246 $bind[] = $record[1]; // value
247 $values[] = $bind;
248
249 $valueSeen = $record[1];
250 }
251
252 if (empty($values)) {
253 return true;
254 }
255
256 $tableName = $this->getTableNameToInsert($valueSeen);
257 $fields = $this->getInsertFields();
258
259 // For numeric records it's faster to do the insert directly; for blobs the data infile is better
260 if ($valueType === 'numeric') {
261 BatchInsert::tableInsertBatchSql($tableName, $fields, $values);
262 } else {
263 BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1');
264 }
265
266 return true;
267 }
268
269 /**
270 * Inserts a record in the right table (either NUMERIC or BLOB)
271 *
272 * @param string $name
273 * @param mixed $value
274 *
275 * @return bool
276 */
277 public function insertRecord($name, $value)
278 {
279 if ($this->isRecordZero($value)) {
280 return false;
281 }
282
283 $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob';
284 $this->recordsToWriteSpool[$valueType][] = [
285 0 => $name,
286 1 => $value
287 ];
288
289 if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) {
290 $this->flushSpool($valueType);
291 }
292
293 return true;
294 }
295
296 public function flushSpools()
297 {
298 $this->flushSpool('numeric');
299 $this->flushSpool('blob');
300 }
301
302 private function flushSpool($valueType)
303 {
304 $numRecords = count($this->recordsToWriteSpool[$valueType]);
305
306 if ($numRecords > 1) {
307 $this->batchInsertSpool($valueType);
308 } elseif ($numRecords === 1) {
309 [$name, $value] = $this->recordsToWriteSpool[$valueType][0];
310 $tableName = $this->getTableNameToInsert($value);
311 $fields = $this->getInsertFields();
312 $record = $this->getInsertRecordBind();
313
314 $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value);
315 }
316 $this->recordsToWriteSpool[$valueType] = [];
317 }
318
319 protected function getInsertRecordBind()
320 {
321 $now = Date::now()->getDatetime();
322 if (empty($this->earliestNow)) {
323 $this->earliestNow = $now;
324 }
325 return [$this->getIdArchive(),
326 $this->idSite,
327 $this->dateStart->toString('Y-m-d'),
328 $this->period->getDateEnd()->toString('Y-m-d'),
329 $this->period->getId(),
330 $now];
331 }
332
333 protected function getTableNameToInsert($value)
334 {
335 if ($this->isRecordNumeric($value)) {
336 return $this->getTableNumeric();
337 }
338
339 return ArchiveTableCreator::getBlobTable($this->dateStart);
340 }
341
342 protected function getTableNumeric()
343 {
344 return ArchiveTableCreator::getNumericTable($this->dateStart);
345 }
346
347 protected function getInsertFields()
348 {
349 return $this->fields;
350 }
351
352 protected function isRecordZero($value)
353 {
354 return ($value === '0' || $value === false || $value === 0 || $value === 0.0);
355 }
356
357 private function isRecordNumeric($value)
358 {
359 return is_numeric($value);
360 }
361
362 private function checkDoneValueIsOnlyPartialForPluginArchives($doneValue)
363 {
364 // if the done flag is not like done%.PluginName, then it shouldn't be a partial archive.
365 // log a warning.
366 if ($doneValue == self::DONE_PARTIAL && strpos($this->doneFlag, '.') == false) {
367 $ex = new \Exception(sprintf("Trying to create a partial archive w/ an all plugins done flag (done flag = %s). This should not happen.",
368 $this->doneFlag));
369 StaticContainer::get(LoggerInterface::class)->warning('{exception}', [
370 'exception' => $ex,
371 ]);
372 }
373 }
374 }
375