/*
* KitchenSync-core Java Library Copyright (C) 2014 Burton Alexander
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mrstampy.kitchensync.stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import rx.Scheduler;
import rx.Subscription;
import rx.functions.Action0;
import rx.schedulers.Schedulers;
import com.github.mrstampy.kitchensync.message.inbound.KiSyInboundMesssageHandler;
import com.github.mrstampy.kitchensync.stream.inbound.StreamAckInboundMessageHandler;
/**
* {@link Streamer}s register themselves when sending a message requiring an
* acknowledgement. When the acknowledgement is received a
* {@link KiSyInboundMesssageHandler} (such as the
* {@link StreamAckInboundMessageHandler}) uses this register to look up the
* appropriate Streamer for {@link Streamer#ackReceived(long)}.
*
* The acknowledgement is a long, the sum of the bytes of the last message sent.
*/
public class StreamerAckRegister {
/**
* The prefix used to identify return messages when {@link Streamer#isAckRequired()}:
* 'StreamAck:'. The suffix is the sum of the btyes of the message being
* acknowledged.
*
* @see #convertToLong(byte[])
* @see Streamer#ackRequired()
*/
public static final String ACK_PREFIX = "StreamAck:";
/**
* Convenience constant for the byte array from {@link #ACK_PREFIX}.
*/
public static final byte[] ACK_PREFIX_BYTES = ACK_PREFIX.getBytes();
private static Map<RegKey, List<RegisterContainer>> awaiting = new ConcurrentHashMap<RegKey, List<RegisterContainer>>();
private static Scheduler cleanupSvc = Schedulers.from(Executors.newCachedThreadPool());
private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private static ReadLock readLock = rwLock.readLock();
private static WriteLock writeLock = rwLock.writeLock();
/**
* Adds the {@link Streamer} and the chunk to the register.
*
* @param chunk
* the chunk
* @param acker
* the acker
* @return the long
*/
public static long add(byte[] chunk, Streamer<?> acker) {
if (!acker.isAckRequired()) return -1l;
long sumOfBytes = convertToLong(chunk);
int port = acker.getChannel().getPort();
final RegKey rk = new RegKey(sumOfBytes, port);
List<RegisterContainer> containers = getContainers(rk);
Subscription sub = cleanupSvc.createWorker().schedule(new Action0() {
@Override
public void call() {
removeAckAwaiter(rk);
}
}, 10, TimeUnit.SECONDS);
RegisterContainer rc = new RegisterContainer(acker, sub, chunk, sumOfBytes);
writeLock.lock();
try {
int idx = containers.indexOf(rc);
if (idx == -1) {
containers.add(rc);
} else {
RegisterContainer existing = containers.get(idx);
existing.add(sub);
}
} finally {
writeLock.unlock();
}
return sumOfBytes;
}
/**
* Gets the {@link Streamer} for the specified acknowledgement value.
*
* @param sumOfBytes
* the sum of bytes
* @param port
* the port
* @return the ack awaiter
*/
public static Streamer<?> getAckAwaiter(long sumOfBytes, int port) {
List<RegisterContainer> containers = getContainers(new RegKey(sumOfBytes, port));
RegisterContainer reg = getContainer(sumOfBytes, containers);
return reg == null ? null : reg.streamer;
}
private static RegisterContainer removeAckAwaiter(RegKey rk) {
List<RegisterContainer> containers = getContainers(rk);
RegisterContainer reg = getContainer(rk.sumOfBytes, containers);
if (reg != null) removeRegisterContainer(rk, containers, reg);
return reg;
}
/**
* Gets the chunk associated with the acknowledgement value.
*
* @param sumOfBytes
* the sum of bytes
* @param port
* the port
* @return the chunk
*/
public static byte[] getChunk(long sumOfBytes, int port) {
RegisterContainer reg = removeAckAwaiter(new RegKey(sumOfBytes, port));
return reg == null ? null : reg.chunk;
}
private static List<RegisterContainer> getContainers(RegKey rk) {
List<RegisterContainer> containers = awaiting.get(rk);
if (containers == null) {
containers = new ArrayList<RegisterContainer>();
awaiting.put(rk, containers);
}
return containers;
}
private static RegisterContainer getContainer(long sumOfBytes, List<RegisterContainer> containers) {
readLock.lock();
try {
RegisterContainer reg = null;
for (RegisterContainer rc : containers) {
if (rc == null || !rc.isAck(sumOfBytes)) continue;
reg = rc;
break;
}
return reg;
} finally {
readLock.unlock();
}
}
private static void removeRegisterContainer(RegKey rk, List<RegisterContainer> containers, RegisterContainer rc) {
if (containers.isEmpty()) return;
writeLock.lock();
try {
Subscription sub = rc.sub();
unsubscribe(sub);
if (sub == null || rc.count() == 0) {
containers.remove(rc);
if(containers.isEmpty()) awaiting.remove(rk);
}
} finally {
writeLock.unlock();
}
}
/**
* Utility method, returns true if the message supplied is an acknowledgement
* message.
*
* @param message
* the message
* @return true, if checks if is ack message
*/
public static boolean isAckMessage(byte[] message) {
if (message.length < ACK_PREFIX_BYTES.length) return false;
byte[] b = Arrays.copyOfRange(message, 0, ACK_PREFIX_BYTES.length);
return Arrays.equals(b, ACK_PREFIX_BYTES);
}
/**
* Creates the ack response to send back to the
* {@link Streamer#isAckRequired()}.
*
* @param sumOfBytes
* the sum of bytes
* @return the byte[]
*/
public static byte[] createAckResponse(long sumOfBytes) {
String s = ACK_PREFIX + sumOfBytes;
return s.getBytes();
}
/**
* Convenience method to sum the bytes of the given byte array.
*
* @param chunk
* the chunk
* @return the long
* @see Streamer#ackReceived(long)
*/
public static long convertToLong(byte[] chunk) {
int hash = 0;
for (byte b : chunk) {
hash += b;
}
return hash;
}
private static void unsubscribe(Subscription sub) {
if (sub != null) sub.unsubscribe();
}
private static class RegisterContainer {
Streamer<?> streamer;
List<Subscription> subs = new ArrayList<Subscription>();
byte[] chunk;
Long ackKey;
public RegisterContainer(Streamer<?> streamer, Subscription sub, byte[] chunk, Long ackKey) {
this.streamer = streamer;
this.chunk = chunk;
this.ackKey = ackKey;
subs.add(sub);
}
public int count() {
return subs.size();
}
public Subscription sub() {
return count() > 0 ? subs.remove(0) : null;
}
public void add(Subscription sub) {
subs.add(sub);
}
public boolean isAck(Long key) {
return ackKey.equals(key);
}
}
private static class RegKey {
long sumOfBytes;
int port;
public RegKey(long sumOfBytes, int port) {
this.sumOfBytes = sumOfBytes;
this.port = port;
}
public boolean equals(Object o) {
// bcos its private...
RegKey rk = (RegKey) o;
return rk.port == port && rk.sumOfBytes == sumOfBytes;
}
public int hashCode() {
return ((int) sumOfBytes * 17) + port * 23;
}
}
private StreamerAckRegister() {
}
}