UNPKG

swarm-pilot

Version:
216 lines (199 loc) 9.5 kB
import { CurrentState, DesiredState, LabelConfig, Scaling, Usage } from './schemas'; import { DockerService } from './schemas/service.js'; import { Effect } from 'effect'; import { Config } from './config.js'; export function current_state_from_string(state: string): CurrentState { switch (state) { case 'Running': return 'Running' case 'Shutdown': return 'Shutdown'; case 'Failed': return 'Failed'; case 'Rejected': return 'Failed'; case 'Complete': return 'Failed'; case 'Orphaned': return 'Orphaned'; case 'Paused': return 'Paused'; case 'Pending': return 'Pending'; case 'Preparing': return 'Preparing'; case 'Starting': return 'Starting'; case 'Removing': return 'Removing'; case 'Accepted': return 'Accepted'; case 'New': return 'New'; case 'Allocated': return 'Allocated'; case 'Assigned': return 'Assigned'; case 'Ready': return 'Ready'; default: return 'Unknown'; } } export function desired_state_from_string(state: string): DesiredState { switch (state) { case 'Running': return 'Running'; case 'Shutdown': return 'Shutdown'; default: return 'Unknown'; } } export function parse_error(error: unknown): Error { if (error instanceof Error) { return error; } else if (typeof error === 'string') { return new Error(error); } else { return new Error('Unknown error'); } } export const get_label_config_from_spec = (service_spec: DockerService) => { return Effect.gen(function* () { const config = yield* Config; const labels = service_spec?.Spec?.Labels || {}; const label_config: LabelConfig = { autoscaler_enable: Boolean(labels[config.config.enable_autoscaler_label] || false), autoscaler_minimum_cpu: Number(labels[config.config.autoscaler_minimum_cpu_label] || config.config.autoscaler_minimum_cpu_default), autoscaler_maximum_cpu: Number(labels[config.config.autoscaler_maximum_cpu_label] || config.config.autoscaler_maximum_cpu_default), autoscaler_minimum_mem: Number(labels[config.config.autoscaler_minimum_mem_label] || config.config.autoscaler_minimum_mem_default), autoscaler_maximum_mem: Number(labels[config.config.autoscaler_maximum_mem_label] || config.config.autoscaler_maximum_mem_default), autoscaler_min_replicas: Number(labels[config.config.autoscaler_min_replicas_label] || config.config.autoscaler_min_replicas_default), autoscaler_max_replicas: Number(labels[config.config.autoscaler_max_replicas_label] || config.config.autoscaler_max_replicas_default), } return label_config; }) } interface Accumulator { cpuSum: number; memSum: number; count: number; task_id: string; instance: string; } export function aggregateUsageByService(usageData: Usage[]): Usage[] { // 1. Use reduce to create a Map accumulating sums and counts const accumulatorMap = usageData.reduce( (acc, usage) => { const { service_name, cpu_usage, memory_usage } = usage; const current = acc.get(service_name); if (current) { // Update existing entry current.cpuSum += cpu_usage; current.memSum += memory_usage; current.count += 1; } else { acc.set(service_name, { cpuSum: cpu_usage, memSum: memory_usage, task_id: usage.task_id, instance: usage.instance, count: 1, }); } return acc; }, new Map<string, Accumulator>() ); const aggregatedResult: Usage[] = []; for (const [service_name, accumulator] of accumulatorMap.entries()) { const avgCpu = accumulator.count > 0 ? accumulator.cpuSum / accumulator.count : 0; const avgMem = accumulator.count > 0 ? accumulator.memSum / accumulator.count : 0; aggregatedResult.push({ service_name: service_name, cpu_usage: avgCpu, memory_usage: avgMem, task_id: accumulator.task_id, instance: accumulator.instance, }); } return aggregatedResult; } export function calculate_scaling(data: { usage: Usage, service_spec: DockerService}[]) { return Effect.gen(function* () { yield* Effect.logInfo(`Calculating scaling for ${data.length} services.`); const config = yield* Config; const divider = 10000000; let scale_services: Scaling[] = []; for (const d of data) { if(d?.service_spec?.Spec?.Mode) { const mode = d?.service_spec?.Spec?.Mode; if ('Replicated' in mode) { const label_config = yield* get_label_config_from_spec(d.service_spec); if(!label_config.autoscaler_enable) { yield* Effect.logDebug(`Autoscaler is disabled for service ${d.usage.service_name}`); continue; }; const current_scaling = mode.Replicated.Replicas; const cpuUsage = d.usage.cpu_usage; const memoryUsage = d.usage.memory_usage/1048576; const cpuLimit = d.service_spec.Spec?.TaskTemplate?.Resources?.Limits?.NanoCPUs ? d.service_spec.Spec?.TaskTemplate?.Resources?.Limits?.NanoCPUs/divider : 0; const memoryLimit = d.service_spec.Spec?.TaskTemplate?.Resources?.Limits?.MemoryBytes ? d.service_spec.Spec?.TaskTemplate?.Resources?.Limits?.MemoryBytes/1048576 : 0; const cpu_percent = Math.round((cpuUsage / cpuLimit) * 100); const memory_percent = Math.round((memoryUsage / (memoryLimit)) * 100); let scale_cpu = 0; let scale_mem = 0; if(cpuLimit !== 0) { yield* Effect.logDebug(`CPU Limit is set for service ${d.usage.service_name}`); if(cpu_percent < label_config.autoscaler_minimum_cpu) { if(current_scaling - 1 < label_config.autoscaler_min_replicas) { yield* Effect.logDebug(`Service ${d.usage.service_name} is already at min scaling`); } else { scale_cpu = current_scaling - 1; } } if(cpu_percent > label_config.autoscaler_maximum_cpu) { if(label_config.autoscaler_max_replicas === 0) scale_cpu = current_scaling + 1; if(current_scaling + 1 > label_config.autoscaler_max_replicas) { yield* Effect.logDebug(`Service ${d.usage.service_name} is already at max scaling`); } else { scale_cpu = current_scaling + 1; } } } if(memoryLimit !== 0) { yield* Effect.logDebug(`Memory Limit is set for service ${d.usage.service_name}`); if(memory_percent < label_config.autoscaler_minimum_mem) { if(current_scaling - 1 < label_config.autoscaler_min_replicas) { yield* Effect.logDebug(`Service ${d.usage.service_name} is already at min scaling`); } else { scale_mem = current_scaling - 1; } } if(memory_percent > label_config.autoscaler_maximum_mem) { if(label_config.autoscaler_max_replicas === 0) scale_mem = current_scaling + 1; if(current_scaling <= label_config.autoscaler_max_replicas) { yield* Effect.logDebug(`Service ${d.usage.service_name} is already at max scaling`); } else { scale_mem = current_scaling + 1; } } } const scale = Math.max(scale_cpu, scale_mem); if (scale !== 0) { scale_services.push({ service_name: d.usage.service_name, from: current_scaling, to: scale, }); yield* Effect.logInfo(`Service ${d.usage.service_name} needs scaling from ${current_scaling} to ${scale} replicas.`); } else { yield* Effect.logInfo(`No scaling needed for service ${d.usage.service_name}, current scaling is ${current_scaling}. CPU: ${cpu_percent}%|${Math.round(cpuUsage)}/${cpuLimit}, Memory: ${memory_percent}%|${Math.round(memoryUsage)}/${memoryLimit}`); } } } } return scale_services; }) }