/* * 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.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.brooklyn.api.entity.Application; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.entitlement.EntitlementContext; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityInternal; 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.LocationConfigKeys; import org.apache.brooklyn.core.location.internal.LocationInternal; import org.apache.brooklyn.core.mgmt.ManagementContextInjectable; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage; import org.apache.brooklyn.core.mgmt.usage.LocationUsage; import org.apache.brooklyn.core.mgmt.usage.UsageListener; import org.apache.brooklyn.core.mgmt.usage.UsageManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.time.Duration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; public class LocalUsageManager implements UsageManager { // TODO Threading model needs revisited. // Synchronizes on updates to storage; but if two Brooklyn nodes were both writing to the same // ApplicationUsage or LocationUsage record there'd be a race. That currently won't happen // (at least for ApplicationUsage?) because the app is mastered in just one node at a time, // and because location events are just manage/unmanage which should be happening in just // one place at a time for a given location. private static final Logger log = LoggerFactory.getLogger(LocalUsageManager.class); private static class ApplicationMetadataImpl implements UsageListener.ApplicationMetadata { private final Application app; private String applicationId; private String applicationName; private String entityType; private String catalogItemId; private Map<String, String> metadata; ApplicationMetadataImpl(Application app) { this.app = checkNotNull(app, "app"); applicationId = app.getId(); applicationName = app.getDisplayName(); entityType = app.getEntityType().getName(); catalogItemId = app.getCatalogItemId(); metadata = ((EntityInternal)app).toMetadataRecord(); } @Override public Application getApplication() { return app; } @Override public String getApplicationId() { return applicationId; } @Override public String getApplicationName() { return applicationName; } @Override public String getEntityType() { return entityType; } @Override public String getCatalogItemId() { return catalogItemId; } @Override public Map<String, String> getMetadata() { return metadata; } } private static class LocationMetadataImpl implements UsageListener.LocationMetadata { private final Location loc; private String locationId; private Map<String, String> metadata; LocationMetadataImpl(Location loc) { this.loc = checkNotNull(loc, "loc"); locationId = loc.getId(); metadata = ((LocationInternal)loc).toMetadataRecord(); } @Override public Location getLocation() { return loc; } @Override public String getLocationId() { return locationId; } @Override public Map<String, String> getMetadata() { return metadata; } } // Register a coercion from String->UsageListener, so that USAGE_LISTENERS defined in brooklyn.properties // will be instantiated, given their class names. static { TypeCoercions.registerAdapter(String.class, UsageListener.class, new Function<String, UsageListener>() { @Override public UsageListener apply(String input) { // TODO Want to use classLoader = mgmt.getCatalog().getRootClassLoader(); ClassLoader classLoader = LocalUsageManager.class.getClassLoader(); Optional<Object> result = Reflections.invokeConstructorWithArgs(classLoader, input); if (result.isPresent()) { return (UsageListener) result.get(); } else { throw new IllegalStateException("Failed to create UsageListener from class name '"+input+"' using no-arg constructor"); } } }); } @VisibleForTesting public static final String APPLICATION_USAGE_KEY = "usage-application"; @VisibleForTesting public static final String LOCATION_USAGE_KEY = "usage-location"; private final LocalManagementContext managementContext; private final Object mutex = new Object(); private final List<UsageListener> listeners = Lists.newCopyOnWriteArrayList(); private final AtomicInteger listenerQueueSize = new AtomicInteger(); private ListeningExecutorService listenerExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() .setNameFormat("brooklyn-usagemanager-listener-%d") .build())); public LocalUsageManager(LocalManagementContext managementContext) { this.managementContext = checkNotNull(managementContext, "managementContext"); // TODO Once org.apache.brooklyn.core.management.internal.UsageManager.UsageListener is deleted, restore this // to normal generics! Collection<?> listeners = managementContext.getBrooklynProperties().getConfig(UsageManager.USAGE_LISTENERS); if (listeners != null) { for (Object listener : listeners) { if (listener instanceof ManagementContextInjectable) { ((ManagementContextInjectable)listener).setManagementContext(managementContext); } if (listener instanceof UsageListener) { addUsageListener((UsageListener)listener); } else if (listener == null) { throw new NullPointerException("null listener in config "+UsageManager.USAGE_LISTENERS); } else { throw new ClassCastException("listener "+listener+" of type "+listener.getClass()+" is not of type "+UsageListener.class.getName()); } } } } public void terminate() { // Wait for the listeners to finish + close the listeners Duration timeout = managementContext.getBrooklynProperties().getConfig(UsageManager.USAGE_LISTENER_TERMINATION_TIMEOUT); if (listenerQueueSize.get() > 0) { log.info("Usage manager waiting for "+listenerQueueSize+" listener events for up to "+timeout); } List<ListenableFuture<?>> futures = Lists.newArrayList(); for (final UsageListener listener : listeners) { ListenableFuture<?> future = listenerExecutor.submit(new Runnable() { public void run() { if (listener instanceof Closeable) { try { ((Closeable)listener).close(); } catch (IOException e) { log.warn("Problem closing usage listener "+listener+" (continuing)", e); } } }}); futures.add(future); } try { Futures.successfulAsList(futures).get(timeout.toMilliseconds(), TimeUnit.MILLISECONDS); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.warn("Problem terminiating usage listeners (continuing)", e); } finally { listenerExecutor.shutdownNow(); } } private void execOnListeners(final Function<UsageListener, Void> job) { for (final UsageListener listener : listeners) { listenerQueueSize.incrementAndGet(); listenerExecutor.execute(new Runnable() { public void run() { try { job.apply(listener); } catch (RuntimeException e) { log.error("Problem notifying listener "+listener+" of "+job, e); Exceptions.propagateIfFatal(e); } finally { listenerQueueSize.decrementAndGet(); } }}); } } @Override public void recordApplicationEvent(final Application app, final Lifecycle state) { log.debug("Storing application lifecycle usage event: application {} in state {}", new Object[] {app, state}); ConcurrentMap<String, ApplicationUsage> eventMap = managementContext.getStorage().getMap(APPLICATION_USAGE_KEY); synchronized (mutex) { ApplicationUsage usage = eventMap.get(app.getId()); if (usage == null) { usage = new ApplicationUsage(app.getId(), app.getDisplayName(), app.getEntityType().getName(), ((EntityInternal)app).toMetadataRecord()); } final ApplicationUsage.ApplicationEvent event = new ApplicationUsage.ApplicationEvent(state, getUser()); usage.addEvent(event); eventMap.put(app.getId(), usage); execOnListeners(new Function<UsageListener, Void>() { public Void apply(UsageListener listener) { listener.onApplicationEvent(new ApplicationMetadataImpl(Entities.proxy(app)), event); return null; } public String toString() { return "applicationEvent("+app+", "+state+")"; }}); } } /** * Adds this location event to the usage record for the given location (creating the usage * record if one does not already exist). */ @Override public void recordLocationEvent(final Location loc, final Lifecycle state) { // TODO This approach (i.e. recording events on manage/unmanage would not work for // locations that are reused. For example, in a FixedListMachineProvisioningLocation // the ssh machine location is returned to the pool and handed back out again. // But maybe the solution there is to hand out different instances so that one user // can't change the config of the SshMachineLocation to subsequently affect the next // user. // // TODO Should perhaps extract the location storage methods into their own class, // but no strong enough feelings yet... checkNotNull(loc, "location"); if (loc.getConfig(AbstractLocation.TEMPORARY_LOCATION)) { log.info("Ignoring location lifecycle usage event for {} (state {}), because location is a temporary location", loc, state); return; } checkNotNull(state, "state of location %s", loc); if (loc.getId() == null) { log.error("Ignoring location lifecycle usage event for {} (state {}), because location has no id", loc, state); return; } if (managementContext.getStorage() == null) { log.warn("Cannot store location lifecycle usage event for {} (state {}), because storage not available", loc, state); return; } Object callerContext = loc.getConfig(LocationConfigKeys.CALLER_CONTEXT); if (callerContext != null && callerContext instanceof Entity) { log.debug("Storing location lifecycle usage event: location {} in state {}; caller context {}", new Object[] {loc, state, callerContext}); Entity caller = (Entity) callerContext; String entityTypeName = caller.getEntityType().getName(); String appId = caller.getApplicationId(); final LocationUsage.LocationEvent event = new LocationUsage.LocationEvent(state, caller.getId(), entityTypeName, appId, getUser()); ConcurrentMap<String, LocationUsage> usageMap = managementContext.getStorage().<String, LocationUsage>getMap(LOCATION_USAGE_KEY); synchronized (mutex) { LocationUsage usage = usageMap.get(loc.getId()); if (usage == null) { usage = new LocationUsage(loc.getId(), ((LocationInternal)loc).toMetadataRecord()); } usage.addEvent(event); usageMap.put(loc.getId(), usage); execOnListeners(new Function<UsageListener, Void>() { public Void apply(UsageListener listener) { listener.onLocationEvent(new LocationMetadataImpl(loc), event); return null; } public String toString() { return "locationEvent("+loc+", "+state+")"; }}); } } else { // normal for high-level locations log.trace("Not recording location lifecycle usage event for {} in state {}, because no caller context", new Object[] {loc, state}); } } /** * Returns the usage info for the location with the given id, or null if unknown. */ @Override public LocationUsage getLocationUsage(String locationId) { BrooklynStorage storage = managementContext.getStorage(); Map<String, LocationUsage> usageMap = storage.getMap(LOCATION_USAGE_KEY); return usageMap.get(locationId); } /** * Returns the usage info that matches the given predicate. * For example, could be used to find locations used within a given time period. */ @Override public Set<LocationUsage> getLocationUsage(Predicate<? super LocationUsage> filter) { // TODO could do more efficient indexing, to more easily find locations in use during a given period. // But this is good enough for first-pass. Map<String, LocationUsage> usageMap = managementContext.getStorage().getMap(LOCATION_USAGE_KEY); Set<LocationUsage> result = Sets.newLinkedHashSet(); for (LocationUsage usage : usageMap.values()) { if (filter.apply(usage)) { result.add(usage); } } return result; } /** * Returns the usage info for the location with the given id, or null if unknown. */ @Override public ApplicationUsage getApplicationUsage(String appId) { BrooklynStorage storage = managementContext.getStorage(); Map<String, ApplicationUsage> usageMap = storage.getMap(APPLICATION_USAGE_KEY); return usageMap.get(appId); } /** * Returns the usage info that matches the given predicate. * For example, could be used to find applications used within a given time period. */ @Override public Set<ApplicationUsage> getApplicationUsage(Predicate<? super ApplicationUsage> filter) { // TODO could do more efficient indexing, to more easily find locations in use during a given period. // But this is good enough for first-pass. Map<String, ApplicationUsage> usageMap = managementContext.getStorage().getMap(APPLICATION_USAGE_KEY); Set<ApplicationUsage> result = Sets.newLinkedHashSet(); for (ApplicationUsage usage : usageMap.values()) { if (filter.apply(usage)) { result.add(usage); } } return result; } @Override public void addUsageListener(UsageListener listener) { listeners.add(listener); } @Override public void removeUsageListener(UsageListener listener) { listeners.remove(listener); } private String getUser() { EntitlementContext entitlementContext = Entitlements.getEntitlementContext(); if (entitlementContext != null) { return entitlementContext.user(); } return null; } }