Skip to content

Process events instantly and consistently, stop skipping the events due to "batching"#844

Merged
nolar merged 6 commits intomainfrom
consistent-processing
Jan 29, 2026
Merged

Process events instantly and consistently, stop skipping the events due to "batching"#844
nolar merged 6 commits intomainfrom
consistent-processing

Conversation

@nolar
Copy link
Copy Markdown
Owner

@nolar nolar commented Oct 3, 2021

TL;DR: Process the events/changes asap, with no artificial delays (even 0.1s), stop skipping some of the events under high load, prevent double-execution if 3rd parties also patch the resources (including non-Kopf-initiated patches from the handlers).

Background

A long time ago, at the dawn of Kopf (5a0b2da, #42, #43), the arrived events for a resource objects were packed into "batches". Only the last event that arrived within the 0.1-second window was processed, all preceding events were ignored.

Originally, it was made to address an issue in the official kubernetes client (which was later replaced by pykube-ng, which in turn was later replaced by raw HTTP queries via aiohttp).

Besides, though not documented, this short time window ensured consistency in some rare cases when a resource was patched by 3rd parties while being processed by Kopf: once Kopf performed its patch, it instantly got both events from the 3rd party and from itself, and only processed the latest state with its own annotations.

Problems

The time-based approach to consistency led to several negative effects noticeable on slow networks between the operator and apiservers (e.g. when operators are executed not in the cluster) or under high load (e.g. with too many resources or too many modifications of several resources).

  • @on.event handlers were not executed for valuable but intermediate events — because they were packed into batches and discarded in favour of the latest event only.
  • All handlers were delayed by some time (0.1 seconds by default; configurable) instead of instant processing.
  • Under the high intensity of changes, the batch could be expanded too much and too long — if all events arrived within 0.1 seconds after each other, potentially leading either to significant delays in processing or no processing at all (if the stream of intense changes never pauses/stops).
  • Double-processing/double-execution of handlers if 3rd parties also patched the resource (including handlers, if they were not doing this via Kopf's patching machinery) — explained below.

These effects were reported and investigated in #729, also directly or indirectly mentioned in #718, #732, #784 (comment). (This PR supersedes and closes #829.)

Besides, code-wise:

  • Added complexity to the code and to tests.

Double-processing

In the mentioned cases (slow networks and/or high load), the version patched by Kopf could arrive later than 0.1 seconds after the version patched by the 3rd party. As a result, the batch was not formed and these events were processed separately. In turn, since the intermediate state of the 3rd-party version did not contain Kopf's annotations about the successful processing of the resource, the handlers were re-executed (double-executed). And only after Kopf's patched version arrived, the operator went idle as expected.

Here is how it happened on the timeline, visually, with an artificial delay of 3 seconds:

         /-- kubectl creates an object (state a=s0)
         | ... sleep 1s
         |    /-- kubectl patches the spec.field with the patch "p1", creates state "b"=s0+p1
         |    |      /-- Kopf patches with annotations (state c=s0+p1+p2)
         |    |      |    /-- Kopf patches with annotations (the same state d=s0+p1+p2+p3, d==c)
         ↓    ↓      |    |
----+-//-aaaaabbbbbbbcccccdddddddddddddddddd--> which state is stored in kubernetes
         ↓    ↓      ↑↓   ↑↓
         |    |      ||   |\----3s----\
         |    |      |\---+3s----\    |
         |    \----3s+---\|      |    |
         \----3s----\|   ||      |    |
                    ↓↑   ↓↑      ↓    ↓
----+-//------------aaaaabbbbbbbbcccccdddddd--> which state is seen by the operator
    ↓               ↓↑   ↓↑      ↓    ↓
    |               ||   ||      |    \-- Kopf gets the state "d"=s0+p1+p2+p3, sees the annotations, goes idle.
    |               ||   ||      \-- Kopf gets the state "c"=s0+p1+p2, sees the annotations, goes idle.
    |               ||   ||
    |               ||   |\-- Kopf reacts, executes handlers (2ND TIME), adds annotations with a patch (p3)
    |               ||   \-- Kopf gets the state "b"=s0+p1 with NO annotations of "p2" yet.
    |               ||       !BUG!: "c"=s0+p1+p2 is not seen yet, though "c"/"p2" exists by now!
    |               ||
    |               |\-- Kopf reacts, executes handlers (1ST TIME), adds annotations with a patch (p2)
    |               \-- Kopf gets a watch-event (state a)
    \-- Kopf starts watching the resource

Solution

The proposed solution introduces a wait for consistency of the resource after it is patched: since the PATCH operation returns the patched resource, we can get its resource version and expect this version in the watch-stream. All states arrived before the expected version are considered inconsistent and thus are not processed at least for the high-level state-dependent handlers.

This is how it looks on the timeline with the same artificial delay of 3 seconds:

         /-- kubectl creates an object (state a=s0)
         | ... sleep 1s
         |    /-- kubectl patches the spec.field with the patch "p1", creates state "b"=s0+p1
         |    |      /-- Kopf patches with annotations (state c=s0+p1+p2)
         ↓    ↓      |
----+-//-aaaaabbbbbbbccccccccccccccccccc-> which state is stored in kubernetes
         ↓    ↓      ↑↓
         |    |      |\----3s----\
         |    \----3s+---\       |
         \----3s----\|   |       |
                    ↓↑   ↓       ↓ 
----+-//------------aaaaabbbbbbbbcccccc-> which state is seen by the operator
    ↓               ↓↑⇶⇶⇶⇶⇶⇶⇶⇶⇶⇶⇶⇶↓  Kopf's own patch "p2" enables the consistency expectation for 5s OR version "c"
    |               ||   |       |
    |               ||   |       \-- Kopf gets a consistent state "c"=s0+p1+p2 as expected, thus goes idle.
    |               ||   |
    |               ||   \-- Kopf executes ONLY the low-level handlers over the state "b"=s0+p1.
    |               ||   \~~~~~~⨳ inconsistency mode: wait until a new event (then discard it) OR timeout (then process it) 
    |               ||
    |               |\-- Kopf reacts, executes handlers, adds annotations with a patch (p2)
    |               \-- Kopf gets a watch-event (state a)
    \-- Kopf starts watching the resource

In case the expected version does not arrive within some reasonable time window (5-10 seconds), assume that it will not arrive at all and reset the consistency waiting as if the consistency is reached (even if not). This is a rare case, mostly impossible, and is needed only as a safe-guard: it is better to double-process the resource and cause side-effects than to cease its processing forever.

Time-based batching is removed completely as outdated and not adding any benefit.

User effects

As a result of this fix, all mentioned problems are addressed:

  • Low-level events are never skipped, each and every event is handled via @on.event(), indexed and passed to daemons.
  • High-level handlers are never double-executed — because only the consistent states are processed, so the 3rd party patches without Kopf's annotations do not trigger these handlers.
  • The processing happens instantly with no artificial delay for batching.
  • Batches do never expand infinitely (because there are no batches anymore).

TODOs

  • Verify that the solution is principally correct, explore the edge-cases manually.
    • Multiple change-detecting handlers, with several patches in a row.
    • Combined change-detecting and low-level handlers (without patches).
    • Combined change-detecting and low-level handlers (with patches).
    • Indexing of resources with long inconsistency.
    • Live body updates in daemons/timers.
  • Adjust tests: remove the time-based ones, add the resource-version-based ones.
  • Adjust the docs where applicable.

@nolar nolar added enhancement New feature or request refactoring Code cleanup without new features added labels Oct 3, 2021
@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Oct 3, 2021

This pull request introduces 3 alerts when merging e856f7d into c1745b7 - view on LGTM.com

new alerts:

  • 3 for Unused import

@nolar nolar force-pushed the consistent-processing branch from e856f7d to 051496e Compare October 3, 2021 15:26
@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Oct 3, 2021

This pull request introduces 3 alerts when merging 051496e into c1745b7 - view on LGTM.com

new alerts:

  • 3 for Unused import

@paxbit
Copy link
Copy Markdown

paxbit commented Feb 17, 2022

Hi @nolar,

could you please give a short update on this PR? Is it still on track?

Cheers,
paxbit

@nolar nolar force-pushed the consistent-processing branch from 051496e to 1e66e5f Compare August 7, 2022 16:13
@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Aug 7, 2022

This pull request introduces 1 alert when merging 1e66e5f into 7b45690 - view on LGTM.com

new alerts:

  • 1 for Unused import

Comment thread tests/handling/test_consistency.py Dismissed
Comment thread kopf/_core/reactor/queueing.py Fixed
Comment thread kopf/_core/reactor/queueing.py Fixed
@nolar nolar force-pushed the consistent-processing branch from 62d3930 to 0a36b8d Compare January 27, 2026 21:07
Comment thread kopf/_core/reactor/orchestration.py Fixed
@nolar nolar force-pushed the consistent-processing branch 5 times, most recently from 7bf795f to f27a1e4 Compare January 29, 2026 09:40
Previously, we used only the status stanza for the reconstruction of the patched body. This was entirely incorrect — besides the patched stanzas, also the metadata changes, such as the `resourceVersion` and other fields.

This was not a problem as the reconstructed body was not used for anything useful except for some inconsistency checking (whether the patch was fully applied or not).

This becomes a problem when we start tracking the `resourceVersion` — we need the latest patched version applied, which means the status patch. The new `resourceVersion` should not be ignored. If we use the `resourceVersion` from the body patch, we track the consistency until that patch arrives and start calling the handlers, despite their status arrives a little bit later from the status patch.

Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
@nolar nolar force-pushed the consistent-processing branch from f27a1e4 to 11555c9 Compare January 29, 2026 11:01
nolar added 5 commits January 29, 2026 13:21
Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
Rationale:

Previously (before the change), we picked ALL the events from the backlog within the batch window, so we knew that the queue is empty, and that we are processing the very last event (the very concept of batching). As such, we were safe to relieve the stream pressure (clear the flag unconditionally).

Now (after the change), we process the events one by one to ensure the low-level handlers never skip, so for every next event, there could be a few more waiting in the backlog. We must get back to them asap, without any sleeps. The only way to ensure that the processor does not sleep, is to not relieve the stream pressure (not clear the flag), possibly even trigger the pressure (raise the flag).

This will automatically wake up the sleeps with the full time "unslept" (i.e., actually sleep for zero time), and will mark the deemed/implied consistency as not achieved, thus skipping the higher-level state-tracking handlers and getting back to the backlog consumption.

This is functionally equivalent to the new events truly arriving to the backlog immediately after the stream pressure is relieved (the flag cleared) — except that they are already in the backlog, and we know this.

Only the very last event in the backlog will relieve the stream pressure, thus letting the processor sleep until the deemed/implied consistency (the only sleep it needs).

Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
Previously, both the low-level and high-level handlers were skipped if there was a newer event in the backlog. This was fine with the concept of batching, where only the latest state was processed. But that broke the promise of seeing ALL the events in the low-level handlers (some intermediate events were skipped).

Now, we nevertheless process the pending event — even if we know there is another newer event waiting in the backlog. This ensures the low-level handlers and daemons are started as if there was no indexing pause. The newer event will be processed as if arrived a bit later.

This change has no effect if indexing is absent — there is simply no such situation when we have the old event & a newer event.

Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
There is no mo batching.

Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
@nolar nolar force-pushed the consistent-processing branch from 11555c9 to 9f2c3fa Compare January 29, 2026 12:30
@nolar nolar marked this pull request as ready for review January 29, 2026 12:31
@nolar
Copy link
Copy Markdown
Owner Author

nolar commented Jan 29, 2026

So far, the PR is ready, all tests are added and green, all critical aspects are explored manually (with artifically injected delays) and work as expected. I am ready to click "merge" & release it — but am a bit afraid. It would be good to test it somewhere in a realistic environment (from git or maybe as an release candidate version).

@nolar nolar merged commit 181413d into main Jan 29, 2026
38 checks passed
@nolar nolar deleted the consistent-processing branch January 29, 2026 13:22
mcp-ci-bot pushed a commit to Mirantis/rockoon that referenced this pull request Feb 2, 2026
latest kopf 1.42.0 is potentially able to resolve a slew of bugs
related to controller missing events, see
nolar/kopf#844

Related-Issue: PRODX-56742 PRODX-57473
Change-Id: Ia535929623d710ab20975b8030d940eb5179780b
@nolar nolar mentioned this pull request Mar 17, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request refactoring Code cleanup without new features added

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants