PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 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 2 weeks ago Actions.php 8 months ago ArchiveSelector.php 2 weeks ago ArchiveTableCreator.php 2 weeks ago ArchiveTableDao.php 2 weeks ago ArchiveWriter.php 2 weeks ago ArchivingDbAdapter.php 2 weeks ago LogAggregator.php 2 weeks ago LogQueryBuilder.php 2 weeks ago LogTableTemporary.php 2 years ago Model.php 2 weeks ago RawLogDao.php 2 weeks ago TableMetadata.php 1 year ago
ArchiveWriter.php
318 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
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\Config;
16 use Piwik\Container\StaticContainer;
17 use Piwik\Date;
18 use Piwik\Db;
19 use Piwik\Db\BatchInsert;
20 use Piwik\Log\LoggerInterface;
21 use Piwik\SettingsServer;
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 public 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 public const DONE_ERROR = 2;
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 public const DONE_OK_TEMPORARY = 3;
53 /**
54 * Flag indicated that archive is done but was marked as invalid later and needs to be re-processed during next archiving process
55 *
56 * @var int
57 */
58 public const DONE_INVALIDATED = 4;
59 /**
60 * Flag indicating that the archive only contains data for a single requested plugin/report rather
61 * than the full set of reports for the period.
62 *
63 * @var int
64 */
65 public const DONE_PARTIAL = 5;
66 /**
67 * Flag indicates an archive that is currently being processed, but has already been invalidated again
68 */
69 public const DONE_ERROR_INVALIDATED = 6;
70 protected $fields = ['idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'];
71 private $recordsToWriteSpool = ['numeric' => [], 'blob' => []];
72 public const MAX_SPOOL_SIZE = 50;
73 /**
74 * @var int|false
75 */
76 public $idArchive;
77 /**
78 * @var int|null
79 */
80 private $idSite;
81 /**
82 * @var \Piwik\Segment
83 */
84 private $segment;
85 /**
86 * @var \Piwik\Period
87 */
88 private $period;
89 /**
90 * @var ArchiveProcessor\Parameters
91 */
92 private $parameters;
93 /**
94 * @var string
95 */
96 private $earliestNow;
97 /**
98 * @var string
99 */
100 private $doneFlag;
101 /**
102 * @var Date|null
103 */
104 private $dateStart;
105 /**
106 * ArchiveWriter constructor.
107 * @throws Exception
108 */
109 public function __construct(ArchiveProcessor\Parameters $params)
110 {
111 $this->idArchive = \false;
112 $this->idSite = $params->getSite()->getId();
113 $this->segment = $params->getSegment();
114 $this->period = $params->getPeriod();
115 $this->parameters = $params;
116 $idSites = [$this->idSite];
117 $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin());
118 $this->dateStart = $this->period->getDateStart();
119 }
120 /**
121 * @param string $name
122 * @param string|string[] $values A blob string or an array of blob strings. If an array
123 * is used, the first element in the array will be inserted
124 * with the `$name` name. The others will be split into chunks. All subtables
125 * within one chunk will be serialized as an array where the index is the
126 * subtableId.
127 */
128 public function insertBlobRecord($name, $values)
129 {
130 if (is_array($values)) {
131 if (isset($values[0])) {
132 // we always store the root table in a single blob for fast access
133 $this->insertRecord($name, $this->compress($values[0]));
134 unset($values[0]);
135 }
136 if (!empty($values)) {
137 // we move all subtables into chunks
138 $chunk = new Chunk();
139 $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values);
140 foreach ($chunks as $index => $subtables) {
141 $this->insertRecord($index, $this->compress(serialize($subtables)));
142 }
143 }
144 } else {
145 $values = $this->compress($values);
146 $this->insertRecord($name, $values);
147 }
148 }
149 public function getIdArchive()
150 {
151 if ($this->idArchive === \false) {
152 throw new Exception("Must call allocateNewArchiveId() first");
153 }
154 return $this->idArchive;
155 }
156 public function initNewArchive()
157 {
158 $idArchive = $this->allocateNewArchiveId();
159 $this->logArchiveStatusAsIncomplete();
160 return $idArchive;
161 }
162 public function finalizeArchive()
163 {
164 if (empty($this->recordsToWriteSpool['blob']) && count($this->recordsToWriteSpool['numeric']) === 1 && $this->recordsToWriteSpool['numeric'][0][0] === $this->doneFlag && $this->parameters->isPartialArchive()) {
165 // This part avoids writing done flags for empty partial archives:
166 // We skip writing the records to the database if there aren't any blob records to write,
167 // the only available numeric record to write would be the done flag and the archive would only be partial
168 return;
169 }
170 $this->flushSpools();
171 $numericTable = $this->getTableNumeric();
172 $idArchive = $this->getIdArchive();
173 $doneValue = $this->parameters->isPartialArchive() ? self::DONE_PARTIAL : self::DONE_OK;
174 $this->checkDoneValueIsOnlyPartialForPluginArchives($doneValue);
175 // check and log
176 $currentStatus = $this->getModel()->getArchiveStatus($numericTable, $idArchive, $this->doneFlag);
177 // If the current archive was already invalidated during runtime, directly update status to invalidated instead of done
178 if (self::DONE_ERROR_INVALIDATED === $currentStatus) {
179 $doneValue = self::DONE_INVALIDATED;
180 }
181 $this->getModel()->updateArchiveStatus($numericTable, $idArchive, $this->doneFlag, $doneValue);
182 if (!$this->parameters->isPartialArchive() && !empty($this->earliestNow)) {
183 $this->getModel()->deleteOlderArchives($this->parameters, $this->doneFlag, $this->earliestNow, $idArchive);
184 }
185 }
186 protected function compress($data)
187 {
188 $compressionLevel = (int) Config::getInstance()->General['archive_blob_compression_level'];
189 // ensure value is between -1 and 9
190 $compressionLevel = min(max(-1, $compressionLevel), 9);
191 if (Db::get()->hasBlobDataType()) {
192 return gzcompress($data, $compressionLevel);
193 }
194 return $data;
195 }
196 protected function allocateNewArchiveId()
197 {
198 $numericTable = $this->getTableNumeric();
199 $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable);
200 return $this->idArchive;
201 }
202 private function getModel()
203 {
204 return new \Piwik\DataAccess\Model();
205 }
206 protected function logArchiveStatusAsIncomplete()
207 {
208 $this->insertRecord($this->doneFlag, self::DONE_ERROR);
209 }
210 private function batchInsertSpool($valueType)
211 {
212 $records = $this->recordsToWriteSpool[$valueType];
213 $bindSql = $this->getInsertRecordBind();
214 $values = [];
215 $valueSeen = \false;
216 foreach ($records as $record) {
217 $bind = $bindSql;
218 $bind[] = $record[0];
219 // name
220 $bind[] = $record[1];
221 // value
222 $values[] = $bind;
223 $valueSeen = $record[1];
224 }
225 if (empty($values)) {
226 return \true;
227 }
228 $tableName = $this->getTableNameToInsert($valueSeen);
229 $fields = $this->getInsertFields();
230 // For numeric records it's faster to do the insert directly; for blobs the data infile is better
231 if ($valueType === 'numeric') {
232 BatchInsert::tableInsertBatchSql($tableName, $fields, $values);
233 } else {
234 BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = \false, $charset = 'latin1');
235 }
236 return \true;
237 }
238 /**
239 * Inserts a record in the right table (either NUMERIC or BLOB)
240 *
241 * @param string $name
242 * @param mixed $value
243 *
244 * @return bool
245 */
246 public function insertRecord($name, $value)
247 {
248 $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob';
249 $this->recordsToWriteSpool[$valueType][] = [0 => $name, 1 => $value];
250 if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) {
251 $this->flushSpool($valueType);
252 }
253 return \true;
254 }
255 public function flushSpools()
256 {
257 if (SettingsServer::isArchivePhpTriggered()) {
258 Db::executeWithDatabaseWriterReconnectionAttempt(function () {
259 $this->flushSpool('numeric');
260 $this->flushSpool('blob');
261 });
262 } else {
263 $this->flushSpool('numeric');
264 $this->flushSpool('blob');
265 }
266 }
267 private function flushSpool($valueType)
268 {
269 $numRecords = count($this->recordsToWriteSpool[$valueType]);
270 if ($numRecords > 1) {
271 $this->batchInsertSpool($valueType);
272 } elseif ($numRecords === 1) {
273 [$name, $value] = $this->recordsToWriteSpool[$valueType][0];
274 $tableName = $this->getTableNameToInsert($value);
275 $fields = $this->getInsertFields();
276 $record = $this->getInsertRecordBind();
277 $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value);
278 }
279 $this->recordsToWriteSpool[$valueType] = [];
280 }
281 protected function getInsertRecordBind()
282 {
283 $now = Date::now()->getDatetime();
284 if (empty($this->earliestNow)) {
285 $this->earliestNow = $now;
286 }
287 return [$this->getIdArchive(), $this->idSite, $this->dateStart->toString('Y-m-d'), $this->period->getDateEnd()->toString('Y-m-d'), $this->period->getId(), $now];
288 }
289 protected function getTableNameToInsert($value)
290 {
291 if ($this->isRecordNumeric($value)) {
292 return $this->getTableNumeric();
293 }
294 return \Piwik\DataAccess\ArchiveTableCreator::getBlobTable($this->dateStart, \true);
295 }
296 protected function getTableNumeric()
297 {
298 return \Piwik\DataAccess\ArchiveTableCreator::getNumericTable($this->dateStart, \true);
299 }
300 protected function getInsertFields()
301 {
302 return $this->fields;
303 }
304 private function isRecordNumeric($value)
305 {
306 return is_numeric($value);
307 }
308 private function checkDoneValueIsOnlyPartialForPluginArchives($doneValue)
309 {
310 // if the done flag is not like done%.PluginName, then it shouldn't be a partial archive.
311 // log a warning.
312 if ($doneValue == self::DONE_PARTIAL && strpos($this->doneFlag, '.') == \false) {
313 $ex = new \Exception(sprintf("Trying to create a partial archive w/ an all plugins done flag (done flag = %s). This should not happen.", $this->doneFlag));
314 StaticContainer::get(LoggerInterface::class)->warning('{exception}', ['exception' => $ex]);
315 }
316 }
317 }
318