From ebae1b170f0c9ba1e3a8dc6e184426e7fefd03fe Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Wed, 27 Mar 2019 22:20:38 +0100
Subject: [PATCH] Add depends_on field on Task

Allow to trigger a new core service update only if there is none
depending on the triggered inventory update.

JIRA INFRA-895
---
 app/models.py                                 |  7 +++++
 app/static/js/tasks.js                        | 21 ++++++++++----
 app/templates/_helpers.html                   | 10 +++++++
 app/templates/task/tasks.html                 |  1 +
 app/templates/task/view_task.html             |  5 ++++
 app/utils.py                                  | 15 ++++++----
 ...777d44627f_add_depends_on_field_on_task.py | 28 +++++++++++++++++++
 7 files changed, 76 insertions(+), 11 deletions(-)
 create mode 100644 migrations/versions/cb777d44627f_add_depends_on_field_on_task.py

diff --git a/app/models.py b/app/models.py
index 965bb63..4fd0936 100644
--- a/app/models.py
+++ b/app/models.py
@@ -327,6 +327,7 @@ class User(db.Model, UserMixin):
             command=job.get_call_string(),
             status=JobStatus(job.status),
             user=self,
+            depends_on_id=kwargs.get("depends_on", None),
         )
         db.session.add(task)
         return task
@@ -1641,6 +1642,11 @@ class Task(db.Model):
         nullable=False,
         default=utils.fetch_current_user_id,
     )
+    depends_on_id = db.Column(postgresql.UUID, db.ForeignKey("task.id"))
+
+    reverse_dependencies = db.relationship(
+        "Task", backref=db.backref("depends_on", remote_side=[id])
+    )
 
     @property
     def awx_job_url(self):
@@ -1672,6 +1678,7 @@ class Task(db.Model):
             "awx_resource": self.awx_resource,
             "awx_job_id": self.awx_job_id,
             "awx_job_url": self.awx_job_url,
+            "depends_on": self.depends_on_id,
             "command": self.command,
             "exception": self.exception,
             "user": str(self.user),
