PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
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 / Archive / ArchiveInvalidator.php
matomo / app / core / Archive Last commit date
ArchiveInvalidator 6 years ago ArchiveInvalidator.php 6 years ago ArchivePurger.php 6 years ago ArchiveQuery.php 6 years ago ArchiveQueryFactory.php 6 years ago Chunk.php 6 years ago DataCollection.php 6 years ago DataTableFactory.php 6 years ago Parameters.php 6 years ago
ArchiveInvalidator.php
514 lines
1 <?php
2 /**
3 * Piwik - 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
10 namespace Piwik\Archive;
11
12 use Piwik\Archive\ArchiveInvalidator\InvalidationResult;
13 use Piwik\ArchiveProcessor\ArchivingStatus;
14 use Piwik\CronArchive\SitesToReprocessDistributedList;
15 use Piwik\DataAccess\ArchiveTableCreator;
16 use Piwik\DataAccess\Model;
17 use Piwik\Date;
18 use Piwik\CliMulti\Process;
19 use Piwik\Db;
20 use Piwik\Option;
21 use Piwik\Common;
22 use Piwik\Piwik;
23 use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
24 use Piwik\Plugins\PrivacyManager\PrivacyManager;
25 use Piwik\Period;
26 use Piwik\Segment;
27 use Piwik\SettingsServer;
28 use Piwik\Site;
29 use Piwik\Tracker\Cache;
30
31 /**
32 * Service that can be used to invalidate archives or add archive references to a list so they will
33 * be invalidated later.
34 *
35 * Archives are put in an "invalidated" state by setting the done flag to `ArchiveWriter::DONE_INVALIDATED`.
36 * This class also adds the archive's associated site to the a distributed list and adding the archive's year month to another
37 * distributed list.
38 *
39 * CronArchive will reprocess the archive data for all sites in the first list, and a scheduled task
40 * will purge the old, invalidated data in archive tables identified by the second list.
41 *
42 * Until CronArchive, or browser triggered archiving, re-processes data for an invalidated archive, the invalidated
43 * archive data will still be displayed in the UI and API.
44 *
45 * ### Deferred Invalidation
46 *
47 * Invalidating archives means running queries on one or more archive tables. In some situations, like during
48 * tracking, this is not desired. In such cases, archive references can be added to a list via the
49 * rememberToInvalidateArchivedReportsLater method, which will add the reference to a distributed list
50 *
51 * Later, during Piwik's normal execution, the list will be read and every archive it references will
52 * be invalidated.
53 */
54 class ArchiveInvalidator
55 {
56 const TRACKER_CACHE_KEY = 'ArchiveInvalidator.rememberToInvalidate';
57
58 private $rememberArchivedReportIdStart = 'report_to_invalidate_';
59
60 /**
61 * @var Model
62 */
63 private $model;
64
65 /**
66 * @var ArchivingStatus
67 */
68 private $archivingStatus;
69
70 public function __construct(Model $model, ArchivingStatus $archivingStatus)
71 {
72 $this->model = $model;
73 $this->archivingStatus = $archivingStatus;
74 }
75
76 public function getAllRememberToInvalidateArchivedReportsLater()
77 {
78 // we do not really have to get the value first. we could simply always try to call set() and it would update or
79 // insert the record if needed but we do not want to lock the table (especially since there are still some
80 // MyISAM installations)
81 $values = Option::getLike('%' . $this->rememberArchivedReportIdStart . '%');
82
83 $all = [];
84 foreach ($values as $name => $value) {
85 $suffix = substr($name, strpos($name, $this->rememberArchivedReportIdStart));
86 $suffix = str_replace($this->rememberArchivedReportIdStart, '', $suffix);
87 list($idSite, $dateStr) = explode('_', $suffix);
88
89 $all[$idSite][$dateStr] = $value;
90 }
91 return $all;
92 }
93
94 public function rememberToInvalidateArchivedReportsLater($idSite, Date $date)
95 {
96 if (SettingsServer::isTrackerApiRequest()) {
97 $value = $this->getRememberedArchivedReportsOptionFromTracker($idSite, $date->toString());
98 } else {
99 // To support multiple transactions at once, look for any other process to have set (and committed)
100 // this report to be invalidated.
101 $key = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
102
103 // we do not really have to get the value first. we could simply always try to call set() and it would update or
104 // insert the record if needed but we do not want to lock the table (especially since there are still some
105 // MyISAM installations)
106 $value = Option::getLike('%' . $key . '%');
107 }
108
109 // getLike() returns an empty array rather than 'false'
110 if (empty($value)) {
111 // In order to support multiple concurrent transactions, add our pid to the end of the key so that it will just insert
112 // rather than waiting on some other process to commit before proceeding.The issue is that with out this, more than
113 // one process is trying to add the exact same value to the table, which causes contention. With the pid suffixed to
114 // the value, each process can successfully enter its own row in the table. The net result will be the same. We could
115 // always just set this, but it would result in a lot of rows in the options table.. more than needed. With this
116 // change you'll have at most N rows per date/site, where N is the number of parallel requests on this same idsite/date
117 // that happen to run in overlapping transactions.
118 $mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString());
119 Option::set($mykey, '1');
120 Cache::clearCacheGeneral();
121 return $mykey;
122 }
123 }
124
125 private function getRememberedArchivedReportsOptionFromTracker($idSite, $dateStr)
126 {
127 $cacheKey = self::TRACKER_CACHE_KEY;
128
129 $generalCache = Cache::getCacheGeneral();
130 if (empty($generalCache[$cacheKey][$idSite][$dateStr])) {
131 return [];
132 }
133
134 return $generalCache[$cacheKey][$idSite][$dateStr];
135 }
136
137 public function getRememberedArchivedReportsThatShouldBeInvalidated()
138 {
139 $reports = Option::getLike('%' . $this->rememberArchivedReportIdStart . '%_%');
140
141 $sitesPerDay = array();
142
143 foreach ($reports as $report => $value) {
144 $report = substr($report, strpos($report, $this->rememberArchivedReportIdStart));
145 $report = str_replace($this->rememberArchivedReportIdStart, '', $report);
146 $report = explode('_', $report);
147 $siteId = (int) $report[0];
148 $date = $report[1];
149
150 if (empty($siteId)) {
151 continue;
152 }
153
154 if (empty($sitesPerDay[$date])) {
155 $sitesPerDay[$date] = array();
156 }
157
158 $sitesPerDay[$date][] = $siteId;
159 }
160
161 return $sitesPerDay;
162 }
163
164 private function buildRememberArchivedReportIdForSite($idSite)
165 {
166 return $this->rememberArchivedReportIdStart . (int) $idSite;
167 }
168
169 private function buildRememberArchivedReportIdForSiteAndDate($idSite, $date)
170 {
171 $id = $this->buildRememberArchivedReportIdForSite($idSite);
172 $id .= '_' . trim($date);
173
174 return $id;
175 }
176
177 // This version is multi process safe on the insert of a new date to invalidate.
178 private function buildRememberArchivedReportIdProcessSafe($idSite, $date)
179 {
180 $id = Common::getRandomString(4, 'abcdefghijklmnoprstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') . '_';
181 $id .= $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date);
182 $id .= '_' . Common::getProcessId();
183
184 return $id;
185 }
186
187 public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
188 {
189 $id = $this->buildRememberArchivedReportIdForSite($idSite);
190 $this->deleteOptionLike($id);
191 Cache::clearCacheGeneral();
192 }
193
194 /**
195 * @internal
196 * After calling this method, make sure to call Cache::clearCacheGeneral(); For performance reasons we don't call
197 * this here immediately in case there are multiple invalidations.
198 */
199 public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date)
200 {
201 $id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
202
203 // The process pid is added to the end of the entry in order to support multiple concurrent transactions.
204 // So this must be a deleteLike call to get all the entries, where there used to only be one.
205 $this->deleteOptionLike($id);
206 Cache::clearCacheGeneral();
207 }
208
209 private function deleteOptionLike($id)
210 {
211 // we're not using deleteLike since it maybe could cause deadlocks see https://github.com/matomo-org/matomo/issues/15545
212 // we want to reduce number of rows scanned and only delete specific primary key
213 $keys = Option::getLike('%' . $id . '%');
214
215 if (empty($keys)) {
216 return;
217 }
218
219 $keys = array_keys($keys);
220
221 $placeholders = Common::getSqlStringFieldsArray($keys);
222
223 $table = Common::prefixTable('option');
224 Db::query('DELETE FROM `' . $table . '` WHERE `option_name` IN (' . $placeholders . ')', $keys);
225 }
226
227 /**
228 * @param $idSites int[]
229 * @param $dates Date[]
230 * @param $period string
231 * @param $segment Segment
232 * @param bool $cascadeDown
233 * @return InvalidationResult
234 * @throws \Exception
235 */
236 public function markArchivesAsInvalidated(array $idSites, array $dates, $period, Segment $segment = null, $cascadeDown = false)
237 {
238 $invalidationInfo = new InvalidationResult();
239
240 // quick fix for #15086, if we're only invalidating today's date for a site, don't add the site to the list of sites
241 // to reprocess.
242 $hasMoreThanJustToday = [];
243 foreach ($idSites as $idSite) {
244 $hasMoreThanJustToday[$idSite] = true;
245 $tz = Site::getTimezoneFor($idSite);
246
247 if (($period == 'day' || $period === false)
248 && count($dates) == 1
249 && $dates[0]->toString() == Date::factoryInTimezone('today', $tz)
250 ) {
251 $hasMoreThanJustToday[$idSite] = false;
252 }
253 }
254
255 /**
256 * Triggered when a Matomo user requested the invalidation of some reporting archives. Using this event, plugin
257 * developers can automatically invalidate another site, when a site is being invalidated. A plugin may even
258 * remove an idSite from the list of sites that should be invalidated to prevent it from ever being
259 * invalidated.
260 *
261 * **Example**
262 *
263 * public function getIdSitesToMarkArchivesAsInvalidates(&$idSites)
264 * {
265 * if (in_array(1, $idSites)) {
266 * $idSites[] = 5; // when idSite 1 is being invalidated, also invalidate idSite 5
267 * }
268 * }
269 *
270 * @param array &$idSites An array containing a list of site IDs which are requested to be invalidated.
271 */
272 Piwik::postEvent('Archiving.getIdSitesToMarkArchivesAsInvalidated', array(&$idSites));
273 // we trigger above event on purpose here and it is good that the segment was created like
274 // `new Segment($segmentString, $idSites)` because when a user adds a site via this event, the added idSite
275 // might not have this segment meaning we avoid a possible error. For the workflow to work, any added or removed
276 // idSite does not need to be added to $segment.
277
278 $datesToInvalidate = $this->removeDatesThatHaveBeenPurged($dates, $invalidationInfo);
279
280 if (empty($period)) {
281 // if the period is empty, we don't need to cascade in any way, since we'll remove all periods
282 $periodDates = $this->getDatesByYearMonthAndPeriodType($dates);
283 } else {
284 $periods = $this->getPeriodsToInvalidate($datesToInvalidate, $period, $cascadeDown);
285 $periodDates = $this->getPeriodDatesByYearMonthAndPeriodType($periods);
286 }
287
288 $periodDates = $this->getUniqueDates($periodDates);
289
290 $this->markArchivesInvalidated($idSites, $periodDates, $segment);
291
292 $yearMonths = array_keys($periodDates);
293 $this->markInvalidatedArchivesForReprocessAndPurge($idSites, $yearMonths, $hasMoreThanJustToday);
294
295 foreach ($idSites as $idSite) {
296 foreach ($dates as $date) {
297 $this->forgetRememberedArchivedReportsToInvalidate($idSite, $date);
298 }
299 }
300 Cache::clearCacheGeneral();
301
302 return $invalidationInfo;
303 }
304
305 /**
306 * @param $idSites int[]
307 * @param $dates Date[]
308 * @param $period string
309 * @param $segment Segment
310 * @param bool $cascadeDown
311 * @return InvalidationResult
312 * @throws \Exception
313 */
314 public function markArchivesOverlappingRangeAsInvalidated(array $idSites, array $dates, Segment $segment = null)
315 {
316 $invalidationInfo = new InvalidationResult();
317
318 $ranges = array();
319 foreach ($dates as $dateRange) {
320 $ranges[] = $dateRange[0] . ',' . $dateRange[1];
321 }
322 $periodsByType = array(Period\Range::PERIOD_ID => $ranges);
323
324 $invalidatedMonths = array();
325 $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE);
326 foreach ($archiveNumericTables as $table) {
327 $tableDate = ArchiveTableCreator::getDateFromTableName($table);
328
329 $result = $this->model->updateArchiveAsInvalidated($table, $idSites, $periodsByType, $segment);
330 $rowsAffected = $result->rowCount();
331 if ($rowsAffected > 0) {
332 $invalidatedMonths[] = $tableDate;
333 }
334 }
335
336 foreach ($idSites as $idSite) {
337 foreach ($dates as $dateRange) {
338 $this->forgetRememberedArchivedReportsToInvalidate($idSite, $dateRange[0]);
339 $invalidationInfo->processedDates[] = $dateRange[0];
340 }
341 }
342
343 Cache::clearCacheGeneral();
344
345 $archivesToPurge = new ArchivesToPurgeDistributedList();
346 $archivesToPurge->add($invalidatedMonths);
347
348 return $invalidationInfo;
349 }
350
351 /**
352 * @param string[][][] $periodDates
353 * @return string[][][]
354 */
355 private function getUniqueDates($periodDates)
356 {
357 $result = array();
358 foreach ($periodDates as $yearMonth => $periodsByYearMonth) {
359 foreach ($periodsByYearMonth as $periodType => $periods) {
360 $result[$yearMonth][$periodType] = array_unique($periods);
361 }
362 }
363 return $result;
364 }
365
366 /**
367 * @param Date[] $dates
368 * @param string $periodType
369 * @param bool $cascadeDown
370 * @return Period[]
371 */
372 private function getPeriodsToInvalidate($dates, $periodType, $cascadeDown)
373 {
374 $periodsToInvalidate = array();
375
376 if ($periodType == 'range') {
377 $rangeString = $dates[0] . ',' . $dates[1];
378 $periodsToInvalidate[] = Period\Factory::build('range', $rangeString);
379 return $periodsToInvalidate;
380 }
381
382 foreach ($dates as $date) {
383 $period = Period\Factory::build($periodType, $date);
384 $periodsToInvalidate[] = $period;
385
386 if ($cascadeDown) {
387 $periodsToInvalidate = array_merge($periodsToInvalidate, $period->getAllOverlappingChildPeriods());
388 }
389
390 if ($periodType != 'year') {
391 $periodsToInvalidate[] = Period\Factory::build('year', $date);
392 }
393 }
394
395 return $periodsToInvalidate;
396 }
397
398 /**
399 * @param Period[] $periods
400 * @return string[][][]
401 */
402 private function getPeriodDatesByYearMonthAndPeriodType($periods)
403 {
404 $result = array();
405 foreach ($periods as $period) {
406 $date = $period->getDateStart();
407 $periodType = $period->getId();
408
409 $yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
410 $dateString = $date->toString();
411 if ($periodType == Period\Range::PERIOD_ID) {
412 $dateString = $period->getRangeString();
413 }
414 $result[$yearMonth][$periodType][] = $dateString;
415 }
416 return $result;
417 }
418
419 /**
420 * Called when deleting all periods.
421 *
422 * @param Date[] $dates
423 * @return string[][][]
424 */
425 private function getDatesByYearMonthAndPeriodType($dates)
426 {
427 $result = array();
428 foreach ($dates as $date) {
429 $yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
430 $result[$yearMonth][null][] = $date->toString();
431
432 // since we're removing all periods, we must make sure to remove year periods as well.
433 // this means we have to make sure the january table is processed.
434 $janYearMonth = $date->toString('Y') . '_01';
435 $result[$janYearMonth][null][] = $date->toString();
436 }
437 return $result;
438 }
439
440 /**
441 * @param int[] $idSites
442 * @param string[][][] $dates
443 * @throws \Exception
444 */
445 private function markArchivesInvalidated($idSites, $dates, Segment $segment = null)
446 {
447 $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE);
448 foreach ($archiveNumericTables as $table) {
449 $tableDate = ArchiveTableCreator::getDateFromTableName($table);
450 if (empty($dates[$tableDate])) {
451 continue;
452 }
453
454 $this->model->updateArchiveAsInvalidated($table, $idSites, $dates[$tableDate], $segment);
455 }
456 }
457
458 /**
459 * @param Date[] $dates
460 * @param InvalidationResult $invalidationInfo
461 * @return \Piwik\Date[]
462 */
463 private function removeDatesThatHaveBeenPurged($dates, InvalidationResult $invalidationInfo)
464 {
465 $this->findOlderDateWithLogs($invalidationInfo);
466
467 $result = array();
468 foreach ($dates as $date) {
469 // we should only delete reports for dates that are more recent than N days
470 if ($invalidationInfo->minimumDateWithLogs
471 && $date->isEarlier($invalidationInfo->minimumDateWithLogs)
472 ) {
473 $invalidationInfo->warningDates[] = $date->toString();
474 continue;
475 }
476
477 $result[] = $date;
478 $invalidationInfo->processedDates[] = $date->toString();
479 }
480 return $result;
481 }
482
483 private function findOlderDateWithLogs(InvalidationResult $info)
484 {
485 // If using the feature "Delete logs older than N days"...
486 $purgeDataSettings = PrivacyManager::getPurgeDataSettings();
487 $logsDeletedWhenOlderThanDays = (int)$purgeDataSettings['delete_logs_older_than'];
488 $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
489
490 if ($logsDeleteEnabled
491 && $logsDeletedWhenOlderThanDays
492 ) {
493 $info->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
494 }
495 }
496
497 /**
498 * @param array $idSites
499 * @param array $yearMonths
500 */
501 private function markInvalidatedArchivesForReprocessAndPurge(array $idSites, $yearMonths, $hasMoreThanJustToday)
502 {
503 $store = new SitesToReprocessDistributedList();
504 foreach ($idSites as $idSite) {
505 if (!empty($hasMoreThanJustToday[$idSite])) {
506 $store->add($idSite);
507 }
508 }
509
510 $archivesToPurge = new ArchivesToPurgeDistributedList();
511 $archivesToPurge->add($yearMonths);
512 }
513 }
514