/*
* 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.entity.openshift;
import com.google.common.base.Functions;
import com.google.common.collect.Iterables;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
import org.apache.brooklyn.api.entity.drivers.EntityDriverManager;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.sensor.EnricherSpec;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.enricher.AbstractEnricher;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.feed.function.FunctionPollConfig;
import org.apache.brooklyn.location.openshift.OpenShiftPaasLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
public abstract class OpenShiftEntityImpl extends AbstractEntity implements OpenShiftEntity,
DriverDependentEntity {
private static final Logger log = LoggerFactory.getLogger(OpenShiftEntityImpl.class);
private transient PaasEntityDriver driver;
/**
* @see #connectServiceUpIsRunning()
*/
private volatile FunctionFeed serviceProcessIsRunning;
protected boolean connectedSensors = false;
public OpenShiftEntityImpl() {
super(MutableMap.of(), null);
}
public OpenShiftEntityImpl(Entity parent) {
this(MutableMap.of(), parent);
}
public OpenShiftEntityImpl(Map properties) {
this(properties, null);
}
public OpenShiftEntityImpl(Map properties, Entity parent) {
super(properties, parent);
}
@Override
public abstract Class getDriverInterface();
@Override
public PaasEntityDriver getDriver() {
return driver;
}
protected OpenShiftPaasLocation getLocationOrNull() {
return Iterables.get(Iterables
.filter(getLocations(), OpenShiftPaasLocation.class), 0, null);
}
@Override
public void init() {
super.init();
}
@Override
protected void initEnrichers() {
super.initEnrichers();
ServiceStateLogic.ServiceNotUpLogic
.updateNotUpIndicator(this, SERVICE_PROCESS_IS_RUNNING,
"No information yet on whether this service is running");
// add an indicator above so that if is_running comes through, the map is cleared and
// an update is guaranteed
addEnricher(EnricherSpec.create(UpdatingNotUpFromServiceProcessIsRunning.class)
.uniqueTag("service-process-is-running-updating-not-up"));
}
/**
* This sub-class was copied directly from {@link brooklyn.entity.basic.SoftwareProcessImpl}
* subscribes to SERVICE_PROCESS_IS_RUNNING and SERVICE_UP; the latter has no effect if
* the former is set, but to support entities which set SERVICE_UP directly we want
* to make sure that the absence of SERVICE_PROCESS_IS_RUNNING does not trigger any
* not-up indicators
*/
protected static class UpdatingNotUpFromServiceProcessIsRunning extends AbstractEnricher
implements SensorEventListener<Object> {
public UpdatingNotUpFromServiceProcessIsRunning() {
}
@Override
public void setEntity(EntityLocal entity) {
super.setEntity(entity);
subscribe(entity, SERVICE_PROCESS_IS_RUNNING, this);
subscribe(entity, Attributes.SERVICE_UP, this);
onUpdated();
}
@Override
public void onEvent(SensorEvent<Object> event) {
onUpdated();
}
protected void onUpdated() {
Boolean isRunning = entity.getAttribute(SERVICE_PROCESS_IS_RUNNING);
if (Boolean.FALSE.equals(isRunning)) {
ServiceStateLogic.ServiceNotUpLogic
.updateNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING,
"The software process for this entity does not appear to be running");
return;
}
if (Boolean.TRUE.equals(isRunning)) {
ServiceStateLogic.ServiceNotUpLogic
.clearNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING);
return;
}
// no info on "isRunning"
Boolean isUp = entity.getAttribute(Attributes.SERVICE_UP);
if (Boolean.TRUE.equals(isUp)) {
// if service explicitly set up, then don't apply our rule
ServiceStateLogic.ServiceNotUpLogic
.clearNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING);
return;
}
// service not up, or no info
ServiceStateLogic.ServiceNotUpLogic
.updateNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING,
"No information on whether this service is running");
}
}
/**
* If custom behaviour is required by sub-classes, consider overriding
* {@link #doStart(java.util.Collection)})}.
*/
@Override
public final void start(final Collection<? extends Location> locations) {
if (DynamicTasks.getTaskQueuingContext() != null) {
doStart(locations);
} else {
Task<?> task = Tasks.builder().name("start (sequential)").body(new Runnable() {
public void run() {
doStart(locations);
}
}).build();
Entities.submit(this, task).getUnchecked();
}
}
/**
* It is a temporal method. It will be contains the start effector body. It will be
* moved to LifeCycle class based on Task. It is the first approach.
* It does not start the entity children.
*/
protected final void doStart(Collection<? extends Location> locations) {
ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
try {
preStart();
driver.start();
log.info("Entity {} was started with driver {}", new Object[]{this, driver});
postDriverStart();
connectSensors();
postStart();
ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
} catch (Throwable t) {
ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
log.error("Error error starting entity {}", this);
throw Exceptions.propagate(t);
}
}
/**
* Called before driver.start; guarantees the driver will exist, and locations will
* have been set.
*/
protected void preStart() {
createDriver();
}
/**
* Create the driver ensuring that the location is ready.
*/
private void createDriver() {
OpenShiftPaasLocation location = getLocationOrNull();
if (location != null) {
this.initDriver(location);
} else {
throw new ExceptionInInitializerError("Location should not be null in " + this +
" the driver needs a initialized Location");
}
}
protected void initDriver(OpenShiftPaasLocation location) {
PaasEntityDriver newDriver = doInitDriver(location);
if (newDriver == null) {
throw new UnsupportedOperationException("cannot start " + this + " on " + location +
": no driver available");
}
driver = newDriver;
}
/**
* Creates the driver (if does not already exist or needs replaced for some reason).
* Returns either the existing driver
* or a new driver. Must not return null.
*/
protected PaasEntityDriver doInitDriver(OpenShiftPaasLocation location) {
if (driver != null) {
if (location.equals(driver.getLocation())) {
return driver; //just reuse
} else {
log.warn("driver/location change is untested for {} at {}; " +
"changing driver and continuing", this, location);
return newDriver(location);
}
} else {
return newDriver(location);
}
}
protected PaasEntityDriver newDriver(OpenShiftPaasLocation loc) {
EntityDriverManager entityDriverManager = getManagementContext().getEntityDriverManager();
return (PaasEntityDriver) entityDriverManager.build(this, loc);
}
private void postDriverStart() {
waitForEntityStart();
}
private void postStart() {
waitForServiceUp();
}
public void waitForServiceUp() {
Duration timeout = getConfig(BrooklynConfigKeys.START_TIMEOUT);
waitForServiceUp(timeout);
}
public void waitForServiceUp(Duration duration) {
Entities.waitForServiceUp(this, duration);
}
/**
* Copied direcly from {@link brooklyn.entity.basic.SoftwareProcessImpl}
*/
// TODO Find a better way to detect early death of process.
public void waitForEntityStart() {
if (log.isDebugEnabled()) {
log.debug("waiting to ensure {} doesn't abort prematurely", this);
}
Duration startTimeout = getConfig(START_TIMEOUT);
CountdownTimer timer = startTimeout.countdownTimer();
boolean isRunningResult = false;
long delay = 100;
while (!isRunningResult && !timer.isExpired()) {
Time.sleep(delay);
try {
isRunningResult = driver.isRunning();
} catch (Exception e) {
ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
// provide extra context info, as we're seeing this happen in strange circumstances
if (driver == null) {
throw new IllegalStateException(this +
" concurrent start and shutdown detected");
}
throw new IllegalStateException("Error detecting whether " + this +
" is running: " + e, e);
}
if (log.isDebugEnabled()) {
log.debug("checked {}, is running returned: {}", this, isRunningResult);
}
// slow exponential delay -- 1.1^N means after 40 tries and 50s elapsed, it reaches
// the max of 5s intervals
// TODO use Repeater
delay = Math.min(delay * 11 / 10, 5000);
}
if (!isRunningResult) {
String msg = "Software process entity " + this + " did not pass is-running " +
"check within the required " + startTimeout + " limit (" +
timer.getDurationElapsed().toStringRounded() + " elapsed)";
log.warn(msg + " (throwing)");
ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
throw new IllegalStateException(msg);
}
}
/**
* For binding to the running service (e.g. connecting sensors to registry). Will be called
* on start() and on rebind().
* <p/>
* Implementations should be idempotent (ie tell whether sensors already connected),
* though the framework is pretty good about not calling when already connected.
*/
protected void connectSensors() {
connectedSensors = true;
connectServiceUpIsRunning();
}
/**
* For connecting the {@link #SERVICE_UP} sensor to the value of the
* {@code getDriver().isRunning()} expression.
* <p/>
* Should be called inside {@link #connectSensors()}.
*
* @see #disconnectServiceUpIsRunning()
*/
protected void connectServiceUpIsRunning() {
serviceProcessIsRunning = FunctionFeed.builder()
.entity(this)
.period(Duration.FIVE_SECONDS)
.poll(new FunctionPollConfig<Boolean, Boolean>(SERVICE_PROCESS_IS_RUNNING)
.onException(Functions.constant(Boolean.FALSE))
.callable(new Callable<Boolean>() {
public Boolean call() {
return getDriver().isRunning();
}
}))
.build();
}
/**
* For disconnecting from the running service. Will be called on stop.
*/
protected void disconnectSensors() {
connectedSensors = false;
disconnectServiceUpIsRunning();
}
/**
* For disconnecting the {@link #SERVICE_UP} feed.
* <p/>
* Should be called from {@link #disconnectSensors()}.
*
* @see #connectServiceUpIsRunning()
*/
protected void disconnectServiceUpIsRunning() {
if (serviceProcessIsRunning != null) serviceProcessIsRunning.stop();
// set null so the SERVICE_UP enricher runs (possibly removing it), then remove so
// everything is removed
// TODO race because the is-running check may be mid-task
setAttribute(SERVICE_PROCESS_IS_RUNNING, null);
removeAttribute(SERVICE_PROCESS_IS_RUNNING);
}
/**
* If custom behaviour is required by sub-classes, consider overriding {@link #doStop()}.
*/
@Override
public final void stop() {
if (DynamicTasks.getTaskQueuingContext() != null) {
doStop();
} else {
Task<?> task = Tasks.builder().name("stop").body(new Runnable() {
public void run() {
doStop();
}
}).build();
Entities.submit(this, task).getUnchecked();
}
}
/**
* To be overridden instead of {@link #stop()}; sub-classes should call {@code super.doStop()}
* and should add do additional work via tasks, executed using
* {@link DynamicTasks#queue(String, java.util.concurrent.Callable)}.
*/
protected final void doStop() {
log.info("Stopping {} in {}", new Object[]{this, getLocationOrNull()});
if (getAttribute(SERVICE_STATE_ACTUAL)
.equals(Lifecycle.STOPPED)) {
log.warn("The entity {} is already stopped", new Object[]{this});
return;
}
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
try {
preStop();
driver.stop();
postDriverStop();
postStop();
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
log.info("The entity stop operation {} is completed without errors",
new Object[]{this});
} catch (Throwable t) {
ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
throw Exceptions.propagate(t);
}
}
protected void preStop() {
setAttribute(SERVICE_UP, false);
disconnectSensors();
}
protected void postDriverStop() {}
protected void postStop() {
}
/**
* If custom behaviour is required by sub-classes, consider overriding {@link #doRestart()}.
*/
@Override
public final void restart() {
if (DynamicTasks.getTaskQueuingContext() != null) {
doRestart(ConfigBag.EMPTY);
} else {
Task<?> task = Tasks.builder().name("restart").body(new Runnable() {
public void run() {
doRestart(ConfigBag.EMPTY);
}
}).build();
Entities.submit(this, task).getUnchecked();
}
}
/**
* To be overridden instead of {@link #restart()}; sub-classes should call
* {@code super.doRestart(ConfigBag)} and should add do additional work via tasks,
* executed using {@link DynamicTasks#queue(String, java.util.concurrent.Callable)}.
*/
protected final void doRestart(ConfigBag parameters) {
}
protected final void doRestart() {
doRestart(ConfigBag.EMPTY);
}
@Override
public void onManagementStarting() {
super.onManagementStarting();
Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
if (state == null || state == Lifecycle.CREATED) {
// Expect this is a normal start() sequence (i.e. start() will subsequently be called)
setAttribute(SERVICE_UP, false);
ServiceStateLogic.setExpectedState(this, Lifecycle.CREATED);
// force actual to be created because this is expected subsequently
setAttribute(SERVICE_STATE_ACTUAL, Lifecycle.CREATED);
}
}
@Override
public void onManagementStarted() {
super.onManagementStarted();
Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
if (state != null && state != Lifecycle.CREATED) {
postRebind();
}
}
/**
* Called after this entity is fully rebound (i.e. it is fully managed).
*/
protected void postRebind() {
}
@Override
public void rebind() {
Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
if (state == null || state != Lifecycle.RUNNING) {
log.warn("On rebind of {}, not calling software process rebind hooks because " +
"state is {}", this, state);
return;
}
// e.g. rebinding to a running instance
// FIXME For rebind, what to do about things in STARTING or STOPPING state?
// FIXME What if location not set?
log.info("Rebind {} connecting to pre-running service", this);
OpenShiftPaasLocation location = getLocationOrNull();
if (location != null) {
initDriver(location);
driver.rebind();
if (log.isDebugEnabled())
log.debug("On rebind of {}, re-created driver {}", this, driver);
} else {
log.info("On rebind of {}, no MachineLocation found (with locations {}) " +
"so not generating driver",
this, getLocations());
}
callRebindHooks();
}
protected void callRebindHooks() {
Duration configuredMaxDelay = getConfig(MAXIMUM_REBIND_SENSOR_CONNECT_DELAY);
if (configuredMaxDelay == null || Duration.ZERO.equals(configuredMaxDelay)) {
connectSensors();
} else {
long delay = (long) (Math.random() * configuredMaxDelay.toMilliseconds());
log.debug("Scheduled reconnection of sensors on {} in {}ms", this, delay);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
if (getManagementSupport().isNoLongerManaged()) {
log.debug("Entity {} no longer managed; ignoring scheduled connect " +
"sensors on rebind", OpenShiftEntityImpl.this);
return;
}
connectSensors();
} catch (Throwable e) {
log.warn("Problem connecting sensors on rebind of " +
OpenShiftEntityImpl.this, e);
Exceptions.propagateIfFatal(e);
}
}
}, delay);
}
}
@Override
public void destroy() {
super.destroy();
disconnectSensors();
}
}