/*
* Lilith - a log event viewer.
* Copyright (C) 2007-2015 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2015 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.lilith.sender;
import de.huxhorn.sulky.io.IOUtilities;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class SimpleSendBytesService
implements SendBytesService
{
/**
* The default reconnection delay (30000 milliseconds or 30 seconds).
*/
public static final int DEFAULT_RECONNECTION_DELAY = 30000;
public static final int DEFAULT_QUEUE_SIZE = 1000;
public static final int DEFAULT_POLL_INTERVAL = 100;
private final Object lock = new Object();
private final BlockingQueue<byte[]> localEventBytes;
private WriteByteStrategy writeByteStrategy;
private DataOutputStreamFactory dataOutputStreamFactory;
//private boolean shutdown;
private final long reconnectionDelay;
private final int queueSize;
private final int pollInterval;
private final AtomicReference<ConnectionState> connectionState=new AtomicReference<>(ConnectionState.Offline);
private final AtomicBoolean shutdown=new AtomicBoolean(false);
private SendBytesThread sendBytesThread;
private boolean debug;
public SimpleSendBytesService(DataOutputStreamFactory dataOutputStreamFactory, WriteByteStrategy writeByteStrategy)
{
this(dataOutputStreamFactory, writeByteStrategy, DEFAULT_QUEUE_SIZE, DEFAULT_RECONNECTION_DELAY, DEFAULT_POLL_INTERVAL);
}
public SimpleSendBytesService(DataOutputStreamFactory dataOutputStreamFactory, WriteByteStrategy writeByteStrategy, int queueSize, long reconnectionDelay, int pollInterval)
{
Objects.requireNonNull(dataOutputStreamFactory, "dataOutputStreamFactory must not be null!");
Objects.requireNonNull(writeByteStrategy, "writeByteStrategy must not be null!");
if(queueSize <= 0)
{
throw new IllegalArgumentException("queueSize must be greater than zero but was "+queueSize+"!");
}
if(reconnectionDelay <= 0)
{
throw new IllegalArgumentException("reconnectionDelay must be greater than zero but was "+reconnectionDelay+"!");
}
if(pollInterval <= 0)
{
throw new IllegalArgumentException("pollInterval must be greater than zero but was "+pollInterval+"!");
}
this.localEventBytes = new ArrayBlockingQueue<>(queueSize, true);
this.dataOutputStreamFactory = dataOutputStreamFactory;
this.writeByteStrategy = writeByteStrategy;
this.queueSize = queueSize;
this.reconnectionDelay = reconnectionDelay;
this.pollInterval = pollInterval;
}
public boolean isDebug()
{
return debug;
}
public void setDebug(boolean debug)
{
this.debug = debug;
}
public ConnectionState getConnectionState()
{
return connectionState.get();
}
public void sendBytes(byte[] bytes)
{
if(connectionState.get() == ConnectionState.Connected && sendBytesThread != null && bytes != null)
{
try
{
localEventBytes.put(bytes);
}
catch(InterruptedException e)
{
IOUtilities.interruptIfNecessary(e);
}
}
}
public void startUp()
{
synchronized(lock)
{
if(sendBytesThread == null)
{
shutdown.set(false);
sendBytesThread = new SendBytesThread();
sendBytesThread.start();
}
}
}
public void shutDown()
{
shutdown.set(true);
synchronized(lock)
{
connectionState.set(ConnectionState.Canceled);
}
if(sendBytesThread != null)
{
sendBytesThread.interrupt();
try
{
sendBytesThread.join();
}
catch(InterruptedException e)
{
// this is ok
}
sendBytesThread = null;
}
localEventBytes.clear();
}
private class SendBytesThread
extends Thread
{
private DataOutputStream dataOutputStream;
SendBytesThread()
{
super("SendBytes@" + dataOutputStreamFactory);
setDaemon(true);
}
public void closeConnection()
{
synchronized(lock)
{
if(dataOutputStream != null)
{
//IOUtilities.closeQuietly(dataOutputStream);
// the above call can result in a ClassNotFoundException if a
// webapp is already unloaded!!!
try
{
dataOutputStream.close();
}
catch(IOException e)
{
// ignore
}
dataOutputStream = null;
if(connectionState.get() != ConnectionState.Canceled)
{
connectionState.set(ConnectionState.Offline);
}
if(debug)
{
System.err.println("Closed dataOutputStream.");
}
}
lock.notifyAll();
}
}
public void run()
{
Thread reconnectionThread = new ReconnectionThread();
reconnectionThread.start();
List<byte[]> copy = new ArrayList<>(queueSize);
for(;;)
{
try
{
localEventBytes.drainTo(copy);
if(copy.size() > 0)
{
DataOutputStream outputStream;
synchronized(lock)
{
outputStream = dataOutputStream;
}
if(outputStream != null)
{
try
{
for(byte[] current : copy)
{
writeByteStrategy.writeBytes(outputStream, current);
}
outputStream.flush();
}
catch(Throwable e)
{
IOUtilities.interruptIfNecessary(e);
closeConnection();
}
}
copy.clear();
}
if(shutdown.get())
{
break;
}
Thread.sleep(pollInterval);
}
catch(InterruptedException e)
{
IOUtilities.interruptIfNecessary(e);
break;
}
}
reconnectionThread.interrupt();
try
{
reconnectionThread.join();
}
catch(InterruptedException e)
{
// this is ok.
}
closeConnection();
}
private class ReconnectionThread
extends Thread
{
ReconnectionThread()
{
super("Reconnection@" + dataOutputStreamFactory);
setDaemon(true);
}
public void run()
{
for(;;)
{
boolean connect = false;
synchronized(lock)
{
if(dataOutputStream == null && connectionState.get() != ConnectionState.Canceled)
{
connect = true;
connectionState.set(ConnectionState.Connecting);
}
}
DataOutputStream newStream = null;
if(connect)
{
try
{
newStream = dataOutputStreamFactory.createDataOutputStream();
}
catch(IOException e)
{
// ignore
}
}
synchronized(lock)
{
if(connect)
{
if(newStream != null)
{
if(connectionState.get() == ConnectionState.Canceled)
{
// cleanup
try
{
newStream.close();
}
catch(IOException e)
{
// ignore
}
}
else
{
dataOutputStream = newStream;
connectionState.set(ConnectionState.Connected);
}
}
else if(connectionState.get() != ConnectionState.Canceled)
{
connectionState.set(ConnectionState.Offline);
}
}
try
{
lock.wait(reconnectionDelay);
}
catch(InterruptedException e)
{
IOUtilities.interruptIfNecessary(e);
return;
}
}
}
}
}
}
}