/*
* Copyright © 2015 Cask Data, Inc.
*
* 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 co.cask.cdap.internal.app.runtime.schedule.store;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.DatasetManagementException;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Scanner;
import co.cask.cdap.api.dataset.table.Table;
import co.cask.cdap.api.schedule.SchedulableProgramType;
import co.cask.cdap.internal.app.runtime.schedule.AbstractSchedulerService;
import co.cask.cdap.internal.app.runtime.schedule.StreamSizeScheduleState;
import co.cask.cdap.internal.schedule.StreamSizeSchedule;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramType;
import co.cask.tephra.TransactionAware;
import co.cask.tephra.TransactionExecutor;
import co.cask.tephra.TransactionExecutorFactory;
import co.cask.tephra.TransactionFailureException;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Persists {@link StreamSizeSchedule} schedule information into datasets.
*/
@Singleton
public class DatasetBasedStreamSizeScheduleStore {
private static final Logger LOG = LoggerFactory.getLogger(DatasetBasedStreamSizeScheduleStore.class);
private static final Gson GSON = new Gson();
private static final String KEY_PREFIX = "streamSizeSchedule";
private static final byte[] SCHEDULE_COL = Bytes.toBytes("schedule");
private static final byte[] BASE_SIZE_COL = Bytes.toBytes("baseSize");
private static final byte[] BASE_TS_COL = Bytes.toBytes("baseTs");
private static final byte[] LAST_RUN_SIZE_COL = Bytes.toBytes("lastRunSize");
private static final byte[] LAST_RUN_TS_COL = Bytes.toBytes("lastRunTs");
private static final byte[] ACTIVE_COL = Bytes.toBytes("active");
private static final byte[] PROPERTIES_COL = Bytes.toBytes("properties");
private static final Type STRING_MAP_TYPE = new TypeToken<Map<String, String>>() { }.getType();
private final TransactionExecutorFactory factory;
private final ScheduleStoreTableUtil tableUtil;
private Table table;
@Inject
public DatasetBasedStreamSizeScheduleStore(TransactionExecutorFactory factory, ScheduleStoreTableUtil tableUtil) {
this.tableUtil = tableUtil;
this.factory = factory;
}
/**
* Initialize this persistent store.
*/
public synchronized void initialize() throws IOException, DatasetManagementException {
table = tableUtil.getMetaTable();
Preconditions.checkNotNull(table, "Could not get dataset client for data set: %s",
ScheduleStoreTableUtil.SCHEDULE_STORE_DATASET_NAME);
}
/**
* Persist a schedule to the store.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param schedule the schedule itself
* @param baseRunSize base size
* @param baseRunTs base timestamp
* @param running true if the schedule is running, false if it is suspended
*/
public void persist(Id.Program programId, SchedulableProgramType programType, StreamSizeSchedule schedule,
Map<String, String> properties, long baseRunSize, long baseRunTs, long lastRunSize,
long lastRunTs, boolean running)
throws TransactionFailureException, InterruptedException {
byte[][] columns = new byte[][] {
SCHEDULE_COL, BASE_SIZE_COL, BASE_TS_COL, LAST_RUN_SIZE_COL, LAST_RUN_TS_COL, ACTIVE_COL, PROPERTIES_COL
};
byte[][] values = new byte[][] {
Bytes.toBytes(GSON.toJson(schedule)),
Bytes.toBytes(baseRunSize),
Bytes.toBytes(baseRunTs),
Bytes.toBytes(lastRunSize),
Bytes.toBytes(lastRunTs),
Bytes.toBytes(running),
Bytes.toBytes(GSON.toJson(properties))
};
updateTable(programId, programType, schedule.getName(), columns, values, null);
}
/**
* Modify a schedule on the store to flag it as suspended.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
*/
public void suspend(Id.Program programId, SchedulableProgramType programType, String scheduleName)
throws TransactionFailureException, InterruptedException {
updateTable(programId, programType, scheduleName,
new byte[][]{ ACTIVE_COL },
new byte[][]{ Bytes.toBytes(false) },
null);
}
/**
* Modify a schedule on the store to flag it as running.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
*/
public void resume(Id.Program programId, SchedulableProgramType programType, String scheduleName)
throws TransactionFailureException, InterruptedException {
updateTable(programId, programType, scheduleName,
new byte[][]{ ACTIVE_COL },
new byte[][]{ Bytes.toBytes(true) },
null);
}
/**
* Modify the base information of a schedule in the store.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
* @param newBaseRunSize new base size
* @param newBaseRunTs new base timestamp
*/
public void updateBaseRun(Id.Program programId, SchedulableProgramType programType, String scheduleName,
long newBaseRunSize, long newBaseRunTs)
throws TransactionFailureException, InterruptedException {
updateTable(programId, programType, scheduleName,
new byte[][]{ BASE_SIZE_COL, BASE_TS_COL },
new byte[][]{ Bytes.toBytes(newBaseRunSize), Bytes.toBytes(newBaseRunTs) },
null);
}
/**
* Modify the last run information of a schedule in the store.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
* @param newLastRunSize new last run size
* @param newLastRunTs new last run timestamp
* @param txMethod method to execute in the same transaction that will change the lastRun information,
* before it is done
*/
public void updateLastRun(Id.Program programId, SchedulableProgramType programType,
String scheduleName, long newLastRunSize, long newLastRunTs,
TransactionMethod txMethod)
throws TransactionFailureException, InterruptedException {
updateTable(programId, programType, scheduleName,
new byte[][]{ LAST_RUN_SIZE_COL, LAST_RUN_TS_COL },
new byte[][]{ Bytes.toBytes(newLastRunSize), Bytes.toBytes(newLastRunTs) },
txMethod);
}
/**
* Update the {@link StreamSizeSchedule} object for a schedule in the store.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
* @param newSchedule new {@link StreamSizeSchedule} object
*/
public void updateSchedule(Id.Program programId, SchedulableProgramType programType,
String scheduleName, StreamSizeSchedule newSchedule)
throws TransactionFailureException, InterruptedException {
updateTable(programId, programType, scheduleName,
new byte[][]{ SCHEDULE_COL },
new byte[][]{ Bytes.toBytes(GSON.toJson(newSchedule)) },
null);
}
/**
* Remove a schedule from the store.
*
* @param programId program id the schedule is running for
* @param programType program type the schedule is running for
* @param scheduleName name of the schedule
*/
public synchronized void delete(final Id.Program programId, final SchedulableProgramType programType,
final String scheduleName) throws InterruptedException, TransactionFailureException {
factory.createExecutor(ImmutableList.of((TransactionAware) table))
.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
byte[] rowKey = Bytes.toBytes(String.format("%s:%s", KEY_PREFIX,
AbstractSchedulerService.scheduleIdFor(programId, programType,
scheduleName)));
table.delete(rowKey);
}
});
}
/**
* @return a list of all the schedules and their states present in the store
*/
public synchronized List<StreamSizeScheduleState> list() throws InterruptedException, TransactionFailureException {
final List<StreamSizeScheduleState> scheduleStates = Lists.newArrayList();
factory.createExecutor(ImmutableList.of((TransactionAware) table))
.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
byte[] startKey = Bytes.toBytes(KEY_PREFIX);
byte[] endKey = Bytes.stopKeyForPrefix(startKey);
Scanner scan = table.scan(startKey, endKey);
Row next;
while ((next = scan.next()) != null) {
byte[] scheduleBytes = next.get(SCHEDULE_COL);
byte[] baseSizeBytes = next.get(BASE_SIZE_COL);
byte[] baseTsBytes = next.get(BASE_TS_COL);
byte[] lastRunSizeBytes = next.get(LAST_RUN_SIZE_COL);
byte[] lastRunTsBytes = next.get(LAST_RUN_TS_COL);
byte[] activeBytes = next.get(ACTIVE_COL);
byte[] propertyBytes = next.get(PROPERTIES_COL);
if (scheduleBytes == null || baseSizeBytes == null || baseTsBytes == null || lastRunSizeBytes == null ||
lastRunTsBytes == null || activeBytes == null) {
continue;
}
String rowKey = Bytes.toString(next.getRow());
String[] splits = rowKey.split(":");
if (splits.length != 6) {
continue;
}
Id.Program program = Id.Program.from(splits[1], splits[2], ProgramType.valueOf(splits[3]), splits[4]);
SchedulableProgramType programType = SchedulableProgramType.valueOf(splits[3]);
StreamSizeSchedule schedule = GSON.fromJson(Bytes.toString(scheduleBytes), StreamSizeSchedule.class);
long baseSize = Bytes.toLong(baseSizeBytes);
long baseTs = Bytes.toLong(baseTsBytes);
long lastRunSize = Bytes.toLong(lastRunSizeBytes);
long lastRunTs = Bytes.toLong(lastRunTsBytes);
boolean active = Bytes.toBoolean(activeBytes);
Map<String, String> properties = Maps.newHashMap();
if (propertyBytes != null) {
properties = GSON.fromJson(Bytes.toString(propertyBytes), STRING_MAP_TYPE);
}
StreamSizeScheduleState scheduleState =
new StreamSizeScheduleState(program, programType, schedule, properties, baseSize, baseTs,
lastRunSize, lastRunTs, active);
scheduleStates.add(scheduleState);
LOG.debug("StreamSizeSchedule found in store: {}", scheduleState);
}
}
});
return scheduleStates;
}
private synchronized void updateTable(final Id.Program programId, final SchedulableProgramType programType,
final String scheduleName, final byte[][] columns, final byte[][] values,
@Nullable final TransactionMethod txMethod)
throws InterruptedException, TransactionFailureException {
factory.createExecutor(ImmutableList.of((TransactionAware) table))
.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
if (txMethod != null) {
txMethod.execute();
}
byte[] rowKey = Bytes.toBytes(String.format("%s:%s", KEY_PREFIX,
AbstractSchedulerService.scheduleIdFor(programId, programType,
scheduleName)));
table.put(rowKey, columns, values);
LOG.debug("Updated schedule {} with columns {}, values {}", scheduleName, columns, values);
}
});
}
/**
* The {@link #execute} method of this interface is made to be run during a transaction.
*/
public interface TransactionMethod {
/**
* Method to execute.
*
* @throws Exception
*/
void execute() throws Exception;
}
}