| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\ArchiveProcessor; |
|
|
| use Piwik\Archive\ArchiveInvalidator; |
| use Piwik\ArchiveProcessor; |
| use Piwik\Cache; |
| use Piwik\CacheId; |
| use Piwik\Common; |
| use Piwik\Config; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Context; |
| use Piwik\DataAccess\ArchiveSelector; |
| use Piwik\DataAccess\ArchiveWriter; |
| use Piwik\DataAccess\Model; |
| use Piwik\DataAccess\RawLogDao; |
| use Piwik\Date; |
| use Piwik\Period; |
| use Piwik\Piwik; |
| use Piwik\SettingsServer; |
| use Piwik\Site; |
| use Piwik\Log\LoggerInterface; |
| use Piwik\CronArchive\SegmentArchiving; |
|
|
| |
| |
| |
| class Loader |
| { |
| private static $archivingDepth = 0; |
|
|
| |
| |
| |
| |
| |
| private $didReuseArchive = false; |
|
|
| |
| |
| |
| protected $params; |
|
|
| |
| |
| |
| private $invalidator; |
|
|
| |
| |
| |
| private $cache; |
|
|
| |
| |
| |
| private $logger; |
|
|
| |
| |
| |
| private $rawLogDao; |
|
|
| |
| |
| |
| private $dataAccessModel; |
|
|
| |
| |
| |
| private $invalidateBeforeArchiving; |
|
|
| public function __construct(Parameters $params, $invalidateBeforeArchiving = false) |
| { |
| $this->params = $params; |
| $this->invalidateBeforeArchiving = $invalidateBeforeArchiving; |
| $this->invalidator = StaticContainer::get(ArchiveInvalidator::class); |
| $this->cache = Cache::getTransientCache(); |
| $this->logger = StaticContainer::get(LoggerInterface::class); |
| $this->rawLogDao = new RawLogDao(); |
| $this->dataAccessModel = new Model(); |
| } |
|
|
| |
| |
| |
| protected function isThereSomeVisits($visits) |
| { |
| return $visits > 0; |
| } |
|
|
| |
| |
| |
| protected function mustProcessVisitCount($visits) |
| { |
| return $visits === false; |
| } |
|
|
| public function prepareArchive($pluginName) |
| { |
| return Context::changeIdSite($this->params->getSite()->getId(), function () use ($pluginName) { |
| try { |
| ++self::$archivingDepth; |
|
|
| if (self::$archivingDepth === 1) { |
| $this->didReuseArchive = false; |
| } |
|
|
| return $this->prepareArchiveImpl($pluginName); |
| } finally { |
| --self::$archivingDepth; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| private function prepareArchiveImpl($pluginName) |
| { |
| $this->params->setRequestedPlugin($pluginName); |
|
|
| if (SettingsServer::isArchivePhpTriggered()) { |
| $requestedReport = Common::getRequestVar('requestedReport', '', 'string'); |
| if (!empty($requestedReport)) { |
| $this->params->setArchiveOnlyReport($requestedReport); |
| } |
| } |
|
|
| |
| |
| if ( |
| $this->invalidateBeforeArchiving |
| && Rules::isBrowserTriggerEnabled() |
| ) { |
| $this->invalidatedReportsIfNeeded(); |
| } |
| |
| $data = $this->loadArchiveData(); |
| if (sizeof($data) == 2) { |
| $this->didReuseArchive = true; |
| return $data; |
| } |
| [$idArchives, $visits, $visitsConverted, $foundRecords] = $data; |
|
|
| |
| if (ArchiveProcessor::$isRootArchivingRequest && !SettingsServer::isArchivePhpTriggered()) { |
| $lockId = $this->makeArchivingLockId(); |
|
|
| |
| $lock = new LoaderLock($lockId); |
|
|
| |
| $lock->setLock(); |
|
|
| try { |
| $data = $this->loadArchiveData(); |
|
|
| if (sizeof($data) == 2) { |
| $this->didReuseArchive = true; |
| return $data; |
| } |
|
|
| [$idArchives, $visits, $visitsConverted, $foundRecords] = $data; |
|
|
| return $this->insertArchiveData($visits, $visitsConverted, $idArchives, $foundRecords); |
| } finally { |
| $lock->unlock(); |
| } |
| } else { |
| return $this->insertArchiveData($visits, $visitsConverted, $idArchives, $foundRecords); |
| } |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| protected function insertArchiveData($visits, $visitsConverted, $existingArchives, $foundRecords) |
| { |
| if (SettingsServer::isArchivePhpTriggered()) { |
| $this->logger->info("initiating archiving via core:archive for " . $this->params); |
| } |
|
|
| if (!empty($foundRecords)) { |
| $this->params->setFoundRequestedReports($foundRecords); |
| } |
|
|
| [$visits, $visitsConverted] = $this->prepareCoreMetricsArchive($visits, $visitsConverted); |
| [$idArchive, $visits] = $this->prepareAllPluginsArchive($visits, $visitsConverted); |
| $idArchivesToQuery = [$idArchive]; |
|
|
| if (!empty($foundRecords)) { |
| $idArchivesToQuery = array_merge($idArchivesToQuery, $existingArchives ?: []); |
| } |
|
|
| return [$idArchivesToQuery, $visits]; |
| } |
|
|
| |
| |
| |
| |
| private function makeArchivingLockId() |
| { |
| $doneFlag = Rules::getDoneStringFlagFor( |
| [$this->params->getSite()->getId()], |
| $this->params->getSegment(), |
| $this->params->getPeriod()->getLabel(), |
| $this->params->getRequestedPlugin() |
| ); |
|
|
| return $this->params->getPeriod()->getDateStart()->toString() . $this->params->getPeriod()->getDateEnd()->toString() . '.' . $doneFlag; |
| } |
|
|
| |
| |
| |
| protected function loadArchiveData() |
| { |
| |
| |
| |
| $archiveInfo = $this->loadExistingArchiveIdFromDb(); |
| $idArchives = $archiveInfo['idArchives']; |
| $visits = $archiveInfo['visits']; |
| $visitsConverted = $archiveInfo['visitsConverted']; |
| $tsArchived = $archiveInfo['tsArchived']; |
| $doneFlagValue = $archiveInfo['doneFlagValue']; |
| $existingArchives = $archiveInfo['existingRecords']; |
|
|
| $requestedRecords = $this->params->getArchiveOnlyReportAsArray(); |
| $isMissingRequestedRecords = !empty($requestedRecords) && is_array($existingArchives) && count($requestedRecords) != count($existingArchives); |
|
|
| if ( |
| !empty($idArchives) |
| && !Rules::isActuallyForceArchivingSinglePlugin() |
| && !$this->shouldForceInvalidatedArchive($doneFlagValue, $tsArchived) |
| && !$isMissingRequestedRecords |
| ) { |
| |
| |
| return [$idArchives, $visits]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| if ($this->canSkipThisArchive()) { |
| if (!empty($idArchives)) { |
| return [$idArchives, $visits]; |
| } else { |
| return [false, 0]; |
| } |
| } |
|
|
| if (self::$archivingDepth > 1) { |
| $this->logger->debug(sprintf("Sub-period archive requires processing. Archiving depth: %d", self::$archivingDepth)); |
| $this->params->logStatusDebug(); |
| } |
|
|
| return [$idArchives, $visits, $visitsConverted, $existingArchives]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| protected function prepareCoreMetricsArchive($visits, $visitsConverted) |
| { |
| $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits) |
| && !$this->doesRequestedPluginIncludeVisitsSummary(); |
|
|
| if ($createSeparateArchiveForCoreMetrics) { |
| $requestedPlugin = $this->params->getRequestedPlugin(); |
| $requestedReport = $this->params->getArchiveOnlyReport(); |
|
|
| $this->params->setRequestedPlugin('VisitsSummary'); |
| $this->params->setArchiveOnlyReport(null); |
|
|
| $metrics = Context::executeWithQueryParameters(['requestedReport' => ''], function () { |
| $pluginsArchiver = new PluginsArchiver($this->params); |
| $metrics = $pluginsArchiver->callAggregateCoreMetrics(); |
| $pluginsArchiver->finalizeArchive(); |
| return $metrics; |
| }); |
|
|
| $this->params->setRequestedPlugin($requestedPlugin); |
| $this->params->setArchiveOnlyReport($requestedReport); |
|
|
| $visits = $metrics['nb_visits']; |
| $visitsConverted = $metrics['nb_visits_converted']; |
| } |
|
|
| return array($visits, $visitsConverted); |
| } |
|
|
| protected function prepareAllPluginsArchive($visits, $visitsConverted) |
| { |
| $pluginsArchiver = new PluginsArchiver($this->params); |
|
|
| if ( |
| $this->mustProcessVisitCount($visits) |
| || $this->doesRequestedPluginIncludeVisitsSummary() |
| ) { |
| $metrics = $pluginsArchiver->callAggregateCoreMetrics(); |
| $visits = $metrics['nb_visits']; |
| $visitsConverted = $metrics['nb_visits_converted']; |
| } |
|
|
| $forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits(); |
| $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits); |
|
|
| $idArchive = $pluginsArchiver->finalizeArchive(); |
|
|
| return array($idArchive, $visits); |
| } |
|
|
| protected function doesRequestedPluginIncludeVisitsSummary() |
| { |
| $processAllReportsIncludingVisitsSummary = |
| Rules::shouldProcessReportsAllPlugins(array($this->params->getSite()->getId()), $this->params->getSegment(), $this->params->getPeriod()->getLabel()); |
| $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary |
| || $this->params->getRequestedPlugin() == 'VisitsSummary'; |
| return $doesRequestedPluginIncludeVisitsSummary; |
| } |
|
|
| protected function isArchivingForcedToTrigger() |
| { |
| $period = $this->params->getPeriod()->getLabel(); |
| $debugSetting = 'always_archive_data_period'; |
|
|
| if ($period == 'day') { |
| $debugSetting = 'always_archive_data_day'; |
| } elseif ($period == 'range') { |
| $debugSetting = 'always_archive_data_range'; |
| } |
|
|
| return (bool) Config::getInstance()->Debug[$debugSetting]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function loadExistingArchiveIdFromDb() |
| { |
| if ($this->isArchivingForcedToTrigger()) { |
| $this->logger->debug("Archiving forced to trigger for {$this->params}."); |
|
|
| |
| |
| return [ |
| 'idArchives' => false, |
| 'visits' => false, |
| 'visitsConverted' => false, |
| 'archiveExists' => false, |
| 'tsArchived' => false, |
| 'doneFlagValue' => false, |
| 'existingRecords' => null, |
| ]; |
| } |
|
|
| $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed(); |
| $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC); |
| return $result; |
| } |
|
|
| |
| |
| |
| |
| |
| protected function getMinTimeArchiveProcessed() |
| { |
| |
| $isRangeArchiveAndArchivingEnabled = $this->params->getPeriod()->getLabel() == 'range' |
| && Rules::isArchivingEnabledFor([$this->params->getSite()->getId()], $this->params->getSegment(), $this->params->getPeriod()->getLabel()); |
|
|
| if (!$isRangeArchiveAndArchivingEnabled) { |
| $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd()); |
| if ($endDateTimestamp) { |
| |
| return $endDateTimestamp; |
| } |
| } |
|
|
| $dateStart = $this->params->getDateStart(); |
| $period = $this->params->getPeriod(); |
| $segment = $this->params->getSegment(); |
| $site = $this->params->getSite(); |
| |
| return Rules::getMinTimeProcessedForInProgressArchive($dateStart, $period, $segment, $site); |
| } |
|
|
| protected static function determineIfArchivePermanent(Date $dateEnd) |
| { |
| $now = time(); |
| $endTimestampUTC = strtotime($dateEnd->getDateEndUTC()); |
|
|
| if ($endTimestampUTC <= $now) { |
| |
| |
| return $endTimestampUTC; |
| } |
|
|
| return false; |
| } |
|
|
| private function shouldArchiveForSiteEvenWhenNoVisits() |
| { |
| $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits(); |
| return in_array($this->params->getSite()->getId(), $idSitesToArchive); |
| } |
|
|
| private function getIdSitesToArchiveWhenNoVisits() |
| { |
| $cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits'; |
|
|
| if (!$this->cache->contains($cacheKey)) { |
| $idSites = array(); |
|
|
| |
| Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites)); |
|
|
| $this->cache->save($cacheKey, $idSites); |
| } |
|
|
| return $this->cache->fetch($cacheKey); |
| } |
|
|
| |
| public function getReportsToInvalidate() |
| { |
| $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated(); |
|
|
| foreach ($sitesPerDays as $dateStr => $siteIds) { |
| if ( |
| empty($siteIds) |
| || !in_array($this->params->getSite()->getId(), $siteIds) |
| ) { |
| unset($sitesPerDays[$dateStr]); |
| } |
|
|
| $date = Date::factory($dateStr); |
| if ( |
| $date->isEarlier($this->params->getPeriod()->getDateStart()) |
| || $date->isLater($this->params->getPeriod()->getDateEnd()) |
| ) { |
| unset($sitesPerDays[$dateStr]); |
| } |
| } |
|
|
| return $sitesPerDays; |
| } |
|
|
| private function invalidatedReportsIfNeeded() |
| { |
| $sitesPerDays = $this->getReportsToInvalidate(); |
| if (empty($sitesPerDays)) { |
| return; |
| } |
|
|
| foreach ($sitesPerDays as $date => $siteIds) { |
| try { |
| $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), false, $this->params->getSegment()); |
| } catch (\Exception $e) { |
| Site::clearCache(); |
| throw $e; |
| } |
| } |
|
|
| Site::clearCache(); |
| } |
|
|
| public function canSkipThisArchive() |
| { |
| return $this->canSkipThisArchiveWithReason()[0]; |
| } |
|
|
| |
| |
| |
| |
| |
| public function canSkipThisArchiveWithReason(): array |
| { |
| $params = $this->params; |
| $idSite = $params->getSite()->getId(); |
|
|
| $isWebsiteUsingTracker = $this->isWebsiteUsingTheTracker($idSite); |
| $isArchivingForcedWhenNoVisits = $this->shouldArchiveForSiteEvenWhenNoVisits(); |
| $hasSiteVisitsBetweenTimeframe = $this->hasSiteVisitsBetweenTimeframe($idSite, $params->getPeriod()); |
| $hasChildArchivesInPeriod = $this->hasChildArchivesInPeriod($idSite, $params->getPeriod()); |
|
|
| $canSkipArchiveForSegment = $this->canSkipArchiveForSegmentWithReason(); |
|
|
| if ($canSkipArchiveForSegment[0]) { |
| return [ |
| true, |
| 'Skip archive for segment: ' . $canSkipArchiveForSegment[1], |
| ]; |
| } |
|
|
| if (!$isWebsiteUsingTracker) { |
| return [ |
| false, |
| 'Site is not using the JavaScript tracker', |
| ]; |
| } |
|
|
| if ($isArchivingForcedWhenNoVisits) { |
| return [ |
| false, |
| 'Archiving is forced when no visits', |
| ]; |
| } |
|
|
| if ($hasSiteVisitsBetweenTimeframe) { |
| return [ |
| false, |
| 'Site has visits between start and end date', |
| ]; |
| } |
|
|
| if ($hasChildArchivesInPeriod) { |
| return [ |
| false, |
| 'There are child archives in the period', |
| ]; |
| } |
|
|
| return [ |
| true, |
| 'Site is using tracker & archiving is not forced when no visits & site has has no visits between start and end date & there are no child archives in the period', |
| ]; |
| } |
|
|
| public function didReuseArchive(): bool |
| { |
| return $this->didReuseArchive; |
| } |
|
|
| private function hasChildArchivesInPeriod($idSite, Period $period): bool |
| { |
| $cacheKey = CacheId::siteAware('Archiving.hasChildArchivesInPeriod.' . $period->getRangeString(), [$idSite]); |
|
|
| if ($this->cache->contains($cacheKey)) { |
| $hasChildArchivesInPeriod = $this->cache->fetch($cacheKey); |
| } else { |
| $hasChildArchivesInPeriod = $this->dataAccessModel->hasChildArchivesInPeriod($idSite, $period); |
| $this->cache->save($cacheKey, $hasChildArchivesInPeriod); |
| } |
| return $hasChildArchivesInPeriod; |
| } |
|
|
| |
| |
| |
| private function canSkipArchiveForSegmentWithReason(): array |
| { |
| $params = $this->params; |
|
|
| if ($params->getSegment()->isEmpty()) { |
| return [false, 'Segment is empty']; |
| } |
|
|
| if (!empty($params->getRequestedPlugin()) && Rules::isSegmentPluginArchivingDisabled($params->getRequestedPlugin(), $params->getSite()->getId())) { |
| return [true, 'Plugin provided and segment plugin archiving disabled']; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (!Rules::shouldProcessReportsAllPlugins($params->getIdSites(), $params->getSegment(), $params->getPeriod()->getLabel())) { |
| return [false, 'shouldProcessReportsAllPlugins reported false']; |
| } |
|
|
| |
| $segmentArchiving = StaticContainer::get(SegmentArchiving::class); |
| $segmentInfo = $segmentArchiving->findSegmentForHash($params->getSegment()->getHash(), $params->getSite()->getId()); |
|
|
| if (!$segmentInfo) { |
| return [false, 'segment not found for hash']; |
| } |
|
|
| $segmentArchiveStartDate = $segmentArchiving->getReArchiveSegmentStartDate($segmentInfo); |
|
|
| if ($segmentArchiveStartDate !== null && $segmentArchiveStartDate->isLater($params->getPeriod()->getDateEnd()->getEndOfDay())) { |
| $doneFlag = Rules::getDoneStringFlagFor( |
| [$params->getSite()->getId()], |
| $params->getSegment(), |
| $params->getPeriod()->getLabel(), |
| $params->getRequestedPlugin() |
| ); |
|
|
| |
| |
| |
| |
| $hasInvalidationsForPeriodAndName = $this->dataAccessModel->hasInvalidationForPeriodAndName($params->getSite()->getId(), $params->getPeriod(), $doneFlag, $params->getArchiveOnlyReport()); |
|
|
| if ($hasInvalidationsForPeriodAndName) { |
| return [false, 'Has invalidations for period and name']; |
| } else { |
| return [true, 'No invalidations for period and name']; |
| } |
| } |
|
|
| return [false, 'Segment archive date set or segment archive start date is earlier than period end of day']; |
| } |
|
|
| public function canSkipArchiveForSegment() |
| { |
| return $this->canSkipArchiveForSegmentWithReason()[0]; |
| } |
|
|
| private function isWebsiteUsingTheTracker($idSite) |
| { |
| $idSitesNotUsingTracker = self::getSitesNotUsingTracker(); |
|
|
| $isUsingTracker = !in_array($idSite, $idSitesNotUsingTracker); |
|
|
| return $isUsingTracker; |
| } |
|
|
| public static function getSitesNotUsingTracker() |
| { |
| $cache = Cache::getTransientCache(); |
|
|
| $cacheKey = 'Archiving.isWebsiteUsingTheTracker'; |
| $idSitesNotUsingTracker = $cache->fetch($cacheKey); |
| if ($idSitesNotUsingTracker === false || !isset($idSitesNotUsingTracker)) { |
| |
| $idSitesNotUsingTracker = array(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$idSitesNotUsingTracker)); |
|
|
| $cache->save($cacheKey, $idSitesNotUsingTracker); |
| } |
| return $idSitesNotUsingTracker; |
| } |
|
|
| private function hasSiteVisitsBetweenTimeframe($idSite, Period $period): bool |
| { |
| $cacheKeyStr = 'Archiving.hasSiteVisitsBetweenTimeframe.%s.%s'; |
| $cacheKey = CacheId::siteAware(sprintf($cacheKeyStr, $period->getLabel(), $period->getRangeString()), [$idSite]); |
|
|
| if ($this->cache->contains($cacheKey)) { |
| return $this->cache->fetch($cacheKey); |
| } |
|
|
| $timezone = Site::getTimezoneFor($idSite); |
| |
| |
| [$date1, $date2] = $period->getBoundsInTimezone($timezone); |
|
|
| $hasSiteVisitsBetweenTimeframe = $this->rawLogDao->hasSiteVisitsBetweenTimeframe($date1->getDatetime(), $date2->getDatetime(), $idSite); |
| $this->cache->save($cacheKey, $hasSiteVisitsBetweenTimeframe); |
|
|
| if ($hasSiteVisitsBetweenTimeframe) { |
| $currentPeriod = $period; |
| do { |
| $parentPeriodLabel = $currentPeriod->getParentPeriodLabel(); |
| if (!Period\Factory::isPeriodEnabledForAPI($parentPeriodLabel)) { |
| $parentPeriodLabel = null; |
| } |
| if ($parentPeriodLabel) { |
| $parentPeriod = Period\Factory::build($parentPeriodLabel, $date1); |
| $cacheKey = CacheId::siteAware(sprintf($cacheKeyStr, $parentPeriod->getLabel(), $parentPeriod->getRangeString()), [$idSite]); |
| $this->cache->save($cacheKey, true); |
| $currentPeriod = $parentPeriod; |
| } |
| } while ($parentPeriodLabel); |
| } |
|
|
| return $hasSiteVisitsBetweenTimeframe; |
| } |
|
|
| public static function getArchivingDepth() |
| { |
| return self::$archivingDepth; |
| } |
|
|
| private function shouldForceInvalidatedArchive($value, $tsArchived) |
| { |
| $params = $this->params; |
|
|
| |
| if ( |
| $value == ArchiveWriter::DONE_INVALIDATED |
| && Rules::isArchivingEnabledFor([$params->getSite()->getId()], $params->getSegment(), $params->getPeriod()->getLabel()) |
| ) { |
| |
| |
| if (SettingsServer::isArchivePhpTriggered()) { |
| return true; |
| } |
|
|
| |
| $timezone = $params->getSite()->getTimezone(); |
| if (!$params->getPeriod()->isDateInPeriod(Date::factoryInTimezone('today', $timezone))) { |
| return true; |
| } |
|
|
| |
| $minDatetimeArchiveProcessedUTC = Rules::getMinTimeProcessedForInProgressArchive( |
| $params->getDateStart(), |
| $params->getPeriod(), |
| $params->getSegment(), |
| $params->getSite() |
| ); |
| $minDatetimeArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC); |
| if ( |
| $minDatetimeArchiveProcessedUTC |
| && Date::factory($tsArchived)->isEarlier($minDatetimeArchiveProcessedUTC) |
| ) { |
| return true; |
| } |
| } |
|
|
| return false; |
| } |
| } |
|
|