/* * Copyright (c) 2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.eclipse.jetty.spdy; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.spdy.api.BytesDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.Handler; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.SessionFrameListener; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StringDataInfo; import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; import org.junit.Test; public class PushStreamTest extends AbstractTest { @Test public void testSynPushStream() throws Exception { final CountDownLatch pushStreamSynLatch = new CountDownLatch(1); Session clientSession = startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { stream.reply(new ReplyInfo(false)); stream.syn(new SynInfo(false)); return null; } }),new SessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { pushStreamSynLatch.countDown(); stream.reply(new ReplyInfo(false)); return super.onSyn(stream,synInfo); } }); clientSession.syn(new SynInfo(false),null).get(); assertThat("onSyn has been called",pushStreamSynLatch.await(5,TimeUnit.SECONDS),is(true)); } @Test public void testSendDataOnPushStreamAfterAssociatedStreamIsClosed() throws Exception { final Exchanger<Stream> streamExchanger = new Exchanger<>(); final CountDownLatch pushStreamSynLatch = new CountDownLatch(1); final CyclicBarrier replyBarrier = new CyclicBarrier(3); final CyclicBarrier closeBarrier = new CyclicBarrier(3); final CountDownLatch streamDataSent = new CountDownLatch(2); final CountDownLatch pushStreamDataReceived = new CountDownLatch(2); final CountDownLatch exceptionCountDownLatch = new CountDownLatch(1); Session clientSession = startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { stream.reply(new ReplyInfo(false)); try { replyBarrier.await(5,TimeUnit.SECONDS); return new StreamFrameListener.Adapter() { @Override public void onData(Stream stream, DataInfo dataInfo) { try { if (dataInfo.isClose()) { stream.data(new StringDataInfo("close stream",true)); closeBarrier.await(5,TimeUnit.SECONDS); } streamDataSent.countDown(); if (pushStreamDataReceived.getCount() == 2) { Stream pushStream = stream.syn(new SynInfo(false)).get(); streamExchanger.exchange(pushStream,5,TimeUnit.SECONDS); } } catch (Exception e) { exceptionCountDownLatch.countDown(); } } }; } catch (Exception e) { exceptionCountDownLatch.countDown(); throw new IllegalStateException(e); } } }),new SessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { pushStreamSynLatch.countDown(); stream.reply(new ReplyInfo(false)); return new StreamFrameListener.Adapter() { @Override public void onData(Stream stream, DataInfo dataInfo) { pushStreamDataReceived.countDown(); super.onData(stream,dataInfo); } }; } }); Stream stream = clientSession.syn(new SynInfo(false),new StreamFrameListener.Adapter() { @Override public void onReply(Stream stream, ReplyInfo replyInfo) { try { replyBarrier.await(5,TimeUnit.SECONDS); } catch (Exception e) { exceptionCountDownLatch.countDown(); } } @Override public void onData(Stream stream, DataInfo dataInfo) { try { closeBarrier.await(5,TimeUnit.SECONDS); } catch (Exception e) { exceptionCountDownLatch.countDown(); } } }).get(); replyBarrier.await(5,TimeUnit.SECONDS); stream.data(new StringDataInfo("client data",false)); Stream pushStream = streamExchanger.exchange(null,5,TimeUnit.SECONDS); pushStream.data(new StringDataInfo("first push data frame",false)); // nasty, but less complex than using another cyclicBarrier for example while (pushStreamDataReceived.getCount() != 1) Thread.sleep(1); stream.data(new StringDataInfo("client close",true)); closeBarrier.await(5,TimeUnit.SECONDS); assertThat("stream is closed",stream.isClosed(),is(true)); pushStream.data(new StringDataInfo("second push data frame while associated stream has been closed already",false)); assertThat("2 pushStream data frames have been received.",pushStreamDataReceived.await(5,TimeUnit.SECONDS),is(true)); assertThat("2 data frames have been sent",streamDataSent.await(5,TimeUnit.SECONDS),is(true)); assertThatNoExceptionOccured(exceptionCountDownLatch); } @Test public void testSynPushStreamOnClosedStream() throws Exception { final CountDownLatch pushStreamFailedLatch = new CountDownLatch(1); Session clientSession = startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { stream.reply(new ReplyInfo(true)); stream.syn(new SynInfo(false),1,TimeUnit.SECONDS,new Handler.Adapter<Stream>() { @Override public void failed(Throwable x) { pushStreamFailedLatch.countDown(); } }); return super.onSyn(stream,synInfo); } }),new SessionFrameListener.Adapter()); clientSession.syn(new SynInfo(true),null); assertThat("pushStream syn has failed",pushStreamFailedLatch.await(5,TimeUnit.SECONDS),is(true)); } @Test public void testSendBigDataOnPushStreamWhenAssociatedStreamIsClosed() throws Exception { final CountDownLatch streamClosedLatch = new CountDownLatch(1); final CountDownLatch allDataReceived = new CountDownLatch(1); final CountDownLatch exceptionCountDownLatch = new CountDownLatch(1); final Exchanger<ByteBuffer> exchanger = new Exchanger<>(); final int dataSizeInBytes = 1024 * 1024 * 1; final byte[] transferBytes = createHugeByteArray(dataSizeInBytes); Session clientSession = startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { try { Stream pushStream = stream.syn(new SynInfo(false)).get(); stream.reply(new ReplyInfo(true)); // wait until stream is closed streamClosedLatch.await(5,TimeUnit.SECONDS); pushStream.data(new BytesDataInfo(transferBytes,true)); return null; } catch (Exception e) { exceptionCountDownLatch.countDown(); throw new IllegalStateException(e); } } }),new SessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { return new StreamFrameListener.Adapter() { ByteBuffer receivedBytes = ByteBuffer.allocate(dataSizeInBytes); @Override public void onData(Stream stream, DataInfo dataInfo) { dataInfo.consumeInto(receivedBytes); if (dataInfo.isClose()) { allDataReceived.countDown(); try { receivedBytes.flip(); exchanger.exchange(receivedBytes.slice(),5,TimeUnit.SECONDS); } catch (Exception e) { exceptionCountDownLatch.countDown(); } } } }; } }); Stream stream = clientSession.syn(new SynInfo(true),new StreamFrameListener.Adapter() { @Override public void onReply(Stream stream, ReplyInfo replyInfo) { streamClosedLatch.countDown(); super.onReply(stream,replyInfo); } }).get(); ByteBuffer receivedBytes = exchanger.exchange(null,5,TimeUnit.SECONDS); assertThat("received byte array is the same as transferred byte array",Arrays.equals(transferBytes,receivedBytes.array()),is(true)); assertThat("onReply has been called to close the stream",streamClosedLatch.await(5,TimeUnit.SECONDS),is(true)); assertThat("stream is closed",stream.isClosed(),is(true)); assertThat("all data has been received",allDataReceived.await(20,TimeUnit.SECONDS),is(true)); assertThatNoExceptionOccured(exceptionCountDownLatch); } private byte[] createHugeByteArray(int sizeInBytes) { byte[] bytes = new byte[sizeInBytes]; new Random().nextBytes(bytes); return bytes; } @Test public void testOddEvenStreamIds() throws Exception { final CountDownLatch pushStreamIdIsEvenLatch = new CountDownLatch(3); Session clientSession = startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { stream.syn(new SynInfo(false)); return null; } }),new SessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { stream.reply(new ReplyInfo(false)); assertStreamIdIsEven(stream); pushStreamIdIsEvenLatch.countDown(); return super.onSyn(stream,synInfo); } }); Stream stream = clientSession.syn(new SynInfo(false),null).get(); Stream stream2 = clientSession.syn(new SynInfo(false),null).get(); Stream stream3 = clientSession.syn(new SynInfo(false),null).get(); assertStreamIdIsOdd(stream); assertStreamIdIsOdd(stream2); assertStreamIdIsOdd(stream3); assertThat("all pushStreams had even ids",pushStreamIdIsEvenLatch.await(5,TimeUnit.SECONDS),is(true)); } private void assertStreamIdIsEven(Stream stream) { assertThat("streamId is odd",stream.getId() % 2,is(0)); } private void assertStreamIdIsOdd(Stream stream) { assertThat("streamId is odd",stream.getId() % 2,is(1)); } private void assertThatNoExceptionOccured(final CountDownLatch exceptionCountDownLatch) throws InterruptedException { assertThat("No exception occured", exceptionCountDownLatch.await(1,TimeUnit.SECONDS),is(false)); } }