/* * ***************************************************************************** * Copyright (C) 2014-2022 Dennis Sheirer * * 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 3 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, see * **************************************************************************** */ package io.github.dsheirer.util; import io.github.dsheirer.sample.Listener; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Threaded processor for receiving elements from a separate producer thread and forwarding those buffers to a * registered listener on this consumer/dispatcher thread. */ public class Dispatcher implements Listener { private final static Logger mLog = LoggerFactory.getLogger(Dispatcher.class); private static final long OVERFLOW_LOG_EVENT_WAIT_PERIOD = TimeUnit.SECONDS.toMillis(10); private LinkedBlockingQueue mQueue; private Listener mListener; private AtomicBoolean mRunning = new AtomicBoolean(); private String mThreadName; private Thread mThread; private E mPoisonPill; private long mLastOverflowLogEvent; /** * Constructs an instance * @param maxSize of the internal queue * @param threadName to name the dispatcher thread * @param poisonPill of type E, used to kill the thread */ public Dispatcher(int maxSize, String threadName, E poisonPill) { if(poisonPill == null) { throw new IllegalArgumentException("Poison pill must be non-null"); } mQueue = new LinkedBlockingQueue<>(maxSize); mThreadName = threadName; mPoisonPill = poisonPill; } /** * Listener to receive the queued buffers each time this processor runs. */ protected Listener getListener() { return mListener; } /** * Sets or changes the listener to receive buffers from this processor. * @param listener to receive buffers */ public void setListener(Listener listener) { mListener = listener; } /** * Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this * processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer * processing. * * @param e to enqueue for distribution to a registered listener */ public void receive(E e) { if(mRunning.get()) { if(!mQueue.offer(e)) { if(System.currentTimeMillis() > (mLastOverflowLogEvent + OVERFLOW_LOG_EVENT_WAIT_PERIOD)) { mLastOverflowLogEvent = System.currentTimeMillis(); mLog.warn("Temporary buffer overflow for thread [" + mThreadName + "] - throwing away samples"); } } } } /** * Starts this buffer processor and allows queuing of incoming buffers. */ public void start() { if(mRunning.compareAndSet(false, true)) { mThread = new Thread(new Processor()); mThread.setName(mThreadName); mThread.setPriority(Thread.MAX_PRIORITY); mThread.start(); } } /** * Stops this buffer processor and waits up to two seconds for the processing thread to terminate. */ public void stop() { if(mRunning.compareAndSet(true, false)) { mQueue.offer(mPoisonPill); try { mThread.join(2000); mThread = null; } catch(Exception e) { mLog.error("Timeout while waiting to join terminating buffer processor thread"); } } } /** * Finishes processing any queued buffers and then stops the processing thread. */ public void flushAndStop() { mQueue.offer(mPoisonPill); } /** * Indicates if this processor is currently running */ public boolean isRunning() { return mRunning.get(); } /** * Processor to service the buffer queue and distribute the buffers to the registered listener */ class Processor implements Runnable { @Override public void run() { mQueue.clear(); E element; while(mRunning.get()) { try { element = mQueue.take(); if(mPoisonPill == element) { mRunning.set(false); } else if(element != null) { if(mListener == null) { throw new IllegalStateException("Listener for [" + mThreadName + "] is null"); } mListener.receive(element); } } catch(InterruptedException e) { mLog.error("Buffer processor thread was interrupted"); } catch(Exception e) { mLog.error("Error while processing element", e); } } mQueue.clear(); } } }