41 lines · 1.2 KB
import { QuotaExceededError, releaseUsage, reserveUsage } from "../metering/quota";
import { runnerMeter } from "../metering/ci";

const MINUTE_MS = 60 * 1000;
const HOLD_SLACK_MS = 60 * MINUTE_MS;

export function jobHoldId(jobId: string): string {
  return `ci-hold:${jobId}`;
}

/**
 * Holds the job's full timeout on its own runner-size meter before it may start, so large/xlarge jobs hit
 * their hard caps and the spending limit at their real price, and concurrent dispatches can't overshoot.
 * False when the plan has no room for it.
 */
export async function holdJobMinutes(input: {
  jobId: string;
  ownerId: string;
  runsOn: string[];
  timeoutMinutes: number;
}): Promise<boolean> {
  const timeoutMs = Math.max(1, input.timeoutMinutes) * MINUTE_MS;
  try {
    await reserveUsage({
      id: jobHoldId(input.jobId),
      accountId: input.ownerId,
      meter: runnerMeter(input.runsOn),
      quantity: timeoutMs / 1000,
      ttlMs: timeoutMs + HOLD_SLACK_MS,
    });
    return true;
  } catch (error) {
    if (error instanceof QuotaExceededError) return false;
    throw error;
  }
}

export async function releaseJobHold(jobId: string, ownerId: string): Promise<void> {
  await releaseUsage(jobHoldId(jobId), ownerId);
}