/**
* Copyright 2014 Ricardo Padilha
*
* 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.dsys.snio.impl.pool;
import java.io.IOException;
import java.io.Serializable;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.meta.When;
import net.dsys.commons.api.exception.Bug;
import net.dsys.commons.api.future.CallbackFuture;
import net.dsys.commons.impl.future.SettableCallbackFuture;
import net.dsys.snio.api.pool.Acceptor;
import net.dsys.snio.api.pool.KeyAcceptor;
import net.dsys.snio.api.pool.KeyProcessor;
import net.dsys.snio.api.pool.Processor;
import net.dsys.snio.api.pool.SelectionType;
import net.dsys.snio.api.pool.SelectorThread;
/**
* @author Ricardo Padilha
*/
final class SelectorThreadImpl implements SelectorThread {
private final SelectionType type;
private final AtomicBoolean newOps;
private final Queue<IOOperation> ops;
private final AtomicBoolean newKeys;
private final NavigableSet<SelectionKey> keys;
private final SettableCallbackFuture<Void> closeFuture;
private Selector selector;
private Loop loop;
SelectorThreadImpl(@Nonnull final SelectionType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (type != SelectionType.OP_READ && type != SelectionType.OP_WRITE && type != SelectionType.OP_ACCEPT) {
throw new IllegalArgumentException("invalid type");
}
this.type = type;
this.newOps = new AtomicBoolean();
this.ops = new ConcurrentLinkedQueue<>();
this.newKeys = new AtomicBoolean();
this.keys = new ConcurrentSkipListSet<>(new KeyComparator());
this.closeFuture = new SettableCallbackFuture<>();
}
void open() throws IOException {
if (selector != null) {
return;
}
selector = Selector.open();
}
boolean isOpen() {
return selector != null && selector.isOpen();
}
CallbackFuture<Void> close() {
final IOOperation close = new IOOperation() {
@Override
public void run() throws IOException {
doClose();
}
};
queueOp(close);
return closeFuture;
}
private void queueOp(@Nonnull final IOOperation op) {
assert selector != null;
if (ops.offer(op)) {
if (newOps.compareAndSet(false, true)) {
selector.wakeup();
}
} else {
throw new Bug("ops.offer(op) == false");
}
}
/**
* Only called from within an {@link IOOperation} submitted by {@link #close()}.
*
* @throws IOException
*/
void doClose() {
if (selector == null) {
return;
}
IOException ioex = null;
for (final SelectionKey key : selector.keys()) {
if (!key.isValid()) {
continue;
}
try {
doCloseAttachment(key);
} catch (final IOException e) {
if (ioex == null) {
ioex = new IOException();
}
ioex.addSuppressed(e);
}
}
try {
selector.close();
} catch (final IOException e) {
if (ioex == null) {
ioex = new IOException();
}
ioex.addSuppressed(e);
}
if (ioex == null) {
closeFuture.success(null);
} else {
closeFuture.fail(ioex);
}
}
static void doCloseAttachment(@Nonnull final SelectionKey key) throws IOException {
final Object attach = key.attachment();
if (attach instanceof Processor) {
((Processor) attach).close();
} else if (attach instanceof Acceptor) {
((Acceptor) attach).close();
} else {
throw new Bug("Unknown attachment type: " + attach);
}
}
SettableCallbackFuture<Void> getCloseFuture() {
return closeFuture;
}
Runnable getRunnable() {
if (loop != null) {
return loop;
}
switch (type) {
case OP_ACCEPT:
loop = new AcceptLoop(selector, newOps, ops);
break;
case OP_READ:
loop = new ReadLoop(selector, newOps, ops);
break;
case OP_WRITE:
loop = new WriteLoop(selector, newOps, ops, newKeys, keys, SelectionKey.OP_WRITE);
break;
default:
throw new Bug("Unsupported selection type: " + type);
}
return loop;
}
void bind(@Nonnull final SelectableChannel channel, @Nonnull final Acceptor acceptor) {
final IOOperation bind = new IOOperation() {
@Override
public void run() throws IOException {
doBind(channel, acceptor);
}
};
queueOp(bind);
}
/**
* Only called from within an IOOperation.
*
* @throws ClosedChannelException
*/
void doBind(@Nonnull final SelectableChannel channel, @Nonnull final Acceptor acceptor) throws IOException {
final SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT, acceptor);
acceptor.getAcceptor().registered(this, key);
}
void connect(@Nonnull final SelectableChannel channel, @Nonnull final Processor processor) {
final IOOperation bind = new IOOperation() {
@Override
public void run() throws IOException {
doConnect(channel, processor);
}
};
queueOp(bind);
}
/**
* Only called from within an IOOperation.
*
* @throws ClosedChannelException
*/
void doConnect(final SelectableChannel channel, final Processor processor)
throws IOException {
if ((channel.validOps() & SelectionKey.OP_CONNECT) != 0) {
final SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT, processor);
processor.getProcessor().registered(this, key, SelectionType.OP_CONNECT);
}
}
void register(@Nonnull final SelectableChannel channel, @Nonnull final Processor processor) {
final IOOperation register = new IOOperation() {
@Override
public void run() throws IOException {
doRegister(channel, processor);
}
};
queueOp(register);
}
/**
* Only called from within an IOOperation.
*
* @throws ClosedChannelException
*/
void doRegister(@Nonnull final SelectableChannel channel, @Nonnull final Processor processor) {
try {
final SelectionKey key = channel.register(selector, type.getOp(), processor);
processor.getProcessor().registered(this, key, type);
} catch (final ClosedChannelException e) {
// channel was already closed, notify the processor all the same;
processor.getProcessor().registered(this, null, type);
}
}
void cancel(@Nonnull final SelectionKey key, @Nonnull final SettableCallbackFuture<Void> future) {
final IOOperation cancel = new IOOperation() {
@Override
public void run() {
try {
key.cancel();
future.success(null);
} catch (final Throwable t) {
future.fail(t);
}
}
};
queueOp(cancel);
}
void cancel(@Nonnull final SelectionKey key, @Nonnull final SettableCallbackFuture<Void> future,
@Nonnull final Callable<Void> task) {
final IOOperation cancel = new IOOperation() {
@Override
public void run() {
try {
key.cancel();
task.call();
future.success(null);
} catch (final Throwable t) {
future.fail(t);
}
}
};
queueOp(cancel);
}
/**
* {@inheritDoc}
* @see net.dsys.snio.api.pool.SelectorThread#enableKey(java.nio.channels.SelectionKey)
*/
@Override
public void enableKey(@Nonnull final SelectionKey key) {
if (keys.add(key) && newKeys.compareAndSet(false, true)) {
selector.wakeup();
}
}
/**
* Base class for all threads.
*
* @author Ricardo Padilha
*/
private abstract static class Loop implements Runnable {
private final Selector selector;
private final AtomicBoolean newOps;
private final Queue<IOOperation> ops;
Loop(@Nonnull final Selector selector, @Nonnull final AtomicBoolean newOps,
@Nonnull final Queue<IOOperation> ops) {
if (selector == null) {
throw new NullPointerException("selector == null");
}
if (newOps == null) {
throw new NullPointerException("selector == null");
}
if (ops == null) {
throw new NullPointerException("ops == null");
}
this.selector = selector;
this.newOps = newOps;
this.ops = ops;
}
@Override
public void run() {
while (selector.isOpen()) {
try {
final int n = selector.select();
runOps();
updateKeys();
if (n == 0) {
continue;
}
final Set<SelectionKey> ks = selector.selectedKeys();
if (ks.isEmpty()) {
continue;
}
for (final Iterator<SelectionKey> it = ks.iterator(); it.hasNext();) {
final SelectionKey k = it.next();
it.remove();
runKey(k);
}
} catch (final ClosedSelectorException e) {
// this is an expected exception when the channel is closed.
break;
} catch (final IOException e) {
// wtf? log and continue
e.printStackTrace();
continue;
}
}
}
/**
* Subclasses can override as needed.
*/
protected void updateKeys() {
return;
}
/**
* Process a single SelectionKey.
*/
protected abstract void runKey(@Nonnull SelectionKey k);
private void runOps() throws IOException {
if (newOps.compareAndSet(true, false)) {
while (!ops.isEmpty()) {
final IOOperation op = ops.poll();
op.run();
}
}
}
}
/**
* @author Ricardo Padilha
*/
private static final class AcceptLoop extends Loop {
AcceptLoop(@Nonnull final Selector selector, @Nonnull final AtomicBoolean newOps,
@Nonnull final Queue<IOOperation> ops) {
super(selector, newOps, ops);
}
/**
* {@inheritDoc}
*/
@Override
protected void runKey(final SelectionKey k) {
try {
if (k.isAcceptable()) {
final Acceptor accp = (Acceptor) k.attachment();
final KeyAcceptor<?> keyaccp = accp.getAcceptor();
keyaccp.accept(k);
}
} catch (final CancelledKeyException e) {
// another thread cancelled the key
return;
} catch (final IOException e) {
// wtf?
e.printStackTrace();
return;
}
}
}
/**
* @author Ricardo Padilha
*/
private static final class ReadLoop extends Loop {
ReadLoop(@Nonnull final Selector selector, @Nonnull final AtomicBoolean newOps,
@Nonnull final Queue<IOOperation> ops) {
super(selector, newOps, ops);
}
/**
* {@inheritDoc}
*/
@Override
protected void runKey(final SelectionKey k) {
try {
if (k.isReadable()) {
final Processor proc = (Processor) k.attachment();
final KeyProcessor<?> keyproc = proc.getProcessor();
try {
if (keyproc.read(k) < 0) {
proc.close();
}
} catch (final IOException e) {
proc.close();
} catch (final NotYetConnectedException e) {
// wtf?
e.printStackTrace();
proc.close();
}
} else if (k.isConnectable()) {
final Processor proc = (Processor) k.attachment();
final KeyProcessor<?> processor = proc.getProcessor();
processor.connect(k);
}
} catch (final CancelledKeyException e) {
// another thread cancelled the key
return;
} catch (final IOException e) {
// wtf?
e.printStackTrace();
return;
}
}
}
/**
* @author Ricardo Padilha
*/
private static final class WriteLoop extends Loop {
private final AtomicBoolean newKeys;
private final NavigableSet<SelectionKey> keys;
private final int op;
WriteLoop(@Nonnull final Selector selector, @Nonnull final AtomicBoolean newOps,
@Nonnull final Queue<IOOperation> ops, @Nonnull final AtomicBoolean newKeys,
@Nonnull final NavigableSet<SelectionKey> keys, final int op) {
super(selector, newOps, ops);
if (newKeys == null) {
throw new NullPointerException("newKeys == null");
}
if (keys == null) {
throw new NullPointerException("newKeys == null");
}
if (op != SelectionKey.OP_WRITE) {
throw new IllegalArgumentException("op != SelectionKey.OP_WRITE");
}
this.newKeys = newKeys;
this.keys = keys;
this.op = op;
}
/**
* {@inheritDoc}
*/
@Override
protected void runKey(final SelectionKey k) {
try {
if (k.isWritable()) {
final Processor proc = (Processor) k.attachment();
final KeyProcessor<?> keyproc = proc.getProcessor();
try {
if (keyproc.write(k) < 0) {
proc.close();
}
} catch (final IOException e) {
proc.close();
} catch (final NotYetConnectedException e) {
e.printStackTrace();
proc.close();
}
}
} catch (final CancelledKeyException e) {
// another thread cancelled the key
return;
} catch (final IOException e) {
// wtf?
e.printStackTrace();
return;
}
}
/**
* {@inheritDoc}
*/
@Override
protected void updateKeys() {
if (newKeys.compareAndSet(true, false)) {
SelectionKey key = null;
while ((key = keys.pollFirst()) != null) {
try {
final int iops = key.interestOps();
if ((iops & op) == 0) {
key.interestOps(iops | op);
}
} catch (final CancelledKeyException e) {
// another thread cancelled the key
continue;
}
}
}
}
}
/**
* Single command to be executed within the selector thread.
*
* @author Ricardo Padilha
*/
private interface IOOperation {
void run() throws IOException;
}
/**
* Comparator for SelectionKeys. Makes sure that identical keys returns
* zero, non-identical keys are sorted by hash code.
*
* @author Ricardo Padilha
*/
private static final class KeyComparator implements Comparator<SelectionKey>, Serializable {
private static final long serialVersionUID = 1L;
KeyComparator() {
super();
}
/**
* {@inheritDoc}
*/
@Override
public int compare(@Nonnull(when = When.MAYBE) final SelectionKey o1,
@Nonnull(when = When.MAYBE) final SelectionKey o2) {
if ((o1 == o2) || (o1 == null && o2 == null)) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
return Integer.signum(o2.hashCode() - o1.hashCode());
}
}
}