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