Unverified Commit 249d675b authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(operator): 每次运行首次就绪后留 3 分钟到达窗口再开始空服计时,停服或重启时清掉旧计时

parent 32534e3d
Loading
Loading
Loading
Loading
+5 −3
Changes for deploy/crd/felis.lolicon.best_minecraftservers.yaml: 5 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -370,8 +370,10 @@ spec:
                description: |-
                  EmptySince is when the operator first observed 0 online players during
                  a Running phase (spec §8 idle auto-stop). It is reset when a player joins
                  or the server stops, so the empty-duration counter starts fresh each time
                  the server becomes unoccupied.
                  or the server restarts or stops, so the empty-duration counter starts fresh
                  each time the server becomes unoccupied. A zero sampled within three minutes
                  of a run's first ready probe stamps nothing: the players it came up for may
                  not be in yet.
                format: date-time
                type: string
              endpoint:
@@ -422,7 +424,7 @@ spec:
              readySignalAt:
                description: |-
                  ReadySignalAt is when the first RCON probe of the current run succeeded.
                  Every Starting pass clears it, so each start is measured once.
                  Every Starting or Stopping pass clears it, so each start is measured once.
                format: date-time
                type: string
              startRequestedAt:
+5 −3
Changes for internal/apis/felis/v1alpha1/minecraftserver_types.go: 5 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -329,7 +329,7 @@ type MinecraftServerStatus struct {
	// LiveMotd is the MOTD currently advertised for the active phase.
	LiveMotd string `json:"liveMotd,omitempty"`
	// ReadySignalAt is when the first RCON probe of the current run succeeded.
	// Every Starting pass clears it, so each start is measured once.
	// Every Starting or Stopping pass clears it, so each start is measured once.
	ReadySignalAt *metav1.Time `json:"readySignalAt,omitempty"`
	// StartRequestedAt is when the current start attempt was first observed
	// (the first Starting reconcile after desiredState=Running). It anchors the
@@ -340,8 +340,10 @@ type MinecraftServerStatus struct {
	StartRequestedAt *metav1.Time `json:"startRequestedAt,omitempty"`
	// EmptySince is when the operator first observed 0 online players during a
	// Running phase (spec §8 idle auto-stop). It is reset when a player joins
	// or the server stops, so the empty-duration counter starts fresh each time
	// the server becomes unoccupied.
	// or the server restarts or stops, so the empty-duration counter starts fresh
	// each time the server becomes unoccupied. A zero sampled within three minutes
	// of a run's first ready probe stamps nothing: the players it came up for may
	// not be in yet.
	EmptySince *metav1.Time `json:"emptySince,omitempty"`
	// StopNoticeAt is when the operator told the players on a server that it is
	// about to stop (desiredState flipped to Stopped with players online). The stop
+3 −1
Changes for internal/operator/events_test.go: 3 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -101,8 +101,10 @@ func TestIdleStopRecordsAnEvent(t *testing.T) {
	reconcile(t, r, "survival")
	markPodReady(t, c, "survival")
	reconcile(t, r, "survival")
	clock = base.Add(operator.ArrivalWindow)
	reconcile(t, r, "survival")
	drain(rec)
	clock = base.Add(61 * time.Second)
	clock = base.Add(operator.ArrivalWindow + 61*time.Second)
	reconcile(t, r, "survival")
	expectEvents(t, rec, "idle", "Normal IdleStop no players online for 60s; set desiredState to Stopped")
}
+4 −0
Changes for internal/operator/export_test.go: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,3 +7,7 @@ func (r *Reconciler) ProbeMissesTracked() int {
	defer r.probeMu.Unlock()
	return len(r.probeFailures)
}

// ArrivalWindow is how long after a run's first ready probe a zero tally starts no
// idle countdown.
const ArrivalWindow = arrivalWindow
+27 −3
Changes for internal/operator/reconciler.go: 27 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -61,6 +61,13 @@ const (
	maxAutoRestarts            = v1alpha1.MaxAutoRestarts
	autoRestartBaseBackoff     = time.Minute
	defaultReadinessTimeoutSec = 300
	// arrivalWindow is how long after a run's first ready probe a zero tally starts
	// no idle countdown. A server usually comes up because someone asked for it: the
	// player who woke it is still being moved in from the lobby, players a restart
	// dropped are reconnecting, and a modpack client sits in its configuration phase,
	// missing from `list`, for a minute or more. Sampled before they arrived, the
	// zero stopped a server with a short idle timeout just as they got in.
	arrivalWindow = 3 * time.Minute
)

// RconSecretAnnotation stamps the pod template with a fingerprint of the
@@ -437,15 +444,16 @@ func (r *Reconciler) reconcileRunning(ctx context.Context, server *v1alpha1.Mine
	if players.Known && idleStopApplies(server) {
		if players.Online == 0 {
			if server.Status.EmptySince == nil {
				t := r.now()
				server.Status.EmptySince = &t
				if now := r.now(); !arriving(server, now) {
					server.Status.EmptySince = &now
				}
			} else if r.now().Time.Sub(server.Status.EmptySince.Time).Seconds() >=
				float64(server.Spec.Idle.EmptySecondsBeforeStop) {
				// Merge patch, not Update: an unrelated reconcile writes status
				// concurrently, and shipping the whole object back risks
				// clobbering it (the reaper's Stop uses the same pattern for
				// the same reason). EmptySince is deliberately left for
				// markStopped to clear once the scale-down completes.
				// markStopping to clear once the scale-down starts.
				patch := client.MergeFrom(server.DeepCopy())
				server.Spec.DesiredState = v1alpha1.DesiredStopped
				if err := r.Patch(ctx, server, patch); err != nil {
@@ -494,6 +502,14 @@ func idleStopApplies(server *v1alpha1.MinecraftServer) bool {
		server.Spec.Idle.AutoStopEnabled && server.Spec.Idle.EmptySecondsBeforeStop > 0
}

// arriving reports whether a zero tally at now may only mean the players this run
// came up for are not in yet: it is the run's first ready probe (ReadySignalAt is
// stamped after it) or within arrivalWindow of that probe.
func arriving(server *v1alpha1.MinecraftServer, now metav1.Time) bool {
	ready := server.Status.ReadySignalAt
	return ready == nil || now.Time.Before(ready.Add(arrivalWindow))
}

func (r *Reconciler) reconcileStopped(ctx context.Context, server *v1alpha1.MinecraftServer) (ctrl.Result, error) {
	var sts appsv1.StatefulSet
	err := r.Get(ctx, types.NamespacedName{Namespace: server.Namespace, Name: server.Name}, &sts)
@@ -937,6 +953,9 @@ func (r *Reconciler) markStarting(server *v1alpha1.MinecraftServer, reason, msg
	// the last run's ready time would make markRunningReady treat the start as
	// already observed, and every start after the first would go unmeasured.
	server.Status.ReadySignalAt = nil
	// The empty clock belongs to a run too: the last run's would stop the new one at
	// its first zero, before anyone it came up for is in.
	server.Status.EmptySince = nil
	server.Status.Endpoint = v1alpha1.EndpointStatus{Mode: v1alpha1.EndpointFallback, Address: server.Spec.FallbackServer}
	server.Status.LiveMotd = server.Spec.Motd.Starting
	r.setCondition(server, v1alpha1.ConditionReady, metav1.ConditionFalse, reason, msg)
@@ -982,6 +1001,11 @@ func (r *Reconciler) markRunningReady(server *v1alpha1.MinecraftServer, players

func (r *Reconciler) markStopping(server *v1alpha1.MinecraftServer) {
	server.Status.Phase = v1alpha1.PhaseStopping
	// The run ends here. A wake can land before the scale-down completes and bring the
	// pod straight back: the idle stop's stale clock would then stop it again at its
	// first zero, and the players the wake is for get their arrival window.
	server.Status.EmptySince = nil
	server.Status.ReadySignalAt = nil
	server.Status.Ready = false
	server.Status.ObservedGeneration = server.Generation
	server.Status.Endpoint = v1alpha1.EndpointStatus{Mode: v1alpha1.EndpointFallback, Address: server.Spec.FallbackServer}
Loading