/*
* 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.core.mgmt.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.Closeable;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.ProvisioningLocation;
import org.apache.brooklyn.api.mgmt.AccessController;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynLogging;
import org.apache.brooklyn.core.BrooklynLogging.LoggingLevel;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.internal.storage.BrooklynStorage;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.core.objs.proxy.InternalLocationFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
import org.apache.brooklyn.util.stream.Streams;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
public class LocalLocationManager implements LocationManagerInternal {
@Beta /* expect to remove when API returns LocationSpec or similar */
public static final ConfigKey<Boolean> CREATE_UNMANAGED = ConfigKeys.newBooleanConfigKey("brooklyn.internal.location.createUnmanaged",
"If set on a location or spec, causes the manager to create it in an unmanaged state (for peeking)", false);
private static final Logger log = LoggerFactory.getLogger(LocalLocationManager.class);
private final LocalManagementContext managementContext;
private final InternalLocationFactory locationFactory;
protected final Map<String,Location> locationsById = Maps.newLinkedHashMap();
private final Map<String, Location> preRegisteredLocationsById = Maps.newLinkedHashMap();
/** Management mode for each location */
protected final Map<String,ManagementTransitionMode> locationModesById = Maps.newLinkedHashMap();
private final BrooklynStorage storage;
private Map<String, String> locationTypes;
private static AtomicLong LOCATION_CNT = new AtomicLong(0);
public LocalLocationManager(LocalManagementContext managementContext) {
this.managementContext = checkNotNull(managementContext, "managementContext");
this.locationFactory = new InternalLocationFactory(managementContext);
this.storage = managementContext.getStorage();
locationTypes = storage.getMap("locations");
}
public InternalLocationFactory getLocationFactory() {
if (!isRunning()) throw new IllegalStateException("Management context no longer running");
return locationFactory;
}
@Override
public <T extends Location> T createLocation(LocationSpec<T> spec) {
try {
boolean createUnmanaged = ConfigBag.coerceFirstNonNullKeyValue(CREATE_UNMANAGED,
spec.getConfig().get(CREATE_UNMANAGED), spec.getFlags().get(CREATE_UNMANAGED.getName()));
if (createUnmanaged) {
spec.removeConfig(CREATE_UNMANAGED);
}
T loc = locationFactory.createLocation(spec);
if (!createUnmanaged) {
manage(loc);
} else {
// remove references
Location parent = loc.getParent();
if (parent!=null) {
((AbstractLocation)parent).removeChild(loc);
}
preRegisteredLocationsById.remove(loc.getId());
}
return loc;
} catch (Throwable e) {
log.warn("Failed to create location using spec "+spec+" (rethrowing)", e);
throw Exceptions.propagate(e);
}
}
@Override
public <T extends Location> T createLocation(Map<?,?> config, Class<T> type) {
return createLocation(LocationSpec.create(config, type));
}
@Override
public synchronized Collection<Location> getLocations() {
return ImmutableList.copyOf(locationsById.values());
}
@Override
public Collection<String> getLocationIds() {
return ImmutableList.copyOf(locationsById.keySet());
}
@Override
public synchronized Location getLocation(String id) {
return locationsById.get(id);
}
public synchronized Location getLocationEvenIfPreManaged(String id) {
Location result = locationsById.get(id);
if (result == null) {
result = preRegisteredLocationsById.get(id);
}
return result;
}
@Override
public boolean isManaged(Location loc) {
return (isRunning() && loc != null && getLocation(loc.getId()) != null);
}
synchronized boolean isPreRegistered(Location loc) {
return preRegisteredLocationsById.containsKey(loc.getId());
}
public boolean isKnownLocationId(String id) {
return preRegisteredLocationsById.containsKey(id) || locationsById.containsKey(id);
}
synchronized void prePreManage(Location loc) {
if (isPreRegistered(loc)) {
log.warn(""+this+" redundant call to pre-pre-manage location "+loc+"; skipping",
new Exception("source of duplicate pre-pre-manage of "+loc));
return;
}
preRegisteredLocationsById.put(loc.getId(), loc);
}
@Override
public ManagementTransitionMode getLastManagementTransitionMode(String itemId) {
return locationModesById.get(itemId);
}
@Override
public void setManagementTransitionMode(Location item, ManagementTransitionMode mode) {
locationModesById.put(item.getId(), mode);
}
// TODO synchronization issues here: see comment in LocalEntityManager.manage(Entity)
/** management on creation */
@Override
public Location manage(Location loc) {
if (isManaged(loc)) {
// TODO put log.warn back in if/when manage(Location) becomes private; or could even have assert.
// Can be stricter about contract.
return loc;
}
Location parent = loc.getParent();
if (parent != null && !managementContext.getLocationManager().isManaged(parent)) {
log.warn("Parent location "+parent+" of "+loc+" is not managed; attempting to manage it (in future this may be disallowed)");
return manage(parent);
} else {
return manageRecursive(loc, ManagementTransitionMode.guessing(BrooklynObjectManagementMode.NONEXISTENT, BrooklynObjectManagementMode.MANAGED_PRIMARY));
}
}
@Override
public void manageRebindedRoot(Location item) {
ManagementTransitionMode mode = getLastManagementTransitionMode(item.getId());
Preconditions.checkNotNull(mode, "Mode not set for rebinding %s", item);
manageRecursive(item, mode);
}
protected void checkManagementAllowed(Location item) {
AccessController.Response access = managementContext.getAccessController().canManageLocation(item);
if (!access.isAllowed()) {
throw new IllegalStateException("Access controller forbids management of "+item+": "+access.getMsg());
}
}
protected Location manageRecursive(Location loc, final ManagementTransitionMode initialMode) {
// TODO see comments in LocalEntityManager about recursive management / manageRebindRoot v manageAll
AccessController.Response access = managementContext.getAccessController().canManageLocation(loc);
if (!access.isAllowed()) {
throw new IllegalStateException("Access controller forbids management of "+loc+": "+access.getMsg());
}
long count = LOCATION_CNT.incrementAndGet();
if (log.isDebugEnabled()) {
String msg = "Managing location " + loc + " ("+initialMode+"), from " + Tasks.current()+" / "+Entitlements.getEntitlementContext();
LoggingLevel level = (!initialMode.wasNotLoaded() || initialMode.isReadOnly() ? LoggingLevel.TRACE : LoggingLevel.DEBUG);
if (count % 100 == 0) {
// include trace periodically in case we get leaks or too much location management
BrooklynLogging.log(log, level,
msg, new Exception("Informational stack trace of call to manage location "+loc+" ("+count+" calls; "+getLocations().size()+" currently managed)"));
} else {
BrooklynLogging.log(log, level, msg);
}
}
recursively(loc, new Predicate<AbstractLocation>() { public boolean apply(AbstractLocation it) {
ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
if (mode==null) {
setManagementTransitionMode(it, mode = initialMode);
}
if (it.isManaged()) {
if (mode.wasNotLoaded()) {
// silently bail out
return false;
} else {
// on rebind, we just replace, fall through to below
}
}
boolean result = manageNonRecursive(it, mode);
if (result) {
it.setManagementContext(managementContext);
if (mode.isPrimary()) {
it.onManagementStarted();
if (mode.isCreating()) {
// Never record event on rebind; this isn't the location (e.g. the VM) being "created"
// so don't tell listeners that.
// TODO The location-event history should be persisted; currently it is lost on
// rebind, unless there is a listener that is persisting the state externally itself.
recordLocationEvent(it, Lifecycle.CREATED);
}
}
managementContext.getRebindManager().getChangeListener().onManaged(it);
}
return result;
} });
return loc;
}
@Override
public void unmanage(final Location loc) {
unmanage(loc, ManagementTransitionMode.guessing(BrooklynObjectManagementMode.MANAGED_PRIMARY, BrooklynObjectManagementMode.NONEXISTENT));
}
public void unmanage(final Location loc, final ManagementTransitionMode mode) {
unmanage(loc, mode, false);
}
private void unmanage(final Location loc, final ManagementTransitionMode mode, boolean hasBeenReplaced) {
if (shouldSkipUnmanagement(loc)) return;
if (hasBeenReplaced) {
// we are unmanaging an old instance after having replaced it;
// don't unmanage or even clear its fields, because there might be references to it
if (mode.wasReadOnly()) {
// if coming *from* read only; nothing needed
} else {
if (!mode.wasPrimary()) {
log.warn("Unexpected mode "+mode+" for unmanage-replace "+loc+" (applying anyway)");
}
// migrating away or in-place active partial rebind:
managementContext.getRebindManager().getChangeListener().onUnmanaged(loc);
if (managementContext.gc != null) managementContext.gc.onUnmanaged(loc);
}
// do not remove from maps below, bail out now
return;
} else if ((mode.wasPrimary() && mode.isReadOnly()) || (mode.wasReadOnly() && mode.isNoLongerLoaded())) {
if (mode.isReadOnly() && mode.wasPrimary()) {
// TODO shouldn't this fall into "hasBeenReplaced" above?
log.debug("Unmanaging on demotion: "+loc+" ("+mode+")");
}
// we are unmanaging an instance whose primary management is elsewhere (either we were secondary, or we are being demoted)
unmanageNonRecursiveRemoveFromRecords(loc, mode);
managementContext.getRebindManager().getChangeListener().onUnmanaged(loc);
if (managementContext.gc != null) managementContext.gc.onUnmanaged(loc);
unmanageNonRecursiveClearItsFields(loc, mode);
} else if (mode.isNoLongerLoaded()) {
// Need to store all child entities as onManagementStopping removes a child from the parent entity
// As above, see TODO in LocalEntityManager about recursive management / unmanagement v manageAll/unmanageAll
recursively(loc, new Predicate<AbstractLocation>() { public boolean apply(AbstractLocation it) {
if (shouldSkipUnmanagement(it)) return false;
boolean result = unmanageNonRecursiveRemoveFromRecords(it, mode);
if (result) {
ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
if (mode==null) {
// ad hoc creation e.g. tests
log.debug("Missing transition mode for "+it+" when unmanaging; assuming primary/destroying");
mode = ManagementTransitionMode.guessing(BrooklynObjectManagementMode.MANAGED_PRIMARY, BrooklynObjectManagementMode.NONEXISTENT);
}
if (mode.wasPrimary()) it.onManagementStopped();
managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
if (mode.isDestroying()) recordLocationEvent(it, Lifecycle.DESTROYED);
if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
}
unmanageNonRecursiveClearItsFields(loc, mode);
return result;
} });
} else {
log.warn("Invalid mode for unmanage: "+mode+" on "+loc+" (ignoring)");
}
if (loc instanceof Closeable) {
Streams.closeQuietly( (Closeable)loc );
}
locationsById.remove(loc.getId());
preRegisteredLocationsById.remove(loc.getId());
locationModesById.remove(loc.getId());
locationTypes.remove(loc.getId());
}
/**
* Adds this location event to the usage record for the given location (creating the usage
* record if one does not already exist).
*/
private void recordLocationEvent(LocationInternal loc, Lifecycle state) {
try {
managementContext.getUsageManager().recordLocationEvent(loc, state);
} catch (RuntimeInterruptedException e) {
throw e;
} catch (RuntimeException e) {
log.warn("Failed to store location lifecycle event for "+loc+" (ignoring)", e);
}
}
private void recursively(Location e, Predicate<AbstractLocation> action) {
boolean success = action.apply( (AbstractLocation)e );
if (!success) {
return; // Don't manage children if action false/unnecessary for parent
}
for (Location child : e.getChildren()) {
recursively(child, action);
}
}
/**
* Should ensure that the location is now managed somewhere, and known about in all the lists.
* Returns true if the location has now become managed; false if it was already managed (anything else throws exception)
* @param rebindPrimary true if rebinding primary, false if rebinding as copy, null if creating (not rebinding)
*/
private synchronized boolean manageNonRecursive(Location loc, ManagementTransitionMode mode) {
Location old = locationsById.put(loc.getId(), loc);
preRegisteredLocationsById.remove(loc.getId());
locationTypes.put(loc.getId(), loc.getClass().getName());
if (old!=null && mode.wasNotLoaded()) {
if (old.equals(loc)) {
log.warn("{} redundant call to start management of location {}", this, loc);
} else {
throw new IllegalStateException("call to manage location "+loc+" but different location "+old+" already known under that id at "+this);
}
return false;
}
if (old!=null && old!=loc) {
// passing the transition info will ensure the right shutdown steps invoked for old instance
unmanage(old, mode, true);
}
return true;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private synchronized void unmanageNonRecursiveClearItsFields(Location loc, ManagementTransitionMode mode) {
if (mode.isDestroying()) {
((AbstractLocation)loc).setParent(null, true);
Location parent = ((AbstractLocation)loc).getParent();
if (parent instanceof ProvisioningLocation<?>) {
try {
((ProvisioningLocation)parent).release(loc);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
log.debug("Error releasing "+loc+" in its parent "+parent+": "+e);
}
}
} else {
// if not destroying, don't change the parent's children list
((AbstractLocation)loc).setParent(null, false);
}
// clear config to help with GC; i know you're not supposed to, but this seems to help, else config bag is littered with refs to entities etc
// FIXME relies on config().getLocalBag() returning the underlying bag!
((AbstractLocation)loc).config().getLocalBag().clear();
}
/**
* Should ensure that the location is no longer managed anywhere, remove from all lists.
* Returns true if the location has been removed from management; if it was not previously managed (anything else throws exception)
*/
private synchronized boolean unmanageNonRecursiveRemoveFromRecords(Location loc, ManagementTransitionMode mode) {
Object old = locationsById.remove(loc.getId());
locationTypes.remove(loc.getId());
locationModesById.remove(loc.getId());
if (old==null) {
log.warn("{} call to stop management of unknown location (already unmanaged?) {}; ignoring", this, loc);
return false;
} else if (!old.equals(loc)) {
// shouldn't happen...
log.error("{} call to stop management of location {} removed different location {}; ignoring", new Object[] { this, loc, old });
return true;
} else {
if (log.isDebugEnabled()) log.debug("{} stopped management of location {}", this, loc);
return true;
}
}
private boolean shouldSkipUnmanagement(Location loc) {
if (loc==null) {
log.warn(""+this+" call to unmanage null location; skipping",
new IllegalStateException("source of null unmanagement call to "+this));
return true;
}
if (!isManaged(loc)) {
log.warn("{} call to stop management of unknown location (already unmanaged?) {}; skipping, and all descendants", this, loc);
return true;
}
return false;
}
private boolean isRunning() {
return managementContext.isRunning();
}
}