Skip to content

GH-3257: Preserve configured bindings when StreamBridge evicts a channel - #3266

Open
adityaanikam wants to merge 1 commit into
spring-cloud:mainfrom
adityaanikam:fix-streambridge-configured-bindings-3257
Open

GH-3257: Preserve configured bindings when StreamBridge evicts a channel#3266
adityaanikam wants to merge 1 commit into
spring-cloud:mainfrom
adityaanikam:fix-streambridge-configured-bindings-3257

Conversation

@adityaanikam

Copy link
Copy Markdown
Contributor

Fixes gh-3257

Problem

StreamBridge's channel cache evicts its eldest entry once it exceeds dynamic-destination-cache-size, and since 07eb699 the eviction also removes the binding from BindingServiceProperties:

protected boolean removeEldestEntry(Map.Entry<String, MessageChannel> eldest) {
    boolean remove = size() > bindingServiceProperties.getDynamicDestinationCacheSize();
    if (remove) {
        ...
        bindingServiceProperties.getBindings().remove(eldest.getKey());
        bindingService.unbindProducers(eldest.getKey());
    }
    return remove;
}

Removing bindings the bridge created for dynamic destinations is the point of that change, but the removal is unconditional, so it also discards bindings the application configured.

The consequence is not just a missing map entry. BindingServiceProperties#getBindingProperties recreates a missing entry and then defaults its destination to the binding name:

public BindingProperties getBindingProperties(String bindingName) {
    this.bindIfNecessary(bindingName);
    BindingProperties bindingProperties = this.bindings.get(bindingName);
    if (bindingProperties.getDestination() == null) {
        bindingProperties.setDestination(bindingName);
    }
    return bindingProperties;
}

So after eviction, a binding configured as

spring.cloud.stream.bindings.foo-out-0.destination: fooDestination

silently resolves to destination foo-out-0, and messages go there instead. Any application with more bindings than dynamic-destination-cache-size (default 10) hits this once enough distinct bindings are used, which matches the regression reported against 5.0.3.

Fix

Capture the binding names present when StreamBridge is constructed. At that point BindingServiceProperties has been bound from configuration and the bridge has not resolved any dynamic destination yet, so those names are exactly the application's own. Eviction then skips them:

if (!configuredBindingNames.contains(eldest.getKey())) {
    bindingServiceProperties.getBindings().remove(eldest.getKey());
}

Dynamic destinations are still removed, so the cleanup 07eb699 added keeps working. The set is a TreeSet with String.CASE_INSENSITIVE_ORDER to match the key semantics of the bindings map itself. unbindProducers is left as-is for every eviction; it predates the regression and is not implicated here.

Testing

configuredBindingsAreNotRemovedWithCache in StreamBridgeTests configures one binding with an explicit destination, sets the cache size to 1, then sends through enough further destinations to evict it, and asserts the configured binding and its destination survive.

Verified with a negative control: reverting only StreamBridge.java (keeping the test, and reinstalling the module so the test ran against the reverted code) fails it with

Expecting actual:
  {"b"=BindingProperties{destination=b,group=null,contentType=application/json}}
to contain key:
  "foo-out-0"

which is the reported symptom. The existing bindingsAreRemovedWithCache passes in both directions, confirming dynamic-binding cleanup is unaffected.

One note on running these: spring-cloud-stream-integration-tests is currently commented out of core/pom.xml, and the module does not compile as it stands — PollableSourceTests has an ambiguous assertThat overload and KotlinConfigurationTests cannot resolve KotlinTestConfiguration. Both are unrelated to this change and predate it. I ran the module directly with -f core/spring-cloud-stream-integration-tests/pom.xml, with those two files temporarily set aside, to execute the test above. I put the new test beside bindingsAreRemovedWithCache since it covers the same eviction path, but happy to move it if you would rather it live somewhere that currently runs in CI.

…victs a channel

StreamBridge caps its channel cache at dynamic-destination-cache-size and, on
eviction, removes the evicted binding from BindingServiceProperties so that
bindings it created for dynamic destinations do not accumulate. That removal is
unconditional, so it also discards bindings the application configured itself.

Once a configured binding is gone, the next call to
BindingServiceProperties#getBindingProperties recreates an empty entry and
defaults its destination to the binding name, so messages then go to the binding
name instead of the configured destination. An application with more bindings
than the cache size hits this as soon as enough distinct bindings are used.

Capture the binding names present when StreamBridge is constructed -- those are
the ones the application configured -- and skip only those during eviction.
Destinations resolved on demand are still removed, so the cleanup added in
07eb699 keeps working and bindingsAreRemovedWithCache passes unchanged.

Signed-off-by: adityaanikam <adityanikam9502@gmail.com>

@kdelay kdelay 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.

The fix and its negative control both reproduce here.

The new test lives in core/spring-cloud-stream-integration-tests, which core/pom.xml keeps commented out of the reactor (line 20), so the mvn clean install in ci-pr.yml never compiles or runs it. The guard sits outside the gate that would catch a re-break.

The map assertion is also a proxy for the reported symptom. Asserting delivery is closer: with the first message drained, output.receive(1000, "fooDestination") after a second bridge.send("foo-out-0", ...) returns null on a reverted StreamBridge, and the message with the fix.

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.

StreamBridge removing BindingProperties leads to wrong destination in ProvisioningProvider

2 participants