package io.cattle.platform.core.cleanup;
import static io.cattle.platform.core.model.tables.ExternalHandlerExternalHandlerProcessMapTable.*;
import static io.cattle.platform.core.model.tables.ExternalHandlerTable.*;
import static io.cattle.platform.core.model.tables.HostLabelMapTable.*;
import static io.cattle.platform.core.model.tables.HostTable.*;
import static io.cattle.platform.core.model.tables.InstanceLabelMapTable.*;
import static io.cattle.platform.core.model.tables.InstanceTable.*;
import static io.cattle.platform.core.model.tables.LabelTable.*;
import static io.cattle.platform.core.model.tables.ServiceEventTable.*;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.core.constants.CommonStatesConstants;
import io.cattle.platform.core.model.tables.AccountTable;
import io.cattle.platform.core.model.tables.AgentTable;
import io.cattle.platform.core.model.tables.AuditLogTable;
import io.cattle.platform.core.model.tables.AuthTokenTable;
import io.cattle.platform.core.model.tables.BackupTable;
import io.cattle.platform.core.model.tables.BackupTargetTable;
import io.cattle.platform.core.model.tables.CertificateTable;
import io.cattle.platform.core.model.tables.ClusterHostMapTable;
import io.cattle.platform.core.model.tables.ConfigItemStatusTable;
import io.cattle.platform.core.model.tables.ContainerEventTable;
import io.cattle.platform.core.model.tables.CredentialInstanceMapTable;
import io.cattle.platform.core.model.tables.CredentialTable;
import io.cattle.platform.core.model.tables.DeploymentUnitTable;
import io.cattle.platform.core.model.tables.DynamicSchemaTable;
import io.cattle.platform.core.model.tables.ExternalEventTable;
import io.cattle.platform.core.model.tables.ExternalHandlerExternalHandlerProcessMapTable;
import io.cattle.platform.core.model.tables.ExternalHandlerProcessTable;
import io.cattle.platform.core.model.tables.ExternalHandlerTable;
import io.cattle.platform.core.model.tables.GenericObjectTable;
import io.cattle.platform.core.model.tables.HealthcheckInstanceHostMapTable;
import io.cattle.platform.core.model.tables.HealthcheckInstanceTable;
import io.cattle.platform.core.model.tables.HostIpAddressMapTable;
import io.cattle.platform.core.model.tables.HostLabelMapTable;
import io.cattle.platform.core.model.tables.HostTable;
import io.cattle.platform.core.model.tables.ImageStoragePoolMapTable;
import io.cattle.platform.core.model.tables.ImageTable;
import io.cattle.platform.core.model.tables.InstanceHostMapTable;
import io.cattle.platform.core.model.tables.InstanceLabelMapTable;
import io.cattle.platform.core.model.tables.InstanceLinkTable;
import io.cattle.platform.core.model.tables.InstanceTable;
import io.cattle.platform.core.model.tables.IpAddressNicMapTable;
import io.cattle.platform.core.model.tables.IpAddressTable;
import io.cattle.platform.core.model.tables.LabelTable;
import io.cattle.platform.core.model.tables.MachineDriverTable;
import io.cattle.platform.core.model.tables.MountTable;
import io.cattle.platform.core.model.tables.NetworkDriverTable;
import io.cattle.platform.core.model.tables.NetworkTable;
import io.cattle.platform.core.model.tables.NicTable;
import io.cattle.platform.core.model.tables.PhysicalHostTable;
import io.cattle.platform.core.model.tables.PortTable;
import io.cattle.platform.core.model.tables.ProcessExecutionTable;
import io.cattle.platform.core.model.tables.ProcessInstanceTable;
import io.cattle.platform.core.model.tables.ProjectMemberTable;
import io.cattle.platform.core.model.tables.ResourcePoolTable;
import io.cattle.platform.core.model.tables.ScheduledUpgradeTable;
import io.cattle.platform.core.model.tables.SecretTable;
import io.cattle.platform.core.model.tables.ServiceConsumeMapTable;
import io.cattle.platform.core.model.tables.ServiceEventTable;
import io.cattle.platform.core.model.tables.ServiceExposeMapTable;
import io.cattle.platform.core.model.tables.ServiceIndexTable;
import io.cattle.platform.core.model.tables.ServiceLogTable;
import io.cattle.platform.core.model.tables.ServiceTable;
import io.cattle.platform.core.model.tables.SnapshotTable;
import io.cattle.platform.core.model.tables.StackTable;
import io.cattle.platform.core.model.tables.StorageDriverTable;
import io.cattle.platform.core.model.tables.StoragePoolHostMapTable;
import io.cattle.platform.core.model.tables.StoragePoolTable;
import io.cattle.platform.core.model.tables.SubnetTable;
import io.cattle.platform.core.model.tables.TaskInstanceTable;
import io.cattle.platform.core.model.tables.UserPreferenceTable;
import io.cattle.platform.core.model.tables.VolumeStoragePoolMapTable;
import io.cattle.platform.core.model.tables.VolumeTable;
import io.cattle.platform.core.model.tables.ZoneTable;
import io.cattle.platform.db.jooq.dao.impl.AbstractJooqDao;
import io.cattle.platform.object.jooq.utils.JooqUtils;
import io.cattle.platform.task.Task;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Record1;
import org.jooq.Result;
import org.jooq.ResultQuery;
import org.jooq.Table;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicLongProperty;
/**
* Programmatically delete purged database rows after they reach a configurable age.
*/
public class TableCleanup extends AbstractJooqDao implements Task {
public static final Long SECOND_MILLIS = 1000L;
private static final Logger log = LoggerFactory.getLogger(TableCleanup.class);
public static final DynamicIntProperty QUERY_LIMIT_ROWS = ArchaiusUtil.getInt("cleanup.query_limit.rows");
public static final DynamicLongProperty MAIN_TABLES_AGE_LIMIT_SECONDS = ArchaiusUtil.getLong("main_tables.purge.after.seconds");
public static final DynamicLongProperty PROCESS_INSTANCE_AGE_LIMIT_SECONDS = ArchaiusUtil.getLong("process_instance.purge.after.seconds");
public static final DynamicLongProperty EVENT_AGE_LIMIT_SECONDS = ArchaiusUtil.getLong("events.purge.after.seconds");
public static final DynamicLongProperty AUDIT_LOG_AGE_LIMIT_SECONDS = ArchaiusUtil.getLong("audit_log.purge.after.seconds");
public static final DynamicLongProperty SERVICE_LOG_AGE_LIMIT_SECONDS = ArchaiusUtil.getLong("service_log.purge.after.seconds");
private List<CleanableTable> processInstanceTables;
private List<CleanableTable> eventTables;
private List<CleanableTable> auditLogTables;
private List<CleanableTable> serviceLogTables;
private List<CleanableTable> otherTables;
public TableCleanup() {
this.processInstanceTables = getProcessInstanceTables();
this.eventTables = getEventTables();
this.auditLogTables = getAuditLogTables();
this.serviceLogTables = getServiceLogTables();
this.otherTables = getOtherTables();
}
@Override
public void run() {
long current = new Date().getTime();
Date otherCutoff = new Date(current - MAIN_TABLES_AGE_LIMIT_SECONDS.getValue() * SECOND_MILLIS);
cleanupLabelTables(otherCutoff);
cleanupExternalHandlerExternalHandlerProcessMapTables(otherCutoff);
Date processInstanceCutoff = new Date(current - PROCESS_INSTANCE_AGE_LIMIT_SECONDS.get() * SECOND_MILLIS);
cleanup("process_instance", processInstanceTables, processInstanceCutoff);
Date eventTableCutoff = new Date(current - EVENT_AGE_LIMIT_SECONDS.get() * SECOND_MILLIS);
cleanupServiceEventTable(eventTableCutoff);
cleanup("event", eventTables, eventTableCutoff);
Date auditLogCutoff = new Date(current - AUDIT_LOG_AGE_LIMIT_SECONDS.get() * SECOND_MILLIS);
cleanup("audit_log", auditLogTables, auditLogCutoff);
Date serviceLogCutoff = new Date(current - SERVICE_LOG_AGE_LIMIT_SECONDS.get() * SECOND_MILLIS);
cleanup("service_log", serviceLogTables, serviceLogCutoff);
cleanup("other", otherTables, otherCutoff);
}
private void cleanupServiceEventTable(Date cutoff) {
ResultQuery<Record1<Long>> ids = create()
.select(SERVICE_EVENT.ID)
.from(SERVICE_EVENT)
.where(SERVICE_EVENT.CREATED.lt(cutoff))
.and(SERVICE_EVENT.STATE.eq(CommonStatesConstants.CREATED))
.limit(QUERY_LIMIT_ROWS.getValue());
List<Long> toDelete = null;
int rowsDeleted = 0;
while ((toDelete = ids.fetch().into(Long.class)).size() > 0) {
rowsDeleted += create().delete(SERVICE_EVENT)
.where(SERVICE_EVENT.ID.in(toDelete)).execute();
}
if (rowsDeleted > 0) {
log.info("[Rows Deleted] service_event={}", rowsDeleted);
}
}
private void cleanupExternalHandlerExternalHandlerProcessMapTables(Date cutoff) {
ResultQuery<Record1<Long>> ids = create()
.select(EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP.ID)
.from(EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP)
.join(EXTERNAL_HANDLER)
.on(EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP.EXTERNAL_HANDLER_ID.eq(EXTERNAL_HANDLER.ID))
.where(EXTERNAL_HANDLER.REMOVED.lt(cutoff))
.limit(QUERY_LIMIT_ROWS.getValue());
List<Long> toDelete = null;
int rowsDeleted = 0;
while ((toDelete = ids.fetch().into(Long.class)).size() > 0) {
rowsDeleted += create().delete(EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP)
.where(EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP.ID.in(toDelete)).execute();
}
if (rowsDeleted > 0) {
log.info("[Rows Deleted] external_handler_external_handler_process_map={}", rowsDeleted);
}
}
private void cleanupLabelTables(Date cutoff) {
ResultQuery<Record1<Long>> ids = create()
.select(INSTANCE_LABEL_MAP.ID)
.from(INSTANCE_LABEL_MAP)
.join(INSTANCE).on(INSTANCE_LABEL_MAP.INSTANCE_ID.eq(INSTANCE.ID))
.where(INSTANCE.REMOVED.lt(cutoff))
.limit(QUERY_LIMIT_ROWS.getValue());
List<Long> toDelete = null;
int ilmRowsDeleted = 0;
while ((toDelete = ids.fetch().into(Long.class)).size() > 0) {
ilmRowsDeleted += create().delete(INSTANCE_LABEL_MAP)
.where(INSTANCE_LABEL_MAP.ID.in(toDelete)).execute();
}
ids = create()
.select(HOST_LABEL_MAP.ID)
.from(HOST_LABEL_MAP)
.join(HOST).on(HOST_LABEL_MAP.HOST_ID.eq(HOST.ID))
.where(HOST.REMOVED.lt(cutoff))
.limit(QUERY_LIMIT_ROWS.getValue());
int hlmRowsDeleted = 0;
while ((toDelete = ids.fetch().into(Long.class)).size() > 0) {
hlmRowsDeleted += create().delete(HOST_LABEL_MAP)
.where(HOST_LABEL_MAP.ID.in(toDelete)).execute();
}
ids = create()
.select(LABEL.ID)
.from(LABEL)
.leftOuterJoin(INSTANCE_LABEL_MAP).on(LABEL.ID.eq(INSTANCE_LABEL_MAP.LABEL_ID))
.leftOuterJoin(HOST_LABEL_MAP).on(LABEL.ID.eq(HOST_LABEL_MAP.LABEL_ID))
.where(INSTANCE_LABEL_MAP.ID.isNull())
.and(HOST_LABEL_MAP.ID.isNull())
.and(LABEL.CREATED.lt(cutoff))
.limit(QUERY_LIMIT_ROWS.getValue());
int labelRowsDeleted = 0;
while ((toDelete = ids.fetch().into(Long.class)).size() > 0) {
labelRowsDeleted += create().delete(LABEL)
.where(LABEL.ID.in(toDelete)).execute();
}
StringBuilder lg = new StringBuilder("[Rows Deleted] ");
if (ilmRowsDeleted > 0) {
lg.append("instance_label_map=").append(ilmRowsDeleted);
}
if (hlmRowsDeleted > 0) {
lg.append("host_label_map=").append(hlmRowsDeleted);
}
if (labelRowsDeleted > 0) {
lg.append(" label=").append(labelRowsDeleted);
}
if (ilmRowsDeleted > 0 || labelRowsDeleted > 0 || hlmRowsDeleted > 0) {
log.info(lg.toString());
}
}
@SuppressWarnings("unchecked")
private void cleanup(String name, List<CleanableTable> tables, Date cutoffTime) {
for (CleanableTable table : tables) {
Field<Long> id = table.idField;
Field<Date> remove = table.removeField;
ResultQuery<Record1<Long>> ids = create()
.select(id)
.from(table.table)
.where(remove.lt(cutoffTime))
.limit(QUERY_LIMIT_ROWS.getValue());
table.clearRowCounts();
Result<Record1<Long>> toDelete;
List<Long> idsToFix = new ArrayList<>();
while ((toDelete = ids.fetch()).size() > 0) {
List<Long> idsToDelete = new ArrayList<>();
for (Record1<Long> record : toDelete) {
if (!idsToFix.contains(record.value1())) {
idsToDelete.add(record.value1());
}
}
if (idsToDelete.size() == 0) {
break;
}
List<ForeignKey<?, ?>> keys = getReferencesFrom(table, tables);
for (ForeignKey<?, ?> key : keys) {
Table<?> referencingTable = key.getTable();
if (key.getFields().size() > 1) {
log.error("Composite foreign key filtering unsupported");
}
Field<Long> foreignKeyField = (Field<Long>) key.getFields().get(0);
ResultQuery<Record1<Long>> filterIds = create()
.selectDistinct(foreignKeyField)
.from(referencingTable)
.where(foreignKeyField.in(idsToDelete));
Result<Record1<Long>> toFilter = filterIds.fetch();
if (toFilter.size() > 0) {
for (Record1<Long> record : toFilter) {
if (idsToDelete.remove(record.value1())) {
idsToFix.add(record.value1());
}
}
}
}
try {
table.addRowsDeleted(create()
.delete(table.table)
.where(id.in(idsToDelete))
.execute());
} catch (org.jooq.exception.DataAccessException e) {
log.info(e.getMessage());
break;
}
}
if (idsToFix.size() > 0) {
table.addRowsSkipped(idsToFix.size());
log.info("Skipped {} where id in {}", table.table, idsToFix);
}
}
StringBuffer buffDeleted = new StringBuffer("[Rows Deleted] ");
StringBuffer buffSkipped = new StringBuffer("[Rows Skipped] ");
boolean deletedActivity = false;
boolean skippedActivity = false;
for (CleanableTable table : tables) {
if (table.getRowsDeleted() > 0) {
buffDeleted.append(table.table.getName())
.append("=")
.append(table.getRowsDeleted())
.append(" ");
deletedActivity = true;
}
if (table.getRowsSkipped() > 0) {
buffSkipped.append(table.table.getName())
.append("=")
.append(table.getRowsSkipped())
.append(" ");
skippedActivity = true;
}
}
log.info("Cleanup {} tables [cutoff={}]", name, cutoffTime);
if (deletedActivity) {
log.info(buffDeleted.toString());
}
if (skippedActivity) {
log.info(buffSkipped.toString());
}
}
/**
* Returns a list of foreign keys referencing a table
*
* @param table
* @param others
* @return
*/
public static List<ForeignKey<?, ?>> getReferencesFrom(CleanableTable table, List<CleanableTable> others) {
List<ForeignKey<?, ?>> keys = new ArrayList<ForeignKey<?, ?>>();
for (CleanableTable other : others) {
keys.addAll(table.table.getReferencesFrom(other.table));
}
return keys;
}
/**
* Sorts a list of tables by their primary key references such that tables may be cleaned in an order
* that doesn't violate any key constraints.
*
* @param tables The list of tables to sort
*/
public static List<CleanableTable> sortByReferences(List<CleanableTable> tables) {
List<CleanableTable> unsorted = new ArrayList<CleanableTable>(tables);
List<CleanableTable> sorted = new ArrayList<CleanableTable>();
int tableCount = unsorted.size();
while (tableCount > 0) {
for (int i = 0; i < unsorted.size(); i++) {
CleanableTable table = unsorted.get(i);
List<CleanableTable> others = new ArrayList<CleanableTable>(unsorted);
others.remove(i);
if (!JooqUtils.isReferencedBy(table.table, stripContext(others))) {
sorted.add(unsorted.remove(i--));
}
}
if (tableCount == unsorted.size()) {
log.error("Cycle detected in table references! Aborting.");
System.exit(1);
} else {
tableCount = unsorted.size();
}
}
if (log.isDebugEnabled()) {
log.debug("Table cleanup plan:");
for (CleanableTable table : sorted) {
log.debug(table.toString());
}
}
return sorted;
}
private static List<Table<?>> stripContext(List<CleanableTable> cleanableTables) {
List<Table<?>> tables = new ArrayList<Table<?>>();
for (CleanableTable cleanableTable : cleanableTables) {
tables.add(cleanableTable.table);
}
return tables;
}
private static List<CleanableTable> getProcessInstanceTables() {
List<CleanableTable> tables = Arrays.asList(
CleanableTable.from(ProcessExecutionTable.PROCESS_EXECUTION),
CleanableTable.from(ProcessInstanceTable.PROCESS_INSTANCE));
return sortByReferences(tables);
}
private static List<CleanableTable> getEventTables() {
List<CleanableTable> tables = Arrays.asList(
CleanableTable.from(ContainerEventTable.CONTAINER_EVENT));
return sortByReferences(tables);
}
private static List<CleanableTable> getAuditLogTables() {
return Arrays.asList(CleanableTable.from(AuditLogTable.AUDIT_LOG));
}
private static List<CleanableTable> getServiceLogTables() {
return Arrays.asList(CleanableTable.from(ServiceLogTable.SERVICE_LOG, ServiceLogTable.SERVICE_LOG.CREATED));
}
private static List<CleanableTable> getOtherTables() {
List<CleanableTable> tables = Arrays.asList(
CleanableTable.from(AccountTable.ACCOUNT),
CleanableTable.from(AgentTable.AGENT),
CleanableTable.from(AuthTokenTable.AUTH_TOKEN),
CleanableTable.from(BackupTable.BACKUP),
CleanableTable.from(BackupTargetTable.BACKUP_TARGET),
CleanableTable.from(CertificateTable.CERTIFICATE),
CleanableTable.from(ClusterHostMapTable.CLUSTER_HOST_MAP),
CleanableTable.from(ConfigItemStatusTable.CONFIG_ITEM_STATUS),
CleanableTable.from(CredentialTable.CREDENTIAL),
CleanableTable.from(CredentialInstanceMapTable.CREDENTIAL_INSTANCE_MAP),
CleanableTable.from(DeploymentUnitTable.DEPLOYMENT_UNIT),
CleanableTable.from(DynamicSchemaTable.DYNAMIC_SCHEMA),
CleanableTable.from(ExternalEventTable.EXTERNAL_EVENT),
CleanableTable.from(ExternalHandlerTable.EXTERNAL_HANDLER),
CleanableTable.from(ExternalHandlerProcessTable.EXTERNAL_HANDLER_PROCESS),
CleanableTable.from(GenericObjectTable.GENERIC_OBJECT),
CleanableTable.from(HealthcheckInstanceTable.HEALTHCHECK_INSTANCE),
CleanableTable.from(HealthcheckInstanceHostMapTable.HEALTHCHECK_INSTANCE_HOST_MAP),
CleanableTable.from(HostTable.HOST),
CleanableTable.from(HostIpAddressMapTable.HOST_IP_ADDRESS_MAP),
CleanableTable.from(ImageTable.IMAGE),
CleanableTable.from(ImageStoragePoolMapTable.IMAGE_STORAGE_POOL_MAP),
CleanableTable.from(InstanceTable.INSTANCE),
CleanableTable.from(InstanceHostMapTable.INSTANCE_HOST_MAP),
CleanableTable.from(InstanceLinkTable.INSTANCE_LINK),
CleanableTable.from(IpAddressTable.IP_ADDRESS),
CleanableTable.from(IpAddressNicMapTable.IP_ADDRESS_NIC_MAP),
CleanableTable.from(LabelTable.LABEL),
CleanableTable.from(MachineDriverTable.MACHINE_DRIVER),
CleanableTable.from(MountTable.MOUNT),
CleanableTable.from(NetworkTable.NETWORK),
CleanableTable.from(NetworkDriverTable.NETWORK_DRIVER),
CleanableTable.from(NicTable.NIC),
CleanableTable.from(PhysicalHostTable.PHYSICAL_HOST),
CleanableTable.from(PortTable.PORT),
CleanableTable.from(ProjectMemberTable.PROJECT_MEMBER),
CleanableTable.from(ResourcePoolTable.RESOURCE_POOL),
CleanableTable.from(ServiceTable.SERVICE),
CleanableTable.from(ServiceConsumeMapTable.SERVICE_CONSUME_MAP),
CleanableTable.from(ServiceExposeMapTable.SERVICE_EXPOSE_MAP),
CleanableTable.from(ServiceIndexTable.SERVICE_INDEX),
CleanableTable.from(SnapshotTable.SNAPSHOT),
CleanableTable.from(StackTable.STACK),
CleanableTable.from(StorageDriverTable.STORAGE_DRIVER),
CleanableTable.from(StoragePoolTable.STORAGE_POOL),
CleanableTable.from(StoragePoolHostMapTable.STORAGE_POOL_HOST_MAP),
CleanableTable.from(SubnetTable.SUBNET),
CleanableTable.from(TaskInstanceTable.TASK_INSTANCE),
CleanableTable.from(UserPreferenceTable.USER_PREFERENCE),
CleanableTable.from(VolumeTable.VOLUME),
CleanableTable.from(VolumeStoragePoolMapTable.VOLUME_STORAGE_POOL_MAP),
// These tables are cleaned through specialized logic but we need to keep them in the "other" list so that they
// are picked up for foreign key references.
CleanableTable.from(ExternalHandlerExternalHandlerProcessMapTable.EXTERNAL_HANDLER_EXTERNAL_HANDLER_PROCESS_MAP),
CleanableTable.from(HostLabelMapTable.HOST_LABEL_MAP),
CleanableTable.from(InstanceLabelMapTable.INSTANCE_LABEL_MAP),
CleanableTable.from(ServiceEventTable.SERVICE_EVENT),
CleanableTable.from(ScheduledUpgradeTable.SCHEDULED_UPGRADE),
CleanableTable.from(SecretTable.SECRET),
CleanableTable.from(ZoneTable.ZONE));
/* The most offending tables never set remove_time
service_event
external_handler_external_handler_process_map
instance_label_map
mount
instance_link
*/
return sortByReferences(tables);
}
@Override
public String getName() {
return "table.cleanup";
}
}