The Connection Cache concept
In some applications there might be multiple parts of the code that require access to a PLC connection.
In contrast to usual microservice architectures, with PLCs we can’t simply open as many connections as we like. For example a S7-1200 typically allows 3 concurrent connections.
Also can the process of establishing a connection be a pretty cost-intensive task. For example in the ADS protocol, when connecting, the driver loads the tables containing the description of all data-types defined in the PLC alongside the symbol-table which declares which variables are defined, which addresses they have, which datatype they reference and where they are located in the PLCs memory.
Even if only one block of code repeatedly requires access to the PLC, simply creating a connection every time would put a too high load on the PLC and the network.
When using the connection cache, many pieces of code can use it in parallel. However, only one piece of code can have access to a connection at the same time.
So the first thread asking for a new connection will have the cache create a new connection and return it to the client. It can then use this just like any ordinary connection retrieved from the basic PlcConnectionFactory. The main difference however is, that as soon as the client calls close() on this so-called connection-lease, the connection is not closed, but the cache puts it back into the storage, waiting for the next thread to require it.
If a thread asks for a connection, which is currently leased by another thread, then the requesting thread will wait till the connection is returned and will then instantly continue using the connection till it then returns it back to the cache.
If a second thread however asks for a different connection (with a different connection string), then the connection cache will create a new connection and return that instantly.
When using the connection cache, connections should not use a connection-lease for a prolonged period of time. So the connection cache keeps track of the leases it hands out and terminates connection-leases that have not been returned for a long time.
Here comes an example application, that uses the connection cache:
public static void main(String[] args) throws Exception {
PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
.withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
.build();
for (int i = 0; i < 10000; i++) {
try(PlcConnection connection = connectionCache.getConnection("s7://192.168.1.192")) {
if (connection.isConnected()){
PlcReadRequest.Builder builder = connection.readRequestBuilder();
builder.addTagAddress("PollingValue", "%DB1:4.0:BOOL");
PlcReadRequest readRequest = builder.build();
PlcReadResponse syncResponse = readRequest.execute().get(2000, TimeUnit.MILLISECONDS);
printResponse(syncResponse);
} else {
logger.info("PLC is not connected, let's try again to connect");
connection.connect();
}
} catch (PlcConnectionException e){
logger.error("Connection exception in trying to connect", e);
} catch (CancellationException e){
logger.error("Polling Thread canceled", e);
} catch (IllegalStateException e){
logger.error("Connection was in an unexpected state", e);
} catch (ExecutionException e){
logger.error("Interrupted Exception fired", e);
} catch (TimeoutException e) {
logger.error("Timeout exception fired", e);
}
TimeUnit.MILLISECONDS.sleep(100);
}
System.exit(0);
}
|
To use the Connection Cache you have to add a dependency to the <dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-tools-connection-cache</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
|
In this snippet of code there are some considerations that are worth to be underlined.
-
In recent versions of PLC4X we have refactored the
PlcDriverManagerto provide access to aPlcConnectionFactoryinterface. This contains all methods that are related to creating connections. The ConnectionCache implements this same interface - viaPlcConnectionManager, which adds theclose()method a cache needs - therefore you can use aPlcConnectionCacheeverywhere you can use a plainPlcConnectionFactory. -
A new
PlcConnectionCacheis usually created using a builder, that can be accessed via:PlcConnectionCache.getBuilder(). This will be explained in more detail in the next chapter. -
A cache always needs a source for the connections it caches, so
withConnectionFactory()is mandatory;build()throws anIllegalStateExceptionwithout it. Usually this is the driver manager, but anyPlcConnectionFactorywill do. -
The
try-with-resourcesstatement (i.e.try (PlcConnection connection = connectionCache.getConnection(connectionString))) ensures that a leased connection will be automatically returned to the cache after the use. As said before if the application keeps hold of the connection for too long, after a configurable amount of time will be automatically closed by the cache and the thread can no-longer use it (i.e. themaxLeaseTimeparameter defaults to 1 minute and is configurable - see the next chapter on configuring the connection cache). -
Before handing out a connection-lease of a connection that has been sitting idle for longer than the
idlePingThreshold, the connection cache will execute aping()operation on it to check if it’s still valid. If this check fails, the cache will terminate this connection, establish a new one and then return a handle for that new connection. Connections that were used recently are handed out without that extra round-trip.
Configuring the PlcConnectionCache
As mentioned before the PlcConnectionCache is configurable. Mainly this involves configuring the timeouts.
All timeouts are set as a long value together with a java.util.concurrent.TimeUnit.
| Name | type | Default | Description |
|---|---|---|---|
connectionFactory |
PlcConnectionFactory |
(mandatory) |
The source the cache creates its connections from - usually |
maxIdleTime |
long + TimeUnit |
|
Time a cached connection may sit unused before the cache closes it and drops it from the pool. The next request for that connection string establishes a fresh connection. |
maxLeaseTime |
long + TimeUnit |
|
Time that a thread is allowed to keep a connection-lease till the connection-cache terminates the lease. This is what protects the cache against clients that forget to close a lease. Set to |
maxWaitTime |
long + TimeUnit |
|
Time that a thread asking for a connection will wait until the connection cache gives up and throws a |
idlePingThreshold |
long + TimeUnit |
|
How long a connection has to have been idle before the cache validates it with a |
pingTimeout |
long + TimeUnit |
|
How long to wait for the answer to such a validation |
closeTimeout |
long + TimeUnit |
|
Upper bound on closing an underlying connection. A wedged socket can otherwise block every operation on that connection; after this time the close is abandoned to a background daemon thread and the cache recovers. Set to |
scheduler |
ScheduledExecutorService |
2 daemon threads |
The scheduler running the idle / lease / wait / validation timeout tasks. Provide your own to share one across several caches. Note that the cache shuts down whichever scheduler it was given when it is closed. |
The configuration of a PlcConnectionCache is done when creating the instance, using the builder methods matching the names above.
Here comes an example:
public static void main(String[] args) throws Exception {
PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
.withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
.withMaxLeaseTime(10, TimeUnit.SECONDS)
.withMaxWaitTime(1, TimeUnit.MINUTES)
.build();
...
}
Shutting the cache down
The cache keeps the connections it hands out, so somebody has to release them again. That is what close() is for: it shuts the scheduler down, closes every cached connection and empties the pool. Afterwards the cache is unusable - any further getConnection() fails with a PlcConnectionCacheClosedException - and closing an already closed cache does nothing.
PlcConnectionCache implements PlcConnectionManager, which extends AutoCloseable, so it can be used as a try-with-resources resource or wired into whatever disposal callback your framework offers:
try (PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
.withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
.build()) {
...
} // all cached connections are closed here
A single connection can also be dropped from the pool without closing the whole cache, using removeCachedConnection(connectionString). For monitoring, getCachedConnectionCount() and getActiveLeaseCount() report how many connections the cache holds and how many of them are currently leased out.
Subscriptions and cached connections
Subscriptions are bound to a connection, so a cache that silently replaces a connection underneath a subscribed client would lose its subscriptions. The connection cache therefore keeps track of the subscriptions and event listeners registered through a leased connection, and re-establishes them on the new connection whenever it has to recreate one - for example after a failed validation ping. From the client’s point of view its subscription simply keeps delivering events.