4
0

wait-healthy.sh 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env bash
  2. # Waits for a freshly started app container to answer its health check.
  3. #
  4. # wait-healthy.sh <container>
  5. #
  6. # A container counts as started only once it is "healthy": the server answers
  7. # /api/v1/health and reaches the database. "running" is not enough; a release
  8. # that boots and then crashes, or never reaches the database, is running too.
  9. #
  10. # The exit code tells a deploy whether putting the previous image back helps:
  11. #
  12. # 0 healthy
  13. # 1 the migrations finished but the app never got healthy. The previous
  14. # image can run against this schema, so rolling back is safe.
  15. # 2 it stopped before the migrations finished, or they failed. The previous
  16. # image runs the same migrate step at start and would fail the same way,
  17. # so rolling back does not help.
  18. # 3 still migrating when the wait ran out. Rolling back now would stop a
  19. # migration halfway, so it is left running.
  20. #
  21. # It follows the lines init-db.sh prints as it goes. Keep the two in step.
  22. set -uo pipefail
  23. container="${1:?usage: wait-healthy.sh <container>}"
  24. DB_TIMEOUT="${DB_TIMEOUT:-120}" # to reach the database
  25. MIGRATE_TIMEOUT="${MIGRATE_TIMEOUT:-1800}" # for the migrations to finish
  26. APP_TIMEOUT="${APP_TIMEOUT:-180}" # from migrated to healthy
  27. DB_READY='PostgreSQL is ready!'
  28. MIGRATED='Migrations applied successfully!'
  29. db_ready_at=""
  30. migrated_at=""
  31. fail() {
  32. echo "::error title=$container is not healthy::$2"
  33. exit "$1"
  34. }
  35. echo "Waiting for $container to be healthy..."
  36. while :; do
  37. if ! state=$(docker inspect --format '{{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container" 2>/dev/null); then
  38. fail 2 "No container named $container."
  39. fi
  40. read -r status restarts health <<<"$state"
  41. logs=$(docker logs "$container" 2>&1)
  42. if [ -z "$db_ready_at" ] && grep -qF "$DB_READY" <<<"$logs"; then
  43. db_ready_at=$SECONDS
  44. echo "Database reachable after ${SECONDS}s, migrating..."
  45. fi
  46. if [ -z "$migrated_at" ] && grep -qF "$MIGRATED" <<<"$logs"; then
  47. migrated_at=$SECONDS
  48. echo "Migrations done after ${SECONDS}s, starting..."
  49. fi
  50. if [ "$health" = "healthy" ]; then
  51. echo "$container is healthy after ${SECONDS}s."
  52. exit 0
  53. fi
  54. # A compose file written before health checks were added has none. Fall
  55. # back to what the deploys used to check, with a short wait for a crash.
  56. if [ "$health" = "none" ] && [ "$status" = "running" ] && [ "$restarts" = "0" ] \
  57. && [ -n "$migrated_at" ] && [ $((SECONDS - migrated_at)) -ge 15 ]; then
  58. echo "::warning::$container has no health check; it has been running for 15s since its migrations, which is all that can be checked."
  59. exit 0
  60. fi
  61. problem=""
  62. if [ "$restarts" != "0" ]; then
  63. problem="it restarted $restarts time(s)"
  64. elif [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
  65. problem="it $status"
  66. elif [ "$health" = "unhealthy" ]; then
  67. problem="its health check failed"
  68. fi
  69. if [ -n "$problem" ]; then
  70. if [ -n "$migrated_at" ]; then
  71. fail 1 "After its migrations finished, $problem."
  72. fi
  73. fail 2 "Before its migrations finished, $problem."
  74. fi
  75. if [ -n "$migrated_at" ]; then
  76. if [ $((SECONDS - migrated_at)) -ge "$APP_TIMEOUT" ]; then
  77. fail 1 "Not healthy ${APP_TIMEOUT}s after its migrations finished."
  78. fi
  79. elif [ -n "$db_ready_at" ]; then
  80. if [ $((SECONDS - db_ready_at)) -ge "$MIGRATE_TIMEOUT" ]; then
  81. fail 3 "Still migrating after ${MIGRATE_TIMEOUT}s."
  82. fi
  83. elif [ "$SECONDS" -ge "$DB_TIMEOUT" ]; then
  84. fail 2 "Could not reach the database within ${DB_TIMEOUT}s."
  85. fi
  86. sleep 3
  87. done