icingadb-web/library/Icingadb/Redis/VolatileStateResults.php
Bastian Lederer f8d4f92566
Some checks failed
L10n Update / update (push) Has been cancelled
CI / PHP (push) Has been cancelled
Ensure forward compatibility with ipl-orm by adding return types (#1340)
Add explicit return types to `ipl-orm`-derived methods to prepare for strict
typing. These additions are safe, as they only annotate existing methods that
previously lacked return type declarations.
2026-03-23 16:17:52 +01:00

237 lines
7.4 KiB
PHP

<?php
// SPDX-FileCopyrightText: 2021 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Icingadb\Redis;
use Icinga\Application\Benchmark;
use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Backend;
use Icinga\Module\Icingadb\Common\IcingaRedis;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\State;
use ipl\Orm\Query;
use ipl\Orm\Resolver;
use ipl\Orm\ResultSet;
use RuntimeException;
class VolatileStateResults extends ResultSet
{
use Auth;
/** @var Resolver */
private $resolver;
/** @var bool Whether Redis is unavailable */
private $redisUnavailable;
/** @var bool Whether Redis updates were applied */
private $updatesApplied = false;
/** @var string Object type host */
protected const TYPE_HOST = 'host';
/** @var string Object type service */
protected const TYPE_SERVICE = 'service';
/** @var array|null Columns to be selected if they were explicitly set, if empty all columns are selected */
protected ?array $columns;
/** @var bool Whether the model's ID should be contained in the results */
protected bool $includeModelID = true;
public static function fromQuery(Query $query): static
{
$self = parent::fromQuery($query);
$self->resolver = $query->getResolver();
$self->redisUnavailable = Backend::getRedis()->isUnavailable();
$self->columns = $query->getColumns();
if (! empty($self->columns)) {
// The id is necessary to apply the redis-updates
if ($query->getModel() instanceof Host && empty(array_intersect(['host.id', 'id'], $self->columns))) {
$query->withColumns('host.id');
$self->includeModelID = false;
} elseif (
$query->getModel() instanceof Service &&
empty(array_intersect(['service.id', 'id'], $self->columns))
) {
$query->withColumns('service.id');
$self->includeModelID = false;
}
}
return $self;
}
/**
* Get whether Redis is unavailable
*
* @return bool
*/
public function isRedisUnavailable(): bool
{
return $this->redisUnavailable;
}
public function current(): mixed
{
if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) {
$this->rewind();
}
$result = parent::current();
if ($this->isCacheDisabled && ! $this->redisUnavailable) {
$this->applyRedisUpdates([$result]);
}
if (! $this->includeModelID) {
unset($result['id']);
}
return $result;
}
public function key(): int
{
if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) {
$this->rewind();
}
return parent::key();
}
public function rewind(): void
{
if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) {
$this->updatesApplied = true;
$this->advance();
Benchmark::measure('Applying Redis updates');
$this->applyRedisUpdates($this);
Benchmark::measure('Redis updates applied');
}
parent::rewind();
}
/**
* Apply redis state details to the given results
*
* @param self|array<int, mixed> $rows
*
* @return void
*/
protected function applyRedisUpdates($rows)
{
$type = null;
$showSourceGranted = $this->getAuth()->hasPermission('icingadb/object/show-source');
$getKeysAndBehaviors = function (State $state, $type): array {
$columns = array_filter($state->getColumns(), function ($column) {
return ! str_ends_with($column, '_id');
});
if (! empty($this->columns)) {
$normalizedColumns = array_map(
fn($column) => preg_replace("/^($type\.state\.|state\.)/", '', $column),
$this->columns
);
$stateColumns = array_intersect($normalizedColumns, $columns);
return [$stateColumns, $this->resolver->getBehaviors($state)];
}
return [$columns, $this->resolver->getBehaviors($state)];
};
$states = [];
foreach ($rows as $row) {
if ($row instanceof DependencyNode) {
if ($row->redundancy_group_id !== null) {
continue;
} elseif ($row->service_id !== null) {
$type = self::TYPE_SERVICE;
$row = $row->service;
} else {
$type = self::TYPE_HOST;
$row = $row->host;
}
} elseif ($type === null) {
switch (true) {
case $row instanceof Host:
$type = self::TYPE_HOST;
break;
case $row instanceof Service:
$type = self::TYPE_SERVICE;
break;
default:
throw new RuntimeException('Volatile states can only be fetched for hosts and services');
}
}
$states[$type][bin2hex($row->id)] = $row->state;
if (! isset($states[$type]['keys'])) {
[$keys, $behaviors] = $getKeysAndBehaviors($row->state, $type);
if (! $showSourceGranted) {
$keys = array_diff($keys, ['check_commandline']);
}
$states[$type]['keys'] = $keys;
$states[$type]['behaviors'] = $behaviors;
}
if ($type === self::TYPE_SERVICE && $row->host instanceof Host && isset($row->host->id)) {
$states[self::TYPE_HOST][bin2hex($row->host->id)] = $row->host->state;
if (! isset($states[self::TYPE_HOST]['keys'])) {
[$keys, $behaviors] = $getKeysAndBehaviors($row->host->state, $type);
$states[self::TYPE_HOST]['keys'] = $keys;
$states[self::TYPE_HOST]['behaviors'] = $behaviors;
}
}
}
if (! empty($states[self::TYPE_SERVICE])) {
$this->apply($states[self::TYPE_SERVICE], self::TYPE_SERVICE);
}
if (! empty($states[self::TYPE_HOST])) {
$this->apply($states[self::TYPE_HOST], self::TYPE_HOST);
}
}
/**
* Apply the given states of given type to the results
*
* @param array $states
* @param string $type The object type ({@see self::TYPE_HOST} OR {@see self::TYPE_SERVICE})
*
* @return void
*/
protected function apply(array $states, string $type): void
{
$keys = $states['keys'];
$behaviors = $states['behaviors'];
unset($states['keys'], $states['behaviors']);
$results = $type === self::TYPE_SERVICE
? IcingaRedis::fetchServiceState(array_keys($states), $keys)
: IcingaRedis::fetchHostState(array_keys($states), $keys);
foreach ($results as $id => $data) {
foreach ($data as $key => $value) {
$data[$key] = $behaviors->retrieveProperty($value, $key);
}
$states[$id]->setProperties($data);
}
}
}