package org.fenixedu.bennu.core.domain.groups;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.fenixedu.bennu.core.domain.Bennu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pt.ist.fenixframework.Atomic;
import pt.ist.fenixframework.Atomic.TxMode;
import pt.ist.fenixframework.FenixFramework;
import pt.ist.fenixframework.dml.DomainClass;
import pt.ist.fenixframework.dml.Role;
import pt.ist.fenixframework.dml.runtime.Relation;
import com.google.common.collect.Lists;
class GroupGC {
private static final Logger logger = LoggerFactory.getLogger(GroupGC.class);
private static Map<Class<? extends PersistentGroup>, Predicate<PersistentGroup>> testers = new ConcurrentHashMap<>();
private static Map<Class<? extends PersistentGroup>, Consumer<PersistentGroup>> cleaners = new ConcurrentHashMap<>();
public static void gc() {
List<PersistentGroup> candidates = new ArrayList<>();
AtomicInteger total = new AtomicInteger(-1);
LongAdder processed = new LongAdder();
int sweep = 0;
int totalCleared = 0;
do {
total.set(Bennu.getInstance().getGroupSet().size());
processed.reset();
totalCleared += candidates.size();
candidates.clear();
logger.debug("Starting sweep #{}", ++sweep);
Timer timer = new Timer();
try {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
logger.debug("Processed {} of {} (found {} candidates for deletion)", processed.intValue(), total,
candidates.size());
}
}, 0, 30 * 1000);
Lists.partition(new ArrayList<>(Bennu.getInstance().getGroupSet()), 10000).stream().parallel()
.forEach(p -> processor(p, candidates, processed));
} finally {
timer.cancel();
}
logger.debug("Finished sweep #{}, clearing {} candidates", sweep, candidates.size());
if (!candidates.isEmpty()) {
List<List<PersistentGroup>> partitions = Lists.partition(candidates, 1000);
logger.debug("Processing {} clear chunks", partitions.size());
int i = 0;
for (List<PersistentGroup> partition : partitions) {
FenixFramework.atomic(() -> partition.forEach(PersistentGroup::gc));
logger.debug("\tProcessed {} out of {}", ++i, partitions.size());
}
}
} while (!candidates.isEmpty());
logger.debug("GC completed. {} total objects collected", totalCleared);
}
@Atomic(mode = TxMode.READ)
private static void processor(List<PersistentGroup> groups, List<PersistentGroup> candidates, LongAdder processed) {
for (PersistentGroup group : groups) {
if (!(group instanceof PersistentDynamicGroup) && emptyCustomRelations(group)) {
candidates.add(group);
}
processed.increment();
}
}
public static void cleanContext(PersistentGroup group) {
Consumer<PersistentGroup> cleaner = cleaners.computeIfAbsent(group.getClass(), type -> {
Consumer<PersistentGroup> result = ignore -> {
};
Set<String> relations = group.getContextRelations().stream().map(Relation::getName).collect(Collectors.toSet());
DomainClass model = FenixFramework.getDomainModel().findClass(type.getName());
for (Role role : fullRoles(model)) {
if (relations.contains(role.getRelation().getName())) {
if (role.getMultiplicityUpper() == 1) {
result = result.andThen(g -> cleanToOneRelation(g, setter(type, role)));
} else {
result = result.andThen(g -> cleanToManyRelation(g, getter(type, role)));
}
}
}
return result;
});
cleaner.accept(group);
}
static boolean emptyCustomRelations(PersistentGroup group) {
Predicate<PersistentGroup> predicate =
testers.computeIfAbsent(
group.getClass(),
type -> {
Predicate<PersistentGroup> result = ignored -> true;
Set<String> relations =
group.getContextRelations().stream().map(Relation::getName).collect(Collectors.toSet());
List<String> getters = new ArrayList<>();
DomainClass model = FenixFramework.getDomainModel().findClass(type.getName());
for (Role role : fullRoles(model)) {
if (relations.contains(role.getRelation().getName()) || role.getName().equals("root")) {
continue;
}
Method method = getter(type, role);
getters.add(method.getName());
result = result.and(g -> testEmptyRole(g, method, role.getMultiplicityUpper() == 1));
}
logger.debug("Registering getters for type {}, that should be empty {}", group.getClass()
.getSimpleName(), getters.stream().collect(Collectors.joining(", ")));
return result;
});
return predicate.test(group);
}
private static List<Role> fullRoles(DomainClass model) {
List<Role> roles = new ArrayList<>();
roles.addAll(model.getRoleSlotsList());
if (model.hasSuperclass()) {
roles.addAll(fullRoles((DomainClass) model.getSuperclass()));
}
return roles;
}
private static boolean testEmptyRole(PersistentGroup group, Method method, boolean single) {
try {
return single ? method.invoke(group) == null : ((Collection<?>) method.invoke(group)).isEmpty();
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
return false;
}
}
private static void cleanToOneRelation(PersistentGroup group, Method method) {
try {
method.invoke(group, (Object) null);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
logger.debug("Could not clear relation of group {} with setter {}", group.getExternalId(), method.getName());
}
}
private static void cleanToManyRelation(PersistentGroup group, Method method) {
try {
((Collection<?>) method.invoke(group)).clear();
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
logger.debug("Could not clear relation of group {} with setter {}", group.getExternalId(), method.getName());
}
}
private static Method getter(Class<?> type, Role role) {
String name = "get" + capitalize(role.getName());
try {
try {
Method method = type.getDeclaredMethod(name);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
}
if (type.getSuperclass() != null) {
return getter(type.getSuperclass(), role);
}
} catch (SecurityException e) {
}
throw new Error(String.format("Unexpected inexistence of method: %s for role %s%n", name, role.getName()));
}
private static Method setter(Class<?> type, Role role) {
String name = "set" + capitalize(role.getName());
try {
try {
Method method = type.getDeclaredMethod(name, Class.forName(role.getType().getFullName()));
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
} catch (ClassNotFoundException e) {
}
if (type.getSuperclass() != null) {
return setter(type.getSuperclass(), role);
}
} catch (SecurityException e) {
}
throw new Error(String.format("Unexpected inexistence of method: %s for role %s%n", name, role.getName()));
}
private static String capitalize(String name) {
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
}