Skip to content

Commit c788554

Browse files
acktsapbclozel
authored andcommitted
Avoid thread pinning in SseEmitter, ResponseBodyEmitter
Closes gh-35423 Signed-off-by: Taeik Lim <[email protected]>
1 parent 9e8c640 commit c788554

File tree

2 files changed

+108
-55
lines changed

2 files changed

+108
-55
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.List;
2323
import java.util.Set;
24+
import java.util.concurrent.locks.Lock;
25+
import java.util.concurrent.locks.ReentrantLock;
2426
import java.util.function.Consumer;
2527

2628
import org.springframework.http.MediaType;
@@ -62,6 +64,7 @@
6264
* @author Rossen Stoyanchev
6365
* @author Juergen Hoeller
6466
* @author Brian Clozel
67+
* @author Taeik Lim
6568
* @since 4.2
6669
*/
6770
public class ResponseBodyEmitter {
@@ -88,6 +91,8 @@ public class ResponseBodyEmitter {
8891

8992
private final DefaultCallback completionCallback = new DefaultCallback();
9093

94+
/** Guards access to write operations on the response. */
95+
protected final Lock writeLock = new ReentrantLock();
9196

9297
/**
9398
* Create a new ResponseBodyEmitter instance.
@@ -117,36 +122,48 @@ public Long getTimeout() {
117122
}
118123

119124

120-
synchronized void initialize(Handler handler) throws IOException {
121-
this.handler = handler;
122-
125+
void initialize(Handler handler) throws IOException {
126+
this.writeLock.lock();
123127
try {
124-
sendInternal(this.earlySendAttempts);
125-
}
126-
finally {
127-
this.earlySendAttempts.clear();
128-
}
128+
this.handler = handler;
129+
130+
try {
131+
sendInternal(this.earlySendAttempts);
132+
}
133+
finally {
134+
this.earlySendAttempts.clear();
135+
}
129136

130-
if (this.complete) {
131-
if (this.failure != null) {
132-
this.handler.completeWithError(this.failure);
137+
if (this.complete) {
138+
if (this.failure != null) {
139+
this.handler.completeWithError(this.failure);
140+
}
141+
else {
142+
this.handler.complete();
143+
}
133144
}
134145
else {
135-
this.handler.complete();
146+
this.handler.onTimeout(this.timeoutCallback);
147+
this.handler.onError(this.errorCallback);
148+
this.handler.onCompletion(this.completionCallback);
136149
}
137150
}
138-
else {
139-
this.handler.onTimeout(this.timeoutCallback);
140-
this.handler.onError(this.errorCallback);
141-
this.handler.onCompletion(this.completionCallback);
151+
finally {
152+
this.writeLock.unlock();
142153
}
143154
}
144155

145-
synchronized void initializeWithError(Throwable ex) {
146-
this.complete = true;
147-
this.failure = ex;
148-
this.earlySendAttempts.clear();
149-
this.errorCallback.accept(ex);
156+
void initializeWithError(Throwable ex) {
157+
this.writeLock.lock();
158+
try {
159+
this.complete = true;
160+
this.failure = ex;
161+
this.earlySendAttempts.clear();
162+
this.errorCallback.accept(ex);
163+
}
164+
finally {
165+
this.writeLock.unlock();
166+
}
150167
}
151168

152169
/**
@@ -183,22 +200,28 @@ public void send(Object object) throws IOException {
183200
* @throws IOException raised when an I/O error occurs
184201
* @throws java.lang.IllegalStateException wraps any other errors
185202
*/
186-
public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException {
203+
public void send(Object object, @Nullable MediaType mediaType) throws IOException {
187204
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
188205
(this.failure != null ? " with error: " + this.failure : ""));
189-
if (this.handler != null) {
190-
try {
191-
this.handler.send(object, mediaType);
192-
}
193-
catch (IOException ex) {
194-
throw ex;
206+
this.writeLock.lock();
207+
try {
208+
if (this.handler != null) {
209+
try {
210+
this.handler.send(object, mediaType);
211+
}
212+
catch (IOException ex) {
213+
throw ex;
214+
}
215+
catch (Throwable ex) {
216+
throw new IllegalStateException("Failed to send " + object, ex);
217+
}
195218
}
196-
catch (Throwable ex) {
197-
throw new IllegalStateException("Failed to send " + object, ex);
219+
else {
220+
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
198221
}
199222
}
200-
else {
201-
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
223+
finally {
224+
this.writeLock.unlock();
202225
}
203226
}
204227

@@ -211,10 +234,16 @@ public synchronized void send(Object object, @Nullable MediaType mediaType) thro
211234
* @throws java.lang.IllegalStateException wraps any other errors
212235
* @since 6.0.12
213236
*/
214-
public synchronized void send(Set<DataWithMediaType> items) throws IOException {
237+
public void send(Set<DataWithMediaType> items) throws IOException {
215238
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
216239
(this.failure != null ? " with error: " + this.failure : ""));
217-
sendInternal(items);
240+
this.writeLock.lock();
241+
try {
242+
sendInternal(items);
243+
}
244+
finally {
245+
this.writeLock.unlock();
246+
}
218247
}
219248

220249
private void sendInternal(Set<DataWithMediaType> items) throws IOException {
@@ -245,10 +274,16 @@ private void sendInternal(Set<DataWithMediaType> items) throws IOException {
245274
* to complete request processing. It should not be used after container
246275
* related events such as an error while {@link #send(Object) sending}.
247276
*/
248-
public synchronized void complete() {
249-
this.complete = true;
250-
if (this.handler != null) {
251-
this.handler.complete();
277+
public void complete() {
278+
this.writeLock.lock();
279+
try {
280+
this.complete = true;
281+
if (this.handler != null) {
282+
this.handler.complete();
283+
}
284+
}
285+
finally {
286+
this.writeLock.unlock();
252287
}
253288
}
254289

@@ -263,11 +298,17 @@ public synchronized void complete() {
263298
* container related events such as an error while
264299
* {@link #send(Object) sending}.
265300
*/
266-
public synchronized void completeWithError(Throwable ex) {
267-
this.complete = true;
268-
this.failure = ex;
269-
if (this.handler != null) {
270-
this.handler.completeWithError(ex);
301+
public void completeWithError(Throwable ex) {
302+
this.writeLock.lock();
303+
try {
304+
this.complete = true;
305+
this.failure = ex;
306+
if (this.handler != null) {
307+
this.handler.completeWithError(ex);
308+
}
309+
}
310+
finally {
311+
this.writeLock.unlock();
271312
}
272313
}
273314

@@ -276,8 +317,14 @@ public synchronized void completeWithError(Throwable ex) {
276317
* called from a container thread when an async request times out.
277318
* <p>As of 6.2, one can register multiple callbacks for this event.
278319
*/
279-
public synchronized void onTimeout(Runnable callback) {
280-
this.timeoutCallback.addDelegate(callback);
320+
public void onTimeout(Runnable callback) {
321+
this.writeLock.lock();
322+
try {
323+
this.timeoutCallback.addDelegate(callback);
324+
}
325+
finally {
326+
this.writeLock.unlock();
327+
}
281328
}
282329

283330
/**
@@ -287,8 +334,14 @@ public synchronized void onTimeout(Runnable callback) {
287334
* <p>As of 6.2, one can register multiple callbacks for this event.
288335
* @since 5.0
289336
*/
290-
public synchronized void onError(Consumer<Throwable> callback) {
291-
this.errorCallback.addDelegate(callback);
337+
public void onError(Consumer<Throwable> callback) {
338+
this.writeLock.lock();
339+
try {
340+
this.errorCallback.addDelegate(callback);
341+
}
342+
finally {
343+
this.writeLock.unlock();
344+
}
292345
}
293346

294347
/**
@@ -298,8 +351,14 @@ public synchronized void onError(Consumer<Throwable> callback) {
298351
* detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
299352
* <p>As of 6.2, one can register multiple callbacks for this event.
300353
*/
301-
public synchronized void onCompletion(Runnable callback) {
302-
this.completionCallback.addDelegate(callback);
354+
public void onCompletion(Runnable callback) {
355+
this.writeLock.lock();
356+
try {
357+
this.completionCallback.addDelegate(callback);
358+
}
359+
finally {
360+
this.writeLock.unlock();
361+
}
303362
}
304363

305364

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import java.util.Collections;
2222
import java.util.LinkedHashSet;
2323
import java.util.Set;
24-
import java.util.concurrent.locks.Lock;
25-
import java.util.concurrent.locks.ReentrantLock;
2624

2725
import org.springframework.http.HttpHeaders;
2826
import org.springframework.http.MediaType;
@@ -46,10 +44,6 @@ public class SseEmitter extends ResponseBodyEmitter {
4644

4745
private static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
4846

49-
/** Guards access to write operations on the response. */
50-
private final Lock writeLock = new ReentrantLock();
51-
52-
5347
/**
5448
* Create a new SseEmitter instance.
5549
*/

0 commit comments

Comments
 (0)