From 0c690ac984eb78b375f352c7a73b5a7c4a785552 Mon Sep 17 00:00:00 2001
From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl>
Date: Thu, 11 Oct 2018 15:26:42 +0200
Subject: [PATCH] Fixing async index runner + adding unit tests

Correcting async indexers running + adding unit tests.

Correcting async indexers running + adding unit tests.
---
 .../ChildProcess/ProcessFactoryInterface.php  |  27 +++
 .../EventLoop/LoopFactoryInterface.php        |  27 +++
 Api/IndexerProcessorInterface.php             |   4 +-
 .../AsyncReindexRunnerInterface.php           |  23 --
 .../SyncReindexRunnerInterface.php            |  23 --
 Api/ReindexRunnerInterface.php                |   8 +-
 Controller/Adminhtml/Indexer/MassReindex.php  |  30 ++-
 .../ReindexFailureAggregateException.php      |  22 ++
 Exception/ReindexFailureException.php         |  49 ----
 .../ReactPHP/ChildProcess/ProcessFactory.php  |  31 +++
 .../ReactPHP/EventLoop/LoopFactory.php        |  46 ++++
 Model/IndexerProcessor.php                    |  15 +-
 Model/ReindexRunner/AsyncReindexRunner.php    | 117 +++++++--
 Model/ReindexRunner/SyncReindexRunner.php     |  56 ++++-
 README.md                                     |   6 +-
 .../Adminhtml/Indexer/MassReindexTest.php     | 223 ++++++++++++++++++
 .../ChildProcess/ProcessFactoryTest.php       |  36 +++
 .../ReactPHP/EventLoop/LoopFactoryTest.php    |  37 +++
 Test/Unit/Model/IndexerProcessorTest.php      |  72 ++++++
 .../ReindexRunner/AsyncReindexRunnerTest.php  | 174 ++++++++++++++
 .../Model/ReindexRunner/MessageBagTest.php    |  46 ++++
 .../ReindexRunner/SyncReindexRunnerTest.php   | 117 +++++++++
 composer.json                                 |   4 +-
 etc/di.xml                                    |  10 +-
 etc/module.xml                                |   2 +-
 i18n/en_US.csv                                |   4 +-
 i18n/pl_PL.csv                                |   2 +
 27 files changed, 1048 insertions(+), 163 deletions(-)
 create mode 100644 Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php
 create mode 100644 Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php
 delete mode 100644 Api/ReindexRunner/AsyncReindexRunnerInterface.php
 delete mode 100644 Api/ReindexRunner/SyncReindexRunnerInterface.php
 create mode 100644 Exception/ReindexFailureAggregateException.php
 delete mode 100644 Exception/ReindexFailureException.php
 create mode 100644 Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php
 create mode 100644 Model/Adapter/ReactPHP/EventLoop/LoopFactory.php
 create mode 100644 Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php
 create mode 100644 Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php
 create mode 100644 Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php
 create mode 100644 Test/Unit/Model/IndexerProcessorTest.php
 create mode 100644 Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php
 create mode 100644 Test/Unit/Model/ReindexRunner/MessageBagTest.php
 create mode 100644 Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php

diff --git a/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php b/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php
new file mode 100644
index 0000000..c953b0b
--- /dev/null
+++ b/Api/Adapter/ReactPHP/ChildProcess/ProcessFactoryInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File:ProcessFactoryInterface.php
+ *
+ * @author Maciej Sławik <maciej.slawik@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess;
+
+use React\ChildProcess\Process;
+
+/**
+ * Interface ProcessFactoryInterface
+ * @package LizardMedia\AdminIndexer\Api\Adapter\ReactPHP
+ */
+interface ProcessFactoryInterface
+{
+    /**
+     * @param string $command
+     * @return Process
+     */
+    public function create(string $command): Process;
+}
diff --git a/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php b/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php
new file mode 100644
index 0000000..94cbc04
--- /dev/null
+++ b/Api/Adapter/ReactPHP/EventLoop/LoopFactoryInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: LoopFactoryInterface.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop;
+
+use React\EventLoop\Factory;
+use React\EventLoop\LoopInterface;
+
+/**
+ * Interface LoopFactoryInterface
+ * @package LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop
+ */
+interface LoopFactoryInterface
+{
+    /**
+     * @return LoopInterface
+     */
+    public function create(): LoopInterface;
+}
\ No newline at end of file
diff --git a/Api/IndexerProcessorInterface.php b/Api/IndexerProcessorInterface.php
index 8a8e8c6..ff6b6c9 100644
--- a/Api/IndexerProcessorInterface.php
+++ b/Api/IndexerProcessorInterface.php
@@ -12,7 +12,7 @@
 
 namespace LizardMedia\AdminIndexer\Api;
 
-use LizardMedia\AdminIndexer\Exception\ReindexFailureException;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
 
 /**
  * Interface IndexerProcessorInterface
@@ -23,7 +23,7 @@ interface IndexerProcessorInterface
     /**
      * @param string[] ...$indexerIds
      * @return void
-     * @throws ReindexFailureException
+     * @throws ReindexFailureAggregateException
      */
     public function process(string ...$indexerIds): void;
 }
