github.com/hernad/nomad@v1.6.112/ui/app/utils/properties/job-client-status.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import { computed } from '@ember/object';
     7  import matchGlob from '../match-glob';
     8  
     9  const STATUS = [
    10    'queued',
    11    'notScheduled',
    12    'starting',
    13    'running',
    14    'complete',
    15    'degraded',
    16    'failed',
    17    'lost',
    18    'unknown',
    19  ];
    20  
    21  // An Ember.Computed property that computes the aggregated status of a job in a
    22  // client based on the desiredStatus of each allocation placed in the client.
    23  //
    24  // ex. clientStaus: jobClientStatus('nodes', 'job'),
    25  export default function jobClientStatus(nodesKey, jobKey) {
    26    return computed(
    27      `${nodesKey}.[]`,
    28      `${jobKey}.{datacenters,status,allocations.@each.clientStatus,taskGroups}`,
    29      function () {
    30        const job = this.get(jobKey);
    31        const nodes = this.get(nodesKey);
    32  
    33        // Filter nodes by the datacenters defined in the job.
    34        const filteredNodes = nodes.filter((n) => {
    35          return job.datacenters.find((dc) => {
    36            return !!matchGlob(dc, n.datacenter);
    37          });
    38        });
    39  
    40        if (job.status === 'pending') {
    41          return allQueued(filteredNodes);
    42        }
    43  
    44        // Group the job allocations by the ID of the client that is running them.
    45        const allocsByNodeID = {};
    46        job.allocations.forEach((a) => {
    47          const nodeId = a.belongsTo('node').id();
    48          if (!allocsByNodeID[nodeId]) {
    49            allocsByNodeID[nodeId] = [];
    50          }
    51          allocsByNodeID[nodeId].push(a);
    52        });
    53  
    54        const result = {
    55          byNode: {},
    56          byStatus: {},
    57          totalNodes: filteredNodes.length,
    58        };
    59        filteredNodes.forEach((n) => {
    60          const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length);
    61          result.byNode[n.id] = status;
    62  
    63          if (!result.byStatus[status]) {
    64            result.byStatus[status] = [];
    65          }
    66          result.byStatus[status].push(n.id);
    67        });
    68        result.byStatus = canonicalizeStatus(result.byStatus);
    69        return result;
    70      }
    71    );
    72  }
    73  
    74  function allQueued(nodes) {
    75    const nodeIDs = nodes.map((n) => n.id);
    76    return {
    77      byNode: Object.fromEntries(nodeIDs.map((id) => [id, 'queued'])),
    78      byStatus: canonicalizeStatus({ queued: nodeIDs }),
    79      totalNodes: nodes.length,
    80    };
    81  }
    82  
    83  // canonicalizeStatus makes sure all possible statuses are present in the final
    84  // returned object. Statuses missing from the input will be assigned an emtpy
    85  // array.
    86  function canonicalizeStatus(status) {
    87    for (let i = 0; i < STATUS.length; i++) {
    88      const s = STATUS[i];
    89      if (!status[s]) {
    90        status[s] = [];
    91      }
    92    }
    93    return status;
    94  }
    95  
    96  // jobStatus computes the aggregated status of a job in a client.
    97  //
    98  // `allocs` are the list of allocations for a job that are placed in a specific
    99  // client.
   100  // `expected` is the number of allocations the client should have.
   101  function jobStatus(allocs, expected) {
   102    // The `pending` status has already been checked, so if at this point the
   103    // client doesn't have any allocations we assume that it was not considered
   104    // for scheduling for some reason.
   105    if (!allocs) {
   106      return 'notScheduled';
   107    }
   108  
   109    // If there are some allocations, but not how many we expected, the job is
   110    // considered `degraded` since it did fully run in this client.
   111    if (allocs.length < expected) {
   112      return 'degraded';
   113    }
   114  
   115    // Count how many allocations are in each `clientStatus` value.
   116    const summary = allocs
   117      .filter((a) => !a.isOld)
   118      .reduce((acc, a) => {
   119        const status = a.clientStatus;
   120        if (!acc[status]) {
   121          acc[status] = 0;
   122        }
   123        acc[status]++;
   124        return acc;
   125      }, {});
   126  
   127    // Theses statuses are considered terminal, i.e., an allocation will never
   128    // move from this status to another.
   129    // If all of the expected allocations are in one of these statuses, the job
   130    // as a whole is considered to be in the same status.
   131    const terminalStatuses = ['failed', 'lost', 'complete'];
   132    for (let i = 0; i < terminalStatuses.length; i++) {
   133      const s = terminalStatuses[i];
   134      if (summary[s] === expected) {
   135        return s;
   136      }
   137    }
   138  
   139    // It only takes one allocation to be in one of these statuses for the
   140    // entire job to be considered in a given status.
   141    if (summary['failed'] > 0 || summary['lost'] > 0) {
   142      return 'degraded';
   143    }
   144  
   145    if (summary['running'] > 0) {
   146      return 'running';
   147    }
   148  
   149    if (summary['unknown'] > 0) {
   150      return 'unknown';
   151    }
   152  
   153    return 'starting';
   154  }