/* * 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.metastore.database; import javax.sql.DataSource; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.AbstractMap; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.linkedin.data.template.GetMode; import com.linkedin.data.template.StringMap; import gobblin.metastore.JobHistoryStore; import gobblin.rest.JobExecutionInfo; import gobblin.rest.JobExecutionQuery; import gobblin.rest.JobStateEnum; import gobblin.rest.LauncherTypeEnum; import gobblin.rest.Metric; import gobblin.rest.MetricArray; import gobblin.rest.MetricTypeEnum; import gobblin.rest.QueryListType; import gobblin.rest.Table; import gobblin.rest.TableTypeEnum; import gobblin.rest.TaskExecutionInfo; import gobblin.rest.TaskExecutionInfoArray; import gobblin.rest.TaskStateEnum; import gobblin.rest.TimeRange; /** * An implementation of {@link JobHistoryStore} backed by MySQL. * * <p> * The DDLs for the MySQL job history store can be found under metastore/src/main/resources. * </p> * * @author Yinan Li */ @SupportedDatabaseVersion(isDefault = false, version = "1.0.1") public class DatabaseJobHistoryStoreV101 implements VersionedDatabaseJobHistoryStore { private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseJobHistoryStoreV101.class); private static final String JOB_EXECUTION_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_job_executions (job_name,job_id,start_time,end_time,duration,state," + "launched_tasks,completed_tasks,launcher_type,tracking_url) VALUES(?,?,?,?,?,?,?,?,?,?)" + " ON DUPLICATE KEY UPDATE start_time=VALUES(start_time),end_time=VALUES(end_time)," + "duration=VALUES(duration),state=VALUES(state),launched_tasks=VALUES(launched_tasks)," + "completed_tasks=VALUES(completed_tasks),launcher_type=VALUES(launcher_type)," + "tracking_url=VALUES(tracking_url)"; private static final String TASK_EXECUTION_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_task_executions (task_id,job_id,start_time,end_time,duration," + "state,failure_exception,low_watermark,high_watermark,table_namespace,table_name,table_type) " + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE start_time=VALUES(start_time)," + "end_time=VALUES(end_time),duration=VALUES(duration),state=VALUES(state)," + "failure_exception=VALUES(failure_exception),low_watermark=VALUES(low_watermark)," + "high_watermark=VALUES(high_watermark),table_namespace=VALUES(table_namespace)," + "table_name=VALUES(table_name),table_type=VALUES(table_type)"; private static final String JOB_METRIC_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_job_metrics (job_id,metric_group,metric_name," + "metric_type,metric_value) VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE " + "metric_value=VALUES(metric_value)"; private static final String TASK_METRIC_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_task_metrics (task_id,metric_group,metric_name," + "metric_type,metric_value) VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE " + "metric_value=VALUES(metric_value)"; private static final String JOB_PROPERTY_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_job_properties (job_id,property_key,property_value) VALUES(?,?,?)" + " ON DUPLICATE KEY UPDATE property_value=VALUES(property_value)"; private static final String TASK_PROPERTY_UPSERT_STATEMENT_TEMPLATE = "INSERT INTO gobblin_task_properties (task_id,property_key,property_value) VALUES(?,?,?)" + " ON DUPLICATE KEY UPDATE property_value=VALUES(property_value)"; private static final String LIST_DISTINCT_JOB_EXECUTION_QUERY_TEMPLATE = "SELECT j.job_id FROM gobblin_job_executions j, " + "(SELECT MAX(last_modified_ts) AS most_recent_ts, job_name " + "FROM gobblin_job_executions GROUP BY job_name) max_results " + "WHERE j.job_name = max_results.job_name AND j.last_modified_ts = max_results.most_recent_ts"; private static final String LIST_RECENT_JOB_EXECUTION_QUERY_TEMPLATE = "SELECT job_id FROM gobblin_job_executions"; private static final String JOB_NAME_QUERY_BY_TABLE_STATEMENT_TEMPLATE = "SELECT j.job_name FROM gobblin_job_executions j, gobblin_task_executions t " + "WHERE j.job_id=t.job_id AND %s GROUP BY j.job_name"; private static final String JOB_ID_QUERY_BY_JOB_NAME_STATEMENT_TEMPLATE = "SELECT job_id FROM gobblin_job_executions WHERE job_name=?"; private static final String JOB_EXECUTION_QUERY_BY_JOB_ID_STATEMENT_TEMPLATE = "SELECT * FROM gobblin_job_executions WHERE job_id IN (%s)"; private static final String TASK_EXECUTION_QUERY_STATEMENT_TEMPLATE = "SELECT * FROM gobblin_task_executions WHERE job_id IN (%s)"; private static final String JOB_METRIC_QUERY_STATEMENT_TEMPLATE = "SELECT job_id,metric_group,metric_name,metric_type,metric_value FROM gobblin_job_metrics WHERE job_id IN (%s)"; private static final String TASK_METRIC_QUERY_STATEMENT_TEMPLATE = "SELECT job_id,m.task_id,metric_group,metric_name,metric_type,metric_value FROM gobblin_task_metrics m JOIN gobblin_task_executions t ON t.task_id = m.task_id WHERE job_id IN (%s)"; private static final String JOB_PROPERTY_QUERY_STATEMENT_TEMPLATE = "SELECT job_id,property_key,property_value FROM gobblin_job_properties WHERE job_id IN (%s)"; private static final String TASK_PROPERTY_QUERY_STATEMENT_TEMPLATE = "SELECT job_id,p.task_id,property_key,property_value FROM gobblin_task_properties p JOIN gobblin_task_executions t ON t.task_id = p.task_id WHERE job_id IN (%s)"; private DataSource dataSource; @Override public void init(DataSource dataSource) { this.dataSource = dataSource; } @Override public synchronized void put(JobExecutionInfo jobExecutionInfo) throws IOException { Optional<Connection> connectionOptional = Optional.absent(); try { connectionOptional = Optional.of(getConnection()); Connection connection = connectionOptional.get(); connection.setAutoCommit(false); // Insert or update job execution information upsertJobExecutionInfo(connection, jobExecutionInfo); upsertJobMetrics(connection, jobExecutionInfo); upsertJobProperties(connection, jobExecutionInfo); // Insert or update task execution information if (jobExecutionInfo.hasTaskExecutions()) { upsertTaskExecutionInfos(connection, jobExecutionInfo.getTaskExecutions()); upsertTaskMetrics(connection, jobExecutionInfo.getTaskExecutions()); Optional<StringMap> jobProperties = Optional.absent(); if (jobExecutionInfo.hasJobProperties()) { jobProperties = Optional.of(jobExecutionInfo.getJobProperties()); } upsertTaskProperties(connection, jobProperties, jobExecutionInfo.getTaskExecutions()); } connection.commit(); } catch (SQLException se) { LOGGER.error("Failed to put a new job execution information record", se); if (connectionOptional.isPresent()) { try { connectionOptional.get().rollback(); } catch (SQLException se1) { LOGGER.error("Failed to rollback", se1); } } throw new IOException(se); } finally { if (connectionOptional.isPresent()) { try { connectionOptional.get().close(); } catch (SQLException se) { LOGGER.error("Failed to close connection", se); } } } } @Override public synchronized List<JobExecutionInfo> get(JobExecutionQuery query) throws IOException { Preconditions.checkArgument(query.hasId() && query.hasIdType()); Optional<Connection> connectionOptional = Optional.absent(); try { connectionOptional = Optional.of(getConnection()); Connection connection = connectionOptional.get(); switch (query.getIdType()) { case JOB_ID: return processQueryByIds(connection, query, Filter.MISSING, Lists.newArrayList(query.getId().getString())); case JOB_NAME: return processQueryByJobName(connection, query.getId().getString(), query, Filter.MISSING); case TABLE: return processQueryByTable(connection, query); case LIST_TYPE: return processListQuery(connection, query); default: throw new IOException("Unsupported query ID type: " + query.getIdType().name()); } } catch (SQLException se) { LOGGER.error("Failed to execute query: " + query, se); throw new IOException(se); } finally { if (connectionOptional.isPresent()) { try { connectionOptional.get().close(); } catch (SQLException se) { LOGGER.error("Failed to close connection", se); } } } } @Override public void close() throws IOException { // Nothing to do } private Connection getConnection() throws SQLException { return this.dataSource.getConnection(); } private void upsertJobExecutionInfo(Connection connection, JobExecutionInfo info) throws SQLException { Preconditions.checkArgument(info.hasJobName()); Preconditions.checkArgument(info.hasJobId()); try (PreparedStatement upsertStatement = connection.prepareStatement(JOB_EXECUTION_UPSERT_STATEMENT_TEMPLATE)) { int index = 0; upsertStatement.setString(++index, info.getJobName()); upsertStatement.setString(++index, info.getJobId()); upsertStatement.setTimestamp(++index, info.hasStartTime() ? new Timestamp(info.getStartTime()) : null, getCalendarUTCInstance()); upsertStatement.setTimestamp(++index, info.hasEndTime() ? new Timestamp(info.getEndTime()) : null, getCalendarUTCInstance()); upsertStatement.setLong(++index, info.hasDuration() ? info.getDuration() : -1); upsertStatement.setString(++index, info.hasState() ? info.getState().name() : null); upsertStatement.setInt(++index, info.hasLaunchedTasks() ? info.getLaunchedTasks() : -1); upsertStatement.setInt(++index, info.hasCompletedTasks() ? info.getCompletedTasks() : -1); upsertStatement.setString(++index, info.hasLauncherType() ? info.getLauncherType().name() : null); upsertStatement.setString(++index, info.hasTrackingUrl() ? info.getTrackingUrl() : null); upsertStatement.executeUpdate(); } } private void upsertTaskExecutionInfos(Connection connection, TaskExecutionInfoArray taskExecutions) throws SQLException { Optional<PreparedStatement> upsertStatement = Optional.absent(); int batchSize = 0; for (TaskExecutionInfo taskExecution : taskExecutions) { if (!upsertStatement.isPresent()) { upsertStatement = Optional.of(connection.prepareStatement(TASK_EXECUTION_UPSERT_STATEMENT_TEMPLATE)); } addTaskExecutionInfoToBatch(upsertStatement.get(), taskExecution); if (batchSize++ > 1000) { executeBatches(upsertStatement); upsertStatement = Optional.absent(); batchSize = 0; } } executeBatches(upsertStatement); } private void upsertJobProperties(Connection connection, JobExecutionInfo jobExecutionInfo) throws SQLException { if (jobExecutionInfo.hasJobProperties()) { Optional<PreparedStatement> upsertStatement = Optional.absent(); int batchSize = 0; for (Map.Entry<String, String> property : jobExecutionInfo.getJobProperties().entrySet()) { if (!upsertStatement.isPresent()) { upsertStatement = Optional.of(connection.prepareStatement(JOB_PROPERTY_UPSERT_STATEMENT_TEMPLATE)); } addPropertyToBatch(upsertStatement.get(), property.getKey(), property.getValue(), jobExecutionInfo.getJobId()); if (batchSize++ > 1000) { executeBatches(upsertStatement); upsertStatement = Optional.absent(); batchSize = 0; } } executeBatches(upsertStatement); } } private void upsertTaskProperties(Connection connection, Optional<StringMap> jobProperties, TaskExecutionInfoArray taskExecutions) throws SQLException { Optional<PreparedStatement> upsertStatement = Optional.absent(); int batchSize = 0; for (TaskExecutionInfo taskExecution : taskExecutions) { if (taskExecution.hasTaskProperties()) { for (Map.Entry<String, String> property : taskExecution.getTaskProperties().entrySet()) { if (!jobProperties.isPresent() || !jobProperties.get().containsKey(property.getKey()) || !jobProperties.get().get(property.getKey()).equals(property.getValue())) { if (!upsertStatement.isPresent()) { upsertStatement = Optional.of(connection.prepareStatement(TASK_PROPERTY_UPSERT_STATEMENT_TEMPLATE)); } addPropertyToBatch(upsertStatement.get(), property.getKey(), property.getValue(), taskExecution.getTaskId()); if (batchSize++ > 1000) { executeBatches(upsertStatement); upsertStatement = Optional.absent(); batchSize = 0; } } } } } executeBatches(upsertStatement); } private void addPropertyToBatch(PreparedStatement upsertStatement, String key, String value, String id) throws SQLException { Preconditions.checkArgument(!Strings.isNullOrEmpty(id)); Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); Preconditions.checkArgument(!Strings.isNullOrEmpty(value)); int index = 0; upsertStatement.setString(++index, id); upsertStatement.setString(++index, key); upsertStatement.setString(++index, value); upsertStatement.addBatch(); } private void upsertJobMetrics(Connection connection, JobExecutionInfo jobExecutionInfo) throws SQLException { if (jobExecutionInfo.hasMetrics()) { Optional<PreparedStatement> upsertStatement = Optional.absent(); int batchSize = 0; for (Metric metric : jobExecutionInfo.getMetrics()) { if (!upsertStatement.isPresent()) { upsertStatement = Optional.of(connection.prepareStatement(JOB_METRIC_UPSERT_STATEMENT_TEMPLATE)); } addMetricToBatch(upsertStatement.get(), metric, jobExecutionInfo.getJobId()); if (batchSize++ > 1000) { executeBatches(upsertStatement); upsertStatement = Optional.absent(); batchSize = 0; } } executeBatches(upsertStatement); } } private void upsertTaskMetrics(Connection connection, TaskExecutionInfoArray taskExecutions) throws SQLException { Optional<PreparedStatement> upsertStatement = Optional.absent(); int batchSize = 0; for (TaskExecutionInfo taskExecution : taskExecutions) { if (taskExecution.hasMetrics()) { for (Metric metric : taskExecution.getMetrics()) { if (!upsertStatement.isPresent()) { upsertStatement = Optional.of(connection.prepareStatement(TASK_METRIC_UPSERT_STATEMENT_TEMPLATE)); } addMetricToBatch(upsertStatement.get(), metric, taskExecution.getTaskId()); if (batchSize++ > 1000) { executeBatches(upsertStatement); upsertStatement = Optional.absent(); batchSize = 0; } } } } executeBatches(upsertStatement); } private void executeBatches(Optional<PreparedStatement> upsertStatement) throws SQLException { if (upsertStatement.isPresent()) { try { upsertStatement.get().executeBatch(); } finally { upsertStatement.get().close(); } } } private void addMetricToBatch(PreparedStatement upsertStatement, Metric metric, String id) throws SQLException { Preconditions.checkArgument(!Strings.isNullOrEmpty(id)); Preconditions.checkArgument(metric.hasGroup()); Preconditions.checkArgument(metric.hasName()); Preconditions.checkArgument(metric.hasType()); Preconditions.checkArgument(metric.hasValue()); int index = 0; upsertStatement.setString(++index, id); upsertStatement.setString(++index, metric.getGroup()); upsertStatement.setString(++index, metric.getName()); upsertStatement.setString(++index, metric.getType().name()); upsertStatement.setString(++index, metric.getValue()); upsertStatement.addBatch(); } private void addTaskExecutionInfoToBatch(PreparedStatement upsertStatement, TaskExecutionInfo info) throws SQLException { Preconditions.checkArgument(info.hasTaskId()); Preconditions.checkArgument(info.hasJobId()); int index = 0; upsertStatement.setString(++index, info.getTaskId()); upsertStatement.setString(++index, info.getJobId()); upsertStatement.setTimestamp(++index, info.hasStartTime() ? new Timestamp(info.getStartTime()) : null, getCalendarUTCInstance()); upsertStatement.setTimestamp(++index, info.hasEndTime() ? new Timestamp(info.getEndTime()) : null, getCalendarUTCInstance()); upsertStatement.setLong(++index, info.hasDuration() ? info.getDuration() : -1); upsertStatement.setString(++index, info.hasState() ? info.getState().name() : null); upsertStatement.setString(++index, info.hasFailureException() ? info.getFailureException() : null); upsertStatement.setLong(++index, info.hasLowWatermark() ? info.getLowWatermark() : -1); upsertStatement.setLong(++index, info.hasHighWatermark() ? info.getHighWatermark() : -1); upsertStatement.setString(++index, info.hasTable() && info.getTable().hasNamespace() ? info.getTable().getNamespace() : null); upsertStatement.setString(++index, info.hasTable() && info.getTable().hasName() ? info.getTable().getName() : null); upsertStatement.setString(++index, info.hasTable() && info.getTable().hasType() ? info.getTable().getType().name() : null); upsertStatement.addBatch(); } private List<JobExecutionInfo> processQueryByIds(Connection connection, JobExecutionQuery query, Filter tableFilter, List<String> jobIds) throws SQLException { Map<String, JobExecutionInfo> jobExecutionInfos = getJobExecutionInfos(connection, jobIds); addMetricsToJobExecutions(connection, query, jobExecutionInfos); addPropertiesToJobExecutions(connection, query, jobExecutionInfos); addTasksToJobExecutions(connection, query, tableFilter, jobExecutionInfos); return ImmutableList.copyOf(jobExecutionInfos.values()); } private Map<String, JobExecutionInfo> getJobExecutionInfos(Connection connection, List<String> jobIds) throws SQLException { Map<String, JobExecutionInfo> jobExecutionInfos = Maps.newLinkedHashMap(); if (jobIds != null && jobIds.size() > 0) { String template = String.format(JOB_EXECUTION_QUERY_BY_JOB_ID_STATEMENT_TEMPLATE, getInPredicate(jobIds.size())); int index = 1; try (PreparedStatement jobExecutionQueryStatement = connection.prepareStatement(template)) { for (String jobId : jobIds) { jobExecutionQueryStatement.setString(index++, jobId); } try (ResultSet jobRs = jobExecutionQueryStatement.executeQuery()) { while (jobRs.next()) { JobExecutionInfo jobExecutionInfo = resultSetToJobExecutionInfo(jobRs); jobExecutionInfos.put(jobExecutionInfo.getJobId(), jobExecutionInfo); } } } } return jobExecutionInfos; } private void addMetricsToJobExecutions(Connection connection, JobExecutionQuery query, Map<String, JobExecutionInfo> jobExecutionInfos) throws SQLException { if (query.isIncludeJobMetrics() && jobExecutionInfos.size() > 0) { String template = String.format(JOB_METRIC_QUERY_STATEMENT_TEMPLATE, getInPredicate(jobExecutionInfos.size())); int index = 1; try (PreparedStatement jobMetricQueryStatement = connection.prepareStatement(template)) { for (String jobId : jobExecutionInfos.keySet()) { jobMetricQueryStatement.setString(index++, jobId); } try (ResultSet jobMetricRs = jobMetricQueryStatement.executeQuery()) { while (jobMetricRs.next()) { String jobId = jobMetricRs.getString("job_id"); JobExecutionInfo jobExecutionInfo = jobExecutionInfos.get(jobId); MetricArray metricArray = jobExecutionInfo.getMetrics(GetMode.NULL); if (metricArray == null) { metricArray = new MetricArray(); jobExecutionInfo.setMetrics(metricArray); } metricArray.add(resultSetToMetric(jobMetricRs)); } } } } } private void addPropertiesToJobExecutions(Connection connection, JobExecutionQuery query, Map<String, JobExecutionInfo> jobExecutionInfos) throws SQLException { if (jobExecutionInfos.size() > 0) { Set<String> propertyKeys = null; if (query.hasJobProperties()) { propertyKeys = Sets.newHashSet(Iterables.filter(Arrays.asList(query.getJobProperties().split(",")), new Predicate<String>() { @Override public boolean apply(String input) { return !Strings.isNullOrEmpty(input); } })); } if (propertyKeys == null || propertyKeys.size() > 0) { String template = String.format(JOB_PROPERTY_QUERY_STATEMENT_TEMPLATE, getInPredicate(jobExecutionInfos.size())); if (propertyKeys != null && propertyKeys.size() > 0) { template += String.format(" AND property_key IN (%s)", getInPredicate(propertyKeys.size())); } int index = 1; try (PreparedStatement jobPropertiesQueryStatement = connection.prepareStatement(template)) { for (String jobId : jobExecutionInfos.keySet()) { jobPropertiesQueryStatement.setString(index++, jobId); } if (propertyKeys != null && propertyKeys.size() > 0) { for (String propertyKey : propertyKeys) { jobPropertiesQueryStatement.setString(index++, propertyKey); } } try (ResultSet jobPropertiesRs = jobPropertiesQueryStatement.executeQuery()) { while (jobPropertiesRs.next()) { String jobId = jobPropertiesRs.getString("job_id"); JobExecutionInfo jobExecutionInfo = jobExecutionInfos.get(jobId); StringMap jobProperties = jobExecutionInfo.getJobProperties(GetMode.NULL); if (jobProperties == null) { jobProperties = new StringMap(Maps.<String, String>newHashMap()); jobExecutionInfo.setJobProperties(jobProperties); } Map.Entry<String, String> property = resultSetToProperty(jobPropertiesRs); if (propertyKeys == null || propertyKeys.contains(property.getKey())) { jobProperties.put(property.getKey(), property.getValue()); } } } } } } } private void addTasksToJobExecutions(Connection connection, JobExecutionQuery query, Filter tableFilter, Map<String, JobExecutionInfo> jobExecutionInfos) throws SQLException { Map<String, Map<String, TaskExecutionInfo>> tasksExecutions = getTasksForJobExecutions(connection, query, tableFilter, jobExecutionInfos); addMetricsToTasks(connection, query, tableFilter, tasksExecutions); addPropertiesToTasks(connection, query, tableFilter, tasksExecutions); for (Map.Entry<String, Map<String, TaskExecutionInfo>> taskExecution : tasksExecutions.entrySet()) { JobExecutionInfo jobExecutionInfo = jobExecutionInfos.get(taskExecution.getKey()); TaskExecutionInfoArray taskExecutionInfos = new TaskExecutionInfoArray(); for (TaskExecutionInfo taskExecutionInfo : taskExecution.getValue().values()) { taskExecutionInfos.add(taskExecutionInfo); } jobExecutionInfo.setTaskExecutions(taskExecutionInfos); } } private Map<String, Map<String, TaskExecutionInfo>> getTasksForJobExecutions(Connection connection, JobExecutionQuery query, Filter tableFilter, Map<String, JobExecutionInfo> jobExecutionInfos) throws SQLException { Map<String, Map<String, TaskExecutionInfo>> taskExecutionInfos = Maps.newLinkedHashMap(); if (query.isIncludeTaskExecutions() && jobExecutionInfos.size() > 0) { String template = String.format(TASK_EXECUTION_QUERY_STATEMENT_TEMPLATE, getInPredicate(jobExecutionInfos.size())); if (tableFilter.isPresent()) { template += " AND " + tableFilter; } int index = 1; try (PreparedStatement taskExecutionQueryStatement = connection.prepareStatement(template)) { for (String jobId : jobExecutionInfos.keySet()) { taskExecutionQueryStatement.setString(index++, jobId); } if (tableFilter.isPresent()) { tableFilter.addParameters(taskExecutionQueryStatement, index); } try (ResultSet taskRs = taskExecutionQueryStatement.executeQuery()) { while (taskRs.next()) { TaskExecutionInfo taskExecutionInfo = resultSetToTaskExecutionInfo(taskRs); if (!taskExecutionInfos.containsKey(taskExecutionInfo.getJobId())) { taskExecutionInfos.put(taskExecutionInfo.getJobId(), Maps.<String, TaskExecutionInfo>newLinkedHashMap()); } taskExecutionInfos.get(taskExecutionInfo.getJobId()).put(taskExecutionInfo.getTaskId(), taskExecutionInfo); } } } } return taskExecutionInfos; } private void addMetricsToTasks(Connection connection, JobExecutionQuery query, Filter tableFilter, Map<String, Map<String, TaskExecutionInfo>> taskExecutionInfos) throws SQLException { if (query.isIncludeTaskMetrics() && taskExecutionInfos.size() > 0) { int index = 1; String template = String.format(TASK_METRIC_QUERY_STATEMENT_TEMPLATE, getInPredicate(taskExecutionInfos.size())); if (tableFilter.isPresent()) { template += " AND t." + tableFilter; } try (PreparedStatement taskMetricQueryStatement = connection.prepareStatement(template)) { for (String jobId : taskExecutionInfos.keySet()) { taskMetricQueryStatement.setString(index++, jobId); } if (tableFilter.isPresent()) { tableFilter.addParameters(taskMetricQueryStatement, index); } try (ResultSet taskMetricRs = taskMetricQueryStatement.executeQuery()) { while (taskMetricRs.next()) { String jobId = taskMetricRs.getString("job_id"); String taskId = taskMetricRs.getString("task_id"); TaskExecutionInfo taskExecutionInfo = taskExecutionInfos.get(jobId).get(taskId); MetricArray metricsArray = taskExecutionInfo.getMetrics(GetMode.NULL); if (metricsArray == null) { metricsArray = new MetricArray(); taskExecutionInfo.setMetrics(metricsArray); } metricsArray.add(resultSetToMetric(taskMetricRs)); } } } } } private void addPropertiesToTasks(Connection connection, JobExecutionQuery query, Filter tableFilter, Map<String, Map<String, TaskExecutionInfo>> taskExecutionInfos) throws SQLException { if (taskExecutionInfos.size() > 0) { Set<String> propertyKeys = null; if (query.hasTaskProperties()) { propertyKeys = Sets.newHashSet(Iterables.filter(Arrays.asList(query.getTaskProperties().split(",")), new Predicate<String>() { @Override public boolean apply(String input) { return !Strings.isNullOrEmpty(input); } })); } if (propertyKeys == null || propertyKeys.size() > 0) { String template = String.format(TASK_PROPERTY_QUERY_STATEMENT_TEMPLATE, getInPredicate(taskExecutionInfos.size())); if (propertyKeys != null && propertyKeys.size() > 0) { template += String.format("AND property_key IN (%s)", getInPredicate(propertyKeys.size())); } if (tableFilter.isPresent()) { template += " AND t." + tableFilter; } int index = 1; try (PreparedStatement taskPropertiesQueryStatement = connection.prepareStatement(template)) { for (String jobId : taskExecutionInfos.keySet()) { taskPropertiesQueryStatement.setString(index++, jobId); } if (propertyKeys != null && propertyKeys.size() > 0) { for (String propertyKey : propertyKeys) { taskPropertiesQueryStatement.setString(index++, propertyKey); } } if (tableFilter.isPresent()) { tableFilter.addParameters(taskPropertiesQueryStatement, index); } try (ResultSet taskPropertiesRs = taskPropertiesQueryStatement.executeQuery()) { while (taskPropertiesRs.next()) { String jobId = taskPropertiesRs.getString("job_id"); String taskId = taskPropertiesRs.getString("task_id"); TaskExecutionInfo taskExecutionInfo = taskExecutionInfos.get(jobId).get(taskId); StringMap taskProperties = taskExecutionInfo.getTaskProperties(GetMode.NULL); if (taskProperties == null) { taskProperties = new StringMap(); taskExecutionInfo.setTaskProperties(taskProperties); } Map.Entry<String, String> property = resultSetToProperty(taskPropertiesRs); if (propertyKeys == null || propertyKeys.contains(property.getKey())) { taskProperties.put(property.getKey(), property.getValue()); } } } } } } } private List<JobExecutionInfo> processQueryByJobName(Connection connection, String jobName, JobExecutionQuery query, Filter tableFilter) throws SQLException { Preconditions.checkArgument(!Strings.isNullOrEmpty(jobName)); // Construct the query for job IDs by a given job name Filter filter = Filter.MISSING; String jobIdByNameQuery = JOB_ID_QUERY_BY_JOB_NAME_STATEMENT_TEMPLATE; if (query.hasTimeRange()) { // Add time range filter if applicable try { filter = constructTimeRangeFilter(query.getTimeRange()); if (filter.isPresent()) { jobIdByNameQuery += " AND " + filter; } } catch (ParseException pe) { LOGGER.error("Failed to parse the query time range", pe); throw new SQLException(pe); } } // Add ORDER BY jobIdByNameQuery += " ORDER BY created_ts DESC"; // Query job IDs by the given job name List<String> jobIds = Lists.newArrayList(); try (PreparedStatement queryStatement = connection.prepareStatement(jobIdByNameQuery)) { int limit = query.getLimit(); if (limit > 0) { queryStatement.setMaxRows(limit); } queryStatement.setString(1, jobName); if (filter.isPresent()) { filter.addParameters(queryStatement, 2); } try (ResultSet rs = queryStatement.executeQuery()) { while (rs.next()) { jobIds.add(rs.getString(1)); } } } return processQueryByIds(connection, query, tableFilter, jobIds); } private List<JobExecutionInfo> processQueryByTable(Connection connection, JobExecutionQuery query) throws SQLException { Preconditions.checkArgument(query.getId().isTable()); Filter tableFilter = constructTableFilter(query.getId().getTable()); // Construct the query for job names by table definition String jobNameByTableQuery = String.format(JOB_NAME_QUERY_BY_TABLE_STATEMENT_TEMPLATE, tableFilter.getFilter()); List<JobExecutionInfo> jobExecutionInfos = Lists.newArrayList(); // Query job names by table definition try (PreparedStatement queryStatement = connection.prepareStatement(jobNameByTableQuery)) { if (tableFilter.isPresent()) { tableFilter.addParameters(queryStatement, 1); } try (ResultSet rs = queryStatement.executeQuery()) { while (rs.next()) { jobExecutionInfos.addAll(processQueryByJobName(connection, rs.getString(1), query, tableFilter)); } } return jobExecutionInfos; } } private List<JobExecutionInfo> processListQuery(Connection connection, JobExecutionQuery query) throws SQLException { Preconditions.checkArgument(query.getId().isQueryListType()); Filter timeRangeFilter = Filter.MISSING; QueryListType queryType = query.getId().getQueryListType(); String listJobExecutionsQuery; if (queryType == QueryListType.DISTINCT) { listJobExecutionsQuery = LIST_DISTINCT_JOB_EXECUTION_QUERY_TEMPLATE; if (query.hasTimeRange()) { try { timeRangeFilter = constructTimeRangeFilter(query.getTimeRange()); if (timeRangeFilter.isPresent()) { listJobExecutionsQuery += " AND " + timeRangeFilter; } } catch (ParseException pe) { LOGGER.error("Failed to parse the query time range", pe); throw new SQLException(pe); } } } else { listJobExecutionsQuery = LIST_RECENT_JOB_EXECUTION_QUERY_TEMPLATE; } listJobExecutionsQuery += " ORDER BY last_modified_ts DESC"; try (PreparedStatement queryStatement = connection.prepareStatement(listJobExecutionsQuery)) { int limit = query.getLimit(); if (limit > 0) { queryStatement.setMaxRows(limit); } if (timeRangeFilter.isPresent()) { timeRangeFilter.addParameters(queryStatement, 1); } try (ResultSet rs = queryStatement.executeQuery()) { List<String> jobIds = Lists.newArrayList(); while (rs.next()) { jobIds.add(rs.getString(1)); } return processQueryByIds(connection, query, Filter.MISSING, jobIds); } } } private JobExecutionInfo resultSetToJobExecutionInfo(ResultSet rs) throws SQLException { JobExecutionInfo jobExecutionInfo = new JobExecutionInfo(); jobExecutionInfo.setJobName(rs.getString("job_name")); jobExecutionInfo.setJobId(rs.getString("job_id")); try { Timestamp startTime = rs.getTimestamp("start_time"); if (startTime != null) { jobExecutionInfo.setStartTime(startTime.getTime()); } } catch (SQLException se) { jobExecutionInfo.setStartTime(0); } try { Timestamp endTime = rs.getTimestamp("end_time"); if (endTime != null) { jobExecutionInfo.setEndTime(endTime.getTime()); } } catch (SQLException se) { jobExecutionInfo.setEndTime(0); } jobExecutionInfo.setDuration(rs.getLong("duration")); String state = rs.getString("state"); if (!Strings.isNullOrEmpty(state)) { jobExecutionInfo.setState(JobStateEnum.valueOf(state)); } jobExecutionInfo.setLaunchedTasks(rs.getInt("launched_tasks")); jobExecutionInfo.setCompletedTasks(rs.getInt("completed_tasks")); String launcherType = rs.getString("launcher_type"); if (!Strings.isNullOrEmpty(launcherType)) { jobExecutionInfo.setLauncherType(LauncherTypeEnum.valueOf(launcherType)); } String trackingUrl = rs.getString("tracking_url"); if (!Strings.isNullOrEmpty(trackingUrl)) { jobExecutionInfo.setTrackingUrl(trackingUrl); } return jobExecutionInfo; } private TaskExecutionInfo resultSetToTaskExecutionInfo(ResultSet rs) throws SQLException { TaskExecutionInfo taskExecutionInfo = new TaskExecutionInfo(); taskExecutionInfo.setTaskId(rs.getString("task_id")); taskExecutionInfo.setJobId(rs.getString("job_id")); try { Timestamp startTime = rs.getTimestamp("start_time"); if (startTime != null) { taskExecutionInfo.setStartTime(startTime.getTime()); } } catch (SQLException se) { taskExecutionInfo.setStartTime(0); } try { Timestamp endTime = rs.getTimestamp("end_time"); if (endTime != null) { taskExecutionInfo.setEndTime(endTime.getTime()); } } catch (SQLException se) { taskExecutionInfo.setEndTime(0); } taskExecutionInfo.setDuration(rs.getLong("duration")); String state = rs.getString("state"); if (!Strings.isNullOrEmpty(state)) { taskExecutionInfo.setState(TaskStateEnum.valueOf(state)); } String failureException = rs.getString("failure_exception"); if (!Strings.isNullOrEmpty(failureException)) { taskExecutionInfo.setFailureException(failureException); } taskExecutionInfo.setLowWatermark(rs.getLong("low_watermark")); taskExecutionInfo.setHighWatermark(rs.getLong("high_watermark")); Table table = new Table(); String namespace = rs.getString("table_namespace"); if (!Strings.isNullOrEmpty(namespace)) { table.setNamespace(namespace); } String name = rs.getString("table_name"); if (!Strings.isNullOrEmpty(name)) { table.setName(name); } String type = rs.getString("table_type"); if (!Strings.isNullOrEmpty(type)) { table.setType(TableTypeEnum.valueOf(type)); } taskExecutionInfo.setTable(table); return taskExecutionInfo; } private Metric resultSetToMetric(ResultSet rs) throws SQLException { Metric metric = new Metric(); metric.setGroup(rs.getString("metric_group")); metric.setName(rs.getString("metric_name")); metric.setType(MetricTypeEnum.valueOf(rs.getString("metric_type"))); metric.setValue(rs.getString("metric_value")); return metric; } private AbstractMap.SimpleEntry<String, String> resultSetToProperty(ResultSet rs) throws SQLException { return new AbstractMap.SimpleEntry<>(rs.getString("property_key"), rs.getString("property_value")); } private Filter constructTimeRangeFilter(TimeRange timeRange) throws ParseException { List<String> values = Lists.newArrayList(); StringBuilder sb = new StringBuilder(); if (!timeRange.hasTimeFormat()) { LOGGER.warn("Skipping the time range filter as there is no time format in: " + timeRange); return Filter.MISSING; } DateFormat dateFormat = new SimpleDateFormat(timeRange.getTimeFormat()); boolean hasStartTime = timeRange.hasStartTime(); if (hasStartTime) { sb.append("start_time>?"); values.add(new Timestamp(dateFormat.parse(timeRange.getStartTime()).getTime()).toString()); } if (timeRange.hasEndTime()) { if (hasStartTime) { sb.append(" AND "); } sb.append("end_time<?"); values.add(new Timestamp(dateFormat.parse(timeRange.getEndTime()).getTime()).toString()); } if (sb.length() > 0) { return new Filter(sb.toString(), values); } return Filter.MISSING; } private Filter constructTableFilter(Table table) { List<String> values = Lists.newArrayList(); StringBuilder sb = new StringBuilder(); boolean hasNamespace = table.hasNamespace(); if (hasNamespace) { sb.append("table_namespace=?"); values.add(table.getNamespace()); } boolean hasName = table.hasName(); if (hasName) { if (hasNamespace) { sb.append(" AND "); } sb.append("table_name=?"); values.add(table.getName()); } if (table.hasType()) { if (hasName) { sb.append(" AND "); } sb.append("table_type=?"); values.add(table.getType().name()); } if (sb.length() > 0) { return new Filter(sb.toString(), values); } return Filter.MISSING; } private static Calendar getCalendarUTCInstance() { return Calendar.getInstance(TimeZone.getTimeZone("UTC")); } private static String getInPredicate(int count) { return StringUtils.join(Iterables.limit(Iterables.cycle("?"), count).iterator(), ","); } }