/** * 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; } }