/**
* 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.camel.component.reactive.streams.engine;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.camel.Exchange;
import org.apache.camel.component.reactive.streams.ReactiveStreamsBackpressureStrategy;
import org.apache.camel.component.reactive.streams.ReactiveStreamsDiscardedException;
import org.apache.camel.component.reactive.streams.ReactiveStreamsHelper;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a contract between a Camel published and an external subscriber.
* It manages backpressure in order to deal with slow subscribers.
*/
public class CamelSubscription implements Subscription {
private static final Logger LOG = LoggerFactory.getLogger(CamelSubscription.class);
private String id;
private ExecutorService workerPool;
private String streamName;
private CamelPublisher publisher;
private ReactiveStreamsBackpressureStrategy backpressureStrategy;
private Subscriber<? super Exchange> subscriber;
/**
* The lock is used just for the time necessary to read/write shared variables.
*/
private Lock mutex = new ReentrantLock(true);
private LinkedList<Exchange> buffer = new LinkedList<>();
/**
* The current number of exchanges requested by the subscriber.
*/
private long requested;
/**
* Indicates that a cancel operation is to be performed.
*/
private boolean terminating;
/**
* Indicates that the subscription is end.
*/
private boolean terminated;
/**
* Indicates that a thread is currently sending items downstream.
* Items must be sent downstream by a single thread for each subscription.
*/
private boolean sending;
public CamelSubscription(String id, ExecutorService workerPool, CamelPublisher publisher, String streamName,
ReactiveStreamsBackpressureStrategy backpressureStrategy, Subscriber<? super Exchange> subscriber) {
this.id = id;
this.workerPool = workerPool;
this.publisher = publisher;
this.streamName = streamName;
this.backpressureStrategy = backpressureStrategy;
this.subscriber = subscriber;
}
@Override
public void request(long l) {
LOG.debug("Requested {} events from subscriber", l);
if (l <= 0) {
// wrong argument
mutex.lock();
terminated = true;
mutex.unlock();
publisher.unsubscribe(this);
subscriber.onError(new IllegalArgumentException("3.9"));
} else {
mutex.lock();
requested += l;
mutex.unlock();
checkAndFlush();
}
}
protected void checkAndFlush() {
mutex.lock();
boolean shouldFlush = !terminated && !sending && requested > 0 && buffer.size() > 0;
if (shouldFlush) {
sending = true;
}
mutex.unlock();
if (shouldFlush) {
workerPool.execute(() -> {
this.flush();
mutex.lock();
sending = false;
mutex.unlock();
// try again to flush
checkAndFlush();
});
} else {
mutex.lock();
boolean shouldComplete = terminating && !terminated;
if (shouldComplete) {
terminated = true;
}
mutex.unlock();
if (shouldComplete) {
this.publisher.unsubscribe(this);
this.subscriber.onComplete();
discardBuffer(this.buffer);
}
}
}
protected void flush() {
LinkedList<Exchange> sendingQueue = null;
try {
mutex.lock();
if (this.terminated) {
return;
}
int amount = (int) Math.min(requested, (long) buffer.size());
if (amount > 0) {
this.requested -= amount;
sendingQueue = new LinkedList<>();
while (amount > 0) {
sendingQueue.add(buffer.removeFirst());
amount--;
}
}
} finally {
mutex.unlock();
}
if (sendingQueue != null) {
LOG.debug("Sending {} events to the subscriber", sendingQueue.size());
for (Exchange data : sendingQueue) {
// TODO what if the subscriber throws an exception?
this.subscriber.onNext(data);
mutex.lock();
boolean shouldStop = this.terminated;
mutex.unlock();
if (shouldStop) {
break;
}
}
}
}
public void signalCompletion() throws Exception {
mutex.lock();
terminating = true;
mutex.unlock();
checkAndFlush();
}
@Override
public void cancel() {
publisher.unsubscribe(this);
mutex.lock();
this.terminated = true;
List<Exchange> bufferCopy = new LinkedList<>(buffer);
this.buffer.clear();
mutex.unlock();
discardBuffer(bufferCopy);
}
protected void discardBuffer(List<Exchange> remaining) {
for (Exchange data : remaining) {
ReactiveStreamsHelper.invokeDispatchCallback(
data,
new IllegalStateException("Cannot process the exchange " + data + ": subscription cancelled")
);
}
}
public void publish(Exchange message) {
Map<Exchange, String> discardedMessages = null;
try {
mutex.lock();
if (!this.terminating && !this.terminated) {
Collection<Exchange> discarded = this.backpressureStrategy.update(buffer, message);
if (!discarded.isEmpty()) {
discardedMessages = new HashMap<>();
for (Exchange ex : discarded) {
discardedMessages.put(ex, "Exchange " + ex + " discarded by backpressure strategy " + this.backpressureStrategy);
}
}
} else {
// acknowledge
discardedMessages = Collections.singletonMap(message, "Exchange " + message + " discarded: subscription closed");
}
} finally {
mutex.unlock();
}
// discarding outside of mutex scope
if (discardedMessages != null) {
for (Exchange exchange: discardedMessages.keySet()) {
ReactiveStreamsHelper.invokeDispatchCallback(
exchange,
new ReactiveStreamsDiscardedException("Discarded by backpressure strategy", exchange, streamName)
);
}
}
checkAndFlush();
}
public void setBackpressureStrategy(ReactiveStreamsBackpressureStrategy backpressureStrategy) {
mutex.lock();
this.backpressureStrategy = backpressureStrategy;
mutex.unlock();
}
public long getBufferSize() {
return buffer.size();
}
public ReactiveStreamsBackpressureStrategy getBackpressureStrategy() {
return backpressureStrategy;
}
public String getId() {
return id;
}
}