artur-graniszewski/ZEUS-for-PHP

View on GitHub
src/Zeus/ServerService/Manager.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

namespace Zeus\ServerService;

use Zend\Log\LoggerInterface;
use Zeus\Kernel\ProcessManager\Helper\EventManager;
use Zeus\Kernel\ProcessManager\Helper\PluginRegistry;
use Zeus\Kernel\ProcessManager\SchedulerEvent;

final class Manager
{
    use EventManager;
    use PluginRegistry;

    /** @var ServerServiceInterface[] */
    protected $services;

    /** @var \Exception[] */
    protected $brokenServices = [];

    protected $eventHandles;

    /** @var ManagerEvent */
    protected $event;

    /** @var LoggerInterface */
    protected $logger;

    /** @var int */
    protected $servicesRunning = 0;

    /** @var ServerServiceInterface[] */
    protected $pidToServiceMap = [];

    public function __construct(array $services)
    {
        $this->services = $services;
    }

    public function __destruct()
    {
        if ($this->eventHandles) {
            $events = $this->getEventManager();
            foreach ($this->eventHandles as $handle) {
                $events->detach($handle);
            }
        }
    }

    /**
     * @return mixed[]
     */
    protected function checkSignal()
    {
        $pid = pcntl_waitpid(-1, $status, WNOHANG);

        if ($pid > 0) {
            return ['pid' => $pid, 'status' => $status];
        }
    }

    /**
     * @return $this
     */
    protected function attach()
    {
        $events = $this->getEventManager();

        $this->eventHandles[] = $events->attach(ManagerEvent::EVENT_MANAGER_LOOP, function (ManagerEvent $e) {
            $signal = $this->checkSignal();

            if (!$signal) {
                sleep(1);
            }

            $service = $this->findServiceByPid($signal['pid']);

            if ($service) {
                $this->onServiceStop($service);
            }
        }, -10000);
    }

    /**
     * @return ManagerEvent
     */
    protected function getEvent()
    {
        if (!$this->event) {
            $this->event = new ManagerEvent();
            $this->event->setManager($this);
        }

        return $this->event;
    }

        /**
     * @param string $serviceName
     * @return ServerServiceInterface
     */
    protected function getService($serviceName)
    {
        if (!isset($this->services[$serviceName]['service'])) {
            throw new \RuntimeException("Service \"$serviceName\" not found");
        }

        $service = $this->services[$serviceName]['service'];
        return ($service instanceof ServerServiceInterface ? $service : $service());
    }

    /**
     * @param bool $isAutoStart
     * @return string[]
     */
    public function getServiceList($isAutoStart)
    {
        $services = [];

        foreach ($this->services as $serviceName => $service) {
            if (!$isAutoStart || ($isAutoStart && $service['auto_start'])) {
                $services[] = $serviceName;
            }
        }
        return $services;
    }

    /**
     * @param string $serviceName
     * @param ServerServiceInterface|\Closure $service
     * @param bool $autoStart
     * @return $this
     */
    public function registerService($serviceName, $service, $autoStart)
    {
        $this->services[$serviceName] = [
            'service' => $service,
            'auto_start' => $autoStart,
        ];

        return $this;
    }

    /**
     * @param string $serviceName
     * @param \Exception|\Throwable $exception
     * @return $this
     */
    public function registerBrokenService($serviceName, $exception)
    {
        $this->brokenServices[$serviceName] = $exception;
        $this->logger->err(sprintf("Unable to start %s, service is broken: %s", $serviceName, $exception->getMessage()));

        return $this;
    }

    /**
     * @return \Exception[]
     */
    public function getBrokenServices()
    {
        return $this->brokenServices;
    }

    /**
     * @param string $serviceName
     * @return $this
     */
    public function startService($serviceName)
    {
        $this->startServices([$serviceName]);

        return $this;
    }

    /**
     * @param string $serviceName
     * @return $this
     */
    protected function doStartService($serviceName)
    {
        $service = $this->getService($serviceName);

        $event = $this->getEvent();
        $event->setName(ManagerEvent::EVENT_SERVICE_START);
        $event->setError(null);
        $event->setService($service);
        $event->stopPropagation(false);

        $this->eventHandles[] = $service->getScheduler()->getEventManager()->attach(SchedulerEvent::EVENT_SCHEDULER_STOP,
            function () use ($service) {
                $this->onServiceStop($service);
            }, -10000);

        $exception = null;
        try {
            $this->getEventManager()->triggerEvent($event);

            $service->start();
            $schedulerPid = $service->getScheduler()->getId();
            $this->logger->debug(sprintf('Scheduler running as process #%d', $schedulerPid));
            $this->pidToServiceMap[$schedulerPid] = $service;
            $this->servicesRunning++;
        } catch (\Exception $exception) {
            $this->registerBrokenService($serviceName, $exception);

            return $this;
        } catch (\Throwable $exception) {
            $this->registerBrokenService($serviceName, $exception);

            return $this;
        }

        return $this;
    }