diff --git a/Api/ReindexRunner/AsyncReindexRunnerInterface.php b/Api/ReindexRunner/AsyncReindexRunnerInterface.php
deleted file mode 100644
index 333701f..0000000
--- a/Api/ReindexRunner/AsyncReindexRunnerInterface.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * File: AsyncReindexRunnerInterface.php
- *
- * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
- * @author Paweł Papke <pawel.papke@lizardmedia.pl>
- * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
- */
-
-namespace LizardMedia\AdminIndexer\Api\ReindexRunner;
-
-use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
-
-/**
- * Interface AsyncReindexRunnerInterface
- * @package LizardMedia\AdminIndexer\Api\ReindexRunner
- */
-interface AsyncReindexRunnerInterface extends ReindexRunnerInterface
-{
-}
diff --git a/Api/ReindexRunner/SyncReindexRunnerInterface.php b/Api/ReindexRunner/SyncReindexRunnerInterface.php
deleted file mode 100644
index 4a09c08..0000000
--- a/Api/ReindexRunner/SyncReindexRunnerInterface.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * File: SyncReindexRunnerInterface.php
- *
- * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
- * @author Paweł Papke <pawel.papke@lizardmedia.pl>
- * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
- */
-
-namespace LizardMedia\AdminIndexer\Api\ReindexRunner;
-
-use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
-
-/**
- * Interface SyncReindexRunnerInterface
- * @package LizardMedia\AdminIndexer\Api\ReindexRunner
- */
-interface SyncReindexRunnerInterface extends ReindexRunnerInterface
-{
-}
diff --git a/Api/ReindexRunnerInterface.php b/Api/ReindexRunnerInterface.php
index 72c18d5..a281315 100644
--- a/Api/ReindexRunnerInterface.php
+++ b/Api/ReindexRunnerInterface.php
@@ -12,6 +12,8 @@
 
 namespace LizardMedia\AdminIndexer\Api;
 
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
+
 /**
  * Interface ReindexRunnerInterface
  * @package LizardMedia\AdminIndexer\Api
@@ -19,9 +21,9 @@
 interface ReindexRunnerInterface
 {
     /**
-     * @param string $indexerId
+     * @param string[] ...$indexerIds
      * @return void
-     * @throws \Exception
+     * @throws ReindexFailureAggregateException
      */
-    public function run(string $indexerId): void;
+    public function run(string ...$indexerIds): void;
 }
diff --git a/Controller/Adminhtml/Indexer/MassReindex.php b/Controller/Adminhtml/Indexer/MassReindex.php
index 825a054..7814299 100644
--- a/Controller/Adminhtml/Indexer/MassReindex.php
+++ b/Controller/Adminhtml/Indexer/MassReindex.php
@@ -14,7 +14,7 @@
 
 use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface;
 use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface;
-use LizardMedia\AdminIndexer\Exception\ReindexFailureException;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
 use Magento\Backend\App\Action;
 use Magento\Backend\App\Action\Context;
 use Magento\Framework\Controller\Result\Redirect;
@@ -60,7 +60,6 @@ public function __construct(
         IndexerProcessorInterface $indexerProcessor
     ) {
         parent::__construct($context);
-
         $this->messageBag = $messageBag;
         $this->redirectFactory = $redirectFactory;
         $this->indexerProcessor = $indexerProcessor;
@@ -69,7 +68,7 @@ public function __construct(
     /**
      * @return Redirect
      */
-    public function execute() : Redirect
+    public function execute(): Redirect
     {
         $indexerIds = $this->getRequest()->getParam('indexer_ids');
 
@@ -77,13 +76,14 @@ public function execute() : Redirect
             $this->messageManager->addErrorMessage(__('Please select at least one index.'));
             return $this->getRedirect();
         }
+
         $indexerIds = $this->castValuesToString($indexerIds);
 
         try {
             $this->indexerProcessor->process(...$indexerIds);
-            $this->displayMessages();
-        } catch (ReindexFailureException $exception) {
-            $this->messageManager->addErrorMessage(__('Reindex failed on indexer %1.', $exception->getIndexerName()));
+            $this->displayMessagesAboutRunningIndexers();
+        } catch (ReindexFailureAggregateException $exception) {
+            $this->displayErrors($exception);
         }
 
         return $this->getRedirect();
@@ -121,17 +121,31 @@ private function validateParam($indexerIds): bool
     private function castValuesToString(array $indexerIds): array
     {
         return array_filter($indexerIds, function ($indexerId) {
-            return (string)$indexerId;
+            return (string) $indexerId;
         });
     }
 
     /**
      * @return void
      */
-    private function displayMessages(): void
+    private function displayMessagesAboutRunningIndexers(): void
     {
         foreach ($this->messageBag->getMessages() as $message) {
             $this->messageManager->addNoticeMessage($message);
         }
     }
+
+    /**
+     * @param ReindexFailureAggregateException $exception
+     * @return void
+     */
+    private function displayErrors(ReindexFailureAggregateException $exception): void
+    {
+        $this->messageManager->addErrorMessage($exception->getMessage());
+        $errors = $exception->getErrors();
+
+        foreach ($errors as $error) {
+            $this->messageManager->addErrorMessage($error->getMessage());
+        }
+    }
 }
diff --git a/Exception/ReindexFailureAggregateException.php b/Exception/ReindexFailureAggregateException.php
new file mode 100644
index 0000000..5490c2d
--- /dev/null
+++ b/Exception/ReindexFailureAggregateException.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: ReindexFailureAggregateException.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Exception;
+
+use Magento\Framework\Exception\AbstractAggregateException;
+
+/**
+ * Class ReindexFailureAggregateException
+ * @package LizardMedia\AdminIndexer\Exception
+ */
+class ReindexFailureAggregateException extends AbstractAggregateException
+{
+}
\ No newline at end of file
diff --git a/Exception/ReindexFailureException.php b/Exception/ReindexFailureException.php
deleted file mode 100644
index f8d8164..0000000
--- a/Exception/ReindexFailureException.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * File: ReindexFailureException.php
- *
- * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
- * @author Paweł Papke <pawel.papke@lizardmedia.pl>
- * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
- */
-
-namespace LizardMedia\AdminIndexer\Exception;
-
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Phrase;
-
-/**
- * Class ReindexFailureException
- * @package LizardMedia\AdminIndexer\Exception
- */
-class ReindexFailureException extends LocalizedException
-{
-    /**
-     * @var string
-     */
-    private $indexerName;
-
-    /**
-     * ReindexFailureException constructor.
-     * @param Phrase $phrase
-     * @param string $indexerName
-     * @param \Exception|null $cause
-     * @param int $code
-     */
-    public function __construct(Phrase $phrase, string $indexerName, \Exception $cause = null, int $code = 0)
-    {
-        parent::__construct($phrase, $cause, $code);
-        $this->indexerName = $indexerName;
-    }
-
-    /**
-     * @return string
-     */
-    public function getIndexerName(): string
-    {
-        return $this->indexerName;
-    }
-}
diff --git a/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php b/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php
new file mode 100644
index 0000000..d078c6f
--- /dev/null
+++ b/Model/Adapter/ReactPHP/ChildProcess/ProcessFactory.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File:ProcessFactory.php
+ *
+ * @author Maciej Sławik <maciej.slawik@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\ChildProcess;
+
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface;
+use React\ChildProcess\Process;
+
+/**
+ * Class ProcessFactory
+ * @package LizardMedia\AdminIndexer\Model\Adapter\ReactPHP
+ */
+class ProcessFactory implements ProcessFactoryInterface
+{
+    /**
+     * @param string $command
+     * @return Process
+     */
+    public function create(string $command): Process
+    {
+        return new Process($command);
+    }
+}
diff --git a/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php b/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php
new file mode 100644
index 0000000..cc53e55
--- /dev/null
+++ b/Model/Adapter/ReactPHP/EventLoop/LoopFactory.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: LoopFactory.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop;
+
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface;
+use React\EventLoop\LoopInterface;
+use React\EventLoop\Factory;
+
+/**
+ * Class LoopFactory
+ * @package LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop
+ */
+class LoopFactory implements LoopFactoryInterface
+{
+    /**
+     * @var Factory
+     */
+    private $factory;
+
+    /**
+     * LoopFactory constructor.
+     * @param Factory $factory
+     */
+    public function __construct(
+        Factory $factory
+    ) {
+        $this->factory = $factory;
+    }
+
+    /**
+     * @return LoopInterface
+     */
+    public function create(): LoopInterface
+    {
+        return $this->factory->create();
+    }
+}
\ No newline at end of file
diff --git a/Model/IndexerProcessor.php b/Model/IndexerProcessor.php
index a775a53..15ecd19 100644
--- a/Model/IndexerProcessor.php
+++ b/Model/IndexerProcessor.php
@@ -14,7 +14,7 @@
 
 use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface;
 use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
-use LizardMedia\AdminIndexer\Exception\ReindexFailureException;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
 
 /**
  * Class IndexerProcessor
@@ -42,17 +42,6 @@ public function __construct(
      */
     public function process(string ...$indexerIds): void
     {
-        foreach ($indexerIds as $indexerId) {
-            try {
-                $this->reindexRunner->run($indexerId);
-            } catch (\Exception $exception) {
-                throw new ReindexFailureException(
-                    __($exception->getMessage()),
-                    $indexerId,
-                    $exception,
-                    $exception->getCode()
-                );
-            }
-        }
+        $this->reindexRunner->run(...$indexerIds);
     }
 }
