diff --git a/src/DefaultTimeoutChecker.php b/src/DefaultTimeoutChecker.php new file mode 100644 index 00000000..cb561526 --- /dev/null +++ b/src/DefaultTimeoutChecker.php @@ -0,0 +1,35 @@ +startTime = $startTime ?? \microtime(true); + } + + public function throwOnTimeout(?float $currentTime = null, string $message = 'Exceeded the timeout'): void + { + $spentTime = ($currentTime ?? \microtime(true)) - $this->startTime; + $isTimeout = $spentTime >= $this->timeout; + if ($isTimeout) { + throw new TimeoutException($message, $spentTime, $this->timeout); + } + } +} diff --git a/src/Exception/TimeoutException.php b/src/Exception/TimeoutException.php new file mode 100644 index 00000000..aff7a243 --- /dev/null +++ b/src/Exception/TimeoutException.php @@ -0,0 +1,33 @@ +timeout; + } + + public function getSpentTime(): float + { + return $this->spentTime; + } +} diff --git a/src/Packer.php b/src/Packer.php index ca261bf9..2f778f19 100644 --- a/src/Packer.php +++ b/src/Packer.php @@ -46,6 +46,8 @@ class Packer implements LoggerAwareInterface private bool $beStrictAboutItemOrdering = false; + protected ?TimeoutChecker $timeoutChecker = null; + public function __construct() { $this->items = new ItemList(); @@ -136,6 +138,11 @@ public function setPackedBoxSorter(PackedBoxSorter $packedBoxSorter): void $this->packedBoxSorter = $packedBoxSorter; } + public function setTimeoutChecker(TimeoutChecker $timeoutChecker): void + { + $this->timeoutChecker = $timeoutChecker; + } + public function throwOnUnpackableItem(bool $throwOnUnpackableItem): void { $this->throwOnUnpackableItem = $throwOnUnpackableItem; @@ -160,12 +167,12 @@ public function getUnpackedItems(): ItemList public function pack(): PackedBoxList { $this->logger->log(LogLevel::INFO, '[PACKING STARTED]'); - + $this->timeoutChecker?->start(); $packedBoxes = $this->doBasicPacking(); // If we have multiple boxes, try and optimise/even-out weight distribution if (!$this->beStrictAboutItemOrdering && $packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) { - $redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxQuantitiesAvailable); + $redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxQuantitiesAvailable, $this->timeoutChecker); $redistributor->setLogger($this->logger); $packedBoxes = $redistributor->redistributeWeight($packedBoxes); } @@ -188,6 +195,7 @@ public function doBasicPacking(bool $enforceSingleBox = false): PackedBoxList // Loop through boxes starting with smallest, see what happens foreach ($this->getBoxList($enforceSingleBox) as $box) { + $this->timeoutChecker?->throwOnTimeout(); $volumePacker = new VolumePacker($box, $this->items); $volumePacker->setLogger($this->logger); $volumePacker->beStrictAboutItemOrdering($this->beStrictAboutItemOrdering); @@ -232,6 +240,7 @@ public function doBasicPacking(bool $enforceSingleBox = false): PackedBoxList public function packAllPermutations(): array { $this->logger->log(LogLevel::INFO, '[PACKING STARTED (all permutations)]'); + $this->timeoutChecker?->start(); $boxQuantitiesAvailable = clone $this->boxQuantitiesAvailable; @@ -252,6 +261,7 @@ public function packAllPermutations(): array $additionalPermutationsForThisPermutation = []; foreach ($this->boxes as $box) { + $this->timeoutChecker?->throwOnTimeout(); if ($remainingBoxQuantities[$box] > 0) { $volumePacker = new VolumePacker($box, $wipPermutation['itemsLeft']); $volumePacker->setLogger($this->logger); diff --git a/src/TimeoutChecker.php b/src/TimeoutChecker.php new file mode 100644 index 00000000..37504ab7 --- /dev/null +++ b/src/TimeoutChecker.php @@ -0,0 +1,24 @@ +logger = new NullLogger(); } @@ -106,6 +107,7 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe $underWeightBoxItems = $underWeightBox->items->asItemArray(); foreach ($overWeightBoxItems as $key => $overWeightItem) { + $this->timeoutChecker?->throwOnTimeout(); if (!self::wouldRepackActuallyHelp($overWeightBoxItems, $overWeightItem, $underWeightBoxItems, $targetWeight)) { continue; // moving this item would harm more than help } diff --git a/tests/PackerTest.php b/tests/PackerTest.php index 1ebad8d3..fe1214d4 100644 --- a/tests/PackerTest.php +++ b/tests/PackerTest.php @@ -10,6 +10,7 @@ namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Exception\NoBoxesAvailableException; +use DVDoug\BoxPacker\Exception\TimeoutException; use DVDoug\BoxPacker\Test\ConstrainedPlacementByCountTestItem; use DVDoug\BoxPacker\Test\LimitedSupplyTestBox; use DVDoug\BoxPacker\Test\PackedBoxByReferenceSorter; @@ -1457,4 +1458,39 @@ public function testIssue620(): void self::assertCount(6, $packedBoxes); } + + public function testTimeoutException(): void + { + $this->expectException(TimeoutException::class); + $packer = new Packer(); + $packer->setTimeoutChecker(new DefaultTimeoutChecker(3.0)); + + for ($i = 0; $i < 100; ++$i) { + $box = new TestBox( + reference: 'box ' . $i, + outerWidth: $i * 10, + outerLength: $i * 10, + outerDepth: $i * 10, + emptyWeight: 1, + innerWidth: $i * 10, + innerLength: $i * 10, + innerDepth: $i * 10, + maxWeight: 10000 + ); + $packer->addBox($box); + } + + $item = new TestItem( + description: 'item 1', + width: 100, + length: 100, + depth: 100, + weight: 100, + allowedRotation: Rotation::BestFit + ); + $packer->addItem($item, 500); + + /** @var PackedBox[] $packedBoxes */ + $packedBoxes = iterator_to_array($packer->pack(), false); + } }