I am working on a project where I need to consume lot of records and then I am sending these records to some other system which uses zeromq.
Here is the flow:
- Store all the incoming records in a CHM from multiple threads. Records will come at a very high speed.
- From a background thread which runs every 30 seconds, send these records from CHM to zeromq servers.
- After sending each record to zeromq servers, add them to a retry bucket as well so that it can be retried after a particular time if acknowledgment is not received for this record.
- We also have a poller runnable thread which receives acknowledgment from zeromq servers that tells these records have been received so once I get an acknowledgment back, I delete that record from retry bucket so that it doesn't get retried.
- Even if some records are sent multiple times it's ok but it's good to minimize this. I am not sure what is the best way to minimize this in my scenario.
Here is my Processor
class in which add
method will be called by multiple threads to populate dataHolderByPartitionReference
CHM in a thread safe way. And then in the constructor of Processor
class, I start the background thread which runs every 30 seconds to push records from same CHM to a zeromq servers by calling the SendToZeroMQ
class:
public class Processor {
private final ScheduledExecutorService executorService = Executors
.newSingleThreadScheduledExecutor();
// creating a ListeningExecutorService (Guava) by wrapping a normal ExecutorService (Java)
private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors
.newCachedThreadPool());
private final AtomicReference<ConcurrentHashMap<Integer, ConcurrentLinkedQueue<DataHolder>>> dataHolderByPartitionReference =
new AtomicReference<>(new ConcurrentHashMap<Integer, ConcurrentLinkedQueue<DataHolder>>());
private static class Holder {
private static final Processor INSTANCE = new Processor();
}
public static Processor getInstance() {
return Holder.INSTANCE;
}
private Processor() {
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
validateAndSendAllPartitions(dataHolderByPartitionReference
.getAndSet(new ConcurrentHashMap<Integer, ConcurrentLinkedQueue<DataHolder>>()));
}
}, 0, 30, TimeUnit.SECONDS);
}
// calling validateAndSend in parallel for each partition
// generally there will be only 5-6 unique partitions max
private void validateAndSendAllPartitions(
ConcurrentHashMap<Integer, ConcurrentLinkedQueue<DataHolder>> dataHolderByPartition) {
List<ListenableFuture<Void>> list = new ArrayList<ListenableFuture<Void>>();
// For each partition, create an independent thread that will
// validate the dataHolder and send it to the zeromq servers
for (Entry<Integer, ConcurrentLinkedQueue<DataHolder>> entry : dataHolderByPartition
.entrySet()) {
final int partition = entry.getKey();
final ConcurrentLinkedQueue<DataHolder> dataHolders = entry.getValue();
ListenableFuture<Void> future = executor.submit(new Callable<Void>() {
public Void call() throws Exception {
validateAndSend(partition, dataHolders);
return null;
}
});
// Add the future to the list
list.add(future);
}
// We want to know when ALL the threads have completed,
// so we use a Guava function to turn a list of ListenableFutures
// into a single ListenableFuture
ListenableFuture<List<Void>> combinedFutures = Futures.allAsList(list);
// The get on the combined ListenableFuture will now block until
// ALL the individual threads have completed work.
try {
List<Void> allPartitionDataHolders = combinedFutures.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
// log error
} catch (ExecutionException ex) {
// log error
}
}
private void validateAndSend(final int partition,
final ConcurrentLinkedQueue<DataHolder> dataHolders) {
Map<byte[], byte[]> clientKeyBytesAndProcessBytesHolder = new HashMap<>();
int totalSize = 0;
while (!dataHolders.isEmpty()) {
DataHolder dataHolder = dataHolders.poll();
byte[] clientKeyBytes = dataHolder.getClientKey().getBytes(StandardCharsets.UTF_8);
if (clientKeyBytes.length > 255)
continue;
byte[] processBytes = dataHolder.getProcessBytes();
int clientKeyLength = clientKeyBytes.length;
int processBytesLength = processBytes.length;
int additionalLength = clientKeyLength + processBytesLength;
if (totalSize + additionalLength > 64000) {
SendToZeroMQ.getInstance().executeAsync(partition, clientKeyBytesAndProcessBytesHolder);
clientKeyBytesAndProcessBytesHolder.clear(); // watch out for gc
totalSize = 0;
}
clientKeyBytesAndProcessBytesHolder.put(clientKeyBytes, processBytes);
totalSize += additionalLength;
}
// calling again with remaining values
SendToZeroMQ.getInstance().executeAsync(partition, clientKeyBytesAndProcessBytesHolder);
}
// called by multiple threads to populate dataHolderByPartitionReference CHM
public void add(final int partition, final DataHolder holder) {
ConcurrentMap<Integer, ConcurrentLinkedQueue<DataHolder>> dataHolderByPartition =
dataHolderByPartitionReference.get();
ConcurrentLinkedQueue<DataHolder> dataHolder =
dataHolderByPartition.get(partition);
if (dataHolder == null) {
dataHolder = Queues.newConcurrentLinkedQueue();
ConcurrentLinkedQueue<DataHolder> currentDataHolder =
dataHolderByPartition.putIfAbsent(partition, dataHolder);
if (currentDataHolder != null)
dataHolder = currentDataHolder;
}
dataHolder.add(holder);
}
}
Here is my SendToZeroMQ
class which sends record to zeromq servers and retry accordingly depending on acknowledgment.
- Firstly it will send record to zeromq servers.
- Then it will add same record to
retryBucket
which will get retried later on depending on whether acknowledgment is received or not. - In the same class, I start a background thread which runs every 1 minute to send records again which are in the retry bucket.
- Same class also starts the
ResponsePoller
thread which will keep running forever to see what records have been acknowledged (which we have sent before) so as soon as records are acknowledged, theResponsePoller
thread will remove those record fromretryBucket
so that it doesn't get retried.
SendToZeroMQ
public class SendToZeroMQ {
// do I need these two ScheduledExecutorService or one is sufficient to start my both the thread?
private final ScheduledExecutorService executorServicePoller = Executors
.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService executorService = Executors
.newSingleThreadScheduledExecutor();
private final Cache<Long, byte[]> retryBucket = CacheBuilder.newBuilder().maximumSize(10000000)
.removalListener(RemovalListeners.asynchronous(new CustomListener(), executorService))
.build();
private static class Holder {
private static final SendToZeroMQ INSTANCE = new SendToZeroMQ();
}
public static SendToZeroMQ getInstance() {
return Holder.INSTANCE;
}
private SendToZeroMQ() {
executorServicePoller.submit(new ResponsePoller());
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
for (Entry<Long, byte[]> entry : retryBucket.asMap().entrySet()) {
executeAsync(entry.getKey(), entry.getValue());
}
}
}, 0, 1, TimeUnit.MINUTES);
}
public boolean executeAsync(final long address, final byte[] encodedByteArray) {
Optional<ZMQObj> liveSockets = PoolManager.getInstance().getNextSocket();
if (!liveSockets.isPresent()) {
return false;
}
return executeAsync(address, encodedByteArray, liveSockets.get().getSocket());
}
public boolean executeAsync(final long address, final byte[] encodedByteArray, final Socket socket) {
ZMsg msg = new ZMsg();
msg.add(encodedByteArray);
boolean sent = msg.send(socket);
msg.destroy();
// add to retry bucket
retryBucket.put(address, encodedByteArray);
return sent;
}
public boolean executeAsync(final int partition,
final Map<byte[], byte[]> clientKeyBytesAndProcessBytesHolder) {
Optional<ZMQObj> liveSockets = PoolManager.getInstance().getNextSocket();
if (!liveSockets.isPresent()) {
return false;
}
Map<Long, byte[]> addressToencodedByteArray = encode(partition, clientKeyBytesAndProcessBytesHolder);
long address = addressToencodedByteArray.entrySet().iterator().next().getKey();
byte[] encodedByteArray = addressToencodedByteArray.entrySet().iterator().next().getValue();
return executeAsync(address, encodedByteArray, liveSockets.get().getSocket());
}
private Map<Long, byte[]> encode(final int partition,
final Map<byte[], byte[]> clientKeyBytesAndProcessBytesHolder) {
// this address will be unique always
long address = TestUtils.getAddress();
Frame frame = new Frame(............);
byte[] packedByteArray = frame.serialize();
// this map will always have one entry in it.
return ImmutableMap.of(address, packedByteArray);
}
public void removeFromRetryBucket(final long address) {
retryBucket.invalidate(address);
}
}
And here is my ResponsePoller
class which waits for the acknowledgment for all those records already sent by the other background thread. If acknowledgement is received, then delete it from the retry bucket so that it doesn't get retried.
public class ResponsePoller implements Runnable {
private static final Random random = new Random();
private static final int listenerPort = 8076;
@Override
public void run() {
ZContext ctx = new ZContext();
Socket client = ctx.createSocket(ZMQ.PULL);
// Set random identity to make tracing easier
String identity = String.format("%04X-%04X", random.nextInt(), random.nextInt());
client.setIdentity(identity.getBytes(ZMQ.CHARSET));
client.bind("tcp://" + TestUtils.getIPAddress() + ":" + listenerPort);
PollItem[] items = new PollItem[] {new PollItem(client, Poller.POLLIN)};
while (!Thread.currentThread().isInterrupted()) {
// Tick once per second, pulling in arriving messages
for (int centitick = 0; centitick < 100; centitick++) {
ZMQ.poll(items, 10);
if (items[0].isReadable()) {
ZMsg msg = ZMsg.recvMsg(client);
Iterator<ZFrame> it = msg.iterator();
while (it.hasNext()) {
ZFrame frame = it.next();
try {
long address = TestUtils.getAddress(frame.getData());
// remove from retry bucket since we got the acknowledgment for this record
SendToZeroMQ.getInstance().removeFromRetryBucket(address);
} catch (Exception ex) {
// log error
} finally {
frame.destroy();
}
}
msg.destroy();
}
}
}
ctx.destroy();
}
}
Is there a better way to design this problem as compared to what I have? I might be breaking Single Responsibility Principle
here. I am working with Java 7.