CommandTransport: Implement chunked transmission handling

Previously, command forms were responsible for this. But they
don't have any notion of fallback handling and are unable to
ensure proper re-submission of failed batches.
This commit is contained in:
Johannes Meyer 2025-11-07 15:32:02 +01:00
parent d5b1ba7f33
commit 6af3c7e595
12 changed files with 69 additions and 46 deletions

View file

@ -20,8 +20,6 @@ use ipl\Validator\CallbackValidator;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -220,9 +218,9 @@ class AcknowledgeProblemForm extends CommandForm
}
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250));
yield $command->setObjects($granted)->setChunkSize(250);
}
}
}

View file

@ -20,8 +20,6 @@ use ipl\Validator\CallbackValidator;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -163,9 +161,9 @@ class AddCommentForm extends CommandForm
}
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500));
yield $command->setObjects($granted)->setChunkSize(500);
}
}
}

View file

@ -11,8 +11,6 @@ use Icinga\Web\Notification;
use ipl\Orm\Model;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
class CheckNowForm extends CommandForm
@ -63,9 +61,9 @@ class CheckNowForm extends CommandForm
$command->setForced();
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000));
yield $command->setObjects($granted)->setChunkSize(1000);
}
}
}

View file

@ -11,8 +11,6 @@ use Icinga\Web\Notification;
use ipl\Orm\Model;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
class DeleteCommentForm extends CommandForm
@ -64,9 +62,9 @@ class DeleteCommentForm extends CommandForm
$command->setAuthor($this->getAuth()->getUser()->getUsername());
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500));
yield $command->setObjects($granted)->setChunkSize(500);
}
}
}

View file

@ -11,8 +11,6 @@ use Icinga\Web\Notification;
use ipl\Orm\Model;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
class DeleteDowntimeForm extends CommandForm
@ -77,9 +75,9 @@ class DeleteDowntimeForm extends CommandForm
$command->setAuthor($this->getAuth()->getUser()->getUsername());
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250));
yield $command->setObjects($granted)->setChunkSize(250);
}
}
}

View file

@ -16,8 +16,6 @@ use ipl\Orm\Model;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -154,9 +152,9 @@ class ProcessCheckResultForm extends CommandForm
$command->setPerformanceData($this->getValue('perfdata'));
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250));
yield $command->setObjects($granted)->setChunkSize(250);
}
}
}

View file

@ -12,8 +12,6 @@ use Icinga\Web\Notification;
use ipl\Orm\Model;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -77,9 +75,9 @@ class RemoveAcknowledgementForm extends CommandForm
$command->setAuthor($this->getAuth()->getUser()->getUsername());
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250));
yield $command->setObjects($granted)->setChunkSize(250);
}
}
}

View file

@ -18,8 +18,6 @@ use ipl\Orm\Model;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -129,9 +127,9 @@ class ScheduleCheckForm extends CommandForm
$command->setCheckTime($this->getValue('check_time')->getTimestamp());
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000));
yield $command->setObjects($granted)->setChunkSize(1000);
}
}
}

View file

@ -19,8 +19,6 @@ use ipl\Validator\CallbackValidator;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
class ScheduleServiceDowntimeForm extends CommandForm
@ -290,9 +288,9 @@ class ScheduleServiceDowntimeForm extends CommandForm
}
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250));
yield $command->setObjects($granted)->setChunkSize(250);
}
}
}

View file

@ -17,8 +17,6 @@ use ipl\Orm\Model;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use ipl\Web\Widget\Icon;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
use function ipl\Stdlib\iterable_value_first;
@ -130,9 +128,9 @@ class SendCustomNotificationForm extends CommandForm
$command->setAuthor($this->getAuth()->getUser()->getUsername());
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500));
yield $command->setObjects($granted)->setChunkSize(500);
}
}
}

View file

@ -12,8 +12,6 @@ use ipl\Html\FormElement\CheckboxElement;
use ipl\Orm\Model;
use ipl\Web\FormDecorator\IcingaFormDecorator;
use Iterator;
use LimitIterator;
use NoRewindIterator;
use Traversable;
class ToggleObjectFeaturesForm extends CommandForm
@ -181,11 +179,11 @@ class ToggleObjectFeaturesForm extends CommandForm
$command->setEnabled((int) $state);
$granted->rewind(); // Forwards the pointer to the first element
while ($granted->valid()) {
if ($granted->valid()) {
$this->submittedFeatures[$command->getFeature()] ??= $command->getEnabled();
// Chunk objects to avoid timeouts with large sets
yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000));
yield $command->setObjects($granted)->setChunkSize(1000);
}
}
}

View file

@ -4,12 +4,12 @@
namespace Icinga\Module\Icingadb\Command\Transport;
use Exception;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\Icingadb\Command\IcingaCommand;
use Icinga\Module\Icingadb\Command\Object\ObjectsCommand;
/**
* Command transport
@ -103,12 +103,57 @@ class CommandTransport implements CommandTransportInterface
public function send(IcingaCommand $command, int $now = null)
{
$errors = [];
$results = [];
$retryCommand = null;
foreach (static::getConfig() as $name => $transportConfig) {
$transport = static::createTransport($transportConfig);
if ($retryCommand !== null) {
if ($command instanceof ObjectsCommand && $command->getChunkSize() > 0) {
$objects = $command->getObjects();
if ($retryCommand !== null) {
try {
$results[] = $transport->send($retryCommand, $now);
} catch (CommandTransportException) {
// It failed prior, so no need to log it again
continue;
}
$retryCommand = null;
} else {
if ($objects->key() === null) {
// We traverse the iterator manually here, so we have to rewind it before the first iteration.
// That should be the case if the current key is null. May fail if an iterator explicitly yields
// null as the key, but I want to see a justified use case for that…
$objects->rewind();
}
}
while ($objects->valid()) {
$batchCommand = clone $command;
$batchCommand->setObjects(
new \LimitIterator(new \NoRewindIterator($objects), 0, $command->getChunkSize())
);
try {
$results[] = $transport->send($batchCommand, $now);
} catch (CommandTransportException $e) {
Logger::error($e);
$errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.'));
$retryCommand = $e->getCommand();
if ($retryCommand !== null) {
continue 2;
} else {
// Non-recoverable error, so stop trying to send further commands
break 2;
}
}
}
return $results;
} elseif ($retryCommand !== null) {
try {
$result = $transport->send($retryCommand, $now);
} catch (CommandTransportException) {