/*
* Copyright 2016 Adobe.
*
* Licensed 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 com.adobe.acs.commons.fam.impl;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.fam.Failure;
import com.adobe.acs.commons.fam.ThrottledTaskRunner;
import com.adobe.acs.commons.functions.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularType;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages a pool of reusable resource resolvers and injects them into tasks
*/
class ActionManagerImpl implements ActionManager {
private static final Logger LOG = LoggerFactory.getLogger(ActionManagerImpl.class);
// This is a delay of how long an action manager should wait before it can safely assume it really is done and no more work is being added
// This helps prevent an action manager from closing itself down while the queue is warming up.
public static final int HESITATION_DELAY = 50;
// The cleanup task will wait this many milliseconds between its polling to see if the queue has been completely processed
public static final int COMPLETION_CHECK_INTERVAL = 100;
private final AtomicInteger tasksAdded = new AtomicInteger();
private final AtomicInteger tasksCompleted = new AtomicInteger();
private final AtomicInteger tasksFilteredOut = new AtomicInteger();
private final AtomicInteger tasksSuccessful = new AtomicInteger();
private final AtomicInteger tasksError = new AtomicInteger();
private final String name;
private final AtomicLong started = new AtomicLong(0);
private long finished;
private int saveInterval;
private final ResourceResolver baseResolver;
private final List<ReusableResolver> resolvers = Collections.synchronizedList(new ArrayList<>());
private final ThreadLocal<ReusableResolver> currentResolver = new ThreadLocal<>();
private final ThrottledTaskRunner taskRunner;
private final ThreadLocal<String> currentPath;
private final List<Failure> failures;
private final AtomicBoolean cleanupHandlerRegistered = new AtomicBoolean(false);
private final List<CheckedConsumer<ResourceResolver>> successHandlers = Collections.synchronizedList(new ArrayList<>());
private final List<CheckedBiConsumer<List<Failure>, ResourceResolver>> errorHandlers = Collections.synchronizedList(new ArrayList<>());
private final List<Runnable> finishHandlers = Collections.synchronizedList(new ArrayList<>());
ActionManagerImpl(String name, ThrottledTaskRunner taskRunner, ResourceResolver resolver, int saveInterval) throws LoginException {
this.name = name;
this.taskRunner = taskRunner;
this.saveInterval = saveInterval;
baseResolver = resolver.clone(null);
currentPath = new ThreadLocal<>();
failures = new ArrayList<>();
}
@Override
public String getName() {
return name;
}
@Override
public int getAddedCount() {
return tasksAdded.get();
}
@Override
public int getSuccessCount() {
return tasksSuccessful.get();
}
@Override
public int getErrorCount() {
return tasksError.get();
}
@Override
public int getCompletedCount() {
return tasksCompleted.get();
}
@Override
public int getRemainingCount() {
return getAddedCount() - (getSuccessCount() + getErrorCount());
}
@Override
public List<Failure> getFailureList() {
return failures;
}
@Override
public void deferredWithResolver(final Consumer<ResourceResolver> action) {
this.deferredWithResolver((CheckedConsumer<ResourceResolver>) action);
}
@Override
public void deferredWithResolver(final CheckedConsumer<ResourceResolver> action) {
deferredWithResolver(action, false);
}
@Override
public void withResolver(Consumer<ResourceResolver> action) throws Exception {
withResolver((CheckedConsumer<ResourceResolver>) action);
}
@Override
public void withResolver(CheckedConsumer<ResourceResolver> action) throws Exception {
ReusableResolver resolver = getResourceResolver();
resolver.setCurrentItem(currentPath.get());
try {
action.accept(resolver.getResolver());
} catch (Throwable ex) {
throw ex;
} finally {
try {
resolver.free();
} catch (PersistenceException ex) {
logPersistenceException(resolver.getPendingItems(), ex);
throw ex;
}
}
}
@Override
public int withQueryResults(
final String queryStatement,
final String language,
final BiConsumer<ResourceResolver, String> callback,
final BiFunction<ResourceResolver, String, Boolean>... filters
)
throws RepositoryException, PersistenceException, Exception {
return withQueryResults(queryStatement, language, (CheckedBiConsumer<ResourceResolver, String>) callback,
Arrays.copyOf(filters, filters.length, CheckedBiFunction[].class));
}
@Override
public int withQueryResults(
final String queryStatement,
final String language,
final CheckedBiConsumer<ResourceResolver, String> callback,
final CheckedBiFunction<ResourceResolver, String, Boolean>... filters
)
throws RepositoryException, PersistenceException, Exception {
withResolver((ResourceResolver resolver) -> {
try {
Session session = resolver.adaptTo(Session.class);
QueryManager queryManager = session.getWorkspace().getQueryManager();
Query query = queryManager.createQuery(queryStatement, language);
QueryResult results = query.execute();
for (NodeIterator nodeIterator = results.getNodes(); nodeIterator.hasNext();) {
final String nodePath = nodeIterator.nextNode().getPath();
LOG.info("Processing found result " + nodePath);
deferredWithResolver((ResourceResolver r) -> {
currentPath.set(nodePath);
if (filters != null) {
for (CheckedBiFunction<ResourceResolver, String, Boolean> filter : filters) {
if (!filter.apply(r, nodePath)) {
logFilteredOutItem(nodePath);
return;
}
}
}
callback.accept(r, nodePath);
});
}
} catch (RepositoryException ex) {
LOG.error("Repository exception processing query "+queryStatement, ex);
}
});
return tasksAdded.get();
}
@Override
public void addCleanupTask() {
// This is deprecated, only included for backwards-compatibility.
}
@Override
public void onSuccess(CheckedConsumer<ResourceResolver> successTask) {
successHandlers.add(successTask);
}
@Override
public void onFailure(CheckedBiConsumer<List<Failure>, ResourceResolver> failureTask) {
errorHandlers.add(failureTask);
}
@Override
public void onFinish(Runnable finishHandler) {
finishHandlers.add(finishHandler);
}
private void runCompletionTasks() {
if (getErrorCount() == 0) {
successHandlers.forEach(handler -> {
try {
this.withResolver(handler);
} catch (Exception ex) {
LOG.error("Error in success handler for action "+getName(), ex);
}
});
} else {
errorHandlers.forEach(handler -> {
try {
this.withResolver(res -> handler.accept(getFailureList(), res));
} catch (Exception ex) {
LOG.error("Error in error handler for action "+getName(), ex);
}
});
}
finishHandlers.forEach(Runnable::run);
}
private void performAutomaticCleanup() {
if (!cleanupHandlerRegistered.getAndSet(true)) {
taskRunner.scheduleWork(() -> {
while (!isComplete()) {
try {
Thread.sleep(COMPLETION_CHECK_INTERVAL);
} catch (InterruptedException ex) {
logError(ex);
}
}
runCompletionTasks();
closeAllResolvers();
});
}
}
@Override
public void setCurrentItem(String item) {
currentPath.set(item);
}
private void deferredWithResolver(
final CheckedConsumer<ResourceResolver> action,
final boolean closesResolver) {
taskRunner.scheduleWork(() -> {
started.compareAndSet(0, System.currentTimeMillis());
try {
withResolver(action);
if (!closesResolver) {
logCompletetion();
}
} catch (Exception ex) {
LOG.error("Error in error handler for action "+getName(), ex);
if (!closesResolver) {
logError(ex);
}
} catch (Throwable t) {
LOG.error("Fatal uncaught error in error handler for action "+getName(), t);
if (!closesResolver) {
logError(new RuntimeException(t));
}
throw t;
}
});
if (!closesResolver) {
tasksAdded.incrementAndGet();
}
}
private ReusableResolver getResourceResolver() throws LoginException {
ReusableResolver resolver = currentResolver.get();
if (resolver == null || !resolver.getResolver().isLive()) {
resolver = new ReusableResolver(baseResolver.clone(null), saveInterval);
currentResolver.set(resolver);
resolvers.add(resolver);
}
return resolver;
}
private void logCompletetion() {
tasksCompleted.incrementAndGet();
tasksSuccessful.incrementAndGet();
if (isComplete()) {
finished = System.currentTimeMillis();
performAutomaticCleanup();
}
}
private void logError(Exception ex) {
LOG.error("Caught exception in task: "+ex.getMessage(), ex);
Failure fail = new Failure();
fail.setNodePath(currentPath.get());
fail.setException(ex);
failures.add(fail);
tasksCompleted.incrementAndGet();
tasksError.incrementAndGet();
if (isComplete()) {
finished = System.currentTimeMillis();
performAutomaticCleanup();
}
}
private void logPersistenceException(List<String> items, PersistenceException ex) {
StringBuilder itemList = new StringBuilder();
for (String item:items) {
itemList.append(item).append("; ");
Failure fail = new Failure();
fail.setNodePath(item);
fail.setException(ex);
failures.add(fail);
tasksError.incrementAndGet();
tasksSuccessful.decrementAndGet();
}
LOG.error("Persistence error prevented saving changes for: "+itemList, ex);
}
private void logFilteredOutItem(String path) {
tasksFilteredOut.incrementAndGet();
LOG.info("Filtered out " + path);
}
private long getRuntime() {
if (isComplete()) {
return finished - started.get();
} else if (tasksAdded.get() == 0) {
return 0;
} else {
return System.currentTimeMillis() - started.get();
}
}
public static TabularType getStaticsTableType() {
return statsTabularType;
}
@Override
public boolean isComplete() {
if (tasksCompleted.get() == tasksAdded.get()) {
try {
Thread.sleep(HESITATION_DELAY);
} catch (InterruptedException ex) {
}
return tasksCompleted.get() == tasksAdded.get();
} else {
return false;
}
}
@Override
public CompositeData getStatistics() throws OpenDataException {
return new CompositeDataSupport(statsCompositeType, statsItemNames,
new Object[]{
name,
tasksAdded.get(),
tasksCompleted.get(),
tasksFilteredOut.get(),
tasksSuccessful.get(),
tasksError.get(),
getRuntime()
}
);
}
@Override
public void closeAllResolvers() {
if (!resolvers.isEmpty()) {
resolvers.stream()
.map(ReusableResolver::getResolver)
.filter(ResourceResolver::isLive)
.forEachOrdered(ResourceResolver::close);
resolvers.clear();
}
baseResolver.close();
}
static public TabularType getFailuresTableType() {
return failureTabularType;
}
@Override
public List<CompositeData> getFailures() throws OpenDataException {
ArrayList<CompositeData> failureData = new ArrayList<>();
int count = 0;
for (Failure fail : failures) {
if (count > 5000) break;
failureData.add(new CompositeDataSupport(
failureCompositeType,
failureItemNames,
new Object[]{name, ++count, fail.getNodePath(), fail.getException().getMessage()}));
}
return failureData;
}
private static String[] statsItemNames;
private static CompositeType statsCompositeType;
private static TabularType statsTabularType;
private static String[] failureItemNames;
private static CompositeType failureCompositeType;
private static TabularType failureTabularType;
static {
try {
statsItemNames = new String[]{"_taskName", "started", "completed", "filtered", "successful", "errors", "runtime"};
statsCompositeType = new CompositeType(
"Statics Row",
"Single row of statistics",
statsItemNames,
new String[]{"Name", "Started", "Completed", "Filtered", "Successful", "Errors", "Runtime"},
new OpenType[]{SimpleType.STRING, SimpleType.INTEGER, SimpleType.INTEGER, SimpleType.INTEGER, SimpleType.INTEGER, SimpleType.INTEGER, SimpleType.LONG});
statsTabularType = new TabularType("Statistics", "Collected statistics", statsCompositeType, new String[]{"_taskName"});
failureItemNames = new String[]{"_taskName", "_count", "item", "error"};
failureCompositeType = new CompositeType(
"Failure",
"Failure",
failureItemNames,
new String[]{"Name", "#", "Item", "Error"},
new OpenType[]{SimpleType.STRING, SimpleType.INTEGER, SimpleType.STRING, SimpleType.STRING});
failureTabularType = new TabularType("Errors", "Collected failures", failureCompositeType, new String[]{"_taskName", "_count"});
} catch (OpenDataException ex) {
LOG.error("Unable to build MBean composite types", ex);
}
}
}