Java tutorial
/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.aurora.scheduler.async; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; import javax.inject.Inject; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import com.google.protobuf.ByteString; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Data; import com.twitter.common.quantity.Time; import com.twitter.common.stats.Stats; import com.twitter.common.stats.StatsProvider; import com.twitter.common.util.Clock; import com.twitter.common.util.Random; import org.apache.aurora.Protobufs; import org.apache.aurora.codec.ThriftBinaryCodec; import org.apache.aurora.codec.ThriftBinaryCodec.CodingException; import org.apache.aurora.gen.comm.AdjustRetainedTasks; import org.apache.aurora.scheduler.TaskLauncher; import org.apache.aurora.scheduler.base.CommandUtil; import org.apache.aurora.scheduler.base.Query; import org.apache.aurora.scheduler.base.Tasks; import org.apache.aurora.scheduler.configuration.Resources; import org.apache.aurora.scheduler.mesos.Driver; import org.apache.aurora.scheduler.storage.Storage; import org.apache.aurora.scheduler.storage.entities.IScheduledTask; import org.apache.mesos.Protos; import org.apache.mesos.Protos.ExecutorID; import org.apache.mesos.Protos.ExecutorInfo; import org.apache.mesos.Protos.Offer; import org.apache.mesos.Protos.OfferID; import org.apache.mesos.Protos.SlaveID; import org.apache.mesos.Protos.TaskID; import org.apache.mesos.Protos.TaskInfo; import org.apache.mesos.Protos.TaskStatus; import static java.util.Objects.requireNonNull; /** * A task launcher that periodically initiates garbage collection on a host, re-using a single * garbage collection executor. */ public class GcExecutorLauncher implements TaskLauncher { private static final Logger LOG = Logger.getLogger(GcExecutorLauncher.class.getName()); private final AtomicLong tasksCreated = Stats.exportLong("scheduler_gc_tasks_created"); private final AtomicLong offersConsumed = Stats.exportLong("scheduler_gc_offers_consumed"); @VisibleForTesting static final Resources TOTAL_GC_EXECUTOR_RESOURCES = new Resources(0.2, Amount.of(128L, Data.MB), Amount.of(16L, Data.MB), 0); // An epsilon is used because we are required to supply executor and task resources. @VisibleForTesting static final Resources EPSILON = new Resources(0.01, Amount.of(1L, Data.MB), Amount.of(1L, Data.MB), 0); private static final Resources GC_EXECUTOR_TASK_RESOURCES = Resources.subtract(TOTAL_GC_EXECUTOR_RESOURCES, EPSILON); @VisibleForTesting static final String SYSTEM_TASK_PREFIX = "system-gc-"; @VisibleForTesting static final String LOST_TASKS_STAT_NAME = "gc_executor_tasks_lost"; @VisibleForTesting static final String INSUFFICIENT_OFFERS_STAT_NAME = "scheduler_gc_insufficient_offers"; private static final String EXECUTOR_NAME = "aurora.gc"; private final GcExecutorSettings settings; private final Storage storage; private final Clock clock; private final Executor executor; private final Driver driver; private final Supplier<String> uuidGenerator; private final Map<String, Long> pulses; private final AtomicLong lostTasks; private final AtomicLong insufficientOffers; @Inject GcExecutorLauncher(GcExecutorSettings settings, Storage storage, Clock clock, Executor executor, Driver driver, StatsProvider statsProvider) { this(settings, storage, clock, executor, driver, statsProvider, new Supplier<String>() { @Override public String get() { return UUID.randomUUID().toString(); } }); } @VisibleForTesting GcExecutorLauncher(GcExecutorLauncher.GcExecutorSettings settings, Storage storage, Clock clock, Executor executor, Driver driver, StatsProvider statsProvider, Supplier<String> uuidGenerator) { this.settings = requireNonNull(settings); this.storage = requireNonNull(storage); this.clock = requireNonNull(clock); this.executor = requireNonNull(executor); this.driver = requireNonNull(driver); this.uuidGenerator = requireNonNull(uuidGenerator); this.pulses = Collections.synchronizedMap(Maps.<String, Long>newHashMap()); this.lostTasks = statsProvider.makeCounter(LOST_TASKS_STAT_NAME); this.insufficientOffers = statsProvider.makeCounter(INSUFFICIENT_OFFERS_STAT_NAME); } @VisibleForTesting static TaskInfo makeGcTask(String sourceName, SlaveID slaveId, String gcExecutorPath, String uuid, AdjustRetainedTasks message) { ExecutorInfo.Builder executorInfo = ExecutorInfo.newBuilder() .setExecutorId(ExecutorID.newBuilder().setValue(EXECUTOR_NAME)).setName(EXECUTOR_NAME) .setSource(sourceName).addAllResources(GC_EXECUTOR_TASK_RESOURCES.toResourceList()) .setCommand(CommandUtil.create(gcExecutorPath)); byte[] data; try { data = ThriftBinaryCodec.encode(message); } catch (CodingException e) { LOG.severe("Failed to encode retained tasks message: " + message); throw Throwables.propagate(e); } return TaskInfo.newBuilder().setName("system-gc") .setTaskId(TaskID.newBuilder().setValue(SYSTEM_TASK_PREFIX + uuid)).setSlaveId(slaveId) .setData(ByteString.copyFrom(data)).setExecutor(executorInfo) .addAllResources(EPSILON.toResourceList()).build(); } private TaskInfo makeGcTask(String hostName, SlaveID slaveId) { Set<IScheduledTask> tasksOnHost = Storage.Util.weaklyConsistentFetchTasks(storage, Query.slaveScoped(hostName)); tasksCreated.incrementAndGet(); return makeGcTask(hostName, slaveId, settings.getGcExecutorPath().get(), uuidGenerator.get(), new AdjustRetainedTasks() .setRetainedTasks(Maps.transformValues(Tasks.mapById(tasksOnHost), Tasks.GET_STATUS))); } private boolean sufficientResources(Offer offer) { boolean sufficient = Resources.from(offer).greaterThanOrEqual(TOTAL_GC_EXECUTOR_RESOURCES); if (!sufficient) { LOG.warning("Offer for host " + offer.getHostname() + " is too small for a GC executor"); insufficientOffers.incrementAndGet(); } return sufficient; } @Override public boolean willUse(final Offer offer) { if (!settings.getGcExecutorPath().isPresent() || !sufficientResources(offer) || !isTimeToCollect(offer.getHostname())) { return false; } executor.execute(new Runnable() { @Override public void run() { driver.launchTask(offer.getId(), makeGcTask(offer.getHostname(), offer.getSlaveId())); } }); offersConsumed.incrementAndGet(); return true; } @Override public boolean statusUpdate(TaskStatus status) { if (status.getTaskId().getValue().startsWith(SYSTEM_TASK_PREFIX)) { LOG.info("Received status update for GC task: " + Protobufs.toString(status)); if (status.getState() == Protos.TaskState.TASK_LOST) { lostTasks.incrementAndGet(); } return true; } else { return false; } } @Override public void cancelOffer(OfferID offer) { // No-op. } private boolean isTimeToCollect(String hostname) { boolean result = false; Optional<Long> timestamp = Optional.fromNullable(pulses.get(hostname)); if (timestamp.isPresent()) { if (clock.nowMillis() >= timestamp.get()) { pulses.put(hostname, clock.nowMillis() + settings.getDelayMs()); result = true; } } else { pulses.put(hostname, clock.nowMillis() + settings.getDelayMs()); } return result; } public static class GcExecutorSettings { protected final Amount<Long, Time> gcInterval; private final Optional<String> gcExecutorPath; @VisibleForTesting GcExecutorSettings(Amount<Long, Time> gcInterval, Optional<String> gcExecutorPath) { this.gcInterval = requireNonNull(gcInterval); this.gcExecutorPath = requireNonNull(gcExecutorPath); } @VisibleForTesting int getDelayMs() { return gcInterval.as(Time.MILLISECONDS).intValue(); } @VisibleForTesting Optional<String> getGcExecutorPath() { return gcExecutorPath; } } /** * Wraps configuration values for the {@code GcExecutorLauncher}. */ static class RandomGcExecutorSettings extends GcExecutorSettings { private final Random rand = new Random.SystemRandom(new java.util.Random()); RandomGcExecutorSettings(Amount<Long, Time> gcInterval, Optional<String> gcExecutorPath) { super(gcInterval, gcExecutorPath); } @Override int getDelayMs() { return rand.nextInt(gcInterval.as(Time.MILLISECONDS).intValue()); } } }