package krasa.grepconsole.grep; import com.google.common.math.IntMath; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.Disposable; import com.intellij.openapi.util.io.FileUtil; import com.lmax.disruptor.*; import com.lmax.disruptor.dsl.Disruptor; import com.lmax.disruptor.dsl.ProducerType; import com.squareup.tape.QueueFile; import krasa.grepconsole.grep.listener.EventConsumer; import org.apache.commons.lang.builder.ToStringBuilder; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; public class HybridQueue implements Disposable { private static final Logger log = LoggerFactory.getLogger(HybridQueue.class); private LongEventProducerWithTranslator bufferProducer; private Disruptor<LogEvent> disruptor; private FileBackingQueue fileBackingQueue; private State state; public HybridQueue(EventConsumer eventConsumer) { // The factory for the event LongEventFactory factory = new LongEventFactory(); // Specify the size of the ring buffer, must be power of 2. int bufferSize = IntMath.pow(2, 16); //2^14 16k , ^16=65536 // Construct the Disruptor state = new State(); fileBackingQueue = new FileBackingQueue(state); // Connect the handler ThreadFactory grepConsole = new ThreadFactory() { @Override public Thread newThread(@NotNull Runnable r) { Thread thread = new Thread(r, "GrepConsole"); thread.setDaemon(true); return thread; } }; this.disruptor = new Disruptor<>(factory, bufferSize, grepConsole, ProducerType.MULTI, new BlockingWaitStrategy()); this.disruptor.handleEventsWith(new LogEventHandler(this.disruptor.getRingBuffer(), fileBackingQueue, state, eventConsumer)); // Start the Disruptor, starts all threads running this.disruptor.start(); // Get the ring buffer from the Disruptor to be used for publishing. RingBuffer<LogEvent> ringBuffer = this.disruptor.getRingBuffer(); bufferProducer = new LongEventProducerWithTranslator(fileBackingQueue, ringBuffer, state); } public void onData(String s, ConsoleViewContentType type) { bufferProducer.onData(s); } @Override public void dispose() { try { try { disruptor.shutdown(0, TimeUnit.NANOSECONDS); // if shutdown is successful: // 1. exception is not thrown (obviously) // Disruptor.halt() is called automatically (less obvious) } catch (TimeoutException e) { disruptor.halt(); } } catch (Exception e) { e.printStackTrace(); } fileBackingQueue.dispose(); } public void clearStats() { state.clear(); } public static class LongEventProducerWithTranslator { private final FileBackingQueue fileBackingQueue; private final RingBuffer<LogEvent> ringBuffer; private final State state; public LongEventProducerWithTranslator(FileBackingQueue fileBackingQueue, RingBuffer<LogEvent> ringBuffer, State state) { this.fileBackingQueue = fileBackingQueue; this.ringBuffer = ringBuffer; this.state = state; } public void onData(String bb) { state.producerDelay(); state.itemsProduced.incrementAndGet(); add(bb, 0); } protected void add(String bb, int tries) { if (tries > 1) { throw new IllegalStateException( "unable to add to queue. " + state + "; " + ringBuffer + "; " + fileBackingQueue); } boolean added; if (!state.isFileBacking()) { // System.out.println("added to ringBuffer " + bb); boolean fileBackingEnabled = false; if (fileBackingEnabled) { added = ringBuffer.tryPublishEvent(TRANSLATOR, bb, null, null); } else { ringBuffer.publishEvent(TRANSLATOR, bb, null, null); added = true; } if (!added) { // System.out.println("ringBuffer full, queing to file"); fileBackingQueue.activateFileBacking(); added = fileBackingQueue.tryAdd(bb); } } else { if (!fileBackingQueue.isQueuePublished()) { fileBackingQueue.tryPublishFileQueue(ringBuffer); } added = fileBackingQueue.tryAdd(bb); } if (!added) { // System.out.println("item not added, race condition, trying to add again " + state); add(bb, ++tries); } } } public static class LogEventHandler implements EventHandler<LogEvent> { private final RingBuffer<LogEvent> ringBuffer; private final FileBackingQueue fileBackingQueue; private final State state; private final EventConsumer eventConsumer; public LogEventHandler(RingBuffer<LogEvent> ringBuffer, FileBackingQueue fileBackingQueue, State state, EventConsumer eventConsumer) { this.ringBuffer = ringBuffer; this.fileBackingQueue = fileBackingQueue; this.state = state; this.eventConsumer = eventConsumer; } public void onEvent(LogEvent event, long sequence, boolean endOfBatch) { try { state.consumerDelay(); if (event.getFileBuffer() == null) { processEvent(event.get()); event.set((ConsoleViewContentType) null); event.set((QueueFile) null); event.set((ConsoleViewContentType) null); if (!fileBackingQueue.isQueuePublished()) { fileBackingQueue.tryPublishFileQueue(ringBuffer); } } else { fileBackingQueue.read(this, event.getFileBuffer()); } } catch (Throwable e) { e.printStackTrace(); log.error(e.getMessage(), e); } } private void processEvent(String s) { eventConsumer.processEvent(s); long l = state.itemsConsumed.incrementAndGet(); // if (l != Long.valueOf(s)) { // throw new RuntimeException(l + "!=" + s); // } } } public static class FileBackingQueue { private final State state; private QueueFile lastFileQueue; private boolean queuePublished = true; private File lastFile; public FileBackingQueue(State state) { this.state = state; } public synchronized QueueFile activateFileBacking() { if (state.fileBacking.get()) { // System.out.println("FileBacking already activated - race condition"); return lastFileQueue; } System.out.println("activateFileBacking " + state); state.fileBacking.set(true); try { lastFile = FileUtil.generateRandomTemporaryPath(); lastFileQueue = new QueueFile(lastFile); queuePublished = false; return lastFileQueue; } catch (IOException e) { throw new RuntimeException(e); } } private synchronized void deactivateFileBacking() { // System.out.println("deactivateFileBacking"); if (!queuePublished) { throw new IllegalStateException("fileQueue not published, therefore processed, yet it is being disabled, fuck"); } state.fileBacking.set(false); } public synchronized boolean isQueuePublished() { return queuePublished; } public synchronized void tryPublishFileQueue(RingBuffer<LogEvent> ringBuffer) { if (queuePublished) { // System.out.println("queue already published, race condition"); return; } if (lastFileQueue == null) { throw new IllegalStateException("publishing null lastFileQueue, fuck"); } if (ringBuffer.tryPublishEvent(TRANSLATOR, null, null, lastFileQueue)) { // System.out.println("FileQueue published to ringBuffer"); queuePublished = true; } else { // System.out.println("FileQueue not published - ringBuffer full - will try on the next event"); } } public synchronized boolean tryAdd(String s) { if (!state.isFileBacking()) { // System.out.println("fileBacking is deactivated - race condition"); return false; } // System.out.println("adding to fileQueue " + s); try { lastFileQueue.add(s.getBytes()); } catch (IOException e) { throw new RuntimeException(e); } return true; } public void read(final LogEventHandler logEventConsumer, QueueFile fileBuffer) { try { while (readAll(fileBuffer, logEventConsumer) > 0) { } deactivateFileBacking(); readAll(fileBuffer, logEventConsumer); fileBuffer.close(); } catch (Exception e) { throw new RuntimeException(e); } } protected int readAll(QueueFile fileBuffer, final LogEventHandler logEventConsumer) throws IOException { long start = System.currentTimeMillis(); int i = 0; while (true) { byte[] bytes = fileBuffer.peek(); if (bytes == null) { break; } i++; fileBuffer.remove(); logEventConsumer.processEvent(new String(bytes)); } // System.out.println("readAll " + i + " " + (System.currentTimeMillis() - start)); return i; } public void dispose() { QueueFile lastFileQueue = this.lastFileQueue; if (lastFileQueue != null) { try { lastFileQueue.close(); lastFile.delete(); } catch (IOException e) { e.printStackTrace(); } } } } private static final EventTranslatorThreeArg<LogEvent, String, ConsoleViewContentType, QueueFile> TRANSLATOR = new EventTranslatorThreeArg<LogEvent, String, ConsoleViewContentType, QueueFile>() { @Override public void translateTo(LogEvent event, long sequence, String arg0, ConsoleViewContentType arg1, QueueFile arg2) { event.set(arg0); event.set(arg1); event.set(arg2); } }; public static class LongEventFactory implements EventFactory<LogEvent> { public LogEvent newInstance() { return new LogEvent(); } } public static class LogEvent { private String value; private ConsoleViewContentType type; private QueueFile fileBuffer; public String get() { return value; } public void set(String value) { this.value = value; } public void set(ConsoleViewContentType arg1) { type = arg1; } public void set(QueueFile arg2) { fileBuffer = arg2; } public String getValue() { return value; } public ConsoleViewContentType getType() { return type; } public QueueFile getFileBuffer() { return fileBuffer; } } public static class State { AtomicBoolean fileBacking = new AtomicBoolean(); AtomicLong itemsConsumed = new AtomicLong(); AtomicLong itemsProduced = new AtomicLong(); public boolean isFileBacking() { return fileBacking.get(); } @Override public String toString() { return new ToStringBuilder(this) .append("fileBacking", fileBacking.get()) .append("itemsConsumed", itemsConsumed.get()) .append("itemsProduced", itemsProduced.get()) .toString(); } protected void producerDelay() { } protected void consumerDelay() { } public void clear() { itemsConsumed.set(0); itemsProduced.set(0); } } }