/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.runtime;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Map;
import org.apache.hadoop.io.Text;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.gson.stream.JsonWriter;
import com.linkedin.data.template.StringMap;
import gobblin.rest.Metric;
import gobblin.rest.MetricArray;
import gobblin.rest.MetricTypeEnum;
import gobblin.rest.Table;
import gobblin.rest.TableTypeEnum;
import gobblin.rest.TaskExecutionInfo;
import gobblin.rest.TaskStateEnum;
import gobblin.configuration.ConfigurationKeys;
import gobblin.configuration.WorkUnitState;
import gobblin.runtime.util.MetricGroup;
import gobblin.runtime.util.TaskMetrics;
import gobblin.source.workunit.Extract;
import gobblin.util.ForkOperatorUtils;
import gobblin.metrics.GobblinMetrics;
import lombok.Getter;
/**
* An extension to {@link WorkUnitState} with run-time task state information.
*
* @author Yinan Li
*/
public class TaskState extends WorkUnitState {
// Built-in metric names
/**
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
private static final String RECORDS = "records";
/**
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
private static final String RECORDS_PER_SECOND = "recordsPerSec";
/**
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
private static final String BYTES = "bytes";
/**
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
private static final String BYTES_PER_SECOND = "bytesPerSec";
private String jobId;
private String taskId;
private String taskKey;
@Getter
private Optional<String> taskAttemptId;
private long startTime = 0;
private long endTime = 0;
private long duration;
// Needed for serialization/deserialization
public TaskState() {}
public TaskState(WorkUnitState workUnitState) {
// Since getWorkunit() returns an immutable WorkUnit object,
// the WorkUnit object in this object is also immutable.
super(workUnitState.getWorkunit(), workUnitState.getJobState(), workUnitState.getTaskBrokerNullable());
addAll(workUnitState);
this.jobId = workUnitState.getProp(ConfigurationKeys.JOB_ID_KEY);
this.taskId = workUnitState.getProp(ConfigurationKeys.TASK_ID_KEY);
this.taskKey = workUnitState.getProp(ConfigurationKeys.TASK_KEY_KEY, "unknown_task_key");
this.taskAttemptId = Optional.fromNullable(workUnitState.getProp(ConfigurationKeys.TASK_ATTEMPT_ID_KEY));
this.setId(this.taskId);
}
public TaskState(TaskState taskState) {
super(taskState.getWorkunit(), taskState.getJobState(), taskState.getTaskBrokerNullable());
addAll(taskState);
this.jobId = taskState.getProp(ConfigurationKeys.JOB_ID_KEY);
this.taskId = taskState.getProp(ConfigurationKeys.TASK_ID_KEY);
this.taskAttemptId = taskState.getTaskAttemptId();
this.setId(this.taskId);
}
/**
* Get the ID of the job this {@link TaskState} is for.
*
* @return ID of the job this {@link TaskState} is for
*/
public String getJobId() {
return this.jobId;
}
/**
* Set the ID of the job this {@link TaskState} is for.
*
* @param jobId ID of the job this {@link TaskState} is for
*/
public void setJobId(String jobId) {
this.jobId = jobId;
}
/**
* Get the sequence number of the task this {@link TaskState} is for.
*
* @return Sequence number of the task this {@link TaskState} is for
*/
public String getTaskKey() {
return this.taskKey;
}
/**
* Get the ID of the task this {@link TaskState} is for.
*
* @return ID of the task this {@link TaskState} is for
*/
public String getTaskId() {
return this.taskId;
}
/**
* Set the ID of the task this {@link TaskState} is for.
*
* @param taskId ID of the task this {@link TaskState} is for
*/
public void setTaskId(String taskId) {
this.taskId = taskId;
}
/**
* Get task start time in milliseconds.
*
* @return task start time in milliseconds
*/
public long getStartTime() {
return this.startTime;
}
/**
* Set task start time in milliseconds.
*
* @param startTime task start time in milliseconds
*/
public void setStartTime(long startTime) {
this.startTime = startTime;
}
/**
* Get task end time in milliseconds.
*
* @return task end time in milliseconds
*/
public long getEndTime() {
return this.endTime;
}
/**
* set task end time in milliseconds.
*
* @param endTime task end time in milliseconds
*/
public void setEndTime(long endTime) {
this.endTime = endTime;
}
/**
* Get task duration in milliseconds.
*
* @return task duration in milliseconds
*/
public long getTaskDuration() {
return this.duration;
}
/**
* Set task duration in milliseconds.
*
* @param duration task duration in milliseconds
*/
public void setTaskDuration(long duration) {
this.duration = duration;
}
/**
* Get the {@link ConfigurationKeys#TASK_FAILURE_EXCEPTION_KEY} if it exists, else return {@link Optional#absent()}.
*/
public Optional<String> getTaskFailureException() {
return Optional.fromNullable(this.getProp(ConfigurationKeys.TASK_FAILURE_EXCEPTION_KEY));
}
/**
* If not already present, set the {@link ConfigurationKeys#TASK_FAILURE_EXCEPTION_KEY} to a {@link String}
* representation of the given {@link Throwable}.
*/
public void setTaskFailureException(Throwable taskFailureException) {
if (!this.contains(ConfigurationKeys.TASK_FAILURE_EXCEPTION_KEY)) {
this.setProp(ConfigurationKeys.TASK_FAILURE_EXCEPTION_KEY,
Throwables.getStackTraceAsString(taskFailureException));
}
}
/**
* Return whether the task has completed running or not.
*
* @return {@code true} if the task has completed or {@code false} otherwise
*/
public boolean isCompleted() {
WorkingState state = getWorkingState();
return state == WorkingState.SUCCESSFUL || state == WorkingState.COMMITTED || state == WorkingState.FAILED;
}
/**
* Update record-level metrics.
*
* @param recordsWritten number of records written by the writer
* @param branchIndex fork branch index
*
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
public synchronized void updateRecordMetrics(long recordsWritten, int branchIndex) {
TaskMetrics metrics = TaskMetrics.get(this);
// chopping branch index from metric name
// String forkBranchId = ForkOperatorUtils.getForkId(this.taskId, branchIndex);
String forkBranchId = TaskMetrics.taskInstanceRemoved(this.taskId);
Counter taskRecordCounter = metrics.getCounter(MetricGroup.TASK.name(), forkBranchId, RECORDS);
long inc = recordsWritten - taskRecordCounter.getCount();
taskRecordCounter.inc(inc);
metrics.getMeter(MetricGroup.TASK.name(), forkBranchId, RECORDS_PER_SECOND).mark(inc);
metrics.getCounter(MetricGroup.JOB.name(), this.jobId, RECORDS).inc(inc);
metrics.getMeter(MetricGroup.JOB.name(), this.jobId, RECORDS_PER_SECOND).mark(inc);
}
/**
* Collect byte-level metrics.
*
* @param bytesWritten number of bytes written by the writer
* @param branchIndex fork branch index
*
* @deprecated see {@link gobblin.instrumented.writer.InstrumentedDataWriterBase}.
*/
public synchronized void updateByteMetrics(long bytesWritten, int branchIndex) {
TaskMetrics metrics = TaskMetrics.get(this);
String forkBranchId = TaskMetrics.taskInstanceRemoved(this.taskId);
Counter taskByteCounter = metrics.getCounter(MetricGroup.TASK.name(), forkBranchId, BYTES);
long inc = bytesWritten - taskByteCounter.getCount();
taskByteCounter.inc(inc);
metrics.getMeter(MetricGroup.TASK.name(), forkBranchId, BYTES_PER_SECOND).mark(inc);
metrics.getCounter(MetricGroup.JOB.name(), this.jobId, BYTES).inc(inc);
metrics.getMeter(MetricGroup.JOB.name(), this.jobId, BYTES_PER_SECOND).mark(inc);
}
/**
* Adjust job-level metrics when the task gets retried.
*
* @param branches number of forked branches
*/
public void adjustJobMetricsOnRetry(int branches) {
TaskMetrics metrics = TaskMetrics.get(this);
for (int i = 0; i < branches; i++) {
String forkBranchId = ForkOperatorUtils.getForkId(this.taskId, i);
long recordsWritten = metrics.getCounter(MetricGroup.TASK.name(), forkBranchId, RECORDS).getCount();
long bytesWritten = metrics.getCounter(MetricGroup.TASK.name(), forkBranchId, BYTES).getCount();
metrics.getCounter(MetricGroup.JOB.name(), this.jobId, RECORDS).dec(recordsWritten);
metrics.getCounter(MetricGroup.JOB.name(), this.jobId, BYTES).dec(bytesWritten);
}
}
@Override
public void readFields(DataInput in) throws IOException {
Text text = new Text();
text.readFields(in);
this.jobId = text.toString().intern();
text.readFields(in);
this.taskId = text.toString().intern();
this.setId(this.taskId);
this.startTime = in.readLong();
this.endTime = in.readLong();
this.duration = in.readLong();
super.readFields(in);
}
@Override
public void write(DataOutput out) throws IOException {
Text text = new Text();
text.set(this.jobId);
text.write(out);
text.set(this.taskId);
text.write(out);
out.writeLong(this.startTime);
out.writeLong(this.endTime);
out.writeLong(this.duration);
super.write(out);
}
@Override
public boolean equals(Object object) {
if (!(object instanceof TaskState)) {
return false;
}
TaskState other = (TaskState) object;
return super.equals(other) && this.jobId.equals(other.jobId) && this.taskId.equals(other.taskId);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + this.jobId.hashCode();
result = prime * result + this.taskId.hashCode();
return result;
}
/**
* Convert this {@link TaskState} to a json document.
*
* @param jsonWriter a {@link com.google.gson.stream.JsonWriter} used to write the json document
* @throws IOException
*/
public void toJson(JsonWriter jsonWriter, boolean keepConfig) throws IOException {
jsonWriter.beginObject();
jsonWriter.name("task id").value(this.getTaskId()).name("task state").value(this.getWorkingState().name())
.name("start time").value(this.getStartTime()).name("end time").value(this.getEndTime()).name("duration")
.value(this.getTaskDuration()).name("retry count")
.value(this.getPropAsInt(ConfigurationKeys.TASK_RETRIES_KEY, 0));
// Also add failure exception information if it exists. This information is useful even in the
// case that the task finally succeeds so we know what happened in the course of task execution.
if (getTaskFailureException().isPresent()) {
jsonWriter.name("exception").value(getTaskFailureException().get());
}
if (keepConfig) {
jsonWriter.name("properties");
jsonWriter.beginObject();
for (String key : this.getPropertyNames()) {
jsonWriter.name(key).value(this.getProp(key));
}
jsonWriter.endObject();
}
jsonWriter.endObject();
}
/**
* Convert this {@link TaskState} instance to a {@link TaskExecutionInfo} instance.
*
* @return a {@link TaskExecutionInfo} instance
*/
public TaskExecutionInfo toTaskExecutionInfo() {
TaskExecutionInfo taskExecutionInfo = new TaskExecutionInfo();
taskExecutionInfo.setJobId(this.jobId);
taskExecutionInfo.setTaskId(this.taskId);
if (this.startTime > 0) {
taskExecutionInfo.setStartTime(this.startTime);
}
if (this.endTime > 0) {
taskExecutionInfo.setEndTime(this.endTime);
}
taskExecutionInfo.setDuration(this.duration);
taskExecutionInfo.setState(TaskStateEnum.valueOf(getWorkingState().name()));
if (this.contains(ConfigurationKeys.TASK_FAILURE_EXCEPTION_KEY)) {
taskExecutionInfo.setFailureException(this.getProp(ConfigurationKeys.TASK_FAILURE_EXCEPTION_KEY));
}
taskExecutionInfo.setHighWatermark(this.getHighWaterMark());
// Add extract/table information
Table table = new Table();
Extract extract = this.getExtract();
table.setNamespace(extract.getNamespace());
table.setName(extract.getTable());
if (extract.hasType()) {
table.setType(TableTypeEnum.valueOf(extract.getType().name()));
}
taskExecutionInfo.setTable(table);
// Add task metrics
TaskMetrics taskMetrics = TaskMetrics.get(this);
MetricArray metricArray = new MetricArray();
for (Map.Entry<String, ? extends com.codahale.metrics.Metric> entry : taskMetrics.getMetricContext().getCounters()
.entrySet()) {
Metric counter = new Metric();
counter.setGroup(MetricGroup.TASK.name());
counter.setName(entry.getKey());
counter.setType(MetricTypeEnum.valueOf(GobblinMetrics.MetricType.COUNTER.name()));
counter.setValue(Long.toString(((Counter) entry.getValue()).getCount()));
metricArray.add(counter);
}
for (Map.Entry<String, ? extends com.codahale.metrics.Metric> entry : taskMetrics.getMetricContext().getMeters()
.entrySet()) {
Metric meter = new Metric();
meter.setGroup(MetricGroup.TASK.name());
meter.setName(entry.getKey());
meter.setType(MetricTypeEnum.valueOf(GobblinMetrics.MetricType.METER.name()));
meter.setValue(Double.toString(((Meter) entry.getValue()).getMeanRate()));
metricArray.add(meter);
}
for (Map.Entry<String, ? extends com.codahale.metrics.Metric> entry : taskMetrics.getMetricContext().getGauges()
.entrySet()) {
Metric gauge = new Metric();
gauge.setGroup(MetricGroup.TASK.name());
gauge.setName(entry.getKey());
gauge.setType(MetricTypeEnum.valueOf(GobblinMetrics.MetricType.GAUGE.name()));
gauge.setValue(((Gauge<?>) entry.getValue()).getValue().toString());
metricArray.add(gauge);
}
taskExecutionInfo.setMetrics(metricArray);
// Add task properties
Map<String, String> taskProperties = Maps.newHashMap();
for (String name : this.getPropertyNames()) {
String value = this.getProp(name);
if (!Strings.isNullOrEmpty(value))
taskProperties.put(name, value);
}
taskExecutionInfo.setTaskProperties(new StringMap(taskProperties));
return taskExecutionInfo;
}
}