Showing
19 changed files
with
1747 additions
and
2 deletions
... | @@ -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> | ... | ... |
... | @@ -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 & 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 | +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 & 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 | +} |
-
Please register or login to post a comment