Skip to content

Handle WebSub delta updates without full undeploy/redeploy#1921

Closed
senthuran16 wants to merge 24 commits into
mainfrom
websub-kafka-improvements-1
Closed

Handle WebSub delta updates without full undeploy/redeploy#1921
senthuran16 wants to merge 24 commits into
mainfrom
websub-kafka-improvements-1

Conversation

@senthuran16

Copy link
Copy Markdown
Member

Purpose

WebSub binding updates were going through full remove-and-readd flows, which caused unnecessary undeploy behavior, dropped live receiver state, and made channel-only
updates heavier than needed. Resolves N/A.

Goals

  • apply WebSub channel-only updates in place
  • keep subscription sync topics and live receiver state intact during delta updates
  • fall back to full rebind only for structural changes

Approach

  • added a WebSub receiver delta-update path that can add/remove channels without recreating the whole binding
  • updated runtime WebSub binding handling to diff channel topics, update hub chains, ensure new topics, and delete removed channel topics while preserving the internal
    subscription topic
  • changed the xDS handler to call UpdateWebSubApiBinding(...) instead of remove-then-add for changed bindings
  • added controller/service-side undeploy handling improvements included in this branch’s diff

User stories

WebSub API updates with channel-only changes are applied without unnecessary undeploys, while real structural changes still rebind safely.

Documentation

N/A - runtime/controller behavior fix only; no product documentation update included in this PR.

Automation tests

  • Unit tests

    Branch includes focused handler/runtime/service test coverage for delta update and undeploy paths

  • Integration tests

    Not run in this turn

Security checks

Samples

N/A

Related PRs

#1918

Test environment

Local diff review on Ubuntu 24.04.4 LTS

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd8b0baf-caf6-4815-a220-3b629a1fbfb3

📥 Commits

Reviewing files that changed from the base of the PR and between 2fcdc92 and bddfed4.

📒 Files selected for processing (1)
  • gateway/gateway-controller/pkg/api/handlers/handlers_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-controller/pkg/api/handlers/handlers_test.go

📝 Walkthrough

Summary

This PR implements delta update support for WebSub API bindings to avoid full undeploy/redeploy cycles when only channel configurations change. The solution preserves live receiver state, subscription sync topics, and receiver subscriptions during partial binding updates, falling back to full replacement only when structural changes occur.

Changes

Event Gateway Runtime - Delta Update Infrastructure

  • Added ApplyBindingDelta method to WebSubReceiver to handle channel add/remove operations in place, managing Kafka topic registration, consumer subscriptions, and stored subscription cleanup for removed channels while preserving the internal subscription sync topic.

  • Implemented UpdateWebSubApiBinding method in Runtime to support xDS-style delta updates. This method intelligently chooses between delta and full replacement based on binding compatibility (name, context, version, receiver, and broker-driver configuration equality). Delta updates compute per-channel topic changes, ensure new topics exist, apply receiver deltas, delete removed channel topics, rebuild policy-chain routes, and update hub registrations.

  • Updated xDS handler (Handler.HandleResources) to use the new UpdateWebSubApiBinding instead of removing and re-adding bindings when an existing EventChannelResource changes.

Concurrency and Thread Safety

  • Added channelMu sync.RWMutex to WebSubReceiver for protecting concurrent access to channel mappings.

  • Updated hub and webhook handlers to accept and use the receiver's channel mutex when reading from the channels map during subscribe, unsubscribe, and webhook reception operations.

Gateway Controller - WebSub API Service

  • Created a new websubapi service package implementing WebSubAPIService.Update to handle WebSub API configuration updates. The service:

    • Parses incoming payloads and validates metadata consistency
    • Distinguishes between deployed and undeployed state transitions
    • For undeployed updates, sets desired state without invoking deployment service, allowing targeted undeploy handling
    • For deployed updates, delegates to the existing deployment service
    • Publishes events and triggers artifact sync for gateway-origin APIs
  • Introduced WebSub API-specific error types: ErrNotFound, ValidationError, ParseError, and HandleMismatchError.

  • Updated UpdateWebSubAPI handler to delegate to the service layer via webSubAPIService.Update.

  • Wired WebSubAPIService into the APIServer during initialization.

Kafka Configuration

  • Extended Kafka configuration with compact_topic_partitions and compact_topic_replication_factor to support configurable compacted topic creation instead of hardcoded defaults.

  • Updated compacted topic creation (EnsureCompactedTopic) to use configured partition and replication factor values.

