package com.hubspot.singularity.data; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.utils.ZKPaths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.MetricRegistry; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.Singleton; import com.hubspot.singularity.RequestCleanupType; import com.hubspot.singularity.RequestState; import com.hubspot.singularity.SingularityCreateResult; import com.hubspot.singularity.SingularityDeleteResult; import com.hubspot.singularity.SingularityDeployKey; import com.hubspot.singularity.SingularityPendingRequest; import com.hubspot.singularity.SingularityPendingRequest.PendingType; import com.hubspot.singularity.SingularityRequest; import com.hubspot.singularity.SingularityRequestCleanup; import com.hubspot.singularity.SingularityRequestHistory; import com.hubspot.singularity.SingularityRequestHistory.RequestHistoryType; import com.hubspot.singularity.SingularityRequestLbCleanup; import com.hubspot.singularity.SingularityRequestWithState; import com.hubspot.singularity.SingularityShellCommand; import com.hubspot.singularity.api.SingularityExpiringRequestParent; import com.hubspot.singularity.config.SingularityConfiguration; import com.hubspot.singularity.data.transcoders.Transcoder; import com.hubspot.singularity.event.SingularityEventListener; import com.hubspot.singularity.expiring.SingularityExpiringBounce; import com.hubspot.singularity.expiring.SingularityExpiringPause; import com.hubspot.singularity.expiring.SingularityExpiringRequestActionParent; import com.hubspot.singularity.expiring.SingularityExpiringScale; import com.hubspot.singularity.expiring.SingularityExpiringSkipHealthchecks; import com.hubspot.singularity.scheduler.SingularityLeaderCache; @Singleton public class RequestManager extends CuratorAsyncManager { private static final Logger LOG = LoggerFactory.getLogger(RequestManager.class); private final Transcoder<SingularityRequestWithState> requestTranscoder; private final Transcoder<SingularityPendingRequest> pendingRequestTranscoder; private final Transcoder<SingularityRequestCleanup> requestCleanupTranscoder; private final Transcoder<SingularityRequestHistory> requestHistoryTranscoder; private final Transcoder<SingularityRequestLbCleanup> requestLbCleanupTranscoder; private final SingularityEventListener singularityEventListener; private final SingularityWebCache webCache; private final SingularityLeaderCache leaderCache; private static final String REQUEST_ROOT = "/requests"; private static final String NORMAL_PATH_ROOT = REQUEST_ROOT + "/all"; private static final String PENDING_PATH_ROOT = REQUEST_ROOT + "/pending"; private static final String CLEANUP_PATH_ROOT = REQUEST_ROOT + "/cleanup"; private static final String HISTORY_PATH_ROOT = REQUEST_ROOT + "/history"; private static final String LB_CLEANUP_PATH_ROOT = REQUEST_ROOT + "/lbCleanup"; private static final String BOUNCING_ROOT = REQUEST_ROOT + "/bouncing"; private static final String EXPIRING_ACTION_PATH_ROOT = REQUEST_ROOT + "/expiring"; private static final String EXPIRING_BOUNCE_PATH_ROOT = EXPIRING_ACTION_PATH_ROOT + "/bounce"; private static final String EXPIRING_PAUSE_PATH_ROOT = EXPIRING_ACTION_PATH_ROOT + "/pause"; private static final String EXPIRING_SCALE_PATH_ROOT = EXPIRING_ACTION_PATH_ROOT + "/scale"; private static final String EXPIRING_SKIP_HC_PATH_ROOT = EXPIRING_ACTION_PATH_ROOT + "/skipHc"; private static final Map<Class<? extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>>, String> EXPIRING_CLASS_TO_PATH = ImmutableMap.of( SingularityExpiringBounce.class, EXPIRING_BOUNCE_PATH_ROOT, SingularityExpiringPause.class, EXPIRING_PAUSE_PATH_ROOT, SingularityExpiringScale.class, EXPIRING_SCALE_PATH_ROOT, SingularityExpiringSkipHealthchecks.class, EXPIRING_SKIP_HC_PATH_ROOT ); private final Map<Class<? extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>>, Transcoder<? extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>>> expiringTranscoderMap; @Inject public RequestManager(CuratorFramework curator, SingularityConfiguration configuration, MetricRegistry metricRegistry, SingularityEventListener singularityEventListener, Transcoder<SingularityRequestCleanup> requestCleanupTranscoder, Transcoder<SingularityRequestWithState> requestTranscoder, Transcoder<SingularityRequestLbCleanup> requestLbCleanupTranscoder, Transcoder<SingularityPendingRequest> pendingRequestTranscoder, Transcoder<SingularityRequestHistory> requestHistoryTranscoder, Transcoder<SingularityExpiringBounce> expiringBounceTranscoder, Transcoder<SingularityExpiringScale> expiringScaleTranscoder, Transcoder<SingularityExpiringPause> expiringPauseTranscoder, Transcoder<SingularityExpiringSkipHealthchecks> expiringSkipHealthchecksTranscoder, SingularityWebCache webCache, SingularityLeaderCache leaderCache) { super(curator, configuration, metricRegistry); this.requestTranscoder = requestTranscoder; this.requestCleanupTranscoder = requestCleanupTranscoder; this.pendingRequestTranscoder = pendingRequestTranscoder; this.requestHistoryTranscoder = requestHistoryTranscoder; this.singularityEventListener = singularityEventListener; this.requestLbCleanupTranscoder = requestLbCleanupTranscoder; this.expiringTranscoderMap = ImmutableMap.of( SingularityExpiringBounce.class, expiringBounceTranscoder, SingularityExpiringPause.class, expiringPauseTranscoder, SingularityExpiringScale.class, expiringScaleTranscoder, SingularityExpiringSkipHealthchecks.class, expiringSkipHealthchecksTranscoder ); this.leaderCache = leaderCache; this.webCache = webCache; } private String getRequestPath(String requestId) { return ZKPaths.makePath(NORMAL_PATH_ROOT, requestId); } @SuppressWarnings("unchecked") private <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> String getExpiringPath(T expiringObject) { return getExpiringPath(expiringObject.getClass(), expiringObject.getRequestId()); } private <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> String getExpiringPath(Class<T> clazz, String requestId) { return ZKPaths.makePath(EXPIRING_CLASS_TO_PATH.get(clazz), requestId); } private String getHistoryParentPath(String requestId) { return ZKPaths.makePath(HISTORY_PATH_ROOT, requestId); } private String getHistoryPath(SingularityRequestHistory history) { return ZKPaths.makePath(getHistoryParentPath(history.getRequest().getId()), history.getEventType() + "-" + history.getCreatedAt()); } private String getPendingPath(String requestId, String deployId) { return ZKPaths.makePath(PENDING_PATH_ROOT, new SingularityDeployKey(requestId, deployId).getId()); } private String getPendingPath(SingularityPendingRequest pendingRequest) { String nodeName = String.format("%s%s", new SingularityDeployKey(pendingRequest.getRequestId(), pendingRequest.getDeployId()), pendingRequest.getPendingType().equals(PendingType.ONEOFF) ? pendingRequest.getTimestamp() : ""); return ZKPaths.makePath(PENDING_PATH_ROOT, nodeName); } private String getCleanupPath(String requestId, RequestCleanupType type) { return ZKPaths.makePath(CLEANUP_PATH_ROOT, requestId + "-" + type.name()); } public int getSizeOfPendingQueue() { return getNumChildren(PENDING_PATH_ROOT); } public int getSizeOfCleanupQueue() { return getNumChildren(CLEANUP_PATH_ROOT); } public int getNumLbCleanupRequests() { return getNumChildren(LB_CLEANUP_PATH_ROOT); } public SingularityDeleteResult deletePendingRequest(SingularityPendingRequest pendingRequest) { return delete(getPendingPath(pendingRequest)); } public SingularityDeleteResult deleteHistoryParent(String requestId) { return delete(getHistoryParentPath(requestId)); } public SingularityDeleteResult deleteHistoryItem(SingularityRequestHistory history) { return delete(getHistoryPath(history)); } public boolean cleanupRequestExists(String requestId) { for (RequestCleanupType type : RequestCleanupType.values()) { if (checkExists(getCleanupPath(requestId, type)).isPresent()) { return true; } if (Thread.currentThread().isInterrupted()) { break; } } return false; } public boolean cleanupRequestExists(String requestId, RequestCleanupType type) { return checkExists(getCleanupPath(requestId, type)).isPresent(); } public void deleteCleanRequest(String requestId, RequestCleanupType type) { delete(getCleanupPath(requestId, type)); } public List<String> getAllRequestIds() { return getChildren(NORMAL_PATH_ROOT); } public List<String> getRequestIdsWithHistory() { return getChildren(HISTORY_PATH_ROOT); } public List<SingularityRequestHistory> getRequestHistory(String requestId) { return getAsyncChildren(getHistoryParentPath(requestId), requestHistoryTranscoder); } public SingularityCreateResult createCleanupRequest(SingularityRequestCleanup cleanupRequest) { return create(getCleanupPath(cleanupRequest.getRequestId(), cleanupRequest.getCleanupType()), cleanupRequest, requestCleanupTranscoder); } public SingularityCreateResult update(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return save(request, getRequest(request.getId()).get().getState(), RequestHistoryType.UPDATED, timestamp, user, message); } public SingularityCreateResult save(SingularityRequest request, RequestState state, RequestHistoryType eventType, long timestamp, Optional<String> user, Optional<String> message) { saveHistory(new SingularityRequestHistory(timestamp, user, eventType, request, message)); leaderCache.putRequest(new SingularityRequestWithState(request, state, timestamp)); return save(getRequestPath(request.getId()), new SingularityRequestWithState(request, state, timestamp), requestTranscoder); } public SingularityCreateResult pause(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { markBounceComplete(request.getId()); return save(request, RequestState.PAUSED, RequestHistoryType.PAUSED, timestamp, user, message); } public SingularityCreateResult cooldown(SingularityRequest request, long timestamp) { return save(request, RequestState.SYSTEM_COOLDOWN, RequestHistoryType.ENTERED_COOLDOWN, timestamp, Optional.<String> absent(), Optional.<String> absent()); } public SingularityCreateResult finish(SingularityRequest request, long timestamp) { return save(request, RequestState.FINISHED, RequestHistoryType.FINISHED, timestamp, Optional.<String> absent(), Optional.<String> absent()); } public SingularityCreateResult addToPendingQueue(SingularityPendingRequest pendingRequest) { SingularityCreateResult result = create(getPendingPath(pendingRequest), pendingRequest, pendingRequestTranscoder); LOG.info("{} added to pending queue with result: {}", pendingRequest, result); return result; } public Optional<SingularityPendingRequest> getPendingRequest(String requestId, String deployId) { return getData(getPendingPath(requestId, deployId), pendingRequestTranscoder); } public SingularityCreateResult saveHistory(SingularityRequestHistory history) { final String path = getHistoryPath(history); singularityEventListener.requestHistoryEvent(history); return save(path, history, requestHistoryTranscoder); } public SingularityCreateResult unpause(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return activate(request, RequestHistoryType.UNPAUSED, timestamp, user, message); } public SingularityCreateResult exitCooldown(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return activate(request, RequestHistoryType.EXITED_COOLDOWN, timestamp, user, message); } public SingularityCreateResult bounce(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return activate(request, RequestHistoryType.BOUNCED, timestamp, user, message); } public SingularityCreateResult deployToUnpause(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return save(request, RequestState.DEPLOYING_TO_UNPAUSE, RequestHistoryType.DEPLOYED_TO_UNPAUSE, timestamp, user, message); } public SingularityCreateResult activate(SingularityRequest request, RequestHistoryType historyType, long timestamp, Optional<String> user, Optional<String> message) { return save(request, RequestState.ACTIVE, historyType, timestamp, user, message); } public SingularityCreateResult markDeleting(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { return save(request, RequestState.DELETING, RequestHistoryType.DELETING, timestamp, user, message); } public SingularityDeleteResult markDeleted(SingularityRequest request, long timestamp, Optional<String> user, Optional<String> message) { save(request, RequestState.DELETED, RequestHistoryType.DELETED, timestamp, user, message); if (leaderCache.active()) { leaderCache.deleteRequest(request.getId()); } return delete(getRequestPath(request.getId())); } public List<SingularityPendingRequest> getPendingRequests() { return getAsyncChildren(PENDING_PATH_ROOT, pendingRequestTranscoder); } public List<SingularityRequestCleanup> getCleanupRequests() { return getAsyncChildren(CLEANUP_PATH_ROOT, requestCleanupTranscoder); } public List<SingularityRequestWithState> getRequests(Collection<String> requestIds) { return getRequests(requestIds, false); } public List<SingularityRequestWithState> getRequests(Collection<String> requestIds, boolean useWebCache) { if (leaderCache.active()) { return leaderCache.getRequests().stream().filter((r) -> requestIds.contains(r.getRequest().getId())).collect(Collectors.toList()); } if (useWebCache) { if (webCache.useCachedRequests()) { return webCache.getRequests().stream().filter((r) -> requestIds.contains(r.getRequest().getId())).collect(Collectors.toList()); } else { List<SingularityRequestWithState> requests = getRequests(true); webCache.cacheRequests(requests); return requests.stream().filter((r) -> requestIds.contains(r.getRequest().getId())).collect(Collectors.toList()); } } final List<String> paths = Lists.newArrayListWithCapacity(requestIds.size()); for (String requestId : requestIds) { paths.add(getRequestPath(requestId)); } return getAsync("getRequests", paths, requestTranscoder); } private Iterable<SingularityRequestWithState> filter(List<SingularityRequestWithState> requests, final RequestState... states) { return Iterables.filter(requests, new Predicate<SingularityRequestWithState>() { @Override public boolean apply(SingularityRequestWithState input) { for (RequestState state : states) { if (input.getState() == state) { return true; } } return false; } }); } private Iterable<SingularityRequestWithState> getRequests(boolean useWebCache, RequestState... states) { return filter(getRequests(useWebCache), states); } public Iterable<SingularityRequestWithState> getPausedRequests(boolean useWebCache) { return getRequests(useWebCache, RequestState.PAUSED); } public Iterable<SingularityRequestWithState> getActiveRequests() { return getActiveRequests(false); } public Iterable<SingularityRequestWithState> getActiveRequests(boolean useWebCache) { return getRequests(useWebCache, RequestState.ACTIVE, RequestState.DEPLOYING_TO_UNPAUSE); } public Iterable<SingularityRequestWithState> getCooldownRequests(boolean useWebCache) { return getRequests(useWebCache, RequestState.SYSTEM_COOLDOWN); } public Iterable<SingularityRequestWithState> getFinishedRequests(boolean useWebCache) { return getRequests(useWebCache, RequestState.FINISHED); } public void activateLeaderCache() { leaderCache.cacheRequests(fetchRequests()); } public List<SingularityRequestWithState> getRequests() { return getRequests(false); } public List<SingularityRequestWithState> getRequests(boolean useWebCache) { if (leaderCache.active()) { return leaderCache.getRequests(); } if (useWebCache && webCache.useCachedRequests()) { return webCache.getRequests(); } List<SingularityRequestWithState> requests = fetchRequests(); if (useWebCache) { webCache.cacheRequests(requests); } return requests; } public List<SingularityRequestWithState> fetchRequests() { return getAsyncChildren(NORMAL_PATH_ROOT, requestTranscoder); } public Optional<SingularityRequestWithState> getRequest(String requestId) { return getRequest(requestId, false); } public Optional<SingularityRequestWithState> getRequest(String requestId, boolean useWebCache) { if (leaderCache.active()) { return leaderCache.getRequest(requestId); } if (useWebCache && webCache.useCachedRequests()) { return webCache.getRequest(requestId); } return getData(getRequestPath(requestId), requestTranscoder); } public void startDeletingRequest(SingularityRequest request, Optional<String> user, Optional<String> actionId, Optional<String> message) { final long now = System.currentTimeMillis(); // delete it no matter if the delete request already exists. createCleanupRequest(new SingularityRequestCleanup(user, RequestCleanupType.DELETING, now, Optional.of(Boolean.TRUE), request.getId(), Optional.<String> absent(), Optional.<Boolean> absent(), message, actionId, Optional.<SingularityShellCommand>absent())); markDeleting(request, System.currentTimeMillis(), user, message); LOG.info("Request {} enqueued for deletion by {} - {}", request.getId(), user, message); } public List<SingularityRequestLbCleanup> getLbCleanupRequests() { return getAsyncChildren(LB_CLEANUP_PATH_ROOT, requestLbCleanupTranscoder); } public Optional<SingularityRequestLbCleanup> getLbCleanupRequest(String requestId) { return getData(getLbCleanupPath(requestId), requestLbCleanupTranscoder); } public List<String> getLbCleanupRequestIds() { return getChildren(LB_CLEANUP_PATH_ROOT); } private String getLbCleanupPath(String requestId) { return ZKPaths.makePath(LB_CLEANUP_PATH_ROOT, requestId); } public void saveLbCleanupRequest(SingularityRequestLbCleanup cleanup) { save(getLbCleanupPath(cleanup.getRequestId()), cleanup, requestLbCleanupTranscoder); } public SingularityDeleteResult deleteLbCleanupRequest(String requestId) { return delete(getLbCleanupPath(requestId)); } public <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> List<T> getExpiringObjects(Class<T> clazz) { return getAsyncChildren(EXPIRING_CLASS_TO_PATH.get(clazz), getTranscoder(clazz)); } @SuppressWarnings("unchecked") private <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> Transcoder<T> getTranscoder(Class<T> clazz) { return (Transcoder<T>) expiringTranscoderMap.get(clazz); } @SuppressWarnings("unchecked") private <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> Transcoder<T> getTranscoder(T expiringObject) { return getTranscoder((Class<T>) expiringObject.getClass()); } public <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> Optional<T> getExpiringObject(Class<T> clazz, String requestId) { return getData(getExpiringPath(clazz, requestId), getTranscoder(clazz)); } public <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> SingularityCreateResult saveExpiringObject(T expiringObject) { return save(getExpiringPath(expiringObject), expiringObject, getTranscoder(expiringObject)); } public <T extends SingularityExpiringRequestActionParent<? extends SingularityExpiringRequestParent>> SingularityDeleteResult deleteExpiringObject(Class<T> clazz, String requestId) { return delete(getExpiringPath(clazz, requestId)); } public Optional<SingularityExpiringBounce> getExpiringBounce(String requestId) { return getExpiringObject(SingularityExpiringBounce.class, requestId); } public Optional<SingularityExpiringPause> getExpiringPause(String requestId) { return getExpiringObject(SingularityExpiringPause.class, requestId); } public Optional<SingularityExpiringScale> getExpiringScale(String requestId) { return getExpiringObject(SingularityExpiringScale.class, requestId); } public Optional<SingularityExpiringSkipHealthchecks> getExpiringSkipHealthchecks(String requestId) { return getExpiringObject(SingularityExpiringSkipHealthchecks.class, requestId); } public String getIsBouncingPath(String requestId) { return ZKPaths.makePath(BOUNCING_ROOT, requestId); } public SingularityCreateResult markAsBouncing(String requestId) { return create(getIsBouncingPath(requestId)); } public SingularityDeleteResult markBounceComplete(String requestId) { return delete(getIsBouncingPath(requestId)); } }