/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.avro.ipc;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;

import org.apache.avro.AvroRemoteException;
import org.apache.avro.ipc.specific.SpecificRequestor;
import org.apache.avro.ipc.specific.SpecificResponder;
import org.apache.avro.test.Simple;
import org.apache.avro.test.TestError;
import org.apache.avro.test.TestRecord;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;

/**
 * Verifies that RPCs executed by different client threads using the same 
 * NettyTransceiver will execute concurrently.  The test follows these steps:
 * 1. Execute the {@link #org.apache.avro.test.Simple.add(int, int)} RPC to 
 *    complete the Avro IPC handshake.
 * 2a. In a background thread, wait for the waitLatch.
 * 3a. In the main thread, invoke 
 *    {@link #org.apache.avro.test.Simple.hello(String)} with the argument 
 *    "wait".  This causes the ClientImpl running on the server to count down 
 *    the wait latch, which will unblock the background thread and allow it to 
 *    proceed.  After counting down the latch, this call blocks, waiting for 
 *    {@link #org.apache.avro.test.Simple.ack()} to be invoked.
 * 2b. The background thread wakes up because the waitLatch has been counted 
 *     down.  Now we know that some thread is executing inside hello(String).
 *     Next, execute {@link #org.apache.avro.test.Simple.ack()} in the 
 *     background thread, which will allow the thread executing hello(String) 
 *     to return.
 * 3b. The thread executing hello(String) on the server unblocks (since ack() 
 *     has been called), allowing hello(String) to return.
 * 4. If control returns to the main thread, we know that two RPCs 
 *    (hello(String) and ack()) were executing concurrently.
 */
public class TestNettyServerConcurrentExecution {
  private Server server;
  private Transceiver transceiver;
  
  @After
  public void cleanUpAfter() throws Exception {
    try {
      if (transceiver != null) {
        transceiver.close();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    try {
      if (server != null) {
        server.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  @Test(timeout=30000)
  public void test() throws Exception {
    final CountDownLatch waitLatch = new CountDownLatch(1);
    server = new NettyServer(
        new SpecificResponder(Simple.class, new SimpleImpl(waitLatch)), 
        new InetSocketAddress(0), 
        new NioServerSocketChannelFactory
          (Executors.newCachedThreadPool(), Executors.newCachedThreadPool()), 
        new ExecutionHandler(Executors.newCachedThreadPool()));
    server.start();
    
    transceiver = new NettyTransceiver(new InetSocketAddress(
        server.getPort()), TestNettyServer.CONNECT_TIMEOUT_MILLIS);
    
    // 1. Create the RPC proxy, and establish the handshake:
    final Simple.Callback simpleClient = 
        SpecificRequestor.getClient(Simple.Callback.class, transceiver);
    SpecificRequestor.getRemote(simpleClient);    // force handshake
    
    /*
     * 2a. In a background thread, wait for the Client.hello("wait") call to be
     *    received by the server, then:
     * 2b. Execute the Client.ack() RPC, which will unblock the 
     *     Client.hello("wait") call, allowing it to return to the main thread.
     */
    new Thread() {
      @Override
      public void run() {
        setName(TestNettyServerConcurrentExecution.class.getSimpleName() + 
            "Ack Thread");
        try {
          // Step 2a:
          waitLatch.await();
          
          // Step 2b:
          simpleClient.ack();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }.start();
    
    /*
     * 3. Execute the Client.hello("wait") RPC, which will block until the
     *    Client.ack() call has completed in the background thread.
     */
    String response = simpleClient.hello("wait");
    
    // 4. If control reaches here, both RPCs have executed concurrently
    Assert.assertEquals("wait", response); 
  }

  /**
   * Implementation of the Simple interface for use with this unit test.
   * If {@link #hello(String)} is called with "wait" as its argument,  
   * {@link #waitLatch} will be counted down, and {@link #hello(String)} will 
   * block until {@link #ack()} has been invoked.
   */
  private static class SimpleImpl implements Simple {
    private final CountDownLatch waitLatch;
    private final CountDownLatch ackLatch = new CountDownLatch(1);
    
    /**
     * Creates a SimpleImpl that uses the given CountDownLatch.
     * @param waitLatch the CountDownLatch to use in {@link #hello(String)}.
     */
    public SimpleImpl(final CountDownLatch waitLatch) {
      this.waitLatch = waitLatch;
    }
    
    @Override
    public int add(int arg1, int arg2) throws AvroRemoteException {
      // Step 1:
      return arg1 + arg2;
    }
    
    @Override
    public String hello(String greeting) throws AvroRemoteException {
      if (greeting.equals("wait")) {
        try {
          // Step 3a:
          waitLatch.countDown();
          
          // Step 3b:
          ackLatch.await();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          return e.toString();
        }
      }
      return greeting;
    }
    
    @Override
    public void ack() {
      // Step 2b:
      ackLatch.countDown();
    }
    
    // All RPCs below this line are irrelevant to this test:
    
    @Override
    public TestRecord echo(TestRecord record) throws AvroRemoteException {
      return record;
    }

    @Override
    public ByteBuffer echoBytes(ByteBuffer data) throws AvroRemoteException {
      return data;
    }

    @Override
    public Void error() throws AvroRemoteException, TestError {
      throw new TestError("TestError");
    }
  }
}
