Avoid thread pinning in SseEmitter, ResponseBodyEmitter
Closes gh-35423 Signed-off-by: Taeik Lim <sibera21@gmail.com>
This commit is contained in:
parent
9e8c64011d
commit
c788554b1d
|
@ -21,6 +21,8 @@ import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
@ -62,6 +64,7 @@ import org.springframework.util.ObjectUtils;
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
|
* @author Taeik Lim
|
||||||
* @since 4.2
|
* @since 4.2
|
||||||
*/
|
*/
|
||||||
public class ResponseBodyEmitter {
|
public class ResponseBodyEmitter {
|
||||||
|
@ -88,6 +91,8 @@ public class ResponseBodyEmitter {
|
||||||
|
|
||||||
private final DefaultCallback completionCallback = new DefaultCallback();
|
private final DefaultCallback completionCallback = new DefaultCallback();
|
||||||
|
|
||||||
|
/** Guards access to write operations on the response. */
|
||||||
|
protected final Lock writeLock = new ReentrantLock();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new ResponseBodyEmitter instance.
|
* Create a new ResponseBodyEmitter instance.
|
||||||
|
@ -117,36 +122,48 @@ public class ResponseBodyEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
synchronized void initialize(Handler handler) throws IOException {
|
void initialize(Handler handler) throws IOException {
|
||||||
this.handler = handler;
|
this.writeLock.lock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendInternal(this.earlySendAttempts);
|
this.handler = handler;
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.earlySendAttempts.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.complete) {
|
try {
|
||||||
if (this.failure != null) {
|
sendInternal(this.earlySendAttempts);
|
||||||
this.handler.completeWithError(this.failure);
|
}
|
||||||
|
finally {
|
||||||
|
this.earlySendAttempts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.complete) {
|
||||||
|
if (this.failure != null) {
|
||||||
|
this.handler.completeWithError(this.failure);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.handler.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.handler.complete();
|
this.handler.onTimeout(this.timeoutCallback);
|
||||||
|
this.handler.onError(this.errorCallback);
|
||||||
|
this.handler.onCompletion(this.completionCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
finally {
|
||||||
this.handler.onTimeout(this.timeoutCallback);
|
this.writeLock.unlock();
|
||||||
this.handler.onError(this.errorCallback);
|
|
||||||
this.handler.onCompletion(this.completionCallback);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void initializeWithError(Throwable ex) {
|
void initializeWithError(Throwable ex) {
|
||||||
this.complete = true;
|
this.writeLock.lock();
|
||||||
this.failure = ex;
|
try {
|
||||||
this.earlySendAttempts.clear();
|
this.complete = true;
|
||||||
this.errorCallback.accept(ex);
|
this.failure = ex;
|
||||||
|
this.earlySendAttempts.clear();
|
||||||
|
this.errorCallback.accept(ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,22 +200,28 @@ public class ResponseBodyEmitter {
|
||||||
* @throws IOException raised when an I/O error occurs
|
* @throws IOException raised when an I/O error occurs
|
||||||
* @throws java.lang.IllegalStateException wraps any other errors
|
* @throws java.lang.IllegalStateException wraps any other errors
|
||||||
*/
|
*/
|
||||||
public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException {
|
public void send(Object object, @Nullable MediaType mediaType) throws IOException {
|
||||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||||
(this.failure != null ? " with error: " + this.failure : ""));
|
(this.failure != null ? " with error: " + this.failure : ""));
|
||||||
if (this.handler != null) {
|
this.writeLock.lock();
|
||||||
try {
|
try {
|
||||||
this.handler.send(object, mediaType);
|
if (this.handler != null) {
|
||||||
|
try {
|
||||||
|
this.handler.send(object, mediaType);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
catch (Throwable ex) {
|
||||||
|
throw new IllegalStateException("Failed to send " + object, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
else {
|
||||||
throw ex;
|
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
|
||||||
}
|
|
||||||
catch (Throwable ex) {
|
|
||||||
throw new IllegalStateException("Failed to send " + object, ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
finally {
|
||||||
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
|
this.writeLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,10 +234,16 @@ public class ResponseBodyEmitter {
|
||||||
* @throws java.lang.IllegalStateException wraps any other errors
|
* @throws java.lang.IllegalStateException wraps any other errors
|
||||||
* @since 6.0.12
|
* @since 6.0.12
|
||||||
*/
|
*/
|
||||||
public synchronized void send(Set<DataWithMediaType> items) throws IOException {
|
public void send(Set<DataWithMediaType> items) throws IOException {
|
||||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||||
(this.failure != null ? " with error: " + this.failure : ""));
|
(this.failure != null ? " with error: " + this.failure : ""));
|
||||||
sendInternal(items);
|
this.writeLock.lock();
|
||||||
|
try {
|
||||||
|
sendInternal(items);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendInternal(Set<DataWithMediaType> items) throws IOException {
|
private void sendInternal(Set<DataWithMediaType> items) throws IOException {
|
||||||
|
@ -245,10 +274,16 @@ public class ResponseBodyEmitter {
|
||||||
* to complete request processing. It should not be used after container
|
* to complete request processing. It should not be used after container
|
||||||
* related events such as an error while {@link #send(Object) sending}.
|
* related events such as an error while {@link #send(Object) sending}.
|
||||||
*/
|
*/
|
||||||
public synchronized void complete() {
|
public void complete() {
|
||||||
this.complete = true;
|
this.writeLock.lock();
|
||||||
if (this.handler != null) {
|
try {
|
||||||
this.handler.complete();
|
this.complete = true;
|
||||||
|
if (this.handler != null) {
|
||||||
|
this.handler.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,11 +298,17 @@ public class ResponseBodyEmitter {
|
||||||
* container related events such as an error while
|
* container related events such as an error while
|
||||||
* {@link #send(Object) sending}.
|
* {@link #send(Object) sending}.
|
||||||
*/
|
*/
|
||||||
public synchronized void completeWithError(Throwable ex) {
|
public void completeWithError(Throwable ex) {
|
||||||
this.complete = true;
|
this.writeLock.lock();
|
||||||
this.failure = ex;
|
try {
|
||||||
if (this.handler != null) {
|
this.complete = true;
|
||||||
this.handler.completeWithError(ex);
|
this.failure = ex;
|
||||||
|
if (this.handler != null) {
|
||||||
|
this.handler.completeWithError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,8 +317,14 @@ public class ResponseBodyEmitter {
|
||||||
* called from a container thread when an async request times out.
|
* called from a container thread when an async request times out.
|
||||||
* <p>As of 6.2, one can register multiple callbacks for this event.
|
* <p>As of 6.2, one can register multiple callbacks for this event.
|
||||||
*/
|
*/
|
||||||
public synchronized void onTimeout(Runnable callback) {
|
public void onTimeout(Runnable callback) {
|
||||||
this.timeoutCallback.addDelegate(callback);
|
this.writeLock.lock();
|
||||||
|
try {
|
||||||
|
this.timeoutCallback.addDelegate(callback);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -287,8 +334,14 @@ public class ResponseBodyEmitter {
|
||||||
* <p>As of 6.2, one can register multiple callbacks for this event.
|
* <p>As of 6.2, one can register multiple callbacks for this event.
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public synchronized void onError(Consumer<Throwable> callback) {
|
public void onError(Consumer<Throwable> callback) {
|
||||||
this.errorCallback.addDelegate(callback);
|
this.writeLock.lock();
|
||||||
|
try {
|
||||||
|
this.errorCallback.addDelegate(callback);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -298,8 +351,14 @@ public class ResponseBodyEmitter {
|
||||||
* detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
|
* detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
|
||||||
* <p>As of 6.2, one can register multiple callbacks for this event.
|
* <p>As of 6.2, one can register multiple callbacks for this event.
|
||||||
*/
|
*/
|
||||||
public synchronized void onCompletion(Runnable callback) {
|
public void onCompletion(Runnable callback) {
|
||||||
this.completionCallback.addDelegate(callback);
|
this.writeLock.lock();
|
||||||
|
try {
|
||||||
|
this.completionCallback.addDelegate(callback);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
@ -46,10 +44,6 @@ public class SseEmitter extends ResponseBodyEmitter {
|
||||||
|
|
||||||
private static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
|
private static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
/** Guards access to write operations on the response. */
|
|
||||||
private final Lock writeLock = new ReentrantLock();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new SseEmitter instance.
|
* Create a new SseEmitter instance.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue