[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[OSSTEST PATCH 66/82] host lifecycle: Machinery, db, for tracking relevant events



From: Ian Jackson <ian.jackson@xxxxxxxxxxxxx>

When we reuse test hosts, we want to be able to give a list of the
other jobs which might be responsible for any problem.

In principle it would be possible to do this by digging into the
db's history tables like sg-report-host-history does, but this is
quite slow and also I don't have enough confidence in that approach to
use it for this application.

So instead, track the host lifecycle explicitly.

The approach taken is a hybrid one.  I first considered two and a half
approaches:

 1. Permanently record all host/share allocations and share state
    changes in a host history table.  But it is nontrivial to update
    all the allocation machinery to keep this table up to date.  It is
    also nontrivial to extract the necessary information from such a
    table: the allocation information would have to be correlated,
    using timestamps, with the steps table.  That's slow and complex.
    We had such a table but it was never used for these reasons;
    I dropped that empty table recently.

 1b. Like 1 but explicitly put a lifecycle sequence number in the
    allocations table,.  This would make it easy to find relevant
    events but would involve even more complicated logic during
    allocation.

 2. Record the host's lifecycle information in a file on the host.
    This means it gets wiped whenever the host does and makes finding
    the relevant jobs easy: read the file during logs capture, and
    we'll find everything of relevance.  It then has to be permanently
    stored somewhere it can be used for logging and archaeology: a
    per-job runvar giving the relevant host history, up to the point
    where that job finished. does that job nicely.  However, this
    has a serious problem: if the host crashes hard, we may not be
    able to recover the complete information about why!  We really
    want to the information recorded outside the host in question.

So I've taken a hybrid approach: effectively replicate the per-host
file from (2), but put the information in the database.  This
necessites a call to clear the host lifecycle history, which we make
at the *end* of the host install.  As a bonus this might let us more
easily identify if there are particular jobs that leave hosts in
states that are hard to recover from, and it will make total host
failure quite obvious because the host install log report will have a
list of the failed attempts (longer in each successive job).

For build jobs we only record the setup job, and concurrent jobs, in
the runvar.  This does not seem to have been a problem so far, and
this avoids having to do work on other allocations (eg, mg-allocate).
It also avoids having very long lists of previous builds listed in
every build job.

Test jobs are only shared within a flight and with much more limited
scope so the same considerations don't arise.  But by the same token,
we also do not need to adjust mg-allocate etc., since the user ought
not to allocate shares of test hosts unless they know what they are
doing.

In this commit we introduce:
 * The database table
 * The runvar syntax
 * The function for recording the lifecycle events

We have what amounts to an ad-hoc compression scheme for the
information in the lifecycle runvars.  Otherwise this data might get
quite voluminous, which can makes various other db queries slow.

There isn't a very good way to represent out-of-job tasks in the
lifecycle runvar.  We could maybe put in something from the tasks
table, but the entry in the tasks table might be gone by now and that
would involve quoting (and it might be quite large).

But this will only matter when a shared/reused host has been manually
messed with, and recording the task is sufficient to
 (1) note the fact of such interference
 (2) if the task is static, or still going when the job reports,
      can actually be put in the report.
 (3) failing that provide something which could be grepped for in logs

We do not call the recording function yet, so the db update is merely
Preparatory.

There is a bug in this patch: the calculation of $olive is wrong.
This will be fixed in a moment.

Signed-off-by: Ian Jackson <ian.jackson@xxxxxxxxxxxxx>
---
 Osstest/JobDB/Executive.pm  | 211 ++++++++++++++++++++++++++++++++++++
 Osstest/JobDB/Standalone.pm |   2 +
 Osstest/TestSupport.pm      |  12 ++
 schema/host-lifecycle.sql   |  43 ++++++++
 4 files changed, 268 insertions(+)
 create mode 100644 schema/host-lifecycle.sql

diff --git a/Osstest/JobDB/Executive.pm b/Osstest/JobDB/Executive.pm
index 8fde2934..cf82b4cf 100644
--- a/Osstest/JobDB/Executive.pm
+++ b/Osstest/JobDB/Executive.pm
@@ -24,6 +24,7 @@ use Osstest;
 use Osstest::TestSupport;
 use Osstest::Executive;
 use Data::Dumper;
+use Carp;
 
 BEGIN {
     use Exporter ();
@@ -461,4 +462,214 @@ sub jobdb_db_glob ($$) { #method
 
 sub can_anoint ($) { return 1; }
 
+sub jobdb_host_update_lifecycle_info ($$$) { #method
+    my ($mo, $ho, $mode) = @_;
+    # $mode is       sigil
+    #  selectprep  1  @     called several times, from selecthost, $isprep
+    #  wiped       2  n/a   called (usually) once, when install succeeds
+    #  select      1  +     called several times, from selecthost, !$isprep
+    #  final       1  none  called (hopefully) once, from capture-logs
+    #                        (once means within this job)
+    # Notes:
+    #   1   causes a new row to be added to the host lifecycle
+    #       subject to sigil check
+    #   2   removes other rows from the host lifecycle
+    #
+    # Where "sigil" above is nonempty: we look at the runvar.  If the
+    # runvar's final sigil is the same, we conclude that the needed
+    # work has already been done: ie, we do not need to add a row to
+    # the table nor do we necessarily need to update the runvar.
+    #
+    # If the sigil is not right, we replace the runvar with a history
+    # string derived from the lifecycle table, and (if appropriate)
+    # add a row.
+    #
+    # In principle it might be useful to update the runvar every time
+    # because the lifecycle table might have gained rows from other
+    # jobs, but we would like to avoid burdening select with more
+    # pratting about.  So we leave that for capture-logs and
+    # reuse/final, ie when newsigil is none.
+    #
+    # The runvar is
+    #     <ident>_lifecycle
+    # and contains space-separated entries
+    #     [+@][<flight>.][<job>]:<stepno>[,<ident>]
+    #     [+@]<stepno>              same flight, job, ident; new stepno
+    #     "["<omitted-count>"]"     ie literal [ ]
+    #     [+]?<taskid>              task not within a flight/job
+    # where omitted [,<ident>] means "host";
+    # omitted <flight> and <job> mean this runvar's flight and/or job;
+    # and then at then end most one of the sigils
+    #     @                      last call was selectprep
+    #     +                      last call was select
+    #     <none>                 last call was final
+    # items with no such sigil don't appear in build jobs
+    # and instead appear as [<omitted-count>] eg "[4]"
+
+    return if $ho->{Host}; # This host *has* a host - ie, nested
+
+    my $ttaskid = findtask();
+    my $hostname = $ho->{Name};
+    my $tident = $ho->{Ident};
+    my $tstepno = $mo->current_stepno();
+
+    if ($mode eq 'wiped') {
+       db_retry($flight, [qw(running)], $dbh_tests,[], sub {
+            $dbh_tests->do(<<END, {}, $hostname);
+                DELETE FROM host_lifecycle h
+                      WHERE hostname=?
+                        AND NOT EXISTS(
+                SELECT 1
+                 FROM tasks t
+                WHERE t.live
+                  AND t.taskid = h.taskid
+                );
+END
+        });
+       logm("host lifecycle: $hostname: wiped, cleared out old info");
+       return;
+    }
+
+    my $newsigil =
+      $mode eq 'selectprep' ? '@' :
+      $mode eq 'select'     ? '+' :
+      $mode eq 'final'      ? ''  :
+      confess "$mode ?";
+
+    my $scanq = $dbh_tests->prepare(<<END);
+          SELECT h.flight, h.job, h.isprep, h.ident, h.stepno,
+                  t.live, t.taskid,
+                  h2.lcseq later_notprep
+            FROM host_lifecycle h
+        LEFT JOIN host_lifecycle h2
+               ON h2.hostname = h.hostname 
+              AND h2.flight   = h.flight
+              AND h2.job      = h.job
+              AND h2.ident    = h.ident
+              AND h2.taskid   = h.taskid
+              AND h2.lcseq    > h.lcseq
+              AND h.isprep AND NOT h2.isprep
+        LEFT JOIN tasks t
+               ON h.taskid = t.taskid
+            WHERE h.hostname = ?
+         ORDER BY h.lcseq;
+END
+    my $insertq = $dbh_tests->prepare(<<END);
+        INSERT INTO host_lifecycle
+                 (hostname, taskid, flight, job, isprep, ident, stepno)
+          VALUES (?,        ?,      ?,      ?,   ?,      ?,     ?     )
+END
+
+    my $ojvn = "$ho->{Ident}_lifecycle";
+    my $firstrun;
+
+    if (length $r{$ojvn}) {
+       my ($oldsigil,) = reverse split / /, $r{$ojvn};
+       $oldsigil = '' unless $oldsigil =~ m/^\W$/;
+       return if $newsigil ne '' && $oldsigil eq $newsigil;
+    } else {
+       $firstrun = 1;
+    }
+
+    my @lifecycle;
+    db_retry($dbh_tests,[], sub {
+       my $elided;
+       @lifecycle = ();
+        my %tj_seen;
+       # keys in %tj_seen are [@][<flight>.][<job>] or ?<taskid>
+        $scanq->execute($hostname);
+
+       while (my $o = $scanq->fetchrow_hashref()) {
+           my $olive =
+             # Any job which appeared since we started thinking
+             # about this must have been concurrent with us,
+             # even if it is dead now.
+             (!$firstrun || $o->{live}) &&
+             # If this task is still live, we need to have something
+             # with a live mark, generally all the prep will have
+             # occurred already, so we don't mark the prep as live
+             # if there's a later nonprep step.
+             !$o->{later_notprep};
+
+           my $olivemark = !!$olive && '+';
+           if (defined($flight) && defined($o->{flight}) &&
+               $o->{flight} eq $flight &&
+               $o->{job} eq $job) {
+               # Don't put the + mark on our own entries.
+               $olivemark = '';
+           }
+
+           my $oisprepmark = !!$o->{isprep} && '@';
+           my $omarks = $olivemark.$oisprepmark;
+
+           my $otj = '';
+           if (!defined $o->{flight}) {
+               $otj .= "?$o->{taskid}";
+           } else {
+               $otj .= "$o->{flight}." if $o->{flight} ne $flight;
+               $otj .= $o->{job} if $o->{job} ne $job;
+           }
+           next if $tj_seen{$oisprepmark.$otj}++;
+
+           if (!$omarks && !$olive && defined($o->{flight}) &&
+               $ho->{Shared} &&
+               $ho->{Shared}{Type} =~ m/^build-/ &&
+               !$tj_seen{"\@$otj"} # do not elide use if we showed prep
+              ) {
+               # elide previous, non-concurrent, build jobs
+               if (!$elided) { $elided = [ scalar(@lifecycle), 0]; }
+               $lifecycle[$elided->[0]] = "[".(++$elided->[1])."]";
+               next;
+           }
+
+           my $osuffix = !!(defined($o->{ident}) && $o->{ident} ne 'host')
+             && ",$o->{ident}";
+
+           my ($lastuncompr,) = grep { !m{^\W*\d+$} } reverse @lifecycle;
+           if (defined($lastuncompr) &&
+               $lastuncompr =~ m{^\W*\Q$otj\E:\d+\Q$osuffix\E$}) {
+               push @lifecycle, "$omarks$o->{stepno}";
+           } else {
+               push @lifecycle, "$omarks$otj:$o->{stepno}$osuffix";
+           }
+       }
+       if (defined $flight) {
+           $insertq->execute($hostname, $ttaskid,
+                             $flight, $job,
+                             ($mode eq 'selectprep')+0,
+                # ^ DBD::Pg doesn't accept perl canonical false for bool!
+                #   https://rt.cpan.org/Public/Bug/Display.html?id=133229
+                             $tident, $tstepno);
+       } else {
+           $insertq->execute($hostname, $ttaskid,
+                             undef,undef,
+                             undef,
+                             undef,undef);
+       }
+    });
+
+    if (defined $flight) {
+       push @lifecycle, $newsigil if length $newsigil;
+       store_runvar($ojvn, "@lifecycle");
+    }
+}
+
+sub current_stepno ($) { #method
+    my ($jd) = @_;
+    my $testid = $ENV{OSSTEST_TESTID} // return undef;
+    my $checkq = $dbh_tests->prepare(<<END);
+        SELECT stepno
+          FROM steps
+         WHERE flight=?
+           AND job=?
+           AND testid=?
+END
+    my $stepno;
+    db_retry($flight,[qw(running)], $dbh_tests,[],sub {
+       $checkq->execute($flight,$job,$testid);
+       ($stepno) = $checkq->fetchrow_array();
+    });
+    return $stepno;
+}
+
 1;
diff --git a/Osstest/JobDB/Standalone.pm b/Osstest/JobDB/Standalone.pm
index 1db4dc78..6e1ae158 100644
--- a/Osstest/JobDB/Standalone.pm
+++ b/Osstest/JobDB/Standalone.pm
@@ -137,4 +137,6 @@ sub jobdb_db_glob ($) { #method
 
 sub can_anoint ($) { return 0; }
 
+sub jobdb_host_update_lifecycle_info { } #method
+
 1;
diff --git a/Osstest/TestSupport.pm b/Osstest/TestSupport.pm
index 28381f05..22141981 100644
--- a/Osstest/TestSupport.pm
+++ b/Osstest/TestSupport.pm
@@ -88,6 +88,7 @@ BEGIN {
                       serial_fetch_logs set_host_property modify_host_flag
                       propname_massage propname_check
                       hostprop_putative_record hostflag_putative_record
+                      host_update_lifecycle_info
          
                       get_stashed open_unique_stashfile compress_stashed
                       dir_identify_vcs
@@ -174,6 +175,7 @@ our @accessible_runvar_pats =
       host_console                   *_host_console
       host_hostflagadjust            *_host_hostflagadjust
       host_hostflags                 *_host_hostflags
+      host_lifecycle                 *_host_lifecycle
       host_linux_boot_append         *_host_linux_boot_append
       host_ip                        *_host_ip
       host_power_install             *_host_power_install
@@ -3166,6 +3168,16 @@ sub sha256file ($;$) {
     return $truncate ? substr($digest, 0, $truncate) : $digest;
 }
 
+sub host_update_lifecycle_info ($$) {
+    my ($ho, $mode) = @_;
+    # $mode is
+    #  selectprep       called several times, from selecthost, $isprep
+    #  wiped            called once, when install succeeds
+    #  select           called several times, from selecthost, !$isprep
+    #  final            called hopefully once, from capture-logs
+    $mjobdb->jobdb_host_update_lifecycle_info($ho, $mode)
+}
+
 sub host_shared_mark_ready($$;$$) {
     my ($ho,$sharetype, $oldstate, $newstate) = @_;
 
diff --git a/schema/host-lifecycle.sql b/schema/host-lifecycle.sql
new file mode 100644
index 00000000..7f1f5bb0
--- /dev/null
+++ b/schema/host-lifecycle.sql
@@ -0,0 +1,43 @@
+-- ##OSSTEST## 012 Preparatory
+--
+-- Records the jobs which have touched a host, for host sharing/reuse
+-- The information here is ephemeral - it is cleared when a host is
+-- reinitialised.  The information is persisted by being copied
+-- into a runvar for each job.
+
+CREATE SEQUENCE host_lifecycle_lcseq_seq
+    NO CYCLE;
+
+CREATE TABLE host_lifecycle (
+    hostname     TEXT NOT NULL,
+    lcseq        INTEGER NOT NULL DEFAULT nextval('host_lifecycle_lcseq_seq'),
+    taskid       INTEGER NOT NULL, -- no constraint, tasks can get deleted
+    flight       INTEGER,
+    job          TEXT,
+    stepno       INTEGER,
+    ident        TEXT,
+    isprep       BOOLEAN,
+    PRIMARY KEY  (hostname, lcseq),
+--  restype      TEXT GENERATED ALWAYS AS ('host'),
+--  FOREIGN KEY  (restype,hostname)    REFERENCES resources(restype, resname),
+--     ^ those two omitted because not supported until pgsql 13
+--  FOREIGN KEY (flight, job)         REFERENCES jobs(flight, job),
+--      ^ omitted because the next constraint implies it
+    FOREIGN KEY  (flight, job, stepno) REFERENCES steps(flight, job, stepno),
+    CHECK ((
+       flight       IS NOT NULL AND
+       job          IS NOT NULL AND
+       stepno       IS NOT NULL AND
+       ident        IS NOT NULL AND
+       isprep       IS NOT NULL
+    ) OR (
+       flight       IS     NULL AND
+       job          IS     NULL AND
+       stepno       IS     NULL AND
+       ident        IS     NULL AND
+       isprep       IS     NULL
+    ))
+);
+
+ALTER SEQUENCE host_lifecycle_lcseq_seq
+    OWNED BY host_lifecycle.lcseq;
-- 
2.20.1




 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.