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