tom

Working on IO loop stuff.

...@@ -12,6 +12,10 @@ public final class TestTools { ...@@ -12,6 +12,10 @@ public final class TestTools {
12 private TestTools() { 12 private TestTools() {
13 } 13 }
14 14
15 + public static void print(String msg) {
16 + System.out.print(msg);
17 + }
18 +
15 /** 19 /**
16 * Suspends the current thread for a specified number of millis. 20 * Suspends the current thread for a specified number of millis.
17 * 21 *
......
...@@ -20,7 +20,10 @@ ...@@ -20,7 +20,10 @@
20 <dependency> 20 <dependency>
21 <groupId>com.google.guava</groupId> 21 <groupId>com.google.guava</groupId>
22 <artifactId>guava-testlib</artifactId> 22 <artifactId>guava-testlib</artifactId>
23 - <scope>test</scope> 23 + </dependency>
24 + <dependency>
25 + <groupId>org.onlab.onos</groupId>
26 + <artifactId>onlab-junit</artifactId>
24 </dependency> 27 </dependency>
25 <dependency> 28 <dependency>
26 <groupId>io.netty</groupId> 29 <groupId>io.netty</groupId>
......
1 +package org.onlab.util;
2 +
3 +import java.util.Objects;
4 +
5 +import static com.google.common.base.MoreObjects.toStringHelper;
6 +import static com.google.common.base.Preconditions.checkArgument;
7 +
8 +/**
9 + * Counting mechanism capable of tracking occurrences and rates.
10 + */
11 +public class Counter {
12 +
13 + private long total = 0;
14 + private long start = System.currentTimeMillis();
15 + private long end = 0;
16 +
17 + /**
18 + * Creates a new counter.
19 + */
20 + public Counter() {
21 + }
22 +
23 + /**
24 + * Creates a new counter in a specific state. If non-zero end time is
25 + * specified, the counter will be frozen.
26 + *
27 + * @param start start time
28 + * @param total total number of items to start with
29 + * @param end end time; if non-ze
30 + */
31 + public Counter(long start, long total, long end) {
32 + checkArgument(start <= end, "Malformed interval: start > end");
33 + checkArgument(total >= 0, "Total must be non-negative");
34 + this.start = start;
35 + this.total = total;
36 + this.end = end;
37 + }
38 +
39 + /**
40 + * Resets the counter, by zeroing out the count and restarting the timer.
41 + */
42 + public synchronized void reset() {
43 + end = 0;
44 + total = 0;
45 + start = System.currentTimeMillis();
46 + }
47 +
48 + /**
49 + * Freezes the counter in the current state including the counts and times.
50 + */
51 + public synchronized void freeze() {
52 + end = System.currentTimeMillis();
53 + }
54 +
55 + /**
56 + * Adds the specified number of occurrences to the counter. No-op if the
57 + * counter has been frozen.
58 + *
59 + * @param count number of occurrences
60 + */
61 + public synchronized void add(long count) {
62 + checkArgument(count >= 0, "Count must be non-negative");
63 + if (end == 0L) {
64 + total += count;
65 + }
66 + }
67 +
68 + /**
69 + * Returns the number of occurrences per second.
70 + *
71 + * @return throughput in occurrences per second
72 + */
73 + public synchronized double throughput() {
74 + return total / duration();
75 + }
76 +
77 + /**
78 + * Returns the total number of occurrences counted.
79 + *
80 + * @return number of counted occurrences
81 + */
82 + public synchronized long total() {
83 + return total;
84 + }
85 +
86 + /**
87 + * Returns the duration expressed in fractional number of seconds.
88 + *
89 + * @return fractional number of seconds since the last reset
90 + */
91 + public synchronized double duration() {
92 + // Protect against 0 return by artificially setting duration to 1ms
93 + long duration = (end == 0L ? System.currentTimeMillis() : end) - start;
94 + return (duration == 0 ? 1 : duration) / 1000.0;
95 + }
96 +
97 + @Override
98 + public int hashCode() {
99 + return Objects.hash(total, start, end);
100 + }
101 +
102 + @Override
103 + public boolean equals(Object obj) {
104 + if (this == obj) {
105 + return true;
106 + }
107 + if (obj instanceof Counter) {
108 + final Counter other = (Counter) obj;
109 + return Objects.equals(this.total, other.total) &&
110 + Objects.equals(this.start, other.start) &&
111 + Objects.equals(this.end, other.end);
112 + }
113 + return false;
114 + }
115 +
116 + @Override
117 + public String toString() {
118 + return toStringHelper(this)
119 + .add("total", total)
120 + .add("start", start)
121 + .add("end", end)
122 + .toString();
123 + }
124 +}
1 +package org.onlab.util;
2 +
3 +import org.junit.Test;
4 +
5 +import static org.junit.Assert.assertEquals;
6 +import static org.junit.Assert.assertTrue;
7 +import static org.onlab.junit.TestTools.delay;
8 +
9 +/**
10 + * Tests of the Counter utility.
11 + */
12 +public class CounterTest {
13 +
14 + @Test
15 + public void basics() {
16 + Counter tt = new Counter();
17 + assertEquals("incorrect number of bytes", 0L, tt.total());
18 + assertEquals("incorrect throughput", 0.0, tt.throughput(), 0.0001);
19 + tt.add(1234567890L);
20 + assertEquals("incorrect number of bytes", 1234567890L, tt.total());
21 + assertTrue("incorrect throughput", 1234567890.0 < tt.throughput());
22 + delay(1500);
23 + tt.add(1L);
24 + assertEquals("incorrect number of bytes", 1234567891L, tt.total());
25 + assertTrue("incorrect throughput", 1234567891.0 > tt.throughput());
26 + tt.reset();
27 + assertEquals("incorrect number of bytes", 0L, tt.total());
28 + assertEquals("incorrect throughput", 0.0, tt.throughput(), 0.0001);
29 + }
30 +
31 + @Test
32 + public void freeze() {
33 + Counter tt = new Counter();
34 + tt.add(123L);
35 + assertEquals("incorrect number of bytes", 123L, tt.total());
36 + delay(1000);
37 + tt.freeze();
38 + tt.add(123L);
39 + assertEquals("incorrect number of bytes", 123L, tt.total());
40 +
41 + double d = tt.duration();
42 + double t = tt.throughput();
43 + assertEquals("incorrect duration", d, tt.duration(), 0.0001);
44 + assertEquals("incorrect throughput", t, tt.throughput(), 0.0001);
45 + assertEquals("incorrect number of bytes", 123L, tt.total());
46 + }
47 +
48 + @Test
49 + public void reset() {
50 + Counter tt = new Counter();
51 + tt.add(123L);
52 + assertEquals("incorrect number of bytes", 123L, tt.total());
53 +
54 + double d = tt.duration();
55 + double t = tt.throughput();
56 + assertEquals("incorrect duration", d, tt.duration(), 0.0001);
57 + assertEquals("incorrect throughput", t, tt.throughput(), 0.0001);
58 + assertEquals("incorrect number of bytes", 123L, tt.total());
59 +
60 + tt.reset();
61 + assertEquals("incorrect throughput", 0.0, tt.throughput(), 0.0001);
62 + assertEquals("incorrect number of bytes", 0, tt.total());
63 + }
64 +
65 + @Test
66 + public void syntheticTracker() {
67 + Counter tt = new Counter(5000, 1000, 6000);
68 + assertEquals("incorrect duration", 1, tt.duration(), 0.1);
69 + assertEquals("incorrect throughput", 1000, tt.throughput(), 1.0);
70 + }
71 +}
...@@ -22,6 +22,15 @@ ...@@ -22,6 +22,15 @@
22 <artifactId>guava-testlib</artifactId> 22 <artifactId>guava-testlib</artifactId>
23 <scope>test</scope> 23 <scope>test</scope>
24 </dependency> 24 </dependency>
25 + <dependency>
26 + <groupId>org.onlab.onos</groupId>
27 + <artifactId>onlab-misc</artifactId>
28 + </dependency>
29 + <dependency>
30 + <groupId>org.onlab.onos</groupId>
31 + <artifactId>onlab-junit</artifactId>
32 + <scope>test</scope>
33 + </dependency>
25 </dependencies> 34 </dependencies>
26 35
27 </project> 36 </project>
......
1 +package org.onlab.nio;
2 +
3 +/**
4 + * Base {@link Message} implementation.
5 + */
6 +public abstract class AbstractMessage implements Message {
7 +
8 + protected int length;
9 +
10 + @Override
11 + public int length() {
12 + return length;
13 + }
14 +
15 +}
...@@ -28,7 +28,7 @@ public abstract class AcceptorLoop extends SelectorLoop { ...@@ -28,7 +28,7 @@ public abstract class AcceptorLoop extends SelectorLoop {
28 public AcceptorLoop(long selectTimeout, SocketAddress listenAddress) 28 public AcceptorLoop(long selectTimeout, SocketAddress listenAddress)
29 throws IOException { 29 throws IOException {
30 super(selectTimeout); 30 super(selectTimeout);
31 - this.listenAddress = checkNotNull(this.listenAddress, "Address cannot be null"); 31 + this.listenAddress = checkNotNull(listenAddress, "Address cannot be null");
32 } 32 }
33 33
34 /** 34 /**
......
1 +package org.onlab.nio;
2 +
3 +import java.io.IOException;
4 +import java.nio.channels.ByteChannel;
5 +import java.nio.channels.CancelledKeyException;
6 +import java.nio.channels.ClosedChannelException;
7 +import java.nio.channels.SelectableChannel;
8 +import java.nio.channels.SelectionKey;
9 +import java.nio.channels.SocketChannel;
10 +import java.util.Iterator;
11 +import java.util.List;
12 +import java.util.Queue;
13 +import java.util.Set;
14 +import java.util.concurrent.ConcurrentLinkedQueue;
15 +import java.util.concurrent.CopyOnWriteArraySet;
16 +
17 +/**
18 + * I/O loop for driving inbound &amp; outbound {@link Message} transfer via
19 + * {@link MessageStream}.
20 + *
21 + * @param <M> message type
22 + * @param <S> message stream type
23 + */
24 +public abstract class IOLoop<M extends Message, S extends MessageStream<M>>
25 + extends SelectorLoop {
26 +
27 + // Queue of requests for new message streams to enter the IO loop processing.
28 + private final Queue<NewStreamRequest> newStreamRequests = new ConcurrentLinkedQueue<>();
29 +
30 + // Carries information required for admitting a new message stream.
31 + private class NewStreamRequest {
32 + private final S stream;
33 + private final SelectableChannel channel;
34 + private final int op;
35 +
36 + public NewStreamRequest(S stream, SelectableChannel channel, int op) {
37 + this.stream = stream;
38 + this.channel = channel;
39 + this.op = op;
40 + }
41 + }
42 +
43 + // Set of message streams currently admitted into the IO loop.
44 + private final Set<MessageStream<M>> streams = new CopyOnWriteArraySet<>();
45 +
46 + /**
47 + * Creates an IO loop with the given selection timeout.
48 + *
49 + * @param timeout selection timeout in milliseconds
50 + * @throws IOException if the backing selector cannot be opened
51 + */
52 + public IOLoop(long timeout) throws IOException {
53 + super(timeout);
54 + }
55 +
56 + /**
57 + * Creates a new message stream backed by the specified socket channel.
58 + *
59 + * @param byteChannel backing byte channel
60 + * @return newly created message stream
61 + */
62 + protected abstract S createStream(ByteChannel byteChannel);
63 +
64 + /**
65 + * Removes the specified message stream from the IO loop.
66 + *
67 + * @param stream message stream to remove
68 + */
69 + void removeStream(MessageStream<M> stream) {
70 + streams.remove(stream);
71 + }
72 +
73 + /**
74 + * Processes the list of messages extracted from the specified message
75 + * stream.
76 + *
77 + * @param messages non-empty list of received messages
78 + * @param stream message stream from which the messages were extracted
79 + */
80 + protected abstract void processMessages(List<M> messages, MessageStream<M> stream);
81 +
82 + /**
83 + * Completes connection request pending on the given selection key.
84 + *
85 + * @param key selection key holding the pending connect operation.
86 + */
87 + protected void connect(SelectionKey key) {
88 + try {
89 + SocketChannel ch = (SocketChannel) key.channel();
90 + ch.finishConnect();
91 + } catch (IOException | IllegalStateException e) {
92 + log.warn("Unable to complete connection", e);
93 + }
94 +
95 + if (key.isValid()) {
96 + key.interestOps(SelectionKey.OP_READ);
97 + }
98 + }
99 +
100 + /**
101 + * Processes an IO operation pending on the specified key.
102 + *
103 + * @param key selection key holding the pending I/O operation.
104 + */
105 + protected void processKeyOperation(SelectionKey key) {
106 + @SuppressWarnings("unchecked")
107 + S stream = (S) key.attachment();
108 +
109 + try {
110 + // If the key is not valid, bail out.
111 + if (!key.isValid()) {
112 + stream.close();
113 + return;
114 + }
115 +
116 + // If there is a pending connect operation, complete it.
117 + if (key.isConnectable()) {
118 + connect(key);
119 + }
120 +
121 + // If there is a read operation, slurp as much data as possible.
122 + if (key.isReadable()) {
123 + List<M> messages = stream.read();
124 +
125 + // No messages or failed flush imply disconnect; bail.
126 + if (messages == null || stream.hadError()) {
127 + stream.close();
128 + return;
129 + }
130 +
131 + // If there were any messages read, process them.
132 + if (!messages.isEmpty()) {
133 + try {
134 + processMessages(messages, stream);
135 + } catch (RuntimeException e) {
136 + onError(stream, e);
137 + }
138 + }
139 + }
140 +
141 + // If there are pending writes, flush them
142 + if (key.isWritable()) {
143 + stream.flushIfPossible();
144 + }
145 +
146 + // If there were any issued flushing, close the stream.
147 + if (stream.hadError()) {
148 + stream.close();
149 + }
150 +
151 + } catch (CancelledKeyException e) {
152 + // Key was cancelled, so silently close the stream
153 + stream.close();
154 + } catch (IOException e) {
155 + if (!stream.isClosed() && !isResetByPeer(e)) {
156 + log.warn("Unable to process IO", e);
157 + }
158 + stream.close();
159 + }
160 + }
161 +
162 + // Indicates whether or not this exception is caused by 'reset by peer'.
163 + private boolean isResetByPeer(IOException e) {
164 + Throwable cause = e.getCause();
165 + return cause != null && cause instanceof IOException &&
166 + cause.getMessage().contains("reset by peer");
167 + }
168 +
169 + /**
170 + * Hook to allow intercept of any errors caused during message processing.
171 + * Default behaviour is to rethrow the error.
172 + *
173 + * @param stream message stream involved in the error
174 + * @param error the runtime exception
175 + */
176 + protected void onError(S stream, RuntimeException error) {
177 + throw error;
178 + }
179 +
180 + /**
181 + * Admits a new message stream backed by the specified socket channel
182 + * with a pending accept operation.
183 + *
184 + * @param channel backing socket channel
185 + */
186 + public void acceptStream(SocketChannel channel) {
187 + createAndAdmit(channel, SelectionKey.OP_READ);
188 + }
189 +
190 +
191 + /**
192 + * Admits a new message stream backed by the specified socket channel
193 + * with a pending connect operation.
194 + *
195 + * @param channel backing socket channel
196 + */
197 + public void connectStream(SocketChannel channel) {
198 + createAndAdmit(channel, SelectionKey.OP_CONNECT);
199 + }
200 +
201 + /**
202 + * Creates a new message stream backed by the specified socket channel
203 + * and admits it into the IO loop.
204 + *
205 + * @param channel socket channel
206 + * @param op pending operations mask to be applied to the selection
207 + * key as a set of initial interestedOps
208 + */
209 + private synchronized void createAndAdmit(SocketChannel channel, int op) {
210 + S stream = createStream(channel);
211 + streams.add(stream);
212 + newStreamRequests.add(new NewStreamRequest(stream, channel, op));
213 + selector.wakeup();
214 + }
215 +
216 + /**
217 + * Safely admits new streams into the IO loop.
218 + */
219 + private void admitNewStreams() {
220 + Iterator<NewStreamRequest> it = newStreamRequests.iterator();
221 + while (isRunning() && it.hasNext()) {
222 + try {
223 + NewStreamRequest request = it.next();
224 + it.remove();
225 + SelectionKey key = request.channel.register(selector, request.op,
226 + request.stream);
227 + request.stream.setKey(key);
228 + } catch (ClosedChannelException e) {
229 + log.warn("Unable to admit new message stream", e);
230 + }
231 + }
232 + }
233 +
234 + @Override
235 + protected void loop() throws IOException {
236 + notifyReady();
237 +
238 + // Keep going until told otherwise.
239 + while (isRunning()) {
240 + admitNewStreams();
241 +
242 + // Process flushes & write selects on all streams
243 + for (MessageStream<M> stream : streams) {
244 + stream.flushIfWriteNotPending();
245 + }
246 +
247 + // Select keys and process them.
248 + int count = selector.select(selectTimeout);
249 + if (count > 0 && isRunning()) {
250 + Iterator<SelectionKey> it = selector.selectedKeys().iterator();
251 + while (it.hasNext()) {
252 + SelectionKey key = it.next();
253 + it.remove();
254 + processKeyOperation(key);
255 + }
256 + }
257 + }
258 + }
259 +
260 + /**
261 + * Prunes the registered streams by discarding any stale ones.
262 + */
263 + public synchronized void pruneStaleStreams() {
264 + for (MessageStream<M> stream : streams) {
265 + if (stream.isStale()) {
266 + stream.close();
267 + }
268 + }
269 + }
270 +
271 +}
1 +package org.onlab.nio;
2 +
3 +/**
4 + * Representation of a message transferred via {@link MessageStream}.
5 + */
6 +public interface Message {
7 +
8 + /**
9 + * Gets the message length in bytes.
10 + *
11 + * @return number of bytes
12 + */
13 + int length();
14 +
15 +}
1 +package org.onlab.nio;
2 +
3 +import org.slf4j.Logger;
4 +import org.slf4j.LoggerFactory;
5 +
6 +import java.io.IOException;
7 +import java.nio.ByteBuffer;
8 +import java.nio.channels.ByteChannel;
9 +import java.nio.channels.SelectionKey;
10 +import java.util.ArrayList;
11 +import java.util.List;
12 +
13 +import static com.google.common.base.Preconditions.checkArgument;
14 +import static com.google.common.base.Preconditions.checkNotNull;
15 +import static java.lang.System.currentTimeMillis;
16 +import static java.nio.ByteBuffer.allocateDirect;
17 +
18 +/**
19 + * Bi-directional message stream for transferring messages to &amp; from the
20 + * network via two byte buffers.
21 + *
22 + * @param <M> message type
23 + */
24 +public abstract class MessageStream<M extends Message> {
25 +
26 + protected Logger log = LoggerFactory.getLogger(getClass());
27 +
28 + private final IOLoop<M, ?> loop;
29 + private final ByteChannel channel;
30 + private final int maxIdleMillis;
31 +
32 + private final ByteBuffer inbound;
33 + private ByteBuffer outbound;
34 + private SelectionKey key;
35 +
36 + private volatile boolean closed = false;
37 + private volatile boolean writePending;
38 + private volatile boolean writeOccurred;
39 +
40 + private Exception ioError;
41 + private long lastActiveTime;
42 +
43 +
44 + /**
45 + * Creates a message stream associated with the specified IO loop and
46 + * backed by the given byte channel.
47 + *
48 + * @param loop IO loop
49 + * @param byteChannel backing byte channel
50 + * @param bufferSize size of the backing byte buffers
51 + * @param maxIdleMillis maximum number of millis the stream can be idle
52 + * before it will be closed
53 + */
54 + protected MessageStream(IOLoop<M, ?> loop, ByteChannel byteChannel,
55 + int bufferSize, int maxIdleMillis) {
56 + this.loop = checkNotNull(loop, "Loop cannot be null");
57 + this.channel = checkNotNull(byteChannel, "Byte channel cannot be null");
58 +
59 + checkArgument(maxIdleMillis > 0, "Idle time must be positive");
60 + this.maxIdleMillis = maxIdleMillis;
61 +
62 + inbound = allocateDirect(bufferSize);
63 + outbound = allocateDirect(bufferSize);
64 + }
65 +
66 + /**
67 + * Gets a single message from the specified byte buffer; this is
68 + * to be done without manipulating the buffer via flip, reset or clear.
69 + *
70 + * @param buffer byte buffer
71 + * @return read message or null if there are not enough bytes to read
72 + * a complete message
73 + */
74 + protected abstract M read(ByteBuffer buffer);
75 +
76 + /**
77 + * Puts the specified message into the specified byte buffer; this is
78 + * to be done without manipulating the buffer via flip, reset or clear.
79 + *
80 + * @param message message to be write into the buffer
81 + * @param buffer byte buffer
82 + */
83 + protected abstract void write(M message, ByteBuffer buffer);
84 +
85 + /**
86 + * Closes the message buffer.
87 + */
88 + public void close() {
89 + synchronized (this) {
90 + if (closed) {
91 + return;
92 + }
93 + closed = true;
94 + }
95 +
96 + loop.removeStream(this);
97 + if (key != null) {
98 + try {
99 + key.cancel();
100 + key.channel().close();
101 + } catch (IOException e) {
102 + log.warn("Unable to close stream", e);
103 + }
104 + }
105 + }
106 +
107 + /**
108 + * Indicates whether this buffer has been closed.
109 + *
110 + * @return true if this stream has been closed
111 + */
112 + public synchronized boolean isClosed() {
113 + return closed;
114 + }
115 +
116 + /**
117 + * Returns the stream IO selection key.
118 + *
119 + * @return socket channel registration selection key
120 + */
121 + public SelectionKey key() {
122 + return key;
123 + }
124 +
125 + /**
126 + * Binds the selection key to be used for driving IO operations on the stream.
127 + *
128 + * @param key IO selection key
129 + */
130 + public void setKey(SelectionKey key) {
131 + this.key = key;
132 + this.lastActiveTime = currentTimeMillis();
133 + }
134 +
135 + /**
136 + * Returns the IO loop to which this stream is bound.
137 + *
138 + * @return I/O loop used to drive this stream
139 + */
140 + public IOLoop<M, ?> loop() {
141 + return loop;
142 + }
143 +
144 + /**
145 + * Indicates whether the any prior IO encountered an error.
146 + *
147 + * @return true if a write failed
148 + */
149 + public boolean hadError() {
150 + return ioError != null;
151 + }
152 +
153 + /**
154 + * Gets the prior IO error, if one occurred.
155 + *
156 + * @return IO error; null if none occurred
157 + */
158 + public Exception getError() {
159 + return ioError;
160 + }
161 +
162 + /**
163 + * Reads, withouth blocking, a list of messages from the stream.
164 + * The list will be empty if there were not messages pending.
165 + *
166 + * @return list of messages or null if backing channel has been closed
167 + * @throws IOException if messages could not be read
168 + */
169 + public List<M> read() throws IOException {
170 + try {
171 + int read = channel.read(inbound);
172 + if (read != -1) {
173 + // Read the messages one-by-one and add them to the list.
174 + List<M> messages = new ArrayList<>();
175 + M message;
176 + inbound.flip();
177 + while ((message = read(inbound)) != null) {
178 + messages.add(message);
179 + }
180 + inbound.compact();
181 +
182 + // Mark the stream with current time to indicate liveness.
183 + lastActiveTime = currentTimeMillis();
184 + return messages;
185 + }
186 + return null;
187 +
188 + } catch (Exception e) {
189 + throw new IOException("Unable to read messages", e);
190 + }
191 + }
192 +
193 + /**
194 + * Writes the specified list of messages to the stream.
195 + *
196 + * @param messages list of messages to write
197 + * @throws IOException if error occurred while writing the data
198 + */
199 + public void write(List<M> messages) throws IOException {
200 + synchronized (this) {
201 + // First write all messages.
202 + for (M m : messages) {
203 + append(m);
204 + }
205 + flushUnlessAlreadyPlanningTo();
206 + }
207 + }
208 +
209 + /**
210 + * Writes the given message to the stream.
211 + *
212 + * @param message message to write
213 + * @throws IOException if error occurred while writing the data
214 + */
215 + public void write(M message) throws IOException {
216 + synchronized (this) {
217 + append(message);
218 + flushUnlessAlreadyPlanningTo();
219 + }
220 + }
221 +
222 + // Appends the specified message into the internal buffer, growing the
223 + // buffer if required.
224 + private void append(M message) {
225 + // If the buffer does not have sufficient length double it.
226 + while (outbound.remaining() < message.length()) {
227 + doubleSize();
228 + }
229 + // Place the message into the buffer and bump the output trackers.
230 + write(message, outbound);
231 + }
232 +
233 + // Forces a flush, unless one is planned already.
234 + private void flushUnlessAlreadyPlanningTo() throws IOException {
235 + if (!writeOccurred && !writePending) {
236 + flush();
237 + }
238 + }
239 +
240 + /**
241 + * Flushes any pending writes.
242 + *
243 + * @throws IOException if flush failed
244 + */
245 + public void flush() throws IOException {
246 + synchronized (this) {
247 + if (!writeOccurred && !writePending) {
248 + outbound.flip();
249 + try {
250 + channel.write(outbound);
251 + } catch (IOException e) {
252 + if (!closed && !e.getMessage().equals("Broken pipe")) {
253 + log.warn("Unable to write data", e);
254 + ioError = e;
255 + }
256 + }
257 + lastActiveTime = currentTimeMillis();
258 + writeOccurred = true;
259 + writePending = outbound.hasRemaining();
260 + outbound.compact();
261 + }
262 + }
263 + }
264 +
265 + /**
266 + * Indicates whether the stream has bytes to be written to the channel.
267 + *
268 + * @return true if there are bytes to be written
269 + */
270 + boolean isWritePending() {
271 + synchronized (this) {
272 + return writePending;
273 + }
274 + }
275 +
276 + /**
277 + * Attempts to flush data, internal stream state and channel availability
278 + * permitting. Invoked by the driver I/O loop during handling of writable
279 + * selection key.
280 + * <p/>
281 + * Resets the internal state flags {@code writeOccurred} and
282 + * {@code writePending}.
283 + *
284 + * @throws IOException if implicit flush failed
285 + */
286 + void flushIfPossible() throws IOException {
287 + synchronized (this) {
288 + writePending = false;
289 + writeOccurred = false;
290 + if (outbound.position() > 0) {
291 + flush();
292 + }
293 + }
294 + key.interestOps(SelectionKey.OP_READ);
295 + }
296 +
297 + /**
298 + * Attempts to flush data, internal stream state and channel availability
299 + * permitting and if other writes are not pending. Invoked by the driver
300 + * I/O loop prior to entering select wait. Resets the internal
301 + * {@code writeOccurred} state flag.
302 + *
303 + * @throws IOException if implicit flush failed
304 + */
305 + void flushIfWriteNotPending() throws IOException {
306 + synchronized (this) {
307 + writeOccurred = false;
308 + if (!writePending && outbound.position() > 0) {
309 + flush();
310 + }
311 + }
312 + if (isWritePending()) {
313 + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
314 + }
315 + }
316 +
317 + /**
318 + * Doubles the size of the outbound buffer.
319 + */
320 + private void doubleSize() {
321 + ByteBuffer newBuffer = allocateDirect(outbound.capacity() * 2);
322 + outbound.flip();
323 + newBuffer.put(outbound);
324 + outbound = newBuffer;
325 + }
326 +
327 + /**
328 + * Returns the maximum number of milliseconds the stream is allowed
329 + * without any read/write operations.
330 + *
331 + * @return number if millis of permissible idle time
332 + */
333 + protected int maxIdleMillis() {
334 + return maxIdleMillis;
335 + }
336 +
337 +
338 + /**
339 + * Returns true if the given stream has gone stale.
340 + *
341 + * @return true if the stream is stale
342 + */
343 + boolean isStale() {
344 + return currentTimeMillis() - lastActiveTime > maxIdleMillis() && key != null;
345 + }
346 +
347 +}
1 +package org.onlab.nio;
2 +
3 +import org.junit.Before;
4 +
5 +import java.util.concurrent.CountDownLatch;
6 +import java.util.concurrent.ExecutorService;
7 +import java.util.concurrent.TimeUnit;
8 +
9 +import static java.util.concurrent.Executors.newSingleThreadExecutor;
10 +import static org.junit.Assert.fail;
11 +import static org.onlab.util.Tools.namedThreads;
12 +
13 +/**
14 + * Base class for various NIO loop unit tests.
15 + */
16 +public abstract class AbstractLoopTest {
17 +
18 + protected static final long MAX_MS_WAIT = 500;
19 +
20 + /** Block on specified countdown latch. Return when countdown reaches
21 + * zero, or fail the test if the {@value #MAX_MS_WAIT} ms timeout expires.
22 + *
23 + * @param latch the latch
24 + * @param label an identifying label
25 + */
26 + protected void waitForLatch(CountDownLatch latch, String label) {
27 + try {
28 + boolean ok = latch.await(MAX_MS_WAIT, TimeUnit.MILLISECONDS);
29 + if (!ok) {
30 + fail("Latch await timeout! [" + label + "]");
31 + }
32 + } catch (InterruptedException e) {
33 + System.out.println("Latch interrupt [" + label + "] : " + e);
34 + fail("Unexpected interrupt");
35 + }
36 + }
37 +
38 + protected ExecutorService exec;
39 +
40 + @Before
41 + public void setUp() {
42 + exec = newSingleThreadExecutor(namedThreads("test"));
43 + }
44 +
45 +}
1 +package org.onlab.nio;
2 +
3 +import org.junit.Test;
4 +
5 +import java.io.IOException;
6 +import java.net.InetSocketAddress;
7 +import java.net.SocketAddress;
8 +import java.nio.channels.ServerSocketChannel;
9 +import java.util.concurrent.CountDownLatch;
10 +
11 +import static org.junit.Assert.assertEquals;
12 +import static org.onlab.junit.TestTools.delay;
13 +
14 +/**
15 + * Unit tests for AcceptLoop.
16 + */
17 +public class AcceptorLoopTest extends AbstractLoopTest {
18 +
19 + private static final int PORT = 9876;
20 +
21 + private static final SocketAddress SOCK_ADDR = new InetSocketAddress("127.0.0.1", PORT);
22 +
23 + private static class MyAcceptLoop extends AcceptorLoop {
24 + private final CountDownLatch loopStarted = new CountDownLatch(1);
25 + private final CountDownLatch loopFinished = new CountDownLatch(1);
26 + private final CountDownLatch runDone = new CountDownLatch(1);
27 + private final CountDownLatch ceaseLatch = new CountDownLatch(1);
28 +
29 + private int acceptCount = 0;
30 +
31 + MyAcceptLoop() throws IOException {
32 + super(500, SOCK_ADDR);
33 + }
34 +
35 + @Override
36 + protected void acceptConnection(ServerSocketChannel ssc) throws IOException {
37 + acceptCount++;
38 + }
39 +
40 + @Override
41 + public void loop() throws IOException {
42 + loopStarted.countDown();
43 + super.loop();
44 + loopFinished.countDown();
45 + }
46 +
47 + @Override
48 + public void run() {
49 + super.run();
50 + runDone.countDown();
51 + }
52 +
53 + @Override
54 + public void shutdown() {
55 + super.shutdown();
56 + ceaseLatch.countDown();
57 + }
58 + }
59 +
60 + @Test
61 +// @Ignore("Doesn't shut down the socket")
62 + public void basic() throws IOException {
63 + MyAcceptLoop myAccLoop = new MyAcceptLoop();
64 + AcceptorLoop accLoop = myAccLoop;
65 + exec.execute(accLoop);
66 + waitForLatch(myAccLoop.loopStarted, "loopStarted");
67 + delay(200); // take a quick nap
68 + accLoop.shutdown();
69 + waitForLatch(myAccLoop.loopFinished, "loopFinished");
70 + waitForLatch(myAccLoop.runDone, "runDone");
71 + assertEquals(0, myAccLoop.acceptCount);
72 + }
73 +}
1 +package org.onlab.nio;
2 +
3 +import org.junit.Before;
4 +import org.junit.Ignore;
5 +import org.junit.Test;
6 +
7 +import java.net.InetAddress;
8 +import java.text.DecimalFormat;
9 +import java.util.Random;
10 +
11 +import static org.onlab.junit.TestTools.delay;
12 +
13 +/**
14 + * Integration test for the select, accept and IO loops.
15 + */
16 +public class IOLoopIntegrationTest {
17 +
18 + private static final int MILLION = 1000000;
19 + private static final int TIMEOUT = 60;
20 +
21 + private static final int THREADS = 6;
22 + private static final int MSG_COUNT = 20 * MILLION;
23 + private static final int MSG_SIZE = 128;
24 +
25 + private static final long MIN_MPS = 10 * MILLION;
26 +
27 + @Before
28 + public void warmUp() throws Exception {
29 + try {
30 + run(MILLION, MSG_SIZE, 15, 0);
31 + } catch (Throwable e) {
32 + System.err.println("Failed warmup but moving on.");
33 + e.printStackTrace();
34 + }
35 + }
36 +
37 + @Ignore
38 + @Test
39 + public void basic() throws Exception {
40 + run(MSG_COUNT, MSG_SIZE, TIMEOUT, MIN_MPS);
41 + }
42 +
43 +
44 + private void run(int count, int size, int timeout, double mps) throws Exception {
45 + DecimalFormat f = new DecimalFormat("#,##0");
46 + System.out.print(f.format(count * THREADS) +
47 + (mps > 0.0 ? " messages: " : " message warm-up: "));
48 +
49 + // Setup the test on a random port to avoid intermittent test failures
50 + // due to the port being already bound.
51 + int port = StandaloneSpeedServer.PORT + new Random().nextInt(100);
52 +
53 + InetAddress ip = InetAddress.getLoopbackAddress();
54 + StandaloneSpeedServer sss = new StandaloneSpeedServer(ip, THREADS, size, port);
55 + StandaloneSpeedClient ssc = new StandaloneSpeedClient(ip, THREADS, count, size, port);
56 +
57 + sss.start();
58 + ssc.start();
59 + delay(250); // give the server and client a chance to go
60 +
61 + ssc.await(timeout);
62 + ssc.report();
63 +
64 + delay(1000);
65 + sss.stop();
66 + sss.report();
67 +
68 + // Note that the client and server will have potentially significantly
69 + // differing rates. This is due to the wide variance in how tightly
70 + // the throughput tracking starts & stops relative to to the short
71 + // test duration.
72 +// System.out.println(f.format(ssc.messages.throughput()) + " mps");
73 +
74 +// // Make sure client sent everything.
75 +// assertEquals("incorrect client message count sent",
76 +// (long) count * THREADS, ssc.messages.total());
77 +// assertEquals("incorrect client bytes count sent",
78 +// (long) size * count * THREADS, ssc.bytes.total());
79 +//
80 +// // Make sure server received everything.
81 +// assertEquals("incorrect server message count received",
82 +// (long) count * THREADS, sss.messages.total());
83 +// assertEquals("incorrect server bytes count received",
84 +// (long) size * count * THREADS, sss.bytes.total());
85 +//
86 +// // Make sure speeds were reasonable.
87 +// if (mps > 0.0) {
88 +// assertAboveThreshold("insufficient client speed", mps,
89 +// ssc.messages.throughput());
90 +// assertAboveThreshold("insufficient server speed", mps / 2,
91 +// sss.messages.throughput());
92 +// }
93 + }
94 +
95 +}
This diff is collapsed. Click to expand it.
1 +package org.onlab.nio;
2 +
3 +import java.io.IOException;
4 +import java.nio.channels.SelectionKey;
5 +import java.nio.channels.Selector;
6 +import java.nio.channels.spi.AbstractSelectableChannel;
7 +import java.nio.channels.spi.AbstractSelector;
8 +import java.util.Set;
9 +
10 +/**
11 + * A selector instrumented for unit tests.
12 + */
13 +public class MockSelector extends AbstractSelector {
14 +
15 + int wakeUpCount = 0;
16 +
17 + /**
18 + * Creates a mock selector, specifying null as the SelectorProvider.
19 + */
20 + public MockSelector() {
21 + super(null);
22 + }
23 +
24 + @Override
25 + public String toString() {
26 + return "{MockSelector: wake=" + wakeUpCount + "}";
27 + }
28 +
29 + @Override
30 + protected void implCloseSelector() throws IOException {
31 + }
32 +
33 + @Override
34 + protected SelectionKey register(AbstractSelectableChannel ch, int ops,
35 + Object att) {
36 + return null;
37 + }
38 +
39 + @Override
40 + public Set<SelectionKey> keys() {
41 + return null;
42 + }
43 +
44 + @Override
45 + public Set<SelectionKey> selectedKeys() {
46 + return null;
47 + }
48 +
49 + @Override
50 + public int selectNow() throws IOException {
51 + return 0;
52 + }
53 +
54 + @Override
55 + public int select(long timeout) throws IOException {
56 + return 0;
57 + }
58 +
59 + @Override
60 + public int select() throws IOException {
61 + return 0;
62 + }
63 +
64 + @Override
65 + public Selector wakeup() {
66 + wakeUpCount++;
67 + return null;
68 + }
69 +
70 +}
1 +package org.onlab.nio;
2 +
3 +import org.slf4j.Logger;
4 +import org.slf4j.LoggerFactory;
5 +
6 +import java.io.IOException;
7 +import java.net.InetAddress;
8 +import java.net.InetSocketAddress;
9 +import java.net.SocketAddress;
10 +import java.nio.channels.ByteChannel;
11 +import java.nio.channels.SelectionKey;
12 +import java.nio.channels.SocketChannel;
13 +import java.util.ArrayList;
14 +import java.util.List;
15 +import java.util.concurrent.ExecutionException;
16 +import java.util.concurrent.ExecutorService;
17 +import java.util.concurrent.Executors;
18 +import java.util.concurrent.FutureTask;
19 +import java.util.concurrent.Semaphore;
20 +import java.util.concurrent.TimeUnit;
21 +import java.util.concurrent.TimeoutException;
22 +
23 +import static org.onlab.junit.TestTools.delay;
24 +import static org.onlab.util.Tools.namedThreads;
25 +
26 +/**
27 + * Auxiliary test fixture to measure speed of NIO-based channels.
28 + */
29 +public class StandaloneSpeedClient {
30 +
31 + private static Logger log = LoggerFactory.getLogger(StandaloneSpeedClient.class);
32 +
33 + private final InetAddress ip;
34 + private final int port;
35 + private final int msgCount;
36 + private final int msgLength;
37 +
38 + private final List<CustomIOLoop> iloops = new ArrayList<>();
39 + private final ExecutorService ipool;
40 + private final ExecutorService wpool;
41 +
42 +// ThroughputTracker messages;
43 +// ThroughputTracker bytes;
44 +
45 + /**
46 + * Main entry point to launch the client.
47 + *
48 + * @param args command-line arguments
49 + * @throws IOException if unable to connect to server
50 + * @throws InterruptedException if latch wait gets interrupted
51 + * @throws ExecutionException if wait gets interrupted
52 + * @throws TimeoutException if timeout occurred while waiting for completion
53 + */
54 + public static void main(String[] args)
55 + throws IOException, InterruptedException, ExecutionException, TimeoutException {
56 + InetAddress ip = InetAddress.getByName(args.length > 0 ? args[0] : "127.0.0.1");
57 + int wc = args.length > 1 ? Integer.parseInt(args[1]) : 6;
58 + int mc = args.length > 2 ? Integer.parseInt(args[2]) : 50 * 1000000;
59 + int ml = args.length > 3 ? Integer.parseInt(args[3]) : 128;
60 + int to = args.length > 4 ? Integer.parseInt(args[4]) : 30;
61 +
62 + log.info("Setting up client with {} workers sending {} {}-byte messages to {} server... ",
63 + wc, mc, ml, ip);
64 + StandaloneSpeedClient sc = new StandaloneSpeedClient(ip, wc, mc, ml, StandaloneSpeedServer.PORT);
65 +
66 + sc.start();
67 + delay(2000);
68 +
69 + sc.await(to);
70 + sc.report();
71 +
72 + System.exit(0);
73 + }
74 +
75 + /**
76 + * Creates a speed client.
77 + *
78 + * @param ip ip address of server
79 + * @param wc worker count
80 + * @param mc message count to send per client
81 + * @param ml message length in bytes
82 + * @param port socket port
83 + * @throws IOException if unable to create IO loops
84 + */
85 + public StandaloneSpeedClient(InetAddress ip, int wc, int mc, int ml, int port) throws IOException {
86 + this.ip = ip;
87 + this.port = port;
88 + this.msgCount = mc;
89 + this.msgLength = ml;
90 + this.wpool = Executors.newFixedThreadPool(wc, namedThreads("worker"));
91 + this.ipool = Executors.newFixedThreadPool(wc, namedThreads("io-loop"));
92 +
93 + for (int i = 0; i < wc; i++) {
94 + iloops.add(new CustomIOLoop());
95 + }
96 + }
97 +
98 + /**
99 + * Starts the client workers.
100 + *
101 + * @throws IOException if unable to open connection
102 + */
103 + public void start() throws IOException {
104 +// messages = new ThroughputTracker();
105 +// bytes = new ThroughputTracker();
106 +
107 + // First start up all the IO loops
108 + for (CustomIOLoop l : iloops) {
109 + ipool.execute(l);
110 + }
111 +
112 +// // Wait for all of them to get going
113 +// for (CustomIOLoop l : iloops)
114 +// l.waitForStart(TIMEOUT);
115 +
116 + // ... and Next open all connections; one-per-loop
117 + for (CustomIOLoop l : iloops) {
118 + openConnection(l);
119 + }
120 + }
121 +
122 +
123 + /**
124 + * Initiates open connection request and registers the pending socket
125 + * channel with the given IO loop.
126 + *
127 + * @param loop loop with which the channel should be registered
128 + * @throws IOException if the socket could not be open or connected
129 + */
130 + private void openConnection(CustomIOLoop loop) throws IOException {
131 + SocketAddress sa = new InetSocketAddress(ip, port);
132 + SocketChannel ch = SocketChannel.open();
133 + ch.configureBlocking(false);
134 + loop.connectStream(ch);
135 + ch.connect(sa);
136 + }
137 +
138 +
139 + /**
140 + * Waits for the client workers to complete.
141 + *
142 + * @param secs timeout in seconds
143 + * @throws ExecutionException if execution failed
144 + * @throws InterruptedException if interrupt occurred while waiting
145 + * @throws TimeoutException if timeout occurred
146 + */
147 + public void await(int secs) throws InterruptedException,
148 + ExecutionException, TimeoutException {
149 + for (CustomIOLoop l : iloops) {
150 + if (l.worker.task != null) {
151 + l.worker.task.get(secs, TimeUnit.SECONDS);
152 + }
153 + }
154 +// messages.freeze();
155 +// bytes.freeze();
156 + }
157 +
158 + /**
159 + * Reports on the accumulated throughput trackers.
160 + */
161 + public void report() {
162 +// DecimalFormat f = new DecimalFormat("#,##0");
163 +// log.info("{} messages; {} bytes; {} mps; {} Mbs",
164 +// f.format(messages.total()),
165 +// f.format(bytes.total()),
166 +// f.format(messages.throughput()),
167 +// f.format(bytes.throughput() / (1024 * 128)));
168 + }
169 +
170 +
171 + // Loop for transfer of fixed-length messages
172 + private class CustomIOLoop extends IOLoop<TestMessage, TestMessageStream> {
173 +
174 + Worker worker = new Worker();
175 +
176 + public CustomIOLoop() throws IOException {
177 + super(500);
178 + }
179 +
180 +
181 + @Override
182 + protected TestMessageStream createStream(ByteChannel channel) {
183 + return new TestMessageStream(msgLength, channel, this);
184 + }
185 +
186 + @Override
187 + protected synchronized void removeStream(MessageStream<TestMessage> b) {
188 + super.removeStream(b);
189 +
190 +// messages.add(b.inMessages().total());
191 +// bytes.add(b.inBytes().total());
192 +// b.inMessages().reset();
193 +// b.inBytes().reset();
194 +
195 +// log.info("Disconnected client; inbound {} mps, {} Mbps; outbound {} mps, {} Mbps",
196 +// StandaloneSpeedServer.format.format(b.inMessages().throughput()),
197 +// StandaloneSpeedServer.format.format(b.inBytes().throughput() / (1024 * 128)),
198 +// StandaloneSpeedServer.format.format(b.outMessages().throughput()),
199 +// StandaloneSpeedServer.format.format(b.outBytes().throughput() / (1024 * 128)));
200 + }
201 +
202 + @Override
203 + protected void processMessages(List<TestMessage> messages,
204 + MessageStream<TestMessage> b) {
205 + worker.release(messages.size());
206 + }
207 +
208 + @Override
209 + protected void connect(SelectionKey key) {
210 + super.connect(key);
211 + TestMessageStream b = (TestMessageStream) key.attachment();
212 + Worker w = ((CustomIOLoop) b.loop()).worker;
213 + w.pump(b);
214 + }
215 +
216 + }
217 +
218 + /**
219 + * Auxiliary worker to connect and pump batched messages using blocking I/O.
220 + */
221 + private class Worker implements Runnable {
222 +
223 + private static final int BATCH_SIZE = 1000;
224 + private static final int PERMITS = 2 * BATCH_SIZE;
225 +
226 + private TestMessageStream b;
227 + private FutureTask<Worker> task;
228 +
229 + // Stuff to throttle pump
230 + private final Semaphore semaphore = new Semaphore(PERMITS);
231 + private int msgWritten;
232 +
233 + void pump(TestMessageStream b) {
234 + this.b = b;
235 + task = new FutureTask<>(this, this);
236 + wpool.execute(task);
237 + }
238 +
239 + @Override
240 + public void run() {
241 + try {
242 + log.info("Worker started...");
243 +
244 + List<TestMessage> batch = new ArrayList<>();
245 + for (int i = 0; i < BATCH_SIZE; i++) {
246 + batch.add(new TestMessage(msgLength));
247 + }
248 +
249 + while (msgWritten < msgCount) {
250 + msgWritten += writeBatch(b, batch);
251 + }
252 +
253 + // Now try to get all the permits back before sending poison pill
254 + semaphore.acquireUninterruptibly(PERMITS);
255 + b.close();
256 +
257 + log.info("Worker done...");
258 +
259 + } catch (IOException e) {
260 + log.error("Worker unable to perform I/O", e);
261 + }
262 + }
263 +
264 +
265 + private int writeBatch(TestMessageStream b, List<TestMessage> batch)
266 + throws IOException {
267 + int count = Math.min(BATCH_SIZE, msgCount - msgWritten);
268 + acquire(count);
269 + if (count == BATCH_SIZE) {
270 + b.write(batch);
271 + } else {
272 + for (int i = 0; i < count; i++) {
273 + b.write(batch.get(i));
274 + }
275 + }
276 + return count;
277 + }
278 +
279 +
280 + // Release permits based on the specified number of message credits
281 + private void release(int permits) {
282 + semaphore.release(permits);
283 + }
284 +
285 + // Acquire permit for a single batch
286 + private void acquire(int permits) {
287 + semaphore.acquireUninterruptibly(permits);
288 + }
289 +
290 + }
291 +
292 +}
1 +package org.onlab.nio;
2 +
3 +import org.slf4j.Logger;
4 +import org.slf4j.LoggerFactory;
5 +
6 +import java.io.IOException;
7 +import java.net.InetAddress;
8 +import java.net.InetSocketAddress;
9 +import java.net.Socket;
10 +import java.net.SocketAddress;
11 +import java.nio.channels.ByteChannel;
12 +import java.nio.channels.ServerSocketChannel;
13 +import java.nio.channels.SocketChannel;
14 +import java.text.DecimalFormat;
15 +import java.util.ArrayList;
16 +import java.util.List;
17 +import java.util.concurrent.ExecutorService;
18 +import java.util.concurrent.Executors;
19 +
20 +import static org.onlab.junit.TestTools.delay;
21 +import static org.onlab.util.Tools.namedThreads;
22 +
23 +/**
24 + * Auxiliary test fixture to measure speed of NIO-based channels.
25 + */
26 +public class StandaloneSpeedServer {
27 +
28 + private static Logger log = LoggerFactory.getLogger(StandaloneSpeedServer.class);
29 +
30 + private static final int PRUNE_FREQUENCY = 1000;
31 +
32 + static final int PORT = 9876;
33 + static final long TIMEOUT = 1000;
34 +
35 + static final boolean SO_NO_DELAY = false;
36 + static final int SO_SEND_BUFFER_SIZE = 1024 * 1024;
37 + static final int SO_RCV_BUFFER_SIZE = 1024 * 1024;
38 +
39 + static final DecimalFormat FORMAT = new DecimalFormat("#,##0");
40 +
41 + private final AcceptorLoop aloop;
42 + private final ExecutorService apool = Executors.newSingleThreadExecutor(namedThreads("accept"));
43 +
44 + private final List<CustomIOLoop> iloops = new ArrayList<>();
45 + private final ExecutorService ipool;
46 +
47 + private final int workerCount;
48 + private final int msgLength;
49 + private int lastWorker = -1;
50 +
51 +// ThroughputTracker messages;
52 +// ThroughputTracker bytes;
53 +
54 + /**
55 + * Main entry point to launch the server.
56 + *
57 + * @param args command-line arguments
58 + * @throws IOException if unable to crate IO loops
59 + */
60 + public static void main(String[] args) throws IOException {
61 + InetAddress ip = InetAddress.getByName(args.length > 0 ? args[0] : "127.0.0.1");
62 + int wc = args.length > 1 ? Integer.parseInt(args[1]) : 6;
63 + int ml = args.length > 2 ? Integer.parseInt(args[2]) : 128;
64 +
65 + log.info("Setting up the server with {} workers, {} byte messages on {}... ",
66 + wc, ml, ip);
67 + StandaloneSpeedServer ss = new StandaloneSpeedServer(ip, wc, ml, PORT);
68 + ss.start();
69 +
70 + // Start pruning clients.
71 + while (true) {
72 + delay(PRUNE_FREQUENCY);
73 + ss.prune();
74 + }
75 + }
76 +
77 + /**
78 + * Creates a speed server.
79 + *
80 + * @param ip optional ip of the adapter where to bind
81 + * @param wc worker count
82 + * @param ml message length in bytes
83 + * @param port listen port
84 + * @throws IOException if unable to create IO loops
85 + */
86 + public StandaloneSpeedServer(InetAddress ip, int wc, int ml, int port) throws IOException {
87 + this.workerCount = wc;
88 + this.msgLength = ml;
89 + this.ipool = Executors.newFixedThreadPool(workerCount, namedThreads("io-loop"));
90 +
91 + this.aloop = new CustomAcceptLoop(new InetSocketAddress(ip, port));
92 + for (int i = 0; i < workerCount; i++) {
93 + iloops.add(new CustomIOLoop());
94 + }
95 + }
96 +
97 + /**
98 + * Start the server IO loops and kicks off throughput tracking.
99 + */
100 + public void start() {
101 +// messages = new ThroughputTracker();
102 +// bytes = new ThroughputTracker();
103 +
104 + for (CustomIOLoop l : iloops) {
105 + ipool.execute(l);
106 + }
107 + apool.execute(aloop);
108 +//
109 +// for (CustomIOLoop l : iloops)
110 +// l.waitForStart(TIMEOUT);
111 +// aloop.waitForStart(TIMEOUT);
112 + }
113 +
114 + /**
115 + * Stop the server IO loops and freezes throughput tracking.
116 + */
117 + public void stop() {
118 + aloop.shutdown();
119 + for (CustomIOLoop l : iloops) {
120 + l.shutdown();
121 + }
122 +
123 +// for (CustomIOLoop l : iloops)
124 +// l.waitForFinish(TIMEOUT);
125 +// aloop.waitForFinish(TIMEOUT);
126 +//
127 +// messages.freeze();
128 +// bytes.freeze();
129 + }
130 +
131 + /**
132 + * Reports on the accumulated throughput trackers.
133 + */
134 + public void report() {
135 +// DecimalFormat f = new DecimalFormat("#,##0");
136 +// log.info("{} messages; {} bytes; {} mps; {} Mbs",
137 +// f.format(messages.total()),
138 +// f.format(bytes.total()),
139 +// f.format(messages.throughput()),
140 +// f.format(bytes.throughput() / (1024 * 128)));
141 + }
142 +
143 + /**
144 + * Prunes the IO loops of stale message buffers.
145 + */
146 + public void prune() {
147 + for (CustomIOLoop l : iloops) {
148 + l.pruneStaleStreams();
149 + }
150 + }
151 +
152 + // Get the next worker to which a client should be assigned
153 + private synchronized CustomIOLoop nextWorker() {
154 + lastWorker = (lastWorker + 1) % workerCount;
155 + return iloops.get(lastWorker);
156 + }
157 +
158 + // Loop for transfer of fixed-length messages
159 + private class CustomIOLoop extends IOLoop<TestMessage, TestMessageStream> {
160 +
161 + public CustomIOLoop() throws IOException {
162 + super(500);
163 + }
164 +
165 + @Override
166 + protected TestMessageStream createStream(ByteChannel channel) {
167 + return new TestMessageStream(msgLength, channel, this);
168 + }
169 +
170 + @Override
171 + protected void removeStream(MessageStream<TestMessage> stream) {
172 + super.removeStream(stream);
173 +//
174 +// messages.add(b.inMessages().total());
175 +// bytes.add(b.inBytes().total());
176 +//
177 +// log.info("Disconnected client; inbound {} mps, {} Mbps; outbound {} mps, {} Mbps",
178 +// format.format(b.inMessages().throughput()),
179 +// format.format(b.inBytes().throughput() / (1024 * 128)),
180 +// format.format(b.outMessages().throughput()),
181 +// format.format(b.outBytes().throughput() / (1024 * 128)));
182 + }
183 +
184 + @Override
185 + protected void processMessages(List<TestMessage> messages,
186 + MessageStream<TestMessage> stream) {
187 + try {
188 + stream.write(messages);
189 + } catch (IOException e) {
190 + log.error("Unable to echo messages", e);
191 + }
192 + }
193 + }
194 +
195 + // Loop for accepting client connections
196 + private class CustomAcceptLoop extends AcceptorLoop {
197 +
198 + public CustomAcceptLoop(SocketAddress address) throws IOException {
199 + super(500, address);
200 + }
201 +
202 + @Override
203 + protected void acceptConnection(ServerSocketChannel channel) throws IOException {
204 + SocketChannel sc = channel.accept();
205 + sc.configureBlocking(false);
206 +
207 + Socket so = sc.socket();
208 + so.setTcpNoDelay(SO_NO_DELAY);
209 + so.setReceiveBufferSize(SO_RCV_BUFFER_SIZE);
210 + so.setSendBufferSize(SO_SEND_BUFFER_SIZE);
211 +
212 + nextWorker().acceptStream(sc);
213 + log.info("Connected client");
214 + }
215 + }
216 +
217 +}
1 +package org.onlab.nio;
2 +
3 +/**
4 + * Fixed-length message.
5 + */
6 +public class TestMessage extends AbstractMessage {
7 +
8 + private final byte[] data;
9 +
10 + /**
11 + * Creates a new message with the specified length.
12 + *
13 + * @param length message length
14 + */
15 + public TestMessage(int length) {
16 + this.length = length;
17 + data = new byte[length];
18 + }
19 +
20 + /**
21 + * Creates a new message with the specified data.
22 + *
23 + * @param data message data
24 + */
25 + TestMessage(byte[] data) {
26 + this.length = data.length;
27 + this.data = data;
28 + }
29 +
30 + /**
31 + * Gets the backing byte array data.
32 + *
33 + * @return backing byte array
34 + */
35 + public byte[] data() {
36 + return data;
37 + }
38 +
39 +}
1 +package org.onlab.nio;
2 +
3 +import java.nio.ByteBuffer;
4 +import java.nio.channels.ByteChannel;
5 +
6 +/**
7 + * Fixed-length message transfer buffer.
8 + */
9 +public class TestMessageStream extends MessageStream<TestMessage> {
10 +
11 + private static final String E_WRONG_LEN = "Illegal message length: ";
12 +
13 + private final int length;
14 +
15 + /**
16 + * Create a new buffer for transferring messages of the specified length.
17 + *
18 + * @param length message length
19 + * @param ch backing channel
20 + * @param loop driver loop
21 + */
22 + public TestMessageStream(int length, ByteChannel ch,
23 + IOLoop<TestMessage, ?> loop) {
24 + super(loop, ch, 64 * 1024, 500);
25 + this.length = length;
26 + }
27 +
28 + @Override
29 + protected TestMessage read(ByteBuffer rb) {
30 + if (rb.remaining() < length) {
31 + return null;
32 + }
33 + TestMessage message = new TestMessage(length);
34 + rb.get(message.data());
35 + return message;
36 + }
37 +
38 + /**
39 + * {@inheritDoc}
40 + * <p/>
41 + * This implementation enforces the message length against the buffer
42 + * supported length.
43 + *
44 + * @throws IllegalArgumentException if message size does not match the
45 + * supported buffer size
46 + */
47 + @Override
48 + protected void write(TestMessage message, ByteBuffer wb) {
49 + if (message.length() != length) {
50 + throw new IllegalArgumentException(E_WRONG_LEN + message.length());
51 + }
52 + wb.put(message.data());
53 + }
54 +
55 +}