tapis_job-1.4.1-alpha1/src/Form/JobStatusRefreshForm.php
src/Form/JobStatusRefreshForm.php
<?php
namespace Drupal\tapis_job\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\tapis_job\JobConditionCode;
use Drupal\tapis_job\TapisProvider\TapisJobProviderInterface;
use Drupal\tapis_system\DrupalIds;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class JobStatusRefreshForm.
*
* Form for refreshing the status of a Tapis job.
*/
class JobStatusRefreshForm extends FormBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The Tapis job provider.
*
* @var \Drupal\tapis_job\TapisProvider\TapisJobProviderInterface
*/
protected TapisJobProviderInterface $tapisJobProvider;
/**
* HTML renderer interface.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* The HTTP client used for making requests.
*
* @var \GuzzleHttp\ClientInterface
*/
protected ClientInterface $httpClient;
/**
* JobStatusRefreshForm constructor.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The HTML renderer.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\tapis_job\TapisProvider\TapisJobProviderInterface $tapisJobProvider
* The Tapis job provider.
* @param \GuzzleHttp\ClientInterface $httpClient
* The HTTP client.
*/
public function __construct(RendererInterface $renderer,
EntityTypeManagerInterface $entityTypeManager,
TapisJobProviderInterface $tapisJobProvider,
ClientInterface $httpClient) {
$this->renderer = $renderer;
$this->entityTypeManager = $entityTypeManager;
$this->tapisJobProvider = $tapisJobProvider;
$this->httpClient = $httpClient;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('renderer'),
$container->get('entity_type.manager'),
$container->get('tapis_job.tapis_job_provider'),
$container->get('http_client'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'job_status_refresh_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity = NULL) {
// Get current job status.
$jobViews = $this->getJobView($entity);
$jobStatusHtml = $this->jobStatusRender($jobViews);
$form['job_status_info'] = [
'#type' => 'markup',
'#markup' => Markup::create($jobStatusHtml),
'#prefix' => '<div id="job-status-container">',
'#suffix' => '</div>',
];
$form['job_status_info']['#job_button'] = $jobViews['jobProxyButton'];
$form['job_status_info']['#terminate_job_button'] = $jobViews['terminateJobProxyButton'];
// Hidden button to trigger AJAX submission.
$form['refresh_button'] = [
'#type' => 'button',
'#value' => t('Refresh'),
'#attributes' => ['style' => 'display:none;'],
'#ajax' => [
'callback' => '::refreshForm',
'event' => 'click',
'progress' => [
// 'type' => 'throbber',
'message' => t('Refreshing job status...'),
// Disables the throbber.
'type' => 'none',
],
'extra_data' => [
'tapis_job_id' => $entity->id(),
],
],
];
$displasyJobStatus = [
"jobProxyURLFlag" => $jobViews['jobProxyURLFlag'],
"status" => $jobViews['jobStatus'],
"revisedLastMessage" => $jobViews['revisedLastMessage'],
'jobCodeDescription' => $jobViews['jobCodeDescription'],
'jobRemoteJobId' => $jobViews['jobRemoteJobId'],
'remoteSubmitted' => $jobViews['remoteSubmitted'],
'remoteStarted' => $jobViews['remoteStarted'],
'remoteEnded' => $jobViews['remoteEnded'],
'jobUsage' => $jobViews['jobUsage'],
];
// Add the js library.
$form['#attached']['library'][] = 'tapis_job/tapis_job.jobpage';
$form['#attached']['library'][] = 'tapis_job/tapis_job.job_usage';
$form['#attached']['drupalSettings']['tapis_job'] = $displasyJobStatus;
return $form;
}
/**
* Ajax handler for the "Check status" button.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The response for ajax call
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function refreshForm(array $form, FormStateInterface $form_state) {
$response = new AjaxResponse();
// Retrieve the extra data passed from the form element.
$extra_data = $form_state->getTriggeringElement()['#ajax']['extra_data'];
$tapisJobId = $extra_data['tapis_job_id'];
$tapisJob = $this->entityTypeManager->getStorage("tapis_job")->load((int) $tapisJobId);
$jobViews = $this->getJobView($tapisJob);
// Update the Drupal settings.
$response->addCommand(new SettingsCommand([
'tapis_job' => [
"jobProxyURLFlag" => $jobViews['jobProxyURLFlag'],
'status' => $jobViews['jobStatus'],
"revisedLastMessage" => $jobViews['revisedLastMessage'],
'jobCodeDescription' => $jobViews['jobCodeDescription'],
'jobRemoteJobId' => $jobViews['jobRemoteJobId'],
'remoteSubmitted' => $jobViews['remoteSubmitted'],
'remoteStarted' => $jobViews['remoteStarted'],
'remoteEnded' => $jobViews['remoteEnded'],
'jobUsage' => $jobViews['jobUsage'],
],
// Expose the job access and terminate button (if any).
'job_button' => $jobViews['jobProxyButton'],
'terminate_job_button' => $jobViews['terminateJobProxyButton'],
]));
$jobStatusHtml = $this->jobStatusRender($jobViews);
// Update the job status container with new content.
$response->addCommand(new HtmlCommand('#job-status-container',
Markup::create($jobStatusHtml)));
return $response;
}
/**
* Get the current job status.
*/
private function getJobView($entity) {
/** @var \Drupal\tapis_job\Entity\TapisJob $entity */
$jobOwnerId = $entity->getOwnerId();
$tenantId = $entity->getTenantId();
$system = $entity->getSystem();
// Get details from Tapis API for this job.
$tapisJob = $this->tapisJobProvider->getJob($tenantId, $entity->getTapisUUID(), $jobOwnerId);
$jobStatus = $tapisJob['status'];
$jobConditionCode = $tapisJob['condition'];
$jobRemoteJobId = $tapisJob['remoteJobId'];
$remoteSubmitted = $tapisJob['remoteSubmitted'];
$remoteStarted = $tapisJob['remoteStarted'];
$remoteEnded = $tapisJob['remoteEnded'];
if ($remoteSubmitted) {
// Format the date to the storage format.
$remoteSubmitted = $this->convertTimestampToUtcAndFormat($remoteSubmitted);
$entity->setRemoteSubmitted($remoteSubmitted);
}
if ($remoteStarted) {
// Format the date to the storage format.
$remoteStarted = $this->convertTimestampToUtcAndFormat($remoteStarted);
$entity->setRemoteStarted($remoteStarted);
}
if ($remoteEnded) {
// Format the date to the storage format.
$remoteEnded = $this->convertTimestampToUtcAndFormat($remoteEnded);
$entity->setRemoteEnded($remoteEnded);
}
// Whenever we view the job, we update the condition locally.
$jobCodeDescription = NULL;
if ($jobConditionCode !== NULL) {
$jobCodeDescription = JobConditionCode::getDescriptionByCode($jobConditionCode);
if ($jobCodeDescription === NULL) {
$jobCodeDescription = $jobConditionCode;
}
}
$entity->setCondition($jobCodeDescription);
// Whenever we view the job, we update the status locally.
$currJobStatus = $entity->getStatus();
if ($currJobStatus === 'Terminating') {
$terminateJobStatuses = ["FINISHED", "CANCELLED", "FAILED"];
if (!in_array($jobStatus, $terminateJobStatuses)) {
$jobStatus = "TERMINATING";
}
}
$lowercaseJobStatus = strtolower($jobStatus);
$capitalizedJobStatus = str_replace('_', ' ', ucfirst($lowercaseJobStatus));
$entity->setStatus($capitalizedJobStatus);
$jobUsage = $this->getJobUsage($tapisJob, $system);
if (!empty($jobUsage)) {
$entity->setJobUsage(json_encode($jobUsage));
}
$lastMessage = $tapisJob['lastMessage'];
$revisedLastMessage = str_replace('_', ' ', str_replace($jobStatus, $lowercaseJobStatus, $lastMessage));
$anAccessLinkForThisJob = $this->tapisJobProvider->getAJobAccessLinkForJob($entity);
$htmlJobProxy = '';
$jobProxyButton = '';
$terminateJobProxyButton = '';
$jobProxyURLFlag = false;
if ($anAccessLinkForThisJob && $jobStatus === "RUNNING") {
// Check proxy health from backend.
$jobProxyURLString = $anAccessLinkForThisJob->getProxyURL();
$jobProxyURLFlag = true;
try {
$res = $this->httpClient->request('GET', $jobProxyURLString, [
'timeout' => 3,
'http_errors' => FALSE,
]);
$status = $res->getStatusCode();
$proxyIsAlive = ($status >= 200 && $status < 400);
} catch (\Exception $e) {
$proxyIsAlive = FALSE;
}
if ($proxyIsAlive) {
$jobProxyURLFlag = false;
$build = [];
$jobProxyURL = Url::fromUri($jobProxyURLString);
$link = new Link('Open app session', $jobProxyURL);
$build['tapis_job_proxyURL'] = $link->toRenderable();
$build['tapis_job_proxyURL']['#attributes'] = [
'class' => [
'button',
'button--primary',
],
'target' => '_blank',
'data-role' => 'open-app-session',
];
$jobCancelURL = Url::fromRoute('entity.tapis_job.cancel_job', ['tapis_job' => $entity->id()]);
$link = new Link('Terminate Job', $jobCancelURL);
$build['tapis_job_terminate_proxyURL'] = $link->toRenderable();
$build['tapis_job_terminate_proxyURL']['#attributes'] = [
'class' => [
'button',
'job--terminate--open--session--action',
],
'onclick' => 'return confirm("Would you like to terminate this job?");',
];
$build[] = ['#type' => 'markup', '#markup' => '<br/>'];
// Render the build array into HTML.
$htmlJobProxy = $this->renderer->renderRoot($build);
$jobProxyButton = $build['tapis_job_proxyURL'];
$terminateJobProxyButton = $build['tapis_job_terminate_proxyURL'];
}
}
$entity->save();
// Clear the cache so that subsequent loads fetch the updated entity.
$this->entityTypeManager->getStorage('tapis_job')->resetCache([$entity->id()]);
return [
'jobProxyURLFlag' => $jobProxyURLFlag,
'jobStatus' => $jobStatus,
'capitalizedJobStatus' => $capitalizedJobStatus,
'revisedLastMessage' => $revisedLastMessage,
'jobCodeDescription' => $jobCodeDescription,
'jobRemoteJobId' => $jobRemoteJobId,
'remoteSubmitted' => $entity->getRemoteSubmitted(),
'remoteStarted' => $entity->getRemoteStarted(),
'remoteEnded' => $entity->getRemoteEnded(),
'jobProxyButton' => $jobProxyButton,
'terminateJobProxyButton' => $terminateJobProxyButton,
'jobUsage' => $jobUsage,
];
}
/**
* Converts a timestamp to UTC.
*
* Formats it according to a predefined storage format.
*
* @param string $timestampInRfc3339
* The original timestamp in RFC3339
* format.
*
* @return string
* The formatted datetime string according to
* DATETIME_STORAGE_FORMAT.
*
* @throws \Exception
*/
private function convertTimestampToUtcAndFormat(string $timestampInRfc3339): string {
// Remove the fractional seconds part
// and append 'Z' to indicate Zulu time (UTC).
$cleanedTimestamp = substr($timestampInRfc3339, 0, strpos($timestampInRfc3339, '.')) . 'Z';
// Retrieve the predefined storage format constant.
$dateTimeStorageFormat = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
// Attempt to create a DrupalDateTime object from
// the cleaned timestamp, specifying UTC timezone.
$utcDateTimeObject = DrupalDateTime::createFromFormat('Y-m-d\TH:i:s\Z', $cleanedTimestamp, new \DateTimeZone('UTC'));
// Check if the DrupalDateTime object was successfully created.
if (!$utcDateTimeObject instanceof DrupalDateTime) {
throw new \Exception("Failed to instantiate DrupalDateTime object.");
}
// Return the datetime formatted according to the predefined storage format.
return $utcDateTimeObject->format($dateTimeStorageFormat);
}
/**
* Render the job status information dynamically.
*/
private function jobStatusRender($jobViews) {
$htmlJobCondition = $jobViews['jobCodeDescription'] !== NULL ?
'<p><b>Condition: </b>' . $jobViews['jobCodeDescription'] . '</p>' : '';
$jobRemoteJobId = $jobViews['jobRemoteJobId'] !== NULL ?
'<p class=”job-id”><b>Remote job ID: </b><span>' . $jobViews['jobRemoteJobId'] . '</span></p>' : '';
$remoteSubmitted = $jobViews['remoteSubmitted'] !== NULL ?
'<p><b>Remote job submitted: </b>' . $jobViews['remoteSubmitted'] . '</p>' : '';
$remoteStarted = $jobViews['remoteStarted'] !== NULL ?
'<p><b>Remote job started: </b>' . $jobViews['remoteStarted'] . '</p>' : '';
$remoteEnded = $jobViews['remoteEnded'] !== NULL ?
'<p><b>Remote job ended: </b>' . $jobViews['remoteEnded'] . '</p>' : '';
$jobUsage = '';
if (!empty($jobViews['jobUsage'])) {
$data = $jobViews['jobUsage'];
$resourceDetails = [
'Processing unit' => $data['resource_type'] ?? '',
'GPU count' => $data['resource']['numGPU'] ?? '',
'GPU factor' => $data['resource']['gpuFactor'] ?? '',
'Node count' => $data['resource']['nodeCount'] ?? '',
'Cores per node' => $data['resource']['coresPerNode'] ?? '',
'Memory' => isset($data['resource']['memoryMB']) ?
$data['resource']['memoryMB'] . ' MB' : '',
];
$jobResourceDetail = array_filter($resourceDetails) ?
'<div class="usage-section"><h4>Resource details</h4>' .
implode('', array_map(fn($label, $value) => $value !== '' ?
"<p><strong>$label:</strong> $value</p>" :
'', array_keys($resourceDetails), $resourceDetails)) . '</div>' : '';
$usageDetails = [
'Execution time' => isset($data['usage']['execution_time']['elapsed_time']) ?
$data['usage']['execution_time']['elapsed_time'] .
' ' . $data['usage']['execution_time']['unit'] : '',
'Total usage time' => isset($data['usage']['total_usage_time']['elapsed_time']) ?
$data['usage']['total_usage_time']['elapsed_time'] .
' ' . $data['usage']['total_usage_time']['unit'] : '',
];
$usageInfo = array_filter($usageDetails) ?
'<div class="usage-section"><h4>Usage time</h4>' .
implode('', array_map(fn($label, $value) => $value !== '' ?
"<p><strong>$label:</strong> $value</p>" :
'', array_keys($usageDetails), $usageDetails)) . '</div>' : '';
if ($jobResourceDetail || $usageInfo) {
$jobUsage = '<h3>Job usage details</h3><div class="usage-sections-container">' .
$jobResourceDetail . $usageInfo . '</div>';
}
}
return '<div class="osp-tapisjob-status"><p><b>Last message: </b> ' .
$jobViews['revisedLastMessage'] . '</p>' . $htmlJobCondition . $jobRemoteJobId .
$remoteSubmitted . $remoteStarted . $remoteEnded . $jobUsage . '</div>';
}
/**
* Get the job usage.
*/
private function getJobUsage($tapisJob, $system) {
$jobUsage = [];
$resourceType = "CPU";
$created = $tapisJob['created'];
$ended = $tapisJob['ended'];
$remoteSubmitted = $tapisJob['remoteSubmitted'];
$remoteStarted = $tapisJob['remoteStarted'];
$remoteEnded = $tapisJob['remoteEnded'];
$execSystemLogicalQueue = $tapisJob['execSystemLogicalQueue'];
$parameterSet = $tapisJob['parameterSet'];
$parameterSetJson = json_decode($parameterSet, TRUE);
$nodeCount = $tapisJob['nodeCount'];
$coresPerNode = $tapisJob['coresPerNode'];
$memoryMB = $tapisJob['memoryMB'];
$resourceAmount = intval($nodeCount) * intval($coresPerNode);
$jobUsage['resource'] = [
'nodeCount' => $nodeCount,
'coresPerNode' => $coresPerNode,
'memoryMB' => $memoryMB,
];
// Check if the string contains "gpu" in any case.
if (isset($execSystemLogicalQueue) &&
stripos($execSystemLogicalQueue, 'gpu') !== FALSE) {
$resourceType = "GPU";
$numGPU = 0;
$gpuFactor = 0;
$schedulerOptions = $parameterSetJson['schedulerOptions'];
foreach ($schedulerOptions as $schedulerOption) {
if (stripos($schedulerOption["arg"], 'gpu') !== FALSE) {
// Adjusted regular expression pattern
// to capture the entire pattern.
$pattern = '/--gpus\s*=?\s*(\d+)/';
// Attempt to match the pattern.
if (preg_match($pattern, trim($schedulerOption["arg"]), $matches)) {
// Extract the number from the second capturing group.
// Note: $matches[0] would give the full match.
$numGPU = $matches[1];
$gpuFactor = 1;
if (isset($system->get(DrupalIds::SYSTEM_NOTES)->getValue()[0]['value']) &&
!empty($system->get(DrupalIds::SYSTEM_NOTES)->getValue()[0]['value'])) {
$systemNotes = trim($system->get(DrupalIds::SYSTEM_NOTES)
->getValue()[0]['value']);
$jsonSystemNotes = json_decode($systemNotes);
$gpuFactor = intval($jsonSystemNotes->gpu_factor);
}
$jobUsage['factor'] = $gpuFactor;
$resourceAmount = $gpuFactor * intval($nodeCount) * intval($numGPU);
}
break;
}
}
$jobUsage['resource']['numGPU'] = $numGPU;
$jobUsage['resource']['gpuFactor'] = $gpuFactor;
}
$jobUsage['resource_type'] = $resourceType;
if ($remoteStarted !== NULL && $remoteEnded !== NULL) {
$jobUsage['usage'] = $this->calculateExecutionTime($remoteStarted, $remoteEnded, $resourceAmount);
}
$jobUsage['timeline'] = [
'created' => $created,
'ended' => $ended,
'remoteSubmitted' => $remoteSubmitted,
'remoteStarted' => $remoteStarted,
'remoteEnded' => $remoteEnded,
];
return $jobUsage;
}
/**
* Calculate the execution time between two date-times in the specified unit.
*
* @param string $start_time
* The start date-time string in ISO 8601 format.
* @param string $end_time
* The end date-time string in ISO 8601 format.
* @param int $totalNumCores
* The end date-time string in ISO 8601 format.
*
* @return array
* The execution time in the specified unit.
*
* @throws \Exception
*/
public function calculateExecutionTime(string $start_time, string $end_time, int $totalNumCores): array {
// Define the start and end date-times.
$start = new \DateTime($start_time);
$end = new \DateTime($end_time);
// Calculate the difference.
$interval = $start->diff($end);
// Calculate the total execution time in seconds.
$execution_time_seconds = ($interval->days * 86400) + ($interval->h * 3600) + ($interval->i * 60) + $interval->s + $interval->f;
// Get execution time and unit.
[$execution_time, $execution_time_unit] = $this->convertTime($execution_time_seconds);
// Calculate total usage time in seconds.
$total_usage_time_seconds = $totalNumCores * $execution_time_seconds;
// Get total usage time and unit.
[$total_usage_time, $total_usage_time_unit] = $this->convertTime($total_usage_time_seconds);
return [
"execution_time" => ["elapsed_time" => $execution_time, "unit" => $execution_time_unit],
"total_usage_time" => ["elapsed_time" => $total_usage_time, "unit" => $total_usage_time_unit],
];
}
/**
* Function to convert time in seconds to appropriate unit.
*/
private function convertTime($time_in_seconds): array {
if ($time_in_seconds >= 3600) {
return [round($time_in_seconds / 3600, 2), 'hour'];
}
elseif ($time_in_seconds >= 60) {
return [round($time_in_seconds / 60, 2), 'min'];
}
else {
return [round($time_in_seconds, 2), 'sec'];
}
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}
