[autotest] Add rpc "host_queue_entries_by_insert_time"

This new RPC is optimized to use the new index table for hqe insert times. It
puts constraints on the id based on the start_time and end_time arguments, which
will reduce DB load by preventing these queries from scanning the entire table.

BUG=chromium:704997
TEST=The new test passes.

Change-Id: Idc9d3554830a04f3bf55af670082a280fd5ee926
Reviewed-on: https://chromium-review.googlesource.com/516605
Commit-Ready: Paul Hobbs <phobbs@google.com>
Tested-by: Paul Hobbs <phobbs@google.com>
Reviewed-by: Paul Hobbs <phobbs@google.com>
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index aa575b6..39c3c98 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -1185,6 +1185,48 @@
     return image
 
 
+def get_host_queue_entries_by_insert_time(
+    insert_time_after=None, insert_time_before=None, **filter_data):
+    """Like get_host_queue_entries, but using the insert index table.
+
+    @param insert_time_after: A lower bound on insert_time
+    @param insert_time_before: An upper bound on insert_time
+    @returns A sequence of nested dictionaries of host and job information.
+    """
+    assert insert_time_after is not None or insert_time_before is not None, \
+      ('Caller to get_host_queue_entries_by_insert_time must provide either'
+       ' insert_time_after or insert_time_before.')
+    # Get insert bounds on the index of the host queue entries.
+    if insert_time_after:
+        query = models.HostQueueEntryStartTimes.objects.filter(
+            # Note: '-insert_time' means descending. We want the largest
+            # insert time smaller than the insert time.
+            insert_time__lte=insert_time_after).order_by('-insert_time')
+        try:
+            constraint = query[0].highest_hqe_id
+            if 'id__gte' in filter_data:
+                constraint = max(constraint, filter_data['id__gte'])
+            filter_data['id__gte'] = constraint
+        except IndexError:
+            pass
+
+    # Get end bounds.
+    if insert_time_before:
+        query = models.HostQueueEntryStartTimes.objects.filter(
+            insert_time__gte=insert_time_before).order_by('insert_time')
+        try:
+            constraint = query[0].highest_hqe_id
+            if 'id__lte' in filter_data:
+                constraint = min(constraint, filter_data['id__lte'])
+            filter_data['id__lte'] = constraint
+        except IndexError:
+            pass
+
+    return rpc_utils.prepare_rows_as_nested_dicts(
+            models.HostQueueEntry.query_objects(filter_data),
+            ('host', 'job'))
+
+
 def get_host_queue_entries(start_time=None, end_time=None, **filter_data):
     """\
     @returns A sequence of nested dictionaries of host and job information.
diff --git a/frontend/afe/rpc_interface_unittest.py b/frontend/afe/rpc_interface_unittest.py
index b2b8881..8ff9cbd 100755
--- a/frontend/afe/rpc_interface_unittest.py
+++ b/frontend/afe/rpc_interface_unittest.py
@@ -323,6 +323,89 @@
         self.assertEquals(entry2['started_on'], '2009-01-03 00:00:00')
 
 
+    def _create_hqes_and_start_time_index_entries(self):
+        shard = models.Shard.objects.create(hostname='shard')
+        job = self._create_job(shard=shard, control_file='foo')
+        HqeStatus = models.HostQueueEntry.Status
+
+        models.HostQueueEntry(
+            id=1, job=job, started_on='2017-01-01',
+            status=HqeStatus.QUEUED).save()
+        models.HostQueueEntry(
+            id=2, job=job, started_on='2017-01-02',
+            status=HqeStatus.QUEUED).save()
+        models.HostQueueEntry(
+            id=3, job=job, started_on='2017-01-03',
+            status=HqeStatus.QUEUED).save()
+
+        models.HostQueueEntryStartTimes(
+            insert_time='2017-01-03', highest_hqe_id=3).save()
+        models.HostQueueEntryStartTimes(
+            insert_time='2017-01-02', highest_hqe_id=2).save()
+        models.HostQueueEntryStartTimes(
+            insert_time='2017-01-01', highest_hqe_id=1).save()
+
+    def test_get_host_queue_entries_by_insert_time(self):
+        """Check the insert_time_after and insert_time_before constraints."""
+        self._create_hqes_and_start_time_index_entries()
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_after='2017-01-01')
+        self.assertEquals(len(hqes), 3)
+
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_after='2017-01-02')
+        self.assertEquals(len(hqes), 2)
+
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_after='2017-01-03')
+        self.assertEquals(len(hqes), 1)
+
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-01')
+        self.assertEquals(len(hqes), 1)
+
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-02')
+        self.assertEquals(len(hqes), 2)
+
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-03')
+        self.assertEquals(len(hqes), 3)
+
+
+    def test_get_host_queue_entries_by_insert_time_with_missing_index_row(self):
+        """Shows that the constraints are approximate.
+
+        The query may return rows which are actually outside of the bounds
+        given, if the index table does not have an entry for the specific time.
+        """
+        self._create_hqes_and_start_time_index_entries()
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2016-12-01')
+        self.assertEquals(len(hqes), 1)
+
+    def test_get_hqe_by_insert_time_with_before_and_after(self):
+        self._create_hqes_and_start_time_index_entries()
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-02',
+            insert_time_after='2017-01-02')
+        self.assertEquals(len(hqes), 1)
+
+    def test_get_hqe_by_insert_time_and_id_constraint(self):
+        self._create_hqes_and_start_time_index_entries()
+        # The time constraint is looser than the id constraint, so the time
+        # constraint should take precedence.
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-02',
+            id__lte=1)
+        self.assertEquals(len(hqes), 1)
+
+        # Now make the time constraint tighter than the id constraint.
+        hqes = rpc_interface.get_host_queue_entries_by_insert_time(
+            insert_time_before='2017-01-01',
+            id__lte=42)
+        self.assertEquals(len(hqes), 1)
+
     def test_view_invalid_host(self):
         # RPCs used by View Host page should work for invalid hosts
         self._create_job_helper(hosts=[1])