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