Repository navigation
Conversation
FIFO.Pop holds the queue lock while calling the process function. Processing the cluster event inside of it blocked queueClusterEvent for this worker until the event was done, e.g. for the whole rolling update of a cluster. Since the postgresql informer handler queues the events, a single busy worker stalled the events of all clusters, leaving the other workers idle. Pop the event and process it after the lock is released. Now that events can pile up in a queue, update events get a key per resource version. Otherwise a pending update would be replaced by a later one and the changes of the first one, based on the diff of old and new spec, get lost. A delete event therefore discards pending events by cluster UID. Also guard the worker assignment map, which is accessed from the informer handlers, the periodic resync and the API server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pod events are sent to the subscriber of a pod without blocking. With an unbuffered channel an event got dropped whenever the subscriber was not waiting on the channel at that very moment, e.g. when two events arrive back to back. A dropped delete event left the worker in waitForPodDeletion until pod_deletion_wait_timeout (10m) expired and the update failed, although the pod was recreated fine. Buffer the subscriber channel and log dropped events. Also recognize a deletion the informer missed: handle tombstones in the pod delete handler and treat an update replacing the pod with a new instance (different UID) as deleted. As events are buffered now, wait for the master label of the candidate after a switchover instead of any role label, skipping earlier replica events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connection was retried every 15s (the connect timeout). Right after a pod is (re)created the first attempt usually fails, so every rolling update spent 15s waiting before syncing roles. Start retrying after 1s and double the interval up to 15s, keeping the overall retry timeout of 2m. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resync ticker fired every resync period only, where a sync was always due, so the repair scan of failed clusters effectively never ran every repair_period. Tick at the shorter of both periods and decide whether a sync or a repair is due. Record the time of the tick as the last sync or repair time instead of the time after listing and queueing all clusters, and tolerate ticks arriving slightly early, so a due sync is not postponed by a whole tick. A repair period that is not positive disables the repair scans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jorsol
requested review from
FxKu,
Jan-M,
hughcapet,
idanovinda,
jopadi and
mikkeloscar
as code owners
November 1, 2026 06:42
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When many clusters get updated at once (e.g. a change to every
postgresqlCR), the operator processes them far slower than its worker count allows, and single lost events can stall it for minutes. Measured on a kind cluster with 100 single-instance clusters, patching an env var into every CR took 32 minutes with the default settings, and 2 clusters ended upUpdateFailedalthough their pods rolled fine.Four independent causes, one commit each:
cache.FIFO.Popholds the queue lock while the process function runs, and the worker processed the event inside it.queueClusterEventtherefore blocked until the worker finished, often a whole rolling update. Since it's called from the postgresql informer handler, the events of every cluster waited behind it. Workers sat idle (on average 2.6 of 8 busy), and the time between patching a CR and the operator queueing its event had a median of 953s. The periodic resync was blocked the same way: it queued only 15 SYNC events in 14 minutes.selectyet. A dropped DELETE event left the worker inwaitForPodDeletionuntilpod_deletion_wait_timeout(10m) expired. Combined with (1), that froze the processing of all clusters for 10 minutes, twice in one run.repair_periodnever ran on its own schedule. The resync ticker fired only everyresync_period, when a sync was always due, so the repair branch was effectively dead. The last sync time was also saved after listing and queueing and compared in whole seconds, so the sync could slip by a tick.Changes
Poponly takes the event off the queue, and processing happens after the lock is released.resourceVersion. Otherwise a later update would replace a pending one, and the first one's changes would be lost, becauseCluster.Updateworks on the diff between old and new spec. SYNC/REPAIR/ADD events still replace each other.retryutil.RetryWithBackoff, which starts at 1s and doubles up to 15s, keeping the 2m overall timeout. This also resolves the existingTODO: make the retry exponential.min(resync_period, repair_period), and each tick decides whether a sync or a repair is due.repair_period <= 0disables repair scans.Results
Single-node kind cluster, 12 CPUs. "Time to roll all clusters" = from patching an env var into every CR until all pods run the new spec.
100 single-instance clusters
At 64 workers the limit becomes the node itself (64 Spilo pods restarting at once on 12 CPUs). With this PR all 64 workers do work at the same time; on master the peak was 63.
Mixed: 33 single-instance + 22 three-instance clusters (99 pods), this PR
Write outages were measured with a probe inserting into every cluster's primary every 0.5s.
Behavior changes worth noting
repair_period(default 5m). Each one lists the CRs and queues a REPAIR event for clusters whose status isn't successful.Testing
isPodGone, and the switchover waiting for the master labelRetryWithBackoffWorkerclusterListAndSyncscheduling, including early ticksgo test -race ./pkg/...passes.resync_period=4m,repair_period=1m: repair scans ran every minute and a full sync every 4 minutes.