/**
* Copyright (c) 2010-2017 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.modbus.internal.pooling;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.wimpi.modbus.net.ModbusSlaveConnection;
import net.wimpi.modbus.net.SerialConnection;
import net.wimpi.modbus.net.TCPMasterConnection;
import net.wimpi.modbus.net.UDPMasterConnection;
/**
* ModbusSlaveConnectionFactoryImpl responsible of the lifecycle of modbus slave connections
*
* The actual pool uses instance of this class to create and destroy connections as-needed.
*
* The overall functionality goes as follow
* - create: create connection object but do not connect it yet
* - destroyObject: close connection and free all resources. Called by the pool when the pool is being closed or the
* object is invalidated.
* - activateObject: prepare connection to be used. In practice, connect if disconnected
* - passivateObject: passivate connection before returning it back to the pool. Currently, passivateObject closes all
* IP-based connections every now and then (reconnectAfterMillis). Serial connections we keep open.
* - wrap: wrap created connection to pooled object wrapper class. It tracks usage statistics and last connection time.
*
* Note that the implementation must be thread safe.
*
*/
public class ModbusSlaveConnectionFactoryImpl
extends BaseKeyedPooledObjectFactory<ModbusSlaveEndpoint, ModbusSlaveConnection> {
private static class PooledConnection extends DefaultPooledObject<ModbusSlaveConnection> {
private long lastConnected;
public PooledConnection(ModbusSlaveConnection object) {
super(object);
}
public long getLastConnected() {
return lastConnected;
}
public void setLastConnected(long lastConnected) {
this.lastConnected = lastConnected;
}
}
private static final Logger logger = LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class);
private volatile Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs = new ConcurrentHashMap<>();
private volatile Map<ModbusSlaveEndpoint, Long> lastPassivateMillis = new ConcurrentHashMap<>();
private volatile Map<ModbusSlaveEndpoint, Long> lastConnectMillis = new ConcurrentHashMap<>();
private InetAddress getInetAddress(ModbusIPSlaveEndpoint key) {
try {
return InetAddress.getByName(key.getAddress());
} catch (UnknownHostException e) {
logger.error("KeyedPooledModbusSlaveConnectionFactory: Unknown host: {}. Connection creation failed.",
e.getMessage());
return null;
}
}
@Override
public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception {
return endpoint.accept(new ModbusSlaveEndpointVisitor<ModbusSlaveConnection>() {
@Override
public ModbusSlaveConnection visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) {
SerialConnection connection = new SerialConnection(modbusSerialSlavePoolingKey.getSerialParameters());
logger.trace("Created connection {} for endpoint {}", connection, modbusSerialSlavePoolingKey);
return connection;
}
@Override
public ModbusSlaveConnection visit(ModbusTCPSlaveEndpoint key) {
InetAddress address = getInetAddress(key);
if (address == null) {
return null;
}
EndpointPoolConfiguration config = endpointPoolConfigs.get(key);
int connectTimeoutMillis = 0;
if (config != null) {
connectTimeoutMillis = config.getConnectTimeoutMillis();
}
TCPMasterConnection connection = new TCPMasterConnection(address, key.getPort(), connectTimeoutMillis);
logger.trace("Created connection {} for endpoint {}", connection, key);
return connection;
}
@Override
public ModbusSlaveConnection visit(ModbusUDPSlaveEndpoint key) {
InetAddress address = getInetAddress(key);
if (address == null) {
return null;
}
UDPMasterConnection connection = new UDPMasterConnection(address, key.getPort());
logger.trace("Created connection {} for endpoint {}", connection, key);
return connection;
}
});
}
@Override
public PooledObject<ModbusSlaveConnection> wrap(ModbusSlaveConnection connection) {
return new PooledConnection(connection);
}
@Override
public void destroyObject(ModbusSlaveEndpoint endpoint, final PooledObject<ModbusSlaveConnection> obj) {
logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", obj.getObject(),
endpoint);
obj.getObject().resetConnection();
}
@Override
public void activateObject(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj) throws Exception {
if (obj.getObject() == null) {
return;
}
try {
ModbusSlaveConnection connection = obj.getObject();
EndpointPoolConfiguration config = endpointPoolConfigs.get(endpoint);
if (connection.isConnected()) {
if (config != null) {
long waited = waitAtleast(lastPassivateMillis.get(endpoint), config.getPassivateBorrowMinMillis());
logger.trace(
"Waited {}ms (passivateBorrowMinMillis {}ms) before giving returning connection {} for endpoint {}, to ensure delay between transactions.",
waited, config.getPassivateBorrowMinMillis(), obj.getObject(), endpoint);
}
} else {
// invariant: !connection.isConnected()
tryConnect(endpoint, obj, connection, config);
}
} catch (Exception e) {
logger.error("Error connecting connection {} for endpoint {}: {}", obj.getObject(), endpoint,
e.getMessage());
}
}
@Override
public void passivateObject(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj) {
ModbusSlaveConnection connection = obj.getObject();
if (connection == null) {
return;
}
logger.trace("Passivating connection {} for endpoint {}...", connection, endpoint);
lastPassivateMillis.put(endpoint, System.currentTimeMillis());
EndpointPoolConfiguration configuration = endpointPoolConfigs.get(endpoint);
long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis();
long connectionAgeMillis = System.currentTimeMillis() - ((PooledConnection) obj).getLastConnected();
if (reconnectAfterMillis == 0 || (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis)) {
logger.trace(
"(passivate) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit -> disconnecting.",
connection, endpoint, connectionAgeMillis, reconnectAfterMillis);
connection.resetConnection();
} else {
logger.trace(
"(passivate) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit. Keep the connection open.",
connection, endpoint, connectionAgeMillis, reconnectAfterMillis);
}
logger.trace("...Passivated connection {} for endpoint {}", obj.getObject(), endpoint);
}
@Override
public boolean validateObject(ModbusSlaveEndpoint key, PooledObject<ModbusSlaveConnection> p) {
boolean valid = p.getObject() != null && p.getObject().isConnected();
logger.trace("Validating endpoint {} connection {} -> {}", key, p.getObject(), valid);
return valid;
}
public Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> getEndpointPoolConfigs() {
return endpointPoolConfigs;
}
public void applyEndpointPoolConfigs(Map<ModbusSlaveEndpoint, EndpointPoolConfiguration> endpointPoolConfigs) {
this.endpointPoolConfigs = new ConcurrentHashMap<>(endpointPoolConfigs);
}
private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj,
ModbusSlaveConnection connection, EndpointPoolConfiguration config) throws Exception {
if (connection.isConnected()) {
return;
}
int tryIndex = 0;
Long lastConnect = lastConnectMillis.get(endpoint);
int maxTries = config == null ? 1 : config.getConnectMaxTries();
do {
try {
if (config != null) {
long waited = waitAtleast(lastConnect,
Math.max(config.getInterConnectDelayMillis(), config.getPassivateBorrowMinMillis()));
if (waited > 0) {
logger.trace(
"Waited {}ms (interConnectDelayMillis {}ms, passivateBorrowMinMillis {}ms) before "
+ "connecting disconnected connection {} for endpoint {}, to allow delay "
+ "between connections re-connects",
waited, config.getInterConnectDelayMillis(), config.getPassivateBorrowMinMillis(),
obj.getObject(), endpoint);
}
}
connection.connect();
long curTime = System.currentTimeMillis();
((PooledConnection) obj).setLastConnected(curTime);
lastConnectMillis.put(endpoint, curTime);
break;
} catch (Exception e) {
tryIndex++;
logger.error("connect try {}/{} error: {}. Connection {}. Endpoint {}", tryIndex, maxTries,
e.getMessage(), connection, endpoint);
if (tryIndex >= maxTries) {
logger.error("re-connect reached max tries {}, throwing last error: {}. Connection {}. Endpoint {}",
maxTries, e.getMessage(), connection, endpoint);
throw e;
}
lastConnect = System.currentTimeMillis();
}
} while (true);
}
private long waitAtleast(Long lastOperation, long waitMillis) {
if (lastOperation == null) {
return 0;
}
long millisSinceLast = System.currentTimeMillis() - lastOperation;
long millisToWaitStill = Math.min(waitMillis, Math.max(0, waitMillis - millisSinceLast));
try {
Thread.sleep(millisToWaitStill);
} catch (InterruptedException e) {
logger.error("wait interrupted: {}", e.getMessage());
}
return millisToWaitStill;
}
}