LegacyLogWriteExecutor.java
/*
** Module : LegacyLogWriteExecutor.java
** Abstract : Implementation for synchronization of LOG-MANAGER instances when writing to one log file.
**
** Copyright (c) 2014-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GBB 20230110 Initial implementation of LegacyLogWriteExecutor to enable synchronized writing to the
** same file.
** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util;
import com.goldencode.p2j.util.logging.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
/** A single point of executing legacy LOG-MANAGER file write actions. */
public class LegacyLogWriteExecutor
{
/** A temporary logger to just write messages to server's log. */
private static final CentralLogger LOG = CentralLogger.get(LegacyLogWriteExecutor.class);
/** Initialization on demand holder for LegacyLogManagerQueue singleton initialization. */
private static class LegacyLogManagerQueueHolder
{
/** Instance of LegacyLogManagerQueue. */
private static final LegacyLogWriteExecutor INSTANCE = new LegacyLogWriteExecutor();
}
/** Thread factory for the thread executor providing threads for writing to log files with rotation. */
private static final ThreadFactory THREAD_FACTORY_ROTATION = new ExecutorsThreadFactory(true);
/** Thread factory for the thread executor providing threads for writing to log files without rotation. */
private static final ThreadFactory THREAD_FACTORY_NO_ROTATION = new ExecutorsThreadFactory(false);
/** Mutex for rotation related actions. */
private final Object rotationMutex = new Object();
/** Map with absolute log file path : client pid pairs for clients that need file rotation. */
private final Map<String, Long> rotationPathToPidMap = new HashMap<>();
/** Map with absolute log file path : executor service pairs for clients that need file rotation. */
private final Map<String, ExecutorService> rotationPathToExecutorServicedMap = new HashMap<>();
/** Mutex for no rotation related actions. */
private final Object noRotationMutex = new Object();
/** Map with absolute log file path : client pids pairs for clients that don't need file rotation. */
private final Map<String, Set<Long>> noRotationPathToPidMap = new HashMap<>();
/** Map with absolute log file path : executor service pairs for clients that don't need file rotation. */
private final Map<String, ExecutorService> noRotationPathToExecutorServicedMap = new HashMap<>();
/**
* Getter for LegacyLogManagerQueue singleton instance.
*
* @return instance of LegacyLogManagerQueue.
*/
public static LegacyLogWriteExecutor get()
{
return LegacyLogManagerQueueHolder.INSTANCE;
}
/** Private constructor for LegacyLogManagerQueue. */
private LegacyLogWriteExecutor()
{
}
/**
* Closes the ExecutorService executing write tasks for the file path if no other pids are using it.
*
* @param pid
* The process id of the client writing to the file.
* @param path
* Absolute path to the file to write to.
* @param isRotation
* Flag to indicate the file gets rotated.
*
* @return Always returns <code>true</code>.
*/
public synchronized boolean close(long pid, String path, boolean isRotation)
{
if (isRotation)
{
synchronized(rotationMutex)
{
rotationPathToPidMap.remove(path);
ExecutorService es = rotationPathToExecutorServicedMap.remove(path);
if (es != null)
{
es.shutdown();
}
}
}
else
{
synchronized(noRotationMutex)
{
Set<Long> pids = noRotationPathToPidMap.get(path);
if (pids == null) {
return true;
}
pids.remove(pid);
if (pids.isEmpty())
{
ExecutorService es = noRotationPathToExecutorServicedMap.remove(path);
if (es != null)
{
es.shutdown();
}
}
}
}
return true;
}
/**
* Each unique path has a dedicated thread and a queue (newSingleThreadExecutor). In the case of
* rotation, only one client / pid will be able to add to the queue. In the case of no rotation,
* multiple clients / pids can write to the same path by adding tasks to the same queue.
*
* @param pid
* The process id of the client writing to the file.
* @param path
* Absolute path to the file to write to.
* @param isRotation
* Flag to indicate the file gets rotated.
* @param writer
* The prepared FileWriter for writing to the file.
* @param msg
* The log message.
*
* @return Empty Optional when the process is not allowed to write to the file (in case of rotation
* and another process already writing). Otherwise the Optional contains Future for the write
* task execution that returns a boolean.
*/
public Optional<Future<Boolean>> write(long pid,
String path,
boolean isRotation,
FileWriter writer,
String msg)
{
if (isRotation)
{
synchronized(rotationMutex)
{
rotationPathToPidMap.putIfAbsent(path, pid);
if (pid == rotationPathToPidMap.get(path))
{
rotationPathToExecutorServicedMap.putIfAbsent(path,
Executors.newSingleThreadExecutor(THREAD_FACTORY_ROTATION));
ExecutorService es = rotationPathToExecutorServicedMap.get(path);
return Optional.of(write(es, writer, msg));
}
}
// only one process can write to the same path
return Optional.empty();
}
synchronized(noRotationMutex)
{
noRotationPathToPidMap.putIfAbsent(path, new HashSet<>());
noRotationPathToPidMap.get(path).add(pid);
noRotationPathToExecutorServicedMap.putIfAbsent(path,
Executors.newSingleThreadExecutor(THREAD_FACTORY_NO_ROTATION));
ExecutorService es = noRotationPathToExecutorServicedMap.get(path);
return Optional.of(write(es, writer, msg));
}
}
/**
* Creates a new WriteCallable and submits it to the ExecutorService.
*
* @param es
* The executor service to execute the WriteCallable.
* @param writer
* The prepared FileWriter for writing to the file.
* @param msg
* The log message.
*
* @return Future for the write task execution that returns a boolean.
*/
private Future<Boolean> write(ExecutorService es, FileWriter writer, String msg)
{
return es.submit(new WriteCallable(writer, msg));
}
/**
* Private static class ExecutorsThreadFactory to provide meaningful names for threads used by the
* executors.
*/
private static class ExecutorsThreadFactory implements ThreadFactory
{
/** An identifier for the type of thread. */
private final String rotationFix;
/** Counter for threads created by the factory to provide unique identification. */
private final AtomicInteger count = new AtomicInteger(1);
/** Package private constructor for ExecutorsThreadFactory */
ExecutorsThreadFactory(boolean hasRotation)
{
rotationFix = hasRotation ? "-Rotation-" : "-NoRotation-";
}
/**
* Creates a new thread for the provided runnable and assigns a name to it that identifies it uniquely.
*
* @param runnable
* The task to be executed by the thread.
*
* @return The new thread.
*/
@Override
public Thread newThread(Runnable runnable)
{
Thread newThread = new Thread(runnable);
newThread.setName(LegacyLogWriteExecutor.class.getSimpleName() + rotationFix + count.getAndIncrement());
return newThread;
}
}
/** Private static class WriteCallable that extends Callable<Boolean>. */
private static class WriteCallable
implements Callable<Boolean>
{
/** The prepared FileWriter for writing to the file. */
private final FileWriter writer;
/** The log message. */
private final String msg;
/** Package-private constructor for WriteCallable. */
WriteCallable(FileWriter writer, String msg)
{
this.writer = writer;
this.msg = msg;
}
/**
* Writes the message using the FileWriter.
*
* @return <code>true</code> if the write was successful, <code>false</code> otherwise.
*/
public Boolean call()
{
try
{
writer.write(msg);
writer.flush();
return Boolean.TRUE;
}
catch (Exception e)
{
LOG.log(Level.FINE, "Writing to the LOG-MANAGER log file failed for msg: " + msg, e);
return Boolean.FALSE;
}
}
}
}