From b62a7d216dbb9daf6ece54f77d7f56131248a906 Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.dos.anjos@gmail.com>
Date: Thu, 28 Apr 2016 14:53:59 +0200
Subject: [PATCH] [backend] Implement poor-man's scheduler & worker

---
 .../backend/templates/backend/scheduler.html  | 146 +++++++++++++++++-
 beat/web/backend/templatetags/backend_tags.py |   9 +-
 beat/web/backend/views.py                     |  41 ++++-
 3 files changed, 187 insertions(+), 9 deletions(-)

diff --git a/beat/web/backend/templates/backend/scheduler.html b/beat/web/backend/templates/backend/scheduler.html
index 14ff20fcf..00a71c4ee 100644
--- a/beat/web/backend/templates/backend/scheduler.html
+++ b/beat/web/backend/templates/backend/scheduler.html
@@ -37,15 +37,58 @@
 <div class="row">
   <div class="col-sm-12">
     <div class="alert alert-success" role="alert" style="text-align: center;">
-      <i class="fa fa-clock-o fa-2x vertical-center"></i> Updated: {% now "H:i O, jS F Y" %}
+      <i class="fa fa-clock-o fa-2x vertical-center"></i> Updated: {% now "H:i:s O, jS F Y" %}
       <div class="pull-right action-buttons">
-        <a id="update-workers-button" class="btn btn-default btn-info" data-toggle="tooltip" data-placement="bottom" title="Force all workers to update their state" href="{% url 'backend:update-workers' %}"><i class="fa fa-times"></i> Update Workers</a>
+        <a id="update-workers-button" class="btn btn-default btn-info" data-toggle="tooltip" data-placement="bottom" title="Request all workers to update their state when possible" href="{% url 'backend:update-workers' %}"><i class="fa fa-gears"></i> Update Workers</a>
         <a id="cancel-experiments-button" class="btn btn-default btn-delete" data-toggle="tooltip" data-placement="bottom" title="Cancel all running experiments" href="{% url 'backend:cancel-experiments' %}"><i class="fa fa-times"></i> Cancel Experiments</a>
       </div>
     </div>
   </div>
 </div>
 
+{% if helper_panel %}
+<div class="row">
+  <div class="col-sm-12">
+    <div class="panel panel-warning">
+      <div class="panel-heading">
+        <h3 class="panel-title">Helper Panel</h3>
+      </div>
+      <div class="panel-body">
+        <p class="help">Use this panel to <strong>locally</strong> launch scheduling activity. This functionality is intended as a <em>test</em> scheduler and worker replacement that can be used to run local experiments or debug. <strong>Don't use this in a production system.</strong> Every time you launch an activity, the page will reload to trigger this action. Scheduling happens in the context of the Django server running on the background. Worker processes are managed using subprocesses and don't block the web server.</p>
+
+        <div class="form-inline">
+          <div id="activity-group" class="form-group">
+            <label class="sr-only" for="activity">Activity</label>
+            <select id="activity" class="form-control">
+              <option value="both">Schedule &amp; Work</option>
+              <option value="schedule">Schedule</option>
+              <option value="work">Work</option>
+            </select>
+          </div>
+          <div id="periodically-group" class="form-group">
+            <div class="checkbox">
+              <label>
+                <input id="periodically" type="checkbox" checked="checked"> periodically
+              </label>
+            </div>
+          </div>
+          <div id="period-group" class="form-group">
+            <label class="sr-only" for="period">Period</label>
+            <div class="input-group">
+              <div class="input-group-addon">every</div>
+              <input type="text" class="form-control" id="period" value="{{ scheduling_period }}">
+              <div class="input-group-addon">s</div>
+            </div>
+          </div>
+          <button id="start" type="submit" class="btn btn-success"><i class="fa fa-play"></i> <span id="start">Start</span></button>
+          <button id="stop" type="submit" class="btn btn-danger disabled"><i class="fa fa-stop"></i> <span id="stop">Stop</span></button>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endif %}
+
 <div class="row">
   <div class="col-sm-12">
 
@@ -88,7 +131,6 @@
           <canvas id="cache-chart" style="width: 80%; height: auto;"></canvas>
           <div id="cache-legend" class="chart-legend"></div>
         </div>
-
       </div>
 
       <!-- Experiments tab -->
@@ -101,6 +143,7 @@
                 <th>Name</th>
                 <th>Blocks/Jobs</th>
                 <th>Job Splits</th>
+                <th>Assigned</th>
                 <th>Running</th>
                 <th>Completed</th>
                 <th>Failed</th>
@@ -114,6 +157,7 @@
                 <td><a href="{{ obj.get_admin_change_url }}">{{ obj.fullname }}</a></td>
                 <td>{{ obj.blocks.count }}</td>
                 <td>{{ obj|count_job_splits }}</td>
+                <td>{{ obj|count_job_splits:"A" }}</td>
                 <td>{{ obj|count_job_splits:"P" }}</td>
                 <td>{{ obj|count_job_splits:"C" }}</td>
                 <td>{{ obj|count_job_splits:"F" }}</td>
