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:&nbsp;&nbsp;</b>' . $jobViews['jobCodeDescription'] . '</p>' : '';
    $jobRemoteJobId = $jobViews['jobRemoteJobId'] !== NULL ?
      '<p class=”job-id”><b>Remote job ID:&nbsp;&nbsp;</b><span>' . $jobViews['jobRemoteJobId'] . '</span></p>' : '';
    $remoteSubmitted = $jobViews['remoteSubmitted'] !== NULL ?
      '<p><b>Remote job submitted:&nbsp;&nbsp;</b>' . $jobViews['remoteSubmitted'] . '</p>' : '';
    $remoteStarted = $jobViews['remoteStarted'] !== NULL ?
      '<p><b>Remote job started:&nbsp;&nbsp;</b>' . $jobViews['remoteStarted'] . '</p>' : '';
    $remoteEnded = $jobViews['remoteEnded'] !== NULL ?
      '<p><b>Remote job ended:&nbsp;&nbsp;</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:&nbsp;&nbsp;</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) {
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc