PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.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 2 years ago ArchiveSelector.php 1 year ago ArchiveTableCreator.php 1 year ago ArchiveTableDao.php 1 year ago ArchiveWriter.php 1 year ago ArchivingDbAdapter.php 1 year ago LogAggregator.php 1 year ago LogQueryBuilder.php 1 year ago LogTableTemporary.php 2 years ago Model.php 1 year ago RawLogDao.php 1 year ago TableMetadata.php 1 year ago
ArchiveWriter.php
326 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\Container\StaticContainer;
16 use Piwik\Date;
17 use Piwik\Db;
18 use Piwik\Db\BatchInsert;
19 use Piwik\Log\LoggerInterface;
20 use Piwik\SettingsServer;
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 public 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 public const DONE_ERROR = 2;
41 /**
42 * Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc.
43 * Archives flagged will be regularly purged from the DB.
44 *
45 * This flag is deprecated, new archives should not be written as temporary.
46 *
47 * @var int
48 * @deprecated it should not be used anymore as temporary archives have been removed. It still exists though for
49 * historical reasons.
50 */
51 public const DONE_OK_TEMPORARY = 3;
52 /**
53 * Flag indicated that archive is done but was marked as invalid later and needs to be re-processed during next archiving process
54 *
55 * @var int
56 */
57 public const DONE_INVALIDATED = 4;
58 /**
59 * Flag indicating that the archive is
60 *
61 * @var int
62 */
63 public const DONE_PARTIAL = 5;
64 /**
65 * Flag indicates an archive that is currently being processed, but has already been invalidated again
66 */
67 public const DONE_ERROR_INVALIDATED = 6;
68 protected $fields = ['idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'];
69 private $recordsToWriteSpool = ['numeric' => [], 'blob' => []];
70 public const MAX_SPOOL_SIZE = 50;
71 /**
72 * @var int|false
73 */
74 public $idArchive;
75 /**
76 * @var int|null
77 */
78 private $idSite;
79 /**
80 * @var \Piwik\Segment
81 */
82 private $segment;
83 /**
84 * @var \Piwik\Period
85 */
86 private $period;
87 /**
88 * @var ArchiveProcessor\Parameters
89 */
90 private $parameters;
91 /**
92 * @var string
93 */
94 private $earliestNow;
95 /**
96 * @var string
97 */
98 private $doneFlag;
99 /**
100 * @var Date|null
101 */
102 private $dateStart;
103 /**
104 * ArchiveWriter constructor.
105 * @param ArchiveProcessor\Parameters $params
106 * @param bool $isArchiveTemporary Deprecated. Has no effect.
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 splitted 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 if (Db::get()->hasBlobDataType()) {
189 return gzcompress($data);
190 }
191 return $data;
192 }
193 protected function allocateNewArchiveId()
194 {
195 $numericTable = $this->getTableNumeric();
196 $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable);
197 return $this->idArchive;
198 }
199 private function getModel()
200 {
201 return new \Piwik\DataAccess\Model();
202 }
203 protected function logArchiveStatusAsIncomplete()
204 {
205 $this->insertRecord($this->doneFlag, self::DONE_ERROR);
206 }
207 private function batchInsertSpool($valueType)
208 {
209 $records = $this->recordsToWriteSpool[$valueType];
210 $bindSql = $this->getInsertRecordBind();
211 $values = [];
212 $valueSeen = \false;
213 foreach ($records as $record) {
214 // don't record zero
215 if (empty($record[1])) {
216 continue;
217 }
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 if ($this->isRecordZero($value)) {
250 return \false;
251 }
252 $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob';
253 $this->recordsToWriteSpool[$valueType][] = [0 => $name, 1 => $value];
254 if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) {
255 $this->flushSpool($valueType);
256 }
257 return \true;
258 }
259 public function flushSpools()
260 {
261 if (SettingsServer::isArchivePhpTriggered()) {
262 Db::executeWithDatabaseWriterReconnectionAttempt(function () {
263 $this->flushSpool('numeric');
264 $this->flushSpool('blob');
265 });
266 } else {
267 $this->flushSpool('numeric');
268 $this->flushSpool('blob');
269 }
270 }
271 private function flushSpool($valueType)
272 {
273 $numRecords = count($this->recordsToWriteSpool[$valueType]);
274 if ($numRecords > 1) {
275 $this->batchInsertSpool($valueType);
276 } elseif ($numRecords === 1) {
277 [$name, $value] = $this->recordsToWriteSpool[$valueType][0];
278 $tableName = $this->getTableNameToInsert($value);
279 $fields = $this->getInsertFields();
280 $record = $this->getInsertRecordBind();
281 $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value);
282 }
283 $this->recordsToWriteSpool[$valueType] = [];
284 }
285 protected function getInsertRecordBind()
286 {
287 $now = Date::now()->getDatetime();
288 if (empty($this->earliestNow)) {
289 $this->earliestNow = $now;
290 }
291 return [$this->getIdArchive(), $this->idSite, $this->dateStart->toString('Y-m-d'), $this->period->getDateEnd()->toString('Y-m-d'), $this->period->getId(), $now];
292 }
293 protected function getTableNameToInsert($value)
294 {
295 if ($this->isRecordNumeric($value)) {
296 return $this->getTableNumeric();
297 }
298 return \Piwik\DataAccess\ArchiveTableCreator::getBlobTable($this->dateStart);
299 }
300 protected function getTableNumeric()
301 {
302 return \Piwik\DataAccess\ArchiveTableCreator::getNumericTable($this->dateStart);
303 }
304 protected function getInsertFields()
305 {
306 return $this->fields;
307 }
308 protected function isRecordZero($value)
309 {
310 return $value === '0' || $value === \false || $value === 0 || $value === 0.0;
311 }
312 private function isRecordNumeric($value)
313 {
314 return is_numeric($value);
315 }
316 private function checkDoneValueIsOnlyPartialForPluginArchives($doneValue)
317 {
318 // if the done flag is not like done%.PluginName, then it shouldn't be a partial archive.
319 // log a warning.
320 if ($doneValue == self::DONE_PARTIAL && strpos($this->doneFlag, '.') == \false) {
321 $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));
322 StaticContainer::get(LoggerInterface::class)->warning('{exception}', ['exception' => $ex]);
323 }
324 }
325 }
326