Compare commits

..
Author SHA1 Message Date
Claude 1aa0887fa9 Implement database connection initialization
- Created DatabaseManager class in config package to manage database connections
- Implemented singleton pattern with JPA EntityManagerFactory and HikariCP connection pooling
- Initialized database connection in MediaManagerApplication on startup
- Added proper shutdown cleanup for database resources in shutdown hook
- Uncommented Media entity class in persistence.xml to enable JPA mapping
- Resolved TODO: Initialize database connection (MediaManagerApplication.java:21)
2025-11-23 19:30:42 +00:00
Gustavo Henrique Santos Souza de Miranda 281618a725 docs: Add REFACTORING.md to document applied refactoring patterns and improvements. 2025-11-16 18:47:20 -03:00
16 changed files with 199 additions and 792 deletions
+1 -9
View File
@@ -39,16 +39,8 @@ build/
.DS_Store
### Application Specific ###
config-local.properties
application-local.properties
*.db
pipes/
*.log
/.idea/
Configuration files with sensitive credentials
# These files contain database passwords and API keys and should never be committed
src/main/resources/config.properties
src/main/resources/application.properties
# Allow example configuration files to be committed
!src/main/resources/*.properties.example
+5 -5
View File
@@ -34,7 +34,7 @@ MediaManager-Core/
│ │ ├── util/ # Utility classes
│ │ └── MediaManagerApplication.java
│ └── resources/
│ ├── config.properties # App configuration
│ ├── application.properties # App configuration
│ ├── log4j2.xml # Logging configuration
│ └── META-INF/
│ └── persistence.xml # JPA configuration
@@ -55,7 +55,7 @@ GRANT ALL PRIVILEGES ON DATABASE mediamanager TO mediamanager;
### 2. Configuration
Edit `src/main/resources/config.properties` and update the database credentials:
Edit `src/main/resources/application.properties` and update the database credentials:
```properties
db.url=jdbc:postgresql://localhost:5432/mediamanager
@@ -90,7 +90,7 @@ The application creates named pipes for inter-process communication. Default con
- Pipe name: `mediamanager-pipe`
- Buffer size: 8192 bytes
You can modify these settings in `config.properties`.
You can modify these settings in `application.properties`.
## Logging
@@ -116,8 +116,8 @@ mvn test
## Dependencies
- PostgreSQL Driver: 42.7.5
- Hibernate ORM: 7.1.7.Final
- PostgreSQL Driver: 42.7.3
- Hibernate ORM: 6.4.4.Final
- HikariCP: 5.1.0
- Log4j 2: 2.23.1
- Jackson: 2.16.1
+103
View File
@@ -0,0 +1,103 @@
# Refactoring History—MediaManager Core
This document records the refactorings applied during development.
## Refactoring #1: Connection Pool Implementation
**Date**: 2025-11-13
**Commits**: ```22476abb27d95e88ae0439d97ef97467b6a4f985```
### Problem Identified
- Database connections were created multiple times
- No centralized control
- Possible resource leaks
### Technique Applied
- **Pattern**: Singleton Pattern
- **Refactoring**: Extract Class + Introduce Singleton
### Solution
Created `DatabaseManager` class that:
- Maintains single instance (Singleton)
- Manages connection pool
- Provides centralized `getConnection()` method
### Result
- ✅ Single reusable connection
- ✅ Code centralized in one class
- ✅ Thread-safe
- ✅ Easier to test
### Extracted Component
`DatabaseManager` - available in `mediamanager-components`
---
## Refactoring #2: Thread Pool for IPC
**Date**: 2025-11-14
**Commits**: ```f1071dca03da33f7355a6927497509b5aedaa4a2```
### Problem Identified
- New thread created for each IPC client
- Doesn't scale well
- Thread creation overhead
### Technique Applied
- **Pattern**: Thread Pool Pattern
- **Refactoring**: Replace Thread Creation with Thread Pool
### Solution
Implemented `ExecutorService` with:
- Fixed thread pool
- Thread reuse
- Ordered shutdown
### Result
- ✅ Better performance
- ✅ Controlled resource usage
- ✅ Scalability
### Extracted Component
`IPCServer` - available in `mediamanager-components`
---
## Refactoring #3: Graceful Shutdown
**Date**: 2025-11-13
**Commits**: ```65e14930076db71a312feae373d6c5daee3c4d29```,```b7ff9d1e16c85bf986a770676827bee390f0d53c```
### Problem Identified
- Resources weren't released on exit
- Database connections remained open
- Thread pool didn't terminate correctly
### Technique Applied
- **Pattern**: Shutdown Hook Pattern
- **Refactoring**: Introduce Shutdown Protocol
### Solution
Implemented shutdown hooks that:
- Close database connections
- Terminate thread pools
- Release sockets
- In specific order with timeouts
### Result
- ✅ Ordered cleanup
- ✅ No resource leaks
- ✅ Shutdown logging
- ✅ Timeout to prevent hanging
### Extracted Component
Logic incorporated in `DatabaseManager` and `IPCServer`
---
## Applied Principles
- **DRY** (Don't Repeat Yourself): Elimination of duplicated code
- **SOLID**: Especially Single Responsibility Principle
- **Design Patterns**: Singleton, Thread Pool, Shutdown Hook
- **Clean Code**: Clear names, small methods, well-defined responsibilities
+1 -85
View File
@@ -55,7 +55,7 @@
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
@@ -78,29 +78,9 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.32.0</version>
</dependency>
<dependency>
<groupId>io.zonky.test</groupId>
<artifactId>embedded-postgres</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -111,75 +91,11 @@
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<!-- Protocol Buffers Plugin -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocExecutable>/usr/bin/protoc</protocExecutable>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Build Helper Plugin - Adiciona generated sources ao classpath -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/protobuf/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Shade Plugin - Creates executable JAR with all dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.mediamanager.MediaManagerApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -1,7 +1,6 @@
package com.mediamanager;
import com.mediamanager.service.delegate.DelegateActionManager;
import com.mediamanager.service.ipc.IPCManager;
import com.mediamanager.config.DatabaseManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -9,25 +8,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import com.mediamanager.service.database.LocalDatabaseManager;
public class MediaManagerApplication {
private static final Logger logger = LogManager.getLogger(MediaManagerApplication.class);
private static Properties config;
private static LocalDatabaseManager databaseManager;
private static DelegateActionManager actionManager;
private static IPCManager ipcManager;
public enum ApplicationMode {
LOCAL("local"),
SERVER("server");
private final String value;
ApplicationMode(String value) {
this.value = value;
}
}
private static DatabaseManager databaseManager;
public static void main(String[] args) {
logger.info("Starting MediaManager Core Application...");
@@ -35,85 +19,23 @@ public class MediaManagerApplication {
try {
// Load configuration
loadConfiguration();
String runTypeString = config.getProperty("runtype","local");
ApplicationMode mode = null;
for (ApplicationMode am : ApplicationMode.values()) {
if (am.value.equalsIgnoreCase(runTypeString)) {
mode = am;
break;
}
}
if (mode == null) {
logger.error("Invalid run type: {}", runTypeString);
throw new Exception("Invalid run type: " + runTypeString);
}
logger.info("Run type: {}", mode);
switch (mode) {
case LOCAL:
logger.info("Starting local database...");
databaseManager = new LocalDatabaseManager(config);
break;
case SERVER:
throw new Exception("Server mode not yet implemented");
default:
}
databaseManager.init();
actionManager = new DelegateActionManager();
actionManager.start();
ipcManager = new IPCManager(config,actionManager);
ipcManager.init();
// Initialize database connection
databaseManager = DatabaseManager.getInstance(config);
// TODO: Initialize IPC server with named pipes
// TODO: Start application services
logger.info("MediaManager Core started successfully");
logger.info("IPC Socket: {}", ipcManager.getSocketPath().toAbsolutePath().toString());
logger.info("IPC Pipe: {}", config.getProperty("ipc.pipe.path") + "/" + config.getProperty("ipc.pipe.name"));
// Keep application running
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Shutting down MediaManager Core...");
if (databaseManager != null) {
databaseManager.close();
databaseManager.shutdown();
}
if (ipcManager != null) {
try {
ipcManager.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
if (actionManager != null) {
actionManager.stop();
}
logger.info("MediaManager Core shutdown successfully");
logger.info("Goodbye!");
// Give Log4j2 time to write all pending messages before shutting down
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Now shutdown Log4j2
org.apache.logging.log4j.LogManager.shutdown();
}));
logger.info("Application is running");
logger.info("Press Ctrl+C to exit");
Thread.currentThread().join();
} catch (InterruptedException e) {
logger.info("Application interrupted, initiating shutdown...");
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("Failed to start MediaManager Core", e);
@@ -123,9 +45,10 @@ public class MediaManagerApplication {
private static void loadConfiguration() throws IOException {
config = new Properties();
try (InputStream input = MediaManagerApplication.class.getClassLoader().getResourceAsStream("config.properties")) {
try (InputStream input = MediaManagerApplication.class.getClassLoader()
.getResourceAsStream("application.properties")) {
if (input == null) {
throw new IOException("Unable to find config.properties");
throw new IOException("Unable to find application.properties");
}
config.load(input);
logger.info("Configuration loaded successfully");
@@ -135,4 +58,8 @@ public class MediaManagerApplication {
public static Properties getConfig() {
return config;
}
public static DatabaseManager getDatabaseManager() {
return databaseManager;
}
}
@@ -0,0 +1,71 @@
package com.mediamanager.config;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class DatabaseManager {
private static final Logger logger = LogManager.getLogger(DatabaseManager.class);
private static DatabaseManager instance;
private EntityManagerFactory entityManagerFactory;
private DatabaseManager(Properties config) {
initializeEntityManagerFactory(config);
}
public static synchronized DatabaseManager getInstance(Properties config) {
if (instance == null) {
instance = new DatabaseManager(config);
}
return instance;
}
private void initializeEntityManagerFactory(Properties config) {
try {
logger.info("Initializing database connection...");
Map<String, String> properties = new HashMap<>();
properties.put("jakarta.persistence.jdbc.url", config.getProperty("db.url"));
properties.put("jakarta.persistence.jdbc.user", config.getProperty("db.username"));
properties.put("jakarta.persistence.jdbc.password", config.getProperty("db.password"));
properties.put("jakarta.persistence.jdbc.driver", config.getProperty("db.driver"));
properties.put("hibernate.hikari.maximumPoolSize", config.getProperty("db.pool.maximum-pool-size"));
properties.put("hibernate.hikari.minimumIdle", config.getProperty("db.pool.minimum-idle"));
properties.put("hibernate.hikari.connectionTimeout", config.getProperty("db.pool.connection-timeout"));
properties.put("hibernate.hikari.idleTimeout", config.getProperty("db.pool.idle-timeout"));
properties.put("hibernate.hikari.maxLifetime", config.getProperty("db.pool.max-lifetime"));
entityManagerFactory = Persistence.createEntityManagerFactory("MediaManagerPU", properties);
logger.info("Database connection initialized successfully");
} catch (Exception e) {
logger.error("Failed to initialize database connection", e);
throw new RuntimeException("Failed to initialize database connection", e);
}
}
public EntityManager createEntityManager() {
if (entityManagerFactory == null) {
throw new IllegalStateException("EntityManagerFactory is not initialized");
}
return entityManagerFactory.createEntityManager();
}
public void shutdown() {
if (entityManagerFactory != null && entityManagerFactory.isOpen()) {
logger.info("Closing database connection...");
entityManagerFactory.close();
logger.info("Database connection closed successfully");
}
}
public boolean isInitialized() {
return entityManagerFactory != null && entityManagerFactory.isOpen();
}
}
@@ -1,62 +0,0 @@
package com.mediamanager.service.database;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public abstract class DatabaseManager {
protected final Properties configuration;
protected Connection connection;
protected static final Logger logger = LogManager.getLogger(DatabaseManager.class);
public DatabaseManager(Properties config) {
this.configuration = config;
logger.debug("DatabaseManager created with configuration:");
}
public abstract void init() throws Exception;
protected abstract Connection createConnection() throws Exception;
protected void performSanityChecks() throws SQLException {
logger.debug("Performing sanity checks...");
try (Statement stmt = connection.createStatement()) {
stmt.execute("SELECT 1");
}
String databaseProductName = connection.getMetaData().getDatabaseProductName();
String databaseProductVersion = connection.getMetaData().getDatabaseProductVersion();
logger.info("Connected to database: {} v{}", databaseProductName, databaseProductVersion);
}
public Connection getConnection() {
return connection;
}
public void close() {
if (connection != null) {
try {
logger.info("Closing database connection...");
connection.close();
logger.info("Database connection closed successfully");
} catch (SQLException e) {
logger.error("Error closing database connection: {}", e.getMessage());
}
} else {
logger.debug("No database connection to close");
}
}
protected boolean testConnection() {
try (Statement stmt = connection.createStatement()) {
stmt.execute("SELECT 1");
return true;
} catch (SQLException e) {
logger.error("Connection test failed", e);
return false;
}
}
}
@@ -1,87 +0,0 @@
package com.mediamanager.service.database;
import io.zonky.test.db.postgres.embedded.EmbeddedPostgres;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.util.Properties;
public class LocalDatabaseManager extends DatabaseManager {
private static final Logger logger = LogManager.getLogger(LocalDatabaseManager.class);
private EmbeddedPostgres embeddedPostgres;
public LocalDatabaseManager(Properties config) {
super(config);
}
@Override
public void init() throws Exception {
logger.info("Initializing embedded PostgreSQL database...");
// Configurações específicas do modo local
String dataDir = configuration.getProperty("local.database.dir",
System.getProperty("user.home") + "/.mediamanager/db");
int port = Integer.parseInt(
configuration.getProperty("local.database.port", "54321")
);
// Cria diretório se não existir
Path dataPath = Paths.get(dataDir);
Files.createDirectories(dataPath);
logger.debug("Using data directory: {}", dataDir);
// Inicia o PostgreSQL embedded
logger.info("Starting embedded PostgreSQL on port {}", port);
embeddedPostgres = EmbeddedPostgres.builder()
.setDataDirectory(dataPath)
.setCleanDataDirectory(false) // Mantém dados
.setPort(port)
.start();
// Cria a conexão
this.connection = createConnection();
logger.info("Embedded database connection established successfully");
// Usa o método da classe pai para sanity checks
performSanityChecks();
logger.info("Database sanity checks passed successfully");
}
@Override
protected Connection createConnection() throws Exception {
if (embeddedPostgres == null) {
throw new IllegalStateException("Embedded PostgreSQL not started");
}
try {
// Pega conexão do embedded
Connection conn = embeddedPostgres.getPostgresDatabase().getConnection();
logger.debug("Got connection from embedded PostgreSQL");
return conn;
} catch (Exception e) {
logger.error("Failed to get embedded connection", e);
throw new Exception("Embedded connection failed: " + e.getMessage(), e);
}
}
@Override
public void close() {
// Primeiro fecha a conexão (método da classe pai)
super.close();
// Depois para o embedded
if (embeddedPostgres != null) {
try {
logger.info("Stopping embedded PostgreSQL...");
embeddedPostgres.close();
logger.info("Embedded PostgreSQL stopped successfully");
} catch (Exception e) {
logger.error("Error stopping embedded PostgreSQL", e);
}
}
}
}
@@ -1,11 +0,0 @@
package com.mediamanager.service.delegate;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TransportProtocol;
@FunctionalInterface
public interface ActionHandler {
TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException;
}
@@ -1,60 +0,0 @@
package com.mediamanager.service.delegate;
import com.google.protobuf.ByteString;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.handler.EchoHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.Map;
public class DelegateActionManager {
private static final Logger logger = LogManager.getLogger(DelegateActionManager.class);
private final Map<String, ActionHandler> handlerRegistry;
public DelegateActionManager() {
logger.debug("DelegateActionManager created");
this.handlerRegistry = new HashMap<>();
registerHandlers();
}
private void registerHandlers() {
handlerRegistry.put("echo",new EchoHandler());
}
public void start(){
logger.info("DelegateActionManager started");
}
public void stop(){
logger.info("DelegateActionManager stopped");
}
public TransportProtocol.Response ProcessedRequest(TransportProtocol.Request request){
String requestId = request.getRequestId();
logger.info("Processing request: {}", requestId);
String action = request.getHeadersMap().getOrDefault("action", "unknown");
ActionHandler handler = handlerRegistry.get(action);
TransportProtocol.Response.Builder responseBuilder;
if (handler == null) {
logger.warn("No handler found for action: {}", action);
responseBuilder = TransportProtocol.Response.newBuilder()
.setStatusCode(404) // 404 Not Found
.setPayload(ByteString.copyFromUtf8("Error: Action '" + action + "' not found."));
} else{
try {
logger.debug("Delegating action '{}' to handler...", action);
responseBuilder = handler.handle(request.getPayload());
}catch (Exception e) {
logger.error("Handler for action '{}' threw an exception:", action, e);
responseBuilder = TransportProtocol.Response.newBuilder()
.setStatusCode(500) // 500 Internal Server Error
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
return responseBuilder.setRequestId(requestId).build();
}
}
@@ -1,17 +0,0 @@
package com.mediamanager.service.delegate.handler;
import com.google.protobuf.ByteString;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.ActionHandler;
public class EchoHandler implements ActionHandler {
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload) {
String payloadText = requestPayload.toStringUtf8();
String responseText = "Server received: " + payloadText;
return TransportProtocol.Response.newBuilder()
.setPayload(ByteString.copyFromUtf8(responseText))
.setStatusCode(200);
}
}
@@ -1,318 +0,0 @@
package com.mediamanager.service.ipc;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.DelegateActionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class IPCManager {
private final Properties configuration;
private static final Logger logger = LogManager.getLogger(IPCManager.class);
private final DelegateActionManager actionManager;
private Path socketPath;
private UnixDomainSocketAddress socketAddress;
private ServerSocketChannel serverChannel;
private ExecutorService clientThreadPool;
private final AtomicBoolean running = new AtomicBoolean(false);
private final ConcurrentHashMap<Integer, ClientHandler> activeClients = new ConcurrentHashMap<>();
private final AtomicInteger clientIdCounter = new AtomicInteger(0);
public IPCManager(Properties config, DelegateActionManager actionManager){
configuration = config;
this.actionManager = actionManager;
logger.debug("IPCManager created with configuration:");
}
public void init() throws Exception {
logger.info("Initializing IPC connection...");
validateConfiguration();
socketPath = Path.of(configuration.getProperty("ipc.socket.path")).resolve("mediamanager.sock");
if (Files.exists(socketPath)) {
logger.warn("Socket file already exists at: {}", socketPath);
logger.info("Deleting existing socket...");
Files.deleteIfExists(socketPath);
}
Path parentDir = socketPath.getParent();
if (parentDir != null && !Files.exists(parentDir)) {
logger.info("Creating parent directory for socket: {}", parentDir);
Files.createDirectories(parentDir);
}
try {
socketAddress = UnixDomainSocketAddress.of(socketPath);
logger.debug("Socket address created");
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
serverChannel.bind(socketAddress);
logger.info("Server bound to socket - file created at: {}", socketPath);
// ESTA É A MUDANÇA CRÍTICA
// Configura o canal para modo não-bloqueante
// Isso faz accept() retornar null imediatamente se não houver cliente
// ao invés de bloquear esperando indefinidamente
serverChannel.configureBlocking(false);
logger.debug("Server channel configured for non-blocking mode");
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(socketPath, perms);
logger.debug("Socket permissions set to: rw-------");
clientThreadPool = Executors.newCachedThreadPool(runnable -> {
Thread thread = new Thread(runnable);
thread.setName("IPC-Client-Handler-" + thread.getId());
thread.setDaemon(true);
return thread;
});
logger.debug("Client thread pool created");
running.set(true);
Thread serverThread = new Thread(this::acceptConnectionsLoop, "IPC-Server-Accept-Thread");
serverThread.setDaemon(true);
serverThread.start();
logger.info("Server thread started - accepting connections");
logger.info("IPC server initialized successfully on {}", socketPath.toAbsolutePath());
} catch (IOException e) {
logger.error("Failed to initialize IPC server: {}", e.getMessage());
throw new Exception("Failed to initialize IPC server: " + e.getMessage(), e);
}
}
private void validateConfiguration() throws Exception {
String[] requiredProperties = {
"ipc.socket.path"
};
for (String property : requiredProperties) {
if (configuration.getProperty(property) == null) {
throw new Exception("Missing required configuration property: " + property);
}
}
logger.debug("IPC configuration validated successfully");
}
public Path getSocketPath(){
return socketPath;
}
public void close() throws Exception {
if(!running.get()){
logger.warn("IPC connection is already closed");
}
logger.info("Closing IPC connection...");
running.set(false);
if (serverChannel != null && serverChannel.isOpen()) {
serverChannel.close();
logger.debug("Server channel closed");
}
if (clientThreadPool != null) {
clientThreadPool.shutdown();
try {
if (!clientThreadPool.awaitTermination(30, TimeUnit.SECONDS)) {
logger.warn("Some client handlers did not finish in time, forcing shutdown");
clientThreadPool.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Interrupted while waiting for client handlers", e);
clientThreadPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
if (socketPath != null && Files.exists(socketPath)) {
Files.deleteIfExists(socketPath);
logger.info("Socket file deleted successfully");
}
logger.info("IPC server closed successfully");
}
private void acceptConnectionsLoop() {
logger.info("Preparing to accept connections...");
while (running.get()) {
try {
// Em modo não-bloqueante, accept() retorna imediatamente
// Se há um cliente esperando, retorna o SocketChannel
// Se não há cliente, retorna null
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
// Um cliente realmente se conectou!
int clientId = clientIdCounter.incrementAndGet();
logger.info("Client {} connected", clientId);
ClientHandler handler = new ClientHandler(clientId, clientChannel);
activeClients.put(clientId, handler);
clientThreadPool.submit(() -> {
try {
handler.handle();
} finally {
activeClients.remove(clientId);
logger.info("Client {} disconnected", clientId);
}
});
} else {
// Nenhum cliente conectado no momento
// Dorme por um curto período antes de verificar novamente
// Isso evita consumir CPU desnecessariamente em um loop vazio
Thread.sleep(100); // 100 milissegundos
}
} catch (InterruptedException e) {
// Thread foi interrompida, provavelmente durante shutdown
logger.debug("Accept loop interrupted");
break;
} catch (IOException e) {
// Erros de I/O reais devem ser logados
if (running.get()) {
logger.error("Error accepting client connection", e);
}
break;
}
}
logger.info("Connection loop stopped gracefully");
}
private class ClientHandler {
private final int clientId;
private final SocketChannel channel;
public ClientHandler(int clientId, SocketChannel channel) {
this.clientId = clientId;
this.channel = channel;
}
/**
* Método principal que processa a comunicação com o cliente.
* Aqui é onde vamos ler mensagens JSON, processá-las, e enviar respostas.
*/
public void handle() {
logger.debug("Client {} handler thread started", clientId);
try {
TransportProtocol.Request request = readRequest(channel);
if (request != null) {
logger.info("Client {} sent request {}", clientId, request.getRequestId());
// Processa usando o DelegateActionManager
TransportProtocol.Response response = actionManager.ProcessedRequest(request);
// Envia resposta de volta
writeResponse(channel, response);
logger.info("Client {} response sent", clientId);
} else {
logger.warn("Client {} sent null message", clientId);
}
} catch (Exception e) {
logger.error("Error handling client {}", clientId, e);
} finally {
try {
channel.close();
logger.debug("Client {} channel closed", clientId);
} catch (IOException e) {
logger.error("Error closing client {} channel", clientId, e);
}
}
}
private TransportProtocol.Request readRequest(SocketChannel channel) throws IOException {
// Primeiro, lê o tamanho da mensagem (4 bytes = int32)
java.nio.ByteBuffer sizeBuffer = java.nio.ByteBuffer.allocate(4);
int bytesRead = 0;
while (bytesRead < 4) {
int read = channel.read(sizeBuffer);
if (read == -1) {
logger.debug("Client disconnected before sending size");
return null;
}
bytesRead += read;
}
sizeBuffer.flip();
int messageSize = sizeBuffer.getInt();
logger.debug("Expecting message of {} bytes", messageSize);
// Validação básica de segurança
if (messageSize <= 0 || messageSize > 1024 * 1024) { // Max 1MB
throw new IOException("Invalid message size: " + messageSize);
}
// Agora lê a mensagem completa
java.nio.ByteBuffer messageBuffer = java.nio.ByteBuffer.allocate(messageSize);
bytesRead = 0;
while (bytesRead < messageSize) {
int read = channel.read(messageBuffer);
if (read == -1) {
throw new IOException("Client disconnected while reading message");
}
bytesRead += read;
}
messageBuffer.flip();
// Deserializa o Protocol Buffers
byte[] messageBytes = new byte[messageSize];
messageBuffer.get(messageBytes);
return TransportProtocol.Request.parseFrom(messageBytes);
}
private void writeResponse(SocketChannel channel, TransportProtocol.Response response) throws IOException {
byte[] messageBytes = response.toByteArray();
int messageSize = messageBytes.length;
logger.debug("Writing response of {} bytes", messageSize);
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(4 + messageSize);
buffer.putInt(messageSize);
buffer.put(messageBytes);
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
logger.debug("Response written successfully");
}
}
}
-19
View File
@@ -1,19 +0,0 @@
syntax = "proto3";
option java_package = "com.mediamanager.protocol";
option java_outer_classname = "TransportProtocol";
package mediamanager;
message Request {
string request_id = 1;
bytes payload = 2;
map<string, string> headers = 3;
}
message Response {
string request_id = 1;
int32 status_code = 2;
bytes payload = 3;
map<string, string> headers = 4;
}
+1 -1
View File
@@ -9,7 +9,7 @@
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- Add entity classes here as they are created -->
<!-- <class>com.mediamanager.model.Media</class> -->
<class>com.mediamanager.model.Media</class>
<properties>
<!-- Database connection properties will be set programmatically -->
@@ -21,7 +21,3 @@ hibernate.format_sql=true
ipc.pipe.name=mediamanager-pipe
ipc.pipe.path=/tmp/mediamanager
ipc.buffer.size=8192
ipc.socket.path=/tmp/mediamanager
runtype=local
+3 -27
View File
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" shutdownHook="disable">
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT" immediateFlush="true">
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<RollingFile name="FileAppender" fileName="logs/mediamanager.log"
filePattern="logs/mediamanager-%d{yyyy-MM-dd}-%i.log"
immediateFlush="true">
filePattern="logs/mediamanager-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
@@ -27,29 +26,6 @@
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<Logger name="io.zonky.test" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<!-- PostgreSQL JDBC Driver -->
<Logger name="org.postgresql" level="WARN" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<!-- Processo do PostgreSQL embedded -->
<Logger name="org.postgresql.embedded" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<!-- HikariCP Connection Pool -->
<Logger name="com.zaxxer.hikari" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>