From de1ebfb3511288fb2ffa45154061f2e656c3964a Mon Sep 17 00:00:00 2001 From: Gunnar Hillert Date: Thu, 7 Jun 2018 23:18:22 -1000 Subject: [PATCH] gh-424 Add the ability to retrieve the last TaskExecution for Tasks - Add the ability to retrieve the last TaskExecution to: - MapTaskExecutionDao - JdbcTaskExecutionDao - Refactor `JdbcTaskExecutionDao` and use `NamedParameterJdbcTemplate` for all persistence store calls - Add the following TaskExecution methods to TaskExplorer: - getLatestTaskExecutionsByTaskNames - getLatestTaskExecutionForTaskName - Add Tests - Ensure that code is JDK7 compatible due to backporting needs - Ensure commit backports to 1.2.x Polishing on tests during merge --- .../cloud/task/repository/TaskExplorer.java | 33 +- .../repository/dao/JdbcTaskExecutionDao.java | 232 ++++++++----- .../repository/dao/MapTaskExecutionDao.java | 73 ++++- .../task/repository/dao/TaskExecutionDao.java | 33 +- .../support/SimpleTaskExplorer.java | 13 +- .../dao/BaseTaskExecutionDaoTestCases.java | 309 ++++++++++++++++++ .../dao/JdbcTaskExecutionDaoTests.java | 21 +- .../dao/MapTaskExecutionDaoTests.java | 29 +- .../support/SimpleTaskExplorerTests.java | 53 ++- 9 files changed, 681 insertions(+), 115 deletions(-) create mode 100644 spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/BaseTaskExecutionDaoTestCases.java diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/TaskExplorer.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/TaskExplorer.java index e998def1..bdd50016 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/TaskExplorer.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/TaskExplorer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.data.domain.Pageable; * * @author Glenn Renfro * @author Michael Minella + * @author Gunnar Hillert */ public interface TaskExplorer { @@ -105,4 +106,34 @@ public interface TaskExplorer { * @return a Set of the ids of the job executions executed within the task. */ Set getJobExecutionIdsByTaskExecutionId(long taskExecutionId); + + /** + * Returns a {@link List} of the latest {@link TaskExecution} for 1 or more task names. + * + * Latest is defined by the most recent start time. A {@link TaskExecution} does not have to be finished + * (The results may including pending {@link TaskExecution}s). + * + * It is theoretically possible that a {@link TaskExecution} with the same name to have more than 1 + * {@link TaskExecution} for the exact same start time. In that case the {@link TaskExecution} with the + * highest Task Execution ID is returned. + * + * This method will not consider end times in its calculations. Thus, when a task execution {@code A} starts + * after task execution {@code B} but finishes BEFORE task execution {@code A}, then task execution {@code B} + * is being returned. + * + * @param taskNames At least 1 task name must be provided + * @return List of TaskExecutions. May be empty but never null. + */ + List getLatestTaskExecutionsByTaskNames(String... taskNames); + + + /** + * Returns the latest task execution for a given task name. Will ultimately apply the same algorithm underneath + * as {@link #getLatestTaskExecutionsByTaskNames(String...)} but will only return a single result. + * + * @param taskName Must not be null or empty + * @return The latest Task Execution or null + * @see #getLatestTaskExecutionsByTaskNames(String...) + */ + TaskExecution getLatestTaskExecutionForTaskName(String taskName); } diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDao.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDao.java index 1f2dbbc5..9c1b14ba 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDao.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDao.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -40,11 +41,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -67,61 +68,75 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { public static final String FROM_CLAUSE = "%PREFIX%EXECUTION"; public static final String RUNNING_TASK_WHERE_CLAUSE = - "where TASK_NAME = ? AND END_TIME IS NULL "; + "where TASK_NAME = :taskName AND END_TIME IS NULL "; - public static final String TASK_NAME_WHERE_CLAUSE = "where TASK_NAME = ? "; + public static final String TASK_NAME_WHERE_CLAUSE = "where TASK_NAME = :taskName "; private static final String SAVE_TASK_EXECUTION = "INSERT into %PREFIX%EXECUTION" + "(TASK_EXECUTION_ID, START_TIME, TASK_NAME, LAST_UPDATED, EXTERNAL_EXECUTION_ID, PARENT_EXECUTION_ID)" - + "values (?, ?, ?, ?, ?, ?)"; + + "values (:taskExecutionId, :startTime, :taskName, :lastUpdated, :externalExecutionId, :parentExecutionId)"; private static final String CREATE_TASK_ARGUMENT = "INSERT into " - + "%PREFIX%EXECUTION_PARAMS(TASK_EXECUTION_ID, TASK_PARAM ) values (?, ?)"; + + "%PREFIX%EXECUTION_PARAMS(TASK_EXECUTION_ID, TASK_PARAM ) values (:taskExecutionId, :taskParam)"; private static final String START_TASK_EXECUTION_PREFIX = "UPDATE %PREFIX%EXECUTION set " - + "START_TIME = ?, TASK_NAME = ?, LAST_UPDATED = ?"; + + "START_TIME = :startTime, TASK_NAME = :taskName, LAST_UPDATED = :lastUpdated"; private static final String START_TASK_EXECUTION_EXTERNAL_ID_SUFFIX = ", " - + "EXTERNAL_EXECUTION_ID = ?, PARENT_EXECUTION_ID = ? where TASK_EXECUTION_ID = ?"; + + "EXTERNAL_EXECUTION_ID = :externalExecutionId, PARENT_EXECUTION_ID = :parentExecutionId where TASK_EXECUTION_ID = :taskExecutionId"; - private static final String START_TASK_EXECUTION_SUFFIX = ", PARENT_EXECUTION_ID = ? where TASK_EXECUTION_ID = ?"; + private static final String START_TASK_EXECUTION_SUFFIX = ", PARENT_EXECUTION_ID = :parentExecutionId where TASK_EXECUTION_ID = :taskExecutionId"; private static final String CHECK_TASK_EXECUTION_EXISTS = "SELECT COUNT(*) FROM " - + "%PREFIX%EXECUTION WHERE TASK_EXECUTION_ID = ?"; + + "%PREFIX%EXECUTION WHERE TASK_EXECUTION_ID = :taskExecutionId"; private static final String UPDATE_TASK_EXECUTION = "UPDATE %PREFIX%EXECUTION set " - + "END_TIME = ?, EXIT_CODE = ?, EXIT_MESSAGE = ?, ERROR_MESSAGE = ?, " - + "LAST_UPDATED = ? where TASK_EXECUTION_ID = ?"; + + "END_TIME = :endTime, EXIT_CODE = :exitCode, EXIT_MESSAGE = :exitMessage, ERROR_MESSAGE = :errorMessage, " + + "LAST_UPDATED = :lastUpdated where TASK_EXECUTION_ID = :taskExecutionId"; private static final String UPDATE_TASK_EXECUTION_EXTERNAL_EXECUTION_ID = "UPDATE %PREFIX%EXECUTION set " - + "EXTERNAL_EXECUTION_ID = ? where TASK_EXECUTION_ID = ?"; + + "EXTERNAL_EXECUTION_ID = :externalExecutionId where TASK_EXECUTION_ID = :taskExecutionId"; private static final String GET_EXECUTION_BY_ID = "SELECT TASK_EXECUTION_ID, " + "START_TIME, END_TIME, TASK_NAME, EXIT_CODE, " + "EXIT_MESSAGE, ERROR_MESSAGE, LAST_UPDATED, EXTERNAL_EXECUTION_ID, " + "PARENT_EXECUTION_ID " - + "from %PREFIX%EXECUTION where TASK_EXECUTION_ID = ?"; + + "from %PREFIX%EXECUTION where TASK_EXECUTION_ID = :taskExecutionId"; private static final String FIND_ARGUMENT_FROM_ID = "SELECT TASK_EXECUTION_ID, " - + "TASK_PARAM from %PREFIX%EXECUTION_PARAMS where TASK_EXECUTION_ID = ?"; + + "TASK_PARAM from %PREFIX%EXECUTION_PARAMS where TASK_EXECUTION_ID = :taskExecutionId"; private static final String TASK_EXECUTION_COUNT = "SELECT COUNT(*) FROM " + "%PREFIX%EXECUTION "; private static final String TASK_EXECUTION_COUNT_BY_NAME = "SELECT COUNT(*) FROM " + - "%PREFIX%EXECUTION where TASK_NAME = ?"; + "%PREFIX%EXECUTION where TASK_NAME = :taskName"; private static final String RUNNING_TASK_EXECUTION_COUNT_BY_NAME = "SELECT COUNT(*) FROM " + - "%PREFIX%EXECUTION where TASK_NAME = ? AND END_TIME IS NULL "; + "%PREFIX%EXECUTION where TASK_NAME = :taskName AND END_TIME IS NULL "; + + private static final String LAST_TASK_EXECUTIONS_BY_TASK_NAMES = + "select TE2.* from (" + + "select MAX(TE.TASK_EXECUTION_ID) as TASK_EXECUTION_ID, TE.TASK_NAME, TE.START_TIME from (" + + "select TASK_NAME, MAX(START_TIME) as START_TIME" + + " FROM %PREFIX%EXECUTION where TASK_NAME in (:taskNames)" + + " GROUP BY TASK_NAME" + + ") TE_MAX " + + "inner join %PREFIX%EXECUTION TE ON TE.TASK_NAME = TE_MAX.TASK_NAME AND TE.START_TIME = TE_MAX.START_TIME " + + "group by TE.TASK_NAME, TE.START_TIME" + + ") TE1 " + + "inner join %PREFIX%EXECUTION TE2 ON TE1.TASK_EXECUTION_ID = TE2.TASK_EXECUTION_ID " + + "order by TE2.START_TIME DESC, TE2.TASK_EXECUTION_ID DESC"; private static final String FIND_TASK_NAMES = "SELECT distinct TASK_NAME from %PREFIX%EXECUTION order by TASK_NAME"; - private static final String FIND_TASK_EXECUTION_BY_JOB_EXECUTION_ID = "SELECT TASK_EXECUTION_ID FROM %PREFIX%TASK_BATCH WHERE JOB_EXECUTION_ID = ?"; - private static final String FIND_JOB_EXECUTION_BY_TASK_EXECUTION_ID = "SELECT JOB_EXECUTION_ID FROM %PREFIX%TASK_BATCH WHERE TASK_EXECUTION_ID = ?"; + private static final String FIND_TASK_EXECUTION_BY_JOB_EXECUTION_ID = "SELECT TASK_EXECUTION_ID FROM %PREFIX%TASK_BATCH WHERE JOB_EXECUTION_ID = :jobExecutionId"; + + private static final String FIND_JOB_EXECUTION_BY_TASK_EXECUTION_ID = "SELECT JOB_EXECUTION_ID FROM %PREFIX%TASK_BATCH WHERE TASK_EXECUTION_ID = :taskExecutionId"; private String tablePrefix = TaskProperties.DEFAULT_TABLE_PREFIX; - private JdbcOperations jdbcTemplate; + private final NamedParameterJdbcTemplate jdbcTemplate; private DataSource dataSource; @@ -146,8 +161,8 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { * @param dataSource used by the dao to execute queries and update the tables. */ public JdbcTaskExecutionDao(DataSource dataSource) { - Assert.notNull(dataSource); - this.jdbcTemplate = new JdbcTemplate(dataSource); + Assert.notNull(dataSource, "The dataSource must not be null."); + this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource); this.dataSource = dataSource; orderMap = new LinkedHashMap<>(); orderMap.put("START_TIME", Order.DESCENDING); @@ -170,14 +185,17 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { TaskExecution taskExecution = new TaskExecution(nextExecutionId, null, taskName, startTime, null, null, arguments, null, externalExecutionId); - Object[] queryParameters = new Object[]{ nextExecutionId, startTime, - taskName, new Date(), externalExecutionId, - parentExecutionId}; + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskExecutionId", nextExecutionId, Types.BIGINT) + .addValue("startTime", startTime, Types.TIMESTAMP) + .addValue("taskName", taskName, Types.VARCHAR) + .addValue("lastUpdated", new Date(), Types.TIMESTAMP) + .addValue("externalExecutionId", externalExecutionId, Types.VARCHAR) + .addValue("parentExecutionId", parentExecutionId, Types.BIGINT); + jdbcTemplate.update( getQuery(SAVE_TASK_EXECUTION), - queryParameters, - new int[]{ Types.BIGINT, Types.TIMESTAMP, Types.VARCHAR, - Types.TIMESTAMP, Types.VARCHAR, Types.BIGINT}); + queryParameters); insertTaskArguments(nextExecutionId, arguments); return taskExecution; } @@ -196,25 +214,25 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { String externalExecutionId, Long parentExecutionId) { TaskExecution taskExecution = new TaskExecution(executionId, null, taskName, startTime, null, null, arguments,null, externalExecutionId, parentExecutionId); - Object[] queryParameters; - int[] argTypes; + + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("startTime", startTime, Types.TIMESTAMP) + .addValue("taskName", taskName, Types.VARCHAR) + .addValue("lastUpdated", new Date(), Types.TIMESTAMP) + .addValue("parentExecutionId", parentExecutionId, Types.BIGINT) + .addValue("taskExecutionId", executionId, Types.BIGINT); + String updateString = START_TASK_EXECUTION_PREFIX; + if(externalExecutionId == null) { - queryParameters = new Object[]{startTime, taskName, - new Date(), parentExecutionId, executionId}; updateString += START_TASK_EXECUTION_SUFFIX; - argTypes = new int[]{Types.TIMESTAMP, Types.VARCHAR, - Types.TIMESTAMP, Types.BIGINT, Types.BIGINT}; } else { - queryParameters = new Object[]{ startTime, taskName, - new Date(), externalExecutionId, parentExecutionId, executionId}; - argTypes = new int[]{ Types.TIMESTAMP, Types.VARCHAR, - Types.TIMESTAMP, Types.VARCHAR, Types.BIGINT, Types.BIGINT }; updateString += START_TASK_EXECUTION_EXTERNAL_ID_SUFFIX; + queryParameters.addValue("externalExecutionId", externalExecutionId, Types.VARCHAR); } - jdbcTemplate.update(getQuery(updateString), queryParameters, argTypes); + jdbcTemplate.update(getQuery(updateString), queryParameters); insertTaskArguments(executionId, arguments); return taskExecution; } @@ -222,20 +240,26 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public void completeTaskExecution(long taskExecutionId, Integer exitCode, Date endTime, String exitMessage, String errorMessage) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskExecutionId", taskExecutionId, Types.BIGINT); + // Check if given TaskExecution's Id already exists, if none is found // it is invalid and an exception should be thrown. - if (jdbcTemplate.queryForObject(getQuery(CHECK_TASK_EXECUTION_EXISTS), Integer.class, - taskExecutionId) != 1) { + if (jdbcTemplate.queryForObject(getQuery(CHECK_TASK_EXECUTION_EXISTS), queryParameters, Integer.class) != 1) { throw new IllegalStateException("Invalid TaskExecution, ID " + taskExecutionId + " not found."); } - Object[] parameters = new Object[]{ endTime, exitCode, exitMessage, errorMessage, new Date(), - taskExecutionId}; + final MapSqlParameterSource parameters = new MapSqlParameterSource() + .addValue("endTime", endTime, Types.TIMESTAMP) + .addValue("exitCode", exitCode, Types.INTEGER) + .addValue("exitMessage", exitMessage, Types.VARCHAR) + .addValue("errorMessage", errorMessage, Types.VARCHAR) + .addValue("lastUpdated", new Date(), Types.TIMESTAMP) + .addValue("taskExecutionId", taskExecutionId, Types.BIGINT); + jdbcTemplate.update( getQuery(UPDATE_TASK_EXECUTION), - parameters, - new int[]{ Types.TIMESTAMP, Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP, - Types.BIGINT}); + parameters); } @Override @@ -246,9 +270,12 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public TaskExecution getTaskExecution(long executionId) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskExecutionId", executionId, Types.BIGINT); + try { TaskExecution taskExecution = jdbcTemplate.queryForObject(getQuery(GET_EXECUTION_BY_ID), - new TaskExecutionRowMapper(), executionId); + queryParameters, new TaskExecutionRowMapper()); taskExecution.setArguments(getTaskArguments(executionId)); return taskExecution; } @@ -259,9 +286,13 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public long getTaskExecutionCountByTaskName(String taskName) { + + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskName", taskName, Types.VARCHAR); + try { - return jdbcTemplate.queryForObject( - getQuery(TASK_EXECUTION_COUNT_BY_NAME), new Object[] { taskName }, Long.class); + return jdbcTemplate.queryForObject( + getQuery(TASK_EXECUTION_COUNT_BY_NAME), queryParameters, Long.class); } catch (EmptyResultDataAccessException e) { return 0; @@ -270,20 +301,65 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public long getRunningTaskExecutionCountByTaskName(String taskName) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskName", taskName, Types.VARCHAR); + try { return jdbcTemplate.queryForObject( - getQuery(RUNNING_TASK_EXECUTION_COUNT_BY_NAME), new Object[] { taskName }, Long.class); + getQuery(RUNNING_TASK_EXECUTION_COUNT_BY_NAME), queryParameters, Long.class); } catch (EmptyResultDataAccessException e) { return 0; } } + @Override + public List getLatestTaskExecutionsByTaskNames(String... taskNames) { + Assert.notEmpty(taskNames, "At least 1 task name must be provided."); + + final List taskNamesAsList = new ArrayList<>(); + + for (String taskName : taskNames) { + if (StringUtils.hasText(taskName)) { + taskNamesAsList.add(taskName); + } + } + + Assert.isTrue(taskNamesAsList.size() == taskNames.length, + String.format("Task names must not contain any empty elements but %s of %s were empty or null.", + taskNames.length - taskNamesAsList.size(), taskNames.length)); + + try { + final Map> paramMap = Collections.singletonMap("taskNames", taskNamesAsList); + return this.jdbcTemplate.query( + getQuery(LAST_TASK_EXECUTIONS_BY_TASK_NAMES), paramMap, new TaskExecutionRowMapper()); + } + catch (EmptyResultDataAccessException e) { + return Collections.emptyList(); + } + } + + @Override + public TaskExecution getLatestTaskExecutionForTaskName(String taskName) { + Assert.hasText(taskName, "The task name must not be empty."); + final List taskExecutions = this.getLatestTaskExecutionsByTaskNames(taskName); + if (taskExecutions.isEmpty()) { + return null; + } + else if (taskExecutions.size() == 1) { + return taskExecutions.get(0); + } + else { + throw new IllegalStateException("Only expected a single TaskExecution but received " + taskExecutions.size()); + } + } + @Override public long getTaskExecutionCount() { + try { return jdbcTemplate.queryForObject( - getQuery(TASK_EXECUTION_COUNT), new Object[] { }, Long.class); + getQuery(TASK_EXECUTION_COUNT), new MapSqlParameterSource(), Long.class); } catch (EmptyResultDataAccessException e) { return 0; @@ -293,26 +369,26 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public Page findRunningTaskExecutions(String taskName, Pageable pageable) { return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE, - RUNNING_TASK_WHERE_CLAUSE, new Object[]{ taskName }, + RUNNING_TASK_WHERE_CLAUSE, new MapSqlParameterSource("taskName", taskName), getRunningTaskExecutionCountByTaskName(taskName)); } @Override public Page findTaskExecutionsByName(String taskName, Pageable pageable) { return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE, - TASK_NAME_WHERE_CLAUSE, new Object[]{ taskName }, + TASK_NAME_WHERE_CLAUSE, new MapSqlParameterSource("taskName", taskName), getTaskExecutionCountByTaskName(taskName)); } @Override public List getTaskNames() { - return jdbcTemplate.queryForList(getQuery(FIND_TASK_NAMES), String.class); + return jdbcTemplate.queryForList(getQuery(FIND_TASK_NAMES), new MapSqlParameterSource(), String.class); } @Override public Page findAll(Pageable pageable) { return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE, null, - new Object[]{ }, getTaskExecutionCount()); + new MapSqlParameterSource(), getTaskExecutionCount()); } public void setTaskIncrementer(DataFieldMaxValueIncrementer taskIncrementer) { @@ -325,10 +401,13 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public Long getTaskExecutionIdByJobExecutionId(long jobExecutionId) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("jobExecutionId", jobExecutionId, Types.BIGINT); + try { return jdbcTemplate.queryForObject( getQuery(FIND_TASK_EXECUTION_BY_JOB_EXECUTION_ID), - new Object[] { jobExecutionId }, + queryParameters, Long.class); } catch (EmptyResultDataAccessException e) { @@ -338,10 +417,13 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public Set getJobExecutionIdsByTaskExecutionId(long taskExecutionId) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskExecutionId", taskExecutionId, Types.BIGINT); + try { return jdbcTemplate.query( getQuery(FIND_JOB_EXECUTION_BY_TASK_EXECUTION_ID), - new Object[] {taskExecutionId}, + queryParameters, new ResultSetExtractor>() { @Override public Set extractData(ResultSet resultSet) throws SQLException, DataAccessException { @@ -362,12 +444,13 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { @Override public void updateExternalExecutionId(long taskExecutionId, String externalExecutionId) { - Object[] parameters = new Object[]{externalExecutionId, - taskExecutionId}; + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("externalExecutionId", externalExecutionId, Types.VARCHAR) + .addValue("taskExecutionId", taskExecutionId, Types.BIGINT); + if (jdbcTemplate.update( getQuery(UPDATE_TASK_EXECUTION_EXTERNAL_EXECUTION_ID), - parameters, - new int[]{Types.VARCHAR, Types.BIGINT}) != 1) { + queryParameters) != 1) { throw new IllegalStateException("Invalid TaskExecution, ID " + taskExecutionId + " not found."); } @@ -377,7 +460,7 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { String selectClause, String fromClause, String whereClause, - Object[] queryParam, + MapSqlParameterSource queryParameters, long totalCount){ SqlPagingQueryProviderFactoryBean factoryBean = new SqlPagingQueryProviderFactoryBean(); factoryBean.setSelectClause(selectClause); @@ -413,7 +496,7 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { String query = pagingQueryProvider.getPageQuery(pageable); List resultList = jdbcTemplate.query( getQuery(query), - queryParam, + queryParameters, new TaskExecutionRowMapper()); return new PageImpl<>(resultList, pageable, totalCount); } @@ -439,13 +522,14 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { * Convenience method that inserts an individual records into the * TASK_EXECUTION_PARAMS table. */ - private void insertArgument(long executionId, String param) { - int[] argTypes = new int[]{ Types.BIGINT, Types.VARCHAR }; - Object[] args = new Object[]{ executionId, param }; - jdbcTemplate.update(getQuery(CREATE_TASK_ARGUMENT), args, argTypes); + private void insertArgument(long taskExecutionId, String taskParam) { + final MapSqlParameterSource queryParameters = new MapSqlParameterSource() + .addValue("taskExecutionId", taskExecutionId, Types.BIGINT) + .addValue("taskParam", taskParam, Types.VARCHAR); + jdbcTemplate.update(getQuery(CREATE_TASK_ARGUMENT), queryParameters); } - private List getTaskArguments(long executionId){ + private List getTaskArguments(long taskExecutionId){ final List params= new ArrayList<>(); RowCallbackHandler handler = new RowCallbackHandler() { @Override @@ -453,8 +537,7 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { params.add(rs.getString(2)); } }; - - jdbcTemplate.query(getQuery(FIND_ARGUMENT_FROM_ID), new Object[] { executionId }, + jdbcTemplate.query(getQuery(FIND_ARGUMENT_FROM_ID), new MapSqlParameterSource("taskExecutionId", taskExecutionId), handler); return params; } @@ -486,10 +569,9 @@ public class JdbcTaskExecutionDao implements TaskExecutionDao { parentExecutionId); } - private Integer getNullableExitCode(ResultSet rs) throws SQLException { - int exitCode = rs.getInt("EXIT_CODE"); - return !rs.wasNull() ? exitCode : null; - } + private Integer getNullableExitCode(ResultSet rs) throws SQLException { + int exitCode = rs.getInt("EXIT_CODE"); + return !rs.wasNull() ? exitCode : null; + } } - } diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDao.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDao.java index 4c67ceda..3ba30b25 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDao.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,11 +34,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Stores Task Execution Information to a in-memory map. * * @author Glenn Renfro + * @author Gunnar Hillert */ public class MapTaskExecutionDao implements TaskExecutionDao { @@ -251,4 +254,72 @@ public class MapTaskExecutionDao implements TaskExecutionDao { executionList.subList((int)pageable.getOffset(), (int)toIndex), pageable, maxSize); } + + @Override + public List getLatestTaskExecutionsByTaskNames(String... taskNames) { + + Assert.notEmpty(taskNames, "At least 1 task name must be provided."); + + final List taskNamesAsList = new ArrayList<>(); + + for (String taskName : taskNames) { + if (StringUtils.hasText(taskName)) { + taskNamesAsList.add(taskName); + } + } + + Assert.isTrue(taskNamesAsList.size() == taskNames.length, + String.format("Task names must not contain any empty elements but %s of %s were empty or null.", + taskNames.length - taskNamesAsList.size(), taskNames.length)); + + final Map tempTaskExecutions = new HashMap<>(); + + for (Map.Entry taskExecutionMapEntry : this.taskExecutions.entrySet()) { + if (!taskNamesAsList.contains(taskExecutionMapEntry.getValue().getTaskName())) { + continue; + } + + final TaskExecution tempTaskExecution = tempTaskExecutions.get(taskExecutionMapEntry.getValue().getTaskName()); + if (tempTaskExecution == null + || tempTaskExecution.getStartTime().before(taskExecutionMapEntry.getValue().getStartTime()) + || ( + tempTaskExecution.getStartTime().equals(taskExecutionMapEntry.getValue().getStartTime()) + && tempTaskExecution.getExecutionId() < taskExecutionMapEntry.getValue().getExecutionId() + ) + ) { + tempTaskExecutions.put(taskExecutionMapEntry.getValue().getTaskName(), taskExecutionMapEntry.getValue()); + } + } + final List latestTaskExecutions = new ArrayList<>(tempTaskExecutions.values()); + Collections.sort(latestTaskExecutions, new TaskExecutionComparator()); + return latestTaskExecutions; + } + + @Override + public TaskExecution getLatestTaskExecutionForTaskName(String taskName) { + Assert.hasText(taskName, "The task name must not be empty."); + final List taskExecutions = this.getLatestTaskExecutionsByTaskNames(taskName); + if (taskExecutions.isEmpty()) { + return null; + } + else if (taskExecutions.size() == 1) { + return taskExecutions.get(0); + } + else { + throw new IllegalStateException("Only expected a single TaskExecution but received " + taskExecutions.size()); + } + } + + private class TaskExecutionComparator implements Comparator { + @Override + public int compare(TaskExecution firstTaskExecution, TaskExecution secondTaskExecution) { + if (firstTaskExecution.getStartTime().equals(secondTaskExecution.getStartTime())) { + return Long.compare(firstTaskExecution.getExecutionId(), secondTaskExecution.getExecutionId()); + } + else { + return secondTaskExecution.getStartTime().compareTo(firstTaskExecution.getStartTime()); + } + } + } + } diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/TaskExecutionDao.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/TaskExecutionDao.java index 69f9626c..13d48909 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/TaskExecutionDao.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/dao/TaskExecutionDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,8 @@ import org.springframework.data.domain.Pageable; * Data Access Object for task executions. * * @author Glenn Renfro + * @author Gunnar Hillert + * */ public interface TaskExecutionDao { @@ -201,4 +203,33 @@ public interface TaskExecutionDao { */ void updateExternalExecutionId(long taskExecutionId, String externalExecutionId); + + /** + * Returns a {@link List} of the latest {@link TaskExecution} for 1 or more task names. + * + * Latest is defined by the most recent start time. A {@link TaskExecution} does not have to be finished + * (The results may including pending {@link TaskExecution}s). + * + * It is theoretically possible that a {@link TaskExecution} with the same name to have more than 1 + * {@link TaskExecution} for the exact same start time. In that case the {@link TaskExecution} with the + * highest Task Execution ID is returned. + * + * This method will not consider end times in its calculations. Thus, when a task execution {@code A} starts + * after task execution {@code B} but finishes BEFORE task execution {@code A}, then task execution {@code B} + * is being returned. + * + * @param taskNames At least 1 task name must be provided + * @return List of TaskExecutions. May be empty but never null. + */ + List getLatestTaskExecutionsByTaskNames(String... taskNames); + + /** + * Returns the latest task execution for a given task name. Will ultimately apply the same algorithm underneath + * as {@link #getLatestTaskExecutionsByTaskNames(String...)} but will only return a single result. + * + * @param taskName Must not be null or empty + * @return The latest Task Execution or null + * @see #getLatestTaskExecutionsByTaskNames(String...) + */ + TaskExecution getLatestTaskExecutionForTaskName(String taskName); } diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorer.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorer.java index 49f70e7d..d955c06f 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorer.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.springframework.util.Assert; * * @author Glenn Renfro * @author Michael Minella + * @author Gunnar Hillert */ public class SimpleTaskExplorer implements TaskExplorer { @@ -92,4 +93,14 @@ public class SimpleTaskExplorer implements TaskExplorer { return taskExecutionDao.getJobExecutionIdsByTaskExecutionId(taskExecutionId); } + @Override + public List getLatestTaskExecutionsByTaskNames(String... taskNames) { + return taskExecutionDao.getLatestTaskExecutionsByTaskNames(taskNames); + } + + @Override + public TaskExecution getLatestTaskExecutionForTaskName(String taskName) { + return taskExecutionDao.getLatestTaskExecutionForTaskName(taskName); + } + } diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/BaseTaskExecutionDaoTestCases.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/BaseTaskExecutionDaoTestCases.java new file mode 100644 index 00000000..1c57e971 --- /dev/null +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/BaseTaskExecutionDaoTestCases.java @@ -0,0 +1,309 @@ +/* + * Copyright 2018 the original author or authors. + * + * 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.springframework.cloud.task.repository.dao; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; + +import org.junit.Test; +import org.springframework.cloud.task.repository.TaskExecution; +import org.springframework.test.annotation.DirtiesContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Defines test cases that shall be shared between {@link JdbcTaskExecutionDaoTests} and {@link MapTaskExecutionDaoTests}. + * + * @author Gunnar Hillert + */ +public class BaseTaskExecutionDaoTestCases { + + protected TaskExecutionDao dao; + + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithNullParameter() { + try { + dao.getLatestTaskExecutionsByTaskNames(null); + } + catch (IllegalArgumentException e) { + assertEquals("At least 1 task name must be provided.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithEmptyArrayParameter() { + try { + dao.getLatestTaskExecutionsByTaskNames(new String[0]); + } + catch (IllegalArgumentException e) { + assertEquals("At least 1 task name must be provided.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithArrayParametersContainingNullAndEmptyValues() { + try { + dao.getLatestTaskExecutionsByTaskNames("foo", null, "bar", " "); + } + catch (IllegalArgumentException e) { + assertEquals("Task names must not contain any empty elements but 2 of 4 were empty or null.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithSingleTaskName() { + initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final List latestTaskExecutions = dao.getLatestTaskExecutionsByTaskNames("FOO1"); + assertTrue("Expected only 1 taskExecution but got " + latestTaskExecutions.size(), latestTaskExecutions.size() == 1); + + final TaskExecution lastTaskExecution = latestTaskExecutions.get(0); + assertEquals("FOO1", lastTaskExecution.getTaskName()); + + final Calendar dateTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTime.setTime(lastTaskExecution.getStartTime()); + + assertEquals(2015, dateTime.get(Calendar.YEAR)); + assertEquals(2, dateTime.get(Calendar.MONTH) + 1); + assertEquals(22, dateTime.get(Calendar.DAY_OF_MONTH)); + assertEquals(23, dateTime.get(Calendar.HOUR_OF_DAY)); + assertEquals(59, dateTime.get(Calendar.MINUTE)); + assertEquals(0, dateTime.get(Calendar.SECOND)); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithMultipleTaskNames() { + initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final List latestTaskExecutions = dao.getLatestTaskExecutionsByTaskNames("FOO1", "FOO3", "FOO4"); + assertTrue("Expected 3 taskExecutions but got " + latestTaskExecutions.size(), latestTaskExecutions.size() == 3); + + final Calendar dateTimeFoo3 = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTimeFoo3.setTime(latestTaskExecutions.get(0).getStartTime()); + + assertEquals(2016, dateTimeFoo3.get(Calendar.YEAR)); + assertEquals(8, dateTimeFoo3.get(Calendar.MONTH) + 1); + assertEquals(20, dateTimeFoo3.get(Calendar.DAY_OF_MONTH)); + assertEquals(14, dateTimeFoo3.get(Calendar.HOUR_OF_DAY)); + assertEquals(45, dateTimeFoo3.get(Calendar.MINUTE)); + assertEquals(0, dateTimeFoo3.get(Calendar.SECOND)); + + final Calendar dateTimeFoo1 = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTimeFoo1.setTime(latestTaskExecutions.get(1).getStartTime()); + + assertEquals(2015, dateTimeFoo1.get(Calendar.YEAR)); + assertEquals(2, dateTimeFoo1.get(Calendar.MONTH) + 1); + assertEquals(22, dateTimeFoo1.get(Calendar.DAY_OF_MONTH)); + assertEquals(23, dateTimeFoo1.get(Calendar.HOUR_OF_DAY)); + assertEquals(59, dateTimeFoo1.get(Calendar.MINUTE)); + assertEquals(0, dateTimeFoo1.get(Calendar.SECOND)); + + final Calendar dateTimeFoo4 = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTimeFoo4.setTime(latestTaskExecutions.get(2).getStartTime()); + + assertEquals(2015, dateTimeFoo4.get(Calendar.YEAR)); + assertEquals(2, dateTimeFoo4.get(Calendar.MONTH) + 1); + assertEquals(20, dateTimeFoo4.get(Calendar.DAY_OF_MONTH)); + assertEquals(14, dateTimeFoo4.get(Calendar.HOUR_OF_DAY)); + assertEquals(45, dateTimeFoo4.get(Calendar.MINUTE)); + assertEquals(0, dateTimeFoo4.get(Calendar.SECOND)); + } + + /** + * This test is a special use-case. While not common, it is theoretically possible, that a task may have + * executed with the exact same start time multiple times. In that case we should still only get 1 returned + * {@link TaskExecution}. + */ + @Test + @DirtiesContext + public void getLatestTaskExecutionsByTaskNamesWithIdenticalTaskExecutions() { + long executionIdOffset = initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final List latestTaskExecutions = dao.getLatestTaskExecutionsByTaskNames("FOO5"); + assertTrue("Expected only 1 taskExecution but got " + latestTaskExecutions.size(), latestTaskExecutions.size() == 1); + + final Calendar dateTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTime.setTime(latestTaskExecutions.get(0).getStartTime()); + + assertEquals(2015, dateTime.get(Calendar.YEAR)); + assertEquals(2, dateTime.get(Calendar.MONTH) + 1); + assertEquals(22, dateTime.get(Calendar.DAY_OF_MONTH)); + assertEquals(23, dateTime.get(Calendar.HOUR_OF_DAY)); + assertEquals(59, dateTime.get(Calendar.MINUTE)); + assertEquals(0, dateTime.get(Calendar.SECOND)); + assertEquals(9 + executionIdOffset, latestTaskExecutions.get(0).getExecutionId()); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionForTaskNameWithNullParameter() { + try { + dao.getLatestTaskExecutionForTaskName(null); + } + catch (IllegalArgumentException e) { + assertEquals("The task name must not be empty.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionForTaskNameWithEmptyStringParameter() { + try { + dao.getLatestTaskExecutionForTaskName(""); + } + catch (IllegalArgumentException e) { + assertEquals("The task name must not be empty.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionForNonExistingTaskName() { + initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final TaskExecution latestTaskExecution = dao.getLatestTaskExecutionForTaskName("Bar5"); + assertNull("Expected the latestTaskExecution to be null but got" + latestTaskExecution, latestTaskExecution); + } + + @Test + @DirtiesContext + public void getLatestTaskExecutionForExistingTaskName() { + initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final TaskExecution latestTaskExecution = dao.getLatestTaskExecutionForTaskName("FOO1"); + assertNotNull("Expected the latestTaskExecution not to be null", latestTaskExecution); + + final Calendar dateTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTime.setTime(latestTaskExecution.getStartTime()); + + assertEquals(2015, dateTime.get(Calendar.YEAR)); + assertEquals(2, dateTime.get(Calendar.MONTH) + 1); + assertEquals(22, dateTime.get(Calendar.DAY_OF_MONTH)); + assertEquals(23, dateTime.get(Calendar.HOUR_OF_DAY)); + assertEquals(59, dateTime.get(Calendar.MINUTE)); + assertEquals(0, dateTime.get(Calendar.SECOND)); + } + + /** + * This test is a special use-case. While not common, it is theoretically possible, that a task may have + * executed with the exact same start time multiple times. In that case we should still only get 1 returned + * {@link TaskExecution}. + */ + @Test + @DirtiesContext + public void getLatestTaskExecutionForTaskNameWithIdenticalTaskExecutions() { + long executionIdOffset = initializeRepositoryNotInOrderWithMultipleTaskExecutions(); + final TaskExecution latestTaskExecution = dao.getLatestTaskExecutionForTaskName("FOO5"); + assertNotNull("Expected the latestTaskExecution not to be null", latestTaskExecution); + + final Calendar dateTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dateTime.setTime(latestTaskExecution.getStartTime()); + + assertEquals(2015, dateTime.get(Calendar.YEAR)); + assertEquals(2, dateTime.get(Calendar.MONTH) + 1); + assertEquals(22, dateTime.get(Calendar.DAY_OF_MONTH)); + assertEquals(23, dateTime.get(Calendar.HOUR_OF_DAY)); + assertEquals(59, dateTime.get(Calendar.MINUTE)); + assertEquals(0, dateTime.get(Calendar.SECOND)); + assertEquals(9 + executionIdOffset, latestTaskExecution.getExecutionId()); + } + + protected long initializeRepositoryNotInOrderWithMultipleTaskExecutions() { + + final TaskExecution foo1_0 = getTaskExecution("FOO1", "externalC"); + foo1_0.setStartTime(getDate(2015, 2, 22, 23, 59)); + + final TaskExecution foo1_1 = getTaskExecution("FOO1", "externalC"); + foo1_1.setStartTime(getDate(2015, 2, 20, 14, 45)); + + final TaskExecution foo1_2 = getTaskExecution("FOO1", "externalC"); + foo1_2.setStartTime(getDate(2015, 1, 19, 14, 30)); + + final TaskExecution foo1_3 = getTaskExecution("FOO1", "externalC"); + foo1_3.setStartTime(getDate(2015, 1, 20, 14, 45)); + + TaskExecution foo2 = getTaskExecution("FOO2", "externalA"); + foo2.setStartTime(getDate(2015, 4, 20, 14, 45)); + + TaskExecution foo3 = getTaskExecution("FOO3", "externalB"); + foo3.setStartTime(getDate(2016, 8, 20, 14, 45)); + + TaskExecution foo4 = getTaskExecution("FOO4", "externalB"); + foo4.setStartTime(getDate(2015, 2, 20, 14, 45)); + + final TaskExecution foo5_0 = getTaskExecution("FOO5", "externalC"); + foo5_0.setStartTime(getDate(2015, 2, 22, 23, 59)); + + final TaskExecution foo5_1 = getTaskExecution("FOO5", "externalC"); + foo5_1.setStartTime(getDate(2015, 2, 22, 23, 59)); + + final TaskExecution foo5_2 = getTaskExecution("FOO5", "externalC"); + foo5_2.setStartTime(getDate(2015, 2, 22, 23, 59)); + + long executionIdOffset = this.createTaskExecution(foo1_0); + this.createTaskExecution(foo1_1); + this.createTaskExecution(foo1_2); + this.createTaskExecution(foo1_3); + + this.createTaskExecution(foo2); + this.createTaskExecution(foo3); + this.createTaskExecution(foo4); + + this.createTaskExecution(foo5_0); + this.createTaskExecution(foo5_1); + this.createTaskExecution(foo5_2); + + return executionIdOffset; + } + + private Date getDate(int year, int month, int day, int hour, int minute) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + calendar.clear(); + calendar.set(year, month - 1, day, hour, minute); + return calendar.getTime(); + } + + private long createTaskExecution(TaskExecution te) { + return dao.createTaskExecution(te.getTaskName(), te.getStartTime(), te.getArguments(), te.getExternalExecutionId()).getExecutionId(); + } + + protected TaskExecution getTaskExecution(String taskName, + String externalExecutionId) { + TaskExecution taskExecution = new TaskExecution(); + taskExecution.setTaskName(taskName); + taskExecution.setExternalExecutionId(externalExecutionId); + taskExecution.setStartTime(new Date()); + return taskExecution; + } +} diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDaoTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDaoTests.java index cdbe1c51..110231d9 100644 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDaoTests.java +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/JdbcTaskExecutionDaoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.UUID; + import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; @@ -49,25 +49,25 @@ import static org.junit.Assert.assertEquals; * Executes unit tests on JdbcTaskExecutionDao. * * @author Glenn Renfro + * @author Gunnar Hillert */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestConfiguration.class, EmbeddedDataSourceConfiguration.class, PropertyPlaceholderAutoConfiguration.class}) -public class JdbcTaskExecutionDaoTests { +public class JdbcTaskExecutionDaoTests extends BaseTaskExecutionDaoTestCases { @Autowired private DataSource dataSource; - private JdbcTaskExecutionDao dao; - @Autowired TaskRepository repository; @Before public void setup(){ - dao = new JdbcTaskExecutionDao(dataSource); + final JdbcTaskExecutionDao dao = new JdbcTaskExecutionDao(dataSource); dao.setTaskIncrementer(TestDBUtils.getIncrementer(dataSource)); + super.dao = dao; } @Test @@ -221,13 +221,4 @@ public class JdbcTaskExecutionDaoTests { repository.createTaskExecution(getTaskExecution("FOO2", "externalA")); repository.createTaskExecution(getTaskExecution("FOO3", "externalB")); } - - private TaskExecution getTaskExecution(String taskName, - String externalExecutionId) { - TaskExecution taskExecution = new TaskExecution(); - taskExecution.setTaskName(taskName); - taskExecution.setExternalExecutionId(externalExecutionId); - taskExecution.setStartTime(new Date()); - return taskExecution; - } } diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDaoTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDaoTests.java index e75c4117..f1c7a742 100644 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDaoTests.java +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/dao/MapTaskExecutionDaoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import java.util.UUID; import org.junit.Before; import org.junit.Test; - import org.springframework.cloud.task.repository.TaskExecution; import org.springframework.cloud.task.util.TestVerifierUtils; @@ -37,15 +36,18 @@ import static org.junit.Assert.assertNull; /** * Executes unit tests on MapTaskExecutionDaoTests. + * * @author Glenn Renfro + * @author Gunnar Hillert */ -public class MapTaskExecutionDaoTests { +public class MapTaskExecutionDaoTests extends BaseTaskExecutionDaoTestCases{ - private MapTaskExecutionDao dao; + private MapTaskExecutionDao mapTaskExecutionDao; @Before public void setUp() { - this.dao = new MapTaskExecutionDao(); + this.mapTaskExecutionDao = new MapTaskExecutionDao(); + super.dao = this.mapTaskExecutionDao; } @Test @@ -59,7 +61,7 @@ public class MapTaskExecutionDaoTests { this.dao.startTaskExecution(expectedTaskExecution.getExecutionId(), expectedTaskExecution.getTaskName(), expectedTaskExecution.getStartTime(), expectedTaskExecution.getArguments(), expectedTaskExecution.getExternalExecutionId()); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); assertNotNull("taskExecutionMap must not be null", taskExecutionMap); TestVerifierUtils.verifyTaskExecution(expectedTaskExecution, taskExecutionMap.get(expectedTaskExecution.getExecutionId())); @@ -70,7 +72,7 @@ public class MapTaskExecutionDaoTests { TaskExecution expectedTaskExecution = dao.createTaskExecution(null, null, new ArrayList(0), null); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); TestVerifierUtils.verifyTaskExecution(expectedTaskExecution, taskExecutionMap.get(expectedTaskExecution.getExecutionId())); } @@ -89,7 +91,7 @@ public class MapTaskExecutionDaoTests { expectedTaskExecution = this.dao.createTaskExecution(expectedTaskExecution.getTaskName(), expectedTaskExecution.getStartTime(), expectedTaskExecution.getArguments(), expectedTaskExecution.getExternalExecutionId()); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); assertNotNull("taskExecutionMap must not be null", taskExecutionMap); TestVerifierUtils.verifyTaskExecution(expectedTaskExecution, taskExecutionMap.get(expectedTaskExecution.getExecutionId())); @@ -104,7 +106,7 @@ public class MapTaskExecutionDaoTests { this.dao.completeTaskExecution(expectedTaskExecution.getExecutionId(), expectedTaskExecution.getExitCode(), expectedTaskExecution.getEndTime(), expectedTaskExecution.getExitMessage()); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); assertNotNull("taskExecutionMap must not be null", taskExecutionMap); TestVerifierUtils.verifyTaskExecution(expectedTaskExecution, taskExecutionMap.get(expectedTaskExecution.getExecutionId())); @@ -124,10 +126,10 @@ public class MapTaskExecutionDaoTests { expectedTaskExecution.getExitCode(), expectedTaskExecution.getEndTime(), expectedTaskExecution.getExitMessage()); } - Set jobIds = new HashSet(2); + Set jobIds = new HashSet<>(2); jobIds.add(123L); jobIds.add(456L); - this.dao.getBatchJobAssociations().put( + this.mapTaskExecutionDao.getBatchJobAssociations().put( expectedTaskExecutionList.get(0).getExecutionId(), jobIds); assertEquals(Long.valueOf(expectedTaskExecutionList.get(0).getExecutionId()), @@ -141,7 +143,7 @@ public class MapTaskExecutionDaoTests { public void testStartExecutionWithNullExternalExecutionIdExisting(){ TaskExecution expectedTaskExecution = initializeTaskExecutionWithExternalExecutionId(); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); this.dao.startTaskExecution(expectedTaskExecution.getExecutionId(), expectedTaskExecution.getTaskName(), expectedTaskExecution.getStartTime(), expectedTaskExecution.getArguments(), null); @@ -153,7 +155,7 @@ public class MapTaskExecutionDaoTests { public void testStartExecutionWithNullExternalExecutionIdNonExisting(){ TaskExecution expectedTaskExecution = initializeTaskExecutionWithExternalExecutionId(); - Map taskExecutionMap = this.dao.getTaskExecutions(); + Map taskExecutionMap = this.mapTaskExecutionDao.getTaskExecutions(); this.dao.startTaskExecution(expectedTaskExecution.getExecutionId(), expectedTaskExecution.getTaskName(), expectedTaskExecution.getStartTime(), expectedTaskExecution.getArguments(), "BAR"); @@ -167,4 +169,5 @@ public class MapTaskExecutionDaoTests { expectedTaskExecution.getStartTime(), expectedTaskExecution.getArguments(), "FOO1"); } + } diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorerTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorerTests.java index 16f60ecb..515ea3c5 100644 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorerTests.java +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/repository/support/SimpleTaskExplorerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; import java.util.TreeSet; @@ -59,6 +58,7 @@ import org.springframework.data.domain.Pageable; /** * @author Glenn Renfro + * @author Gunnar Hillert */ @RunWith(Parameterized.class) public class SimpleTaskExplorerTests { @@ -124,7 +124,7 @@ public class SimpleTaskExplorerTests { @Test public void taskExecutionNotFound() { - Map< Long, TaskExecution> expectedResults = createSampleDataSet(5); + createSampleDataSet(5); TaskExecution actualTaskExecution = taskExplorer.getTaskExecution(-5); @@ -147,7 +147,7 @@ public class SimpleTaskExplorerTests { @Test public void getTaskCount() { - Map expectedResults = createSampleDataSet(33); + createSampleDataSet(33); assertEquals(String.format( "task count did not match expected result for test Type %s", testType), @@ -192,7 +192,6 @@ public class SimpleTaskExplorerTests { public void findTasksByName() { final int TEST_COUNT = 5; final int COMPLETE_COUNT = 7; - Random randomGenerator = new Random(); Map expectedResults = new HashMap<>(); //Store completed jobs @@ -271,12 +270,50 @@ public class SimpleTaskExplorerTests { assertEquals(0, taskExplorer.getJobExecutionIdsByTaskExecutionId(555555L).size()); } + @Test + public void getLatestTaskExecutionForTaskName() { + Map expectedResults = createSampleDataSet(5); + for (Map.Entry taskExecutionMapEntry: expectedResults.entrySet()) { + TaskExecution latestTaskExecution = + taskExplorer.getLatestTaskExecutionForTaskName(taskExecutionMapEntry.getValue().getTaskName()); + assertNotNull(String.format( + "expected a taskExecution but got null for test type %s", testType), + latestTaskExecution); + TestVerifierUtils.verifyTaskExecution( + expectedResults.get(latestTaskExecution.getExecutionId()), + latestTaskExecution); + } + } + + @Test + public void getLatestTaskExecutionsByTaskNames() { + Map expectedResults = createSampleDataSet(5); + + final List taskNamesAsList = new ArrayList<>(); + + for (TaskExecution taskExecution : expectedResults.values()) { + taskNamesAsList.add(taskExecution.getTaskName()); + } + + final List latestTaskExecutions = taskExplorer.getLatestTaskExecutionsByTaskNames( + taskNamesAsList.toArray(new String[taskNamesAsList.size()])); + + for (TaskExecution latestTaskExecution : latestTaskExecutions) { + assertNotNull(String.format( + "expected a taskExecution but got null for test type %s", testType), + latestTaskExecution); + TestVerifierUtils.verifyTaskExecution( + expectedResults.get(latestTaskExecution.getExecutionId()), + latestTaskExecution); + } + } + private void verifyPageResults(Pageable pageable, int totalNumberOfExecs) { Map expectedResults = createSampleDataSet(totalNumberOfExecs); List sortedExecIds = getSortedOfTaskExecIds(expectedResults); Iterator expectedTaskExecutionIter = sortedExecIds.iterator(); //Verify pageable totals - Page taskPage = taskExplorer.findAll(pageable); + Page taskPage = taskExplorer.findAll(pageable); int pagesExpected = (int) Math.ceil(totalNumberOfExecs / ((double) pageable.getPageSize())); assertEquals("actual page count return was not the expected total", pagesExpected, @@ -356,7 +393,7 @@ public class SimpleTaskExplorerTests { private List getSortedOfTaskExecIds(Map taskExecutionMap){ List sortedExecIds = new ArrayList<>(taskExecutionMap.size()); - TreeSet sortedSet = getTreeSet(); + TreeSet sortedSet = getTreeSet(); sortedSet.addAll(taskExecutionMap.values()); Iterator iterator = sortedSet.descendingIterator(); while(iterator.hasNext()){ @@ -365,7 +402,7 @@ public class SimpleTaskExplorerTests { return sortedExecIds; } - private TreeSet getTreeSet(){ + private TreeSet getTreeSet(){ return new TreeSet(new Comparator() { @Override public int compare(TaskExecution e1, TaskExecution e2) {