Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | # |
| 3 | # Symbolic asynchronous tasks for devstack |
| 4 | # |
| 5 | # Usage: |
| 6 | # |
| 7 | # async_runfunc my_shell_func foo bar baz |
| 8 | # |
| 9 | # ... do other stuff ... |
| 10 | # |
| 11 | # async_wait my_shell_func |
| 12 | # |
| 13 | |
| 14 | DEVSTACK_PARALLEL=$(trueorfalse False DEVSTACK_PARALLEL) |
| 15 | _ASYNC_BG_TIME=0 |
| 16 | |
| 17 | # Keep track of how much total time was spent in background tasks |
| 18 | # Takes a job runtime in ms. |
| 19 | function _async_incr_bg_time { |
| 20 | local elapsed_ms="$1" |
| 21 | _ASYNC_BG_TIME=$(($_ASYNC_BG_TIME + $elapsed_ms)) |
| 22 | } |
| 23 | |
| 24 | # Get the PID of a named future to wait on |
| 25 | function async_pidof { |
| 26 | local name="$1" |
| 27 | local inifile="${DEST}/async/${name}.ini" |
| 28 | |
| 29 | if [ -f "$inifile" ]; then |
| 30 | iniget $inifile job pid |
| 31 | else |
| 32 | echo 'UNKNOWN' |
| 33 | return 1 |
| 34 | fi |
| 35 | } |
| 36 | |
| 37 | # Log a message about a job. If the message contains "%command" then the |
| 38 | # full command line of the job will be substituted in the output |
| 39 | function async_log { |
| 40 | local name="$1" |
| 41 | shift |
| 42 | local message="$*" |
| 43 | local inifile=${DEST}/async/${name}.ini |
| 44 | local pid |
| 45 | local command |
| 46 | |
| 47 | pid=$(iniget $inifile job pid) |
| 48 | command=$(iniget $inifile job command | tr '#' '-') |
| 49 | message=$(echo "$message" | sed "s#%command#$command#g") |
| 50 | |
| 51 | echo "[Async ${name}:${pid}]: $message" |
| 52 | } |
| 53 | |
| 54 | # Inner function that actually runs the requested task. We wrap it like this |
| 55 | # just so we can emit a finish message as soon as the work is done, to make |
| 56 | # it easier to find the tracking just before an error. |
| 57 | function async_inner { |
| 58 | local name="$1" |
| 59 | local rc |
| 60 | shift |
| 61 | set -o xtrace |
| 62 | if $* >${DEST}/async/${name}.log 2>&1; then |
| 63 | rc=0 |
| 64 | set +o xtrace |
| 65 | async_log "$name" "finished successfully" |
| 66 | else |
| 67 | rc=$? |
| 68 | set +o xtrace |
| 69 | async_log "$name" "FAILED with rc $rc" |
| 70 | fi |
| 71 | iniset ${DEST}/async/${name}.ini job end_time $(date "+%s%3N") |
| 72 | return $rc |
| 73 | } |
| 74 | |
| 75 | # Run something async. Takes a symbolic name and a list of arguments of |
| 76 | # what to run. Ideally this would be rarely used and async_runfunc() would |
| 77 | # be used everywhere for readability. |
| 78 | # |
| 79 | # This spawns the work in a background worker, records a "future" to be |
| 80 | # collected by a later call to async_wait() |
| 81 | function async_run { |
| 82 | local xtrace |
| 83 | xtrace=$(set +o | grep xtrace) |
| 84 | set +o xtrace |
| 85 | |
| 86 | local name="$1" |
| 87 | shift |
| 88 | local inifile=${DEST}/async/${name}.ini |
| 89 | |
| 90 | touch $inifile |
| 91 | iniset $inifile job command "$*" |
| 92 | iniset $inifile job start_time $(date +%s%3N) |
| 93 | |
| 94 | if [[ "$DEVSTACK_PARALLEL" = "True" ]]; then |
| 95 | async_inner $name $* & |
| 96 | iniset $inifile job pid $! |
| 97 | async_log "$name" "running: %command" |
| 98 | $xtrace |
| 99 | else |
| 100 | iniset $inifile job pid "self" |
| 101 | async_log "$name" "Running synchronously: %command" |
| 102 | $xtrace |
| 103 | $* |
| 104 | return $? |
| 105 | fi |
| 106 | } |
| 107 | |
| 108 | # Shortcut for running a shell function async. Uses the function name as the |
| 109 | # async name. |
| 110 | function async_runfunc { |
| 111 | async_run $1 $* |
| 112 | } |
| 113 | |
| 114 | # Wait for an async future to complete. May return immediately if already |
| 115 | # complete, or of the future has already been waited on (avoid this). May |
| 116 | # block until the future completes. |
| 117 | function async_wait { |
| 118 | local xtrace |
| 119 | xtrace=$(set +o | grep xtrace) |
| 120 | set +o xtrace |
| 121 | |
| 122 | local pid rc running inifile runtime |
| 123 | rc=0 |
| 124 | for name in $*; do |
| 125 | running=$(ls ${DEST}/async/*.ini 2>/dev/null | wc -l) |
| 126 | inifile="${DEST}/async/${name}.ini" |
| 127 | |
| 128 | if pid=$(async_pidof "$name"); then |
| 129 | async_log "$name" "Waiting for completion of %command" \ |
| 130 | "($running other jobs running)" |
| 131 | time_start async_wait |
| 132 | if [[ "$pid" != "self" ]]; then |
| 133 | # Do not actually call wait if we ran synchronously |
| 134 | if wait $pid; then |
| 135 | rc=0 |
| 136 | else |
| 137 | rc=$? |
| 138 | fi |
| 139 | cat ${DEST}/async/${name}.log |
| 140 | fi |
| 141 | time_stop async_wait |
| 142 | local start_time |
| 143 | local end_time |
| 144 | start_time=$(iniget $inifile job start_time) |
| 145 | end_time=$(iniget $inifile job end_time) |
| 146 | _async_incr_bg_time $(($end_time - $start_time)) |
| 147 | runtime=$((($end_time - $start_time) / 1000)) |
| 148 | async_log "$name" "finished %command with result" \ |
| 149 | "$rc in $runtime seconds" |
| 150 | rm -f $inifile |
| 151 | if [ $rc -ne 0 ]; then |
| 152 | echo Stopping async wait due to error: $* |
| 153 | break |
| 154 | fi |
| 155 | else |
| 156 | # This could probably be removed - it is really just here |
| 157 | # to help notice if you wait for something by the wrong |
| 158 | # name, but it also shows up for things we didn't start |
| 159 | # because they were not enabled. |
| 160 | echo Not waiting for async task $name that we never started or \ |
| 161 | has already been waited for |
| 162 | fi |
| 163 | done |
| 164 | |
| 165 | $xtrace |
| 166 | return $rc |
| 167 | } |
| 168 | |
| 169 | # Check for uncollected futures and wait on them |
| 170 | function async_cleanup { |
| 171 | local name |
| 172 | |
| 173 | if [[ "$DEVSTACK_PARALLEL" != "True" ]]; then |
| 174 | return 0 |
| 175 | fi |
| 176 | |
| 177 | for inifile in $(find ${DEST}/async -name '*.ini'); do |
| 178 | name=$(basename $pidfile .ini) |
| 179 | echo "WARNING: uncollected async future $name" |
| 180 | async_wait $name || true |
| 181 | done |
| 182 | } |
| 183 | |
| 184 | # Make sure our async dir is created and clean |
| 185 | function async_init { |
| 186 | local async_dir=${DEST}/async |
| 187 | |
| 188 | # Clean any residue if present from previous runs |
| 189 | rm -Rf $async_dir |
| 190 | |
| 191 | # Make sure we have a state directory |
| 192 | mkdir -p $async_dir |
| 193 | } |
| 194 | |
| 195 | function async_print_timing { |
| 196 | local bg_time_minus_wait |
| 197 | local elapsed_time |
| 198 | local serial_time |
| 199 | local speedup |
| 200 | |
| 201 | if [[ "$DEVSTACK_PARALLEL" != "True" ]]; then |
| 202 | return 0 |
| 203 | fi |
| 204 | |
| 205 | # The logic here is: All the background task time would be |
| 206 | # serialized if we did not do them in the background. So we can |
| 207 | # add that to the elapsed time for the whole run. However, time we |
| 208 | # spend waiting for async things to finish adds to the elapsed |
| 209 | # time, but is time where we're not doing anything useful. Thus, |
| 210 | # we substract that from the would-be-serialized time. |
| 211 | |
| 212 | bg_time_minus_wait=$((\ |
| 213 | ($_ASYNC_BG_TIME - ${_TIME_TOTAL[async_wait]}) / 1000)) |
| 214 | elapsed_time=$(($(date "+%s") - $_TIME_BEGIN)) |
| 215 | serial_time=$(($elapsed_time + $bg_time_minus_wait)) |
| 216 | |
| 217 | echo |
| 218 | echo "=================" |
| 219 | echo " Async summary" |
| 220 | echo "=================" |
| 221 | echo " Time spent in the background minus waits: $bg_time_minus_wait sec" |
| 222 | echo " Elapsed time: $elapsed_time sec" |
| 223 | echo " Time if we did everything serially: $serial_time sec" |
| 224 | echo " Speedup: " $(echo | awk "{print $serial_time / $elapsed_time}") |
| 225 | } |