@@ -212,6 +256,10 @@
 <script type="text/javascript">
 $(document).ready(function() {
 
+  /**
+   * This bit of code here is to manage the normal parts of the scheduler
+   * page, like the tags and the cache chart which is displayed.
+   */
   manage_tabs('ul#object-tabs');
 
   var data = {{ cache_chart_data|safe }};
@@ -235,6 +283,98 @@ $(document).ready(function() {
     display_cache_chart();
   });
 
+  {% if helper_panel %}
+  /**
+   * This bit of code here is to manage helper panel, only included if that
+   * shows up.
+   */
+
+  function display_periodic(period) {
+    $("input#periodically").prop("checked", true);
+    $("#period-group").show();
+    $("button#start > span#start").text("Start");
+    $("button#stop > span#stop").text("Stop");
+    $("button#stop").show();
+    $("input#period").val(period);
+    $("button#stop").disable();
+  }
+
+  function display_single_shot() {
+    $("input#periodically").prop("checked", false);
+    $("#period-group").hide();
+    $("button#start > span#start").text("Go");
+    $("button#stop > span#stop").text("Reset");
+    $("button#start").enable();
+    $("button#stop").enable();
+  }
+
+  /* controls button display */
+  $("input#periodically").click(function() {
+    if($(this).is(":checked")) {
+      display_periodic({{ scheduling_period }});
+    } else {
+      display_single_shot();
+    }
+  });
+
+  /* get url parameters */
+  function get_parameters() {
+    var vars = {};
+    var parts = location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,
+        function(m,key,value) {
+          vars[key] = value;
+        });
+    return vars;
+  }
+
+  /* controls the initial appearance */
+  var params = get_parameters();
+  if (location.search !== "") {
+    if ("period" in params) {
+      var period = parseInt(params.period, 10);
+      display_periodic(period);
+      $("button#start").disable();
+      $("button#stop").enable();
+      $("select#activity").disable();
+      $("input#period").disable();
+      $("input#periodically").disable();
+      setTimeout("location.reload(true);", period * 1000);
+    }
+    else {
+      display_single_shot();
+    }
+    if ("activity" in params) {
+      $("select#activity").val(params.activity);
+    }
+  } else {
+    display_periodic({{ scheduling_period }});
+    $("button#start").enable();
+    $("button#stop").disable();
+    $("select#activity").enable();
+    $("input#period").enable();
+    $("input#periodically").enable();
+  }
+
+  /* controls form submission buttons */
+  $("button#start").click(function() {
+    var params = '?activity=' + $("select#activity").val();
+    if ($("input#periodically").is(":checked")) {
+      params += '&period=' + $("input#period").val();
+    }
+    if (location.search === params) {
+      location.reload(true);
+    }
+    else {
+      location.search = params;
+    }
+  });
+
+  $("button#stop").click(function() {
+    location.search = "";
+  });
+
+  {% endif %}
+
 });
 </script>
 {% endblock %}
diff --git a/beat/web/backend/templatetags/backend_tags.py b/beat/web/backend/templatetags/backend_tags.py
index dfdc78291..b3be2c7db 100644
--- a/beat/web/backend/templatetags/backend_tags.py
+++ b/beat/web/backend/templatetags/backend_tags.py
@@ -29,6 +29,8 @@
 from django import template
 from django.contrib.auth.models import User
 
+from ..models import Job
+
 
 register = template.Library()
 
@@ -86,9 +88,6 @@ def visible_queues(context, object):
 @register.filter
 def count_job_splits(xp, status=None):
     """Returns job splits for an experiment in a certain state"""
+    if status == 'A':
+        return xp.job_splits(status=Job.QUEUED).filter(worker__isnull=False).count()
     return xp.job_splits(status=status).count()
-
-
-#--------------------------------------------------
-
-
diff --git a/beat/web/backend/views.py b/beat/web/backend/views.py
index 8c51ca58d..c10629608 100644
--- a/beat/web/backend/views.py
+++ b/beat/web/backend/views.py
@@ -25,7 +25,7 @@
 #                                                                             #
 ###############################################################################
 
-import collections
+import os
 import simplejson
 
 from django.http import Http404, HttpResponseRedirect
@@ -38,9 +38,37 @@ from django.contrib.auth.decorators import login_required
 from django.http import HttpResponseForbidden
 from django.contrib import messages
 
+from beat.core.async import resolve_cpulimit_path
+
 from .models import Environment, Worker, Queue
 from ..experiments.models import Experiment
 from . import state
+from .schedule import schedule, work, worker_update
+from .schedule import find_environments, resolve_process_path
+
+
+#------------------------------------------------
+
+
+class Work:
+    '''Helper to do the required worker job for local scheduling'''
+
+    cpulimit = resolve_cpulimit_path(None)
+    process = resolve_process_path()
+    environments = find_environments(None)
+    django_settings = os.environ.get('DJANGO_SETTINGS_MODULE',
+        'beat.web.settings.zzz')
+
+    def __call__(self):
+        # update workers that require updates
+        worker_update()
+
+        work(
+            Work.environments,
+            Work.cpulimit,
+            Work.process,
+            Work.django_settings,
+            )
 
 
 #------------------------------------------------
@@ -70,6 +98,16 @@ def scheduler(request):
 
     cache_gb = int(cache['capacity-in-megabytes'] / 1024.0)
 
+    # do scheduling and/or worker activity if required
+    if request.GET.has_key('activity'):
+        activity = request.GET['activity']
+
+        if activity in ('both', 'schedule'):
+            schedule()
+
+        if activity in ('both', 'work'):
+            Work()()
+
     return render_to_response('backend/scheduler.html',
             dict(
                 jobs=state.jobs(),
@@ -79,6 +117,7 @@ def scheduler(request):
                 cache_chart_data=simplejson.dumps(cache_chart_data),
                 cache_gb=cache_gb,
                 helper_panel=getattr(settings, 'SCHEDULING_PANEL', False),
+                scheduling_period=getattr(settings, 'SCHEDULING_INTERVAL', 5),
                 ),
             context_instance=RequestContext(request))
 
-- 
GitLab