blob: b4843597f4e03dd3ed9d46a09cdef3bb8a5d83b3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.policy.autoscaling;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.brooklyn.api.catalog.Catalog;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.trait.Resizable;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.policy.AbstractPolicy;
import org.apache.brooklyn.core.sensor.BasicNotificationSensor;
import org.apache.brooklyn.policy.autoscaling.SizeHistory.WindowSummary;
import org.apache.brooklyn.policy.loadbalancing.LoadBalancingPolicy;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import groovy.lang.Closure;
/**
* Policy that is attached to a {@link Resizable} entity and dynamically adjusts its size in response to
* emitted {@code POOL_COLD} and {@code POOL_HOT} events. Alternatively, the policy can be configured to
* keep a given metric within a required range.
* <p>
* This policy does not itself determine whether the pool is hot or cold, but instead relies on these
* events being emitted by the monitored entity itself, or by another policy that is attached to it; see,
* for example, {@link LoadBalancingPolicy}.)
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Catalog(name="Auto-scaler", description="Policy that is attached to a Resizable entity and dynamically "
+ "adjusts its size in response to either keep a metric within a given range, or in response to "
+ "POOL_COLD and POOL_HOT events")
public class AutoScalerPolicy extends AbstractPolicy {
private static final Logger LOG = LoggerFactory.getLogger(AutoScalerPolicy.class);
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String id;
private String name;
private AttributeSensor<? extends Number> metric;
private Entity entityWithMetric;
private Number metricUpperBound;
private Number metricLowerBound;
private int minPoolSize = 1;
private int maxPoolSize = Integer.MAX_VALUE;
private Integer resizeDownIterationIncrement;
private Integer resizeDownIterationMax;
private Integer resizeUpIterationIncrement;
private Integer resizeUpIterationMax;
private Duration minPeriodBetweenExecs;
private Duration resizeUpStabilizationDelay;
private Duration resizeDownStabilizationDelay;
private ResizeOperator resizeOperator;
private Function<Entity,Integer> currentSizeOperator;
private BasicNotificationSensor<?> poolHotSensor;
private BasicNotificationSensor<?> poolColdSensor;
private BasicNotificationSensor<?> poolOkSensor;
private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor;
private Duration maxReachedNotificationDelay;
public Builder id(String val) {
this.id = val; return this;
}
public Builder name(String val) {
this.name = val; return this;
}
public Builder metric(AttributeSensor<? extends Number> val) {
this.metric = val; return this;
}
public Builder entityWithMetric(Entity val) {
this.entityWithMetric = val; return this;
}
public Builder metricLowerBound(Number val) {
this.metricLowerBound = val; return this;
}
public Builder metricUpperBound(Number val) {
this.metricUpperBound = val; return this;
}
public Builder metricRange(Number min, Number max) {
metricLowerBound = checkNotNull(min);
metricUpperBound = checkNotNull(max);
return this;
}
public Builder minPoolSize(int val) {
this.minPoolSize = val; return this;
}
public Builder maxPoolSize(int val) {
this.maxPoolSize = val; return this;
}
public Builder sizeRange(int min, int max) {
minPoolSize = min;
maxPoolSize = max;
return this;
}
public Builder resizeUpIterationIncrement(Integer val) {
this.resizeUpIterationIncrement = val; return this;
}
public Builder resizeUpIterationMax(Integer val) {
this.resizeUpIterationMax = val; return this;
}
public Builder resizeDownIterationIncrement(Integer val) {
this.resizeUpIterationIncrement = val; return this;
}
public Builder resizeDownIterationMax(Integer val) {
this.resizeUpIterationMax = val; return this;
}
public Builder minPeriodBetweenExecs(Duration val) {
this.minPeriodBetweenExecs = val; return this;
}
public Builder resizeUpStabilizationDelay(Duration val) {
this.resizeUpStabilizationDelay = val; return this;
}
public Builder resizeDownStabilizationDelay(Duration val) {
this.resizeDownStabilizationDelay = val; return this;
}
public Builder resizeOperator(ResizeOperator val) {
this.resizeOperator = val; return this;
}
public Builder currentSizeOperator(Function<Entity, Integer> val) {
this.currentSizeOperator = val; return this;
}
public Builder poolHotSensor(BasicNotificationSensor<?> val) {
this.poolHotSensor = val; return this;
}
public Builder poolColdSensor(BasicNotificationSensor<?> val) {
this.poolColdSensor = val; return this;
}
public Builder poolOkSensor(BasicNotificationSensor<?> val) {
this.poolOkSensor = val; return this;
}
public Builder maxSizeReachedSensor(BasicNotificationSensor<? super MaxPoolSizeReachedEvent> val) {
this.maxSizeReachedSensor = val; return this;
}
public Builder maxReachedNotificationDelay(Duration val) {
this.maxReachedNotificationDelay = val; return this;
}
public AutoScalerPolicy build() {
return new AutoScalerPolicy(toFlags());
}
public PolicySpec<AutoScalerPolicy> buildSpec() {
return PolicySpec.create(AutoScalerPolicy.class)
.configure(toFlags());
}
private Map<String,?> toFlags() {
return MutableMap.<String,Object>builder()
.putIfNotNull("id", id)
.putIfNotNull("name", name)
.putIfNotNull("metric", metric)
.putIfNotNull("entityWithMetric", entityWithMetric)
.putIfNotNull("metricUpperBound", metricUpperBound)
.putIfNotNull("metricLowerBound", metricLowerBound)
.putIfNotNull("minPoolSize", minPoolSize)
.putIfNotNull("maxPoolSize", maxPoolSize)
.putIfNotNull("resizeUpIterationMax", resizeUpIterationMax)
.putIfNotNull("resizeUpIterationIncrement", resizeUpIterationIncrement)
.putIfNotNull("resizeDownIterationMax", resizeDownIterationMax)
.putIfNotNull("resizeDownIterationIncrement", resizeDownIterationIncrement)
.putIfNotNull("minPeriodBetweenExecs", minPeriodBetweenExecs)
.putIfNotNull("resizeUpStabilizationDelay", resizeUpStabilizationDelay)
.putIfNotNull("resizeDownStabilizationDelay", resizeDownStabilizationDelay)
.putIfNotNull("resizeOperator", resizeOperator)
.putIfNotNull("currentSizeOperator", currentSizeOperator)
.putIfNotNull("poolHotSensor", poolHotSensor)
.putIfNotNull("poolColdSensor", poolColdSensor)
.putIfNotNull("poolOkSensor", poolOkSensor)
.putIfNotNull("maxSizeReachedSensor", maxSizeReachedSensor)
.putIfNotNull("maxReachedNotificationDelay", maxReachedNotificationDelay)
.build();
}
}
// TODO Is there a nicer pattern for registering such type-coercions?
// Can't put it in the ResizeOperator interface, nor in core TypeCoercions class because interface is defined in policy/.
static {
TypeCoercions.registerAdapter(Closure.class, ResizeOperator.class, new Function<Closure,ResizeOperator>() {
@Override
public ResizeOperator apply(final Closure closure) {
return new ResizeOperator() {
@Override public Integer resize(Entity entity, Integer input) {
return (Integer) closure.call(entity, input);
}
};
}
});
}
// Pool workrate notifications.
public static BasicNotificationSensor<Map> DEFAULT_POOL_HOT_SENSOR = new BasicNotificationSensor<Map>(
Map.class, "resizablepool.hot", "Pool is over-utilized; it has insufficient resource for current workload");
public static BasicNotificationSensor<Map> DEFAULT_POOL_COLD_SENSOR = new BasicNotificationSensor<Map>(
Map.class, "resizablepool.cold", "Pool is under-utilized; it has too much resource for current workload");
public static BasicNotificationSensor<Map> DEFAULT_POOL_OK_SENSOR = new BasicNotificationSensor<Map>(
Map.class, "resizablepool.cold", "Pool utilization is ok; the available resources are fine for the current workload");
/**
* A convenience for policies that want to register a {@code builder.maxSizeReachedSensor(sensor)}.
* Note that this "default" is not set automatically; the default is for no sensor to be used (so
* no events emitted).
*/
public static BasicNotificationSensor<MaxPoolSizeReachedEvent> DEFAULT_MAX_SIZE_REACHED_SENSOR = new BasicNotificationSensor<MaxPoolSizeReachedEvent>(
MaxPoolSizeReachedEvent.class, "resizablepool.maxSizeReached", "Consistently wanted to resize the pool above the max allowed size");
public static final String POOL_CURRENT_SIZE_KEY = "pool.current.size";
public static final String POOL_HIGH_THRESHOLD_KEY = "pool.high.threshold";
public static final String POOL_LOW_THRESHOLD_KEY = "pool.low.threshold";
public static final String POOL_CURRENT_WORKRATE_KEY = "pool.current.workrate";
@SuppressWarnings("serial")
@SetFromFlag("metric")
public static final ConfigKey<AttributeSensor<? extends Number>> METRIC = BasicConfigKey.builder(new TypeToken<AttributeSensor<? extends Number>>() {})
.name("autoscaler.metric")
.build();
@SetFromFlag("entityWithMetric")
public static final ConfigKey<Entity> ENTITY_WITH_METRIC = BasicConfigKey.builder(Entity.class)
.name("autoscaler.entityWithMetric")
.description("The Entity with the metric that will be monitored")
.build();
@SetFromFlag("metricLowerBound")
public static final ConfigKey<Number> METRIC_LOWER_BOUND = BasicConfigKey.builder(Number.class)
.name("autoscaler.metricLowerBound")
.description("The lower bound of the monitored metric. Below this the policy will resize down")
.reconfigurable(true)
.build();
@SetFromFlag("metricUpperBound")
public static final ConfigKey<Number> METRIC_UPPER_BOUND = BasicConfigKey.builder(Number.class)
.name("autoscaler.metricUpperBound")
.description("The upper bound of the monitored metric. Above this the policy will resize up")
.reconfigurable(true)
.build();
@SetFromFlag("resizeUpIterationIncrement")
public static final ConfigKey<Integer> RESIZE_UP_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
.name("autoscaler.resizeUpIterationIncrement")
.description("Batch size for resizing up; the size will be increased by a multiple of this value")
.defaultValue(1)
.reconfigurable(true)
.build();
@SetFromFlag("resizeUpIterationMax")
public static final ConfigKey<Integer> RESIZE_UP_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
.name("autoscaler.resizeUpIterationMax")
.defaultValue(Integer.MAX_VALUE)
.description("Maximum change to the size on a single iteration when scaling up")
.reconfigurable(true)
.build();
@SetFromFlag("resizeDownIterationIncrement")
public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
.name("autoscaler.resizeDownIterationIncrement")
.description("Batch size for resizing down; the size will be decreased by a multiple of this value")
.defaultValue(1)
.reconfigurable(true)
.build();
@SetFromFlag("resizeDownIterationMax")
public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
.name("autoscaler.resizeDownIterationMax")
.defaultValue(Integer.MAX_VALUE)
.description("Maximum change to the size on a single iteration when scaling down")
.reconfigurable(true)
.build();
@SetFromFlag("minPeriodBetweenExecs")
public static final ConfigKey<Duration> MIN_PERIOD_BETWEEN_EXECS = BasicConfigKey.builder(Duration.class)
.name("autoscaler.minPeriodBetweenExecs")
.defaultValue(Duration.millis(100))
.build();
@SetFromFlag("resizeUpStabilizationDelay")
public static final ConfigKey<Duration> RESIZE_UP_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
.name("autoscaler.resizeUpStabilizationDelay")
.defaultValue(Duration.ZERO)
.reconfigurable(true)
.build();
@SetFromFlag("resizeDownStabilizationDelay")
public static final ConfigKey<Duration> RESIZE_DOWN_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
.name("autoscaler.resizeDownStabilizationDelay")
.defaultValue(Duration.ZERO)
.reconfigurable(true)
.build();
@SetFromFlag("minPoolSize")
public static final ConfigKey<Integer> MIN_POOL_SIZE = BasicConfigKey.builder(Integer.class)
.name("autoscaler.minPoolSize")
.defaultValue(1)
.reconfigurable(true)
.build();
@SetFromFlag("maxPoolSize")
public static final ConfigKey<Integer> MAX_POOL_SIZE = BasicConfigKey.builder(Integer.class)
.name("autoscaler.maxPoolSize")
.defaultValue(Integer.MAX_VALUE)
.reconfigurable(true)
.build();
public static final ConfigKey<Integer> INSUFFICIENT_CAPACITY_HIGH_WATER_MARK = BasicConfigKey.builder(Integer.class)
.name("autoscaler.insufficientCapacityHighWaterMark")
.defaultValue(null)
.reconfigurable(true)
.build();
@SetFromFlag("resizeOperator")
public static final ConfigKey<ResizeOperator> RESIZE_OPERATOR = BasicConfigKey.builder(ResizeOperator.class)
.name("autoscaler.resizeOperator")
.defaultValue(new ResizeOperator() {
public Integer resize(Entity entity, Integer desiredSize) {
return ((Resizable)entity).resize(desiredSize);
}})
.build();
@SuppressWarnings("serial")
@SetFromFlag("currentSizeOperator")
public static final ConfigKey<Function<Entity,Integer>> CURRENT_SIZE_OPERATOR = BasicConfigKey.builder(new TypeToken<Function<Entity,Integer>>() {})
.name("autoscaler.currentSizeOperator")
.defaultValue(new Function<Entity,Integer>() {
public Integer apply(Entity entity) {
return ((Resizable)entity).getCurrentSize();
}})
.build();
@SuppressWarnings("serial")
@SetFromFlag("poolHotSensor")
public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_HOT_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
.name("autoscaler.poolHotSensor")
.defaultValue(DEFAULT_POOL_HOT_SENSOR)
.build();
@SuppressWarnings("serial")
@SetFromFlag("poolColdSensor")
public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_COLD_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
.name("autoscaler.poolColdSensor")
.defaultValue(DEFAULT_POOL_COLD_SENSOR)
.build();
@SuppressWarnings("serial")
@SetFromFlag("poolOkSensor")
public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_OK_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
.name("autoscaler.poolOkSensor")
.defaultValue(DEFAULT_POOL_OK_SENSOR)
.build();
@SuppressWarnings("serial")
@SetFromFlag("maxSizeReachedSensor")
public static final ConfigKey<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>> MAX_SIZE_REACHED_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>>() {})
.name("autoscaler.maxSizeReachedSensor")
.description("Sensor for which a notification will be emitted (on the associated entity) when " +
"we consistently wanted to resize the pool above the max allowed size, for " +
"maxReachedNotificationDelay milliseconds")
.build();
@SetFromFlag("maxReachedNotificationDelay")
public static final ConfigKey<Duration> MAX_REACHED_NOTIFICATION_DELAY = BasicConfigKey.builder(Duration.class)
.name("autoscaler.maxReachedNotificationDelay")
.description("Time that we consistently wanted to go above the maxPoolSize for, after which the " +
"maxSizeReachedSensor (if any) will be emitted")
.defaultValue(Duration.ZERO)
.build();
private Entity poolEntity;
private final AtomicBoolean executorQueued = new AtomicBoolean(false);
private volatile long executorTime = 0;
private volatile ScheduledExecutorService executor;
private SizeHistory recentUnboundedResizes;
private SizeHistory recentDesiredResizes;
private long maxReachedLastNotifiedTime;
private final SensorEventListener<Map> utilizationEventHandler = new SensorEventListener<Map>() {
public void onEvent(SensorEvent<Map> event) {
Map<String, ?> properties = (Map<String, ?>) event.getValue();
Sensor<?> sensor = event.getSensor();
if (sensor.equals(getPoolColdSensor())) {
onPoolCold(properties);
} else if (sensor.equals(getPoolHotSensor())) {
onPoolHot(properties);
} else if (sensor.equals(getPoolOkSensor())) {
onPoolOk(properties);
} else {
throw new IllegalStateException("Unexpected sensor type: "+sensor+"; event="+event);
}
}
};
private final SensorEventListener<Number> metricEventHandler = new SensorEventListener<Number>() {
public void onEvent(SensorEvent<Number> event) {
assert event.getSensor().equals(getMetric());
onMetricChanged(event.getValue());
}
};
public AutoScalerPolicy() {
this(MutableMap.<String,Object>of());
}
public AutoScalerPolicy(Map<String,?> props) {
super(props);
}
@Override
public void init() {
doInit();
}
@Override
public void rebind() {
doInit();
}
protected void doInit() {
long maxReachedNotificationDelay = getMaxReachedNotificationDelay().toMilliseconds();
recentUnboundedResizes = new SizeHistory(maxReachedNotificationDelay);
long maxResizeStabilizationDelay = Math.max(getResizeUpStabilizationDelay().toMilliseconds(), getResizeDownStabilizationDelay().toMilliseconds());
recentDesiredResizes = new SizeHistory(maxResizeStabilizationDelay);
// TODO Should re-use the execution manager's thread pool, somehow
executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
}
public void setMetricLowerBound(Number val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing metricLowerBound from {} to {}", new Object[] {this, getMetricLowerBound(), val});
config().set(METRIC_LOWER_BOUND, checkNotNull(val));
}
public void setMetricUpperBound(Number val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing metricUpperBound from {} to {}", new Object[] {this, getMetricUpperBound(), val});
config().set(METRIC_UPPER_BOUND, checkNotNull(val));
}
private <T> void setOrDefault(ConfigKey<T> key, T val) {
if (val==null) val = key.getDefaultValue();
config().set(key, val);
}
public int getResizeUpIterationIncrement() { return getConfig(RESIZE_UP_ITERATION_INCREMENT); }
public void setResizeUpIterationIncrement(Integer val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationIncrement from {} to {}", new Object[] {this, getResizeUpIterationIncrement(), val});
setOrDefault(RESIZE_UP_ITERATION_INCREMENT, val);
}
public int getResizeDownIterationIncrement() { return getConfig(RESIZE_DOWN_ITERATION_INCREMENT); }
public void setResizeDownIterationIncrement(Integer val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationIncrement from {} to {}", new Object[] {this, getResizeDownIterationIncrement(), val});
setOrDefault(RESIZE_DOWN_ITERATION_INCREMENT, val);
}
public int getResizeUpIterationMax() { return getConfig(RESIZE_UP_ITERATION_MAX); }
public void setResizeUpIterationMax(Integer val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationMax from {} to {}", new Object[] {this, getResizeUpIterationMax(), val});
setOrDefault(RESIZE_UP_ITERATION_MAX, val);
}
public int getResizeDownIterationMax() { return getConfig(RESIZE_DOWN_ITERATION_MAX); }
public void setResizeDownIterationMax(Integer val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationMax from {} to {}", new Object[] {this, getResizeDownIterationMax(), val});
setOrDefault(RESIZE_DOWN_ITERATION_MAX, val);
}
public void setMinPeriodBetweenExecs(Duration val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing minPeriodBetweenExecs from {} to {}", new Object[] {this, getMinPeriodBetweenExecs(), val});
config().set(MIN_PERIOD_BETWEEN_EXECS, val);
}
public void setResizeUpStabilizationDelay(Duration val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpStabilizationDelay from {} to {}", new Object[] {this, getResizeUpStabilizationDelay(), val});
config().set(RESIZE_UP_STABILIZATION_DELAY, val);
}
public void setResizeDownStabilizationDelay(Duration val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownStabilizationDelay from {} to {}", new Object[] {this, getResizeDownStabilizationDelay(), val});
config().set(RESIZE_DOWN_STABILIZATION_DELAY, val);
}
public void setMinPoolSize(int val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing minPoolSize from {} to {}", new Object[] {this, getMinPoolSize(), val});
config().set(MIN_POOL_SIZE, val);
}
public void setMaxPoolSize(int val) {
if (LOG.isInfoEnabled()) LOG.info("{} changing maxPoolSize from {} to {}", new Object[] {this, getMaxPoolSize(), val});
config().set(MAX_POOL_SIZE, val);
}
private AttributeSensor<? extends Number> getMetric() {
return getConfig(METRIC);
}
private Entity getEntityWithMetric() {
return getConfig(ENTITY_WITH_METRIC);
}
private Number getMetricLowerBound() {
return getConfig(METRIC_LOWER_BOUND);
}
private Number getMetricUpperBound() {
return getConfig(METRIC_UPPER_BOUND);
}
private Duration getMinPeriodBetweenExecs() {
return getConfig(MIN_PERIOD_BETWEEN_EXECS);
}
private Duration getResizeUpStabilizationDelay() {
return getConfig(RESIZE_UP_STABILIZATION_DELAY);
}
private Duration getResizeDownStabilizationDelay() {
return getConfig(RESIZE_DOWN_STABILIZATION_DELAY);
}
private int getMinPoolSize() {
return getConfig(MIN_POOL_SIZE);
}
private int getMaxPoolSize() {
return getConfig(MAX_POOL_SIZE);
}
private Integer getInsufficientCapacityHighWaterMark() {
return getConfig(INSUFFICIENT_CAPACITY_HIGH_WATER_MARK);
}
private ResizeOperator getResizeOperator() {
return getConfig(RESIZE_OPERATOR);
}
private Function<Entity,Integer> getCurrentSizeOperator() {
return getConfig(CURRENT_SIZE_OPERATOR);
}
private BasicNotificationSensor<? extends Map> getPoolHotSensor() {
return getConfig(POOL_HOT_SENSOR);
}
private BasicNotificationSensor<? extends Map> getPoolColdSensor() {
return getConfig(POOL_COLD_SENSOR);
}
private BasicNotificationSensor<? extends Map> getPoolOkSensor() {
return getConfig(POOL_OK_SENSOR);
}
private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> getMaxSizeReachedSensor() {
return getConfig(MAX_SIZE_REACHED_SENSOR);
}
private Duration getMaxReachedNotificationDelay() {
return getConfig(MAX_REACHED_NOTIFICATION_DELAY);
}
@Override
protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
if (key.equals(RESIZE_UP_STABILIZATION_DELAY)) {
Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeDownStabilizationDelay());
recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
} else if (key.equals(RESIZE_DOWN_STABILIZATION_DELAY)) {
Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeUpStabilizationDelay());
recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
} else if (key.equals(METRIC_LOWER_BOUND)) {
// TODO If recorded what last metric value was then we could recalculate immediately
// Rely on next metric-change to trigger recalculation;
// and same for those below...
} else if (key.equals(METRIC_UPPER_BOUND)) {
// see above
} else if (key.equals(RESIZE_UP_ITERATION_INCREMENT) || key.equals(RESIZE_UP_ITERATION_MAX) || key.equals(RESIZE_DOWN_ITERATION_INCREMENT) || key.equals(RESIZE_DOWN_ITERATION_MAX)) {
// no special actions needed
} else if (key.equals(MIN_POOL_SIZE)) {
int newMin = (Integer) val;
if (newMin > getConfig(MAX_POOL_SIZE)) {
throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
}
onPoolSizeLimitsChanged(newMin, getConfig(MAX_POOL_SIZE));
} else if (key.equals(MAX_POOL_SIZE)) {
int newMax = (Integer) val;
if (newMax < getConfig(MIN_POOL_SIZE)) {
throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
}
onPoolSizeLimitsChanged(getConfig(MIN_POOL_SIZE), newMax);
} else if (key.equals(INSUFFICIENT_CAPACITY_HIGH_WATER_MARK)) {
Integer newVal = (Integer) val;
Integer oldVal = config().get(INSUFFICIENT_CAPACITY_HIGH_WATER_MARK);
if (oldVal != null && (newVal == null || newVal > oldVal)) {
LOG.info("{} resetting {} to {}, which will enable resizing above previous level of {}",
new Object[] {AutoScalerPolicy.this, INSUFFICIENT_CAPACITY_HIGH_WATER_MARK.getName(), newVal, oldVal});
// TODO see above about changing metricLowerBound; not triggering resize now
}
} else {
throw new UnsupportedOperationException("reconfiguring "+key+" unsupported for "+this);
}
}
@Override
public void suspend() {
super.suspend();
// TODO unsubscribe from everything? And resubscribe on resume?
if (executor != null) executor.shutdownNow();
}
@Override
public void resume() {
super.resume();
executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
}
@Override
public void setEntity(EntityLocal entity) {
if (!config().getRaw(RESIZE_OPERATOR).isPresentAndNonNull()) {
Preconditions.checkArgument(entity instanceof Resizable, "Provided entity "+entity+" must be an instance of Resizable, because no custom-resizer operator supplied");
}
super.setEntity(entity);
this.poolEntity = entity;
if (getMetric() != null) {
Entity entityToSubscribeTo = (getEntityWithMetric() != null) ? getEntityWithMetric() : entity;
subscriptions().subscribe(entityToSubscribeTo, getMetric(), metricEventHandler);
}
subscriptions().subscribe(poolEntity, getPoolColdSensor(), utilizationEventHandler);
subscriptions().subscribe(poolEntity, getPoolHotSensor(), utilizationEventHandler);
subscriptions().subscribe(poolEntity, getPoolOkSensor(), utilizationEventHandler);
}
private ThreadFactory newThreadFactory() {
return new ThreadFactoryBuilder()
.setNameFormat("brooklyn-autoscalerpolicy-%d")
.build();
}
/**
* Forces an immediate resize (without waiting for stabilization etc) if the current size is
* not within the min and max limits. We schedule this so that all resize operations are done
* by the same thread.
*/
private void onPoolSizeLimitsChanged(final int min, final int max) {
if (LOG.isTraceEnabled()) LOG.trace("{} checking pool size on limits changed for {} (between {} and {})", new Object[] {this, poolEntity, min, max});
if (isRunning() && isEntityUp()) {
executor.submit(new Runnable() {
@Override public void run() {
try {
int currentSize = getCurrentSizeOperator().apply(entity);
int desiredSize = Math.min(max, Math.max(min, currentSize));
if (currentSize != desiredSize) {
if (LOG.isInfoEnabled()) LOG.info("{} resizing pool {} immediateley from {} to {} (due to new pool size limits)", new Object[] {this, poolEntity, currentSize, desiredSize});
getResizeOperator().resize(poolEntity, desiredSize);
}
} catch (Exception e) {
if (isRunning()) {
LOG.error("Error resizing: "+e, e);
} else {
if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
}
} catch (Throwable t) {
LOG.error("Error resizing: "+t, t);
throw Throwables.propagate(t);
}
}});
}
}
private enum ScalingType { HOT, COLD }
private static class ScalingData {
ScalingType scalingMode;
int currentSize;
double currentMetricValue;
Double metricUpperBound;
Double metricLowerBound;
public double getCurrentTotalActivity() {
return currentMetricValue * currentSize;
}
public boolean isHot() {
return ((scalingMode==null || scalingMode==ScalingType.HOT) && isValid(metricUpperBound) && currentMetricValue > metricUpperBound);
}
public boolean isCold() {
return ((scalingMode==null || scalingMode==ScalingType.COLD) && isValid(metricLowerBound) && currentMetricValue < metricLowerBound);
}
private boolean isValid(Double bound) {
return (bound!=null && bound>0);
}
}
private void onMetricChanged(Number val) {
if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-metric for {}: {}", new Object[] {this, poolEntity, val});
if (val==null) {
// occurs e.g. if using an aggregating enricher who returns null when empty, the sensor has gone away
if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {}, inbound metric is null", new Object[] {this, poolEntity});
return;
}
ScalingData data = new ScalingData();
data.currentMetricValue = val.doubleValue();
data.currentSize = getCurrentSizeOperator().apply(entity);
data.metricUpperBound = getMetricUpperBound().doubleValue();
data.metricLowerBound = getMetricLowerBound().doubleValue();
analyze(data, "pool");
}
private void onPoolCold(Map<String, ?> properties) {
if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-cold for {}: {}", new Object[] {this, poolEntity, properties});
analyzeOnHotOrColdSensor(ScalingType.COLD, "cold pool", properties);
}
private void onPoolHot(Map<String, ?> properties) {
if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-hot for {}: {}", new Object[] {this, poolEntity, properties});
analyzeOnHotOrColdSensor(ScalingType.HOT, "hot pool", properties);
}
private void analyzeOnHotOrColdSensor(ScalingType scalingMode, String description, Map<String, ?> properties) {
ScalingData data = new ScalingData();
data.scalingMode = scalingMode;
data.currentMetricValue = (Double) properties.get(POOL_CURRENT_WORKRATE_KEY);
data.currentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
data.metricUpperBound = (Double) properties.get(POOL_HIGH_THRESHOLD_KEY);
data.metricLowerBound = (Double) properties.get(POOL_LOW_THRESHOLD_KEY);
analyze(data, description);
}
private void analyze(ScalingData data, String description) {
int desiredSizeUnconstrained;
/* We always scale out (modulo stabilization delay) if:
* currentTotalActivity > currentSize*metricUpperBound
* With newDesiredSize the smallest n such that   n*metricUpperBound >= currentTotalActivity
* ie  n >= currentTotalActiviy/metricUpperBound, thus n := Math.ceil(currentTotalActivity/metricUpperBound)
*
* Else consider scale back if:
*  currentTotalActivity < currentSize*metricLowerBound
* With newDesiredSize normally the largest n such that:  
*   n*metricLowerBound <= currentTotalActivity
* BUT with an absolute requirement which trumps the above computation
* that the newDesiredSize doesn't cause immediate scale out:
*   n*metricUpperBound >= currentTotalActivity
* thus n := Math.max ( floor(currentTotalActiviy/metricLowerBound), ceil(currentTotal/metricUpperBound) )
*/
if (data.isHot()) {
// scale out
desiredSizeUnconstrained = (int)Math.ceil(data.getCurrentTotalActivity() / data.metricUpperBound);
data.scalingMode = ScalingType.HOT;
} else if (data.isCold()) {
// scale back
desiredSizeUnconstrained = (int)Math.floor(data.getCurrentTotalActivity() / data.metricLowerBound);
data.scalingMode = ScalingType.COLD;
} else {
if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} ({} within range {}..{})", new Object[] {this, poolEntity, data.currentSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound});
abortResize(data.currentSize);
return; // within the healthy range; no-op
}
if (LOG.isTraceEnabled()) LOG.debug("{} detected unconstrained desired size {}", new Object[] {this, desiredSizeUnconstrained});
int desiredSize = applyMinMaxConstraints(desiredSizeUnconstrained);
if ((data.scalingMode==ScalingType.COLD) && (desiredSize < data.currentSize)) {
int delta = data.currentSize - desiredSize;
int scaleIncrement = getResizeDownIterationIncrement();
int scaleMax = getResizeDownIterationMax();
if (delta>scaleMax) {
delta=scaleMax;
} else if (delta % scaleIncrement != 0) {
// keep scaling to the increment
delta += scaleIncrement - (delta % scaleIncrement);
}
desiredSize = data.currentSize - delta;
if (data.metricUpperBound!=null) {
// if upper bound supplied, check that this desired scale-back size
// is not going to cause scale-out on next run; i.e. anti-thrashing
while (desiredSize < data.currentSize && data.getCurrentTotalActivity() > data.metricUpperBound * desiredSize) {
if (LOG.isTraceEnabled()) LOG.trace("{} when resizing back pool {} from {}, tweaking from {} to prevent thrashing", new Object[] {this, poolEntity, data.currentSize, desiredSize });
desiredSize += scaleIncrement;
}
}
desiredSize = applyMinMaxConstraints(desiredSize);
if (desiredSize >= data.currentSize) data.scalingMode = null;
} else if ((data.scalingMode==ScalingType.HOT) && (desiredSize > data.currentSize)) {
int delta = desiredSize - data.currentSize;
int scaleIncrement = getResizeUpIterationIncrement();
int scaleMax = getResizeUpIterationMax();
if (delta>scaleMax) {
delta=scaleMax;
} else if (delta % scaleIncrement != 0) {
// keep scaling to the increment
delta += scaleIncrement - (delta % scaleIncrement);
}
desiredSize = data.currentSize + delta;
desiredSize = applyMinMaxConstraints(desiredSize);
if (desiredSize <= data.currentSize) data.scalingMode = null;
} else {
data.scalingMode = null;
}
if (data.scalingMode!=null) {
if (LOG.isDebugEnabled()) LOG.debug("{} provisionally resizing {} {} from {} to {} ({} < {}; ideal size {})", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, desiredSizeUnconstrained});
scheduleResize(desiredSize);
} else {
if (LOG.isTraceEnabled()) LOG.trace("{} not resizing {} {} from {} to {}, {} out of healthy range {}..{} but unconstrained size {} blocked by bounds/check", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound, desiredSizeUnconstrained});
abortResize(data.currentSize);
// but add to the unbounded record for future consideration
}
onNewUnboundedPoolSize(desiredSizeUnconstrained);
}
private int applyMinMaxConstraints(long desiredSize) {
return applyMinMaxConstraints(desiredSize > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)desiredSize);
}
private int applyMinMaxConstraints(int desiredSize) {
int minSize = getMinPoolSize();
int maxSize = getMaxPoolSize();
Integer insufficientCapacityHighWaterMark = getInsufficientCapacityHighWaterMark();
desiredSize = Math.max(minSize, desiredSize);
desiredSize = Math.min(maxSize, desiredSize);
if (insufficientCapacityHighWaterMark != null) desiredSize = Math.min(insufficientCapacityHighWaterMark, desiredSize);
return desiredSize;
}
private void onPoolOk(Map<String, ?> properties) {
if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-ok for {}: {}", new Object[] {this, poolEntity, properties});
int poolCurrentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
if (LOG.isTraceEnabled()) LOG.trace("{} not resizing ok pool {} from {}", new Object[] {this, poolEntity, poolCurrentSize});
abortResize(poolCurrentSize);
}
/**
* Schedules a resize, if there is not already a resize operation queued up. When that resize
* executes, it will resize to whatever the latest value is to be (rather than what it was told
* to do at the point the job was queued).
*/
private void scheduleResize(final int newSize) {
recentDesiredResizes.add(newSize);
scheduleResize();
}
/**
* If a listener is registered to be notified of the max-pool-size cap being reached, then record
* what our unbounded size would be and schedule a check to see if this unbounded size is sustained.
*
* Piggy-backs off the existing scheduleResize execution, which now also checks if the listener
* needs to be called.
*/
private void onNewUnboundedPoolSize(final int val) {
if (getMaxSizeReachedSensor() != null) {
recentUnboundedResizes.add(val);
scheduleResize();
}
}
private void abortResize(final int currentSize) {
recentDesiredResizes.add(currentSize);
recentUnboundedResizes.add(currentSize);
}
private boolean isEntityUp() {
if (entity == null) {
return false;
} else if (entity.getEntityType().getSensors().contains(Startable.SERVICE_UP)) {
return Boolean.TRUE.equals(entity.getAttribute(Startable.SERVICE_UP));
} else {
return true;
}
}
private void scheduleResize() {
// TODO Make scale-out calls concurrent, rather than waiting for first resize to entirely
// finish. On ec2 for example, this can cause us to grow very slowly if first request is for
// just one new VM to be provisioned.
if (isRunning() && isEntityUp() && executorQueued.compareAndSet(false, true)) {
long now = System.currentTimeMillis();
long delay = Math.max(0, (executorTime + getMinPeriodBetweenExecs().toMilliseconds()) - now);
if (LOG.isTraceEnabled()) LOG.trace("{} scheduling resize in {}ms", this, delay);
executor.schedule(new Runnable() {
@Override public void run() {
try {
executorTime = System.currentTimeMillis();
executorQueued.set(false);
resizeNow();
notifyMaxReachedIfRequiredNow();
} catch (Exception e) {
if (isRunning()) {
LOG.error("Error resizing: "+e, e);
} else {
if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
}
} catch (Throwable t) {
LOG.error("Error resizing: "+t, t);
throw Throwables.propagate(t);
}
}},
delay,
TimeUnit.MILLISECONDS);
}
}
/**
* Looks at the values for "unbounded pool size" (i.e. if we ignore caps of minSize and maxSize) to report what
* those values have been within a time window. The time window used is the "maxReachedNotificationDelay",
* which determines how many milliseconds after being consistently above the max-size will it take before
* we emit the sensor event (if any).
*/
private void notifyMaxReachedIfRequiredNow() {
BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor = getMaxSizeReachedSensor();
if (maxSizeReachedSensor == null) {
return;
}
WindowSummary valsSummary = recentUnboundedResizes.summarizeWindow(getMaxReachedNotificationDelay());
long timeWindowSize = getMaxReachedNotificationDelay().toMilliseconds();
long currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
int maxAllowedPoolSize = getMaxPoolSize();
long unboundedSustainedMaxPoolSize = valsSummary.min; // The sustained maximum (i.e. the smallest it's dropped down to)
long unboundedCurrentPoolSize = valsSummary.latest;
if (maxReachedLastNotifiedTime > 0) {
// already notified the listener; don't do it again
// TODO Could have max period for notifications, or a step increment to warn when exceeded by ever bigger amounts
} else if (unboundedSustainedMaxPoolSize > maxAllowedPoolSize) {
// We have consistently wanted to be bigger than the max allowed; tell the listener
if (LOG.isDebugEnabled()) LOG.debug("{} notifying listener of max pool size reached; current {}, max {}, unbounded current {}, unbounded max {}",
new Object[] {this, currentPoolSize, maxAllowedPoolSize, unboundedCurrentPoolSize, unboundedSustainedMaxPoolSize});
maxReachedLastNotifiedTime = System.currentTimeMillis();
MaxPoolSizeReachedEvent event = MaxPoolSizeReachedEvent.builder()
.currentPoolSize(currentPoolSize)
.maxAllowed(maxAllowedPoolSize)
.currentUnbounded(unboundedCurrentPoolSize)
.maxUnbounded(unboundedSustainedMaxPoolSize)
.timeWindow(timeWindowSize)
.build();
entity.sensors().emit(maxSizeReachedSensor, event);
} else if (valsSummary.max > maxAllowedPoolSize) {
// We temporarily wanted to be bigger than the max allowed; check back later to see if consistent
// TODO Could check if there has been anything bigger than "min" since min happened (would be more efficient)
if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling max-reached check for {}, as unbounded size not stable (min {}, max {}, latest {})",
new Object[] {this, poolEntity, valsSummary.min, valsSummary.max, valsSummary.latest});
scheduleResize();
} else {
// nothing to write home about; continually below maxAllowed
}
}
private void resizeNow() {
final int currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
CalculatedDesiredPoolSize calculatedDesiredPoolSize = calculateDesiredPoolSize(currentPoolSize);
long desiredPoolSize = calculatedDesiredPoolSize.size;
boolean stable = calculatedDesiredPoolSize.stable;
final int targetPoolSize = applyMinMaxConstraints(desiredPoolSize);
if (!stable) {
// the desired size fluctuations are not stable; ensure we check again later (due to time-window)
// even if no additional events have been received
// (note we continue now with as "good" a resize as we can given the instability)
if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling resize check for {}, as desired size not stable (current {}, desired {}); continuing with resize...",
new Object[] {this, poolEntity, currentPoolSize, targetPoolSize});
scheduleResize();
}
if (currentPoolSize == targetPoolSize) {
if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} to {}",
new Object[] {this, poolEntity, currentPoolSize, targetPoolSize});
return;
}
if (LOG.isDebugEnabled()) LOG.debug("{} requesting resize to {}; current {}, min {}, max {}",
new Object[] {this, targetPoolSize, currentPoolSize, getMinPoolSize(), getMaxPoolSize()});
Entities.submit(entity, Tasks.<Void>builder().displayName("Auto-scaler")
.description("Auto-scaler recommending resize from "+currentPoolSize+" to "+targetPoolSize)
.tag(BrooklynTaskTags.NON_TRANSIENT_TASK_TAG)
.body(new Callable<Void>() {
@Override
public Void call() throws Exception {
// TODO Should we use int throughout, rather than casting here?
try {
getResizeOperator().resize(poolEntity, (int) targetPoolSize);
} catch (Resizable.InsufficientCapacityException e) {
// cannot resize beyond this; set the high-water mark
int insufficientCapacityHighWaterMark = getCurrentSizeOperator().apply(poolEntity);
LOG.warn("{} failed to resize {} due to insufficient capacity; setting high-water mark to {}, "
+ "and will not attempt to resize above that level again",
new Object[] {AutoScalerPolicy.this, poolEntity, insufficientCapacityHighWaterMark});
config().set(INSUFFICIENT_CAPACITY_HIGH_WATER_MARK, insufficientCapacityHighWaterMark);
}
return null;
}
}).build())
.blockUntilEnded();
}
/**
* Complicated logic for stabilization-delay...
* Only grow if we have consistently been asked to grow for the resizeUpStabilizationDelay period;
* Only shrink if we have consistently been asked to shrink for the resizeDownStabilizationDelay period.
*
* @return tuple of desired pool size, and whether this is "stable" (i.e. if we receive no more events
* will this continue to be the desired pool size)
*/
private CalculatedDesiredPoolSize calculateDesiredPoolSize(long currentPoolSize) {
long now = System.currentTimeMillis();
WindowSummary downsizeSummary = recentDesiredResizes.summarizeWindow(getResizeDownStabilizationDelay());
WindowSummary upsizeSummary = recentDesiredResizes.summarizeWindow(getResizeUpStabilizationDelay());
// this is the _sustained_ growth value; the smallest size that has been requested in the "stable-for-growing" period
long maxDesiredPoolSize = upsizeSummary.min;
boolean stableForGrowing = upsizeSummary.stableForGrowth;
// this is the _sustained_ shrink value; largest size that has been requested in the "stable-for-shrinking" period:
long minDesiredPoolSize = downsizeSummary.max;
boolean stableForShrinking = downsizeSummary.stableForShrinking;
// (it is a logical consequence of the above that minDesired >= maxDesired -- this is correct, if confusing:
// think of minDesired as the minimum size we are allowed to resize to, and similarly for maxDesired;
// if min > max we can scale to max if current < max, or scale to min if current > min)
long desiredPoolSize;
boolean stable;
if (currentPoolSize < maxDesiredPoolSize) {
// we have valid request to grow
// (we'll never have a valid request to grow and a valid to shrink simultaneously, btw)
desiredPoolSize = maxDesiredPoolSize;
stable = stableForGrowing;
} else if (currentPoolSize > minDesiredPoolSize) {
// we have valid request to shrink
desiredPoolSize = minDesiredPoolSize;
stable = stableForShrinking;
} else {
desiredPoolSize = currentPoolSize;
stable = stableForGrowing && stableForShrinking;
}
if (LOG.isTraceEnabled()) LOG.trace("{} calculated desired pool size: from {} to {}; minDesired {}, maxDesired {}; " +
"stable {}; now {}; downsizeHistory {}; upsizeHistory {}",
new Object[] {this, currentPoolSize, desiredPoolSize, minDesiredPoolSize, maxDesiredPoolSize, stable, now, downsizeSummary, upsizeSummary});
return new CalculatedDesiredPoolSize(desiredPoolSize, stable);
}
private static class CalculatedDesiredPoolSize {
final long size;
final boolean stable;
CalculatedDesiredPoolSize(long size, boolean stable) {
this.size = size;
this.stable = stable;
}
}
@Override
public String toString() {
return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
}
}