/* * Copyright (C) 2015 The Android Open Source Project * * 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 com.android.tools.rpclib.multiplex; import com.android.tools.rpclib.binary.Encoder; import gnu.trove.TLongObjectHashMap; import gnu.trove.TLongObjectIterator; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; class Sender { private static final int MAX_PENDING_SEND_COUNT = 1024; private static final SendItem NOP_ITEM = new SendNop(); private final int mMtu; @NotNull private ExecutorService mExecutorService; @NotNull private final LinkedBlockingQueue<SendItem> mPendingItems; private Worker mWorker; Sender(int mtu, @NotNull ExecutorService executorService) { mMtu = mtu; mExecutorService = executorService; mPendingItems = new LinkedBlockingQueue<SendItem>(MAX_PENDING_SEND_COUNT); } void begin(Encoder out) { mWorker = new Worker(out); mExecutorService.execute(mWorker); } void end() { try { synchronized (mWorker) { mWorker.setRunning(false); mPendingItems.add(NOP_ITEM); // Unblock the sender while (!mWorker.isStopped()) { mWorker.wait(); } mWorker = null; } } catch (InterruptedException e) { } } void sendData(long channel, byte b[], int off, int len) throws IOException { send(new SendData(channel, b, off, len)); } void sendOpenChannel(long channel) throws IOException { send(new OpenChannel(channel)); } void sendCloseChannel(long channel) throws IOException { send(new CloseChannel(channel)); } private void send(SendItem item) throws IOException { if (mWorker == null) { throw new RuntimeException("Attempting to send item when sender is not running"); } mPendingItems.add(item); item.sync(); } private static abstract class SendItem { final long mChannel; private boolean mDone; private IOException mException; SendItem(long channel) { mChannel = channel; } /** * Encodes the item to the provided {@link Encoder}, unblocking any calls to {@link #sync}. * * @return true if the item was fully sent, or false if there is more to send. */ final boolean send(Encoder e) { try { return encode(e); } catch (IOException exception) { synchronized (this) { mException = exception; } return true; } finally { synchronized (this) { mDone = true; notifyAll(); } } } /** * Waits for {@link #send} to be called, re-throwing an {@link java.io.IOException} if there was an exception * thrown while sending the item. */ final void sync() throws IOException { synchronized (this) { while (!mDone) { try { this.wait(); } catch (InterruptedException e) { } } if (mException != null) { throw mException; } } } /** @return true if the item was fully sent, or false if there is more to send. */ protected abstract boolean encode(Encoder e) throws IOException; } private static class OpenChannel extends SendItem { OpenChannel(long channel) { super(channel); } @Override protected boolean encode(Encoder e) throws IOException { e.uint8(Message.OPEN_CHANNEL); e.uint32(mChannel); return true; } } private static class CloseChannel extends SendItem { CloseChannel(long channel) { super(channel); } @Override protected boolean encode(Encoder e) throws IOException { e.uint8(Message.CLOSE_CHANNEL); e.uint32(mChannel); return true; } } /** SendNop encodes nothing, and is simply used to unblock the sender in {@link #end}. */ private static class SendNop extends SendItem { SendNop() { super(0); } @Override protected boolean encode(Encoder e) { return true; } } private final class Worker extends Thread { private final Encoder mEncoder; private boolean mIsRunning; private boolean mIsStopped; Worker(Encoder encoder) { super("rpclib.multiplex Sender"); mEncoder = encoder; mIsRunning = true; } public boolean isStopped() { return mIsStopped; } public void setRunning(boolean running) { mIsRunning = running; } @Override public void run() { SendMap map = new SendMap(); try { while (mIsRunning) { SendItem item; if (map.size() == 0) { // If there's nothing being worked on, block until we have something. item = mPendingItems.take(); } else { // If we're busy, grab more work only if there's something there. item = mPendingItems.poll(); } if (item != null) { map.add(item); map.flush(mEncoder); } } // Drain map while (map.size() > 0) { map.flush(mEncoder); } // Signal that this thread is done synchronized (this) { mIsStopped = true; notifyAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } } private class SendData extends SendItem { final byte[] mData; int mOffset; int mLength; SendData(long channel, byte[] data, int off, int len) { super(channel); mData = data; mOffset = off; mLength = len; } @Override protected boolean encode(Encoder e) throws IOException { e.uint8(Message.DATA); e.uint32(mChannel); int c = Math.min(mLength, mMtu); e.uint32(c); e.stream().write(mData, mOffset, c); mOffset += c; mLength -= c; return mLength == 0; } } private class SendMap { @NotNull private final TLongObjectHashMap<Queue<SendItem>> mQueues = new TLongObjectHashMap<Queue<SendItem>>(); public int size() { return mQueues.size(); } public void add(SendItem item) { long channel = item.mChannel; Queue<SendItem> queue = mQueues.get(channel); if (queue == null) { queue = new ArrayDeque<SendItem>(); mQueues.put(channel, queue); } queue.add(item); } public void flush(Encoder e) { TLongObjectIterator<Queue<SendItem>> it = mQueues.iterator(); for (int i = mQueues.size(); i-- > 0; ) { it.advance(); Queue<SendItem> queue = it.value(); SendItem item = queue.peek(); if (item.send(e)) { // Item has been fully encoded. queue.remove(); if (queue.poll() == null) { it.remove(); } } } } } }