diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java index 22e04669..e734a0b4 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java @@ -37,8 +37,8 @@ public class DeploymentExecutor { @Value("${cameleer.runtime.container-memory-limit:512m}") private String globalMemoryLimit; - @Value("${cameleer.runtime.container-cpu-shares:512}") - private int globalCpuShares; + @Value("${cameleer.runtime.container-cpu-request:500}") + private int globalCpuRequest; @Value("${cameleer.runtime.health-check-timeout:60}") private int healthCheckTimeout; @@ -86,7 +86,7 @@ public class DeploymentExecutor { var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults( parseMemoryLimitMb(globalMemoryLimit), - globalCpuShares, + globalCpuRequest, globalRoutingMode, globalRoutingDomain, globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl @@ -126,7 +126,6 @@ public class DeploymentExecutor { for (int i = 0; i < config.replicas(); i++) { String containerName = env.slug() + "-" + app.slug() + "-" + i; - Long cpuQuota = config.cpuLimit() != null ? (long) (config.cpuLimit() * 100_000) : null; String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null; ContainerRequest request = new ContainerRequest( @@ -136,7 +135,7 @@ public class DeploymentExecutor { envNet != null ? List.of(envNet) : List.of(), baseEnvVars, labels, config.memoryLimitBytes(), config.memoryReserveBytes(), - config.cpuShares(), cpuQuota, + config.dockerCpuShares(), config.dockerCpuQuota(), config.exposedPorts(), agentHealthPort, "on-failure", 3 ); @@ -273,19 +272,21 @@ public class DeploymentExecutor { private int waitForAnyHealthy(List containerIds, int timeoutSeconds) { long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L); + int lastHealthy = 0; while (System.currentTimeMillis() < deadline) { int healthy = 0; for (String cid : containerIds) { ContainerStatus status = orchestrator.getContainerStatus(cid); if ("healthy".equals(status.state())) healthy++; } - if (healthy > 0) return healthy; + lastHealthy = healthy; + if (healthy == containerIds.size()) return healthy; try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - return 0; + return lastHealthy; } } - return 0; + return lastHealthy; } private List> updateReplicaHealth(List> replicas, diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java index e74441f7..a7b3ea1c 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java @@ -17,8 +17,8 @@ public final class ConfigMerger { return new ResolvedContainerConfig( intVal(appConfig, envConfig, "memoryLimitMb", global.memoryLimitMb()), intOrNull(appConfig, envConfig, "memoryReserveMb"), - intVal(appConfig, envConfig, "cpuShares", global.cpuShares()), - doubleOrNull(appConfig, envConfig, "cpuLimit"), + intVal(appConfig, envConfig, "cpuRequest", global.cpuRequest()), + intOrNull(appConfig, envConfig, "cpuLimit"), intVal(appConfig, envConfig, "appPort", 8080), intList(appConfig, envConfig, "exposedPorts"), stringMap(appConfig, envConfig, "customEnvVars"), @@ -88,7 +88,7 @@ public final class ConfigMerger { /** Global defaults extracted from application.yml @Value fields */ public record GlobalRuntimeDefaults( int memoryLimitMb, - int cpuShares, + int cpuRequest, String routingMode, String routingDomain, String serverUrl diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java index 65bcd65f..0bc36081 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java @@ -6,8 +6,8 @@ import java.util.Map; public record ResolvedContainerConfig( int memoryLimitMb, Integer memoryReserveMb, - int cpuShares, - Double cpuLimit, + int cpuRequest, + Integer cpuLimit, int appPort, List exposedPorts, Map customEnvVars, @@ -26,4 +26,14 @@ public record ResolvedContainerConfig( public Long memoryReserveBytes() { return memoryReserveMb != null ? (long) memoryReserveMb * 1024 * 1024 : null; } + + /** Convert cpuRequest (millicores) to Docker CPU shares (proportional to 1024 = 1 core). */ + public int dockerCpuShares() { + return cpuRequest * 1024 / 1000; + } + + /** Convert cpuLimit (millicores) to Docker CPU quota (microseconds per 100ms period). */ + public Long dockerCpuQuota() { + return cpuLimit != null ? (long) cpuLimit * 100 : null; + } } diff --git a/ui/src/pages/Admin/EnvironmentsPage.tsx b/ui/src/pages/Admin/EnvironmentsPage.tsx index 3b6f51d9..01c08a24 100644 --- a/ui/src/pages/Admin/EnvironmentsPage.tsx +++ b/ui/src/pages/Admin/EnvironmentsPage.tsx @@ -331,7 +331,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: { const [editing, setEditing] = useState(false); const [memoryLimit, setMemoryLimit] = useState(''); const [memoryReserve, setMemoryReserve] = useState(''); - const [cpuShares, setCpuShares] = useState(''); + const [cpuRequest, setCpuRequest] = useState(''); const [cpuLimit, setCpuLimit] = useState(''); const [routingMode, setRoutingMode] = useState(String(defaults.routingMode ?? 'path')); const [routingDomain, setRoutingDomain] = useState(String(defaults.routingDomain ?? '')); @@ -341,7 +341,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: { useEffect(() => { setMemoryLimit(String(defaults.memoryLimitMb ?? '')); setMemoryReserve(String(defaults.memoryReserveMb ?? '')); - setCpuShares(String(defaults.cpuShares ?? '')); + setCpuRequest(String(defaults.cpuRequest ?? '')); setCpuLimit(String(defaults.cpuLimit ?? '')); setRoutingMode(String(environment.defaultContainerConfig.routingMode ?? 'path')); setRoutingDomain(String(environment.defaultContainerConfig.routingDomain ?? '')); @@ -353,7 +353,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: { function handleCancel() { setMemoryLimit(String(defaults.memoryLimitMb ?? '')); setMemoryReserve(String(defaults.memoryReserveMb ?? '')); - setCpuShares(String(defaults.cpuShares ?? '')); + setCpuRequest(String(defaults.cpuRequest ?? '')); setCpuLimit(String(defaults.cpuLimit ?? '')); setRoutingMode(String(defaults.routingMode ?? 'path')); setRoutingDomain(String(defaults.routingDomain ?? '')); @@ -366,8 +366,8 @@ function DefaultResourcesSection({ environment, onSave, saving }: { await onSave({ memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, - cpuShares: cpuShares ? parseInt(cpuShares) : null, - cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, + cpuRequest: cpuRequest ? parseInt(cpuRequest) : null, + cpuLimit: cpuLimit ? parseInt(cpuLimit) : null, routingMode, routingDomain: routingDomain || null, serverUrl: serverUrl || null, @@ -393,15 +393,15 @@ function DefaultResourcesSection({ environment, onSave, saving }: { ? setMemoryReserve(e.target.value)} placeholder="e.g. 256" style={{ width: 100 }} /> : {defaults.memoryReserveMb ? `${defaults.memoryReserveMb} MB` : '—'}} - CPU Shares + CPU Request {editing - ? setCpuShares(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} /> - : {String(defaults.cpuShares ?? '—')}} + ? setCpuRequest(e.target.value)} placeholder="e.g. 500" style={{ width: 100 }} /> + : {defaults.cpuRequest ? `${defaults.cpuRequest}m` : '—'}} CPU Limit {editing - ? setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 100 }} /> - : {defaults.cpuLimit ? `${defaults.cpuLimit} cores` : '—'}} + ? setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 100 }} /> + : {defaults.cpuLimit ? `${defaults.cpuLimit}m` : '—'}} Routing Mode {editing diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index e06dd8bb..320f768d 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -171,7 +171,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen const defaults = env?.defaultContainerConfig ?? {}; const [memoryLimit, setMemoryLimit] = useState(String(defaults.memoryLimitMb ?? 512)); const [memoryReserve, setMemoryReserve] = useState(String(defaults.memoryReserveMb ?? '')); - const [cpuShares, setCpuShares] = useState(String(defaults.cpuShares ?? 512)); + const [cpuRequest, setCpuRequest] = useState(String(defaults.cpuRequest ?? 500)); const [cpuLimit, setCpuLimit] = useState(String(defaults.cpuLimit ?? '')); const [ports, setPorts] = useState(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []); const [newPort, setNewPort] = useState(''); @@ -182,7 +182,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen const [stripPrefix, setStripPrefix] = useState(true); const [sslOffloading, setSslOffloading] = useState(true); - const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'resources'>('variables'); + const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('monitoring'); const [busy, setBusy] = useState(false); const [step, setStep] = useState(''); @@ -191,7 +191,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen const d = environments.find((e) => e.id === envId)?.defaultContainerConfig ?? {}; setMemoryLimit(String(d.memoryLimitMb ?? 512)); setMemoryReserve(String(d.memoryReserveMb ?? '')); - setCpuShares(String(d.cpuShares ?? 512)); + setCpuRequest(String(d.cpuRequest ?? 500)); setCpuLimit(String(d.cpuLimit ?? '')); setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []); }, [envId, environments]); @@ -224,8 +224,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen const containerConfig: Record = { memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, - cpuShares: cpuShares ? parseInt(cpuShares) : null, - cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, + cpuRequest: cpuRequest ? parseInt(cpuRequest) : null, + cpuLimit: cpuLimit ? parseInt(cpuLimit) : null, exposedPorts: ports, customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), appPort: appPort ? parseInt(appPort) : 8080, @@ -322,9 +322,9 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen {/* Config Tabs */}
- +
{configTab === 'variables' && ( @@ -421,13 +421,13 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen {!isProd && Available in production environments only} - CPU Shares - setCpuShares(e.target.value)} style={{ width: 80 }} /> + CPU Request + setCpuRequest(e.target.value)} style={{ width: 80 }} /> CPU Limit
- setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> - cores + setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} /> + millicores
Exposed Ports @@ -488,7 +488,7 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s const stopDeployment = useStopDeployment(); const deleteApp = useDeleteApp(); const fileInputRef = useRef(null); - const [subTab, setSubTab] = useState<'overview' | 'config'>('overview'); + const [subTab, setSubTab] = useState<'overview' | 'config'>('config'); const [deleteConfirm, setDeleteConfirm] = useState(false); const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); @@ -549,8 +549,8 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
- +
{subTab === 'overview' && ( @@ -702,7 +702,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug); const isProd = environment?.production ?? false; const [editing, setEditing] = useState(false); - const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'traces' | 'recording' | 'resources'>('variables'); + const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'traces' | 'recording'>('monitoring'); const appRoutes: CatalogRoute[] = useMemo(() => { if (!catalog) return []; @@ -731,7 +731,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen const merged = useMemo(() => ({ ...defaults, ...app.containerConfig }), [defaults, app.containerConfig]); const [memoryLimit, setMemoryLimit] = useState('512'); const [memoryReserve, setMemoryReserve] = useState(''); - const [cpuShares, setCpuShares] = useState('512'); + const [cpuRequest, setCpuRequest] = useState('500'); const [cpuLimit, setCpuLimit] = useState(''); const [ports, setPorts] = useState([]); const [newPort, setNewPort] = useState(''); @@ -758,7 +758,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen } setMemoryLimit(String(merged.memoryLimitMb ?? 512)); setMemoryReserve(String(merged.memoryReserveMb ?? '')); - setCpuShares(String(merged.cpuShares ?? 512)); + setCpuRequest(String(merged.cpuRequest ?? 500)); setCpuLimit(String(merged.cpuLimit ?? '')); setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []); const vars = merged.customEnvVars as Record | undefined; @@ -808,8 +808,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen const containerConfig: Record = { memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, - cpuShares: cpuShares ? parseInt(cpuShares) : null, - cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, + cpuRequest: cpuRequest ? parseInt(cpuRequest) : null, + cpuLimit: cpuLimit ? parseInt(cpuLimit) : null, exposedPorts: ports, customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), appPort: appPort ? parseInt(appPort) : 8080, @@ -910,11 +910,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen )}
- + + -
{configTab === 'variables' && ( @@ -1033,13 +1033,13 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen {!isProd && Available in production environments only} - CPU Shares - setCpuShares(e.target.value)} style={{ width: 80 }} /> + CPU Request + setCpuRequest(e.target.value)} style={{ width: 80 }} /> CPU Limit
- setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> - cores + setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} /> + millicores
Exposed Ports