package org.apache.cassandra.thrift;
/*
 * 
 * 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.
 * 
 */


import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.spi.SelectorProvider;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;

import org.apache.cassandra.service.SocketSessionManagementService;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.transport.TNonblockingServerTransport;
import org.apache.thrift.transport.TNonblockingSocket;
import org.apache.thrift.transport.TNonblockingTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a interim solution till THRIFT-1167 gets committed...
 * 
 * The idea here is to avoid sticking to one CPU for IO's. For better throughput
 * it is spread across multiple threads. Number of selector thread can be the
 * number of CPU available.
 */
public class CustomTHsHaServer extends TNonblockingServer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomTHsHaServer.class.getName());
    private Set<SelectorThread> ioThreads = new HashSet<SelectorThread>();
    private volatile boolean stopped_ = true;
    private ExecutorService invoker;

    /**
     * All the arguments to Non Blocking Server will apply here. In addition,
     * executor pool will be responsible for creating the internal threads which
     * will process the data. threads for selection usually are equal to the
     * number of cpu's
     */
    public CustomTHsHaServer(Args args, ExecutorService invoker, int threadCount)
    {
        super(args);
        this.invoker = invoker;
        // Create all the Network IO Threads.
        for (int i = 0; i < threadCount; ++i)
            ioThreads.add(new SelectorThread("Selector-Thread-" + i));
    }

    /** @inheritDoc */
    @Override
    public void serve()
    {
        if (!startListening())
            return;
        if (!startThreads())
            return;
        setServing(true);
        joinSelector();
        invoker.shutdown();
        setServing(false);
        stopListening();
    }

    /**
     * Save the remote socket as a thead local for future use of client state.
     */
    protected class Invocation implements Runnable
    {
        private final FrameBuffer frameBuffer;
        private SelectorThread thread;

        public Invocation(final FrameBuffer frameBuffer, SelectorThread thread)
        {
            this.frameBuffer = frameBuffer;
            this.thread = thread;
        }

        public void run()
        {
            TNonblockingSocket socket = (TNonblockingSocket) frameBuffer.trans_;
            SocketSessionManagementService.remoteSocket.set(socket.getSocketChannel().socket().getRemoteSocketAddress());
            frameBuffer.invoke();
            // this is how we let the same selector thread change the selection type.
            thread.requestSelectInterestChange(frameBuffer);
        }
    }

    protected boolean startThreads()
    {
        stopped_ = false;
        // start all the threads.
        for (SelectorThread thread : ioThreads)
            thread.start();
        return true;
    }

    @Override
    protected void joinSelector()
    {
        try
        {
            // wait till all done with stuff's
            for (SelectorThread thread : ioThreads)
                thread.join();
        } 
        catch (InterruptedException e)
        {
            LOGGER.error("Interrupted while joining threads!", e);
        }
    }

    /**
     * Stop serving and shut everything down.
     */
    @Override
    public void stop()
    {
        stopListening();
        stopped_ = true;
        for (SelectorThread thread : ioThreads)
            thread.wakeupSelector();
        joinSelector();
    }

    /**
     * IO Threads will perform expensive IO operations...
     */
    protected class SelectorThread extends Thread
    {
        private final Selector selector;
        private TNonblockingServerTransport serverTransport;
        private Set<FrameBuffer> selectInterestChanges = new HashSet<FrameBuffer>();

        public SelectorThread(String name)
        {
            super(name);
            try
            {
                this.selector = SelectorProvider.provider().openSelector();
                this.serverTransport = (TNonblockingServerTransport) serverTransport_;
                this.serverTransport.registerSelector(selector);
            } 
            catch (IOException ex)
            {
                throw new RuntimeException("Couldnt open the NIO selector", ex);
            }
        }

        public void run()
        {
            try
            {
                while (!stopped_)
                {
                    select();
                }
                try
                {
                    selector.close(); // CASSANDRA-3867
                }
                catch (IOException e)
                {
                    // ignore this exception.
                }
            } 
            catch (Throwable t)
            {
                LOGGER.error("Uncaught Exception: ", t);
            }
        }

        private void select() throws InterruptedException, IOException
        {
            // wait for new keys
            selector.select();
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext())
            {
                SelectionKey key = keyIterator.next();
                keyIterator.remove();
                if (!key.isValid())
                {
                    // if invalid cleanup.
                    cleanupSelectionkey(key);
                    continue;
                }

                if (key.isAcceptable())
                    handleAccept();
                if (key.isReadable())
                    handleRead(key);
                else if (key.isWritable())
                    handleWrite(key);
                else
                    LOGGER.debug("Unexpected state " + key.interestOps());
            }
            // process the changes which are inserted after completion.
            processInterestChanges();
        }
        
        private void handleAccept()
        {
            SelectionKey clientKey = null;
            TNonblockingTransport client = null;
            try
            {
                // accept the connection
                client = (TNonblockingTransport) serverTransport.accept();
                clientKey = client.registerSelector(selector, SelectionKey.OP_READ);
                // add this key to the map
                FrameBuffer frameBuffer = new FrameBuffer(client, clientKey);
                clientKey.attach(frameBuffer); 
            } catch (TTransportException ex)
            {
                // ignore this might have been handled by the other threads.
                // serverTransport.accept() as it returns null as nothing to accept.
                return;
            }
            catch (IOException tte)
            {
                // something went wrong accepting.
                LOGGER.warn("Exception trying to accept!", tte);
                tte.printStackTrace();
                if (clientKey != null)
                    cleanupSelectionkey(clientKey);
                if (client != null)
                    client.close();
            }
        }
        
        private void handleRead(SelectionKey key)
        {
            FrameBuffer buffer = (FrameBuffer) key.attachment();
            if (!buffer.read())
            {
                cleanupSelectionkey(key);
                return;
            }

            if (buffer.isFrameFullyRead())
            {
                if (!requestInvoke(buffer, this))
                    cleanupSelectionkey(key);
            }
        }
        
        private void handleWrite(SelectionKey key)
        {
            FrameBuffer buffer = (FrameBuffer) key.attachment();
            if (!buffer.write())
                cleanupSelectionkey(key);
        }
        
        public void requestSelectInterestChange(FrameBuffer frameBuffer)
        {
            synchronized (selectInterestChanges)
            {
                selectInterestChanges.add(frameBuffer);
            }
            // Wake-up the selector, if it's currently blocked.
            selector.wakeup();
        }

        private void processInterestChanges()
        {
            synchronized (selectInterestChanges)
            {
                for (FrameBuffer fb : selectInterestChanges)
                    fb.changeSelectInterests();
                selectInterestChanges.clear();
            }
        }
        
        private void cleanupSelectionkey(SelectionKey key)
        {
            FrameBuffer buffer = (FrameBuffer) key.attachment();
            if (buffer != null)
                buffer.close();
            // cancel the selection key
            key.cancel();
        }
        
        public void wakeupSelector()
        {
            selector.wakeup();
        }
    }
    
    protected boolean requestInvoke(FrameBuffer frameBuffer, SelectorThread thread)
    {
        try
        {
            Runnable invocation = new Invocation(frameBuffer, thread);
            invoker.execute(invocation);
            return true;
        } 
        catch (RejectedExecutionException rx)
        {
            LOGGER.warn("ExecutorService rejected execution!", rx);
            return false;
        }
    }

    @Override
    protected void requestSelectInterestChange(FrameBuffer fb)
    {
        // Dont change the interest here, this has to be done by the selector
        // thread because the method is not synchronized with the rest of the
        // selectors threads.
    }
}
