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 | |
Dan Smith | 84b328c | 2021-03-26 07:17:42 -0700 | [diff] [blame] | 14 | DEVSTACK_PARALLEL=$(trueorfalse True DEVSTACK_PARALLEL) |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 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 | |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 51 | echo "[$BASHPID Async ${name}:${pid}]: $message" |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 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 |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 60 | local fifo="${DEST}/async/${name}.fifo" |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 61 | shift |
| 62 | set -o xtrace |
| 63 | if $* >${DEST}/async/${name}.log 2>&1; then |
| 64 | rc=0 |
| 65 | set +o xtrace |
| 66 | async_log "$name" "finished successfully" |
| 67 | else |
| 68 | rc=$? |
| 69 | set +o xtrace |
| 70 | async_log "$name" "FAILED with rc $rc" |
| 71 | fi |
| 72 | iniset ${DEST}/async/${name}.ini job end_time $(date "+%s%3N") |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 73 | # Block on the fifo until we are signaled to exit by the main process |
| 74 | cat $fifo |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 75 | return $rc |
| 76 | } |
| 77 | |
| 78 | # Run something async. Takes a symbolic name and a list of arguments of |
| 79 | # what to run. Ideally this would be rarely used and async_runfunc() would |
| 80 | # be used everywhere for readability. |
| 81 | # |
| 82 | # This spawns the work in a background worker, records a "future" to be |
| 83 | # collected by a later call to async_wait() |
| 84 | function async_run { |
| 85 | local xtrace |
| 86 | xtrace=$(set +o | grep xtrace) |
| 87 | set +o xtrace |
| 88 | |
| 89 | local name="$1" |
| 90 | shift |
| 91 | local inifile=${DEST}/async/${name}.ini |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 92 | local fifo=${DEST}/async/${name}.fifo |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 93 | |
| 94 | touch $inifile |
| 95 | iniset $inifile job command "$*" |
| 96 | iniset $inifile job start_time $(date +%s%3N) |
| 97 | |
| 98 | if [[ "$DEVSTACK_PARALLEL" = "True" ]]; then |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 99 | mkfifo $fifo |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 100 | async_inner $name $* & |
| 101 | iniset $inifile job pid $! |
| 102 | async_log "$name" "running: %command" |
| 103 | $xtrace |
| 104 | else |
| 105 | iniset $inifile job pid "self" |
| 106 | async_log "$name" "Running synchronously: %command" |
| 107 | $xtrace |
| 108 | $* |
| 109 | return $? |
| 110 | fi |
| 111 | } |
| 112 | |
| 113 | # Shortcut for running a shell function async. Uses the function name as the |
| 114 | # async name. |
| 115 | function async_runfunc { |
| 116 | async_run $1 $* |
| 117 | } |
| 118 | |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 119 | # Dump some information to help debug a failed wait |
| 120 | function async_wait_dump { |
| 121 | local failpid=$1 |
| 122 | |
| 123 | echo "=== Wait failure dump from $BASHPID ===" |
| 124 | echo "Processes:" |
| 125 | ps -f |
| 126 | echo "Waiting jobs:" |
| 127 | for name in $(ls ${DEST}/async/*.ini); do |
| 128 | echo "Job $name :" |
| 129 | cat "$name" |
| 130 | done |
| 131 | echo "Failed PID status:" |
| 132 | sudo cat /proc/$failpid/status |
| 133 | sudo cat /proc/$failpid/cmdline |
| 134 | echo "=== End wait failure dump ===" |
| 135 | } |
| 136 | |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 137 | # Wait for an async future to complete. May return immediately if already |
| 138 | # complete, or of the future has already been waited on (avoid this). May |
| 139 | # block until the future completes. |
| 140 | function async_wait { |
| 141 | local xtrace |
| 142 | xtrace=$(set +o | grep xtrace) |
| 143 | set +o xtrace |
| 144 | |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 145 | local pid rc running inifile runtime fifo |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 146 | rc=0 |
| 147 | for name in $*; do |
| 148 | running=$(ls ${DEST}/async/*.ini 2>/dev/null | wc -l) |
| 149 | inifile="${DEST}/async/${name}.ini" |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 150 | fifo="${DEST}/async/${name}.fifo" |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 151 | |
| 152 | if pid=$(async_pidof "$name"); then |
| 153 | async_log "$name" "Waiting for completion of %command" \ |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 154 | "running on PID $pid ($running other jobs running)" |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 155 | time_start async_wait |
| 156 | if [[ "$pid" != "self" ]]; then |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 157 | # Signal the child to go ahead and exit since we are about to |
| 158 | # wait for it to collect its status. |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 159 | async_log "$name" "Signaling child to exit" |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 160 | echo WAKEUP > $fifo |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 161 | async_log "$name" "Signaled" |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 162 | # Do not actually call wait if we ran synchronously |
| 163 | if wait $pid; then |
| 164 | rc=0 |
| 165 | else |
| 166 | rc=$? |
| 167 | fi |
| 168 | cat ${DEST}/async/${name}.log |
Dan Smith | aa5c387 | 2021-04-14 14:27:32 -0700 | [diff] [blame] | 169 | rm -f $fifo |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 170 | fi |
| 171 | time_stop async_wait |
| 172 | local start_time |
| 173 | local end_time |
| 174 | start_time=$(iniget $inifile job start_time) |
| 175 | end_time=$(iniget $inifile job end_time) |
| 176 | _async_incr_bg_time $(($end_time - $start_time)) |
| 177 | runtime=$((($end_time - $start_time) / 1000)) |
| 178 | async_log "$name" "finished %command with result" \ |
| 179 | "$rc in $runtime seconds" |
| 180 | rm -f $inifile |
| 181 | if [ $rc -ne 0 ]; then |
Dan Smith | 51e3845 | 2021-04-14 07:23:10 -0700 | [diff] [blame] | 182 | async_wait_dump $pid |
Dan Smith | 30d9bf9 | 2021-01-19 12:10:52 -0800 | [diff] [blame] | 183 | echo Stopping async wait due to error: $* |
| 184 | break |
| 185 | fi |
| 186 | else |
| 187 | # This could probably be removed - it is really just here |
| 188 | # to help notice if you wait for something by the wrong |
| 189 | # name, but it also shows up for things we didn't start |
| 190 | # because they were not enabled. |
| 191 | echo Not waiting for async task $name that we never started or \ |
| 192 | has already been waited for |
| 193 | fi |
| 194 | done |
| 195 | |
| 196 | $xtrace |
| 197 | return $rc |
| 198 | } |
| 199 | |
| 200 | # Check for uncollected futures and wait on them |
| 201 | function async_cleanup { |
| 202 | local name |
| 203 | |
| 204 | if [[ "$DEVSTACK_PARALLEL" != "True" ]]; then |
| 205 | return 0 |
| 206 | fi |
| 207 | |
| 208 | for inifile in $(find ${DEST}/async -name '*.ini'); do |
| 209 | name=$(basename $pidfile .ini) |
| 210 | echo "WARNING: uncollected async future $name" |
| 211 | async_wait $name || true |
| 212 | done |
| 213 | } |
| 214 | |
| 215 | # Make sure our async dir is created and clean |
| 216 | function async_init { |
| 217 | local async_dir=${DEST}/async |
| 218 | |
| 219 | # Clean any residue if present from previous runs |
| 220 | rm -Rf $async_dir |
| 221 | |
| 222 | # Make sure we have a state directory |
| 223 | mkdir -p $async_dir |
| 224 | } |
| 225 | |
| 226 | function async_print_timing { |
| 227 | local bg_time_minus_wait |
| 228 | local elapsed_time |
| 229 | local serial_time |
| 230 | local speedup |
| 231 | |
| 232 | if [[ "$DEVSTACK_PARALLEL" != "True" ]]; then |
| 233 | return 0 |
| 234 | fi |
| 235 | |
| 236 | # The logic here is: All the background task time would be |
| 237 | # serialized if we did not do them in the background. So we can |
| 238 | # add that to the elapsed time for the whole run. However, time we |
| 239 | # spend waiting for async things to finish adds to the elapsed |
| 240 | # time, but is time where we're not doing anything useful. Thus, |
| 241 | # we substract that from the would-be-serialized time. |
| 242 | |
| 243 | bg_time_minus_wait=$((\ |
| 244 | ($_ASYNC_BG_TIME - ${_TIME_TOTAL[async_wait]}) / 1000)) |
| 245 | elapsed_time=$(($(date "+%s") - $_TIME_BEGIN)) |
| 246 | serial_time=$(($elapsed_time + $bg_time_minus_wait)) |
| 247 | |
| 248 | echo |
| 249 | echo "=================" |
| 250 | echo " Async summary" |
| 251 | echo "=================" |
| 252 | echo " Time spent in the background minus waits: $bg_time_minus_wait sec" |
| 253 | echo " Elapsed time: $elapsed_time sec" |
| 254 | echo " Time if we did everything serially: $serial_time sec" |
| 255 | echo " Speedup: " $(echo | awk "{print $serial_time / $elapsed_time}") |
| 256 | } |