diff --git a/Model/ReindexRunner/AsyncReindexRunner.php b/Model/ReindexRunner/AsyncReindexRunner.php
index 881e755..bab8d3b 100644
--- a/Model/ReindexRunner/AsyncReindexRunner.php
+++ b/Model/ReindexRunner/AsyncReindexRunner.php
@@ -12,23 +12,36 @@
 
 namespace LizardMedia\AdminIndexer\Model\ReindexRunner;
 
-use LizardMedia\AdminIndexer\Api\ReindexRunner\AsyncReindexRunnerInterface;
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface;
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface;
+use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
 use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
 use Magento\Framework\Filesystem\DirectoryList;
 use Magento\Framework\Indexer\IndexerRegistry;
-use Symfony\Component\Process\Process;
-use Symfony\Component\Process\ProcessFactory;
+use React\EventLoop\LoopInterface;
+use React\ChildProcess\Process;
 
 /**
  * Class AsyncReindexRunner
  * @package LizardMedia\AdminIndexer\Model\ReindexRunner
  */
-class AsyncReindexRunner implements AsyncReindexRunnerInterface
+class AsyncReindexRunner implements ReindexRunnerInterface
 {
     /**
-     * @const string
+     * @var string
      */
-    const INDEXER_REINDEX_COMMAND = 'bin/magento indexer:reindex';
+    private const INDEXER_REINDEX_COMMAND = 'bin/magento indexer:reindex';
+
+    /**
+     * @var ProcessFactoryInterface
+     */
+    private $childProcessFactory;
+
+    /**
+     * @var LoopFactoryInterface
+     */
+    private $loopFactory;
 
     /**
      * @var MessageBagInterface
@@ -46,47 +59,86 @@ class AsyncReindexRunner implements AsyncReindexRunnerInterface
     private $indexerRegistry;
 
     /**
-     * @var ProcessFactory
-     */
-    private $processFactory;
-
-    /**
-     * ReindexRunner constructor.
+     * AsyncReindexRunner constructor.
+     * @param ProcessFactoryInterface $childProcessFactory
+     * @param LoopFactoryInterface $loopFactory
      * @param MessageBagInterface $messageBag
      * @param DirectoryList $directoryList
      * @param IndexerRegistry $indexerRegistry
-     * @param ProcessFactory $processFactory
      */
     public function __construct(
+        ProcessFactoryInterface $childProcessFactory,
+        LoopFactoryInterface $loopFactory,
         MessageBagInterface $messageBag,
         DirectoryList $directoryList,
-        IndexerRegistry $indexerRegistry,
-        ProcessFactory $processFactory
+        IndexerRegistry $indexerRegistry
     ) {
+        $this->childProcessFactory = $childProcessFactory;
+        $this->loopFactory = $loopFactory;
         $this->messageBag = $messageBag;
         $this->directoryList = $directoryList;
         $this->indexerRegistry = $indexerRegistry;
-        $this->processFactory = $processFactory;
     }
 
     /**
      * {@inheritDoc}
      */
-    public function run(string $indexerId): void
+    public function run(string ...$indexerIds): void
+    {
+        $this->informAboutIndexing($indexerIds);
+        $indexerIds = $this->formatIndexersToBeReindex(...$indexerIds);
+        $loop = $this->instantiateLoop();
+
+        try {
+            $command = $this->buildCommand($indexerIds);
+            $process = $this->instantiateNewProcess($command);
+            $process->start($loop);
+            $loop->run();
+        } catch (\Exception $exception) {
+            $this->handleException($exception);
+        }
+    }
+
+    /**
+     * @param string[] ...$indexerIds
+     * @return string
+     */
+    private function formatIndexersToBeReindex(string ...$indexerIds): string
     {
-        $command = $this->buildCommand($indexerId);
-        $process = $this->instantiateNewProcess($command);
-        $process->start();
-        $this->messageBag->addMessage(__('Indexing of indexer %1 has been executed', $indexerId)->render());
+        return implode(' ', $indexerIds);
     }
 
     /**
-     * @param string $indexerId
+     * @param array $indexerIds
+     * @return void
+     */
+    private function informAboutIndexing(array $indexerIds): void
+    {
+        foreach ($indexerIds as $indexerId) {
+            $this->messageBag->addMessage(__('Indexing of indexer %1 has been executed', $indexerId)->render());
+        }
+    }
+
+    /**
+     * @return LoopInterface
+     */
+    private function instantiateLoop(): LoopInterface
+    {
+        return $this->loopFactory->create();
+    }
+
+    /**
+     * @param string $indexerIds
      * @return string
      */
-    private function buildCommand(string $indexerId): string
+    private function buildCommand(string $indexerIds): string
     {
-        return sprintf('php %s/%s %s', $this->getRootDir(), self::INDEXER_REINDEX_COMMAND, $indexerId);
+        return sprintf(
+            'php %s/%s %s > /dev/null 2>&1 &',
+            $this->getRootDir(),
+            self::INDEXER_REINDEX_COMMAND,
+            $indexerIds
+        );
     }
 
     /**
@@ -103,6 +155,21 @@ private function getRootDir(): string
      */
     private function instantiateNewProcess(string $command): Process
     {
-        return $this->processFactory->create(['commandline' => $command]);
+        return $this->childProcessFactory->create($command);
+    }
+
+
+    /**
+     * @param \Exception $exception
+     * @return void
+     * @throws ReindexFailureAggregateException
+     */
+    private function handleException(\Exception $exception): void
+    {
+        throw new ReindexFailureAggregateException(
+            __($exception->getMessage()),
+            $exception,
+            $exception->getCode()
+        );
     }
 }
