Skip to content

Make BindingServiceProperties.bindings thread-safe and case-insensitive - #3322

Open
AkashB23 wants to merge 1 commit into
spring-cloud:mainfrom
AkashB23:gh-bindings-thread-safe
Open

AkashB23 wants to merge 1 commit into
spring-cloud:mainfrom
AkashB23:gh-bindings-thread-safe

Conversation

@AkashB23

@AkashB23 AkashB23 commented Nov 2, 2026 •

Copy link
Copy Markdown

Fixes #3321

BindingServiceProperties.bindings became a plain TreeMap again in GH-3158, which dropped the thread safety added by #3011. The map is written on read (bindIfNecessary -> bindToDefault -> bindings.put) by StreamBridge, BindingService and the binders, from many threads and without a lock, so concurrent first use of binding names corrupts it (lost entries, size() not matching the iteration, a NullPointerException in TreeMap.rotateRight, or a thread spinning forever in TreeMap.successor). Details, the history of the field and the affected versions are in #3321.

This changes the map to new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER):

#3076 replaced the ConcurrentSkipListMap for performance, but the reporter saw no improvement with the ConcurrentHashMap build and traced the slowdown to a Spring Boot child-context change and a misplaced output-bindings property, so the map was not shown to be the cause (see #3321 for the details and a measurement of the per-lookup cost).

Tests

BindingServiceTests#concurrentFirstUseOfBindingsKeepsTheMapConsistent (new): 16 threads call getBindingProperties for the same 1000 new names in different orders, for 5 rounds, and the test checks that nothing hangs or throws and that no entry is lost.

I ran this method in two batches of 30 against the unmodified class, where it failed 29 and 27 times (mostly NullPointerExceptions from the corrupted tree, otherwise lost entries or a thread that never finished), and 30 times against the patched class, where it passed every time. I did that with the 5.0.2 jars outside Maven, because I could not run the Maven build of main locally (its snapshot parent POM was not resolvable for me). So I have not run the rest of the module's tests, including ExplicitBindingTests, which GH-3158 touched; I am relying on CI for those.

spring-cloudGH-3158 changed the bindings map back to a TreeMap with a case-insensitive
comparator to restore case-insensitive binding names. That also dropped the
thread safety added by spring-cloudgh-3011 and kept by spring-cloudgh-3076.

The map is mutated on read: getBindingDestination(), getBindingProperties(),
getProducerProperties() and getConsumerProperties() add a missing binding on
first use (bindIfNecessary -> bindToDefault -> bindings.put). StreamBridge,
BindingService and the binders call these from many threads without any
lock. Concurrent first use of binding names corrupts the red-black tree:
entries are lost, size() no longer matches the iteration, a
NullPointerException is thrown from TreeMap.rotateRight (the failure
reported in spring-cloudgh-3011) or a thread spins forever in TreeMap.successor while
holding whatever monitor its caller holds, which stalls every thread that
needs the bindings.

Use a ConcurrentSkipListMap with String.CASE_INSENSITIVE_ORDER. It keeps
what spring-cloudGH-3158 needs (case-insensitive lookup, original key casing, sorted
iteration, same as the TreeMap) and is safe for concurrent use. A
ConcurrentHashMap cannot be used because it is case-sensitive.

The ConcurrentSkipListMap of spring-cloudgh-3011 was replaced by spring-cloudgh-3076 because of
slower start-up. That issue's thread does not show the map as the cause: the
reporter saw no improvement with the ConcurrentHashMap build and traced the
slowdown to a Spring Boot change in how child contexts are created, plus
binding properties placed under spring.cloud.config instead of
spring.cloud.stream, which made every first getBinder() call take the
bindToDefault() path.

The new test, in BindingServiceTests, fails on the TreeMap (entries are lost or
the map throws) and passes with this change.

Fixes spring-cloudgh-3321
See spring-cloudgh-3011, spring-cloudgh-3076, spring-cloudgh-3158

Signed-off-by: Akash Bhogar <55949790+AkashB23@users.noreply.github.com>
@AkashB23
AkashB23 force-pushed the gh-bindings-thread-safe branch from e9ac12e to 5d66d26 Compare November 2, 2026 09:30
@AkashB23

AkashB23 commented Nov 2, 2026

Copy link
Copy Markdown
Author

@olegz can you please check

This branch has not been deployed

No deployments
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.

BindingServiceProperties.bindings regressed to a non-thread-safe TreeMap in GH-3158: concurrent first use of binding names corrupts it

1 participant