diff --git a/app/static/js/tasks.js b/app/static/js/tasks.js
index 5d1730c..80a75d0 100644
--- a/app/static/js/tasks.js
+++ b/app/static/js/tasks.js
@@ -1,5 +1,14 @@
 $(document).ready(function() {
 
+  function render_task_link(data) {
+    // render funtion to create link to Task view page
+    if ( data === null ) {
+      return data;
+    }
+    var url = $SCRIPT_ROOT + "/task/tasks/view/" + data;
+    return '<a href="' + url + '">' + data + '</a>';
+  }
+
   $("#allTasks").on('change', function() {
     // reload the data from the Ajax source
     tasks_table.ajax.reload();
@@ -21,12 +30,7 @@ $(document).ready(function() {
     "columns": [
       { data: 'id',
         render: function(data, type, row) {
-          // render funtion to create link to Task view page
-          if ( data === null ) {
-            return data;
-          }
-          var url = $SCRIPT_ROOT + "/task/tasks/view/" + data;
-          return '<a href="' + url + '">' + data + '</a>';
+          return render_task_link(data);
         }
       },
       { data: 'name' },
@@ -42,6 +46,11 @@ $(document).ready(function() {
           return '<a href="' + row.awx_job_url + '">' + row.awx_job_id + '</a>';
         }
       },
+      { data: 'depends_on',
+        render: function(data, type, row) {
+          return render_task_link(data);
+        }
+      },
       { data: 'command' },
       { data: 'user' },
     ]
diff --git a/app/templates/_helpers.html b/app/templates/_helpers.html
index 353685d..1bf33fc 100644
--- a/app/templates/_helpers.html
+++ b/app/templates/_helpers.html
@@ -97,6 +97,16 @@
   {% endfor %}
 {%- endmacro %}
 
+{% macro link_to_task(task) -%}
+  <a href="{{ url_for('task.view_task', id_=task.id) }}">{{ task.id }}</a>
+{%- endmacro %}
+
+{% macro link_to_tasks(tasks) -%}
+  {% for task in tasks %}
+    {{ link_to_task(task) }}
+  {% endfor %}
+{%- endmacro %}
+
 {% macro render_field(field) -%}
   {% set field_class = kwargs.pop('class_', '') + ' form-control' %}
   {% set label_size = kwargs.pop('label_size', '2') %}
diff --git a/app/templates/task/tasks.html b/app/templates/task/tasks.html
index 2c0c6f8..64ea3fd 100644
--- a/app/templates/task/tasks.html
+++ b/app/templates/task/tasks.html
@@ -35,6 +35,7 @@
         <th>Ended at</th>
         <th>Status</th>
         <th>AWX job</th>
+        <th>Depends on</th>
         <th>Command</th>
         <th>User</th>
       </tr>
diff --git a/app/templates/task/view_task.html b/app/templates/task/view_task.html
index bfd1a8f..15ae5fb 100644
--- a/app/templates/task/view_task.html
+++ b/app/templates/task/view_task.html
@@ -1,4 +1,5 @@
 {% extends "task/tasks.html" %}
+{% from "_helpers.html" import link_to_task, link_to_tasks %}
 
 {% block title %}View Task - CSEntry{% endblock %}
 
@@ -30,6 +31,10 @@
         {% else %}
         <dd class="col-sm-9">{{ task.awx_job_id }}</dd>
         {% endif %}
+        <dt class="col-sm-3">Depends on</dt>
+        <dd class="col-sm-9">{{ link_to_task(task.depends_on) }}</dd>
+        <dt class="col-sm-3">Reverse dependencies</dt>
+        <dd class="col-sm-9">{{ link_to_tasks(task.reverse_dependencies) }}</dd>
         {% if task.exception %}
         <dt class="col-sm-3">Exception</dt>
         <dd class="col-sm-9"><pre>{{ task.exception }}</pre></dd>
diff --git a/app/utils.py b/app/utils.py
index a4c2f95..6a20b77 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -240,11 +240,16 @@ def trigger_core_services_update():
     """
     # Start by triggering an inventory update
     inventory_task = trigger_inventory_update()
-    # If there is already a core service update in queue, we could probably skip starting a new
-    # one but we would have to be sure that the inventory sync it depends on didn't already
-    # start
-    # Without this optimization, we might trigger a job that won't change anything.
-    # If jobs can run in parallel this shouldn't be an issue.
+    # If there is already a core service update depending on this inventory update,
+    # there is no need to trigger a new one
+    for reverse_dependency in inventory_task.reverse_dependencies:
+        if reverse_dependency.name == "trigger_core_services_update":
+            current_app.logger.info(
+                f'Already one "trigger_core_services_update" {reverse_dependency} '
+                f"depending on the inventory update {inventory_task}. "
+                "No need to trigger a new one."
+            )
+            return None
     job_template = current_app.config["AWX_CORE_SERVICES_UPDATE"]
     resource = current_app.config.get("AWX_CORE_SERVICES_UPDATE_RESOURCE", "job")
     kwargs = {
diff --git a/migrations/versions/cb777d44627f_add_depends_on_field_on_task.py b/migrations/versions/cb777d44627f_add_depends_on_field_on_task.py
new file mode 100644
index 0000000..e234cbc
--- /dev/null
+++ b/migrations/versions/cb777d44627f_add_depends_on_field_on_task.py
@@ -0,0 +1,28 @@
+"""Add depends_on field on Task
+
+Revision ID: cb777d44627f
+Revises: 166572b78449
+Create Date: 2019-03-27 20:51:05.385857
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "cb777d44627f"
+down_revision = "166572b78449"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column("task", sa.Column("depends_on_id", postgresql.UUID(), nullable=True))
+    op.create_foreign_key(
+        op.f("fk_task_depends_on_id_task"), "task", "task", ["depends_on_id"], ["id"]
+    )
+
+
+def downgrade():
+    op.drop_constraint(op.f("fk_task_depends_on_id_task"), "task", type_="foreignkey")
+    op.drop_column("task", "depends_on_id")
-- 
GitLab