Skip to content

Commit 82b4d8f

Browse files
authored
Merge pull request #1384 from seonwooj0810/fix/issue-1371-converters-init-race
Fix concurrent initialization race in FeignHttpMessageConverters
2 parents 99f73a0 + 4ad38de commit 82b4d8f

2 files changed

Lines changed: 129 additions & 10 deletions

File tree

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpMessageConverters.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class FeignHttpMessageConverters {
3636

3737
private final ObjectProvider<HttpMessageConverterCustomizer> cloudCustomizers;
3838

39-
private List<HttpMessageConverter<?>> converters;
39+
private volatile List<HttpMessageConverter<?>> converters;
4040

4141
public FeignHttpMessageConverters(ObjectProvider<ClientHttpMessageConvertersCustomizer> customizers,
4242
ObjectProvider<HttpMessageConverterCustomizer> cloudCustomizers) {
@@ -51,16 +51,23 @@ public List<HttpMessageConverter<?>> getConverters() {
5151

5252
private void initConvertersIfRequired() {
5353
if (this.converters == null) {
54-
this.converters = new ArrayList<>();
55-
HttpMessageConverters.ClientBuilder builder = HttpMessageConverters.forClient();
56-
// TODO: allow disabling of registerDefaults
57-
builder.registerDefaults();
58-
// TODO: check if already added? Howto order?
54+
synchronized (this) {
55+
if (this.converters == null) {
56+
List<HttpMessageConverter<?>> converters = new ArrayList<>();
57+
HttpMessageConverters.ClientBuilder builder = HttpMessageConverters.forClient();
58+
// TODO: allow disabling of registerDefaults
59+
builder.registerDefaults();
60+
// TODO: check if already added? Howto order?
5961

60-
this.customizers.orderedStream().forEach(customizer -> customizer.customize(builder));
61-
HttpMessageConverters hmc = builder.build();
62-
hmc.forEach(converter -> converters.add(converter));
63-
cloudCustomizers.forEach(customizer -> customizer.accept(this.converters));
62+
this.customizers.orderedStream().forEach(customizer -> customizer.customize(builder));
63+
HttpMessageConverters hmc = builder.build();
64+
hmc.forEach(converter -> converters.add(converter));
65+
cloudCustomizers.forEach(customizer -> customizer.accept(converters));
66+
// Publish only once fully built so concurrent callers never
67+
// observe a partially populated list.
68+
this.converters = converters;
69+
}
70+
}
6471
}
6572
}
6673

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2013-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.support;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.concurrent.CountDownLatch;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.atomic.AtomicReference;
25+
import java.util.stream.Stream;
26+
27+
import org.junit.jupiter.api.Test;
28+
29+
import org.springframework.beans.factory.ObjectProvider;
30+
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
31+
import org.springframework.http.converter.HttpMessageConverter;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
36+
37+
/**
38+
* Tests for {@link FeignHttpMessageConverters}.
39+
*
40+
* @author seonwoo_jung
41+
*/
42+
class FeignHttpMessageConvertersTests {
43+
44+
@Test
45+
void shouldNotExposePartiallyInitializedConvertersToConcurrentCallers() throws Exception {
46+
CountDownLatch initStarted = new CountDownLatch(1);
47+
CountDownLatch allowInitToFinish = new CountDownLatch(1);
48+
49+
// A customizer that blocks while the converter list is being built so that we can
50+
// observe what a second, concurrent caller sees mid-initialization.
51+
ClientHttpMessageConvertersCustomizer blockingCustomizer = builder -> {
52+
initStarted.countDown();
53+
try {
54+
allowInitToFinish.await();
55+
}
56+
catch (InterruptedException ex) {
57+
Thread.currentThread().interrupt();
58+
}
59+
};
60+
61+
@SuppressWarnings("unchecked")
62+
ObjectProvider<ClientHttpMessageConvertersCustomizer> customizers = mock(ObjectProvider.class);
63+
when(customizers.orderedStream()).thenReturn(Stream.of(blockingCustomizer));
64+
@SuppressWarnings("unchecked")
65+
ObjectProvider<HttpMessageConverterCustomizer> cloudCustomizers = mock(ObjectProvider.class);
66+
when(cloudCustomizers.iterator()).thenReturn(Collections.emptyIterator());
67+
68+
FeignHttpMessageConverters feignConverters = new FeignHttpMessageConverters(customizers, cloudCustomizers);
69+
70+
AtomicReference<List<HttpMessageConverter<?>>> initializerResult = new AtomicReference<>();
71+
AtomicReference<List<HttpMessageConverter<?>>> readerResult = new AtomicReference<>();
72+
AtomicBoolean readerObservedEmptyList = new AtomicBoolean();
73+
74+
Thread initializer = new Thread(() -> initializerResult.set(feignConverters.getConverters()),
75+
"converters-init");
76+
Thread reader = new Thread(() -> {
77+
List<HttpMessageConverter<?>> converters = feignConverters.getConverters();
78+
readerObservedEmptyList.set(converters.isEmpty());
79+
readerResult.set(converters);
80+
}, "converters-reader");
81+
82+
initializer.start();
83+
assertThat(initStarted.await(5, TimeUnit.SECONDS)).isTrue();
84+
85+
// The first caller is now stuck building the list. Start a concurrent caller and
86+
// wait until it has either blocked waiting for initialization to complete (fixed
87+
// behaviour) or already returned a value (buggy behaviour) before releasing.
88+
reader.start();
89+
waitUntilBlockedOrFinished(reader);
90+
allowInitToFinish.countDown();
91+
92+
initializer.join(TimeUnit.SECONDS.toMillis(5));
93+
reader.join(TimeUnit.SECONDS.toMillis(5));
94+
95+
assertThat(initializerResult.get()).isNotEmpty();
96+
assertThat(readerObservedEmptyList).as("concurrent caller observed an empty converter list").isFalse();
97+
assertThat(readerResult.get()).isSameAs(initializerResult.get());
98+
}
99+
100+
private static void waitUntilBlockedOrFinished(Thread thread) {
101+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
102+
while (System.nanoTime() < deadline) {
103+
Thread.State state = thread.getState();
104+
if (state == Thread.State.BLOCKED || state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING
105+
|| state == Thread.State.TERMINATED) {
106+
return;
107+
}
108+
Thread.yield();
109+
}
110+
}
111+
112+
}

0 commit comments

Comments
 (0)