<?php
namespace CU\SASCommonBundle\Controller;
use CU\SASCommonBundle\Annotation\CUSAS;
use CU\SASCommonBundle\Entity\JobManagerOption;
use CU\SASCommonBundle\Entity\JobRunConfig;
use CU\SASCommonBundle\Form\Type\CUSASPlainType;
use CU\SASCommonBundle\Service\JobManager;
use CU\SASCommonBundle\Service\TableRenderer;
use Doctrine\DBAL\Schema\Table;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
/**
* Job Manager
*
* @author "Cornell University, Student Services IT"
* @Route("/common/job-manager")
* @CUSAS(priv="JOBMANAGER")
*/
class JobManagerController extends AbstractController
{
/**
* @Route("/config", name="cusas_jobmanager", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function configAction(JobManager $jobManager)
{
$token = $this->generateCsrfToken('jobmanager');
$form = $this->getJobManagerOptionsForm($jobManager);
$tableSeqs = $this->getSeqsTable($jobManager, $token);
$tableJobs = $this->getJobsTable($jobManager, $token);
return $this->render('@CUSASCommon/JobManager/status.html.twig', ['tableSeqs' => $tableSeqs, 'tableJobs' => $tableJobs, 'form' => $form->createView()]);
}
/**
* @param string $token
* @return \CU\SASCommonBundle\Entity\ResultTable
* @throws \Exception
*/
private function getSeqsTable(JobManager $jobManager, $token)
{
// if we have JOBMANAGER-EDIT we'll show the schedule+token link
$hasJobManagerEdit = $this->getUser()->hasPriv('JOBMANAGER-EDIT');
$table = $this->getTableRenderer()->getTable();
$table
->setColumnCount(6)
->setColumnGridWidthsMd(2, 2, 4, 2, 1, 1)
->setHeaderRow('Sequence', 'Min Rest Interval', 'Job Order', 'Last Started', 'Elapsed', 'Action');
$sequences = $jobManager->getAllSequences();
foreach ($sequences as $sequence) {
$minRest = $this->secondsToTime($sequence->getMinRestSec());
$sequenceTasks = $sequence->getTasks();
$jobDescr = '';
$i = 0;
foreach ($sequenceTasks AS $sequenceTask) {
if ($i) $jobDescr .= '<br>';
$jobDescr .= $sequenceTask->getJob()->getDescrshort();
$i++;
}
$action = '';
if ($hasJobManagerEdit) {
$action = '<a href="' . $this->generateUrl('cusas_jobmanager_seq_enqueue', ['seqId' => $sequence->getId(), 'token' => $token]);
$action .= '">Enqueue</a>';
}
$elapsed = '';
if ($sequence->getLastStartDttm() && $sequence->getLastStopDttm() &&
$sequence->getLastStopDttm() >= $sequence->getLastStartDttm()
) {
$elapsed = $this->secondsToTime($sequence->getLastStopDttm()->getTimestamp() - $sequence->getLastStartDttm()->getTimestamp());
}
$table->addRow($sequence->getDescrshort(),
$minRest,
$jobDescr,
$sequence->getLastStartDttm()?$sequence->getLastStartDttm()->format('Y-m-d h:ia'):'',
$elapsed,
$action);
}
return $table;
}
/**
* @param string $token
* @return \CU\SASCommonBundle\Entity\ResultTable
* @throws \Exception
*/
private function getJobsTable(JobManager $jobManager, $token)
{
// if we have JOBMANAGER-EDIT we'll show the schedule+token link
$hasJobManagerEdit = $this->getUser()->hasPriv('JOBMANAGER-EDIT');
$table = $this->getTableRenderer()->getTable();
$table
->setColumnCount(3)
->setColumnGridWidthsMd(5, 1, 5)
->setHeaderRow('Job', 'Hidden', 'Enqueue');
$jobs = $jobManager->getAllJobs();
foreach ($jobs as $job) {
if (!$job->hasAllowExec()) {
continue;
}
$runConfigCount = 0;
// handle statically linked run configs
$runConfigs = $job->getRunconfigs();
$runConfigCount += count($runConfigs);
$i = 0;
foreach ($runConfigs as $runConfig) {
$jobContent = '';
$jobContent .= '<a href="' . $this->generateUrl('cusas_jobmanager_job_enqueue', ['jobId' => $job->getId(), 'runConfigId' => $runConfig->getId(), 'token' => $token]);
$jobContent .= '">' . $runConfig->getDescrshort() . '</a><br />';
if ($hasJobManagerEdit) {
$table->addRow($i==0?$job->getDescrshort():'', $job->isHidden()?'Yes':'No', $jobContent);
}
$i++;
}
// handle support for dynamically linked run configs
$jobService = $jobManager->getServiceByAlias($job->getServiceAlias());
$dynamicRunConfigs = $jobService->getDynamicRunConfigs($job);
$runConfigCount += count($dynamicRunConfigs);
foreach ($dynamicRunConfigs as $dynamicRunConfig) {
$jobContent = '';
$jobContent .= '<a href="' . $this->generateUrl('cusas_jobmanager_job_enqueue_dynamic', ['jobId' => $job->getId(), 'id' => $dynamicRunConfig->getId(), 'token' => $token]);
$jobContent .= '">' . $dynamicRunConfig->getDescrshort() . '</a><br />';
if ($hasJobManagerEdit) {
$table->addRow($i==0?$job->getDescrshort():'', $job->isHidden()?'Yes':'No', $jobContent);
}
$i++;
}
// if no run configs, include the default (empty run config)
if (!$runConfigCount) {
$jobContent = '<a href="' . $this->generateUrl('cusas_jobmanager_job_enqueue', ['jobId' => $job->getId(), 'token' => $token]);
$jobContent .= '">Default</a>';
$table->addRow($job->getDescrshort(), $job->isHidden()?'Yes':'No', $hasJobManagerEdit?$jobContent:'');
}
}
return $table;
}
/**
* process save for roster specific filter edit
*
* @Route("/save", name="cusas_jobmanager_save", methods={"POST"})
* @CUSAS(priv="JOBMANAGER-EDIT")
*
* @param Request $request
* @return RedirectResponse
*/
public function saveAction(JobManager $jobManager, Request $request)
{
$form = $this->getJobManagerOptionsForm($jobManager);
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData();
$supportedOptions = ['enabled', 'sequence'];
foreach ($supportedOptions AS $supportedOption) {
$jobManagerOption = $jobManager->getJobManagerOption($supportedOption);
if ($jobManagerOption) {
$oldValue = $jobManagerOption->getMetaValue();
$jobManagerOption->setMetaValue($data[$supportedOption]);
if ($oldValue != $data[$supportedOption]) {
$jobManagerOption->setUpdatedDttm(new \DateTime());
}
$this->getEntityManager()->flush($jobManagerOption);
} else {
$jobManagerOption = new JobManagerOption($supportedOption, $data[$supportedOption]);
$jobManagerOption->setUpdatedDttm(new \DateTime());
$this->getEntityManager()->persist($jobManagerOption);
$this->getEntityManager()->flush($jobManagerOption);
}
}
$this->get('session')->getFlashBag()->add('info', 'Job Manager configuration saved.');
$shouldClearQueue = $data['clearqueue'];
if (in_array('Y', $shouldClearQueue)) {
$this->get('session')->getFlashBag()->add('warning', 'Failed any running and cancelled any scheduled jobs.');
$jobManager->forceClearQueue();
}
} else {
// for any errors, add to flashbag, will display after redirect
foreach ($form->getErrors() AS $error) {
$this->get('session')->getFlashBag()->add('danger', $error->getMessage());
}
}
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/**
* @Route("/job/queue-dynamic/{token}/{jobId}/{id}", name="cusas_jobmanager_job_enqueue_dynamic", methods={"GET"})
* @CUSAS(priv="JOBMANAGER-EDIT")
*
* @param string $token
* @param int $jobId
* @param int $id
* @return RedirectResponse
*/
public function queueJobAddDynamicAction(JobManager $jobManager, $token, $jobId, $id)
{
// does token validate?
$tokenValid = $this->isCsrfTokenValid('jobmanager', $token);
if (!$tokenValid) {
$this->get('session')->getFlashBag()->add('danger', 'Invalid token.');
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/* @var $job \CU\SASCommonBundle\Entity\Job */
$job = $this->getEntityManager()->getRepository('CU\SASCommonBundle\Entity\Job')->find($jobId);
$jobService = $jobManager->getServiceByAlias($job->getServiceAlias());
$dynamicRunConfig = $jobService->getDynamicRunConfigById($job, $id);
$runconfig = new JobRunConfig();
$runconfig->setDescrshort($dynamicRunConfig->getDescrshort());
$runconfig->setInitJson($dynamicRunConfig->getInitJson());
$this->getEntityManager()->persist($runconfig);
$this->getEntityManager()->flush($runconfig);
$isScheduled = $jobManager->isJobScheduled($job, $runconfig);
if ($isScheduled) {
$this->get('session')->getFlashBag()->add('danger', 'Job for roster is already scheduled.');
} else if (!$job->hasAllowExec()) {
$this->get('session')->getFlashBag()->add('danger', 'Job is disabled and cannot be scheduled.');
} else {
$userId = $this->getUser()->getUserId();
$jobManager->scheduleJob($job, $userId, $runconfig);
$this->get('session')->getFlashBag()->add('info', 'Job has been enqueued.');
}
// redirect
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/**
* @Route("/job/queue-static/{token}/{jobId}/{runConfigId}", name="cusas_jobmanager_job_enqueue", defaults={"runConfigId" =""}, methods={"GET"})
* @CUSAS(priv="JOBMANAGER-EDIT")
*
* @param string $token
* @param int $jobId
* @param int|string $runConfigId
* @return RedirectResponse
*/
public function queueJobAddAction(JobManager $jobManager, $token, $jobId, $runConfigId = '')
{
// does token validate?
$tokenValid = $this->isCsrfTokenValid('jobmanager', $token);
if (!$tokenValid) {
$this->get('session')->getFlashBag()->add('danger', 'Invalid token.');
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/* @var $job \CU\SASCommonBundle\Entity\Job */
$job = $this->getEntityManager()->getRepository('CU\SASCommonBundle\Entity\Job')->find($jobId);
$runconfig = false;
if ($runConfigId) {
$runconfig = $this->getEntityManager()->getRepository('CU\SASCommonBundle\Entity\JobRunConfig')->find($runConfigId);
}
$isScheduled = $jobManager->isJobScheduled($job, $runconfig);
if ($isScheduled) {
$this->get('session')->getFlashBag()->add('danger', 'Job for roster is already scheduled.');
} else if (!$job->hasAllowExec()) {
$this->get('session')->getFlashBag()->add('danger', 'Job is disabled and cannot be scheduled.');
} else {
$userId = $this->getUser()->getUserId();
$jobManager->scheduleJob($job, $userId, $runconfig);
$this->get('session')->getFlashBag()->add('info', 'Job has been enqueued.');
}
// redirect
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/**
* @Route("/seq/queue/{token}/{seqId}", name="cusas_jobmanager_seq_enqueue", methods={"GET"})
* @CUSAS(priv="JOBMANAGER-EDIT")
*
* @param string $token
* @param int $seqId
* @return RedirectResponse
*/
public function queueSeqAddAction(JobManager $jobManager, $token, $seqId)
{
// does token validate?
$tokenValid = $this->isCsrfTokenValid('jobmanager', $token);
if (!$tokenValid) {
$this->get('session')->getFlashBag()->add('danger', 'Invalid token.');
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
//$userId = $this->getUser()->getUserId();
// get the last run time...
$sequence = $jobManager->getSequence($seqId);
if ($jobManager->canQueueSequence($sequence)) {
$jobsScheduledCount = $jobManager->queueSequence($sequence);
$this->get('session')->getFlashBag()->add('info', 'Sequence has been enqueued.');
} else {
$this->get('session')->getFlashBag()->add('danger', 'Sequence cannot be enqueued.');
}
// redirect
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/**
* @Route("/queue/cancel/{queueId}/{token}", name="cusas_jobmanager_queue_cancel", methods={"GET"})
* @CUSAS(priv="JOBMANAGER-EDIT")
*
* @param int $queueId
* @param string $token
* @return RedirectResponse
*/
public function queueCancelAction(JobManager $jobManager, $queueId, $token)
{
// does token validate?
$tokenValid = $this->isCsrfTokenValid('jobmanager', $token);
if (!$tokenValid) {
$this->get('session')->getFlashBag()->add('danger', 'Invalid token.');
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
$affectedJobs = $jobManager->cancelQueueId($queueId);
if ($affectedJobs) {
$this->get('session')->getFlashBag()->add('info', 'Job has been cancelled.');
} else {
$this->get('session')->getFlashBag()->add('danger', 'Job could not be cancelled.');
}
// redirect
return $this->redirect($this->generateUrl('cusas_jobmanager', []), '302');
}
/**
* @Route("/history", name="cusas_jobmanager_history", methods={"POST"})
*
* @param Request $request
* @return Response
*/
public function historyAjaxAction(Request $request)
{
$showHidden = $request->request->get('showHidden', 0);
$defaultConn = $this->getConnection();
$token = $this->generateCsrfToken('jobmanager');
$hasJobManagerEdit = $this->getUser()->hasPriv('JOBMANAGER-EDIT');
$historyTable = $this->getTableRenderer()->getTable();
$historyTable
->setColumnCount(7)
->setColumnGridWidthsSm(1,3,2,1,2,2,1)
->setHeaderRow('Sequence', 'Job', 'Param', 'Status', 'Started', 'Elapsed', 'Action');
$dttm = new \DateTime();
$dttm->setTimestamp(time() - 10*86400);
$query = $this->getEntityManager()->createQuery('
SELECT q
FROM CUSASCommonBundle:JobManagerQueue q
JOIN q.job j
WHERE q.schedDttm >= :dt
ORDER BY q.schedDttm DESC, q.id DESC
')
->setParameter('dt', $dttm);
$queueEntries = $query->getResult();
/* @var $queueEntry \CU\SASCommonBundle\Entity\JobManagerQueue */
foreach ($queueEntries AS $queueEntry) {
if (!$showHidden && $queueEntry->getJob()->isHidden()) {
continue;
}
$elapsed = '';
if ($queueEntry->getStatus() == 'COMPLETE' && $queueEntry->getStopDttm()
&& $queueEntry->getStopDttm() >= $queueEntry->getStartDttm()) {
$elapsed = $this->secondsToTime($queueEntry->getStopDttm()->getTimestamp() - $queueEntry->getStartDttm()->getTimestamp());
}
$otherAction = '';
if ($queueEntry->getStatus() == 'SCHEDULED' && $hasJobManagerEdit) {
$otherAction = '<a href="' . $this->generateUrl('cusas_jobmanager_queue_cancel', ['queueId' => $queueEntry->getId(), 'token' => $token]);
$otherAction .= '">cancel</a>';
}
$historyTable->addRow(($queueEntry->getSequence()?$queueEntry->getSequence()->getName():''),
$queueEntry->getJob()->getDescrshort(),
$queueEntry->getRunconfig()?$queueEntry->getRunconfig()->getDescrshort():'',
$queueEntry->getStatus(),
$queueEntry->getStartDttm()?$queueEntry->getStartDttm()->format('Y-m-d h:ia'):'',
$elapsed,
$otherAction
);
}
return $this->render('@CUSASCommon/JobManager/historyAjax.html.twig', ['historyTable' => $historyTable]);
}
/**
* @return \Symfony\Component\Form\FormInterface
*/
private function getJobManagerOptionsForm(JobManager $jobManager)
{
$formBuilder = $this->createFormBuilder();
$hasJobManagerEdit = $this->getUser()->hasPriv('JOBMANAGER-EDIT');
$formBuilder
->setMethod('post')
->setAction($this->generateUrl('cusas_jobmanager_save'))
->add('master_node_name',
CUSASPlainType::class,
['data' => $jobManager->getMasterNodeName()?$jobManager->getMasterNodeName():'None',
'label' => 'Master Node',
'attr' => ['tip' => '']])
->add('enabled',
ChoiceType::class,
['choices' => ['Yes' => 'Y', 'No' => 'N'],
'multiple' => false,
'expanded' => false,
'data' => $jobManager->isSchedulerEnabled()?'Y':'N',
'label' => 'Enabled',
'required' => true,
'disabled' => !$hasJobManagerEdit,
'attr' => ['tip' => 'Process queued sequences and jobs.']])
->add('sequence',
ChoiceType::class,
['choices' => ['Yes' => 'Y', 'No' => 'N'],
'multiple' => false,
'expanded' => false,
'data' => $jobManager->areSequencesEnabled()?'Y':'N',
'label' => 'Automatic Sequences',
'required' => true,
'disabled' => !$hasJobManagerEdit,
'attr' => ['tip' => 'Automatically enqueue sequences. (Enabled must be Yes.)']]);
if ($this->getUser()->hasPriv('JOBMANAGER-EDIT')) {
$formBuilder
->add('clearqueue',
ChoiceType::class,
['choices' => ['Fail running sequences, jobs and cancel all scheduled.' => 'Y'],
'multiple' => true,
'expanded' => true,
'data' => [false],
'label' => 'Power Cancel',
'required' => false,
'attr' => ['tip' => 'For use only when JobManager is in a bad state.']])
->add('save', SubmitType::class, ['attr' => ['class' => 'btn-primary']]);
}
return $formBuilder->getForm();
}
protected function secondsToTime($ss)
{
if ($ss == 0) {
return '0 secs';
}
$s = $ss % 60;
$m = (floor(($ss%3600)/60)>0)?floor(($ss%3600)/60):0;
$h = (floor(($ss % 86400) / 3600)>0)?floor(($ss % 86400) / 3600):0;
$d = (floor(($ss % 604800) / 86400)>0)?floor(($ss % 604800) / 86400):0;
$w = (floor($ss / 604800)>0)?floor($ss / 604800):0;
$str = '';
if ($w)
$str .= ' ' . $w . ' wk' . ($w>1?'s':'');
if ($d)
$str .= ' ' . $d . ' day' . ($d>1?'s':'');
if ($h)
$str .= ' ' . $h . ' hr' . ($h>1?'s':'');
if ($m)
$str .= ' ' . $m . ' min' . ($m>1?'s':'');
if ($s)
$str .= ' ' . $s . ' sec' . ($s>1?'s':'');
return trim($str);
}
}