-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Description
Preconditions (*)
- 2.4.1 - 2.4.3-p1
- A very large collection of orders / shipments
Steps to reproduce (*)
I'm trying to process all orders related to the stored shipments to do some data clean up when I stumbled into this memory leak.
Basically on each repository->get() action the memory increases and is never released.
Check the following sample code using a single OrderRepositoryInterface which simply iterates a large collection of shipments found by the repository's getList function and tries to retrieve the order by id in a CLI command.
As you can see there's no reference inside the code that is keeping the object from being collected and the memory should presumably be released on every gc cycle for all the objects inside the foreach loop.
<?php
/**
* Copyright (c) 2021. IOWEB TECHNOLOGIES
*/
namespace Ioweb\CliTools\Console;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
use Magento\Framework\App\State;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Magento\Sales\Api\InvoiceRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterfaceFactory;
use Magento\Sales\Api\ShipmentRepositoryInterface;
use Magento\Sales\Model\OrderFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DeleteUnlinkedDocuments extends Command
{
protected $searchCriteriaBuilderFactory;
protected $invoiceRepository;
protected $creditmemoRepository;
protected $shipmentRepository;
protected $orderRepository;
protected $orderFactory;
protected $order = null;
protected $orderRepositoryFactory;
public function __construct(
State $state,
SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
InvoiceRepositoryInterface $invoiceRepository,
CreditmemoRepositoryInterface $creditmemoRepository,
ShipmentRepositoryInterface $shipmentRepository,
OrderRepositoryInterface $orderRepository,
OrderRepositoryInterfaceFactory $orderRepositoryFactory,
OrderFactory $orderFactory
)
{
$this->_state = $state;
$this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
$this->invoiceRepository = $invoiceRepository;
$this->creditmemoRepository = $creditmemoRepository;
$this->shipmentRepository = $shipmentRepository;
$this->orderRepository = $orderRepository;
$this->orderRepositoryFactory = $orderRepositoryFactory;
$this->orderFactory = $orderFactory;
parent::__construct();
}
/**
* Configure CLI command
*/
protected function configure()
{
$this->setName('ioweb:documents:delete-unlinked');
$this->setDescription('Deletes shipments/invoices/credit memos that do not have an order id linked in the database');
parent::configure();
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->_state->setAreaCode('adminhtml');
$pageSize = 50;
$output->writeln("Fetching shipments with repo interface");
/** @var SearchCriteriaBuilder $shipmentBuilder */
$shipmentBuilder = $this->searchCriteriaBuilderFactory->create();
$shipmentsList = $this->shipmentRepository->getList($shipmentBuilder->create());
$shipments = $shipmentsList->getItems();
foreach ($shipments as $index => $shipment) {
$orderId = $shipment->getOrderId();
$order = $this->orderRepository->get($orderId);
unset($order, $shipment);
if ($index % 1000 === 0) {
$output->writeln("collecting cycles");
$output->writeln($this->convert(memory_get_usage()));
gc_collect_cycles();
$output->writeln($this->convert(memory_get_usage()));
}
}
$output->writeln("End deletion");
}
private function convert($size)
{
$unit = array('b', 'kb', 'mb', 'gb', 'tb', 'pb');
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
}
private function getOrder($orderId)
{
$repo = $this->orderRepositoryFactory->create();
return $repo->get($orderId);
}
}
On the contrary the memory is never released as the output says every 1000 items processed. I end up with this result
Fetching shipments with repo factory
collecting cycles
1.28 gb
1.28 gb
collecting cycles
1.67 gb
1.67 gb
collecting cycles
2.08 gb
2.08 gb
collecting cycles
2.48 gb
2.48 gb
collecting cycles
2.87 gb
2.87 gb
collecting cycles
3.28 gb
3.28 gb
collecting cycles
3.68 gb
3.68 gb
PHP Fatal error: Allowed memory size of 4294967296 bytes exhausted (tried to allocate 28672 bytes) in /httpdocs/vendor/magento/framework/DB/Statement/Pdo/Mysql.php on line 91
This lead me to think that the repository itself is keeping a weak reference or a hidden reference to the object thus it cannot be released even if I unset it on my code.
Thus I changed the way of the code to use a repository factory instead
<?php
/**
* Copyright (c) 2021. IOWEB TECHNOLOGIES
*/
namespace Ioweb\CliTools\Console;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
use Magento\Framework\App\State;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Magento\Sales\Api\InvoiceRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterfaceFactory;
use Magento\Sales\Api\ShipmentRepositoryInterface;
use Magento\Sales\Model\OrderFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DeleteUnlinkedDocuments extends Command
{
protected $searchCriteriaBuilderFactory;
protected $invoiceRepository;
protected $creditmemoRepository;
protected $shipmentRepository;
protected $orderRepository;
protected $orderFactory;
protected $order = null;
protected $orderRepositoryFactory;
public function __construct(
State $state,
SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
InvoiceRepositoryInterface $invoiceRepository,
CreditmemoRepositoryInterface $creditmemoRepository,
ShipmentRepositoryInterface $shipmentRepository,
OrderRepositoryInterface $orderRepository,
OrderRepositoryInterfaceFactory $orderRepositoryFactory,
OrderFactory $orderFactory
)
{
$this->_state = $state;
$this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
$this->invoiceRepository = $invoiceRepository;
$this->creditmemoRepository = $creditmemoRepository;
$this->shipmentRepository = $shipmentRepository;
$this->orderRepository = $orderRepository;
$this->orderRepositoryFactory = $orderRepositoryFactory;
$this->orderFactory = $orderFactory;
parent::__construct();
}
/**
* Configure CLI command
*/
protected function configure()
{
$this->setName('ioweb:documents:delete-unlinked');
$this->setDescription('Deletes shipments/invoices/credit memos that do not have an order id linked in the database');
parent::configure();
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->_state->setAreaCode('adminhtml');
$output->writeln("Start deleting invoices/shipments and credit memos");
$output->writeln("Fetching shipments with repo factory");
/** @var SearchCriteriaBuilder $shipmentBuilder */
$shipmentBuilder = $this->searchCriteriaBuilderFactory->create();
$shipmentsList = $this->shipmentRepository->getList($shipmentBuilder->create());
$shipments = $shipmentsList->getItems();
foreach ($shipments as $index => $shipment) {
$orderId = $shipment->getOrderId();
$order = $this->getOrder($orderId);
unset($order, $shipment);
if ($index % 1000 === 0) {
$output->writeln("collecting cycles");
$output->writeln($this->convert(memory_get_usage()));
gc_collect_cycles();
$output->writeln($this->convert(memory_get_usage()));
}
}
$output->writeln("End deletion");
}
private function convert($size)
{
$unit = array('b', 'kb', 'mb', 'gb', 'tb', 'pb');
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
}
private function getOrder($orderId)
{
$repo = $this->orderRepositoryFactory->create();
return $repo->get($orderId);
}
}
Interestingly enough I saw that memory was decreasing on each gc cycle.
Fetching shipments with repo factory
collecting cycles
1.25 gb
1.18 gb
collecting cycles
1.57 gb
1.35 gb
collecting cycles
1.75 gb
1.52 gb
collecting cycles
1.93 gb
1.69 gb
collecting cycles
2.09 gb
1.86 gb
collecting cycles
2.26 gb
2.03 gb
collecting cycles
2.44 gb
2.21 gb
collecting cycles
2.61 gb
2.38 gb
collecting cycles
2.8 gb
2.56 gb
collecting cycles
2.96 gb
2.73 gb
collecting cycles
3.13 gb
2.9 gb
collecting cycles
3.31 gb
3.08 gb
collecting cycles
3.48 gb
3.25 gb
collecting cycles
3.66 gb
3.43 gb
PHP Fatal error: Allowed memory size of 4294967296 bytes exhausted (tried to allocate 28672 bytes) in /httpdocs/vendor/magento/framework/DB/Statement/Pdo/Mysql.php on line 91
However it seems that it's not entirely released. The footprint is still pretty big but even so using the factory and creating a new instance each time leads to better memory management than using a single repository instance. This doesn't seem right.
Expected result (*)
- Memory is released when references are destroyed in our code.
- I can use foreach normally and let gc do it's work when using repositories in a foreach statement.
Actual result (*)
- The repositories and factories are keeping references we don't have access to preventing the unused objects to be collected in the next gc cycle.
- Repositories are unusable in foreach statements for large SearchResultsInterface sets
It seems the only way to bypass this is to process each item in external scripts so that when they end up execution they will forcefully release all memory. This is not a solution thogh.
Please provide Severity assessment for the Issue as Reporter. This information will help during Confirmation and Issue triage processes.
- Severity: S0 - Affects critical data or functionality and leaves users without workaround.
- Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
- Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
- Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.