ScalingFactory.java
package com.cloudforgeci.api.scaling;
import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.interfaces.ApplicationSpec;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.applicationautoscaling.EnableScalingProps;
import software.amazon.awscdk.services.autoscaling.AutoScalingGroup;
import software.amazon.awscdk.services.ecs.*;
import software.amazon.awscdk.services.ecs.CpuUtilizationScalingProps;
import software.constructs.Construct;
public class ScalingFactory extends BaseFactory {
@DeploymentContext("minInstanceCapacity")
private Integer minInstanceCapacity;
@DeploymentContext("maxInstanceCapacity")
private Integer maxInstanceCapacity;
@DeploymentContext("cpuTargetUtilization")
private Integer cpuTargetUtilization;
@DeploymentContext("enableAutoScaling")
private Boolean enableAutoScaling;
@SystemContext("applicationSpec")
private ApplicationSpec applicationSpec;
public ScalingFactory(Construct scope, String id) {
super(scope, id);
}
@Override
public void create() {
// ScalingFactory doesn't create infrastructure directly
// It provides scaling methods for other factories to use
// The create() method is required by BaseFactory but not used for this factory
}
/**
* Fails synthesis when a deployment requests more than one instance
* ({@code minInstanceCapacity}/{@code maxInstanceCapacity} above 1, or {@code enableAutoScaling})
* for an application that can't safely run that way, in either of two distinct senses:
*
* <ul>
* <li>{@link com.cloudforge.core.interfaces.ApplicationSpec#supportsAutoScaling()} is {@code
* false} -- no clustering/HA support at all in this edition, such as the Jenkins controller or
* the Nexus/SonarQube community editions.</li>
* <li>{@link com.cloudforge.core.interfaces.ApplicationSpec#requiresSequentialDeploymentWithoutDatabase()}
* is {@code true} and no managed database connection is provisioned ({@code
* ctx.dbConnection} empty) -- a single-writer embedded-file database (e.g. CloudForge
* Manager's own H2) has no way to coordinate more than one writer; two replicas both racing
* to become that database's primary is exactly the deployment-time version of the same
* problem {@code FargateFactory}'s {@code minHealthyPercent(0)}/{@code maxHealthyPercent(100)}
* rollout override already defends against for a single *replacement*, but that override
* doesn't -- can't -- prevent two simultaneously-running peers when {@code desiredCount > 1}
* itself is the actual request. A managed database handles concurrent writers safely, so this
* only applies while none is configured.</li>
* </ul>
*
* UIs may hide these fields, but this check is the enforcement point.
*
* <p>Called from both {@link #scale(FargateService)} and {@link #scale(AutoScalingGroup)} --
* the EC2 runtime's own paths to more than one instance -- and, standalone (public, for exactly
* this reason), from {@code FargateFactory#create()}: Fargate's OWN scaling wiring lives in each
* topology configuration class's {@code wireBaseAutoscalingAndDns} (e.g. {@code
* CmsServiceTopologyConfiguration}), already correctly guarded against double-registering
* {@code autoScaleTaskCount} -- {@link #scale(FargateService)} itself is never invoked for a
* real Fargate deploy, only this validation is.
*/
public void rejectUnsupportedAutoScaling() {
if (applicationSpec == null) {
return;
}
boolean requestedMultiInstance = (minInstanceCapacity != null && minInstanceCapacity > 1)
|| (maxInstanceCapacity != null && maxInstanceCapacity > 1)
|| Boolean.TRUE.equals(enableAutoScaling);
if (!requestedMultiInstance) {
return;
}
if (!applicationSpec.supportsAutoScaling()) {
throw new IllegalArgumentException(applicationSpec.displayName()
+ " does not support running more than one instance (no clustering/HA support in this "
+ "edition) -- set minInstanceCapacity/maxInstanceCapacity to 1 and enableAutoScaling to "
+ "false.");
}
if (applicationSpec.requiresSequentialDeploymentWithoutDatabase() && ctx.dbConnection.get().isEmpty()) {
throw new IllegalArgumentException(applicationSpec.displayName()
+ " uses a single-writer embedded database with no managed database connection "
+ "provisioned -- running more than one instance would race multiple writers against "
+ "the same database file. Either set minInstanceCapacity/maxInstanceCapacity to 1 and "
+ "enableAutoScaling to false, or provision a managed database (see DatabaseSpec) so "
+ "concurrent writers are handled safely.");
}
}
public void scale(final FargateService service) {
rejectUnsupportedAutoScaling();
// Check if auto-scaling is explicitly disabled
if (Boolean.FALSE.equals(enableAutoScaling)) {
return; // Auto-scaling explicitly disabled
}
// Only enable scaling if maxInstanceCapacity > 1
// Use injected DeploymentContext values via annotations
if (maxInstanceCapacity == null || maxInstanceCapacity <= 1) {
return; // No scaling configuration
}
// Use injected values or defaults
int minCapacity = minInstanceCapacity != null ? minInstanceCapacity : 1;
int maxCapacity = maxInstanceCapacity;
int targetUtilization = cpuTargetUtilization != null ? cpuTargetUtilization : 60;
ScalableTaskCount scalable = service.autoScaleTaskCount(
EnableScalingProps.builder()
.minCapacity(minCapacity)
.maxCapacity(maxCapacity)
.build());
scalable.scaleOnCpuUtilization("CpuScaleSvc",
CpuUtilizationScalingProps.builder()
.targetUtilizationPercent(targetUtilization)
.scaleInCooldown(Duration.minutes(2))
.scaleOutCooldown(Duration.minutes(2))
.build());
}
public void scale(final AutoScalingGroup asg) {
rejectUnsupportedAutoScaling();
// Check if auto-scaling is explicitly disabled
if (Boolean.FALSE.equals(enableAutoScaling)) {
return; // Auto-scaling explicitly disabled
}
// Use injected cpuTargetUtilization or default to 60%
int targetUtilization = cpuTargetUtilization != null ? cpuTargetUtilization : 60;
asg.scaleOnCpuUtilization("CpuScaleAsg",
software.amazon.awscdk.services.autoscaling.CpuUtilizationScalingProps.builder()
.targetUtilizationPercent(targetUtilization)
.cooldown(Duration.minutes(2))
.build());
}
}