package com.hubspot.singularity.mesos; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.mesos.Protos.Offer; import org.apache.mesos.Protos.OfferID; import org.apache.mesos.Protos.Status; import org.apache.mesos.SchedulerDriver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalCause; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.inject.Inject; import com.google.inject.Singleton; import com.hubspot.mesos.JavaUtils; import com.hubspot.singularity.config.SingularityConfiguration; import com.hubspot.singularity.mesos.SingularityOfferCache.CachedOffer; @Singleton public class SingularityOfferCache implements OfferCache, RemovalListener<String, CachedOffer> { private static final Logger LOG = LoggerFactory.getLogger(SingularityOfferCache.class); private final Cache<String, CachedOffer> offerCache; private final SchedulerDriverSupplier schedulerDriverSupplier; private final SingularityConfiguration configuration; private final AtomicBoolean useOfferCache = new AtomicBoolean(true); @Inject public SingularityOfferCache(SingularityConfiguration configuration, SchedulerDriverSupplier schedulerDriverSupplier) { this.configuration = configuration; this.schedulerDriverSupplier = schedulerDriverSupplier; offerCache = CacheBuilder.newBuilder() .expireAfterWrite(configuration.getCacheOffersForMillis(), TimeUnit.MILLISECONDS) .maximumSize(configuration.getOfferCacheSize()) .removalListener(this) .build(); } @Override public void cacheOffer(SchedulerDriver driver, long timestamp, Offer offer) { if (!useOfferCache.get()) { driver.declineOffer(offer.getId()); return; } LOG.debug("Caching offer {} for {}", offer.getId().getValue(), JavaUtils.durationFromMillis(configuration.getCacheOffersForMillis())); offerCache.put(offer.getId().getValue(), new CachedOffer(offer)); } @Override public void onRemoval(RemovalNotification<String, CachedOffer> notification) { if (notification.getCause() == RemovalCause.EXPLICIT) { return; } LOG.debug("Cache removal for {} due to {}", notification.getKey(), notification.getCause()); synchronized (offerCache) { if (notification.getValue().offerState == OfferState.AVAILABLE) { declineOffer(notification.getValue()); } else { notification.getValue().expire(); } } } @Override public void rescindOffer(SchedulerDriver driver, OfferID offerId) { offerCache.invalidate(offerId.getValue()); } @Override public void useOffer(CachedOffer cachedOffer) { offerCache.invalidate(cachedOffer.offerId); } @Override public List<CachedOffer> checkoutOffers() { if (!useOfferCache.get()) { return Collections.emptyList(); } List<CachedOffer> offers = new ArrayList<>((int) offerCache.size()); for (CachedOffer cachedOffer : offerCache.asMap().values()) { cachedOffer.checkOut(); offers.add(cachedOffer); } return offers; } @Override public List<Offer> peekOffers() { if (!useOfferCache.get()) { return Collections.emptyList(); } List<Offer> offers = new ArrayList<>((int) offerCache.size()); for (CachedOffer cachedOffer : offerCache.asMap().values()) { offers.add(cachedOffer.offer); } return offers; } @Override public void returnOffer(CachedOffer cachedOffer) { synchronized (offerCache) { if (cachedOffer.offerState == OfferState.EXPIRED) { declineOffer(cachedOffer); } else { cachedOffer.checkIn(); } } } @Override public void disableOfferCache() { useOfferCache.set(false); } @Override public void enableOfferCache() { useOfferCache.set(true); } private void declineOffer(CachedOffer offer) { Optional<SchedulerDriver> driver = schedulerDriverSupplier.get(); if (!driver.isPresent()) { LOG.error("No scheduler driver present to handle expired offer {} - this should never happen", offer.offerId); return; } Status status = driver.get().declineOffer(offer.offer.getId()); LOG.debug("Declined cached offer {} - driver status {}", offer.offerId, status); } private enum OfferState { AVAILABLE, CHECKED_OUT, EXPIRED; } public static class CachedOffer { private final String offerId; private final Offer offer; private OfferState offerState; public CachedOffer(Offer offer) { this.offerId = offer.getId().getValue(); this.offer = offer; this.offerState = OfferState.AVAILABLE; } public Offer getOffer() { return offer; } public String getOfferId() { return offerId; } private void checkOut() { Preconditions.checkState(offerState == OfferState.AVAILABLE, "Offer %s was in state %s", offerId, offerState); this.offerState = OfferState.CHECKED_OUT; } private void checkIn() { Preconditions.checkState(offerState == OfferState.CHECKED_OUT, "Offer %s was in state %s", offerId, offerState); this.offerState = OfferState.AVAILABLE; } private void expire() { Preconditions.checkState(offerState == OfferState.CHECKED_OUT, "Offer %s was in state %s", offerId, offerState); this.offerState = OfferState.EXPIRED; } } }