/**
* 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.storage.db;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
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.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import org.apache.aurora.common.base.MorePreconditions;
import org.apache.aurora.common.stats.StatsProvider;
import org.apache.aurora.gen.JobUpdateAction;
import org.apache.aurora.gen.JobUpdateStatus;
import org.apache.aurora.gen.storage.StoredJobUpdateDetails;
import org.apache.aurora.scheduler.storage.JobUpdateStore;
import org.apache.aurora.scheduler.storage.db.views.DbJobUpdate;
import org.apache.aurora.scheduler.storage.db.views.DbJobUpdateInstructions;
import org.apache.aurora.scheduler.storage.db.views.DbStoredJobUpdateDetails;
import org.apache.aurora.scheduler.storage.entities.IInstanceTaskConfig;
import org.apache.aurora.scheduler.storage.entities.IJobInstanceUpdateEvent;
import org.apache.aurora.scheduler.storage.entities.IJobUpdate;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateDetails;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateEvent;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateInstructions;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateKey;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateQuery;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateSummary;
import org.apache.aurora.scheduler.storage.entities.IMetadata;
import org.apache.aurora.scheduler.storage.entities.IRange;
import static java.util.Objects.requireNonNull;
import static org.apache.aurora.common.inject.TimedInterceptor.Timed;
/**
* A relational database-backed job update store.
*/
public class DbJobUpdateStore implements JobUpdateStore.Mutable {
private final JobKeyMapper jobKeyMapper;
private final JobUpdateDetailsMapper detailsMapper;
private final JobUpdateEventMapper jobEventMapper;
private final JobInstanceUpdateEventMapper instanceEventMapper;
private final TaskConfigManager taskConfigManager;
private final LoadingCache<JobUpdateStatus, AtomicLong> jobUpdateEventStats;
private final LoadingCache<JobUpdateAction, AtomicLong> jobUpdateActionStats;
@Inject
DbJobUpdateStore(
JobKeyMapper jobKeyMapper,
JobUpdateDetailsMapper detailsMapper,
JobUpdateEventMapper jobEventMapper,
JobInstanceUpdateEventMapper instanceEventMapper,
TaskConfigManager taskConfigManager,
StatsProvider statsProvider) {
this.jobKeyMapper = requireNonNull(jobKeyMapper);
this.detailsMapper = requireNonNull(detailsMapper);
this.jobEventMapper = requireNonNull(jobEventMapper);
this.instanceEventMapper = requireNonNull(instanceEventMapper);
this.taskConfigManager = requireNonNull(taskConfigManager);
this.jobUpdateEventStats = CacheBuilder.newBuilder()
.build(new CacheLoader<JobUpdateStatus, AtomicLong>() {
@Override
public AtomicLong load(JobUpdateStatus status) {
return statsProvider.makeCounter(jobUpdateStatusStatName(status));
}
});
for (JobUpdateStatus status : JobUpdateStatus.values()) {
jobUpdateEventStats.getUnchecked(status).get();
}
this.jobUpdateActionStats = CacheBuilder.newBuilder()
.build(new CacheLoader<JobUpdateAction, AtomicLong>() {
@Override
public AtomicLong load(JobUpdateAction action) {
return statsProvider.makeCounter(jobUpdateActionStatName(action));
}
});
for (JobUpdateAction action : JobUpdateAction.values()) {
jobUpdateActionStats.getUnchecked(action).get();
}
}
@Timed("job_update_store_save_update")
@Override
public void saveJobUpdate(IJobUpdate update, Optional<String> lockToken) {
requireNonNull(update);
if (!update.getInstructions().isSetDesiredState()
&& update.getInstructions().getInitialState().isEmpty()) {
throw new IllegalArgumentException(
"Missing both initial and desired states. At least one is required.");
}
IJobUpdateKey key = update.getSummary().getKey();
jobKeyMapper.merge(key.getJob());
detailsMapper.insert(update.newBuilder());
if (lockToken.isPresent()) {
detailsMapper.insertLockToken(key, lockToken.get());
}
if (!update.getSummary().getMetadata().isEmpty()) {
detailsMapper.insertJobUpdateMetadata(
key,
IMetadata.toBuildersSet(update.getSummary().getMetadata()));
}
// Insert optional instance update overrides.
Set<IRange> instanceOverrides =
update.getInstructions().getSettings().getUpdateOnlyTheseInstances();
if (!instanceOverrides.isEmpty()) {
detailsMapper.insertInstanceOverrides(key, IRange.toBuildersSet(instanceOverrides));
}
// Insert desired state task config and instance mappings.
if (update.getInstructions().isSetDesiredState()) {
IInstanceTaskConfig desired = update.getInstructions().getDesiredState();
detailsMapper.insertTaskConfig(
key,
taskConfigManager.insert(desired.getTask()),
true,
new InsertResult());
detailsMapper.insertDesiredInstances(
key,
IRange.toBuildersSet(MorePreconditions.checkNotBlank(desired.getInstances())));
}
// Insert initial state task configs and instance mappings.
if (!update.getInstructions().getInitialState().isEmpty()) {
for (IInstanceTaskConfig config : update.getInstructions().getInitialState()) {
InsertResult result = new InsertResult();
detailsMapper.insertTaskConfig(
key,
taskConfigManager.insert(config.getTask()),
false,
result);
detailsMapper.insertTaskConfigInstances(
result.getId(),
IRange.toBuildersSet(MorePreconditions.checkNotBlank(config.getInstances())));
}
}
}
@VisibleForTesting
static String jobUpdateStatusStatName(JobUpdateStatus status) {
return "update_transition_" + status;
}
@Timed("job_update_store_save_event")
@Override
public void saveJobUpdateEvent(IJobUpdateKey key, IJobUpdateEvent event) {
jobEventMapper.insert(key, event.newBuilder());
jobUpdateEventStats.getUnchecked(event.getStatus()).incrementAndGet();
}
@VisibleForTesting
static String jobUpdateActionStatName(JobUpdateAction action) {
return "update_instance_transition_" + action;
}
@Timed("job_update_store_save_instance_event")
@Override
public void saveJobInstanceUpdateEvent(IJobUpdateKey key, IJobInstanceUpdateEvent event) {
instanceEventMapper.insert(key, event.newBuilder());
jobUpdateActionStats.getUnchecked(event.getAction()).incrementAndGet();
}
@Timed("job_update_store_delete_all")
@Override
public void deleteAllUpdatesAndEvents() {
detailsMapper.truncate();
}
private static final Function<PruneVictim, IJobUpdateKey> GET_UPDATE_KEY =
victim -> IJobUpdateKey.build(victim.getUpdate());
@Timed("job_update_store_prune_history")
@Override
public Set<IJobUpdateKey> pruneHistory(int perJobRetainCount, long historyPruneThresholdMs) {
ImmutableSet.Builder<IJobUpdateKey> pruned = ImmutableSet.builder();
Set<Long> jobKeyIdsToPrune = detailsMapper.selectJobKeysForPruning(
perJobRetainCount,
historyPruneThresholdMs);
for (long jobKeyId : jobKeyIdsToPrune) {
Set<PruneVictim> pruneVictims = detailsMapper.selectPruneVictims(
jobKeyId,
perJobRetainCount,
historyPruneThresholdMs);
detailsMapper.deleteCompletedUpdates(
FluentIterable.from(pruneVictims).transform(PruneVictim::getRowId).toSet());
pruned.addAll(FluentIterable.from(pruneVictims).transform(GET_UPDATE_KEY));
}
return pruned.build();
}
@Timed("job_update_store_fetch_summaries")
@Override
public List<IJobUpdateSummary> fetchJobUpdateSummaries(IJobUpdateQuery query) {
return IJobUpdateSummary.listFromBuilders(detailsMapper.selectSummaries(query.newBuilder()));
}
@Timed("job_update_store_fetch_details_list")
@Override
public List<IJobUpdateDetails> fetchJobUpdateDetails(IJobUpdateQuery query) {
return FluentIterable
.from(detailsMapper.selectDetailsList(query.newBuilder()))
.transform(DbStoredJobUpdateDetails::toThrift)
.transform(StoredJobUpdateDetails::getDetails)
.transform(IJobUpdateDetails::build)
.toList();
}
@Timed("job_update_store_fetch_details")
@Override
public Optional<IJobUpdateDetails> fetchJobUpdateDetails(final IJobUpdateKey key) {
return Optional.fromNullable(detailsMapper.selectDetails(key))
.transform(DbStoredJobUpdateDetails::toThrift)
.transform(StoredJobUpdateDetails::getDetails)
.transform(IJobUpdateDetails::build);
}
@Timed("job_update_store_fetch_update")
@Override
public Optional<IJobUpdate> fetchJobUpdate(IJobUpdateKey key) {
return Optional.fromNullable(detailsMapper.selectUpdate(key))
.transform(DbJobUpdate::toImmutable);
}
@Timed("job_update_store_fetch_instructions")
@Override
public Optional<IJobUpdateInstructions> fetchJobUpdateInstructions(IJobUpdateKey key) {
return Optional.fromNullable(detailsMapper.selectInstructions(key))
.transform(DbJobUpdateInstructions::toImmutable);
}
@Timed("job_update_store_fetch_all_details")
@Override
public Set<StoredJobUpdateDetails> fetchAllJobUpdateDetails() {
return FluentIterable.from(detailsMapper.selectAllDetails())
.transform(DbStoredJobUpdateDetails::toThrift)
.toSet();
}
@Timed("job_update_store_get_lock_token")
@Override
public Optional<String> getLockToken(IJobUpdateKey key) {
// We assume here that cascading deletes will cause a lock-update associative row to disappear
// when the lock is invalidated. This further assumes that a lock row is deleted when a lock
// is no longer valid.
return Optional.fromNullable(detailsMapper.selectLockToken(key));
}
@Timed("job_update_store_fetch_instance_events")
@Override
public List<IJobInstanceUpdateEvent> fetchInstanceEvents(IJobUpdateKey key, int instanceId) {
return IJobInstanceUpdateEvent.listFromBuilders(
detailsMapper.selectInstanceUpdateEvents(key, instanceId));
}
}