Consumer Management

  • Changed consumer group ID generation to use the first 32 hex characters of the SHA-256 hash of the callback URL (previously 16 characters) for better uniqueness.

Testing

Unit tests cover delta update paths and undeploy handling, including a new test verifying that WebSub API updates with deploymentState: "undeployed" follow the dedicated service path and publish appropriate events. Integration tests were not executed.

Related

Addresses improvements related to PR #1918.

Walkthrough

This PR implements delta-update support for WebSub API bindings: runtime delta-vs-replace decision and orchestration, receiver ApplyBindingDelta to mutate channels in-place, xDS handler update integration and tests, controller WebSubAPI service to handle updates/deploy/undeploy and publish events, and Kafka compact-topic configuration plus tests.

Sequence Diagram

sequenceDiagram
  participant xDS as xDS Handler
  participant Runtime
  participant Receiver as WebSubReceiver
  participant Broker as BrokerDriver
  participant Service as WebSubAPIService
  participant Storage
  participant EventHub
  xDS->>Runtime: UpdateWebSubApiBinding(old,new)
  Runtime->>Runtime: canDeltaUpdateWebSubBinding?
  alt delta
    Runtime->>Broker: Ensure topics for added channels
    Runtime->>Receiver: ApplyBindingDelta(removed,added)
    Runtime->>Broker: Delete topics for removed channels
    Runtime->>Runtime: Rebuild policy-chains and update stored topic list
  else replace
    Runtime->>Runtime: RemoveWebSubApiBinding(old)
    Runtime->>Runtime: AddWebSubApiBinding(new)
  end
  Runtime-->>xDS: success
  xDS->>Service: Update(UpdateParams)
  Service->>Storage: load existing config
  Service->>Service: parse/validate/render config
  Service->>Storage: persist configuration
  Service->>EventHub: publish UPDATE event
  Service-->>xDS: UpdateResult
Loading

</details>

<!-- walkthrough_end -->

<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>

### ❌ Failed checks (1 warning)

|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |
| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |

