2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.transport.modbus.internal.pooling;
15 import java.net.InetAddress;
16 import java.net.UnknownHostException;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.function.Function;
21 import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
22 import org.apache.commons.pool2.PooledObject;
23 import org.apache.commons.pool2.impl.DefaultPooledObject;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
27 import org.openhab.io.transport.modbus.endpoint.ModbusIPSlaveEndpoint;
28 import org.openhab.io.transport.modbus.endpoint.ModbusSerialSlaveEndpoint;
29 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
30 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpointVisitor;
31 import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
32 import org.openhab.io.transport.modbus.endpoint.ModbusUDPSlaveEndpoint;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import net.wimpi.modbus.net.ModbusSlaveConnection;
37 import net.wimpi.modbus.net.SerialConnection;
38 import net.wimpi.modbus.net.TCPMasterConnection;
39 import net.wimpi.modbus.net.UDPMasterConnection;
42 * ModbusSlaveConnectionFactoryImpl responsible of the lifecycle of modbus slave connections
44 * The actual pool uses instance of this class to create and destroy connections as-needed.
46 * The overall functionality goes as follow
47 * - create: create connection object but do not connect it yet
48 * - destroyObject: close connection and free all resources. Called by the pool when the pool is being closed or the
49 * object is invalidated.
50 * - activateObject: prepare connection to be used. In practice, connect if disconnected
51 * - passivateObject: passivate connection before returning it back to the pool. Currently, passivateObject closes all
52 * IP-based connections every now and then (reconnectAfterMillis). Serial connections we keep open.
53 * - wrap: wrap created connection to pooled object wrapper class. It tracks usage statistics and last connection time.
55 * Note that the implementation must be thread safe.
57 * @author Sami Salonen - Initial contribution
60 public class ModbusSlaveConnectionFactoryImpl
61 extends BaseKeyedPooledObjectFactory<ModbusSlaveEndpoint, ModbusSlaveConnection> {
63 class PooledConnection extends DefaultPooledObject<ModbusSlaveConnection> {
65 private volatile long lastConnected;
66 private volatile @Nullable ModbusSlaveEndpoint endpoint;
68 public PooledConnection(ModbusSlaveConnection object) {
72 public long getLastConnected() {
76 public void setLastConnected(ModbusSlaveEndpoint endpoint, long lastConnected) {
77 this.endpoint = endpoint;
78 this.lastConnected = lastConnected;
83 * Reset connection if it is too old or fulfills some of the other criteria
85 * @param activityName ongoing activity calling this method. For logging
86 * @return whether connection was reseted
88 public boolean maybeResetConnection(String activityName) {
89 ModbusSlaveEndpoint localEndpoint = endpoint;
90 if (localEndpoint == null) {
91 // We have not connected yet, abort
92 // Without endpoint we have no age parameters available (endpointPoolConfigs &
93 // disconnectIfConnectedBefore)
96 long localLastConnected = lastConnected;
98 ModbusSlaveConnection connection = getObject();
101 EndpointPoolConfiguration configuration = endpointPoolConfigs.get(localEndpoint);
102 long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis();
103 long connectionAgeMillis = System.currentTimeMillis() - localLastConnected;
104 long disconnectIfConnectedBeforeMillis = disconnectIfConnectedBefore.getOrDefault(localEndpoint, -1L);
105 boolean disconnectSinceTooOldConnection = disconnectIfConnectedBeforeMillis < 0L ? false
106 : localLastConnected <= disconnectIfConnectedBeforeMillis;
107 boolean shouldBeDisconnected = (reconnectAfterMillis == 0
108 || (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis)
109 || disconnectSinceTooOldConnection);
110 if (shouldBeDisconnected) {
112 "({}) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit or has been connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={} -> disconnecting.",
113 activityName, connection, localEndpoint, connectionAgeMillis, reconnectAfterMillis,
114 localLastConnected, disconnectIfConnectedBeforeMillis);
115 connection.resetConnection();
119 "({}) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit and connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={}. Keep the connection open.",
120 activityName, connection, localEndpoint, connectionAgeMillis, reconnectAfterMillis,
121 localLastConnected, disconnectIfConnectedBeforeMillis);
127 private final Logger logger = LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class);
128 private volatile Map<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> endpointPoolConfigs = new ConcurrentHashMap<>();
129 private volatile Map<ModbusSlaveEndpoint, Long> lastPassivateMillis = new ConcurrentHashMap<>();
130 private volatile Map<ModbusSlaveEndpoint, Long> lastConnectMillis = new ConcurrentHashMap<>();
131 private volatile Map<ModbusSlaveEndpoint, Long> disconnectIfConnectedBefore = new ConcurrentHashMap<>();
132 private volatile Function<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> defaultPoolConfigurationFactory = endpoint -> null;
134 private @Nullable InetAddress getInetAddress(ModbusIPSlaveEndpoint key) {
136 return InetAddress.getByName(key.getAddress());
137 } catch (UnknownHostException e) {
138 logger.error("KeyedPooledModbusSlaveConnectionFactory: Unknown host: {}. Connection creation failed.",
145 public ModbusSlaveConnection create(ModbusSlaveEndpoint endpoint) throws Exception {
146 return endpoint.accept(new ModbusSlaveEndpointVisitor<ModbusSlaveConnection>() {
148 public @Nullable ModbusSlaveConnection visit(ModbusSerialSlaveEndpoint modbusSerialSlavePoolingKey) {
149 SerialConnection connection = new SerialConnection(modbusSerialSlavePoolingKey.getSerialParameters());
150 logger.trace("Created connection {} for endpoint {}", connection, modbusSerialSlavePoolingKey);
155 public @Nullable ModbusSlaveConnection visit(ModbusTCPSlaveEndpoint key) {
156 InetAddress address = getInetAddress(key);
157 if (address == null) {
160 EndpointPoolConfiguration config = getEndpointPoolConfiguration(key);
161 int connectTimeoutMillis = 0;
162 if (config != null) {
163 connectTimeoutMillis = config.getConnectTimeoutMillis();
165 TCPMasterConnection connection = new TCPMasterConnection(address, key.getPort(), connectTimeoutMillis);
166 logger.trace("Created connection {} for endpoint {}", connection, key);
171 public @Nullable ModbusSlaveConnection visit(ModbusUDPSlaveEndpoint key) {
172 InetAddress address = getInetAddress(key);
173 if (address == null) {
176 UDPMasterConnection connection = new UDPMasterConnection(address, key.getPort());
177 logger.trace("Created connection {} for endpoint {}", connection, key);
184 public PooledObject<ModbusSlaveConnection> wrap(ModbusSlaveConnection connection) {
185 return new PooledConnection(connection);
189 public void destroyObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj) {
193 logger.trace("destroyObject for connection {} and endpoint {} -> closing the connection", obj.getObject(),
195 obj.getObject().resetConnection();
199 public void activateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj)
204 ModbusSlaveConnection connection = obj.getObject();
207 EndpointPoolConfiguration config = getEndpointPoolConfiguration(endpoint);
208 if (!connection.isConnected()) {
209 tryConnect(endpoint, obj, connection, config);
212 if (config != null) {
213 long waited = waitAtleast(lastPassivateMillis.get(endpoint), config.getInterTransactionDelayMillis());
215 "Waited {}ms (interTransactionDelayMillis {}ms) before giving returning connection {} for endpoint {}, to ensure delay between transactions.",
216 waited, config.getInterTransactionDelayMillis(), obj.getObject(), endpoint);
218 } catch (InterruptedException e) {
219 // Someone wants to cancel us, reset the connection and abort
220 if (connection.isConnected()) {
221 connection.resetConnection();
223 } catch (Exception e) {
224 logger.error("Error connecting connection {} for endpoint {}: {}", obj.getObject(), endpoint,
230 public void passivateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject<ModbusSlaveConnection> obj) {
234 ModbusSlaveConnection connection = obj.getObject();
235 logger.trace("Passivating connection {} for endpoint {}...", connection, endpoint);
236 lastPassivateMillis.put(endpoint, System.currentTimeMillis());
237 ((PooledConnection) obj).maybeResetConnection("passivate");
238 logger.trace("...Passivated connection {} for endpoint {}", obj.getObject(), endpoint);
242 public boolean validateObject(ModbusSlaveEndpoint key, @Nullable PooledObject<ModbusSlaveConnection> p) {
243 boolean valid = p != null && p.getObject().isConnected();
244 logger.trace("Validating endpoint {} connection {} -> {}", key, p.getObject(), valid);
249 * Configure general connection settings with a given endpoint
251 * @param endpoint endpoint to configure
252 * @param configuration configuration for the endpoint. Use null to reset the configuration to default settings.
254 public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, @Nullable EndpointPoolConfiguration config) {
255 if (config == null) {
256 endpointPoolConfigs.remove(endpoint);
258 endpointPoolConfigs.put(endpoint, config);
263 * Get general configuration settings applied to a given endpoint
265 * Note that default configuration settings are returned in case the endpoint has not been configured.
267 * @param endpoint endpoint to query
268 * @return general connection settings of the given endpoint
270 @SuppressWarnings("null")
271 public @Nullable EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint) {
273 EndpointPoolConfiguration config = endpointPoolConfigs.computeIfAbsent(endpoint,
274 defaultPoolConfigurationFactory);
279 * Set default factory for {@link EndpointPoolConfiguration}
281 * @param defaultPoolConfigurationFactory function providing defaults for a given endpoint
283 public void setDefaultPoolConfigurationFactory(
284 Function<ModbusSlaveEndpoint, @Nullable EndpointPoolConfiguration> defaultPoolConfigurationFactory) {
285 this.defaultPoolConfigurationFactory = defaultPoolConfigurationFactory;
288 private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject<ModbusSlaveConnection> obj,
289 ModbusSlaveConnection connection, @Nullable EndpointPoolConfiguration config) throws Exception {
290 if (connection.isConnected()) {
294 Long lastConnect = lastConnectMillis.get(endpoint);
295 int maxTries = config == null ? 1 : config.getConnectMaxTries();
298 if (config != null) {
299 long waited = waitAtleast(lastConnect,
300 Math.max(config.getInterConnectDelayMillis(), config.getInterTransactionDelayMillis()));
303 "Waited {}ms (interConnectDelayMillis {}ms, interTransactionDelayMillis {}ms) before "
304 + "connecting disconnected connection {} for endpoint {}, to allow delay "
305 + "between connections re-connects",
306 waited, config.getInterConnectDelayMillis(), config.getInterTransactionDelayMillis(),
307 obj.getObject(), endpoint);
310 connection.connect();
311 long curTime = System.currentTimeMillis();
312 ((PooledConnection) obj).setLastConnected(endpoint, curTime);
313 lastConnectMillis.put(endpoint, curTime);
315 } catch (InterruptedException e) {
316 logger.error("connect try {}/{} error: {}. Aborting since interrupted. Connection {}. Endpoint {}.",
317 tryIndex, maxTries, e.getMessage(), connection, endpoint);
319 } catch (Exception e) {
321 logger.error("connect try {}/{} error: {}. Connection {}. Endpoint {}", tryIndex, maxTries,
322 e.getMessage(), connection, endpoint);
323 if (tryIndex >= maxTries) {
324 logger.error("re-connect reached max tries {}, throwing last error: {}. Connection {}. Endpoint {}",
325 maxTries, e.getMessage(), connection, endpoint);
328 lastConnect = System.currentTimeMillis();
334 * Sleep until <code>waitMillis</code> has passed from <code>lastOperation</code>
336 * @param lastOperation last time operation was executed, or null if it has not been executed
338 * @return milliseconds slept
339 * @throws InterruptedException
341 public static long waitAtleast(@Nullable Long lastOperation, long waitMillis) throws InterruptedException {
342 if (lastOperation == null) {
345 long millisSinceLast = System.currentTimeMillis() - lastOperation;
346 long millisToWaitStill = Math.min(waitMillis, Math.max(0, waitMillis - millisSinceLast));
348 Thread.sleep(millisToWaitStill);
349 } catch (InterruptedException e) {
350 LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class).debug("wait interrupted", e);
353 return millisToWaitStill;
357 * Disconnect returning connections which have been connected before certain time
359 * @param disconnectBeforeConnectedMillis disconnected connections that have been connected before this time
361 public void disconnectOnReturn(ModbusSlaveEndpoint endpoint, long disconnectBeforeConnectedMillis) {
362 disconnectIfConnectedBefore.put(endpoint, disconnectBeforeConnectedMillis);