vitessio/vitess · error

mysqld_shutdown hook failed: %v

Error message

mysqld_shutdown hook failed: %v

What it means

During executeShutdown, Vitess runs the optional 'mysqld_shutdown' hook to customize how mysqld is stopped. If the hook returns any status other than success or does-not-exist, shutdown is aborted immediately and this error is returned. Unlike the preflight hook, this one fires mid-shutdown, so mysqld may already be in the middle of stopping when it fails.

Source

Thrown at go/vt/mysqlctl/mysqld.go:1177

			// The error is only suppressed when the wait below will run:
			// for waitForMysqld=false callers a nil return would claim a
			// shutdown nothing verified, so they get the error as before.
			if !mysqladminAbortedWaiting(output) {
				// The SHUTDOWN command was never delivered.
				return false, err
			}
			if !waitForMysqld {
				// Delivered, but nothing below will verify the outcome.
				return true, err
			}
			log.Warn("mysqladmin gave up waiting for mysqld to stop, waiting on pid/socket files instead", slog.Any("error", err))
			var cancel context.CancelFunc
			ctx, cancel = boundShutdownWaitContext(ctx)
			defer cancel()
		}
	default:
		// hook failed, we report error
		return false, fmt.Errorf("mysqld_shutdown hook failed: %v", hr.String())
	}

	// Wait for mysqld to really stop. Use the socket and pid files as a
	// proxy for that since we can't call wait() in a process we
	// didn't start.
	if waitForMysqld {
		log.Info(fmt.Sprintf("Mysqld.Shutdown: waiting for socket file (%v) and pid file (%v) to disappear", cnf.SocketFile, cnf.PidFile))
		if err := waitForMysqldExit(ctx, cnf.SocketFile, cnf.PidFile); err != nil {
			return true, err
		}
	}
	return true, nil
}

// StartAfterExit waits for a mysqld process that shut itself down (e.g. after a
// CLONE operation) to fully exit, then starts a new one. It polls for the
// disappearance of the socket and pid files before calling Start.
func (mysqld *Mysqld) StartAfterExit(ctx context.Context, cnf *Mycnf) error {

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Read the hook's exit status and output embedded in the error (hr.String()) and fix the custom shutdown script accordingly.
  2. Test the hook script manually on the host with the same user and environment Vitess runs under.
  3. Ensure the script handles the already-stopped-mysqld case gracefully (idempotent exit 0).
  4. Remove the hook if it is no longer needed so the default shutdown path is used.

Example fix

// before
mysqladmin -p wrongpass shutdown || exit 1
// after
mysqladmin shutdown 2>/dev/null || exit 0  # already stopped is OK
Defensive patterns

Strategy: try-catch

Validate before calling

hr := hook.NewHook("mysqld_shutdown").ExecuteContext(ctx)
if hr.ExitStatus != hook.HOOK_SUCCESS && hr.ExitStatus != hook.HOOK_DOES_NOT_EXIST {
    return fmt.Errorf("shutdown hook not ready: %s", hr.String())
}

Type guard

func shutdownHookOK(hr *hook.HookResult) bool {
    return hr.ExitStatus == hook.HOOK_SUCCESS || hr.ExitStatus == hook.HOOK_DOES_NOT_EXIST
}

Try / catch

err := mysqld.Shutdown(ctx, cnf, true, timeout)
if err != nil && strings.Contains(err.Error(), "mysqld_shutdown hook failed") {
    // parse hr from message; if mysqld is already partially stopping, poll socket file removal
}

Prevention

When it happens

Trigger: Mysqld.executeShutdown (via shutdownWithReplicaCrashSafety) invokes the mysqld_shutdown hook; the hook script exits with a non-success status (other than HOOK_SUCCESS / HOOK_DOES_NOT_EXIST).

Common situations: A custom shutdown script (e.g. using mysqladmin shutdown with special flags) fails because of wrong credentials or mysqld already gone; the hook times out; the script's environment (PATH, config) differs from what it expects under Vitess.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/8dc6d268a33a9fe7. Report an issue: GitHub.