SetWriteReadyOnHandlingWritableChannelTestCase.java

/*
 * JBoss, Home of Professional Open Source.
 *
 * Copyright 2012 Red Hat, Inc. and/or its affiliates, and individual
 * contributors as indicated by the @author tags.
 *
 * 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.xnio.racecondition;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.lang.reflect.Field;

import org.jboss.byteman.contrib.bmunit.BMScript;
import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xnio.Bits;
import org.xnio.ChannelListener;
import org.xnio.channels.SuspendableChannel;
import org.xnio.channels.TranslatingSuspendableChannel;
import org.xnio.mock.ConnectedStreamChannelMock;

/**
 * Set write ready on a channel that is handling writable. Check if the effects of setting write ready are
 * not mistakenly covered by handleWritable.
 * 
 * @author <a href="mailto:flavia.rainone@jboss.com">Flavia Rainone</a>
 */
@RunWith(BMUnitRunner.class)
@BMScript(dir="src/test/resources")
public class SetWriteReadyOnHandlingWritableChannelTestCase {
    @Test
    public void test() throws Exception {
        // creating channel and threads
        ConnectedStreamChannelMock connectedChannelMock = new ConnectedStreamChannelMock();
        final MyTranslatingSuspendableChannel channel = new MyTranslatingSuspendableChannel(connectedChannelMock);
        final Thread setWriteReadyThread = new Thread(new SetWriteReady(channel));
        final Thread handleWritableThread = new Thread(new InvokeHandleWritable(channel));
        // set up scenario
        final ChannelListener<? super SuspendableChannel> listener = new ChannelListener<SuspendableChannel>() {
            @Override
            public void handleEvent(SuspendableChannel c) {
                channel.clearWriteReady();
            }
        };
        channel.getWriteSetter().set(listener);
        channel.resumeWrites();
        channel.setWriteRequiresRead();
        channel.handleWritable();
        assertFalse(channel.isWriteReady());
        assertFalse(connectedChannelMock.isWriteResumed());
        // first, try to invoke handleWritable and then setWriteReady
        System.out.println("Attempt 1: invoke handleWritable before setWriteReady");
        channel.handleWritable();
        channel.setWriteReady();
        assertTrue(channel.isWriteReady());
        assertTrue(connectedChannelMock.isWriteAwaken());
        // clear everything
        channel.clearWriteReady();
        channel.handleWritable();
        assertFalse(channel.isWriteReady());
        assertFalse(connectedChannelMock.isWriteResumed());
        // now, try the opposite, invoke handleWritable after setWriteReady()
        System.out.println("Attempt 2: invoke handleWritable after setWriteReady");
        channel.setWriteReady();
        channel.handleWritable();
        assertFalse(channel.isWriteReady());
        assertTrue(connectedChannelMock.isWriteAwaken());
        channel.clearWriteReady();
        channel.handleWritable();
        assertFalse(channel.isWriteReady());
        assertFalse(connectedChannelMock.isWriteResumed());
        System.out.println("Attempt 3: race condition scenario involving handleWritable and setWriteReady");
        // finally, create the race condition scenario... handleWritable and setWriteReady occur
        // practically at the same time (see SetWriteReadyOnHandlingWritableChannelTestCase.btm)
        setWriteReadyThread.start();
        handleWritableThread.start();
        // joining threads
        setWriteReadyThread.join();
        handleWritableThread.join();
        assertFalse(channel.isWriteReady());
        assertTrue(connectedChannelMock.isWriteResumed());
    }

    private static class SetWriteReady implements Runnable {
        private MyTranslatingSuspendableChannel channel;

        public SetWriteReady(MyTranslatingSuspendableChannel channel) {
            this.channel = channel;
        }

        @Override
        public void run() {
            channel.setWriteReady();
        }
    }

    private static class InvokeHandleWritable implements Runnable {
        private MyTranslatingSuspendableChannel channel;

        public InvokeHandleWritable(MyTranslatingSuspendableChannel channel) {
            this.channel = channel;
        }

        @Override
        public void run() {
            channel.handleWritable();
        }
    }

    private static class MyTranslatingSuspendableChannel extends TranslatingSuspendableChannel<SuspendableChannel, SuspendableChannel> {

        private static final int WRITE_READY;
        private static final Field stateField;

        static {
            try {
                Field WRITE_READY_field = TranslatingSuspendableChannel.class.getDeclaredField("WRITE_READY");
                WRITE_READY_field.setAccessible(true);
                WRITE_READY = WRITE_READY_field.getInt(null);
                stateField = TranslatingSuspendableChannel.class.getDeclaredField("state");
                stateField.setAccessible(true);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException("Unexpected exception " + e);
            }
        }

        protected MyTranslatingSuspendableChannel(SuspendableChannel c) {
            super(c);
        }

        public void handleWritable() {
            super.handleWritable();
        }

        public void setWriteReady() {
            super.setWriteReady();
        }

        public void clearWriteReady() {
            super.clearWriteReady();
        }

        public boolean isWriteReady() throws Exception {
            return Bits.allAreSet(stateField.getInt(this), WRITE_READY);
        }

        public void setWriteRequiresRead() {
            super.setWriteRequiresRead();
        }
    }
}