    /**
     * @param string|string[] $serviceNames
     * @return $this
     */
    public function startServices($serviceNames)
    {
        $plugins = $this->getPluginRegistry()->count();
        $this->logger->info(sprintf("Starting Server Service Manager with %d plugin%s", $plugins, $plugins !== 1 ? 's' : ''));

        $event = $this->getEvent();

        $this->attach();

        $event->setName(ManagerEvent::EVENT_MANAGER_INIT);
        $this->getEventManager()->triggerEvent($event);

        $startTime = microtime(true);

        foreach ($serviceNames as $service) {
            $this->doStartService($service);
        }

        $now = microtime(true);
        $phpTime = $now - (float) $_SERVER['REQUEST_TIME_FLOAT'];
        $managerTime = $now - $startTime;

        $engine = defined("HHVM_VERSION") ? 'HHVM' : 'PHP';
        if ($this->servicesRunning > 0) {
            $this->logger->info(sprintf("Started %d services in %.2f seconds ($engine running for %.2fs)", $this->servicesRunning, $managerTime, $phpTime));
        }

        if ($this->servicesRunning === 0) {
            $this->logger->err(sprintf("No server service started ($engine running for %.2fs)", $managerTime, $phpTime));

            return $this;
        }

        // @todo: get rid of this loop!!
        while ($this->servicesRunning > 0 && !$event->propagationIsStopped()) {
            $event->setName(ManagerEvent::EVENT_MANAGER_LOOP);
            $event->setError(null);
            $event->stopPropagation(false);
            $this->getEventManager()->triggerEvent($event);
        }

        return $this;
    }

    /**
     * @param ServerServiceInterface[] $services
     * @param bool $mustBeRunning
     * @return int Amount of services which Manager was unable to stop
     * @throws \Exception
     */
    public function stopServices($services, $mustBeRunning)
    {
        $servicesAmount = 0;
        foreach ($services as $service) {
            try {
                $this->stopService($service);
                $servicesAmount++;
            } catch (\Exception $exception) {
                if ($mustBeRunning) {
                    throw $exception;
                }
            }
        }

        $servicesLeft = $servicesAmount;

        $signalInfo = [];

        if (function_exists('pcntl_sigtimedwait')) {
            while ($servicesLeft > 0 && pcntl_sigtimedwait([SIGCHLD], $signalInfo, 1)) {
                $servicesLeft--;
            }
        }

        if ($servicesLeft) {
            $this->logger->warn(sprintf("Only %d out of %d services were stopped gracefully", $servicesAmount - $servicesLeft, $servicesAmount));
        }

        $this->logger->info(sprintf("Stopped %d service(s)", $servicesAmount - $servicesLeft));

        return $servicesLeft;
    }

    /**
     * @param string $serviceName
     * @return $this
     */
    public function stopService($serviceName)
    {
        $service = $this->getService($serviceName);
        $service->stop();

        $this->onServiceStop($service);

        return $this;
    }

    /**
     * @param ServerServiceInterface $service
     * @return $this
     */
    protected function onServiceStop(ServerServiceInterface $service)
    {
        $this->servicesRunning--;

        $event = $this->getEvent();
        $event->setName(ManagerEvent::EVENT_SERVICE_STOP);
        $event->setError(null);
        $event->setService($service);
        $event->stopPropagation(false);
        $this->getEventManager()->triggerEvent($event);

        if ($this->servicesRunning === 0) {
            $this->logger->info("All services exited");
        }

        return $this;
    }

    /**
     * @param string $serviceName
     * @return mixed[]
     */
    public function getServiceConfig($serviceName)
    {
        $service = $this->getService($serviceName);

        return $service->getConfig();
    }

    /**
     * @param string $serviceName
     * @param object $statusDecorator
     * @return mixed
     * @internal
     */
    public function getServiceStatus($serviceName, $statusDecorator)
    {
        $service = $this->getService($serviceName);
        $status = $statusDecorator->getStatus($service);

        return $status;
    }

    /**
     * @param int $pid
     * @return null|ServerServiceInterface
     */
    protected function findServiceByPid($pid)
    {
        if (!isset($this->pidToServiceMap[$pid])) {
            return null;
        }

        $service = $this->pidToServiceMap[$pid];

        return $service;
    }

    /**
     * @return LoggerInterface
     */
    public function getLogger()
    {
        return $this->logger;
    }

    /**
     * @param LoggerInterface $logger
     * @return $this
     */
    public function setLogger($logger)
    {
        $this->logger = $logger;

        return $this;
    }
}