/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.remoting3.util;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Iterator;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;
import org.jboss.remoting3.AbstractDelegatingMessageOutputStream;
import org.jboss.remoting3.Channel;
import org.jboss.remoting3.MessageInputStream;
import org.jboss.remoting3.MessageOutputStream;
import org.jboss.remoting3.RemotingOptions;
import org.jboss.remoting3._private.IntIndexHashMap;
import org.jboss.remoting3._private.IntIndexMap;
import org.wildfly.common.Assert;
/**
* An invocation tracker.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
public final class InvocationTracker {
private final IntIndexMap<Invocation> invocations = new IntIndexHashMap<Invocation>(Invocation::getIndex);
private final MessageTracker messageTracker;
private final IntUnaryOperator intMasker;
/**
* Construct a new instance.
*
* @param channel the channel that is being tracked
* @param maxMessages the maximum number of concurrent messages to allow
* @param intMasker the function to apply to ID numbers to limit them to a specific range
*/
public InvocationTracker(final Channel channel, final int maxMessages, final IntUnaryOperator intMasker) {
this(channel, new MessageTracker(channel, maxMessages), intMasker);
}
/**
* Construct a new instance.
*
* @param channel the channel that is being tracked
* @param messageTracker the message tracker to use
* @param intMasker the function to apply to ID numbers to limit them to a specific range
*/
public InvocationTracker(final Channel channel, final MessageTracker messageTracker, final IntUnaryOperator intMasker) {
Assert.checkNotNullParam("channel", channel);
Assert.checkNotNullParam("messageTracker", messageTracker);
Assert.checkNotNullParam("intMasker", intMasker);
this.messageTracker = messageTracker;
channel.addCloseHandler((closed, exception) -> connectionClosed(exception));
this.intMasker = intMasker;
}
/**
* Construct a new instance, using the maximum number of messages from the channel.
*
* @param channel the channel that is being tracked
* @param intMasker the function to apply to ID numbers to limit them to a specific range
*/
public InvocationTracker(final Channel channel, final IntUnaryOperator intMasker) {
this(channel, channel.getOption(RemotingOptions.MAX_OUTBOUND_MESSAGES).intValue(), intMasker);
}
/**
* Construct a new instance.
*
* @param channel the channel that is being tracked
* @param maxMessages the maximum number of concurrent messages to allow
*/
public InvocationTracker(final Channel channel, final int maxMessages) {
this(channel, maxMessages, InvocationTracker::defaultFunction);
}
/**
* Construct a new instance, using the maximum number of messages from the channel.
*
* @param channel the channel that is being tracked
*/
public InvocationTracker(final Channel channel) {
this(channel, channel.getOption(RemotingOptions.MAX_OUTBOUND_MESSAGES).intValue(), InvocationTracker::defaultFunction);
}
private static int defaultFunction(int random) {
return random & 0xffff;
}
/**
* Add an invocation to this tracker.
*
* @param producer the invocation producer, which may be called more than once
* @param <T> the invocation type
* @return the produced invocation
*/
public <T extends Invocation> T addInvocation(IntFunction<T> producer) {
final ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
final IntUnaryOperator intMasker = this.intMasker;
final IntIndexMap<Invocation> invocations = this.invocations;
int id;
T invocation;
for (;;) {
id = intMasker.applyAsInt(threadLocalRandom.nextInt());
if (invocations.containsKey(id)) {
continue;
}
invocation = producer.apply(id);
if (invocations.putIfAbsent(invocation) != null) {
continue;
}
return invocation;
}
}
/**
* Determine if the tracker contains an entry at the given index.
*
* @param index the index
* @return {@code true} if the tracker contains the entry, {@code false} otherwise
*/
public boolean containsIndex(int index) {
return invocations.containsKey(index);
}
/**
* Put an invocation into the tracker if there is none with the corresponding ID.
*
* @param invocation the invocation
* @return the existing invocation, or {@code null} if the put was successful
*/
public Invocation putIfAbsent(Invocation invocation) {
return invocations.putIfAbsent(invocation);
}
/**
* Signal the arrival of a response with the given index.
*
* @param index the index of the response
* @param parameter an integer parameter to pass to the response handler (typically a message ID type)
* @param responseStream the response stream
* @param remove {@code true} to release the index for subsequent invocations, {@code false} otherwise
* @return {@code true} if the index was valid, {@code false} otherwise
*/
public boolean signalResponse(int index, int parameter, MessageInputStream responseStream, boolean remove) {
final Invocation invocation = remove ? invocations.removeKey(index) : invocations.get(index);
if (invocation == null) {
return false;
}
invocation.handleResponse(parameter, responseStream);
return true;
}
/**
* Unconditionally remove an invocation from the map. This should only be done if the outbound request definitely
* failed to be written.
*
* @param invocation the invocation
*/
public void remove(final Invocation invocation) {
invocations.remove(invocation);
}
/**
* Allocate a message, possibly blocking until one is available.
*
* @return the allocated message
* @throws IOException if an error occurs
*/
public MessageOutputStream allocateMessage() throws IOException {
try {
return messageTracker.openMessage();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException("Message allocation interrupted");
}
}
/**
* Allocate a message on behalf of an invocation, possibly blocking until a message is available. The invocation
* will automatically be removed if writing the message fails or is cancelled.
*
* @param invocation the invocation of the message
* @return the allocated message
* @throws IOException if an error occurs
*/
public MessageOutputStream allocateMessage(Invocation invocation) throws IOException {
return new AbstractDelegatingMessageOutputStream(allocateMessage()) {
public MessageOutputStream cancel() {
super.cancel();
remove(invocation);
return this;
}
};
}
private void connectionClosed(final IOException exception) {
final Iterator<Invocation> iterator = invocations.iterator();
while (iterator.hasNext()) {
final Invocation invocation = iterator.next();
try {
if (exception != null) {
invocation.handleException(exception);
} else {
invocation.handleClosed();
}
} catch (Throwable ignored) {
}
iterator.remove();
}
}
}