package org.araqne.logdb.msgbus;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
import java.util.zip.GZIPOutputStream;
import org.araqne.api.NamedThreadFactory;
import org.araqne.codec.Base64;
import org.araqne.codec.FastEncodingRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StreamingResultEncoder {
private final Logger slog = LoggerFactory.getLogger(StreamingResultEncoder.class);
private ThreadPoolExecutor executor;
private int poolSize;
public StreamingResultEncoder(String name, int poolSize) {
if (poolSize < 1)
throw new IllegalArgumentException("pool size should be positive");
this.poolSize = poolSize;
this.executor = new ThreadPoolExecutor(poolSize, poolSize, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(
poolSize), new NamedThreadFactory(name), new CallerRunsPolicy());
slog.info("araqne logdb: created encoder thread pool [{}]", poolSize);
}
public List<Map<String, Object>> encode(List<Object> rows, boolean useGzip) throws InterruptedException, ExecutionException {
int flushSize = (rows.size() + poolSize) / poolSize;
List<Map<String, Object>> chunks = new ArrayList<Map<String, Object>>();
List<Future<Map<String, Object>>> futures = new ArrayList<Future<Map<String, Object>>>();
int total = rows.size();
int from = 0;
boolean exit = false;
while (!exit) {
int to = from + flushSize;
if (to >= total) {
to = total;
exit = true;
}
List<Object> slice = rows.subList(from, to);
Future<Map<String, Object>> future = executor.submit(new Encoder(slice, useGzip));
futures.add(future);
from = to;
}
for (Future<Map<String, Object>> f : futures) {
do {
Map<String, Object> chunk = f.get();
if (chunk != null) {
chunks.add(chunk);
if (slog.isDebugEnabled()) {
int original = (Integer) chunk.get("size");
int compressed = ((byte[]) chunk.get("bin")).length;
slog.debug("araqne logdb: compressed chunk size [{}] original size [{}]", compressed, original);
}
}
} while (!f.isDone());
}
return chunks;
}
public void close() {
executor.shutdown();
slog.info("araqne logdb: closed encoder thread pool [{}]", poolSize);
}
private class Encoder extends FunctorBase<Map<String, Object>> {
private List<Object> rows;
private boolean useGzip;
public Encoder(List<Object> rows, boolean useGzip) {
super(slog);
this.rows = rows;
this.useGzip = useGzip;
}
@Override
@SuppressWarnings("unchecked")
protected Map<String, Object> callSafely() throws Exception {
// row-oriented to column-oriented
int len = rows.size();
Map<String, Object[]> columns = new HashMap<String, Object[]>();
int i = 0;
for (Object o : rows) {
Map<String, Object> rows = (Map<String, Object>) o;
for (Entry<String, Object> e : rows.entrySet()) {
String key = e.getKey();
Object[] items = columns.get(key);
if (items == null) {
items = new Object[len];
columns.put(key, items);
}
items[i] = e.getValue();
}
i++;
}
// encode and compress
Map<String, Object> msg = new HashMap<String, Object>();
FastEncodingRule enc = new FastEncodingRule();
ByteBuffer bb = enc.encode(columns);
ByteBuffer compressed = null;
int compressedSize = 0;
if (useGzip) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream zos = null;
try {
zos = new GZIPOutputStream(bos);
zos.write(bb.array());
zos.finish();
byte[] out = bos.toByteArray();
compressed = ByteBuffer.wrap(out);
compressedSize = out.length;
} finally {
if (zos != null)
zos.close();
}
} else {
Deflater c = new Deflater();
try {
c.setInput(bb.array(), 0, bb.array().length);
c.finish();
compressed = ByteBuffer.allocate(bb.array().length * 2);
compressedSize = c.deflate(compressed.array());
compressed = ByteBuffer.wrap(Arrays.copyOf(compressed.array(), compressedSize));
} finally {
c.end();
}
}
msg.put("size", bb.array().length);
msg.put("bin", new String(Base64.encode(compressed.array())));
return msg;
}
}
private abstract class FunctorBase<T> implements Callable<T> {
private final Logger logger;
public FunctorBase(Logger logger) {
this.logger = logger;
}
@Override
public final T call() {
try {
return callSafely();
} catch (Throwable t) {
if (logger != null)
logger.error("unexpected error while running Task", t);
else {
System.err.println("unexpected error while running Task");
t.printStackTrace(System.err);
}
throw new IllegalStateException(t);
}
}
protected abstract T callSafely() throws Exception;
}
}