Skip to content

Commit

Permalink
[FEATURE] Introduce concept of task identifiers to AbstractBuilder
Browse files Browse the repository at this point in the history
Task identifiers (in short taskIds) can now be used in AbstractBuilder to define the order of multiple task executions. So far the name of the task (taskName) was treated the identifier of a task. If the same task was added to the execution multiple times, an ID in form taskName--1 was generated, counting upwards from one. In the task definition there was no way of defining the sub-order of multiple executions.

Now with the introduction of task identifiers, one can define a { id: "taskA", ... } in the task definition or call addTask with both an identifier and the name of the task to call. The old logic of generating an identifier, in case only a name is defined stays in place.

The before/afterTask declarations now no longer refer to task names but to task identifiers instead. However due to task names have previously been treated as task identifiers exclusively and also the old incrementation logic for task names stayed the same, this change is compatible with any old configuration.
  • Loading branch information
kristian committed Aug 23, 2021
1 parent 46176a2 commit f2b8b57
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 38 deletions.
103 changes: 65 additions & 38 deletions lib/types/AbstractBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class AbstractBuilder {
this.taskLog = this.log.createTaskLogger("🔨");

this.tasks = {};
this.taskExecutions = {};
this.taskExecutionOrder = [];
this.addStandardTasks({
resourceCollections,
Expand Down Expand Up @@ -85,25 +86,34 @@ class AbstractBuilder {
`at index ${i}`);
}
if (taskDef.beforeTask && taskDef.afterTask) {
throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` +
`defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`);
throw new Error(`Custom task definition ${taskDef.id || taskDef.name} of project ` +
`${project.metadata.name} defines both "beforeTask" and "afterTask" parameters. ` +
`Only one must be defined.`);
}
if (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) {
// Iff there are tasks configured, beforeTask or afterTask must be given
throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` +
`defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`);
// If there are tasks configured, beforeTask or afterTask must be given
throw new Error(`Custom task definition ${taskDef.id || taskDef.name} of project ` +
`${project.metadata.name} defines neither a "beforeTask" nor an "afterTask" parameter. ` +
`One must be defined.`);
}

let newTaskName = taskDef.name;
if (this.tasks[newTaskName]) {
// Task is already known
// => add a suffix to allow for multiple configurations of the same task
let suffixCounter = 0;
while (this.tasks[newTaskName]) {
suffixCounter++; // Start at 1
newTaskName = `${taskDef.name}--${suffixCounter}`;
let taskId = taskDef.id;
if (!taskId) {
// No identifier defined, use the task name and add a suffix if necessary
taskId = taskDef.name;
if (this.tasks[taskId]) {
// Task is already known => add a suffix to allow for multiple configurations of the same task
let suffixCounter = 0;
while (this.tasks[taskId]) {
suffixCounter++; // Start at 1
taskId = `${taskDef.name}--${suffixCounter}`;
}
}
} else if (this.tasks[taskId]) {
throw new Error(`Conflicting custom task definition ${taskId} of project ${project.metadata.name}, ` +
`more than one task with the same identifier defined. Task identifiers must be unique.`);
}

// Create custom task if not already done (task might be referenced multiple times, first one wins)
const {specVersion, task} = taskRepository.getTask(taskDef.name);
const execTask = function() {
Expand Down Expand Up @@ -140,24 +150,25 @@ class AbstractBuilder {
return task(params);
};

this.tasks[newTaskName] = execTask;
this.tasks[taskId] = execTask;

(this.taskExecutions[taskDef.name] || (this.taskExecutions[taskDef.name] = [])).push(taskId);
if (this.taskExecutionOrder.length) {
// There is at least one task configured. Use before- and afterTask to add the custom task
const refTaskName = taskDef.beforeTask || taskDef.afterTask;
let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskName);
const refTaskId = taskDef.beforeTask || taskDef.afterTask;
let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskId);
if (refTaskIdx === -1) {
throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` +
throw new Error(`Could not find task ${refTaskId}, referenced by custom task ${taskId}, ` +
`to be scheduled for project ${project.metadata.name}`);
}
if (taskDef.afterTask) {
// Insert after index of referenced task
refTaskIdx++;
}
this.taskExecutionOrder.splice(refTaskIdx, 0, newTaskName);
this.taskExecutionOrder.splice(refTaskIdx, 0, taskId);
} else {
// There is no task configured so far. Just add the custom task
this.taskExecutionOrder.push(newTaskName);
this.taskExecutionOrder.push(taskId);
}
}
}
Expand All @@ -167,48 +178,64 @@ class AbstractBuilder {
*
* The order this function is being called defines the build order. FIFO.
*
* @param {string} [taskId] Identifier of the task which should be in the list availableTasks.
* @param {string} taskName Name of the task which should be in the list availableTasks.
* @param {Function} taskFunction
*/
addTask(taskName, taskFunction) {
if (this.tasks[taskName]) {
throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`);
addTask(taskId, taskName, taskFunction) {
if (typeof taskName === "function") {
taskFunction = taskName;
taskName = taskId;
}

if (this.tasks[taskId]) {
throw new Error(`Failed to add duplicate task ${taskId} for project ${this.project.metadata.name}`);
}
if (this.taskExecutionOrder.includes(taskName)) {
throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` +
if (this.taskExecutionOrder.includes(taskId)) {
throw new Error(`Builder: Failed to add task ${taskId} for project ${this.project.metadata.name}. ` +
`It has already been scheduled for execution.`);
}
this.tasks[taskName] = taskFunction;
this.taskExecutionOrder.push(taskName);

this.tasks[taskId] = taskFunction;
(this.taskExecutions[taskName] || (this.taskExecutions[taskName] = [])).push(taskId);
this.taskExecutionOrder.push(taskId);
}

/**
* Check whether a task is defined
*
* @private
* @param {string} taskName
* @param {string} taskId
* @returns {boolean}
*/
hasTask(taskName) {
return Object.prototype.hasOwnProperty.call(this.tasks, taskName);
hasTask(taskId) {
return Object.prototype.hasOwnProperty.call(this.tasks, taskId);
}

/**
* Takes a list of tasks which should be executed from the available task list of the current builder
*
* @param {Array} tasksToRun List of tasks which should be executed
* @param {Array} tasksToRun List of tasks names which should be executed
* @returns {Promise} Returns promise chain with tasks
*/
build(tasksToRun) {
const allTasks = this.taskExecutionOrder.filter((taskName) => this.hasTask(taskName) &&
tasksToRun.includes(taskName.replace(/--\d+$/g, String())));
const taskIdsToRun = tasksToRun.reduce((tasks, taskName) => {
if (this.taskExecutions[taskName]) {
tasks.push(...this.taskExecutions[taskName]);
}

return tasks;
}, []);

const allTasks = this.taskExecutionOrder.filter((taskId) =>
this.hasTask(taskId) && taskIdsToRun.includes(taskId));
this.taskLog.addWork(allTasks.length);

return allTasks.reduce((taskChain, taskName) => {
const taskFunction = this.tasks[taskName];
return allTasks.reduce((taskChain, taskId) => {
const taskFunction = this.tasks[taskId];

if (typeof taskFunction === "function") {
taskChain = taskChain.then(this.wrapTask(taskName, taskFunction));
taskChain = taskChain.then(this.wrapTask(taskId, taskFunction));
}

return taskChain;
Expand All @@ -219,13 +246,13 @@ class AbstractBuilder {
* Adds progress related functionality to task function.
*
* @private
* @param {string} taskName Name of the task
* @param {string} taskId Name of the task
* @param {Function} taskFunction Function which executed the task
* @returns {Function} Wrapped task function
*/
wrapTask(taskName, taskFunction) {
wrapTask(taskId, taskFunction) {
return () => {
this.taskLog.startWork(`Running task ${taskName}...`);
this.taskLog.startWork(`Running task ${taskId}...`);
return taskFunction().then(() => this.taskLog.completeWork(1));
};
}
Expand Down
Loading

0 comments on commit f2b8b57

Please sign in to comment.