Skip to content

GH-3242: Key StreamBridge function cache by binding name - #3244

Open
kdelay wants to merge 1 commit into
spring-cloud:mainfrom
kdelay:fix/issue-3242-streambridge-cache-key
Open

GH-3242: Key StreamBridge function cache by binding name#3244
kdelay wants to merge 1 commit into
spring-cloud:mainfrom
kdelay:fix/issue-3242-streambridge-cache-key

Conversation

@kdelay

@kdelay kdelay commented Aug 8, 2026

Copy link
Copy Markdown

Fixes #3242

What

StreamBridge.send(..) caches the FunctionInvocationWrapper under a hash of the producer properties. The binding name only entered that hash when partitionKeyExpression and ProducerProperties#getBindingName() were both non-null, and getBindingName() is never populated on the instance StreamBridge reads: BindingService#bindProducer calls populateBindingName(..) on the extended copy it builds for an ExtendedPropertiesBinder, while StreamBridge reads the original from BindingServiceProperties#getProducerProperties(bindingName).

So a partitioned binding can share its cached function with another binding. PartitionAwareFunctionWrapper sets the partition enhancer on that shared function and only clears it when the result carries no scst_partition header, so after a successful partitioned send the enhancer stays on the cached function. The next send on the colliding binding then runs through PartitionHandler without a partition key and fails with IllegalArgumentException: Partition key cannot be null.

The hash also summed its components, so Boolean.hashCode(true) + partitionCount and Boolean.hashCode(false) + partitionCount collide whenever the partitioned binding's count is 6 higher than the other one's.

Change

hashProducerProperties now takes the binding name from the send(..) argument, which is always available, and includes it for partitioned bindings. Non-partitioned bindings keep sharing one cached function, so the behaviour asserted by test_2783 is unchanged. Objects.hash replaces the summation so the components are mixed.

Verification

test_3242 in StreamBridgeTests reproduces the failure: a binding partitioned via partition-key-extractor-name (which leaves partitionKeyExpression null, so the old guard never added the binding name) with partition-count: 7, and a non-partitioned binding with partition-count: 1. On main the second send fails with Partition key cannot be null; with this change both sends succeed and only the partitioned message carries scst_partition.

  • ./mvnw -f core/pom.xml clean install (the build CI runs): BUILD SUCCESS.
  • StreamBridgeTests: 39/40 pass. test_3033 fails on main as well, before this change, and is unrelated (that module is currently commented out of the core reactor).
  • Whole spring-cloud-stream-integration-tests module: the set of failing tests is identical before and after this change.

StreamBridge caches the FunctionInvocationWrapper used by send(..) under a
hash of the producer properties. The binding name was only part of that hash
when partitionKeyExpression and ProducerProperties#getBindingName() were both
set, and getBindingName() is never populated on the instance StreamBridge
reads: BindingService#bindProducer populates it on the extended copy it
creates for an ExtendedPropertiesBinder, not on the original returned by
BindingServiceProperties#getProducerProperties.

A partitioned binding therefore shares its cached function with any other
binding whose properties hash to the same value, and since the partition
enhancer is left on the cached function after a send that produced a
partition header, the next send on the colliding binding fails with
IllegalArgumentException: Partition key cannot be null.

Take the binding name from the send(..) argument, which is always available,
and include it for partitioned bindings. Non-partitioned bindings keep
sharing a cached function as before. Use Objects.hash so that the components
are mixed rather than summed, which is what let a partitioned binding with
partitionCount N collide with a non-partitioned one with partitionCount N-6.

Signed-off-by: kdelay <kdelay20@gmail.com>
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's hashProducerProperties produces hash collisions across different binding names, causing Partition key cannot be null

1 participant