| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataAccess; |
|
|
| use Exception; |
| use Piwik\Archive\ArchiveInvalidator; |
| use Piwik\ArchiveProcessor\Parameters; |
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\Common; |
| use Piwik\Config\GeneralConfig; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\DbHelper; |
| use Piwik\Period; |
| use Piwik\Segment; |
| use Piwik\Sequence; |
| use Piwik\SettingsServer; |
| use Piwik\Site; |
| use Piwik\Log\LoggerInterface; |
|
|
| |
| |
| |
| class Model |
| { |
| |
| |
| |
| private $logger; |
|
|
| public function __construct(?LoggerInterface $logger = null) |
| { |
| $this->logger = $logger ?: StaticContainer::get(LoggerInterface::class); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getInvalidatedArchiveIdsSafeToDelete($archiveTable, $setGroupContentMaxLen = true) |
| { |
| if ($setGroupContentMaxLen) { |
| try { |
| Db::get()->query('SET SESSION group_concat_max_len=' . (128 * 1024)); |
| } catch (\Exception $ex) { |
| $this->logger->info("Could not set group_concat_max_len MySQL session variable."); |
| } |
| } |
|
|
| $sql = "SELECT idsite, date1, date2, period, name, |
| GROUP_CONCAT(idarchive, '.', value ORDER BY ts_archived DESC, idarchive DESC) as archives |
| FROM `$archiveTable` |
| WHERE name LIKE 'done%' |
| AND `value` NOT IN (" . ArchiveWriter::DONE_ERROR . ", " . ArchiveWriter::DONE_ERROR_INVALIDATED . ") |
| GROUP BY idsite, date1, date2, period, name HAVING count(*) > 1"; |
|
|
| $archiveIds = array(); |
|
|
| $rows = Db::fetchAll($sql); |
| foreach ($rows as $row) { |
| $duplicateArchives = explode(',', $row['archives']); |
|
|
| |
| |
| while (!empty($duplicateArchives)) { |
| $pair = $duplicateArchives[0]; |
| if ($this->isCutOffGroupConcatResult($pair)) { |
| break; |
| } |
|
|
| [$idarchive, $value] = explode('.', $pair); |
|
|
| array_shift($duplicateArchives); |
|
|
| if ($value != ArchiveWriter::DONE_PARTIAL) { |
| break; |
| } |
| } |
|
|
| |
| if (!empty($duplicateArchives)) { |
| foreach ($duplicateArchives as $pair) { |
| if ($this->isCutOffGroupConcatResult($pair)) { |
| $this->logger->info("GROUP_CONCAT cut off the query result, you may have to purge archives again."); |
| break; |
| } |
|
|
| [$idarchive, $value] = explode('.', $pair); |
| $archiveIds[] = $idarchive; |
| } |
| } |
| } |
|
|
| return $archiveIds; |
| } |
|
|
| public function updateArchiveAsInvalidated( |
| ?string $archiveTable, |
| $idSites, |
| $allPeriodsToInvalidate, |
| ?Segment $segment = null, |
| bool $forceInvalidateNonexistentRanges = false, |
| ?string $name = null, |
| bool $doNotCreateInvalidations = false |
| ) { |
| if (empty($idSites)) { |
| return 0; |
| } |
|
|
| $periodCondition = ''; |
| if (!empty($allPeriodsToInvalidate)) { |
| $periodCondition .= " AND ("; |
|
|
| $isFirst = true; |
| |
| foreach ($allPeriodsToInvalidate as $period) { |
| if ($isFirst) { |
| $isFirst = false; |
| } else { |
| $periodCondition .= " OR "; |
| } |
|
|
| if ($period->getLabel() == 'range') { |
| $periodCondition .= "(period = " . (int)$period->getId() |
| . " AND date2 >= '" . $period->getDateStart()->getDatetime() |
| . "' AND date1 <= '" . $period->getDateEnd()->getDatetime() . "')"; |
| } else { |
| $periodCondition .= "(period = " . (int)$period->getId() |
| . " AND date1 = '" . $period->getDateStart()->getDatetime() . "'" |
| . " AND date2 = '" . $period->getDateEnd()->getDatetime() . "')"; |
| } |
| } |
| $periodCondition .= ")"; |
| } |
| if (!empty($name)) { |
| if (strpos($name, '.') !== false) { |
| [$plugin, $name] = explode('.', $name, 2); |
| } else { |
| $plugin = $name; |
| $name = null; |
| } |
| } |
|
|
| $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($segment ?: new Segment('', [])); |
|
|
| if (empty($plugin)) { |
| if (null === $segment) { |
| $nameCondition = "name LIKE '$doneFlag%'"; |
| } else { |
| $nameCondition = "(name = '$doneFlag' OR name LIKE '$doneFlag.%')"; |
| } |
| } else { |
| if (null === $segment) { |
| $nameCondition = "name LIKE '$doneFlag%.$plugin'"; |
| } else { |
| $nameCondition = "name = '$doneFlag.$plugin'"; |
| } |
| } |
|
|
| $idArchives = []; |
| $archivesToInvalidate = []; |
|
|
| |
| |
| if (!empty($archiveTable) && empty($name)) { |
| |
| $sql = "SELECT idarchive, idsite, period, date1, date2, `name`, `value` |
| FROM `$archiveTable` |
| WHERE idsite IN (" . implode(',', $idSites) . ") AND value <> " . ArchiveWriter::DONE_PARTIAL |
| . $periodCondition |
| . " AND $nameCondition"; |
|
|
| $archivesToInvalidate = Db::fetchAll($sql); |
| $idArchives = array_column($archivesToInvalidate, 'idarchive'); |
|
|
| if (!empty($idArchives)) { |
| $idArchives = array_map('intval', $idArchives); |
|
|
| |
| $sql = "UPDATE `$archiveTable` SET `value` = " . ArchiveWriter::DONE_INVALIDATED . " WHERE idarchive IN (" |
| . implode(',', $idArchives) . ") AND value NOT IN (" . ArchiveWriter::DONE_ERROR . ", " . ArchiveWriter::DONE_ERROR_INVALIDATED . ") AND $nameCondition"; |
|
|
| Db::query($sql); |
|
|
| |
| $sql = "UPDATE `$archiveTable` SET `value` = " . ArchiveWriter::DONE_ERROR_INVALIDATED . " WHERE idarchive IN (" |
| . implode(',', $idArchives) . ") AND value = " . ArchiveWriter::DONE_ERROR . " AND $nameCondition"; |
|
|
| Db::query($sql); |
| } |
| } |
|
|
| if ($doNotCreateInvalidations) { |
| return count($idArchives); |
| } |
|
|
| |
| |
| $archivesToCreateInvalidationRowsFor = []; |
| foreach ($archivesToInvalidate as $row) { |
| $archivesToCreateInvalidationRowsFor[$row['idsite']][$row['period']][$row['date1']][$row['date2']][$row['name']] = $row['idarchive']; |
| } |
|
|
| $now = Date::now()->getDatetime(); |
|
|
| $existingInvalidations = $this->getExistingInvalidations($idSites, $periodCondition, $nameCondition); |
|
|
| $hashesOfAllSegmentsToArchiveInCoreArchive = Rules::getSegmentsToProcess($idSites); |
| $hashesOfAllSegmentsToArchiveInCoreArchive = array_map(function ($definition) { |
| return Segment::getSegmentHash($definition); |
| }, $hashesOfAllSegmentsToArchiveInCoreArchive); |
|
|
|
|
| if (empty($plugin)) { |
| $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($segment ?: new Segment('', [])); |
| } else { |
| $doneFlag = Rules::getDoneFlagArchiveContainsOnePlugin($segment ?: new Segment('', []), $plugin); |
| } |
|
|
| $dummyArchives = []; |
| foreach ($idSites as $idSite) { |
| try { |
| $siteCreationTime = Site::getCreationDateFor($idSite); |
| } catch (\Exception $ex) { |
| continue; |
| } |
|
|
| $siteCreationTime = Date::factory($siteCreationTime); |
| foreach ($allPeriodsToInvalidate as $period) { |
| if ( |
| $period->getLabel() == 'range' |
| && !$forceInvalidateNonexistentRanges |
| ) { |
| continue; |
| } |
|
|
| if ($period->getDateEnd()->isEarlier($siteCreationTime)) { |
| continue; |
| } |
|
|
| $date1 = $period->getDateStart()->toString(); |
| $date2 = $period->getDateEnd()->toString(); |
|
|
| |
| |
| |
| $doneFlagsFound = $archivesToCreateInvalidationRowsFor[$idSite][$period->getId()][$date1][$date2] ?? []; |
| $doneFlagsFound = array_keys($doneFlagsFound); |
| $doneFlagsToCheck = array_merge([$doneFlag], $doneFlagsFound); |
| $doneFlagsToCheck = array_unique($doneFlagsToCheck); |
|
|
| foreach ($doneFlagsToCheck as $doneFlagToCheck) { |
| $key = $this->makeExistingInvalidationArrayKey($idSite, $date1, $date2, $period->getId(), $doneFlagToCheck, $name); |
| if (!empty($existingInvalidations[$key])) { |
| continue; |
| } |
|
|
| $hash = $this->getHashFromDoneFlag($doneFlagToCheck); |
| if ( |
| $doneFlagToCheck != $doneFlag |
| && (empty($hash) |
| || !in_array($hash, $hashesOfAllSegmentsToArchiveInCoreArchive) |
| || strpos($doneFlagToCheck, '.') !== false) |
| ) { |
| continue; |
| } |
|
|
| $idArchive = $archivesToCreateInvalidationRowsFor[$idSite][$period->getId()][$date1][$date2][$doneFlagToCheck] ?? null; |
|
|
| $dummyArchives[] = [ |
| 'idarchive' => $idArchive, |
| 'name' => $doneFlagToCheck, |
| 'report' => $name, |
| 'idsite' => $idSite, |
| 'date1' => $period->getDateStart()->getDatetime(), |
| 'date2' => $period->getDateEnd()->getDatetime(), |
| 'period' => $period->getId(), |
| 'ts_invalidated' => $now, |
| ]; |
| } |
| } |
| } |
|
|
| if (!empty($dummyArchives)) { |
| $fields = ['idarchive', 'name', 'report', 'idsite', 'date1', 'date2', 'period', 'ts_invalidated']; |
| Db\BatchInsert::tableInsertBatch(Common::prefixTable('archive_invalidations'), $fields, $dummyArchives); |
| } |
|
|
| return count($idArchives); |
| } |
|
|
| private function getExistingInvalidations($idSites, $periodCondition, $nameCondition) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
|
|
| $idSites = array_map('intval', $idSites); |
|
|
| $sql = "SELECT idsite, date1, date2, period, name, report, COUNT(*) as `count` FROM `$table` |
| WHERE idsite IN (" . implode(',', $idSites) . ") AND status = " . ArchiveInvalidator::INVALIDATION_STATUS_QUEUED . " |
| $periodCondition AND $nameCondition |
| GROUP BY idsite, date1, date2, period, name"; |
| $rows = Db::fetchAll($sql); |
|
|
| $invalidations = []; |
| foreach ($rows as $row) { |
| $key = $this->makeExistingInvalidationArrayKey($row['idsite'], $row['date1'], $row['date2'], $row['period'], $row['name'], $row['report']); |
| $invalidations[$key] = $row['count']; |
| } |
| return $invalidations; |
| } |
|
|
| private function makeExistingInvalidationArrayKey($idSite, $date1, $date2, $period, $name, $report) |
| { |
| return implode('.', [$idSite, $date1, $date2, $period, $name, $report]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function updateRangeArchiveAsInvalidated($archiveTable, $idSites, $allPeriodsToInvalidate, ?Segment $segment = null): void |
| { |
| if (empty($idSites)) { |
| return; |
| } |
|
|
| $bind = array(); |
|
|
| $periodConditions = array(); |
| if (!empty($allPeriodsToInvalidate)) { |
| foreach ($allPeriodsToInvalidate as $period) { |
| $dateConditions = array(); |
|
|
| $dateConditions[] = "(date1 <= ? AND ? <= date2)"; |
| $bind[] = $period->getDateStart()->getDatetime(); |
| $bind[] = $period->getDateEnd()->getDatetime(); |
|
|
| $dateConditionsSql = implode(" OR ", $dateConditions); |
| $periodConditions[] = "(period = 5 AND ($dateConditionsSql))"; |
| } |
| } |
|
|
| if (null === $segment) { |
| $nameCondition = "name LIKE 'done%'"; |
| } else { |
| $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($segment); |
| $nameCondition = "(name = '$doneFlag' OR name LIKE '$doneFlag.%')"; |
| } |
|
|
| $sql = "SELECT idarchive FROM `$archiveTable` " |
| . " WHERE $nameCondition |
| AND idsite IN (" . implode(", ", $idSites) . ") |
| AND (" . implode(" OR ", $periodConditions) . ")"; |
|
|
| $recordsToUpdate = Db::fetchAll($sql, $bind); |
|
|
| if (empty($recordsToUpdate)) { |
| return; |
| } |
|
|
| $idArchives = array_map('intval', array_column($recordsToUpdate, 'idarchive')); |
|
|
| $updateSql = "UPDATE `$archiveTable` SET value = " . ArchiveWriter::DONE_INVALIDATED |
| . " WHERE idarchive IN (" . implode(', ', $idArchives) . ") AND $nameCondition" |
| . " AND value NOT IN (" . ArchiveWriter::DONE_ERROR . ", " . ArchiveWriter::DONE_ERROR_INVALIDATED . ")"; |
|
|
| Db::query($updateSql); |
|
|
| $updateSql = "UPDATE `$archiveTable` SET value = " . ArchiveWriter::DONE_ERROR_INVALIDATED |
| . " WHERE idarchive IN (" . implode(', ', $idArchives) . ") AND $nameCondition AND value = " . ArchiveWriter::DONE_ERROR; |
|
|
| Db::query($updateSql); |
| } |
|
|
| public function getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan) |
| { |
| $temporaryArchiveValues = [ |
| ArchiveWriter::DONE_OK_TEMPORARY, |
| ArchiveWriter::DONE_ERROR, |
| ArchiveWriter::DONE_ERROR_INVALIDATED, |
| ]; |
|
|
| $query = "SELECT idarchive FROM `$archiveTable` |
| WHERE name LIKE 'done%' |
| AND ts_archived < ? |
| AND value IN (" . implode(', ', $temporaryArchiveValues) . ")"; |
|
|
| return Db::fetchAll($query, array($purgeArchivesOlderThan)); |
| } |
|
|
| public function getArchivesMissingDoneFlag(string $archiveTable): array |
| { |
| $query = "SELECT DISTINCT idarchive |
| FROM `$archiveTable` |
| WHERE idarchive NOT IN ( |
| SELECT DISTINCT idarchive |
| FROM `$archiveTable` |
| WHERE name LIKE 'done%' |
| )"; |
|
|
| return Db::fetchAll($query); |
| } |
|
|
| public function deleteArchivesWithPeriod($numericTable, ?string $blobTable, $period, $date) |
| { |
| if (SettingsServer::isArchivePhpTriggered()) { |
| StaticContainer::get(LoggerInterface::class)->info('deleteArchivesWithPeriod: ' . $numericTable . ' with period = ' . $period . ' and date = ' . $date); |
| } |
|
|
| $query = "DELETE FROM `%s` WHERE period = ? AND ts_archived < ?"; |
| $bind = array($period, $date); |
|
|
| $queryObj = Db::query(sprintf($query, $numericTable), $bind); |
| $deletedRows = $queryObj->rowCount(); |
|
|
| if (!empty($blobTable)) { |
| try { |
| $queryObj = Db::query(sprintf($query, $blobTable), $bind); |
| $deletedRows += $queryObj->rowCount(); |
| } catch (Exception $e) { |
| |
| $this->logger->debug("Unable to delete archives by period from {blobTable}.", array( |
| 'blobTable' => $blobTable, |
| 'exception' => $e, |
| )); |
| } |
| } |
|
|
| return $deletedRows; |
| } |
|
|
| public function deleteArchiveIds($numericTable, ?string $blobTable, $idsToDelete) |
| { |
| $idsToDelete = array_values($idsToDelete); |
|
|
| $idsToDelete = array_map('intval', $idsToDelete); |
| $query = "DELETE FROM `%s` WHERE idarchive IN (" . implode(',', $idsToDelete) . ")"; |
|
|
| $queryObj = Db::query(sprintf($query, $numericTable), array()); |
| $deletedRows = $queryObj->rowCount(); |
|
|
| if (!empty($blobTable)) { |
| try { |
| $queryObj = Db::query(sprintf($query, $blobTable), array()); |
| $deletedRows += $queryObj->rowCount(); |
| } catch (Exception $e) { |
| |
| $this->logger->debug("Unable to delete archive IDs from {blobTable}.", array( |
| 'blobTable' => $blobTable, |
| 'exception' => $e, |
| )); |
| } |
| } |
|
|
| return $deletedRows; |
| } |
|
|
| public function deleteOlderArchives(Parameters $params, $name, $tsArchived, $idArchive) |
| { |
| $dateStart = $params->getPeriod()->getDateStart(); |
| $dateEnd = $params->getPeriod()->getDateEnd(); |
|
|
| $numericTable = ArchiveTableCreator::getNumericTable($dateStart, false); |
| if (empty($numericTable)) { |
| return; |
| } |
|
|
| $blobTable = ArchiveTableCreator::getBlobTable($dateStart, false); |
|
|
| $sql = "SELECT idarchive FROM `$numericTable` WHERE idsite = ? AND date1 = ? AND date2 = ? AND period = ? AND name = ? AND ts_archived <= ? AND idarchive < ?"; |
|
|
| $idArchives = Db::fetchAll($sql, [$params->getSite()->getId(), $dateStart->getDatetime(), $dateEnd->getDatetime(), $params->getPeriod()->getId(), $name, $tsArchived, $idArchive]); |
| $idArchives = array_column($idArchives, 'idarchive'); |
| if (empty($idArchives)) { |
| return; |
| } |
|
|
| if (SettingsServer::isArchivePhpTriggered()) { |
| StaticContainer::get(LoggerInterface::class)->info('deleteOlderArchives with ' . $params . ', name = ' . $name . ', ts_archived < ' . $tsArchived . ', idarchive < ' . $idArchive); |
| } |
|
|
| $this->deleteArchiveIds($numericTable, $blobTable, $idArchives); |
| } |
|
|
| public function getArchiveIdAndVisits( |
| $numericTable, |
| $idSite, |
| $period, |
| $dateStartIso, |
| $dateEndIso, |
| $minDatetimeIsoArchiveProcessedUTC, |
| $doneFlags, |
| $doneFlagValues = null |
| ) { |
| $bindSQL = array($idSite, |
| $dateStartIso, |
| $dateEndIso, |
| $period, |
| ); |
|
|
| $sqlWhereArchiveName = self::getNameCondition($doneFlags, $doneFlagValues); |
|
|
| $timeStampWhere = ''; |
| if ($minDatetimeIsoArchiveProcessedUTC) { |
| $timeStampWhere = " AND arc1.ts_archived >= ? "; |
| $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC; |
| } |
|
|
| |
| $sqlQuery = "SELECT arc1.idarchive, arc1.value, arc1.name, arc1.ts_archived, arc1.date1 as startDate, arc2.value as " . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . ", arc3.value as " . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . " |
| FROM `$numericTable` arc1 |
| LEFT JOIN `$numericTable` arc2 on arc2.idarchive = arc1.idarchive and (arc2.name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "') |
| LEFT JOIN `$numericTable` arc3 on arc3.idarchive = arc1.idarchive and (arc3.name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') |
| WHERE arc1.idsite = ? |
| AND arc1.date1 = ? |
| AND arc1.date2 = ? |
| AND arc1.period = ? |
| AND ($sqlWhereArchiveName) |
| $timeStampWhere |
| ORDER BY arc1.ts_archived DESC, arc1.idarchive DESC"; |
|
|
| $results = Db::fetchAll($sqlQuery, $bindSQL); |
|
|
| return $results; |
| } |
|
|
| public function createArchiveTable($tableName, $tableNamePrefix) |
| { |
| $db = Db::get(); |
| $sql = DbHelper::getTableCreateSql($tableNamePrefix); |
|
|
| |
| $tableNamePrefix = Common::prefixTable($tableNamePrefix); |
| $sql = str_replace($tableNamePrefix, $tableName, $sql); |
|
|
| try { |
| $db->query($sql); |
| } catch (Exception $e) { |
| |
| if (!$db->isErrNo($e, '1050')) { |
| throw $e; |
| } |
| } |
|
|
| try { |
| if (ArchiveTableCreator::NUMERIC_TABLE === ArchiveTableCreator::getTypeFromTableName($tableName)) { |
| $sequence = new Sequence($tableName); |
| $sequence->create(); |
| } |
| } catch (Exception $e) { |
| } |
| } |
|
|
| public function getInstalledArchiveTables() |
| { |
| $allArchiveNumeric = Db::get()->fetchCol("SHOW TABLES LIKE '" . Common::prefixTable('archive_numeric%') . "'"); |
| $allArchiveBlob = Db::get()->fetchCol("SHOW TABLES LIKE '" . Common::prefixTable('archive_blob%') . "'"); |
|
|
| return array_merge($allArchiveBlob, $allArchiveNumeric); |
| } |
|
|
| public function allocateNewArchiveId($numericTable) |
| { |
| $sequence = new Sequence($numericTable); |
|
|
| try { |
| $idarchive = $sequence->getNextId(); |
| } catch (Exception $e) { |
| |
| try { |
| $sequence->create(); |
| } catch (Exception $ex) { |
| |
| if (!Db::get()->isErrNo($ex, \Piwik\Updater\Migration\Db::ERROR_CODE_DUPLICATE_ENTRY)) { |
| throw $ex; |
| } |
| } |
|
|
| $idarchive = $sequence->getNextId(); |
| } |
|
|
| return $idarchive; |
| } |
|
|
| public function updateArchiveStatus($numericTable, $archiveId, $doneFlag, $value) |
| { |
| Db::query( |
| "UPDATE $numericTable SET `value` = ? WHERE idarchive = ? and `name` = ?", |
| array($value, $archiveId, $doneFlag) |
| ); |
| } |
|
|
| public function getArchiveStatus($numericTable, $archiveId, $doneFlag): int |
| { |
| return (int) Db::fetchOne( |
| "SELECT value FROM `$numericTable` WHERE idarchive = ? AND `name` = ?", |
| [$archiveId, $doneFlag] |
| ); |
| } |
|
|
| public function insertRecord($tableName, $fields, $record, $name, $value) |
| { |
| |
| $query = "INSERT IGNORE INTO `$tableName` (" . implode(", ", $fields) . ") |
| VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " . end($fields) . " = ?"; |
|
|
| $bindSql = $record; |
| $bindSql[] = $name; |
| $bindSql[] = $value; |
| $bindSql[] = $value; |
|
|
| Db::query($query, $bindSql); |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getSitesWithInvalidatedArchive($numericTable) |
| { |
| $rows = Db::fetchAll("SELECT DISTINCT idsite FROM `$numericTable` WHERE `name` LIKE 'done%' AND `value` IN (" |
| . ArchiveWriter::DONE_INVALIDATED . ")"); |
|
|
| $result = array(); |
| foreach ($rows as $row) { |
| $result[] = $row['idsite']; |
| } |
| return $result; |
| } |
|
|
| |
| |
| |
| |
| |
| public function getArchiveIdsForDeletedSites($archiveTableName) |
| { |
| $sql = "SELECT DISTINCT idsite FROM `$archiveTableName`"; |
| $rows = Db::getReader()->fetchAll($sql, array()); |
|
|
| if (empty($rows)) { |
| return array(); |
| } |
|
|
| $idSitesUsed = array_column($rows, 'idsite'); |
|
|
| $model = new \Piwik\Plugins\SitesManager\Model(); |
| $idSitesExisting = $model->getSitesId(); |
|
|
| $deletedSites = array_diff($idSitesUsed, $idSitesExisting); |
|
|
| if (empty($deletedSites)) { |
| return array(); |
| } |
| $deletedSites = array_values($deletedSites); |
| $deletedSites = array_map('intval', $deletedSites); |
|
|
| $sql = "SELECT DISTINCT idarchive FROM `$archiveTableName` WHERE idsite IN (" . implode(',', $deletedSites) . ")"; |
|
|
| $rows = Db::getReader()->fetchAll($sql, array()); |
|
|
| return array_column($rows, 'idarchive'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function getArchiveIdsForSegments($archiveTableName, array $segments, $oldestToKeep) |
| { |
| $segmentClauses = []; |
| foreach ($segments as $segment) { |
| if (!empty($segment['definition'])) { |
| $segmentClauses[] = $this->getDeletedSegmentWhereClause($segment); |
| } |
| } |
|
|
| if (empty($segmentClauses)) { |
| return array(); |
| } |
|
|
| $segmentClauses = implode(' OR ', $segmentClauses); |
|
|
| $sql = 'SELECT idarchive FROM `' . $archiveTableName . '`' |
| . ' WHERE ts_archived < ?' |
| . ' AND (' . $segmentClauses . ')'; |
|
|
| $rows = Db::fetchAll($sql, array($oldestToKeep)); |
|
|
| return array_column($rows, 'idarchive'); |
| } |
|
|
| private function getDeletedSegmentWhereClause(array $segment) |
| { |
| $idSite = (int)$segment['enable_only_idsite']; |
| $segmentHash = $segment['hash'] ?? ''; |
| |
| if (!ctype_xdigit($segmentHash)) { |
| throw new Exception($segmentHash . ' expected to be an md5 hash'); |
| } |
|
|
| $nameClause = 'name LIKE "done' . $segmentHash . '%"'; |
| $idSiteClause = ''; |
| if ($idSite > 0) { |
| $idSiteClause = ' AND idsite = ' . $idSite; |
| } elseif (! empty($segment['idsites_to_preserve'])) { |
| |
| $idSitesToPreserve = array_map('intval', $segment['idsites_to_preserve']); |
| $idSiteClause = ' AND idsite NOT IN (' . implode(',', $idSitesToPreserve) . ')'; |
| } |
|
|
| return "($nameClause $idSiteClause)"; |
| } |
|
|
| |
| |
| |
| |
| private static function getNameCondition($doneFlags, $possibleValues) |
| { |
| $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; |
|
|
| |
| $result = "((arc1.name IN ($allDoneFlags))"; |
|
|
| if (!empty($possibleValues)) { |
| $result .= " AND (arc1.value IN (" . implode(',', $possibleValues) . ")))"; |
| } |
| $result .= ')'; |
|
|
| return $result; |
| } |
|
|
| |
| |
| |
| |
| public function startArchive($invalidation) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
|
|
| |
| $statement = Db::query("UPDATE `$table` SET `status` = ?, `processing_host` = ?, `process_id` = ?, `ts_started` = NOW() WHERE `idinvalidation` = ? AND `status` = ?", [ |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| gethostname() ?: null, |
| Common::getProcessId(), |
| $invalidation['idinvalidation'], |
| ArchiveInvalidator::INVALIDATION_STATUS_QUEUED, |
| ]); |
|
|
| |
| return $statement->rowCount() > 0; |
| } |
|
|
| public function isSimilarArchiveInProgress($invalidation) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
|
|
| $bind = [ |
| $invalidation['idsite'], |
| $invalidation['period'], |
| $invalidation['date1'], |
| $invalidation['date2'], |
| $invalidation['name'], |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| ]; |
|
|
| if (empty($invalidation['report'])) { |
| $reportClause = "(report IS NULL OR report = '')"; |
| } else { |
| $reportClause = "report = ?"; |
| $bind[] = $invalidation['report']; |
| } |
|
|
| $sql = "SELECT idinvalidation FROM `$table` WHERE idsite = ? AND `period` = ? AND date1 = ? AND date2 = ? AND `name` = ? AND `status` = ? AND ts_started IS NOT NULL AND $reportClause LIMIT 1"; |
| $result = Db::fetchOne($sql, $bind); |
|
|
| return !empty($result); |
| } |
|
|
| public function getInvalidationsInProgress( |
| array $idSites = [], |
| array $processingHosts = [], |
| ?Date $startTime = null, |
| ?Date $endTime = null |
| ): array { |
| $table = Common::prefixTable('archive_invalidations'); |
|
|
| $bind = [ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS]; |
| $whereConditions = ''; |
|
|
| if (!empty($processingHosts)) { |
| $whereConditions .= sprintf(' AND `processing_host` IN (%1$s)', Common::getSqlStringFieldsArray($processingHosts)); |
| $bind = array_merge($bind, $processingHosts); |
| } |
|
|
| if (!empty($idSites)) { |
| $whereConditions .= sprintf(' AND `idsite` IN (' . implode(', ', $idSites) . ')'); |
| } |
|
|
| if (!empty($startTime)) { |
| $whereConditions .= ' AND `ts_started` > ?'; |
| $bind[] = $startTime->toString('Y-m-d H:i:s'); |
| } |
|
|
| if (!empty($endTime)) { |
| $whereConditions .= ' AND `ts_started` < ?'; |
| $bind[] = $endTime->toString('Y-m-d H:i:s'); |
| } |
|
|
| $sql = "SELECT idinvalidation, idsite, period, date1, date2, name, report, ts_invalidated, ts_started, processing_host, process_id FROM `$table` WHERE `status` = ? $whereConditions AND ts_started IS NOT NULL ORDER BY ts_started ASC"; |
| return Db::fetchAll($sql, $bind); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function getNextInvalidatedArchive($idSite, $archivingStartTime, $idInvalidationsToExclude = null, $useLimit = true) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "SELECT * |
| FROM `$table` |
| WHERE idsite = ? AND status != ? AND ts_invalidated <= ?"; |
| $bind = [ |
| $idSite, |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| $archivingStartTime, |
| ]; |
|
|
| if (!empty($idInvalidationsToExclude)) { |
| $idInvalidationsToExclude = array_map('intval', $idInvalidationsToExclude); |
| $sql .= " AND idinvalidation NOT IN (" . implode(',', $idInvalidationsToExclude) . ')'; |
| } |
|
|
| |
| |
| $sql .= " ORDER BY date1 DESC, period ASC, CHAR_LENGTH(name) ASC, idinvalidation DESC"; |
|
|
| if ($useLimit) { |
| $sql .= " LIMIT 1"; |
| return Db::fetchRow($sql, $bind); |
| } else { |
| return Db::fetchAll($sql, $bind); |
| } |
| } |
|
|
| public function deleteInvalidations($archiveInvalidations) |
| { |
| $ids = array_column($archiveInvalidations, 'idinvalidation'); |
| $ids = array_map('intval', $ids); |
|
|
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "DELETE FROM `$table` WHERE idinvalidation IN (" . implode(', ', $ids) . ")"; |
|
|
| Db::query($sql); |
| } |
|
|
| public function removeInvalidationsLike($idSite, $start) |
| { |
| $idSitesClause = $this->getRemoveInvalidationsIdSitesClause($idSite); |
|
|
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "DELETE FROM `$table` WHERE $idSitesClause `name` LIKE ?"; |
|
|
| Db::query($sql, ['done%.' . str_replace('_', "\\_", $start)]); |
| } |
|
|
| public function removeInvalidations($idSite, $plugin, $report) |
| { |
| $idSitesClause = $this->getRemoveInvalidationsIdSitesClause($idSite); |
|
|
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "DELETE FROM `$table` WHERE $idSitesClause `name` LIKE ? AND report = ?"; |
|
|
| Db::query($sql, ['done%.' . str_replace('_', "\\_", $plugin), $report]); |
| } |
|
|
| public function isArchiveAlreadyInProgress($invalidatedArchive) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
|
|
| $bind = [ |
| $invalidatedArchive['idsite'], |
| $invalidatedArchive['date1'], |
| $invalidatedArchive['date2'], |
| $invalidatedArchive['period'], |
| $invalidatedArchive['name'], |
| ]; |
|
|
| $reportClause = "(report = '' OR report IS NULL)"; |
| if (!empty($invalidatedArchive['report'])) { |
| $reportClause = "report = ?"; |
| $bind[] = $invalidatedArchive['report']; |
| } |
|
|
| $sql = "SELECT MAX(idinvalidation) FROM `$table` WHERE idsite = ? AND date1 = ? AND date2 = ? AND `period` = ? AND `name` = ? AND status = 1 AND $reportClause"; |
|
|
| $inProgressInvalidation = Db::fetchOne($sql, $bind); |
| return $inProgressInvalidation; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function hasChildArchivesInPeriod($idSite, Period $period) |
| { |
| $date = $period->getDateStart(); |
| while ($date->isEarlier($period->getDateEnd()->addPeriod(1, 'month'))) { |
| $archiveTable = ArchiveTableCreator::getNumericTable($date, false); |
| if (empty($archiveTable)) { |
| $date = $date->addPeriod(1, 'month'); |
| continue; |
| } |
|
|
| |
| |
| |
| |
| $usableDoneFlags = [ArchiveWriter::DONE_OK, ArchiveWriter::DONE_INVALIDATED, ArchiveWriter::DONE_PARTIAL, ArchiveWriter::DONE_OK_TEMPORARY]; |
|
|
| $sql = "SELECT idarchive |
| FROM `$archiveTable` |
| WHERE idsite = ? AND date1 >= ? AND date2 <= ? AND period < ? AND `name` LIKE 'done%' AND `value` IN (" . implode(', ', $usableDoneFlags) . ") |
| LIMIT 1"; |
| $bind = [$idSite, $period->getDateStart()->getDatetime(), $period->getDateEnd()->getDatetime(), $period->getId()]; |
|
|
| $result = (bool) Db::fetchOne($sql, $bind); |
| if ($result) { |
| return true; |
| } |
|
|
| $date = $date->addPeriod(1, 'month'); |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function hasInvalidationForPeriodAndName($idSite, Period $period, $doneFlag, $report = null) |
| { |
| $table = Common::prefixTable('archive_invalidations'); |
| $report = (!empty($report) && !is_array($report)) ? [$report] : $report; |
|
|
| if (empty($report)) { |
| $sql = "SELECT idinvalidation FROM `$table` WHERE idsite = ? AND date1 = ? AND date2 = ? AND `period` = ? AND `name` = ? AND `report` IS NULL LIMIT 1"; |
| } else { |
| $sql = "SELECT idinvalidation FROM `$table` WHERE idsite = ? AND date1 = ? AND date2 = ? AND `period` = ? AND `name` = ? AND `report` IN (" . Common::getSqlStringFieldsArray($report) . ") LIMIT 1"; |
| } |
|
|
| $bind = [ |
| $idSite, |
| $period->getDateStart()->toString(), |
| $period->getDateEnd()->toString(), |
| $period->getId(), |
| $doneFlag, |
| ]; |
|
|
| if (!empty($report)) { |
| $bind = array_merge($bind, $report); |
| } |
|
|
| $idInvalidation = Db::fetchOne($sql, $bind); |
|
|
| if (empty($idInvalidation)) { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| public function deleteInvalidationsForSites(array $idSites) |
| { |
| $idSites = array_map('intval', $idSites); |
|
|
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "DELETE FROM `$table` WHERE idsite IN (" . implode(',', $idSites) . ")"; |
|
|
| Db::query($sql); |
| } |
|
|
| public function deleteInvalidationsForDeletedSites() |
| { |
| $siteTable = Common::prefixTable('site'); |
| $table = Common::prefixTable('archive_invalidations'); |
| $sql = "DELETE a FROM `$table` a LEFT JOIN `$siteTable` s ON a.idsite = s.idsite WHERE s.idsite IS NULL"; |
| Db::query($sql); |
| } |
|
|
| private function getRemoveInvalidationsIdSitesClause($idSite) |
| { |
| if ($idSite === 'all') { |
| return ''; |
| } |
|
|
| $idSites = is_array($idSite) ? $idSite : [$idSite]; |
| $idSites = array_map('intval', $idSites); |
| $idSitesStr = implode(',', $idSites); |
|
|
| return "idsite IN ($idSitesStr) AND"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function releaseInProgressInvalidations(array $idinvalidations): int |
| { |
| $idinvalidations = array_map('intval', $idinvalidations); |
| $table = Common::prefixTable('archive_invalidations'); |
| $changedCount = 0; |
|
|
| $sql = "SELECT * FROM `$table` WHERE idinvalidation IN (" . implode(',', $idinvalidations) . ")"; |
| $invalidations = Db::fetchAll($sql); |
|
|
| |
| foreach ($invalidations as $invalidation) { |
| |
| $query = "SELECT COUNT(*) FROM `$table` WHERE name = ? AND idsite = ? AND date1 = ? AND date2 = ? AND period = ? AND " |
| . "(status = ? OR (status = ? AND ts_started > ?)) AND idinvalidation != ?"; |
| $bind = [ |
| $invalidation['name'], |
| $invalidation['idsite'], |
| $invalidation['date1'], |
| $invalidation['date2'], |
| $invalidation['period'], |
| ArchiveInvalidator::INVALIDATION_STATUS_QUEUED, |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| $invalidation['ts_invalidated'], |
| $invalidation['idinvalidation'], |
| ]; |
|
|
| if (empty($invalidation['report'])) { |
| $query .= " AND (report IS NULL OR report = '')"; |
| } else { |
| $query .= " AND report = ?"; |
| $bind[] = $invalidation['report']; |
| } |
|
|
| $count = Db::fetchOne($query, $bind); |
|
|
| if ($count > 0) { |
| $this->logger->info( |
| 'Found duplicate invalidation for params (name = {name}, idsite = {idsite}, date1 = {date1}, date2 = {date2}, period = {period}, report = {report}). Removing invalidation {idinvalidation} instead of resetting it.', |
| $invalidation |
| ); |
|
|
| $sql = "DELETE FROM `$table` WHERE status = ? AND idinvalidation = ?"; |
|
|
| $bind = [ |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| $invalidation['idinvalidation'], |
| ]; |
|
|
| $query = Db::query($sql, $bind); |
| $changedCount += $query->rowCount(); |
| } else { |
| $sql = "UPDATE `$table` SET status = ?, processing_host = NULL, process_id = NULL, ts_started = NULL WHERE status = ? AND idinvalidation = ?"; |
|
|
| $bind = [ |
| ArchiveInvalidator::INVALIDATION_STATUS_QUEUED, |
| ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS, |
| $invalidation['idinvalidation'], |
| ]; |
|
|
| $query = Db::query($sql, $bind); |
| $changedCount += $query->rowCount(); |
| } |
| } |
|
|
| return $changedCount; |
| } |
|
|
| public function resetFailedArchivingJobs() |
| { |
| $invalidationsInProgress = $this->getInvalidationsInProgress(); |
| $idsToReset = []; |
|
|
| foreach ($invalidationsInProgress as $invalidation) { |
| $archiveFailureRecoveryTimeout = GeneralConfig::getConfigValue('archive_failure_recovery_timeout', $invalidation['idsite']); |
|
|
| if (empty($invalidation['ts_started']) || Date::factory($invalidation['ts_started'])->getTimestamp() < Date::now()->getTimestamp() - $archiveFailureRecoveryTimeout) { |
| $idsToReset[] = $invalidation['idinvalidation']; |
| } |
| } |
|
|
| if (empty($idsToReset)) { |
| return 0; |
| } |
|
|
| return $this->releaseInProgressInvalidations($idsToReset); |
| } |
|
|
| public function getRecordsContainedInArchives(Date $archiveStartDate, array $idArchives, $requestedRecords): array |
| { |
| $idArchives = array_map('intval', $idArchives); |
| $idArchives = implode(',', $idArchives); |
|
|
| $requestedRecords = is_string($requestedRecords) ? [$requestedRecords] : $requestedRecords; |
| $placeholders = Common::getSqlStringFieldsArray($requestedRecords); |
|
|
| $countSql = "SELECT DISTINCT name FROM `%s` WHERE idarchive IN ($idArchives) AND name IN ($placeholders) LIMIT " . count($requestedRecords); |
|
|
| $numericTable = ArchiveTableCreator::getNumericTable($archiveStartDate, false); |
| $blobTable = ArchiveTableCreator::getBlobTable($archiveStartDate, false); |
|
|
| |
| |
| if ($this->doRequestedRecordsLookNumeric($requestedRecords)) { |
| $tablesToSearch = [$numericTable, $blobTable]; |
| } else { |
| $tablesToSearch = [$blobTable, $numericTable]; |
| } |
|
|
| $existingRecords = []; |
| foreach ($tablesToSearch as $tableName) { |
| if (empty($tableName)) { |
| continue; |
| } |
|
|
| $sql = sprintf($countSql, $tableName); |
| $rows = Db::fetchAll($sql, $requestedRecords); |
| $existingRecords = array_merge($existingRecords, array_column($rows, 'name')); |
|
|
| if (count($existingRecords) == count($requestedRecords)) { |
| break; |
| } |
| } |
| return $existingRecords; |
| } |
|
|
| private function isCutOffGroupConcatResult($pair) |
| { |
| $position = strpos($pair, '.'); |
| return $position === false || $position === strlen($pair) - 1; |
| } |
|
|
| private function getHashFromDoneFlag($doneFlag) |
| { |
| preg_match('/^done([a-zA-Z0-9]+)/', $doneFlag, $matches); |
| return $matches[1] ?? ''; |
| } |
|
|
| private function doRequestedRecordsLookNumeric(array $requestedRecords): bool |
| { |
| foreach ($requestedRecords as $record) { |
| if (preg_match('/^nb_/', $record)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
|
|