diff --git a/Model/ReindexRunner/SyncReindexRunner.php b/Model/ReindexRunner/SyncReindexRunner.php
index fd5dbca..368acba 100644
--- a/Model/ReindexRunner/SyncReindexRunner.php
+++ b/Model/ReindexRunner/SyncReindexRunner.php
@@ -12,15 +12,21 @@
 
 namespace LizardMedia\AdminIndexer\Model\ReindexRunner;
 
-use LizardMedia\AdminIndexer\Api\ReindexRunner\SyncReindexRunnerInterface;
+use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
 use Magento\Framework\Indexer\IndexerRegistry;
 
 /**
  * Class SyncReindexRunner
  * @package LizardMedia\AdminIndexer\Model\ReindexRunner
  */
-class SyncReindexRunner implements SyncReindexRunnerInterface
+class SyncReindexRunner implements ReindexRunnerInterface
 {
+    /**
+     * @var ReindexFailureAggregateException
+     */
+    private $reindexFailureAggregateException;
+
     /**
      * @var IndexerRegistry
      */
@@ -39,9 +45,49 @@ public function __construct(
     /**
      * {@inheritDoc}
      */
-    public function run(string $indexerId): void
+    public function run(string ...$indexerIds): void
+    {
+        foreach ($indexerIds as $indexerId) {
+            try {
+                $indexer = $this->indexerRegistry->get($indexerId);
+                $indexer->reindexAll();
+            } catch (\Exception $exception) {
+                $this->addReindexFailureException();
+                $this->reindexFailureAggregateException->addError(
+                    __(
+                        'Indexing of %1 has failed: %2',
+                        $indexerId,
+                        $exception->getMessage()
+                    )
+                );
+
+                continue;
+            }
+        }
+
+        $this->handleExceptions();
+    }
+
+    /**
+     * @return void
+     */
+    private function addReindexFailureException(): void
+    {
+        if (!$this->reindexFailureAggregateException instanceof ReindexFailureAggregateException) {
+            $this->reindexFailureAggregateException = new ReindexFailureAggregateException(
+                __('Following indexing errors has occurred: ')
+            );
+        }
+    }
+
+    /**
+     * @throws ReindexFailureAggregateException
+     * @return void
+     */
+    private function handleExceptions(): void
     {
-        $indexer = $this->indexerRegistry->get($indexerId);
-        $indexer->reindexAll();
+        if ($this->reindexFailureAggregateException instanceof ReindexFailureAggregateException) {
+            throw $this->reindexFailureAggregateException;
+        }
     }
 }
diff --git a/README.md b/README.md
index 99a13f7..3fcd23a 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ bin/magento setup:upgrade
 
 ## For developers
 
-Indexing is performed in background, each index in separate process.
+Indexing is performed in background, using reactPHP child process component
 
 ## Contributing
 
@@ -73,5 +73,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
 
 ## To do
 
-* write unit test for module
-* add possibility to track indexing progress
\ No newline at end of file
+* add possibility to track indexing progress
+* add integration tests
\ No newline at end of file
diff --git a/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php b/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php
new file mode 100644
index 0000000..9460a28
--- /dev/null
+++ b/Test/Unit/Controller/Adminhtml/Indexer/MassReindexTest.php
@@ -0,0 +1,223 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: MassReindexTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Test\Unit\Controller\Adminhtml\Indexer;
+
+use LizardMedia\AdminIndexer\Api\IndexerProcessorInterface;
+use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface;
+use LizardMedia\AdminIndexer\Controller\Adminhtml\Indexer\MassReindex;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
+use Magento\Backend\App\Action\Context;
+use Magento\Framework\App\RequestInterface;
+use Magento\Framework\Controller\Result\Redirect;
+use Magento\Framework\Controller\Result\RedirectFactory;
+use Magento\Framework\Message\ManagerInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class MassReindexTest
+ * @package LizardMedia\AdminIndexer\Test\Unit\Controller\Adminhtml\Indexer
+ */
+class MassReindexTest extends TestCase
+{
+    /**
+     * @var IndexerProcessorInterface | MockObject
+     */
+    private $indexerProcessorMock;
+
+    /**
+     * @var MessageBagInterface | MockObject
+     */
+    private $messageBagMock;
+
+    /**
+     * @var MassReindex
+     */
+    private $massReindex;
+
+    /**
+     * @var ReindexFailureAggregateException
+     */
+    private $reindexFailureAggregateException;
+
+    /**
+     * @var Redirect | MockObject
+     */
+    private $redirectMock;
+
+    /**
+     * @var RequestInterface | MockObject
+     */
+    private $requestMock;
+
+    /**
+     * @var RedirectFactory | MockObject
+     */
+    private $redirectFactoryMock;
+
+    /**
+     * @var ManagerInterface | MockObject
+     */
+    private $messageManager;
+
+    /**
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        //Internal mocks
+        $this->reindexFailureAggregateException = new ReindexFailureAggregateException(__('Some message'));
+        $context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock();
+        $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMock();
+        $this->redirectMock = $this->getMockBuilder(Redirect::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $this->messageManager = $this->getMockBuilder(ManagerInterface::class)->getMock();
+
+        //Dependencies mocks
+        $this->messageBagMock = $this->getMockBuilder(MessageBagInterface::class)->getMock();
+        $this->redirectFactoryMock = $this->getMockBuilder(RedirectFactory::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $this->indexerProcessorMock = $this->getMockBuilder(IndexerProcessorInterface::class)->getMock();
+
+        $context->expects($this->once())
+            ->method('getRequest')
+            ->willReturn($this->requestMock);
+        $context->expects($this->once())
+            ->method('getMessageManager')
+            ->willReturn($this->messageManager);
+
+        $this->massReindex = new MassReindex(
+            $this->messageBagMock,
+            $context,
+            $this->redirectFactoryMock,
+            $this->indexerProcessorMock
+        );
+    }
+
+
+    /**
+     * @test
+     * @dataProvider provideInvalidRequestParamsExamples
+     * @param $invalidParam
+     * @return void
+     */
+    public function testExecuteWhenParamsAreInvalid($invalidParam): void
+    {
+        $this->requestMock->expects($this->once())
+            ->method('getParam')
+            ->with('indexer_ids')
+            ->willReturn($invalidParam);
+        $this->gettingRedirectExpectations();
+        $this->massReindex->execute();
+    }
+
+
+    /**
+     * @return array
+     */
+    public function provideInvalidRequestParamsExamples(): array
+    {
+        return [
+            [[]],
+            [''],
+            ['test'],
+            [1],
+            [null]
+        ];
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testExecuteWhenIndexerProcessorThrowsException(): void
+    {
+        $this->requestMock->expects($this->once())
+            ->method('getParam')
+            ->with('indexer_ids')
+            ->willReturn(['catalog_product', 'customer_grid']);
+        $this->indexerProcessorMock->expects($this->once())
+            ->method('process')
+            ->with('catalog_product', 'customer_grid')
+            ->willThrowException($this->reindexFailureAggregateException);
+        $this->simulateTwoIndexerErros();
+
+        $this->messageBagMock->expects($this->never())->method('getMessages');
+
+        $this->messageManager->expects($this->exactly(3))
+            ->method('addErrorMessage')
+            ->withConsecutive(
+                [$this->reindexFailureAggregateException->getMessage()],
+                ['sth'],
+                ['sth else']
+            );
+        $this->gettingRedirectExpectations();
+        $this->massReindex->execute();
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testExecuteWhenSucceed(): void
+    {
+        $this->requestMock->expects($this->once())
+            ->method('getParam')
+            ->with('indexer_ids')
+            ->willReturn(['catalog_product', 'customer_grid']);
+        $this->indexerProcessorMock->expects($this->once())
+            ->method('process')
+            ->with('catalog_product', 'customer_grid');
+        $this->expectationsForInformingAboutIndexersRunning();
+        $this->gettingRedirectExpectations();
+        $this->massReindex->execute();
+    }
+
+
+    /**
+     * @return void
+     */
+    private function gettingRedirectExpectations(): void
+    {
+        $this->redirectFactoryMock->expects($this->once())
+            ->method('create')
+            ->willReturn($this->redirectMock);
+        $this->redirectMock->expects($this->once())
+            ->method('setPath')
+            ->with('indexer/indexer/list')
+            ->willReturnSelf();
+    }
+
+    /**
+     * @return void
+     */
+    private function expectationsForInformingAboutIndexersRunning(): void
+    {
+        $this->messageBagMock->expects($this->once())
+            ->method('getMessages')
+            ->willReturn(['one', 'two']);
+        $this->messageManager->expects($this->exactly(2))
+            ->method('addNoticeMessage')
+            ->withConsecutive(['one'], ['two']);
+    }
+
+    /**
+     * @return void
+     */
+    private function simulateTwoIndexerErros(): void
+    {
+        $this->reindexFailureAggregateException->addError(__('sth'));
+        $this->reindexFailureAggregateException->addError(__('sth else'));
+    }
+}
diff --git a/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php b/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php
new file mode 100644
index 0000000..1d0dd90
--- /dev/null
+++ b/Test/Unit/Model/Adapter/ReactPHP/ChildProcess/ProcessFactoryTest.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: ProcessFactoryTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP;
+
+use LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\ChildProcess\ProcessFactory;
+use PHPUnit\Framework\TestCase;
+use React\ChildProcess\Process;
+
+/**
+ * Class ProcessFactoryTest
+ * @package LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\ChildProcess
+ */
+class ProcessFactoryTest extends TestCase
+{
+    /**
+     * @test
+     * @return void
+     */
+    public function testCreateReturnsCorrectInstance(): void
+    {
+        $processFactory = new ProcessFactory();
+        $this->assertInstanceOf(
+            Process::class,
+            $processFactory->create('ls')
+        );
+    }
+}
diff --git a/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php b/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php
new file mode 100644
index 0000000..db0ebeb
--- /dev/null
+++ b/Test/Unit/Model/Adapter/ReactPHP/EventLoop/LoopFactoryTest.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: LoopFactoryTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\EventLoop;
+
+use LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop\LoopFactory;
+use PHPUnit\Framework\TestCase;
+use React\EventLoop\Factory;
+use React\EventLoop\LoopInterface;
+
+/**
+ * Class LoopFactoryTest
+ * @package LizardMedia\AdminIndexer\Test\Unit\Model\Adapter\ReactPHP\EventLoop
+ */
+class LoopFactoryTest extends TestCase
+{
+    /**
+     * @test
+     * @return void
+     */
+    public function testCreateReturnsCorrectInstance(): void
+    {
+        $loopFactory = new LoopFactory(new Factory());
+        $this->assertInstanceOf(
+            LoopInterface::class,
+            $loopFactory->create()
+        );
+    }
+}
\ No newline at end of file
diff --git a/Test/Unit/Model/IndexerProcessorTest.php b/Test/Unit/Model/IndexerProcessorTest.php
new file mode 100644
index 0000000..0024143
--- /dev/null
+++ b/Test/Unit/Model/IndexerProcessorTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: IndexerProcessorTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Unit\Test\Model;
+
+use LizardMedia\AdminIndexer\Api\ReindexRunnerInterface;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
+use LizardMedia\AdminIndexer\Model\IndexerProcessor;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class IndexerProcessorTest
+ * @package LizardMedia\AdminIndexer\Test\Unit\Model
+ */
+class IndexerProcessorTest extends TestCase
+{
+    /**
+     * @var ReindexRunnerInterface | MockObject
+     */
+    private $reindexRunnerMock;
+
+    /**
+     * @var IndexerProcessor
+     */
+    private $indexerProcessor;
+
+    /**
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        $this->reindexRunnerMock = $this->getMockBuilder(ReindexRunnerInterface::class)->getMock();
+        $this->indexerProcessor = new IndexerProcessor($this->reindexRunnerMock);
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testWhenReindexeRunnerThrowsException(): void
+    {
+        $this->reindexRunnerMock->expects($this->once())
+            ->method('run')
+            ->withAnyParameters()
+            ->willThrowException(new ReindexFailureAggregateException(__()));
+        $this->expectException(ReindexFailureAggregateException::class);
+        $this->indexerProcessor->process('catalog_category_product', 'catalog_product_category');
+    }
+
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testWhenReindexed(): void
+    {
+        $exampleIndexers = ['catalog_category_product', 'catalog_product_category'];
+        $this->reindexRunnerMock->expects($this->once())
+            ->method('run')
+            ->with(...$exampleIndexers);
+        $this->indexerProcessor->process('catalog_category_product', 'catalog_product_category');
+    }
+}
diff --git a/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php b/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php
new file mode 100644
index 0000000..76e648f
--- /dev/null
+++ b/Test/Unit/Model/ReindexRunner/AsyncReindexRunnerTest.php
@@ -0,0 +1,174 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: AsyncReindexRunnerTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Test\Unit\Model\ReindexRunner;
+
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface;
+use LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface;
+use LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface;
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
+use LizardMedia\AdminIndexer\Model\ReindexRunner\AsyncReindexRunner;
+use Magento\Framework\Filesystem\DirectoryList;
+use Magento\Framework\Indexer\IndexerInterface;
+use Magento\Framework\Indexer\IndexerRegistry;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use PHPUnit\Framework\TestCase;
+use React\ChildProcess\Process as ChildProcess;
+use React\EventLoop\LoopInterface;
+
+/**
+ * Class AsyncReindexRunnerTest
+ * @package LizardMedia\AdminIndexer\Test\Unit\Model\ReindexRunner
+ */
+class AsyncReindexRunnerTest extends TestCase
+{
+    /**
+     * @var array
+     */
+    private $exampleIndexes;
+
+    /**
+     * @var ProcessFactoryInterface | MockObject
+     */
+    private $childProcessFactoryMock;
+
+    /**
+     * @var LoopFactoryInterface | MockObject
+     */
+    private $loopFactory;
+
+    /**
+     * @var MessageBagInterface | MockObject
+     */
+    private $messageBagMock;
+
+    /**
+     * @var ReindexFailureAggregateException | MockObject
+     */
+    private $reindexFailureAggregateExceptionMock;
+
+    /**
+     * @var AsyncReindexRunner
+     */
+    private $asyncReindexRunner;
+
+    /**
+     * @var DirectoryList | MockObject
+     */
+    private $directoryListMock;
+
+    /**
+     * @var IndexerInterface | MockObject
+     */
+    private $indexerMock;
+
+    /**
+     * @var IndexerRegistry | MockObject
+     */
+    private $indexerRegistryMock;
+
+    /**
+     * @var ChildProcess | MockObject
+     */
+    private $childProcess;
+
+    /**
+     * @var LoopInterface | MockObject
+     */
+    private $loopMock;
+
+    /**
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        //Internal mocks
+        $this->exampleIndexes = ['catalog_product', 'customer_grid'];
+        $this->reindexFailureAggregateExceptionMock = $this->getMockBuilder(ReindexFailureAggregateException::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $this->indexerMock = $this->getMockBuilder(IndexerInterface::class)->getMock();
+        $this->loopMock = $this->getMockBuilder(LoopInterface::class)->getMock();
+
+        //Dependencies mocks
+        $this->childProcessFactoryMock = $this->getMockBuilder(ProcessFactoryInterface::class)->getMock();
+        $this->loopFactory = $this->getMockBuilder(LoopFactoryInterface::class)->getMock();
+        $this->messageBagMock = $this->getMockBuilder(MessageBagInterface::class)->getMock();
+        $this->directoryListMock = $this->getMockBuilder(DirectoryList::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $this->childProcess = $this->getMockBuilder(ChildProcess::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $this->indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->asyncReindexRunner = new AsyncReindexRunner(
+            $this->childProcessFactoryMock,
+            $this->loopFactory,
+            $this->messageBagMock,
+            $this->directoryListMock,
+            $this->indexerRegistryMock
+        );
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testRunWhenProcessStartingThrownException(): void
+    {
+        $this->initialExpectations();
+        $this->childProcess->expects($this->once())
+            ->method('start')
+            ->with($this->loopMock)
+            ->willThrowException(new \RuntimeException);
+        $this->loopMock->expects($this->never())->method('run');
+
+        $this->expectException(ReindexFailureAggregateException::class);
+        $this->asyncReindexRunner->run(...$this->exampleIndexes);
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testRunWhenSucceed(): void
+    {
+        $this->initialExpectations();
+        $this->childProcess->expects($this->once())
+            ->method('start');
+        $this->loopMock->expects($this->once())
+            ->method('run');
+
+        $this->asyncReindexRunner->run(...$this->exampleIndexes);
+    }
+
+    /**
+     * @return void
+     */
+    private function initialExpectations(): void
+    {
+        $this->messageBagMock->expects($this->exactly(2))
+            ->method('addMessage');
+        $this->loopFactory->expects($this->once())
+            ->method('create')
+            ->willReturn($this->loopMock);
+        $this->directoryListMock->expects($this->once())
+            ->method('getRoot')
+            ->willReturn('/var/www/html');
+        $this->childProcessFactoryMock->expects($this->once())
+            ->method('create')
+            ->with(sprintf('php /var/www/html/bin/magento indexer:reindex %s > /dev/null 2>&1 &', implode(' ', $this->exampleIndexes)))
+            ->willReturn($this->childProcess);
+    }
+}
diff --git a/Test/Unit/Model/ReindexRunner/MessageBagTest.php b/Test/Unit/Model/ReindexRunner/MessageBagTest.php
new file mode 100644
index 0000000..4758d7f
--- /dev/null
+++ b/Test/Unit/Model/ReindexRunner/MessageBagTest.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: MessageBagTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner;
+
+use LizardMedia\AdminIndexer\Model\ReindexRunner\MessageBag;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class MessageBagTest
+ * @package LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner
+ */
+class MessageBagTest extends TestCase
+{
+    /**
+     * @test
+     * @return void
+     */
+    public function testAddMessage(): void
+    {
+        $messageBag = new MessageBag();
+        $messageBag->addMessage('test');
+        $this->assertAttributeSame(['test'], 'messages', $messageBag);
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testGetMessage(): void
+    {
+        $messageBag = new MessageBag();
+        $messageBag->addMessage('test');
+        $messageBag->addMessage('test1');
+
+        $this->assertSame(['test', 'test1'], $messageBag->getMessages());
+    }
+}
diff --git a/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php b/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php
new file mode 100644
index 0000000..9ea09e9
--- /dev/null
+++ b/Test/Unit/Model/ReindexRunner/SyncReindexRunnerTest.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * File: SyncReindexRunnerTest.php
+ *
+ * @author Bartosz Kubicki bartosz.kubicki@lizardmedia.pl>
+ * @copyright Copyright (C) 2018 Lizard Media (http://lizardmedia.pl)
+ */
+
+namespace LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner;
+
+use LizardMedia\AdminIndexer\Exception\ReindexFailureAggregateException;
+use LizardMedia\AdminIndexer\Model\ReindexRunner\SyncReindexRunner;
+use Magento\Framework\Indexer\IndexerInterface;
+use Magento\Framework\Indexer\IndexerRegistry;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class SyncReindexRunnerTest
+ * @package LizardMedia\AdminIndexer\Unit\Test\Model\ReindexRunner
+ */
+class SyncReindexRunnerTest extends TestCase
+{
+    /**
+     * @var array
+     */
+    private $exampleIndexes;
+
+    /**
+     * @var ReindexFailureAggregateException | MockObject
+     */
+    private $reindexFailureAggregateExceptionMock;
+
+    /**
+     * @var SyncReindexRunner
+     */
+    private $syncReindexRunner;
+
+    /**
+     * @var IndexerInterface | MockObject
+     */
+    private $indexerMock;
+
+    /**
+     * @var IndexerRegistry | MockObject
+     */
+    private $indexerRegistryMock;
+
+    /**
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        //Internal mocks
+        $this->exampleIndexes = ['catalog_product', 'customer_grid'];
+        $this->reindexFailureAggregateExceptionMock = $this->getMockBuilder(ReindexFailureAggregateException::class)
+            ->disableOriginalConstructor()
+            ->setMethodsExcept(['addError', 'getErrors'])
+            ->getMock();
+        $this->indexerMock = $this->getMockBuilder(IndexerInterface::class)->getMock();
+
+        //Dependencies mocks
+        $this->indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->syncReindexRunner = new SyncReindexRunner($this->indexerRegistryMock);
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testRunWhenIndexerThrowsException(): void
+    {
+        $this->indexerRegistryMock->expects($this->exactly(2))
+            ->method('get')
+            ->withConsecutive(
+                [$this->exampleIndexes[0]],
+                [$this->exampleIndexes[1]]
+            )->willReturn($this->indexerMock);
+
+        $indexerException = new \Exception('Something went wrong');
+
+        $this->indexerMock->expects($this->at(0))
+            ->method('reindexAll')
+            ->willThrowException($indexerException);
+        $this->indexerMock->expects($this->at(1))
+            ->method('reindexAll');
+
+        $this->expectException(ReindexFailureAggregateException::class);
+        $this->syncReindexRunner->run(...$this->exampleIndexes);
+        $this->assertCount(1, $this->reindexFailureAggregateExceptionMock->getErrors());
+    }
+
+    /**
+     * @test
+     * @return void
+     */
+    public function testRunWhenSucceed(): void
+    {
+        $this->indexerRegistryMock->expects($this->exactly(2))
+            ->method('get')
+            ->withConsecutive(
+                [$this->exampleIndexes[0]],
+                [$this->exampleIndexes[1]]
+            )->willReturn($this->indexerMock);
+
+        $this->indexerMock->expects($this->exactly(2))
+            ->method('reindexAll');
+
+        $this->syncReindexRunner->run(...$this->exampleIndexes);
+    }
+}
diff --git a/composer.json b/composer.json
index 29c97e8..c0817e5 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
   "name": "lizardmedia/module-admin-indexer",
   "description": "Magento2 module adding indexation from the admin panel",
   "type": "magento2-module",
-  "version": "1.0.2",
+  "version": "1.1.0",
   "license": [
     "MIT"
   ],
@@ -19,7 +19,7 @@
   "require": {
     "php": "^7.1.0",
     "magento/module-indexer": "^100.2.3",
-    "symfony/process": "^2.7 || ^3.0"
+    "react/child-process": "^0.5.2"
   },
   "require-dev": {
     "phpunit/phpunit": ">=7.0.0"
diff --git a/etc/di.xml b/etc/di.xml
index cb6795a..0561769 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -10,12 +10,12 @@
         xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
 
     <!-- API section-->
+    <preference for="LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\ChildProcess\ProcessFactoryInterface"
+                type="LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\ChildProcess\ProcessFactory"/>
+    <preference for="LizardMedia\AdminIndexer\Api\Adapter\ReactPHP\EventLoop\LoopFactoryInterface"
+                type="LizardMedia\AdminIndexer\Model\Adapter\ReactPHP\EventLoop\LoopFactory"/>
     <preference for="LizardMedia\AdminIndexer\Api\IndexerProcessorInterface"
                 type="LizardMedia\AdminIndexer\Model\IndexerProcessor" />
-    <preference for="LizardMedia\AdminIndexer\Api\ReindexRunner\SyncReindexRunnerInterface"
-                type="LizardMedia\AdminIndexer\Model\ReindexRunner\SyncReindexRunner" />
-    <preference for="LizardMedia\AdminIndexer\Api\ReindexRunner\AsyncReindexRunnerInterface"
-                type="LizardMedia\AdminIndexer\Model\ReindexRunner\AsyncReindexRunner" />
     <preference for="LizardMedia\AdminIndexer\Api\ReindexRunner\MessageBagInterface"
                 type="LizardMedia\AdminIndexer\Model\ReindexRunner\MessageBag"/>
     <!-- End of API section-->
@@ -23,7 +23,7 @@
     <!-- Construct arguments replacements section -->
     <type name="LizardMedia\AdminIndexer\Model\IndexerProcessor">
         <arguments>
-            <argument name="reindexRunner" xsi:type="object">LizardMedia\AdminIndexer\Api\ReindexRunner\AsyncReindexRunnerInterface</argument>
+            <argument name="reindexRunner" xsi:type="object">LizardMedia\AdminIndexer\Model\ReindexRunner\AsyncReindexRunner</argument>
         </arguments>
     </type>
     <!-- End of construct arguments replacements section -->
diff --git a/etc/module.xml b/etc/module.xml
index 30c0e21..ac0815a 100644
--- a/etc/module.xml
+++ b/etc/module.xml
@@ -7,7 +7,7 @@
 -->
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
-    <module name="LizardMedia_AdminIndexer" setup_version="1.0.2">
+    <module name="LizardMedia_AdminIndexer" setup_version="1.1.0">
         <sequence>
             <module name="Magento_Indexer"/>
         </sequence>
diff --git a/i18n/en_US.csv b/i18n/en_US.csv
index ae628af..9801df9 100644
--- a/i18n/en_US.csv
+++ b/i18n/en_US.csv
@@ -2,4 +2,6 @@
 "Please select at least one index.","Please select at least one index."
 "LizardMedia admin indexer","LizardMedia admin indexer"
 "Reindex failed on indexer %1","Reindex failed on indexer %1"
-"Indexing of indexer %1 has been executed","Indexing of indexer %1 has been executed"
\ No newline at end of file
+"Indexing of indexer %1 has been executed","Indexing of indexer %1 has been executed"
+"Following indexing errors has occured: ","Following indexing errors has occured: "
+"Indexing of %1 has failed: %2","Indexing of %1 has failed: %2"
\ No newline at end of file
diff --git a/i18n/pl_PL.csv b/i18n/pl_PL.csv
index ac8a8f3..7be3248 100644
--- a/i18n/pl_PL.csv
+++ b/i18n/pl_PL.csv
@@ -3,3 +3,5 @@
 "LizardMedia admin indexer","LizardMedia admin indexer"
 "Reindex failed on indexer %1","Błąd indeksowania na indeksie %1"
 "Indexing of indexer %1 has been executed","Rozpoczęto indeksowanie indeksu %1"
+"Following indexing errors has occured: ","Następujące błędy indeksowanie miały miejsce: "
+"Indexing of %1 has failed: %2","Indeksowanie %1 nie udało się: %2"