package org.zstack.storage.primary; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.config.GlobalConfig; import org.zstack.core.config.GlobalConfigUpdateExtensionPoint; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.thread.PeriodicTask; import org.zstack.core.thread.ThreadFacade; import org.zstack.header.Component; import org.zstack.header.managementnode.ManagementNodeChangeListener; import org.zstack.header.message.MessageReply; import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.storage.primary.*; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; import javax.persistence.LockModeType; import javax.persistence.Query; import javax.persistence.TypedQuery; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; public class ImageCacheGarbageCollector implements Component, ManagementNodeChangeListener, PeriodicTask { private static CLogger logger = Utils.getLogger(ImageCacheGarbageCollector.class); @Autowired private ThreadFacade thdf; @Autowired private DatabaseFacade dbf; @Autowired private CloudBus bus; private int garbageCollectionInterval; private Future<Void> garbageCollectionThread; @Override public boolean start() { garbageCollectionInterval = PrimaryStorageGlobalConfig.IMAGE_CACHE_GARBAGE_COLLECTOR_INTERVAL.value(Integer.class); PrimaryStorageGlobalConfig.IMAGE_CACHE_GARBAGE_COLLECTOR_INTERVAL.installUpdateExtension(new GlobalConfigUpdateExtensionPoint() { @Override public void updateGlobalConfig(GlobalConfig oldConfig, GlobalConfig newConfig) { garbageCollectionInterval = newConfig.value(Integer.class); if (garbageCollectionThread != null) { garbageCollectionThread.cancel(true); } startGarbageCollectionThread(); } }); return true; } @Override public boolean stop() { return true; } private void startGarbageCollectionThread() { garbageCollectionThread = thdf.submitPeriodicTask(this); logger.debug(String.format("Image cache garbage collector starts running by interval[%ss]", garbageCollectionInterval)); } @Override public void nodeJoin(String nodeId) { startGarbageCollectionThread(); } @Override public void nodeLeft(String nodeId) { } @Override public void iAmDead(String nodeId) { } @Override public void iJoin(String nodeId) { } @Transactional private List<ImageCacheVO> getImageCacheToDelete() { String sql = "select i from ImageCacheVO i where i.imageUuid = NULL and i.state = :state"; TypedQuery<ImageCacheVO> q = dbf.getEntityManager().createQuery(sql, ImageCacheVO.class); q.setLockMode(LockModeType.PESSIMISTIC_WRITE); q.setParameter("state", ImageCacheState.ready); List<ImageCacheVO> ret = q.getResultList(); // if ImageCacheVO in deleting state and it has been stayed for 1 day // that means zstack that issued garbage collection exited before removing this entry from database // we garbage this entry again here sql = "select i from ImageCacheVO i where i.imageUuid = NULL and i.state = :state and CURRENT_TIMESTAMP > DATE_ADD(i.lastOpDate, INTERVAL 1 DAY)"; Query q1 = dbf.getEntityManager().createNativeQuery(sql, ImageCacheVO.class); q1.setLockMode(LockModeType.PESSIMISTIC_WRITE); q1.setParameter("state", ImageCacheState.deleting); ret.addAll(q1.getResultList()); if (ret.isEmpty()) { return ret; } List<Long> ids = new ArrayList<Long>(ret.size()); for (ImageCacheVO i : ret) { ids.add(i.getId()); } sql = "update ImageCacheVO i set i.state = :state where i.id in (:ids)"; TypedQuery<ImageCacheVO> q2 = dbf.getEntityManager().createQuery(sql, ImageCacheVO.class); q2.setParameter("state", ImageCacheState.deleting); q2.setParameter("ids", ids); q2.executeUpdate(); return ret; } private void deleteImageCacheOnPrimaryStorage(final ImageCacheVO ic) { PrimaryStorageRemoveCachedImageMsg msg = new PrimaryStorageRemoveCachedImageMsg(); msg.setInventory(ImageCacheInventory.valueOf(ic)); bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ic.getPrimaryStorageUuid()); bus.send(msg, new CloudBusCallBack(null) { private void fail(String error) { ic.setState(ImageCacheState.ready); dbf.update(ic); logger.warn(String.format("failed to garbage collect image cache[id:%s, install:%s] on primary storage[uuid], because %s. Change its state back to ready and try garbage collecting it next time", ic.getId(), ic.getInstallUrl(), ic.getPrimaryStorageUuid(), error)); } private void success() { dbf.remove(ic); logger.debug(String.format("successfully garbage collected image cache[id:%s, install url:%s] on primary storage[uuid:%s]", ic.getId(), ic.getInstallUrl(), ic.getPrimaryStorageUuid())); } @Override public void run(MessageReply reply) { if (reply.isSuccess()) { success(); } else { fail(reply.getError().toString()); } } }); } @Override public void run() { try { List<ImageCacheVO> ics = getImageCacheToDelete(); for (ImageCacheVO i : ics) { deleteImageCacheOnPrimaryStorage(i); } } catch (Exception e) { logger.warn(e.getMessage(), e); } } @Override public TimeUnit getTimeUnit() { return TimeUnit.SECONDS; } @Override public long getInterval() { return garbageCollectionInterval; } @Override public String getName() { return this.getClass().getSimpleName(); } }