PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.5
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.5
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
385 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 public $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 * @var string
117 */
118 private $doneFlag;
119
120 /**
121 * @var Date|null
122 */
123 private $dateStart;
124
125 /**
126 * ArchiveWriter constructor.
127 * @param ArchiveProcessor\Parameters $params
128 * @param bool $isArchiveTemporary Deprecated. Has no effect.
129 * @throws Exception
130 */
131 public function __construct(ArchiveProcessor\Parameters $params)
132 {
133 $this->idArchive = false;
134 $this->idSite = $params->getSite()->getId();
135 $this->segment = $params->getSegment();
136 $this->period = $params->getPeriod();
137 $this->parameters = $params;
138
139 $idSites = [$this->idSite];
140 $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin());
141
142 $this->dateStart = $this->period->getDateStart();
143 }
144
145 /**
146 * @param string $name
147 * @param string|string[] $values A blob string or an array of blob strings. If an array
148 * is used, the first element in the array will be inserted
149 * with the `$name` name. The others will be splitted into chunks. All subtables
150 * within one chunk will be serialized as an array where the index is the
151 * subtableId.
152 */
153 public function insertBlobRecord($name, $values)
154 {
155 if (is_array($values)) {
156
157 if (isset($values[0])) {
158 // we always store the root table in a single blob for fast access
159 $this->insertRecord($name, $this->compress($values[0]));
160 unset($values[0]);
161 }
162
163 if (!empty($values)) {
164 // we move all subtables into chunks
165 $chunk = new Chunk();
166 $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values);
167 foreach ($chunks as $index => $subtables) {
168 $this->insertRecord($index, $this->compress(serialize($subtables)));
169 }
170 }
171 } else {
172 $values = $this->compress($values);
173 $this->insertRecord($name, $values);
174 }
175 }
176
177 public function getIdArchive()
178 {
179 if ($this->idArchive === false) {
180 throw new Exception("Must call allocateNewArchiveId() first");
181 }
182
183 return $this->idArchive;
184 }
185
186 public function initNewArchive()
187 {
188 $idArchive = $this->allocateNewArchiveId();
189 $this->logArchiveStatusAsIncomplete();
190 return $idArchive;
191 }
192
193 public function finalizeArchive()
194 {
195 $this->flushSpools();
196
197 $numericTable = $this->getTableNumeric();
198 $idArchive = $this->getIdArchive();
199
200 $doneValue = $this->parameters->isPartialArchive() ? self::DONE_PARTIAL : self::DONE_OK;
201 $this->checkDoneValueIsOnlyPartialForPluginArchives($doneValue); // check and log
202
203 $this->getModel()->updateArchiveStatus($numericTable, $idArchive, $this->doneFlag, $doneValue);
204
205 if (!$this->parameters->isPartialArchive()
206 // sanity check, just in case nothing was inserted (the archive status should always be inserted)
207 && !empty($this->earliestNow)
208 ) {
209 $this->getModel()->deleteOlderArchives($this->parameters, $this->doneFlag, $this->earliestNow, $this->idArchive);
210 }
211 }
212
213 protected function compress($data)
214 {
215 if (Db::get()->hasBlobDataType()) {
216 return gzcompress($data);
217 }
218
219 return $data;
220 }
221
222 protected function allocateNewArchiveId()
223 {
224 $numericTable = $this->getTableNumeric();
225
226 $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable);
227 return $this->idArchive;
228 }
229
230 private function getModel()
231 {
232 return new Model();
233 }
234
235 protected function logArchiveStatusAsIncomplete()
236 {
237 $this->insertRecord($this->doneFlag, self::DONE_ERROR);
238 }
239
240 private function batchInsertSpool($valueType)
241 {
242 $records = $this->recordsToWriteSpool[$valueType];
243
244 $bindSql = $this->getInsertRecordBind();
245 $values = [];
246
247 $valueSeen = false;
248 foreach ($records as $record) {
249 // don't record zero
250 if (empty($record[1])) {
251 continue;
252 }
253
254 $bind = $bindSql;
255 $bind[] = $record[0]; // name
256 $bind[] = $record[1]; // value
257 $values[] = $bind;
258
259 $valueSeen = $record[1];
260 }
261
262 if (empty($values)) {
263 return true;
264 }
265
266 $tableName = $this->getTableNameToInsert($valueSeen);
267 $fields = $this->getInsertFields();
268
269 // For numeric records it's faster to do the insert directly; for blobs the data infile is better
270 if ($valueType === 'numeric') {
271 BatchInsert::tableInsertBatchSql($tableName, $fields, $values);
272 } else {
273 BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1');
274 }
275
276 return true;
277 }
278
279 /**
280 * Inserts a record in the right table (either NUMERIC or BLOB)
281 *
282 * @param string $name
283 * @param mixed $value
284 *
285 * @return bool
286 */
287 public function insertRecord($name, $value)
288 {
289 if ($this->isRecordZero($value)) {
290 return false;
291 }
292
293 $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob';
294 $this->recordsToWriteSpool[$valueType][] = [
295 0 => $name,
296 1 => $value
297 ];
298
299 if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) {
300 $this->flushSpool($valueType);
301 }
302
303 return true;
304 }
305
306 public function flushSpools()
307 {
308 $this->flushSpool('numeric');
309 $this->flushSpool('blob');
310 }
311
312 private function flushSpool($valueType)
313 {
314 $numRecords = count($this->recordsToWriteSpool[$valueType]);
315
316 if ($numRecords > 1) {
317 $this->batchInsertSpool($valueType);
318 } elseif ($numRecords === 1) {
319 [$name, $value] = $this->recordsToWriteSpool[$valueType][0];
320 $tableName = $this->getTableNameToInsert($value);
321 $fields = $this->getInsertFields();
322 $record = $this->getInsertRecordBind();
323
324 $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value);
325 }
326 $this->recordsToWriteSpool[$valueType] = [];
327 }
328
329 protected function getInsertRecordBind()
330 {
331 $now = Date::now()->getDatetime();
332 if (empty($this->earliestNow)) {
333 $this->earliestNow = $now;
334 }
335 return [$this->getIdArchive(),
336 $this->idSite,
337 $this->dateStart->toString('Y-m-d'),
338 $this->period->getDateEnd()->toString('Y-m-d'),
339 $this->period->getId(),
340 $now];
341 }
342
343 protected function getTableNameToInsert($value)
344 {
345 if ($this->isRecordNumeric($value)) {
346 return $this->getTableNumeric();
347 }
348
349 return ArchiveTableCreator::getBlobTable($this->dateStart);
350 }
351
352 protected function getTableNumeric()
353 {
354 return ArchiveTableCreator::getNumericTable($this->dateStart);
355 }
356
357 protected function getInsertFields()
358 {
359 return $this->fields;
360 }
361
362 protected function isRecordZero($value)
363 {
364 return ($value === '0' || $value === false || $value === 0 || $value === 0.0);
365 }
366
367 private function isRecordNumeric($value)
368 {
369 return is_numeric($value);
370 }
371
372 private function checkDoneValueIsOnlyPartialForPluginArchives($doneValue)
373 {
374 // if the done flag is not like done%.PluginName, then it shouldn't be a partial archive.
375 // log a warning.
376 if ($doneValue == self::DONE_PARTIAL && strpos($this->doneFlag, '.') == false) {
377 $ex = new \Exception(sprintf("Trying to create a partial archive w/ an all plugins done flag (done flag = %s). This should not happen.",
378 $this->doneFlag));
379 StaticContainer::get(LoggerInterface::class)->warning('{exception}', [
380 'exception' => $ex,
381 ]);
382 }
383 }
384 }
385