
com.elastisys.scale.cloudpool.commons.basepool.BaseCloudPool Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cloudpool.commons Show documentation
Show all versions of cloudpool.commons Show documentation
Common utility classes for building cloud pools.
package com.elastisys.scale.cloudpool.commons.basepool;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.elastisys.scale.cloudpool.api.CloudPool;
import com.elastisys.scale.cloudpool.api.CloudPoolException;
import com.elastisys.scale.cloudpool.api.NotConfiguredException;
import com.elastisys.scale.cloudpool.api.NotFoundException;
import com.elastisys.scale.cloudpool.api.NotStartedException;
import com.elastisys.scale.cloudpool.api.types.CloudPoolStatus;
import com.elastisys.scale.cloudpool.api.types.MachinePool;
import com.elastisys.scale.cloudpool.api.types.MachineState;
import com.elastisys.scale.cloudpool.api.types.MembershipStatus;
import com.elastisys.scale.cloudpool.api.types.PoolSizeSummary;
import com.elastisys.scale.cloudpool.api.types.ServiceState;
import com.elastisys.scale.cloudpool.commons.basepool.config.BaseCloudPoolConfig;
import com.elastisys.scale.cloudpool.commons.basepool.driver.CloudPoolDriver;
import com.elastisys.scale.cloudpool.commons.basepool.driver.DriverConfig;
import com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.impl.CachingPoolFetcher;
import com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.impl.RetryingPoolFetcher;
import com.elastisys.scale.cloudpool.commons.basepool.poolupdater.PoolUpdater;
import com.elastisys.scale.cloudpool.commons.basepool.poolupdater.impl.StandardPoolUpdater;
import com.elastisys.scale.commons.json.JsonUtils;
import com.elastisys.scale.commons.net.alerter.Alert;
import com.elastisys.scale.commons.net.alerter.Alerter;
import com.elastisys.scale.commons.net.alerter.multiplexing.MultiplexingAlerter;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* A generic {@link CloudPool} that is provided as a basis for building
* cloud-specific {@link CloudPool}s.
*
* The {@link BaseCloudPool} implements sensible behavior for the
* {@link CloudPool} methods and relieves implementors from dealing with the
* details of continuously monitoring and re-scaling the pool with the right
* amount of machines given the member machine states, handling
* (re-)configurations, sending alerts, etc. Implementers of cloud-specific
* {@link CloudPool}s only need to implements a small set of machine pool
* management primitives for a particular cloud. These management primitives are
* supplied to the {@link BaseCloudPool} at construction-time in the form of a
* {@link CloudPoolDriver}, which implements the management primitives according
* to the API of the targeted cloud.
*
* Before use, a {@link BaseCloudPool} needs to be configured with a
* {@link BaseCloudPoolConfig}, which specifies how the {@link BaseCloudPool}:
*
* - should configure its {@link CloudPoolDriver} to identify pool members
* (the {@code name} key). As an example, the {@link CloudPoolDriver} may choose
* to assign a metadata tag with the pool name to each started machine.
* - should configure its {@link CloudPoolDriver} to allow it to communicate
* with its cloud API (the {@code cloudApiSettings} key).
* - should configure its {@link CloudPoolDriver} to provision new instances
* when the pool needs to grow (the {@code provisioningTemplate} key).
* - decommissions instances when the pool needs to shrink (the
* {@code scaleInConfig} key).
* - alerts system administrators (via email) or other systems (via webhooks)
* of interesting events: resize operations, error conditions, etc (the
* {@code alerts} key).
*
* A configuration document has the following structure:
*
*
* {
* "name": "webserver-pool",
*
* "cloudApiSettings": {
* ... cloud provider-specific API access credentials and settings ...
* },
*
* "provisioningTemplate": {
* ... cloud provider-specific provisioning parameters ...
* },
*
* "scaleInConfig": {
* "victimSelectionPolicy": "CLOSEST_TO_INSTANCE_HOUR",
* "instanceHourMargin": 300
* },
*
* "alerts": {
* "duplicateSuppression": { "time": 5, "unit": "minutes" },
* "smtp": [
* {
* "subject": "[elastisys:scale] cloud pool alert for MyScalingPool",
* "recipients": ["[email protected]"],
* "sender": "[email protected]",
* "smtpClientConfig": {
* "smtpHost": "mail.server.com",
* "smtpPort": 465,
* "authentication": {"userName": "john", "password": "secret"},
* "useSsl": True
* }
* }
* ],
* "http": [
* {
* "destinationUrls": ["https://some.host1:443/"],
* "severityFilter": "ERROR|FATAL",
* "auth": {
* "basicCredentials": { "username": "user1", "password": "secret1" }
* }
* },
* {
* "destinationUrls": ["https://some.host2:443/"],
* "severityFilter": "INFO|WARN",
* "auth": {
* "certificateCredentials": { "keystorePath": "src/test/resources/security/client_keystore.p12", "keystorePassword": "secret" }
* }
* }
* ]
* },
*
* "poolFetch": {
* "retries": {
* "maxRetries": 3,
* "initialBackoffDelay": {"time": 3, "unit": "seconds"}
* },
* "refreshInterval": {"time": 30, "unit": "seconds"},
* "reachabilityTimeout": {"time": 5, "unit": "minutes"}
* },
*
* "poolUpdate": {
* "updateInterval": {"time": 1, "unit": "minutes"}
* }
* }
*
*
* The {@link BaseCloudPool} operates according to the {@link CloudPool}
* contract. Some details on how the {@link BaseCloudPool} satisfies the
* contract are summarized below.
*
* Configuration:
*
* When {@link #configure} is called, the {@link BaseCloudPool} expects a JSON
* document with the above structure. From this document a {@link DriverConfig}
* is constructed and passed on to the {@link CloudPoolDriver} via a call to
* {@link CloudPoolDriver#configure}.
*
* Identifying pool members:
*
* Pool members are identified via a call to
* {@link CloudPoolDriver#listMachines()}. How to identify pool members are left
* to the {@link CloudPoolDriver} implementation but could make use of tags (if
* supported by the cloud API).
*
* Handling resize requests:
*
* When {@link #setDesiredSize(int)} is called, the {@link BaseCloudPool} notes
* the new desired size but does not immediately apply the necessary changes to
* the machine pool. Instead, pool updates are carried out in a periodical
* manner (with a period specified by the {@code poolUpdate} configuration key).
*
* When a pool update is triggered, the actions taken depend on if the pool
* needs to grow or shrink.
*
*
* - scale out: start by sparing machines from termination if the
* termination queue is non-empty. For any remaining instances: request them to
* be started by the {@link CloudPoolDriver} via
* {@link CloudPoolDriver#startMachines}.
* - scale in: start by terminating any machines in
* {@link MachineState#REQUESTED} state, since these are likely to not yet incur
* cost. Any such machines are terminated immediately. If additional capacity is
* to be removed, select a victim according to the configured
* {@code victimSelectionPolicy} and schedule it for termination according to
* the configured {@code instanceHourMargin}. Each instance termination is
* delegated to {@link CloudPoolDriver#terminateMachine(String)}.
*
*
* Alerts:
*
* If email and/or HTTP webhook alerts have been configured, the
* {@link BaseCloudPool} will send alerts to notify selected recipients of
* interesting events (such as error conditions, scale-ups/scale-downs, etc).
*
* @see CloudPoolDriver
*/
public class BaseCloudPool implements CloudPool {
/** {@link Logger} instance. */
static final Logger LOG = LoggerFactory.getLogger(BaseCloudPool.class);
/** Declares where the runtime state is stored. */
private final StateStorage stateStorage;
/** A cloud-specific management driver for the cloud pool. */
private CloudPoolDriver cloudDriver = null;
/**
* {@link EventBus} used to post {@link Alert} events that are to be
* forwarded by configured {@link Alerter}s (if any).
*/
private final EventBus eventBus;
/** Used to perform any periodical tasks or background jobs. */
private final ScheduledExecutorService executor;
/** The currently set configuration. */
private BaseCloudPoolConfig config;
/** true
if pool has been started. */
private boolean started;
/**
* Dispatches {@link Alert}s sent on the {@link EventBus} to configured
* {@link Alerter}s.
*/
private final MultiplexingAlerter alerter;
/** Retrieves {@link MachinePool} members. */
private CachingPoolFetcher poolFetcher;
/** Manages the machine pool to keep it at its desired size. */
private PoolUpdater poolUpdater;
/**
* Constructs a new {@link BaseCloudPool} managing a given
* {@link CloudPoolDriver}.
*
* @param stateStorage
* Declares where the runtime state is stored.
* @param cloudDriver
* A cloud-specific management driver for the cloud pool.
* @param executor
* Used to perform any periodical tasks or background jobs.
*/
public BaseCloudPool(StateStorage stateStorage, CloudPoolDriver cloudDriver, ScheduledExecutorService executor) {
this(stateStorage, cloudDriver, executor, new EventBus());
}
/**
* Constructs a new {@link BaseCloudPool} managing a given
* {@link CloudPoolDriver} and using an {@link EventBus} provided by the
* caller.
*
* @param stateStorage
* Declares where the runtime state is stored.
* @param cloudDriver
* A cloud-specific management driver for the cloud pool.
* @param executor
* Used to perform any periodical tasks or background jobs.
* @param eventBus
* The {@link EventBus} used to send {@link Alert}s and event
* messages between components of the cloud pool.
*/
public BaseCloudPool(StateStorage stateStorage, CloudPoolDriver cloudDriver, ScheduledExecutorService executor,
EventBus eventBus) {
checkArgument(stateStorage != null, "no stateStorage given");
checkArgument(cloudDriver != null, "no cloudDriver given");
checkArgument(executor != null, "no executor given");
checkArgument(eventBus != null, "no eventBus given");
this.stateStorage = stateStorage;
this.cloudDriver = cloudDriver;
this.executor = executor;
this.eventBus = eventBus;
this.alerter = new MultiplexingAlerter();
this.eventBus.register(this.alerter);
this.config = null;
this.started = false;
}
@Override
public void configure(JsonObject jsonConfig) throws IllegalArgumentException, CloudPoolException {
BaseCloudPoolConfig configuration = validate(jsonConfig);
synchronized (this) {
boolean wasStarted = isStarted();
if (wasStarted) {
stop();
}
LOG.debug("setting new configuration: {}", JsonUtils.toPrettyString(jsonConfig));
// re-configure driver
DriverConfig driverConfig = new DriverConfig(configuration.getName(), configuration.getCloudApiSettings(),
configuration.getProvisioningTemplate());
this.cloudDriver.configure(driverConfig);
// set configuration only it it was successfully set on driver
this.config = configuration;
// alerters may have changed
this.alerter.unregisterAlerters();
this.alerter.registerAlerters(config().getAlerts(), standardAlertMetadata());
if (wasStarted) {
start();
}
}
}
private BaseCloudPoolConfig validate(JsonObject jsonConfig) throws IllegalArgumentException {
try {
BaseCloudPoolConfig configuration = JsonUtils.toObject(jsonConfig, BaseCloudPoolConfig.class);
configuration.validate();
return configuration;
} catch (Exception e) {
Throwables.propagateIfInstanceOf(e, IllegalArgumentException.class);
throw new IllegalArgumentException("failed to validate cloud pool configuration: " + e.getMessage(), e);
}
}
@Override
public Optional getConfiguration() {
if (this.config == null) {
return Optional.absent();
}
return Optional.of(JsonUtils.toJson(this.config).getAsJsonObject());
}
@Override
public void start() throws NotConfiguredException {
ensureConfigured();
if (isStarted()) {
return;
}
LOG.info("starting {} driving a {}", getClass().getSimpleName(), this.cloudDriver.getClass().getSimpleName());
RetryingPoolFetcher retryingFetcher = new RetryingPoolFetcher(this.cloudDriver,
config().getPoolFetch().getRetries());
// note: we wait for first attempt to get the pool to complete
this.poolFetcher = new CachingPoolFetcher(this.stateStorage, retryingFetcher, config().getPoolFetch(),
this.executor, this.eventBus);
this.poolFetcher.awaitFirstFetch();
this.poolUpdater = new StandardPoolUpdater(this.cloudDriver, this.poolFetcher, this.executor, this.eventBus,
config());
this.started = true;
LOG.info(getClass().getSimpleName() + " started.");
}
@Override
public void stop() {
if (isStarted()) {
LOG.debug("stopping {} ...", getClass().getSimpleName());
// cancel tasks (allow any running tasks to finish)
this.poolFetcher.close();
this.poolUpdater.close();
this.started = false;
}
LOG.info(getClass().getSimpleName() + " stopped.");
}
@Override
public CloudPoolStatus getStatus() {
return new CloudPoolStatus(isStarted(), isConfigured());
}
private boolean isConfigured() {
return getConfiguration().isPresent();
}
/**
* Checks that a configuration has been set for the {@link CloudPool} or
* throws a {@link NotConfiguredException}.
*/
private void ensureConfigured() throws NotConfiguredException {
if (!isConfigured()) {
throw new NotConfiguredException("cloud pool is not configured");
}
}
boolean isStarted() {
return this.started;
}
@Override
public MachinePool getMachinePool() throws CloudPoolException {
ensureStarted();
return this.poolFetcher.get();
}
/**
* Ensures that the {@link CloudPool} has been started or otherwise throws a
* {@link NotStartedException}.
*/
private void ensureStarted() throws NotStartedException {
if (!isStarted()) {
throw new NotStartedException("attempt to use cloud pool that is stopped");
}
}
@Override
public PoolSizeSummary getPoolSize() throws CloudPoolException {
ensureStarted();
MachinePool pool = this.poolFetcher.get();
return new PoolSizeSummary(pool.getTimestamp(), this.poolUpdater.getDesiredSize(),
pool.getAllocatedMachines().size(), pool.getActiveMachines().size());
}
@Override
public void setDesiredSize(int desiredSize) throws IllegalArgumentException, CloudPoolException {
ensureStarted();
this.poolUpdater.setDesiredSize(desiredSize);
}
@Override
public void terminateMachine(String machineId, boolean decrementDesiredSize)
throws IllegalArgumentException, CloudPoolException {
ensureStarted();
this.poolUpdater.terminateMachine(machineId, decrementDesiredSize);
}
@Override
public void attachMachine(String machineId) throws IllegalArgumentException, CloudPoolException {
ensureStarted();
this.poolUpdater.attachMachine(machineId);
}
@Override
public void detachMachine(String machineId, boolean decrementDesiredSize)
throws IllegalArgumentException, CloudPoolException {
ensureStarted();
this.poolUpdater.detachMachine(machineId, decrementDesiredSize);
}
@Override
public void setServiceState(String machineId, ServiceState serviceState) throws IllegalArgumentException {
ensureStarted();
this.poolUpdater.setServiceState(machineId, serviceState);
}
@Override
public void setMembershipStatus(String machineId, MembershipStatus membershipStatus)
throws NotFoundException, CloudPoolException {
ensureStarted();
this.poolUpdater.setMembershipStatus(machineId, membershipStatus);
}
/**
* Standard tags that are to be included in all sent out {@link Alert}s (in
* addition to those already set on the {@link Alert} itself).
*
* @return
*/
private Map standardAlertMetadata() {
Map standardTags = Maps.newHashMap();
standardTags.put("cloudPoolName", JsonUtils.toJson(config().getName()));
return standardTags;
}
BaseCloudPoolConfig config() {
return this.config;
}
void updateMachinePool() {
this.poolUpdater.resize(config());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy