net.oneandone.reactive.sse.servlet.SseWriteableChannel Maven / Gradle / Ivy
/*
* Copyright 1&1 Internet AG, https://github.com/1and1/
*
* Licensed 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 net.oneandone.reactive.sse.servlet;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import net.oneandone.reactive.sse.ServerSentEvent;
import com.google.common.collect.Lists;
class SseWriteableChannel {
private static final List> whenWritePossibles = Lists.newArrayList();
private final ServletOutputStream out;
private final Consumer errorConsumer;
public SseWriteableChannel(ServletOutputStream out, Consumer errorConsumer) {
this(out, errorConsumer, Duration.ZERO, null);
}
public SseWriteableChannel(ServletOutputStream out, Consumer errorConsumer, Duration keepAlivePeriod, ScheduledExecutorService executor) {
this.errorConsumer = errorConsumer;
this.out = out;
out.setWriteListener(new ServletWriteListener());
if (executor != null) {
// start the keep alive emitter
new KeepAliveEmitter(this, keepAlivePeriod, executor).start();
}
// write http header, implicitly
requestWriteNotificationAsync().thenAccept(Void -> flush());
}
public CompletableFuture writeEventAsync(ServerSentEvent event) {
CompletableFuture writtenFuture = new CompletableFuture<>();
requestWriteNotificationAsync().thenAccept(Void -> writeToWrite(event, writtenFuture));
return writtenFuture;
}
private void writeToWrite(ServerSentEvent event, CompletableFuture writtenSizeFuture) {
try {
synchronized (out) {
byte[] data = event.toWire().getBytes("UTF-8");
out.write(data);
out.flush();
writtenSizeFuture.complete(data.length);
}
} catch (IOException | RuntimeException t) {
errorConsumer.accept(t);
writtenSizeFuture.completeExceptionally(t);
}
}
private CompletableFuture requestWriteNotificationAsync() {
CompletableFuture whenWritePossible = new CompletableFuture<>();
synchronized (whenWritePossibles) {
if (isWritePossible()) {
whenWritePossible.complete(true);
} else {
// if not the WriteListener#onWritePossible will be called by the servlet container later
whenWritePossibles.add(whenWritePossible);
}
}
return whenWritePossible;
}
private boolean isWritePossible() {
// triggers that write listener's onWritePossible will be called, if is possible to write data
// According to the Servlet 3.1 spec the onWritePossible will be invoked if and only if isReady()
// method has been called and has returned false.
//
// Unfortunately the Servlet 3.1 spec left it open how many bytes can be written
// Jetty for instance keeps a reference to the passed byte array and essentially owns it until the write is complete
try {
return out.isReady();
} catch (IllegalStateException ise) {
return false;
}
}
private void flush() {
try {
out.flush();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
public void close() {
try {
out.close();
} catch (IOException ignore) { }
}
private final class ServletWriteListener implements WriteListener {
@Override
public void onWritePossible() throws IOException {
synchronized (whenWritePossibles) {
whenWritePossibles.forEach(whenWritePossible -> whenWritePossible.complete(null));
whenWritePossibles.clear();
}
}
@Override
public void onError(Throwable t) {
errorConsumer.accept(t);
}
}
/**
* sents keep alive messages to keep the http connection alive in case of idling
* @author grro
*/
private static final class KeepAliveEmitter {
private final SseWriteableChannel channel;
private final Duration keepAlivePeriod;
private final ScheduledExecutorService executor;
public KeepAliveEmitter(SseWriteableChannel channel, Duration keepAlivePeriod, ScheduledExecutorService executor) {
this.channel = channel;
this.keepAlivePeriod = keepAlivePeriod;
this.executor = executor;
}
public void start() {
scheduleNextKeepAliveEvent();
}
private void scheduleNextKeepAliveEvent() {
Runnable task = () -> channel.writeEventAsync(ServerSentEvent.newEvent().comment("keep alive"))
.thenAccept(numWritten -> scheduleNextKeepAliveEvent());
executor.schedule(task, keepAlivePeriod.getSeconds(), TimeUnit.SECONDS);
}
}
}