org.apache.cxf.jaxrs.sse.SseEventSinkImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cxf-rt-rs-sse Show documentation
Show all versions of cxf-rt-rs-sse Show documentation
Apache CXF JAX-RS Server-Side Events Support
The newest version!
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cxf.jaxrs.sse;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.logging.Logger;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.ws.rs.container.AsyncResponse;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.sse.OutboundSseEvent;
import jakarta.ws.rs.sse.SseEventSink;
import org.apache.cxf.common.logging.LogUtils;
public class SseEventSinkImpl implements SseEventSink {
public static final String BUFFER_SIZE_PROPERTY = "org.apache.cxf.sse.sink.buffer.size";
private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation [] {};
private static final Logger LOG = LogUtils.getL7dLogger(SseEventSinkImpl.class);
private static final int DEFAULT_BUFFER_SIZE = 10000; // buffering 10000 messages
private final AsyncContext ctx;
private final MessageBodyWriter writer;
private final Queue buffer;
private final AtomicBoolean closed = new AtomicBoolean(false);
private final AtomicBoolean dispatching = new AtomicBoolean(false);
private final AtomicReference throwable = new AtomicReference<>();
private final AtomicBoolean completed = new AtomicBoolean(false);
private final int bufferSize;
/**
* Create new SseEventSink implementation with the default buffer size of 10000
* SSE events.
*
* @param writer message body writer
* @param async asynchronous response
* @param ctx asynchronous context
*/
public SseEventSinkImpl(final MessageBodyWriter writer,
final AsyncResponse async, final AsyncContext ctx) {
this(writer, async, ctx, DEFAULT_BUFFER_SIZE);
}
/**
* Create new SseEventSink implementation with the configurable SSE events buffer
* size.
*
* @param writer message body writer
* @param async asynchronous response
* @param ctx asynchronous context
* @param bufferSize SSE events buffer size
*/
public SseEventSinkImpl(final MessageBodyWriter writer,
final AsyncResponse async, final AsyncContext ctx, final int bufferSize) {
this.writer = writer;
this.buffer = new ArrayBlockingQueue<>(bufferSize);
this.ctx = ctx;
this.bufferSize = bufferSize;
if (ctx == null) {
throw new IllegalStateException("Unable to retrieve the AsyncContext for this request. "
+ "Is the Servlet configured properly?");
}
ctx.getResponse().setContentType(OutboundSseEventBodyWriter.SERVER_SENT_EVENTS);
ctx.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
// This callback should be called when dequeue() has encountered an
// error during the execution and is forced to complete the context.
close();
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
}
@Override
public void onError(AsyncEvent event) throws IOException {
// In case of Tomcat, the context is closed automatically when client closes
// the connection.
if (throwable.get() != null || throwable.compareAndSet(null, event.getThrowable())) {
// This callback should be called when dequeue() has encountered an
close();
}
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
}
});
}
public AsyncContext getAsyncContext() {
return ctx;
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
LOG.fine("Closing SSE sink now");
// In case we are still dispatching, give the events the chance to be
// sent over to the consumers. The good example would be sent(event) call,
// immediately followed by the close() call.
if (!awaitQueueToDrain(5, TimeUnit.SECONDS)) {
LOG.warning("There are still SSE events the queue which may not be delivered (closing now)");
}
if (completed.compareAndSet(false, true)) {
try {
// In case of Tomcat, the context may be already closed (f.e. due to error),
// in this case request is set to null.
if (ctx.getRequest() != null) {
LOG.fine("Completing the AsyncContext");
ctx.complete();
}
} catch (final IllegalStateException ex) {
LOG.fine("Failed to close the AsyncContext cleanly: " + ex.getMessage());
}
}
// Complete all the accepted but not dispatched send request with the
// error (if any) or signal that sink has been closed already.
Throwable ex = throwable.get();
if (ex == null) {
ex = new IllegalStateException("The sink has been already closed");
}
QueuedEvent queuedEvent = buffer.poll();
while (queuedEvent != null) {
queuedEvent.completion.completeExceptionally(ex);
queuedEvent = buffer.poll();
}
}
}
private boolean awaitQueueToDrain(int timeout, TimeUnit unit) {
final long parkTime = unit.toNanos(timeout) / 20;
int attempt = 0;
while (dispatching.get() && ++attempt < 20) {
LockSupport.parkNanos(parkTime);
}
return buffer.isEmpty();
}
@Override
public boolean isClosed() {
return closed.get();
}
@Override
public CompletionStage> send(OutboundSseEvent event) {
final CompletableFuture> future = new CompletableFuture<>();
if (!closed.get() && writer != null) {
final Throwable ex = throwable.get();
if (ex != null) {
future.completeExceptionally(ex);
} else if (buffer.offer(new QueuedEvent(event, future))) {
if (dispatching.compareAndSet(false, true)) {
ctx.start(this::dequeue);
}
} else {
future.completeExceptionally(new IllegalStateException("The buffer is full ("
+ bufferSize + "), unable to queue SSE event for send. Please use '"
+ BUFFER_SIZE_PROPERTY + "' property to increase the limit."));
}
} else {
future.completeExceptionally(new IllegalStateException(
"The sink is already closed, unable to queue SSE event for send"));
}
return future;
}
/**
* Processes the buffered events and sends the off to the output channel. There is
* a special handling for the IOException, which forces the sink to switch to closed
* state:
* - when the IOException is detected, the AsyncContext is forcebly closed (unless
* it is already closed like in case of the Tomcat)
* - all unsent events are completed exceptionally
* - all unscheduled events are completed exceptionally (see please close() method)
*
*/
private void dequeue() {
Throwable error = throwable.get();
try {
while (true) {
final QueuedEvent queuedEvent = buffer.poll();
// Nothing queued, release the thread
if (queuedEvent == null) {
break;
}
final OutboundSseEvent event = queuedEvent.event;
final CompletableFuture> future = queuedEvent.completion;
try {
if (error == null) {
LOG.fine("Dispatching SSE event over the wire");
writer.writeTo(event, event.getClass(), event.getGenericType(), EMPTY_ANNOTATIONS,
event.getMediaType(), null, ctx.getResponse().getOutputStream());
ctx.getResponse().flushBuffer();
LOG.fine("Completing the future successfully");
future.complete(null);
} else {
LOG.fine("Completing the future unsuccessfully (error enountered previously)");
future.completeExceptionally(error);
}
} catch (final Exception ex) {
// Very likely the connection is closed by the client (but we cannot
// detect if for sure, container-specific).
if (ex instanceof IOException) {
error = (IOException)ex;
}
LOG.fine("Completing the future unsuccessfully (error enountered)");
future.completeExceptionally(ex);
}
}
} finally {
final boolean shouldComplete = (error != null) && throwable.compareAndSet(null, error);
dispatching.set(false);
// Ideally, we should be rethrowing the exception here (error) and handle
// it inside the onError() callback. However, most of the servlet containers
// do not handle this case properly (and onError() is not called).
if (shouldComplete && completed.compareAndSet(false, true)) {
LOG.fine("Prematurely completing the AsyncContext due to error encountered: " + error);
// In case of Tomcat, the context is closed automatically when client closes
// the connection and onError callback will be called (in this case request
// is set to null).
try {
LOG.fine("Completing the AsyncContext");
// Older versions of Tomcat returned 'null', now the getRequest() throws
// IllegalStateException if it is 'null'.
if (ctx.getRequest() != null) {
ctx.complete();
}
} catch (final IllegalStateException ex) {
LOG.fine("Failed to close the AsyncContext cleanly: " + ex.getMessage());
}
}
}
}
private static class QueuedEvent {
private final OutboundSseEvent event;
private final CompletableFuture> completion;
QueuedEvent(OutboundSseEvent event, CompletableFuture> completion) {
this.event = event;
this.completion = completion;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy