Skip to content

Commit

Permalink
Merge pull request #100 from magento-hackathon/process-management
Browse files Browse the repository at this point in the history
Process management
  • Loading branch information
Ethan Yehuda authored Feb 27, 2019
2 parents b81b7ed + 2225703 commit 632ed13
Show file tree
Hide file tree
Showing 32 changed files with 954 additions and 108 deletions.
25 changes: 25 additions & 0 deletions Api/Data/ScheduleInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@

namespace EthanYehuda\CronjobManager\Api\Data;

use Magento\Cron\Model\Schedule;

interface ScheduleInterface
{
const STATUS_PENDING = Schedule::STATUS_PENDING;

const STATUS_RUNNING = Schedule::STATUS_RUNNING;

const STATUS_SUCCESS = Schedule::STATUS_SUCCESS;

const STATUS_MISSED = Schedule::STATUS_MISSED;

const STATUS_ERROR = Schedule::STATUS_ERROR;

const STATUS_KILLED = 'killed';

/**
* @return int
*/
Expand Down Expand Up @@ -49,6 +63,11 @@ public function getExecutedAt();
*/
public function getFinishedAt();

/**
* @return string|null
*/
public function getKillRequest();

/**
* @param int $scheduleId
* @return ScheduleInterface
Expand Down Expand Up @@ -102,4 +121,10 @@ public function setExecutedAt(string $executedAt): self;
* @return ScheduleInterface
*/
public function setFinishedAt(string $finishedAt): self;

/**
* @param string $killRequest
* @return ScheduleInterface
*/
public function setKillRequest(string $killRequest): self;
}
7 changes: 7 additions & 0 deletions Api/ScheduleManagementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,11 @@ public function schedule(string $jobCode, int $time): Schedule;
* @return bool
*/
public function flush(): bool;

/**
* @param int $jobId
* @return bool
*/
public function kill(int $jobId, int $timestamp): bool;

}
9 changes: 9 additions & 0 deletions Api/ScheduleRepositoryAdapterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace EthanYehuda\CronjobManager\Api;

use EthanYehuda\CronjobManager\Api\Data\ScheduleInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;

Expand Down Expand Up @@ -31,4 +32,12 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr
* @throws CouldNotSaveException
*/
public function save(\EthanYehuda\CronjobManager\Api\Data\ScheduleInterface $schedule, $scheduleId = null): \EthanYehuda\CronjobManager\Api\Data\ScheduleInterface;

/**
* Return all jobs with given status
*
* @param string $status
* @return ScheduleInterface[]
*/
public function getByStatus($status);
}
52 changes: 52 additions & 0 deletions Controller/Adminhtml/Manage/Job/Kill.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace EthanYehuda\CronjobManager\Controller\Adminhtml\Manage\Job;

use EthanYehuda\CronjobManager\Api\ScheduleManagementInterface;
use Magento\Backend\App\AbstractAction;
use Magento\Backend\App\Action\Context;

class Kill extends AbstractAction
{
const ADMIN_RESOURCE = "EthanYehuda_CronjobManager::cronjobmanager";

/**
* @var ScheduleManagementInterface
*/
private $scheduleManagement;

/**
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Magento\Backend\App\Action\Context $context
*/
public function __construct(
Context $context,
ScheduleManagementInterface $scheduleManagement
) {
parent::__construct($context);
$this->scheduleManagement = $scheduleManagement;
}

/**
* Save cronjob
*
* @return Void
*/
public function execute()
{
$jobId = (int)$this->getRequest()->getParam('id');
$jobCode = $this->getRequest()->getParam('job_code');
try {
if ($this->scheduleManagement->kill($jobId, \time())) {
$this->getMessageManager()->addSuccessMessage("Job will be killed by next cron run: {$jobCode}");
} else {
$this->getMessageManager()->addNoticeMessage("Job cannot be killed.");
}
} catch (\Exception $e) {
$this->getMessageManager()->addErrorMessage($e->getMessage());
$this->_redirect('*/manage/index/');
return;
}
$this->_redirect('*/manage/index/');
}
}
74 changes: 74 additions & 0 deletions Model/CleanRunningJobs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);

namespace EthanYehuda\CronjobManager\Model;

use EthanYehuda\CronjobManager\Api\Data\ScheduleInterface;
use EthanYehuda\CronjobManager\Api\ScheduleRepositoryAdapterInterface;
use Magento\Cron\Model\ResourceModel\Schedule\CollectionFactory;
use Magento\Cron\Model\Schedule;
use Magento\Framework\Stdlib\DateTime\DateTime;

/**
* Update jobs with dead processes from running to error
*/
class CleanRunningJobs
{
/**
* @var ProcessManagement
*/
private $processManagement;
/**
* @var ScheduleRepositoryAdapterInterface
*/
private $scheduleRepository;
/**
* @var Clock
*/
private $clock;
/**
* @var DateTime
*/
private $dateTime;

public function __construct(
ScheduleRepositoryAdapterInterface $scheduleRepository,
ProcessManagement $processManagement,
DateTime $dateTime,
Clock $clock
) {
$this->processManagement = $processManagement;
$this->scheduleRepository = $scheduleRepository;
$this->dateTime = $dateTime;
$this->clock = $clock;
}

/**
* Find all jobs in status "running" (according to db),
* and check if the process is alive. If not, set status to error, with the message
* "Process went away"
*/
public function execute()
{
$runningJobs = $this->scheduleRepository->getByStatus(ScheduleInterface::STATUS_RUNNING);

foreach ($runningJobs as $schedule) {
if ($this->processManagement->isPidAlive($schedule->getPid())) {
continue;
}

$messages = [];
if ($schedule->getMessages()) {
$messages[] = $schedule->getMessages();
}

$messages[] = __('Process went away at %1', $this->dateTime->gmtDate(null, $this->clock->now()));

$schedule
->setStatus(Schedule::STATUS_ERROR)
->setMessages(implode("\n", $messages));

$this->scheduleRepository->save($schedule);
}
}
}
9 changes: 9 additions & 0 deletions Model/Clock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace EthanYehuda\CronjobManager\Model;

interface Clock
{
public function now(): int;
}
29 changes: 21 additions & 8 deletions Model/Data/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

namespace EthanYehuda\CronjobManager\Model\Data;

use EthanYehuda\CronjobManager\Api\Data\ScheduleInterface;
use Magento\Framework\DataObject;

/**
* @codeCoverageIgnore
*/
class Schedule extends DataObject implements \EthanYehuda\CronjobManager\Api\Data\ScheduleInterface
{
const KEY_SCHEDULE_ID = 'schedule_id';
const KEY_JOB_CODE = 'job_code';
const KEY_STATUS = 'status';
const KEY_PID = 'pid';
const KEY_MESSAGES = 'messages';
const KEY_CREATED_AT = 'created_at';
const KEY_SCHEDULE_ID = 'schedule_id';
const KEY_JOB_CODE = 'job_code';
const KEY_STATUS = 'status';
const KEY_PID = 'pid';
const KEY_MESSAGES = 'messages';
const KEY_CREATED_AT = 'created_at';
const KEY_SCHEDULED_AT = 'scheduled_at';
const KEY_EXECUTED_AT = 'executed_at';
const KEY_FINISHED_AT = 'finished_at';
const KEY_EXECUTED_AT = 'executed_at';
const KEY_FINISHED_AT = 'finished_at';
const KEY_KILL_REQUEST = 'kill_request';

public function __construct(array $data = [])
{
Expand Down Expand Up @@ -69,6 +71,11 @@ public function getFinishedAt()
return $this->getData(self::KEY_FINISHED_AT);
}

public function getKillRequest()
{
return $this->getData(self::KEY_KILL_REQUEST);
}

public function setScheduleId(int $scheduleId): \EthanYehuda\CronjobManager\Api\Data\ScheduleInterface
{
$this->setData(self::KEY_SCHEDULE_ID, $scheduleId);
Expand Down Expand Up @@ -111,6 +118,12 @@ public function setScheduledAt(string $scheduledAt): \EthanYehuda\CronjobManager
return $this;
}

public function setKillRequest(string $killRequest): \EthanYehuda\CronjobManager\Api\Data\ScheduleInterface
{
$this->setData(self::KEY_KILL_REQUEST, $killRequest);
return $this;
}

public function setExecutedAt(string $executedAt): \EthanYehuda\CronjobManager\Api\Data\ScheduleInterface
{
$this->setData(self::KEY_EXECUTED_AT, $executedAt);
Expand Down
67 changes: 67 additions & 0 deletions Model/ProcessKillRequests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);

namespace EthanYehuda\CronjobManager\Model;

use EthanYehuda\CronjobManager\Api\Data\ScheduleInterface;
use EthanYehuda\CronjobManager\Api\ScheduleRepositoryAdapterInterface;
use Magento\Framework\Stdlib\DateTime\DateTime;

class ProcessKillRequests
{
/**
* @var ProcessManagement
*/
private $processManagement;

/**
* @var ScheduleRepositoryAdapterInterface
*/
private $scheduleRepository;
/**
* @var DateTime
*/
private $dateTime;
/**
* @var Clock
*/
private $clock;

public function __construct(
ScheduleRepositoryAdapterInterface $scheduleRepository,
ProcessManagement $processManagement,
DateTime $dateTime,
Clock $clock
) {
$this->processManagement = $processManagement;
$this->scheduleRepository = $scheduleRepository;
$this->dateTime = $dateTime;
$this->clock = $clock;
}

public function execute()
{
$runningJobs = $this->scheduleRepository->getByStatus(ScheduleInterface::STATUS_RUNNING);
foreach ($runningJobs as $schedule) {
if ($schedule->getKillRequest() && $schedule->getKillRequest() <= \time() && $schedule->getPid()) {
$this->killScheduleProcess($schedule);
}
}
}

private function killScheduleProcess(ScheduleInterface $schedule): void
{
if ($this->processManagement->killPid($schedule->getPid())) {
$messages = [];
if ($schedule->getMessages()) {
$messages[] = $schedule->getMessages();
}
$messages[] = 'Process was killed at ' . $this->dateTime->gmtDate(null, $this->clock->now());
$schedule
->setMessages(\implode("\n", $messages))
->setStatus(ScheduleInterface::STATUS_KILLED);

$this->scheduleRepository->save($schedule);
}
}
}
31 changes: 31 additions & 0 deletions Model/ProcessManagement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace EthanYehuda\CronjobManager\Model;

class ProcessManagement
{
const SIGKILL = 9;

public function isPidAlive(int $pid): bool
{
return \file_exists('/proc/' . $pid);
}

public function killPid($pid): bool
{
if (!$this->isPidAlive($pid)) {
return false;
}
//TODO first try to send SIGINT, wait up to X seconds, then send SIGKILL if process still running
$killed = \posix_kill($pid, self::SIGKILL);
if ($killed && !$this->isPidAlive($pid)) {
\sleep(5);
if ($this->isPidAlive($pid)) {
return false;
}
}
return $killed;

}
}
Loading

0 comments on commit 632ed13

Please sign in to comment.