<details>
<summary>✅ Passed checks (4 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                                                                                            |
| :------------------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|         Title check        | ✅ Passed | The title accurately summarizes the main change: implementing delta updates for WebSub bindings to avoid full undeploy/redeploy cycles.                                                                                |
|      Description check     | ✅ Passed | The description follows the template with complete Purpose, Goals, Approach, User Stories, Documentation, Automation Tests, and Security Checks sections. All required sections are provided with substantive content. |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                                                               |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                                                               |

</details>

<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>

</details>

<!-- pre_merge_checks_walkthrough_end -->

<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>📝 Generate docstrings</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch

</details>
<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit unit tests in branch `websub-kafka-improvements-1`

</details>

</details>

<!-- finishing_touch_checkbox_end -->

<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 golangci-lint (2.12.1)</summary>
> 
> level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"
> 
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>💬 Introducing Slack Agent: The best way for teams to turn conversations into code.</summary>
> 
> [Slack Agent](https://www.coderabbit.ai/agent) is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
> 
> - Generate code and open pull requests
> - Plan features and break down work
> - Investigate incidents and troubleshoot customer tickets together
> - Automate recurring tasks and respond to alerts with triggers
> - Summarize progress and report instantly
> 
> Built for teams:
> 
> - **Shared memory** across your entire org—no repeating context
> - **Per-thread sandboxes** to safely plan and execute work
> - **Governance built-in**—scoped access, auditability, and budget controls
> 
> One agent for your entire SDLC. Right inside Slack.
> 
> 👉 [Get started](https://agent.coderabbit.ai/)
> 
> </details>

<!-- announcements_end -->

<!-- tips_start -->

---

Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=wso2/api-platform&utm_content=1921)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>


<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>

<!-- tips_end -->

<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@event-gateway/gateway-runtime/internal/connectors/receiver/websub/delta.go`:
- Around line 41-67: ApplyBindingDelta mutates e.channel.Channels concurrently
with HubHandler and WebhookReceiverHandler reading it, causing a data race; add
a receiver-level sync.RWMutex to the receiver struct and use it to protect all
accesses to e.channel.Channels: wrap mutations in ApplyBindingDelta (the
delete(e.channel.Channels, ...) loop and the e.channel.Channels[...] =
kafkaTopic assignments) with mu.Lock()/mu.Unlock(), and wrap reads in HubHandler
and WebhookReceiverHandler with mu.RLock()/mu.RUnlock(); alternatively, if you
prefer snapshots, have ApplyBindingDelta produce an immutable copy of the
channel map and ensure handlers always read from that atomic snapshot reference
(use the mutex or atomic.Value to swap snapshots) so no handler reads the live
map while it is being modified.

In `@event-gateway/gateway-runtime/internal/runtime/runtime.go`:
- Around line 928-963: The code currently ensures topics and applies the binding
delta (brokerDriver.EnsureTopics, updater.ApplyBindingDelta,
brokerDriver.DeleteTopics) before constructing the new policy chains, so if
r.buildWebSubApiPolicyChains fails the system state has already changed; move
the call to r.buildWebSubApiPolicyChains(newWSB, vhost) (and capture subKey,
inKey, outKey, chChainKeys, err) to execute and validate before calling
brokerDriver.EnsureTopics or updater.ApplyBindingDelta, then proceed with
EnsureTopics/ApplyBindingDelta/DeleteTopics only after the chains are
successfully built (or alternatively implement an explicit rollback path that
reverts ApplyBindingDelta/DeleteTopics if buildWebSubApiPolicyChains fails).
- Around line 906-957: Snapshot the necessary state under r.mu (receiver from
r.activeReceivers[oldWSB.Name], assert it implements webSubBindingUpdater,
brokerDriver from r.activeBrokerDrivers[oldWSB.Name], oldBinding :=
r.hub.GetBinding(oldWSB.Name], and the addedChannels/removedChannels computed
from webSubChannelTopicMap/diffChannelTopics), then release r.mu before calling
any blocking external methods (brokerDriver.EnsureTopics,
updater.ApplyBindingDelta, brokerDriver.DeleteTopics); after those calls
complete reacquire r.mu to validate current runtime state and commit the updated
binding state (or handle errors) so the long-running
EnsureTopics/ApplyBindingDelta/DeleteTopics do not execute while holding r.mu.
- Around line 980-981: Replace the direct call to
binding.WebSubApiSubscriptionTopic(...) with the instance helper
r.webSubSubscriptionSyncTopic(...) when building the entry for r.bindingTopics
so the stored topic matches what AddWebSubApiBinding recorded; specifically,
update the line that sets r.bindingTopics[newWSB.Name] =
webSubTopicList(newChannels, internalSubTopic) to pass
r.webSubSubscriptionSyncTopic(newWSB.Name, newWSB.Version) (instead of
binding.WebSubApiSubscriptionTopic(...)) so the webSubTopicList uses the
configured subscription-sync topic; ensure you keep the same arguments
(newWSB.Name, newWSB.Version) and the webSubTopicList usage unchanged.

In `@gateway/gateway-controller/pkg/api/handlers/websub_api_handler.go`:
- Around line 200-205: The handler sends a 404 JSON with c.JSON when
errors.Is(err, websubapi.ErrNotFound) is true but does not return, so execution
falls through into mapRenderError/generic error handling and attempts to write a
second response; update the branch in websub_api_handler.go (the block that
checks websubapi.ErrNotFound) to immediately return after calling c.JSON to stop
further processing and avoid duplicate responses.

In `@gateway/gateway-controller/pkg/service/websubapi/service.go`:
- Around line 155-188: RenderSpec currently mutates existing (via
templateengine.RenderSpec(existing,...)) and you then call
s.db.UpdateConfig(existing), which can persist resolved secret values; instead
call templateengine.RenderSpec on a deep copy of existing (create a copy of the
config struct before rendering), use that copy to obtain renderedConfig and to
run s.validator.Validate and s.validateArtifactConflicts, and only persist the
original unresolved existing (after updating non-secret fields like DisplayName,
Version, DesiredState, UpdatedAt, DeployedAt, CPSyncStatus) with
s.db.UpdateConfig; keep all secret resolution and renderedConfig usage confined
to the copied object so plaintext secrets are never written back to storage.
- Around line 115-120: The code allows an empty apiConfig.Metadata.Name which
later leads to persisting a blank handle; change the logic around the
HandleMismatchError check to first validate a non-empty mismatch (if
apiConfig.Metadata.Name != "" && apiConfig.Metadata.Name != params.Handle return
HandleMismatchError) and then, if apiConfig.Metadata.Name == "", set
apiConfig.Metadata.Name = params.Handle before assigning into
existing.Configuration and existing.SourceConfiguration; apply the same fix at
the second occurrence (around the code referenced at lines 152-153) so the
persisted source config always has the correct default handle while still
rejecting explicit mismatches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 532e0778-3d9b-4f9c-a1c0-9c833f93614f

📥 Commits

Reviewing files that changed from the base of the PR and between 6d4ca3a and 7de2034.

📒 Files selected for processing (9)
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/delta.go
  • event-gateway/gateway-runtime/internal/runtime/runtime.go
  • event-gateway/gateway-runtime/internal/xdsclient/handler.go
  • event-gateway/gateway-runtime/internal/xdsclient/handler_test.go
  • gateway/gateway-controller/pkg/api/handlers/handlers.go
  • gateway/gateway-controller/pkg/api/handlers/handlers_test.go
  • gateway/gateway-controller/pkg/api/handlers/websub_api_handler.go
  • gateway/gateway-controller/pkg/service/websubapi/errors.go
  • gateway/gateway-controller/pkg/service/websubapi/service.go

Comment thread event-gateway/gateway-runtime/internal/runtime/runtime.go
Comment thread event-gateway/gateway-runtime/internal/runtime/runtime.go
Comment thread event-gateway/gateway-runtime/internal/runtime/runtime.go Outdated
Comment thread gateway/gateway-controller/pkg/api/handlers/websub_api_handler.go
Comment thread gateway/gateway-controller/pkg/service/websubapi/service.go
Comment thread gateway/gateway-controller/pkg/service/websubapi/service.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
event-gateway/gateway-runtime/internal/connectors/receiver/websub/connector.go (1)

140-174: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unguarded reads of e.channel.Channels outside HTTP handlers create a race condition.

Verification confirms that ApplyBindingDelta correctly acquires channelMu.Lock() around all mutations (lines 62–70 in delta.go). However, the following reads lack synchronization and will race with concurrent ApplyBindingDelta calls:

  • Start() ranges over e.channel.Channels at lines 143 and 169 without holding channelMu.RLock().
  • reconcileSubscriptions() builds the ownedChannels map at lines 197–200 without the lock.
  • The reconciler callback at line 214 accesses e.channel.Channels[sub.Topic] from a background goroutine without holding channelMu.RLock().

Since ApplyBindingDelta is called asynchronously via UpdateWebSubApiBinding (runtime.go line 950), the reconciler callback and writer can execute concurrently, causing a data race. Guard all reads with channelMu.RLock() to prevent this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@event-gateway/gateway-runtime/internal/connectors/receiver/websub/connector.go`
around lines 140 - 174, Reads of e.channel.Channels in Start(),
reconcileSubscriptions(), and the reconciler callback are not synchronized and
can race with ApplyBindingDelta which uses channelMu.Lock(); fix by wrapping all
reads with channelMu.RLock()/RUnlock() (use defer for unlock) — specifically,
acquire channelMu.RLock() around the loop in WebSubReceiver.Start() that ranges
e.channel.Channels, around the ownedChannels map construction inside
reconcileSubscriptions(), and inside the reconciler callback before accessing
e.channel.Channels[sub.Topic]; keep the lock scope minimal and mirror the
existing channelMu naming used by ApplyBindingDelta.
♻️ Duplicate comments (1)
gateway/gateway-controller/pkg/service/websubapi/service.go (1)

159-161: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a deep copy for render/validate isolation, not a shallow struct copy.

renderedExisting := *existing is shallow. If rendering mutates nested/interface-backed data in place, resolved values can leak back into existing and then be persisted by UpdateConfig(existing). Please deep-copy configuration payloads before RenderSpec.

Based on learnings: For secret handling during API deployment in the gateway-controller codebase, follow the intended design “resolve at runtime, persist unresolved.” Never persist or write resolvedCfg (plaintext secrets) to storage or logs.

Also applies to: 191-191

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/service/websubapi/service.go` around lines 159
- 161, The code uses a shallow copy renderedExisting := *existing before calling
templateengine.RenderSpec, which can allow in-place mutations of nested or
interface-backed fields to leak plaintext secrets back into existing and be
persisted by UpdateConfig; instead create a true deep copy of the configuration
payload (e.g., via a canonical deep-clone helper/JSON marshal-unmarshal or an
existing Clone/DeepCopy utility) and pass that deep-copied value to
templateengine.RenderSpec with s.secretResolver and log so resolved secrets
never mutate the original existing object or get passed to
UpdateConfig(existing); ensure the same deep-copy fix is applied at the other
occurrence around line 191.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-controller/pkg/service/websubapi/service.go`:
- Around line 108-109: params.Logger is used directly (log := params.Logger)
which can be nil and cause panics when calling log.Error/log.Info; change the
initialization to use a safe fallback (e.g., if params.Logger == nil { log =
s.logger } or fallback to slog.Default()) before any use. Update the three
occurrences around the file (the assignment at "log := params.Logger" and the
similar uses at the blocks mentioned near lines 192-193 and 206-210) to ensure
log is non-nil by defaulting to s.logger (or slog.Default()) so subsequent calls
like log.Error/log.Info are safe.

---

Outside diff comments:
In
`@event-gateway/gateway-runtime/internal/connectors/receiver/websub/connector.go`:
- Around line 140-174: Reads of e.channel.Channels in Start(),
reconcileSubscriptions(), and the reconciler callback are not synchronized and
can race with ApplyBindingDelta which uses channelMu.Lock(); fix by wrapping all
reads with channelMu.RLock()/RUnlock() (use defer for unlock) — specifically,
acquire channelMu.RLock() around the loop in WebSubReceiver.Start() that ranges
e.channel.Channels, around the ownedChannels map construction inside
reconcileSubscriptions(), and inside the reconciler callback before accessing
e.channel.Channels[sub.Topic]; keep the lock scope minimal and mirror the
existing channelMu naming used by ApplyBindingDelta.

---

Duplicate comments:
In `@gateway/gateway-controller/pkg/service/websubapi/service.go`:
- Around line 159-161: The code uses a shallow copy renderedExisting :=
*existing before calling templateengine.RenderSpec, which can allow in-place
mutations of nested or interface-backed fields to leak plaintext secrets back
into existing and be persisted by UpdateConfig; instead create a true deep copy
of the configuration payload (e.g., via a canonical deep-clone helper/JSON
marshal-unmarshal or an existing Clone/DeepCopy utility) and pass that
deep-copied value to templateengine.RenderSpec with s.secretResolver and log so
resolved secrets never mutate the original existing object or get passed to
UpdateConfig(existing); ensure the same deep-copy fix is applied at the other
occurrence around line 191.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c557a3d4-39af-4760-9447-af3126938eec

📥 Commits

Reviewing files that changed from the base of the PR and between 7de2034 and 8a49616.

📒 Files selected for processing (6)
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/connector.go
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/delta.go
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/handler.go
  • event-gateway/gateway-runtime/internal/runtime/runtime.go
  • gateway/gateway-controller/pkg/api/handlers/websub_api_handler.go
  • gateway/gateway-controller/pkg/service/websubapi/service.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • gateway/gateway-controller/pkg/api/handlers/websub_api_handler.go
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/delta.go
  • event-gateway/gateway-runtime/internal/runtime/runtime.go

Comment thread gateway/gateway-controller/pkg/service/websubapi/service.go
senthuran16 and others added 3 commits May 11, 2026 11:59
…tition

Make the compacted topic's partition/replication settings configurable

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
event-gateway/gateway-runtime/internal/config/config.go (1)

304-309: ⚡ Quick win

Add upper-bound checks in runtime validation for fail-fast behavior.

validateKafkaConfig now checks only > 0. Adding max checks here (matching connector validation) prevents oversized values from passing config load and failing later in runtime flows.

Proposed patch
@@
 import (
 	"fmt"
 	"log/slog"
+	"math"
 	"os"
 	"strconv"
 	"strings"
@@
 	if kafkaCfg.CompactTopicPartitions <= 0 {
 		return fmt.Errorf("kafka.compact_topic_partitions must be a positive integer, got %d", kafkaCfg.CompactTopicPartitions)
 	}
+	if kafkaCfg.CompactTopicPartitions > math.MaxInt32 {
+		return fmt.Errorf("kafka.compact_topic_partitions must be <= %d, got %d", math.MaxInt32, kafkaCfg.CompactTopicPartitions)
+	}
 	if kafkaCfg.CompactTopicReplicationFactor <= 0 {
 		return fmt.Errorf("kafka.compact_topic_replication_factor must be a positive integer, got %d", kafkaCfg.CompactTopicReplicationFactor)
 	}
+	if kafkaCfg.CompactTopicReplicationFactor > math.MaxInt16 {
+		return fmt.Errorf("kafka.compact_topic_replication_factor must be <= %d, got %d", math.MaxInt16, kafkaCfg.CompactTopicReplicationFactor)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@event-gateway/gateway-runtime/internal/config/config.go` around lines 304 -
309, validateKafkaConfig currently only enforces lower bounds for
CompactTopicPartitions and CompactTopicReplicationFactor; add matching
upper-bound checks (same maxima used by the connector) so oversized values fail
fast during config load. Modify the validation in validateKafkaConfig to verify
kafkaCfg.CompactTopicPartitions and kafkaCfg.CompactTopicReplicationFactor are
within the allowed range (e.g., 1 <= value <= MAX) and return fmt.Errorf with a
clear message referencing the field when the value exceeds the maximum; use the
existing field names kafkaCfg.CompactTopicPartitions and
kafkaCfg.CompactTopicReplicationFactor to locate the checks and mirror the
connector's limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@event-gateway/gateway-runtime/internal/connectors/receiver/websub/consumer_manager.go`:
- Around line 221-224: The consumerGroupID change widened the hex slice from 16
to 32 characters and will change IDs for all existing callbacks; revert to the
previous ID format or add a compatibility fallback: in consumerGroupID (using
cm.groupPrefix and sha256.Sum256) generate the original ID
(hex.EncodeToString(h[:])[:16]) to preserve existing groups, or implement a
deterministic dual-check/migration path that can look up both the old ID and the
new ID formats so existing consumer state/offsets are not lost during rollout.

---

Nitpick comments:
In `@event-gateway/gateway-runtime/internal/config/config.go`:
- Around line 304-309: validateKafkaConfig currently only enforces lower bounds
for CompactTopicPartitions and CompactTopicReplicationFactor; add matching
upper-bound checks (same maxima used by the connector) so oversized values fail
fast during config load. Modify the validation in validateKafkaConfig to verify
kafkaCfg.CompactTopicPartitions and kafkaCfg.CompactTopicReplicationFactor are
within the allowed range (e.g., 1 <= value <= MAX) and return fmt.Errorf with a
clear message referencing the field when the value exceeds the maximum; use the
existing field names kafkaCfg.CompactTopicPartitions and
kafkaCfg.CompactTopicReplicationFactor to locate the checks and mirror the
connector's limits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d7c6a8b-6d0e-4b6e-9cc3-75cac4fd8a54

📥 Commits

Reviewing files that changed from the base of the PR and between 8a49616 and 2fcdc92.

📒 Files selected for processing (7)
  • event-gateway/gateway-runtime/configs/config.toml
  • event-gateway/gateway-runtime/internal/config/config.go
  • event-gateway/gateway-runtime/internal/connectors/brokerdriver/kafka/config.go
  • event-gateway/gateway-runtime/internal/connectors/brokerdriver/kafka/config_test.go
  • event-gateway/gateway-runtime/internal/connectors/brokerdriver/kafka/endpoint.go
  • event-gateway/gateway-runtime/internal/connectors/receiver/websub/consumer_manager.go
  • gateway/gateway-controller/pkg/api/handlers/handlers_test.go
✅ Files skipped from review due to trivial changes (1)
  • event-gateway/gateway-runtime/configs/config.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-controller/pkg/api/handlers/handlers_test.go

Comment on lines +221 to +224
// Format: {prefix}-websub-{sha256(callbackURL)[:32]}
func (cm *ConsumerManager) consumerGroupID(callbackURL string) string {
h := sha256.Sum256([]byte(callbackURL))
return cm.groupPrefix + "-websub-" + hex.EncodeToString(h[:])[:16]
return cm.groupPrefix + "-websub-" + hex.EncodeToString(h[:])[:32]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve consumer-group ID compatibility to avoid state reset

Changing the hash slice from 16 to 32 at Line 224 changes consumer group IDs for all existing callbacks. That can break continuity of consumer state/offset tracking after rollout. Please keep the previous ID format (or add an explicit migration/fallback path) so existing groups remain stable across upgrades.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@event-gateway/gateway-runtime/internal/connectors/receiver/websub/consumer_manager.go`
around lines 221 - 224, The consumerGroupID change widened the hex slice from 16
to 32 characters and will change IDs for all existing callbacks; revert to the
previous ID format or add a compatibility fallback: in consumerGroupID (using
cm.groupPrefix and sha256.Sum256) generate the original ID
(hex.EncodeToString(h[:])[:16]) to preserve existing groups, or implement a
deterministic dual-check/migration path that can look up both the old ID and the
new ID formats so existing consumer state/offsets are not lost during rollout.

@senthuran16

Copy link
Copy Markdown
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants