pl.morgwai.base.concurrent.OrderedConcurrentOutputBuffer Maven / Gradle / Ivy
// Copyright (c) Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0
package pl.morgwai.base.concurrent;
import java.util.LinkedList;
import java.util.List;
/**
* Buffers messages until all of those that should be written before to the output are available,
* so that they all will be written in the correct order.
* Useful for processing input streams in several concurrent threads when the order of response
* messages must reflect the order of request messages.
*
* A buffer consists of ordered buckets. Each bucket implements {@link OutputStream} just as the
* underlying output stream passed to
* {@link #OrderedConcurrentOutputBuffer(OutputStream) the constructor}.
* Each bucket gets flushed automatically to the output stream after all the previous buckets are
* {@link OutputStream#close() closed}. A user can {@link #addBucket() add a new bucket} at the end
* of the buffer, {@link OutputStream#write(Object) write messages} to it and finally
* {@link OutputStream#close() close it} to indicate that no more messages will be written to it and
* trigger flushing of subsequent bucket(s).
* Within each bucket, messages will be written to the output in the order they were buffered.
* When it is known that no more buckets will be added, {@link #signalNoMoreBuckets()} should be
* called. After this, when all existing buckets are closed, the underlying output stream will be
* closed automatically.
*
* All bucket methods and {@link #signalNoMoreBuckets()} are thread-safe. {@link #addBucket()} is
* not thread-safe and concurrent invocations must be synchronized (in case of websockets and
* gRPC, it is usually not a problem as endpoints and request observers are guaranteed to be called
* by only 1 thread at a time).
*
* Note: this class should only be used if the response messages order requirement cannot be
* dropped: if you control a given stream API, then it's more efficient to add some unique id to
* request messages, include it in response messages and send them as soon as they are produced,
* so nothing needs to be buffered.
*/
public class OrderedConcurrentOutputBuffer {
public interface OutputStream {
void write(MessageT message);
void close();
}
final OutputStream output;
// A buffer always has a preallocated guard bucket at the tail of the queue. As addBucket() is
// synchronized on the tail, having a guard prevents addBucket() to be delayed if the last
// bucket handed out has a huge number of buffered messages and is just being flushed.
Bucket tailGuard;
boolean noMoreBuckets = false; // See signalNoMoreBuckets().
public OrderedConcurrentOutputBuffer(OutputStream outputStream) {
this.output = outputStream;
tailGuard = new Bucket();
tailGuard.buffer = null; // the first bucket is initially flushed.
}
/**
* Adds a new empty bucket at the end of this buffer. This method is not thread-safe.
* @return bucket placed right after the one returned by the previous call to this method (or
* the first one if this is the first call). All methods of the returned bucket are
* thread-safe.
* @throws IllegalStateException if {@link #signalNoMoreBuckets()} have been already called.
*/
public OutputStream addBucket() {
// the below synchronization does not guarantee thread-safety: if 2 threads that call
// addBucket() synchronize on the same tailGuard, they will branch the queue into 2.
// synchronization here is for memory consistency with a thread that may be flushing
// tailGuard at the same time.
synchronized (tailGuard.lock) {
if (noMoreBuckets) {
throw new IllegalStateException("noMoreBuckets has been already signaled");
}
// return the current tailGuard after adding a new one after it and
// updating the pointer
Bucket result = tailGuard;
tailGuard.next = new Bucket();
tailGuard = tailGuard.next;
return result;
}
}
/**
* Indicates that no more new buckets will be added. After a call to this method, when all
* existing buckets are closed, the underlying output stream will be closed automatically.
* This method is thread-safe.
*/
public void signalNoMoreBuckets() {
synchronized (tailGuard.lock) {
noMoreBuckets = true;
// tailGuard has no buffer => it's flushed => all previous buckets closed & flushed
if (tailGuard.buffer == null) output.close();
}
}
// A list of messages that will have a well defined position relatively to other buckets within
// the output stream. All methods are thread-safe.
class Bucket implements OutputStream {
final Object lock = new Object(); // all bucket methods are synchronized on this lock
List buffer = new LinkedList<>();// null <=> flushed <=> all previous also flushed
boolean closed = false;
Bucket next; // null <=> this is the tailGuard
// (buffer == null && ! closed) <=> this is the current head bucket (first unclosed one)
// Appends message to the end of this bucket.
// If this is the head bucket (first unclosed one), then the message will be written
// directly to the output stream. Otherwise it will be buffered in this bucket until all the
// previous buckets are closed and flushed.
@Override
public void write(MessageT message) {
synchronized (lock) {
if (closed) throw new IllegalStateException(BUCKET_CLOSED_MESSAGE);
if (buffer == null) {
output.write(message);
} else {
buffer.add(message);
}
}
}
// Marks this bucket as closed.
// If this is the head bucket (the first unclosed one), then flushes all buffered messages
// from the subsequent buckets that can be sent now. Specifically, a continuous chain of
// subsequent closed buckets and the first unclosed one will be flushed.
// Each flushing is synchronized on the given bucket.
// The first unclosed bucket becomes the new head: its messages will be written directly to
// the underlying output stream from now on.
// If all buckets are closed & flushed and signalNoMoreBuckets() has already been called,
// then the underlying output stream will be closed.
@Override
public void close() {
synchronized (lock) {
if (closed) throw new IllegalStateException(BUCKET_CLOSED_MESSAGE);
closed = true;
// if this is the head bucket, then flush subsequent continuous closed chain
if (buffer == null) next.flush();
}
}
// Flushes this bucket and if it is already closed, then recursively flushes the next one.
// If there is no next one (meaning this is tailGuard) and signalNoMoreBuckets() has been
// already called, then the underlying output stream will be closed.
private void flush() {
synchronized (lock) {
for (MessageT bufferedMessage: buffer) output.write(bufferedMessage);
buffer = null;
if (next != null) {
if (closed) next.flush();
} else { // this is tailGuard, so all "real" buckets are closed & flushed
if (noMoreBuckets) output.close();
}
}
}
}
static final String BUCKET_CLOSED_MESSAGE = "bucket already closed";
}