package org.zstack.rest; import org.apache.commons.collections.map.LRUMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.cloudbus.ResourceDestinationMaker; import org.zstack.core.config.GlobalConfig; import org.zstack.core.config.GlobalConfigUpdateExtensionPoint; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.UpdateQuery; import org.zstack.core.thread.PeriodicTask; import org.zstack.core.thread.ThreadFacade; import org.zstack.header.Component; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.APIEvent; import org.zstack.header.message.APIMessage; import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import javax.persistence.Query; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import static org.zstack.utils.CollectionDSL.e; import static org.zstack.utils.CollectionDSL.map; /** * Created by xing5 on 2016/12/8. */ public class MysqlAsyncRestStore implements AsyncRestApiStore, Component { private static final CLogger logger = Utils.getLogger(MysqlAsyncRestStore.class); @Autowired private DatabaseFacade dbf; @Autowired private ResourceDestinationMaker destinationMaker; @Autowired private ThreadFacade thdf; // cache 2000 API results private Map<String, APIEvent> results = Collections.synchronizedMap(new LRUMap(RestGlobalProperty.MAX_CACHED_API_RESULTS)); private Future cleanupThread; @Override public void save(RequestData d) { AsyncRestVO vo = new AsyncRestVO(); vo.setUuid(d.apiMessage.getId()); vo.setRequestData(d.toJson()); vo.setState(AsyncRestState.processing); dbf.persist(vo); } @Override public RequestData complete(APIEvent evt) { RequestData d = null; if (destinationMaker.isManagedByUs(evt.getApiId())) { AsyncRestVO vo = dbf.findByUuid(evt.getApiId(), AsyncRestVO.class); if (vo == null) { // for cases that directly send API message which we don't // have records if (logger.isTraceEnabled()) { logger.warn(String.format("cannot find record for the API event %s", JSONObjectUtil.toJsonString(evt))); } return null; } vo.setState(AsyncRestState.done); vo.setResult(ApiEventResult.toJson(evt)); dbf.update(vo); d = RequestData.fromJson(vo.getRequestData()); } if (!CoreGlobalProperty.UNIT_TEST_ON) { // don't use the cache for unit test // we want to test the database results.put(evt.getApiId(), evt); } return d; } @Override public AsyncRestQueryResult query(String uuid) { AsyncRestQueryResult result = new AsyncRestQueryResult(); result.setUuid(uuid); APIEvent evt = results.get(uuid); if (evt != null) { result.setState(AsyncRestState.done); result.setResult(evt); return result; } AsyncRestVO vo = dbf.findByUuid(uuid, AsyncRestVO.class); if (vo == null) { result.setState(AsyncRestState.expired); return result; } if (vo.getState() != AsyncRestState.done) { result.setState(vo.getState()); return result; } try { result.setState(AsyncRestState.done); result.setResult(ApiEventResult.fromJson(vo.getResult())); results.put(uuid, result.getResult()); } catch (Exception e) { throw new CloudRuntimeException(e); } return result; } @Override public boolean start() { startExpiredApiCleanupThread(); RestGlobalConfig.SCAN_EXPIRED_API_INTERVAL.installUpdateExtension(new GlobalConfigUpdateExtensionPoint() { @Override public void updateGlobalConfig(GlobalConfig oldConfig, GlobalConfig newConfig) { startExpiredApiCleanupThread(); } }); return true; } private void startExpiredApiCleanupThread() { if (cleanupThread != null) { cleanupThread.cancel(true); } cleanupThread = thdf.submitPeriodicTask(new PeriodicTask() { @Override public TimeUnit getTimeUnit() { return TimeUnit.SECONDS; } @Override public long getInterval() { return RestGlobalConfig.SCAN_EXPIRED_API_INTERVAL.value(Integer.class).longValue(); } @Override public String getName() { return "scan-expired-api-records"; } @Override public void run() { try { cleanup(); } catch (Throwable t) { logger.warn("unhandled error", t); } } @Transactional private void cleanup() { String sql = "DELETE FROM AsyncRestVO vo WHERE vo.state = :state and vo.createDate < (NOW() - INTERVAL :period SECOND)"; Query q = dbf.getEntityManager().createQuery(sql); q.setParameter("state", AsyncRestState.done); q.setParameter("period", RestGlobalConfig.COMPLETED_API_EXPIRED_PERIOD.value(Integer.class)); q.executeUpdate(); } }); } @Override public boolean stop() { return true; } }