/**
* 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.reconciliation;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import javax.inject.Inject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.AbstractIdleService;
import org.apache.aurora.common.quantity.Amount;
import org.apache.aurora.common.quantity.Time;
import org.apache.aurora.common.stats.StatsProvider;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.mesos.Driver;
import org.apache.aurora.scheduler.reconciliation.ReconciliationModule.BackgroundWorker;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.mesos.v1.Protos;
import org.apache.mesos.v1.Protos.TaskStatus;
import static java.util.Objects.requireNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.aurora.common.quantity.Time.MINUTES;
import static org.apache.aurora.common.quantity.Time.SECONDS;
/**
* A task reconciler that periodically triggers Mesos (implicit) and Aurora (explicit) task
* reconciliation to synchronize global task states. More on task reconciliation:
* http://mesos.apache.org/documentation/latest/reconciliation.
*/
public class TaskReconciler extends AbstractIdleService {
@VisibleForTesting
static final String EXPLICIT_STAT_NAME = "reconciliation_explicit_runs";
@VisibleForTesting
static final String IMPLICIT_STAT_NAME = "reconciliation_implicit_runs";
private final TaskReconcilerSettings settings;
private final Storage storage;
private final Driver driver;
private final ScheduledExecutorService executor;
private final AtomicLong explicitRuns;
private final AtomicLong implicitRuns;
static class TaskReconcilerSettings {
private final Amount<Long, Time> explicitInterval;
private final Amount<Long, Time> implicitInterval;
private final long explicitDelayMinutes;
private final long implicitDelayMinutes;
private final long explicitBatchDelaySeconds;
private final int explicitBatchSize;
@VisibleForTesting
TaskReconcilerSettings(
Amount<Long, Time> initialDelay,
Amount<Long, Time> explicitInterval,
Amount<Long, Time> implicitInterval,
Amount<Long, Time> scheduleSpread,
Amount<Long, Time> explicitBatchInterval,
int explicitBatchSize) {
this.explicitInterval = requireNonNull(explicitInterval);
this.implicitInterval = requireNonNull(implicitInterval);
explicitDelayMinutes = requireNonNull(initialDelay).as(MINUTES);
implicitDelayMinutes = initialDelay.as(MINUTES) + scheduleSpread.as(MINUTES);
explicitBatchDelaySeconds = explicitBatchInterval.as(SECONDS);
this.explicitBatchSize = explicitBatchSize;
checkArgument(
explicitDelayMinutes >= 0,
"Invalid explicit reconciliation delay: %s", explicitDelayMinutes);
checkArgument(
implicitDelayMinutes >= 0L,
"Invalid implicit reconciliation delay: %s", implicitDelayMinutes);
checkArgument(
explicitBatchDelaySeconds >= 0L,
"Invalid explicit batch reconciliation delay: %s", explicitBatchDelaySeconds
);
}
}
@Inject
TaskReconciler(
TaskReconcilerSettings settings,
Storage storage,
Driver driver,
@BackgroundWorker ScheduledExecutorService executor,
StatsProvider stats) {
this.settings = requireNonNull(settings);
this.storage = requireNonNull(storage);
this.driver = requireNonNull(driver);
this.executor = requireNonNull(executor);
this.explicitRuns = stats.makeCounter(EXPLICIT_STAT_NAME);
this.implicitRuns = stats.makeCounter(IMPLICIT_STAT_NAME);
}
public void triggerExplicitReconciliation(Optional<Integer> batchSize) {
doExplicitReconcile(batchSize.or(settings.explicitBatchSize));
}
public void triggerImplicitReconciliation() {
doImplicitReconcile();
}
@Override
protected void startUp() {
scheduleExplicitReconciliation();
scheduleImplicitReconciliation();
}
private void scheduleExplicitReconciliation() {
executor.scheduleAtFixedRate(
() -> doExplicitReconcile(settings.explicitBatchSize),
settings.explicitDelayMinutes,
settings.explicitInterval.as(MINUTES),
MINUTES.getTimeUnit());
}
private void scheduleImplicitReconciliation() {
executor.scheduleAtFixedRate(
() -> doImplicitReconcile(),
settings.implicitDelayMinutes,
settings.implicitInterval.as(MINUTES),
MINUTES.getTimeUnit());
}
private void doImplicitReconcile() {
driver.reconcileTasks(ImmutableSet.of());
implicitRuns.incrementAndGet();
}
private void doExplicitReconcile(int batchSize) {
Iterable<List<IScheduledTask>> activeBatches = Iterables.partition(
Storage.Util.fetchTasks(storage, Query.unscoped().byStatus(Tasks.SLAVE_ASSIGNED_STATES)),
batchSize);
long delay = 0;
for (List<IScheduledTask> batch : activeBatches) {
executor.schedule(() -> driver.reconcileTasks(
batch.stream().map(TASK_TO_PROTO::apply).collect(Collectors.toList())),
delay,
SECONDS.getTimeUnit());
delay += settings.explicitBatchDelaySeconds;
}
explicitRuns.incrementAndGet();
}
@Override
protected void shutDown() {
// Nothing to do - await VM shutdown.
}
@VisibleForTesting
static final Function<IScheduledTask, TaskStatus> TASK_TO_PROTO = t -> TaskStatus.newBuilder()
// TODO(maxim): State is required by protobuf but ignored by Mesos for reconciliation
// purposes. This is the artifact of the native API. The new HTTP Mesos API will be
// accepting task IDs instead. AURORA-1326 tracks solution on the scheduler side.
// Setting TASK_RUNNING as a safe dummy value here.
.setState(Protos.TaskState.TASK_RUNNING)
.setAgentId(
Protos.AgentID.newBuilder().setValue(t.getAssignedTask().getSlaveId()).build())
.setTaskId(Protos.TaskID.newBuilder().setValue(t.getAssignedTask().getTaskId()).build())
.build();
}