| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataAccess; |
|
|
| use Exception; |
| use Piwik\Archive\Chunk; |
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\ArchiveProcessor; |
| use Piwik\Config; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\Db\BatchInsert; |
| use Piwik\Log\LoggerInterface; |
| use Piwik\SettingsServer; |
|
|
| |
| |
| |
| |
| |
| class ArchiveWriter |
| { |
| |
| |
| |
| |
| |
| public const DONE_OK = 1; |
| |
| |
| |
| |
| |
| |
| public const DONE_ERROR = 2; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public const DONE_OK_TEMPORARY = 3; |
|
|
| |
| |
| |
| |
| |
| public const DONE_INVALIDATED = 4; |
|
|
| |
| |
| |
| |
| |
| |
| public const DONE_PARTIAL = 5; |
|
|
| |
| |
| |
| public const DONE_ERROR_INVALIDATED = 6; |
|
|
| protected $fields = ['idarchive', |
| 'idsite', |
| 'date1', |
| 'date2', |
| 'period', |
| 'ts_archived', |
| 'name', |
| 'value']; |
|
|
| private $recordsToWriteSpool = [ |
| 'numeric' => [], |
| 'blob' => [], |
| ]; |
|
|
| public const MAX_SPOOL_SIZE = 50; |
|
|
| |
| |
| |
| public $idArchive; |
|
|
| |
| |
| |
| private $idSite; |
|
|
| |
| |
| |
| private $segment; |
|
|
| |
| |
| |
| private $period; |
|
|
| |
| |
| |
| private $parameters; |
|
|
| |
| |
| |
| private $earliestNow; |
|
|
| |
| |
| |
| private $doneFlag; |
|
|
| |
| |
| |
| private $dateStart; |
|
|
| |
| |
| |
| |
| public function __construct(ArchiveProcessor\Parameters $params) |
| { |
| $this->idArchive = false; |
| $this->idSite = $params->getSite()->getId(); |
| $this->segment = $params->getSegment(); |
| $this->period = $params->getPeriod(); |
| $this->parameters = $params; |
|
|
| $idSites = [$this->idSite]; |
| $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin()); |
|
|
| $this->dateStart = $this->period->getDateStart(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function insertBlobRecord($name, $values) |
| { |
| if (is_array($values)) { |
| if (isset($values[0])) { |
| |
| $this->insertRecord($name, $this->compress($values[0])); |
| unset($values[0]); |
| } |
|
|
| if (!empty($values)) { |
| |
| $chunk = new Chunk(); |
| $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values); |
| foreach ($chunks as $index => $subtables) { |
| $this->insertRecord($index, $this->compress(serialize($subtables))); |
| } |
| } |
| } else { |
| $values = $this->compress($values); |
| $this->insertRecord($name, $values); |
| } |
| } |
|
|
| public function getIdArchive() |
| { |
| if ($this->idArchive === false) { |
| throw new Exception("Must call allocateNewArchiveId() first"); |
| } |
|
|
| return $this->idArchive; |
| } |
|
|
| public function initNewArchive() |
| { |
| $idArchive = $this->allocateNewArchiveId(); |
| $this->logArchiveStatusAsIncomplete(); |
| return $idArchive; |
| } |
|
|
| public function finalizeArchive() |
| { |
| if ( |
| empty($this->recordsToWriteSpool['blob']) |
| && count($this->recordsToWriteSpool['numeric']) === 1 |
| && $this->recordsToWriteSpool['numeric'][0][0] === $this->doneFlag |
| && $this->parameters->isPartialArchive() |
| ) { |
| |
| |
| |
| return; |
| } |
|
|
| $this->flushSpools(); |
|
|
| $numericTable = $this->getTableNumeric(); |
| $idArchive = $this->getIdArchive(); |
|
|
| $doneValue = $this->parameters->isPartialArchive() ? self::DONE_PARTIAL : self::DONE_OK; |
| $this->checkDoneValueIsOnlyPartialForPluginArchives($doneValue); |
|
|
| $currentStatus = $this->getModel()->getArchiveStatus($numericTable, $idArchive, $this->doneFlag); |
|
|
| |
| if (self::DONE_ERROR_INVALIDATED === $currentStatus) { |
| $doneValue = self::DONE_INVALIDATED; |
| } |
|
|
| $this->getModel()->updateArchiveStatus($numericTable, $idArchive, $this->doneFlag, $doneValue); |
|
|
| if ( |
| !$this->parameters->isPartialArchive() |
| |
| && !empty($this->earliestNow) |
| ) { |
| $this->getModel()->deleteOlderArchives($this->parameters, $this->doneFlag, $this->earliestNow, $idArchive); |
| } |
| } |
|
|
| protected function compress($data) |
| { |
| $compressionLevel = (int) Config::getInstance()->General['archive_blob_compression_level']; |
| |
| $compressionLevel = min(max(-1, $compressionLevel), 9); |
|
|
| if (Db::get()->hasBlobDataType()) { |
| return gzcompress($data, $compressionLevel); |
| } |
|
|
| return $data; |
| } |
|
|
| protected function allocateNewArchiveId() |
| { |
| $numericTable = $this->getTableNumeric(); |
|
|
| $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable); |
| return $this->idArchive; |
| } |
|
|
| private function getModel() |
| { |
| return new Model(); |
| } |
|
|
| protected function logArchiveStatusAsIncomplete() |
| { |
| $this->insertRecord($this->doneFlag, self::DONE_ERROR); |
| } |
|
|
| private function batchInsertSpool($valueType) |
| { |
| $records = $this->recordsToWriteSpool[$valueType]; |
|
|
| $bindSql = $this->getInsertRecordBind(); |
| $values = []; |
|
|
| $valueSeen = false; |
| foreach ($records as $record) { |
| $bind = $bindSql; |
| $bind[] = $record[0]; |
| $bind[] = $record[1]; |
| $values[] = $bind; |
|
|
| $valueSeen = $record[1]; |
| } |
|
|
| if (empty($values)) { |
| return true; |
| } |
|
|
| $tableName = $this->getTableNameToInsert($valueSeen); |
| $fields = $this->getInsertFields(); |
|
|
| |
| if ($valueType === 'numeric') { |
| BatchInsert::tableInsertBatchSql($tableName, $fields, $values); |
| } else { |
| BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1'); |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function insertRecord($name, $value) |
| { |
| $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob'; |
| $this->recordsToWriteSpool[$valueType][] = [ |
| 0 => $name, |
| 1 => $value, |
| ]; |
|
|
| if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) { |
| $this->flushSpool($valueType); |
| } |
|
|
| return true; |
| } |
|
|
| public function flushSpools() |
| { |
| if (SettingsServer::isArchivePhpTriggered()) { |
| Db::executeWithDatabaseWriterReconnectionAttempt(function () { |
| $this->flushSpool('numeric'); |
| $this->flushSpool('blob'); |
| }); |
| } else { |
| $this->flushSpool('numeric'); |
| $this->flushSpool('blob'); |
| } |
| } |
|
|
| private function flushSpool($valueType) |
| { |
| $numRecords = count($this->recordsToWriteSpool[$valueType]); |
|
|
| if ($numRecords > 1) { |
| $this->batchInsertSpool($valueType); |
| } elseif ($numRecords === 1) { |
| [$name, $value] = $this->recordsToWriteSpool[$valueType][0]; |
| $tableName = $this->getTableNameToInsert($value); |
| $fields = $this->getInsertFields(); |
| $record = $this->getInsertRecordBind(); |
|
|
| $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value); |
| } |
| $this->recordsToWriteSpool[$valueType] = []; |
| } |
|
|
| protected function getInsertRecordBind() |
| { |
| $now = Date::now()->getDatetime(); |
| if (empty($this->earliestNow)) { |
| $this->earliestNow = $now; |
| } |
| return [$this->getIdArchive(), |
| $this->idSite, |
| $this->dateStart->toString('Y-m-d'), |
| $this->period->getDateEnd()->toString('Y-m-d'), |
| $this->period->getId(), |
| $now]; |
| } |
|
|
| protected function getTableNameToInsert($value) |
| { |
| if ($this->isRecordNumeric($value)) { |
| return $this->getTableNumeric(); |
| } |
|
|
| return ArchiveTableCreator::getBlobTable($this->dateStart, true); |
| } |
|
|
| protected function getTableNumeric() |
| { |
| return ArchiveTableCreator::getNumericTable($this->dateStart, true); |
| } |
|
|
| protected function getInsertFields() |
| { |
| return $this->fields; |
| } |
|
|
| private function isRecordNumeric($value) |
| { |
| return is_numeric($value); |
| } |
|
|
| private function checkDoneValueIsOnlyPartialForPluginArchives($doneValue) |
| { |
| |
| |
| if ($doneValue == self::DONE_PARTIAL && strpos($this->doneFlag, '.') == false) { |
| $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 |
| )); |
| StaticContainer::get(LoggerInterface::class)->warning('{exception}', [ |
| 'exception' => $ex, |
| ]); |
| } |
| } |
| } |
|
|