import { ALLOCATION_STATUSES, CONSUMING_ALLOCATION_STATUSES, RESERVING_ALLOCATION_STATUSES, } from '@pig/core'; import type { AllocationStatus, GuaranteeType, GpuSocket, InterconnectType, SecurityTier, } from '@pig/core'; import type { Allocation, CapacityCommitment, Database, DemandDeal, NewCapacityCommitment, } from '@pig/db'; import { allocations, capacityCommitments, demandDeals } from '@pig/db'; import { eq } from 'drizzle-orm'; import type { Principal } from '../lib/auth'; import { MutationError } from '../lib/mutation'; import { quantityAt, type CommitmentShape } from './capacity'; export type CapacityWriteTransaction = Parameters[0]>[0]; export interface CommitmentCapacity { id: string; gpuCount: number; startsAt: Date; endsAt: Date; totalGpuHours: number; shape: CommitmentShape | null; oversubscriptionPct: number; terminatedAt: Date | null; } export interface ReservationCapacity { id?: string; gpuHours: number; startsAt: Date; endsAt: Date; status: AllocationStatus; holdExpiresAt: Date | null; } export interface CapacityViolation { code: 'outside_commitment_window' | 'total_capacity_exceeded' | 'shape_capacity_exceeded'; message: string; at?: Date; reserved?: number; available?: number; } const EPSILON = 1e-7; function isLiveReservation(reservation: ReservationCapacity, now: Date): boolean { if (!(RESERVING_ALLOCATION_STATUSES as readonly string[]).includes(reservation.status)) { return false; } return !( reservation.status === 'planned' && reservation.holdExpiresAt !== null && reservation.holdExpiresAt <= now ); } function reservationRate(reservation: ReservationCapacity): number { const durationHours = (reservation.endsAt.getTime() - reservation.startsAt.getTime()) / 3_600_000; return durationHours > 0 ? reservation.gpuHours / durationHours : Number.POSITIVE_INFINITY; } /** * Checks both the contracted hour budget and every instantaneous shape interval. * The row lock used by the caller turns this pure check into a concurrency-safe * invariant rather than an optimistic preflight that two requests can both pass. */ export function findCapacityViolation( commitment: CommitmentCapacity, existing: readonly ReservationCapacity[], candidate: ReservationCapacity | null, now: Date, ): CapacityViolation | null { const reservations = [...existing, ...(candidate ? [candidate] : [])].filter((reservation) => isLiveReservation(reservation, now), ); for (const reservation of reservations) { if ( reservation.startsAt < commitment.startsAt || reservation.endsAt > commitment.endsAt || reservation.endsAt <= reservation.startsAt ) { return { code: 'outside_commitment_window', message: 'The allocation window must sit inside the commitment window.', }; } } const multiplier = 1 + commitment.oversubscriptionPct / 100; const allowedGpuHours = commitment.totalGpuHours * multiplier; const reservedGpuHours = reservations.reduce((sum, reservation) => sum + reservation.gpuHours, 0); if (reservedGpuHours - allowedGpuHours > EPSILON) { return { code: 'total_capacity_exceeded', message: 'The allocation would exceed the commitment GPU-hour budget.', reserved: reservedGpuHours, available: allowedGpuHours, }; } const boundaries = new Set([ commitment.startsAt.getTime(), commitment.endsAt.getTime(), ...(commitment.shape?.intervals.map((boundary) => Date.parse(boundary)) ?? []), ]); for (const reservation of reservations) { boundaries.add(reservation.startsAt.getTime()); boundaries.add(reservation.endsAt.getTime()); } const ordered = [...boundaries] .filter( (boundary) => Number.isFinite(boundary) && boundary >= commitment.startsAt.getTime() && boundary <= commitment.endsAt.getTime(), ) .sort((a, b) => a - b); for (let index = 0; index < ordered.length - 1; index++) { const intervalStart = ordered[index]!; const intervalEnd = ordered[index + 1]!; if (intervalEnd <= intervalStart) continue; const overlapping = reservations.filter( (reservation) => reservation.startsAt.getTime() < intervalEnd && reservation.endsAt.getTime() > intervalStart, ); if (overlapping.length === 0) continue; const at = new Date(intervalStart + (intervalEnd - intervalStart) / 2); const reserved = overlapping.reduce( (sum, reservation) => sum + reservationRate(reservation), 0, ); const available = quantityAt(commitment.shape, commitment.gpuCount, at) * multiplier; if (reserved - available > EPSILON) { return { code: 'shape_capacity_exceeded', message: 'The allocation would exceed available GPUs during part of its window.', at, reserved, available, }; } } return null; } export function commitmentCapacityError(commitment: CommitmentCapacity): string | null { const durationHours = (commitment.endsAt.getTime() - commitment.startsAt.getTime()) / 3_600_000; if (durationHours <= 0) return 'Commitment end must be after its start.'; if (commitment.oversubscriptionPct < 0) return 'Oversubscription cannot be negative.'; const shape = commitment.shape; if (!shape) { if (commitment.totalGpuHours - durationHours * commitment.gpuCount > EPSILON) { return 'Total GPU-hours cannot exceed the flat commitment envelope.'; } return null; } if (shape.intervals.length < 2 || shape.quantities.length !== shape.intervals.length - 1) { return 'Shape quantities must have exactly one entry per interval.'; } const boundaries = shape.intervals.map((boundary) => Date.parse(boundary)); if (boundaries.some((boundary) => !Number.isFinite(boundary))) { return 'Shape intervals must be valid ISO-8601 timestamps.'; } if ( boundaries[0] !== commitment.startsAt.getTime() || boundaries[boundaries.length - 1] !== commitment.endsAt.getTime() ) { return 'Shape intervals must cover the full commitment window.'; } for (let index = 0; index < boundaries.length - 1; index++) { if (boundaries[index + 1]! <= boundaries[index]!) { return 'Shape intervals must be strictly ascending.'; } } if ( shape.quantities.some( (quantity) => !Number.isInteger(quantity) || quantity < 0 || quantity > commitment.gpuCount, ) ) { return 'Shape quantities must be whole GPUs within the commitment envelope.'; } const shapedGpuHours = shape.quantities.reduce( (sum, quantity, index) => sum + ((boundaries[index + 1]! - boundaries[index]!) / 3_600_000) * quantity, 0, ); if (commitment.totalGpuHours - shapedGpuHours > EPSILON) { return 'Total GPU-hours cannot exceed the capacity described by the shape.'; } return null; } export interface CommitmentWriteInput { accountId: string; siteId?: string | null; supplyDealId?: string | null; name: string; gpuType: string; socket?: GpuSocket | null; gpuCount: number; interconnectType?: InterconnectType; securityTier?: SecurityTier; startsAt: string; endsAt: string; totalGpuHours: number; costPerGpuHourCents: number; currency?: string; shape?: CommitmentShape | null; colocateWith?: string[]; isContiguous?: boolean; minimumSpendCents?: number | null; isAutoRenew?: boolean; noticeDays?: number | null; takeOrPayFloorPct?: number | null; prepaidPct?: number | null; prepaidAmountCents?: number | null; usefulLifeYears?: number | null; salvageValuePct?: number | null; depreciationStartAt?: string | null; costOfCapitalBps?: number | null; financingInstrument?: string | null; oversubscriptionPct?: number; notes?: string | null; terminatedAt?: string | null; } export interface AllocationWriteInput { capacityCommitmentId: string; demandDealId: string; gpuHours: number; pricePerGpuHourCents: number; currency?: string; startsAt: string; endsAt: string; status: (typeof CONSUMING_ALLOCATION_STATUSES)[number]; guaranteeType?: GuaranteeType; priority?: number; complianceDecisionId?: string | null; notes?: string | null; } export interface HoldWriteInput extends Omit { pricePerGpuHourCents?: number; holdExpiresAt: string; holdOpportunityCostCents?: number | null; } export interface ReleaseWriteInput { reason?: string; } export interface CommitmentWriteResult { commitment: CapacityCommitment; } export interface AllocationWriteResult { allocation: Allocation; commitment: Pick; deal: Pick; } export interface ReleaseWriteResult { allocation: Allocation; accountId?: string; previousStatus: AllocationStatus; } function toCapacity(commitment: CapacityCommitment): CommitmentCapacity { return { id: commitment.id, gpuCount: commitment.gpuCount, startsAt: commitment.startsAt, endsAt: commitment.endsAt, totalGpuHours: Number(commitment.totalGpuHours), shape: commitment.shape, oversubscriptionPct: Number(commitment.oversubscriptionPct), terminatedAt: commitment.terminatedAt, }; } function toReservation(allocation: Allocation): ReservationCapacity { return { id: allocation.id, gpuHours: Number(allocation.gpuHours), startsAt: allocation.startsAt, endsAt: allocation.endsAt, status: allocation.status, holdExpiresAt: allocation.holdExpiresAt, }; } function assertCommitmentValid(commitment: CommitmentCapacity): void { const error = commitmentCapacityError(commitment); if (error) throw new MutationError('invalid_commitment', error, 400); } function assertCapacityAvailable( commitment: CommitmentCapacity, existing: readonly Allocation[], candidate: ReservationCapacity | null, now: Date, ): void { const violation = findCapacityViolation( commitment, existing.map(toReservation), candidate, now, ); if (!violation) return; throw new MutationError(violation.code, violation.message, 409); } export class CapacityWriteService { constructor(private readonly tx: CapacityWriteTransaction) {} private async lockCommitment(id: string): Promise { const [commitment] = await this.tx .select() .from(capacityCommitments) .where(eq(capacityCommitments.id, id)) .limit(1) .for('update'); if (!commitment) throw MutationError.notFound('Capacity commitment'); return commitment; } private async reservations(commitmentId: string): Promise { return this.tx .select() .from(allocations) .where(eq(allocations.capacityCommitmentId, commitmentId)); } private async demandDeal(id: string): Promise> { const [deal] = await this.tx .select({ id: demandDeals.id, accountId: demandDeals.accountId }) .from(demandDeals) .where(eq(demandDeals.id, id)) .limit(1); if (!deal) throw MutationError.notFound('Demand deal'); return deal; } async createCommitment(input: CommitmentWriteInput, now: Date): Promise { const capacity: CommitmentCapacity = { id: 'new', gpuCount: input.gpuCount, startsAt: new Date(input.startsAt), endsAt: new Date(input.endsAt), totalGpuHours: input.totalGpuHours, shape: input.shape ?? null, oversubscriptionPct: input.oversubscriptionPct ?? 0, terminatedAt: null, }; assertCommitmentValid(capacity); const [commitment] = await this.tx .insert(capacityCommitments) .values({ accountId: input.accountId, siteId: input.siteId, supplyDealId: input.supplyDealId, name: input.name, gpuType: input.gpuType, socket: input.socket, gpuCount: input.gpuCount, interconnectType: input.interconnectType, securityTier: input.securityTier, startsAt: capacity.startsAt, endsAt: capacity.endsAt, totalGpuHours: String(input.totalGpuHours), costPerGpuHourCents: input.costPerGpuHourCents, currency: input.currency, shape: input.shape, colocateWith: input.colocateWith, isContiguous: input.isContiguous, minimumSpendCents: input.minimumSpendCents, isAutoRenew: input.isAutoRenew, noticeDays: input.noticeDays, takeOrPayFloorPct: input.takeOrPayFloorPct === undefined || input.takeOrPayFloorPct === null ? input.takeOrPayFloorPct : String(input.takeOrPayFloorPct), prepaidPct: input.prepaidPct === undefined || input.prepaidPct === null ? input.prepaidPct : String(input.prepaidPct), prepaidAmountCents: input.prepaidAmountCents, usefulLifeYears: input.usefulLifeYears === undefined || input.usefulLifeYears === null ? input.usefulLifeYears : String(input.usefulLifeYears), salvageValuePct: input.salvageValuePct === undefined || input.salvageValuePct === null ? input.salvageValuePct : String(input.salvageValuePct), depreciationStartAt: input.depreciationStartAt === undefined ? undefined : input.depreciationStartAt === null ? null : new Date(input.depreciationStartAt), costOfCapitalBps: input.costOfCapitalBps, financingInstrument: input.financingInstrument, oversubscriptionPct: String(input.oversubscriptionPct ?? 0), notes: input.notes, createdAt: now, updatedAt: now, }) .returning(); if (!commitment) throw new MutationError('write_failed', 'Commitment was not created.', 409); return { commitment }; } async updateCommitment( id: string, input: Partial, now: Date, ): Promise { const current = await this.lockCommitment(id); const existing = await this.reservations(id); const prospective: CommitmentCapacity = { id, gpuCount: input.gpuCount ?? current.gpuCount, startsAt: input.startsAt ? new Date(input.startsAt) : current.startsAt, endsAt: input.endsAt ? new Date(input.endsAt) : current.endsAt, totalGpuHours: input.totalGpuHours ?? Number(current.totalGpuHours), shape: input.shape === undefined ? current.shape : input.shape, oversubscriptionPct: input.oversubscriptionPct ?? Number(current.oversubscriptionPct), terminatedAt: input.terminatedAt === undefined ? current.terminatedAt : input.terminatedAt ? new Date(input.terminatedAt) : null, }; assertCommitmentValid(prospective); assertCapacityAvailable(prospective, existing, null, now); if ( prospective.terminatedAt && existing.some((allocation) => isLiveReservation(toReservation(allocation), now)) ) { throw new MutationError( 'commitment_in_use', 'Release live allocations before terminating the commitment.', 409, ); } const changes: Partial = { updatedAt: now }; const assign = ( key: Key, value: NewCapacityCommitment[keyof NewCapacityCommitment], ) => { if (input[key] !== undefined) { (changes as Record)[key] = value; } }; assign('accountId', input.accountId); assign('siteId', input.siteId); assign('supplyDealId', input.supplyDealId); assign('name', input.name); assign('gpuType', input.gpuType); assign('socket', input.socket); assign('gpuCount', input.gpuCount); assign('interconnectType', input.interconnectType); assign('securityTier', input.securityTier); assign('startsAt', prospective.startsAt); assign('endsAt', prospective.endsAt); assign('totalGpuHours', String(prospective.totalGpuHours)); assign('costPerGpuHourCents', input.costPerGpuHourCents); assign('currency', input.currency); assign('shape', input.shape); assign('colocateWith', input.colocateWith); assign('isContiguous', input.isContiguous); assign('minimumSpendCents', input.minimumSpendCents); assign('isAutoRenew', input.isAutoRenew); assign('noticeDays', input.noticeDays); assign( 'takeOrPayFloorPct', input.takeOrPayFloorPct === null ? null : String(input.takeOrPayFloorPct), ); assign('prepaidPct', input.prepaidPct === null ? null : String(input.prepaidPct)); assign('prepaidAmountCents', input.prepaidAmountCents); assign('usefulLifeYears', input.usefulLifeYears === null ? null : String(input.usefulLifeYears)); assign('salvageValuePct', input.salvageValuePct === null ? null : String(input.salvageValuePct)); assign( 'depreciationStartAt', input.depreciationStartAt ? new Date(input.depreciationStartAt) : null, ); assign('costOfCapitalBps', input.costOfCapitalBps); assign('financingInstrument', input.financingInstrument); assign('oversubscriptionPct', String(prospective.oversubscriptionPct)); assign('notes', input.notes); assign('terminatedAt', prospective.terminatedAt); const [commitment] = await this.tx .update(capacityCommitments) .set(changes) .where(eq(capacityCommitments.id, id)) .returning(); if (!commitment) throw MutationError.notFound('Capacity commitment'); return { commitment }; } async createAllocation( input: AllocationWriteInput, principal: Principal, now: Date, ): Promise { const commitment = await this.lockCommitment(input.capacityCommitmentId); if (commitment.terminatedAt) { throw new MutationError('commitment_terminated', 'The commitment is terminated.', 409); } const deal = await this.demandDeal(input.demandDealId); const existing = await this.reservations(commitment.id); const candidate: ReservationCapacity = { gpuHours: input.gpuHours, startsAt: new Date(input.startsAt), endsAt: new Date(input.endsAt), status: input.status, holdExpiresAt: null, }; assertCapacityAvailable(toCapacity(commitment), existing, candidate, now); const [allocation] = await this.tx .insert(allocations) .values({ capacityCommitmentId: commitment.id, demandDealId: deal.id, gpuHours: String(input.gpuHours), pricePerGpuHourCents: input.pricePerGpuHourCents, currency: input.currency, startsAt: candidate.startsAt, endsAt: candidate.endsAt, status: input.status, guaranteeType: input.guaranteeType, priority: input.priority, complianceDecisionId: input.complianceDecisionId, createdByUserId: principal.userId, notes: input.notes, createdAt: now, updatedAt: now, }) .returning(); if (!allocation) throw new MutationError('write_failed', 'Allocation was not created.', 409); return { allocation, commitment, deal }; } async createHold( input: HoldWriteInput, principal: Principal, now: Date, ): Promise { const holdExpiresAt = new Date(input.holdExpiresAt); if (holdExpiresAt <= now) { throw new MutationError('hold_already_expired', 'A new hold must expire in the future.', 400); } const commitment = await this.lockCommitment(input.capacityCommitmentId); if (commitment.terminatedAt) { throw new MutationError('commitment_terminated', 'The commitment is terminated.', 409); } const deal = await this.demandDeal(input.demandDealId); const existing = await this.reservations(commitment.id); const candidate: ReservationCapacity = { gpuHours: input.gpuHours, startsAt: new Date(input.startsAt), endsAt: new Date(input.endsAt), status: 'planned', holdExpiresAt, }; assertCapacityAvailable(toCapacity(commitment), existing, candidate, now); const [allocation] = await this.tx .insert(allocations) .values({ capacityCommitmentId: commitment.id, demandDealId: deal.id, gpuHours: String(input.gpuHours), pricePerGpuHourCents: input.pricePerGpuHourCents ?? 0, currency: input.currency, startsAt: candidate.startsAt, endsAt: candidate.endsAt, status: 'planned', holdExpiresAt, holdOpportunityCostCents: input.holdOpportunityCostCents, guaranteeType: input.guaranteeType, priority: input.priority, complianceDecisionId: input.complianceDecisionId, createdByUserId: principal.userId, notes: input.notes, createdAt: now, updatedAt: now, }) .returning(); if (!allocation) throw new MutationError('write_failed', 'Hold was not created.', 409); return { allocation, commitment, deal }; } async releaseAllocation( id: string, _input: ReleaseWriteInput, now: Date, ): Promise { const [reference] = await this.tx .select({ capacityCommitmentId: allocations.capacityCommitmentId }) .from(allocations) .where(eq(allocations.id, id)) .limit(1); if (!reference) throw MutationError.notFound('Allocation'); await this.lockCommitment(reference.capacityCommitmentId); const [current] = await this.tx .select() .from(allocations) .where(eq(allocations.id, id)) .limit(1) .for('update'); if (!current) throw MutationError.notFound('Allocation'); if (current.status === 'released') { throw new MutationError('already_released', 'The allocation is already released.', 409); } if (current.status === 'completed') { throw new MutationError( 'completed_allocation', 'Completed billable usage cannot be released.', 409, ); } const [allocation] = await this.tx .update(allocations) .set({ status: 'released', releasedAt: now, holdExpiresAt: null, updatedAt: now }) .where(eq(allocations.id, id)) .returning(); if (!allocation) throw MutationError.notFound('Allocation'); const deal = current.demandDealId ? await this.demandDeal(current.demandDealId) : undefined; return { allocation, accountId: deal?.accountId, previousStatus: current.status }; } } export const VALID_ALLOCATION_STATUSES = ALLOCATION_STATUSES;