Unverified Commit 857b6da8 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(operator): 删除依赖 rcon-cli 的 preStop 死代码,缩容前由 operator 经 RCON 执行 save-all flush

parent 5521e498
Loading
Loading
Loading
Loading
+0 −4
Changes for deploy/crd/felis.lolicon.best_minecraftservers.yaml: 0 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -123,10 +123,6 @@ spec:
              lifecycle:
                description: Lifecycle tunes graceful shutdown (spec §7).
                properties:
                  preStopSaveAndStop:
                    description: PreStopSaveAndStop enables the operator-injected
                      RCON save+stop preStop.
                    type: boolean
                  terminationGracePeriodSeconds:
                    description: TerminationGracePeriodSeconds is the pod grace period
                      (default 300).
+1 −1
Changes for docs/troubleshooting.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -256,7 +256,7 @@ What holds the world, in order:
   ```

A restore, backup or file write refused with `409 not_stopped` although the
panel shows `Stopped` means the game pod is still terminating (its preStop save
panel shows `Stopped` means the game pod is still terminating (its shutdown save
can take a while); retry once `kubectl -n minecraft get pods -l
felis.lolicon.best/server=<name>` shows nothing.

+3 −4
Changes for internal/apis/felis/v1alpha1/minecraftserver_types.go: 3 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -197,13 +197,12 @@ type StorageSpec struct {
	StorageClassName string `json:"storageClassName,omitempty"`
}

// LifecycleSpec tunes graceful shutdown (spec §7). The operator injects a
// preStop RCON "save-all flush; stop" hook when PreStopSaveAndStop is set.
// LifecycleSpec tunes graceful shutdown (spec §7). Before scaling a server with
// RCON to zero the operator runs "save-all flush" over RCON; the grace period is
// then the time the server has to finish its own shutdown save after SIGTERM.
type LifecycleSpec struct {
	// TerminationGracePeriodSeconds is the pod grace period (default 300).
	TerminationGracePeriodSeconds int64 `json:"terminationGracePeriodSeconds,omitempty"`
	// PreStopSaveAndStop enables the operator-injected RCON save+stop preStop.
	PreStopSaveAndStop bool `json:"preStopSaveAndStop,omitempty"`
}

// StartupSpec bounds the Starting phase (spec §5).
+4 −24
Changes for internal/operator/builders.go: 4 added lines, 24 removed lines.
Original line number Diff line number Diff line
@@ -108,18 +108,6 @@ func rconAddress(server *v1alpha1.MinecraftServer) string {
	return fmt.Sprintf("%s.%s.svc.cluster.local:%d", server.Name, server.Namespace, rconPort(server))
}

// preStopScript is the operator-injected graceful-shutdown sequence (spec §7):
// flush the world, then stop the server, both over RCON. It relies on rcon-cli
// being present in the Felis base image and reading the RCON_* env injected
// alongside it.
func preStopScript(server *v1alpha1.MinecraftServer) string {
	port := rconPort(server)
	return fmt.Sprintf(
		`rcon-cli --port %d --password "$RCON_PASSWORD" save-all flush; rcon-cli --port %d --password "$RCON_PASSWORD" stop`,
		port, port,
	)
}

// buildHeadlessService backs the StatefulSet's stable network identity.
func buildHeadlessService(server *v1alpha1.MinecraftServer) *corev1.Service {
	svc := &corev1.Service{
@@ -198,9 +186,10 @@ func readinessProbe(server *v1alpha1.MinecraftServer) *corev1.Probe {
	return probe
}

// buildStatefulSet renders the workload for replicas in {0,1}. It is where
// graceful shutdown is injected: the pod gets terminationGracePeriodSeconds and
// (when enabled) a preStop RCON save+stop hook.
// buildStatefulSet renders the workload for replicas in {0,1}. Its half of
// graceful shutdown is terminationGracePeriodSeconds, the time the server gets to
// save on SIGTERM; the reconciler flushes the world over RCON before it scales to
// zero (saveBeforeStop).
func buildStatefulSet(server *v1alpha1.MinecraftServer, replicas int32, felisImage string) (*appsv1.StatefulSet, error) {
	storageSize := server.Spec.Storage.Size
	if storageSize == "" {
@@ -245,15 +234,6 @@ func buildStatefulSet(server *v1alpha1.MinecraftServer, replicas int32, felisIma
		container.Ports = append(container.Ports, corev1.ContainerPort{
			Name: "rcon", ContainerPort: rconPort(server), Protocol: corev1.ProtocolTCP,
		})
		if server.Spec.Lifecycle.PreStopSaveAndStop {
			container.Lifecycle = &corev1.Lifecycle{
				PreStop: &corev1.LifecycleHandler{
					Exec: &corev1.ExecAction{
						Command: []string{"/bin/sh", "-c", preStopScript(server)},
					},
				},
			}
		}
	}

	// Every server first hands its world volume to the game uid (prepareDataInitContainer),
+54 −15
Changes for internal/operator/prober.go: 54 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -20,21 +20,52 @@ type PlayerCount struct {
	Known  bool
}

// Prober reports whether a server's RCON endpoint is reachable and accepts the
// password, and best-effort returns its current player tally. A nil error is the
// loader-agnostic readiness gate (spec §5); the PlayerCount is advisory and has
// Known=false (with a nil error) whenever the tally could not be sampled. It is an
// interface so the reconciler can be tested without a live server.
// Prober is the operator's RCON channel into a running server. It is an interface
// so the reconciler can be tested without a live server.
//
// Probe reports whether the endpoint is reachable and accepts the password, and
// best-effort returns the current player tally. A nil error is the loader-agnostic
// readiness gate (spec §5); the PlayerCount is advisory and has Known=false (with
// a nil error) whenever the tally could not be sampled.
//
// Save flushes the world to disk (`save-all flush`) and returns once the server
// has answered, i.e. once the save is done. The reconciler runs it right before
// scaling a server to zero (spec §7).
type Prober interface {
	Probe(ctx context.Context, addr, password string) (PlayerCount, error)
	Save(ctx context.Context, addr, password string) error
}

// RconProber is the production Prober: a successful Dial (TCP connect + auth)
// is the readiness gate; on that same connection it then runs `list` to sample
// the player tally before closing.
type RconProber struct {
	// Timeout bounds a single probe. Defaults to 5s.
	// Timeout bounds a single probe, and the connect+auth step of a save.
	// Defaults to 5s.
	Timeout time.Duration
	// SaveTimeout bounds the wait for `save-all flush` to answer. Defaults to
	// defaultSaveTimeout.
	SaveTimeout time.Duration
}

// defaultSaveTimeout is how long a stop waits for the pre-stop save. The server
// answers `save-all flush` only after every loaded chunk is written, which takes
// seconds on a large world. Past this the stop goes ahead anyway: SIGTERM runs the
// server's own shutdown save within the pod's grace period, so the explicit save
// only moves most of that work ahead of the kill deadline.
const defaultSaveTimeout = 30 * time.Second

// boundTimeout returns d (or def when d is unset), shortened to ctx's deadline.
func boundTimeout(ctx context.Context, d, def time.Duration) time.Duration {
	if d <= 0 {
		d = def
	}
	if dl, ok := ctx.Deadline(); ok {
		if remaining := time.Until(dl); remaining > 0 && remaining < d {
			d = remaining
		}
	}
	return d
}

// Probe dials addr and authenticates with password, honoring the smaller of the
@@ -43,15 +74,7 @@ type RconProber struct {
// a failed or unparseable `list` yields an unknown PlayerCount, never a probe error,
// so a transient count-read hiccup can never flap a healthy server out of Ready.
func (p RconProber) Probe(ctx context.Context, addr, password string) (PlayerCount, error) {
	timeout := p.Timeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	if dl, ok := ctx.Deadline(); ok {
		if remaining := time.Until(dl); remaining > 0 && remaining < timeout {
			timeout = remaining
		}
	}
	timeout := boundTimeout(ctx, p.Timeout, 5*time.Second)
	conn, err := rcon.Dial(addr, password, timeout)
	if err != nil {
		return PlayerCount{}, err
@@ -71,6 +94,22 @@ func (p RconProber) Probe(ctx context.Context, addr, password string) (PlayerCou
	return pc, nil
}

// Save runs `save-all flush` and waits for its reply. The reply text is not
// checked: vanilla, Paper and the modded loaders word it differently, and any
// reply at all means the command ran to completion on the server thread.
func (p RconProber) Save(ctx context.Context, addr, password string) error {
	conn, err := rcon.Dial(addr, password, boundTimeout(ctx, p.Timeout, 5*time.Second))
	if err != nil {
		return err
	}
	defer conn.Close()
	if err := conn.SetDeadline(time.Now().Add(boundTimeout(ctx, p.SaveTimeout, defaultSaveTimeout))); err != nil {
		return err
	}
	_, err = conn.Execute("save-all flush")
	return err
}

// listReplyPatterns match the `list` replies of the loaders Felis runs, tried in
// order against the reply with § color codes stripped:
//
Loading