EPICS Jackie Reference Manual

Version 2.0.0

Sebastian Marsching

aquenos GmbH

Table of Contents
I. Overview of EPICS Jackie
1. Library design
2. Comparison with other Channel Access libraries
3. Compatibility
4. Requirements
II. What’s new in EPICS Jackie 2.x
III. EPICS Jackie Client
1. Getting started
2. Using the client
3. Configuration options
4. Error handling
5. Listener lock-policy
6. Threading strategy
7. Further customization options
IV. EPICS Jackie Common Components
1. Channel Access message codec
2. Channel Access value objects
3. I/O support
List of Figures
IV.1. Overview of the ChannelAccessValue class hierarchy
IV.2. Class hierarchy of the ChannelAccessDouble type
IV.3. Class hierarchy of numeric ChannelAccessValue types
List of Tables
IV.1. Channel Access value types and corresponding interfaces

Chapter I. Overview of EPICS Jackie

EPICS Jackie is a pure Java implementation of the Channel Access protocol used by EPICS. It is designed to easily integrate Java applications into EPICS environments without carrying heavy dependencies or even relying on Java Native Interface (JNI) calls to a C library. At the same time, it retains full compatibility with the “standard” implementation of Channel Access that is part of EPICS Base. Please refer to Section 3, “Compatibility” for information about this library’s compatibility.

While this library tries to retain full compatibility on the network layer, its API design differs significantly from that of most other Channel Access libraries. In particular, this library makes use of modern concepts like object-orientation and generics to provide an intuitive yet type-safe programming interface. For a closer look at how this library compares to other implementations, please refer to Section 2, “Comparison with other Channel Access libraries”.

This document is intended as a reference guide on how to use EPICS Jackie. However, it does not intend to describe all the details of the library’s API and its implementation. For a fully detailed description of the API and its standard implementation, please refer to the API reference documentation. This document is intended as a guide to the EPICS Jackie library and not as a guide to the Channel Access protocol itself. If interested in details of the protocol, the reader is encouraged to refer to the EPICS R3.16 Channel Access Reference Manual and the EPICS Channel Access Protocol Specification.

1. Library design

The EPICS Jackie library has been designed with two goals in mind: First, it should be easy and convenient to use it. Second, it should be flexible so that it can be used as a building block for applications with special requirements. Keeping the first goal in mind, this means that the library should offer configurable parameters that modify its behavior but should also provide sensible defaults for these parameters.

1.1. Easy and convenient use

In order to attain the goal of providing a library that can be used easily and conveniently, the API has been designed as a first-grade Java API instead of trying to reuse the concepts of the API of the Channel Access library for the C programming language. This means that the Channel Access client, channels, and channel monitors are all represented by objects. These objects are safe for concurrent use by multiple threads, lifting the user from the burden of having to synchronize access to the API by using a mutex.

The API uses generics to enable the user to deal with the various types of Channel Access values in a type-safe way, benefiting from automatic type checks at compile time. This is combined with a class-hierarchy that actually reflects the hierarchy of Channel Access types. This means that (unlike the API of the C library) there are common base-types for all values that provide alarm information and all values that provide display information (the second being derived from the first because display information always includes alarm information).

The asynchronous nature of network communication in general and the Channel Access protocol in particular is reflected in the API by having asynchronous operations return futures that allow the calling code to wait for the operation to finish or to be notified asynchronously when it finishes.

In order to make using this library convenient, dependencies are kept to a minimum and the library itself is split into modules so that each application can choose to only bundle the parts it actually needs. At the moment, there is a module providing common components and a module providing the client. In the future, there might also be a module providing a server. Please refer to Section 4, “Requirements” to learn more about the (minimal) requirements for using EPICS Jackie.

1.2. Flexibility and extensibility

The goal of being flexible and extensible has lead to a design of the API that focuses on the use of interfaces instead of concrete classes. This way, a user can choose to swap certain components with her own implementation if the built-in configuration options are not sufficient for a certain use case. In addition to that, components for different sub-tasks have been split into separate interfaces and classes, so that they can be used independently.

For example, the encoding and decoding of Channel Access messages is handled by a separate component, so that a user wanting to implement a special application (for example a diagnostics tool for analyzing Channel Access traffic) can use this building block.

Another example is the Channel Access name resolution (finding the server that hosts a certain channel), which is handled by the ChannelAccessNameResolver. This component can also be used on its own (for example for collecting information about channels on the network) or it can be replaced by a custom implementation (for example an implementation that uses a central directory service instead of using the regular name resolution protocol built into Channel Access).

The concept of flexibility even extends to the threading model of the Chanel Access client, which can be changed by providing a different ClientThreadingStrategy.

2. Comparison with other Channel Access libraries

Traditionally, there have been two Channel Access libraries for Java: Java Channel Access (JCA) and Channel Access for Java (CAJ). The JCA library is a thin wrapper around the C implementation of Channel Access which is used through JNI. The CAJ library, on the other hand, is a pure Java implementation, but it uses the API of JCA, which for obvious reasons has a design close to the C API. This API also has never been updated to use modern features of the Java platform like generics or the concurrency support introduced with Java 5. Besides, the CAJ library is distributed under the terms of the rather restrictive GNU General Public License.

EPICS Jackie, in contrast, has been designed using a modern API that makes use of the features introduced with Java 5 and focusing on the user’s view on channels rather than how the protocol works internally. At the same time, it is distributed under the more liberal Eclipse Public License.

On the network layer, EPICS Jackie has been carefully implemented to mimic the behavior of the C library distributed as part of EPICS Base (3.16.1). Please refer to Section 3.1, “Channel Access protocol” for details on the compatibility level.

In summary, EPICS Jackie provides a modern, pure-Java implementation of the Channel Access protocol that is distributed under a liberal license.

3. Compatibility

EPICS Jackie has been designed to be compatible with a wide range of software components, covering many software versions (including rather old ones).

3.1. Channel Access protocol

EPICS Jackie implements the Channel Access protocol in the same way that the C library provided with EPICS Base 3.16.1 implements it. This includes implementation details like the timing scheme used when sending UDP packets and how the broadcast addresses for name resolution are determined when not configured explicitly.

Just like the C library since EPICS Base 3.16.1, EPICS Jackie is only compatible with Channel Access version 4.4 and newer. The current version of the Channel Access protocol (as of EPICS Base 3.16.1) is 4.13. This limitation should not have any practical impact. The oldest EPICS release publicly available (version 3.12.2 released on April 19th, 1996) already supports Channel Access version 4.6.

The EPICS Jackie client also differs slightly in how it handles connections when TCP is used for name resolution (the Channel Access protocol usually uses UDP, but TCP can be enabled explicitly): While the C implementation uses only one TCP connection when a server is both a name server and a channel server, the EPICS Jackie Client always uses separate connections for name resolution and for accessing channels. This allows for a much simpler and cleaner design of the network logic, separating tasks that are not directly related into separate components. Compared to these benefits, the overhead for maintaining an extra TCP connection is negligible. Most environments use UDP for name resolution and even if TCP is used, the number of name servers is typically low (single digit), so this not have any practical effects.

3.2. Java

EPICS Jackie can be run on Java 6 and newer. Java 6 was chosen because it provides all the convenient language features introduced with Java 5, even adding a few improvements. As of today (May 2016), there should be virtually no platforms supporting Java 5, but not supporting Java 6. On the other hand, there are still a few supported Linux distributions that provide Java 6, but do not provide Java 7. For this reason, EPICS Jackie does not require Java 7. This might change in the future as Java 6 is being phased out in favor of more recent versions.

Please note that in order to compile EPICS Jackie from the sources, JDK 1.7 or newer is needed. The compiler from JDK 1.6 has a bug regarding multiple inheritance in interfaces. The original bug report is not available in the bug database any longer, but there are duplicates in both the Oracle JDK and OpenJDK databases. This bug has only been fixed in JDK 1.7, so the compiler from JDK 1.7 has to be used. However, the compiler’s source compatibility level is set to 1.6, so the compiled classes run inside the JVM from JDK 1.6 without any problems.

This compiler bug supposedly does not exist in some alternative compilers so that it might be possible to compile the code with the compiler from a different implementation of the Java 6 platform. However, as most users are going to use OpenJDK or Oracle JDK anyway, it was decided that it was acceptable to have other dependencies on Java 7 in the build process. In particular, this concerns the use of some Maven plugins that can only run with Java 7. This means that the Maven build process always has to run with Java 7 (or newer), but it is acceptable to specify a Java 6 only compiler in the configuration of the Apache Maven Compiler Plugin. There is no particular reason for using such a setup though. Simply running Maven with JDK 1.7 or JDK 1.8 will result in successfully compiled code that will work in the Java Virtual Machine of Java 6.

3.3. OSGi

The Java archives in which the EPICS Jackie library is distributed are ready-to-use OSGi bundles, containing the required manifest. The same applies to all dependencies of EPICS Jackie, so that it can easily be deployed inside an OSGi container (for example Apache Felix or Eclipse Equinox). However, EPICS Jackie does not have any dependencies on OSGi and can thus be used outside an OSGi container without any modifications.

3.4. Operating systems and architectures

EPICS Jackie is a pure Java-library and is designed to run on all operating systems and architectures supporting the Java Standard Edition. There are a few places where the library makes use of platform-specific features, but neither of these are critical. In these places, the code has typically been designed to support most UNIX-like operating systems (including Linux and OS X) and Microsoft Windows.

4. Requirements

EPICS Jackie can run in any Java run-time environment supporting Java SE 6 or newer. Its only dependency is Apache Commons Lang version 3.4 (or a newer version from the 3.x branch). Apart from this, there are no dependencies (not even a logging framework), making EPICS Jackie very light-weight.

Chapter II. What’s new in EPICS Jackie 2.x

EPICS Jackie 2.0 introduces a few new features. Unfortunately, these features could not be implemented in a way that is completely backwards compatible. However, care has been taken to make the migration as smoothly as possible and in most cases no changes to code using the library are necessary.

Broadcast address discovery

This release changes how the broadcast addresses for name resolution are discovered. This automatic discovery is typically enabled (unless the EPICS_CA_AUTO_ADDR_LIST has been set to “NO” or the corresponding configuration option has been disabled).

In earlier releases, all discovered broadcast addresses of the local interfaces and the local addresses of interfaces that did not have a broadcast address (e.g. point-to-point interfaces or the loop-back interface) were added to the list of addresses to use when resolving channel names.

Now, only broadcast addresses are added. No local addresses are automatically added any longer. This behavior is more consistent with the behavior of the C library. The only difference to the C library is that the destination address of point-to-point interfaces is not added to the list. This is because the Java API does offer any way to determine that address for a point-to-point interface. The loop-back address is only added when no other broadcast address could be found. Typically, this only happens if the computer does not have a regular network interface.

This change has the effect that channels provided by a local server that is only bound to the loop-back interface are not discovered any longer, unless the loop-back address (127.0.0.1) is explicitly added to EPICS_CA_ADDR_LIST (or the corresponding configuration option). Usually, this is not a problem because an IOC will bind to all interfaces by default and will thus be reachable through one of the broadcast addresses automatically added to the list. Therefore, this change in behavior should only make a difference if a server has explicitly been restricted to the loop-back interface (e.g. by setting EPICS_CAS_INTF_ADDR_LIST to “127.0.0.1”).

Echo interval configuration setting

EPICS Jackie now allows setting the echo interval. As a consequence of this, it now honors the EPICS_CA_CONN_TMO environment variable, while older versions used a fixed value of 30 seconds. Obviously this is a breaking change because in older versions, setting this environment variable did not have any effect. In addition to that, the constructors of three classes have been changed so that they now take an additional parameter for this option. In order to keep the compatibility with older code, old versions of these constructors without the new parameter are still available.

For the configuration objects (ChannelAccessClientConfiguration and ChannelNameResolverConfiguration), using these old constructors has the effect that the setting is initialized from the EPICS_CA_CONN_TMO environment variable. For the ChannelAccessServerConnection, using the old constructor has the effect that the default value of 30 seconds is used. The old constructors have been marked as deprecated and are going to be removed in the next major release.

Large array handling

The handling of the EPICS_CA_MAX_ARRAY_BYTES environment variable has changed as well. Starting with version 2.0, EPICS Jackie does not limit the payload size by default. This means that setting EPICS_CA_MAX_ARRAY_BYTES only has an effect when also setting EPICS_CA_AUTO_ARRAY_BYTES (see the section called “Maximum payload size” for details). This change is consistent with the new behavior of the C library introduced with EPICS Base 3.16.1.

Authentication with TCP-based name servers

The client now sends a username and hostname to TCP name-servers. Before, it only sent that information to channel servers. This change is in accordance with the new behavior implemented by the C library of EPICS Base 3.16.1.

As a consequence of this change, the constructors of the ChannelNameResolverConfiguration and ChannelAccessServerConnection had to be changed. The old constructors are still available, but they have been marked as deprecated and are going to be removed in the next major release.

Multicast socket options

This release introduces a few changes that are useful when using multicast packets for name resolution. In particular, IP_MULTICAST_LOOP and IP_MULTICAST_TTL options are set on the UDP sockets used for name resolution (if the platform supports it). The value of the IP_MULTICAST_TTL option can be controlled with the EPICS_CA_MCAST_TTL environment variable (see the section called “Multicast time-to-live” for details). This change is consistent with the new features of the C library introduced with EPICS Base 3.16.1.

As a consequence of this change, the constructor of ChannelNameResolverConfiguration had to be changed in order to accommodate for the new option. The old constructor is still available, but it has been marked as deprecated and is going to be removed in the next major release.

Listener lock-policy

This release introduces a new feature called the “listener lock-policy”. This feature can help to reduce the risk of deadlocks caused by listeners acquiring a lock.

This risk of a deadlock when acquiring a lock in a listener stems from the fact that the code calling the listener also acquires an internal lock and holds it while calling the listener. The measures to reduce the risk of a deadlock consist of two parts: The first part is fully backwards compatible (and has in fact been back-ported to the 1.x branch of this software). The second part introduces a new option, namely the listener lock-policy that can be chosen by the developer using this library.

The first improvement is that the lock that is held while calling a listener is not the same lock acquired by most methods any longer. Instead, a separate lock is used for each listener. The only method also acquiring this lock is the method that removes the listener and the destroy method of the object holding the listener (only for channels and monitors). This lock is needed because it is the only way to assure that a listener is not called after the remove method has returned or the respective object has been destroyed.

Effectively, this means that the only way to cause a dead-lock is to call the remove listener method or the destroy method holding a lock that is also acquired from within the listener being removed (or any listener registered to the object in case of the destroy method). When only the listener itself calls the remove or destroy method, this is safe because it already holds the lock and thus the method will never block.

The second improvement introduces the new ListenerLockPolicy. By default, the BLOCK policy is used, which results in the exact behavior described earlier. This means that when using the BLOCK policy, the code behaves exactly the same way as before and a deadlock might occur when the listener takes a lock that is also held while removing the same listener.

A different behavior is seen when choosing the IGNORE or REPORT policy. In this case, even removing the listener while holding a lock that is also acquired by the same listener cannot cause a deadlock. The price one has to pay for this is that it is no longer guaranteed that the listener is not going to be notified after the remove listener method has returned or the object to which the listener is registered has been destroyed.

The difference between the IGNORE and the REPORT policy is that the REPORT policy causes the remove listener method to throw a ConcurrentNotificationException when it detects that the listener might receive one more notification. In any case, the listener will receive at most one more notification after the remove listener method returns, but if the policy is set to REPORT and the remove listener method (or the destroy method) does not throw an exception, it is guaranteed that the listener is not going to be called again – just like when using the BLOCK policy.

Even with these changes, having listeners that might potentially block is not without danger. By default, listeners are executed in the I/O thread and thus a listener that blocks will result in the I/O thread being blocked. Please refer to Section 5, “Listener lock-policy” and Section 6, “Threading strategy” for details.

In order to support this new feature, a few changes to the API were necessary. The DefaultChanneAccessClient has a number of new constructors that accept a ListenerLockPolicy. The old constructors are still fully supported and a client created with one of these constructors is going to use the BLOCK policy, thus showing the same behavior as in previous releases.

The ChannelAccessChannel, ChannelAccessMonitor, and ListenableFuture interfaces have been extended in order to account for the new feature. They now all have a getListenerLockPolicy() method that returns the lock policy in use.

The AbstractChannelAccessChannel, AbstractChannelAccessMonitor, AbstractListenableFuture, and DelegatingListenableFuture classes implement this new method, so any class derived from one of these classes automatically implements the new behavior. However, it is necessary to change the constructor of such derived classes if a different lock policy than BLOCK shall be used.

Effectively, this means that most code does not have to be changed. Code simply using the Channel Access client provided by EPICS Jackie will simply work as before, but it can now choose a different listener lock-policy if desired. Only code that provides a custom implementation of one of the interfaces mentioned earlier (e.g. for tests) might have to be adapted.

Changes to AbstractChannelAccessChannel

The AbstractChannelAccessChannel class has a new method failWaitForConnectionStateFutures() that should be called by child classes when the channel is destroyed.

This method makes sure that futures returned by waitForConnectionState(boolean) fail with an IllegalStateException when the channel is destroyed. In older releases, these futures would never complete when the channel was destroyed and thus threads waiting on these futures would block indefinitely.

The default implementation of ChannelAccessChannel shipped with the library implements the new behavior, so usually no changes in code using the library are needed. Only custom implementations of ChannelAccessChannel (e.g. ones that have been created for tests) need to be updated.

Changes to ChannelAccessValueFactory

The methods in the ChannelAccessValueFactory that take an alarm severity and status have been refactored. The signatures of the old methods were inconsistent and to some extent confusing. The methods with the old signatures are still available, but they have been marked as deprecated and are going to be removed in the next major release.

Most code should not be affected by these changes: Creating a value with an alarm severity and status typicall only makes sense on the server side, but there is no server-side implementation for EPICS Jackie yet. The most likely scenario in which existing code might already use these methods is to create values that look like they had been created by a server (e.g. for tests).

Chapter III. EPICS Jackie Client

The client component is the part of the EPICS Jackie library that most users are going to use. Section 1, “Getting started” provides all the information needed to get an application running that uses the client. The subsequent sections of this chapter provide more information about how the client can be customized for a specific environment or use-case. A reader wanting to use the client is also encouraged to read Chapter IV, EPICS Jackie Common Components, Section 2, “Channel Access value objects” in order to understand how values read from or written to Channel Access channels are handled.

1. Getting started

This section provides all the information needed for getting the first first application using the EPICS Jackie library running.

1.1. Installation

When using the Apache Maven (or a compatible) build tool, using the EPICS Jackie Client in a project is as simple as adding the following dependency to the project configuration:

    <dependency>
      <groupId>com.aquenos.epics.jackie</groupId>
      <artifactId>epics-jackie-client</artifactId>
      <version>2.0.0</version>
    </dependency>

When managing dependencies manually, please make sure that the commons-lang3-3.4.jar, epics-jackie-client-2.0.0.jar, and epics-jackie-client-2.0.0.jar are included in the classpath of your project.

1.2. Simple example

In order to get started, here is a very simple example that creates a client using the default configuration, reads a value, increments it by one, writes it back to the channel, and finally reads the updated value again:

import com.aquenos.epics.jackie.client.ChannelAccessChannel;
import com.aquenos.epics.jackie.client.ChannelAccessClient;
import com.aquenos.epics.jackie.client.DefaultChannelAccessClient;
import com.aquenos.epics.jackie.common.value.ChannelAccessDouble;

public class SimpleClient {

    public static void main(String[] args) throws Exception {
        ChannelAccessClient client = new DefaultChannelAccessClient();
        String channelName = "testChannel";
        ChannelAccessChannel channel = client.getChannel(channelName);
        channel.waitForConnectionState(true).get();
        ChannelAccessDouble value = channel.getDouble().get();
        double doubleValue = value.getValue().get(0);
        System.out.println("The current value is " + doubleValue + ".");
        channel.putDouble(doubleValue + 1.0).get();
        value = channel.getDouble().get();
        doubleValue = value.getValue().get(0);
        System.out.println("The new value is " + doubleValue + ".");
        channel.destroy();
        client.destroy();
    }

}

Running this simple example program twice should result in an output similar to the following one (assuming that there is a server hosting “testChannel” on the network and that the channel is writable):

The current value is 0.0.
The new value is 1.0.
The current value is 1.0.
The new value is 2.0.

The individual steps involved in this example are explained in greater detail in the next section.

2. Using the client

When using the EPICS Jackie client, an application first has to create a client instance that it can then use to retrieve channels. These channel objects can then be used to perform operations on the individual channels. When they are no longer needed, channels should be destroyed. The same applies to the client instance, which should also be destroyed when it is no longer needed. A typical application, however, will retain a single client instance over the whole life-cycle of the application, only destroying it when the application is shutdown.

2.1. Initializing the client

The client is represented by an object implementing the ChannelAccessClient interface. The EPICS Jackie client library provides an implementation of this interface that is called DefaultChannelAccessClient. This implementation should be suitable for most applications.

ChannelAccessClient client = new DefaultChannelAccessClient();

When using the default constructor, the Channel Access client uses a default-constructed ChannelAccessClientConfiguration and a DefaultClientThreadingStrategy that uses the ErrorHandler from the ChannelAccessClientConfiguration.

By default, the ChannelAccessClientConfiguration initializes itself using environment variables with the EPICS_CA_* prefix These are the same environment variables that are also used by the Channel Access C library. These defaults can be overridden by passing an explicitly constructed ChannelAccessClientConfiguration. There also are a few configuration options that cannot be specified through environment variables but can only be specified by passing them to the ChannelAccessClientConfiguration constructor. Please refer to Section 3, “Configuration options” for details concerning the client configuration.

The DefaultClientThreadingStrategy in its default configuration should be suitable for most use-cases. However, it is possible to modify some behavior by constructing it explicitly and specifying certain options. In the rare case that these customization options are not sufficient, it is even possible to use a custom implementation of the ClientThreadingStrategy. Please refer to Section 6, “Threading strategy” for details concerning the threading strategy.

The ChannelAccessClient is thread-safe. This means that a single instance is typically sufficient for the whole application. In fact, sharing a single instance actually help to save system resources. The only reason for using more than one instance in the same application is when it is necessary to have clients that use different configurations.

2.2. Connecting a channel

After the client has been constructed, channels can be created. For example, we can create a reference to a channel named “testChannel” like this:

ChannelAccessChannel channel = client.getChannel("testChannel");

Each time a channel is requested from the client, a new instance of ChannelAccessChannel is returned. However, all instances referring to the same channel name internally use the same implementation so that the overhead for maintaining several instances is negligible. This concept facilitates the use of a single client for a whole application because a component can request a channel without having to worry whether another component has requested the same channel. This resource sharing is implicitly handled by the library and completely transparent to the user.

Like the ChannelAccessClient, instances of ChannelAccessChannel are thread-safe.

2.3. Waiting for the connection

When a channel is created, the new channel object is returned immediately without blocking for network I/O. However, the client starts a name-lookup process for the specified channel name in the background. When the server hosting the channel is found, the client automatically connects to this server and subsequently to the channel. When the client is already connected to a channel hosted by the same server, the existing connection is reused instead of establishing a new one. In any case, once the channel has been successfully connected, the respective channel object changes to the connected state. When, at some later point, the connection is interrupted (for example because of a general network interruption or because the server is stopped), the channel object changes to the disconnected state and the client starts the lookup process again. Once the (new) server for the channel is found, the connection to the server is established again and the channel changes to the connected state once again.

This whole process is transparent to the user of the client. The ChannelAccessChannel object remains valid over the whole life-cycle until it is explicitly destroyed by the user. However, read or write operations fail while a channel is not connected and monitor subscriptions do not receive updates. For this reason, the user might be interested in knowing the channel’s connection state.

Querying the connection state

The easiest way of learning the channel’s connection state is by calling the isConnected method:

if (channel.isConnected()) {
    System.out.println("The channel is connected.");
} else {
    System.out.println("The channel is not connected.");
}

However, when we are interested in waiting for the channel to be connected, this method is not very useful because using it would involve inefficient polling. Instead, there are two more efficient ways to wait for a certain connection state. The first one involves waiting for a future and the second one registers a listener. Waiting for a future is more suitable when we just want to wait once, while using a connection listener is better when we want to be informed of all connection-state changes.

Waiting using a future

We can wait for the channel to be connected just once like this:

try {
    channel.waitForConnectionState(true).get();
    System.out.println("The channel is now connected.");
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

If we wanted to wait for the channel to be disconnected once, we would pass false instead of true to the waitForConnectionState method. The future returned by waitForConnectionState implements ListenableFuture. This means that we do not have to block in order to wait for the future to complete. Instead, we can register a listener that is notified asynchronously when the future completes. Actually, this applies to all methods of the EPICS Jackie Client that return a future. These futures always are instances of ListenableFuture and can thus be used with listeners.

In order to register a listener, we first create the future and then add the listener to it:

ListenableFuture<Boolean> waitForConnectionStateFuture = channel
        .waitForConnectionState(true);
waitForConnectionStateFuture
        .addCompletionListener(new FutureCompletionListener<Boolean>() {
            @Override
            public void completed(ListenableFuture<? extends Boolean> future) {
                try {
                    future.get();
                    System.out.println("The channel is connected now.");
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    throw new RuntimeException(
                            "Unexpected InterruptedException.");
                }
            }
        });

The completion listener is notified when the future completes. If the future represents the result of a successful operation, the value returned by its get method represents the result of the operation. In this example, this value is the connection state (true for connected, false for disconnected). If the future represents a failed operation, the future’s get method throws an ExecutionException that wraps the cause of the execution failure. An InterruptedException is only thrown when the thread blocked in order to wait for the future to complete and was interrupted before the future completed. As the future has already completed when the listener is notified, such an exception does not have to be expected.

A listener registered with a future must not block or perform long running operations. Typically, such a listener is executed in the thread that completes the future. For a future that represents the connection state, this is most likely the I/O thread associated with the server hosting the channel. When the listener blocks, this will bring the whole thread to a halt, stopping any I/O from happening. Depending on the blocking action, this could even lead to a dead lock when the action the listener is waiting for would actually need to perform I/O operations in order to finish.

While the user is strongly discouraged from performing blocking operations in listeners, there is a way to protect the client from being stalled. Please refer to Section 6, “Threading strategy” for details.

Even a simple blocking action, that usually finishes quickly, like acquiring a lock, can be dangerous. Please refer to Section 5, “Listener lock-policy” for details.

Waiting using a connection listener

The alternative way of being notified when the channel is connected (or disconnected) is through a ChannelAccessConnectionListener that is notified every time when the channel changes its connection state:

channel.addConnectionListener(new ChannelAccessConnectionListener() {
    @Override
    public void connectionStateChanged(ChannelAccessChannel channel,
            boolean nowConnected) {
        if (nowConnected) {
            System.out.println("Channel " + channel.getName() + " connected.");
        } else {
            System.out.println("Channel " + channel.getName()
                    + " disconnected.");
        }
    }
});

Directly after being registered, the listener is called once with the current connection state and after that once for every change of the connection state.

Like future listeners, connection listeners must not block unless specific precautions have been taken. Please refer to Section 5, “Listener lock-policy” and Section 6, “Threading strategy” for details.

[Note]Note

In the default implementation of the Channel Access client, the client does not keep a strong reference to a channel. This means that a channel will eventually be marked for garbage collection and destroyed if the application does not retain a strong reference to it. This happens even if there are active connection listeners attached to the channel.

However, if possible, an application should not rely solely on garbage collection for resource management because garbage collection is inherently unpredictable. The application should call the channel’s destroy method as soon as it does not need the channel any longer. This will ensure that network resources associated with the channel are released immediately (for example, the connection to the server can be closed if there are no remaining channels hosted by the same server).

The automatic destruction during garbage collection is only intended as a last resort in case a bug in the application would otherwise cause a resource leak, very similarly to how the Java platform manages I/O resources. Please refer to Section 2.8, “Resource management” for more information about resource management in the EPICS Jackie Client.

2.4. Reading from a channel

The Channel Access protocol provides a “get” operation that can be used to read the current value of a channel just once. This operation should not be used if continuous updates of a channel’s value are desired. For this use-case, Channel Access provides much more efficient “monitor subscription” (see Section 2.6, “Using monitor subscriptions”).

It is an inherent feature of the Channel Access protocol that the type of a channel cannot be known a priori. For this reason, there are two options for getting a channel’s value: The request can be made using the native type of the channel, that can only be determined at run-time. Alternatively, the request can be made using a specific type. In this case, the server will convert the channel’s value to the requested type before sending it to the client.

First, we look at an example where we specify a specific data-type:

try {
    ChannelAccessDouble value = channel.getDouble().get();
    System.out.println("The channel has the value " + value.getValue().get(0)
            + ".");
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

As we specified that we want to get a value of type DBR_DOUBLE (by using the getDouble method), we know at compile time that we will get a value of type ChannelAccessDouble. Actually, the type of the return value is ChannelAccessSimpleOnlyDouble, but that interface does not offer any additional methods, so we can simply treat it like a ChannelAccessDouble for now.

The getDouble method does not return the value directly. Reading a channel's value requires network I/O, so the operation can not complete right away. Instead of blocking, the getDouble method returns a listenable future that completes when the value has been read (or the operation has failed). Like we have already seen in the section called “Waiting using a future”, we can block, waiting for the future to complete, or we can register a listener that is notified when the future has completed.

The get… methods of ChannelAccessChannel return sub-types of ChannelAccessValue instead of returning a primitive (or an array thereof) directly. There are two reasons for this: First, these values fit in the same type hierarchy that is also used when the actual type is determined at run-time. This makes the code more generic. Second, depending on the type, the values may carry additional information like the channel’s alarm state or a time stamp.

The value’s getValue method returns a DoubleBuffer instead of a double[] array. This way, the same value may be shared by different components of an application without having to worry that one component might accidentally modify the value, leading to undesired or even undefined behavior. As an alternative, values could be copied whenever they are passed to more than one component, but this would be very inefficient, in particular when large arrays are involved.

Each of the channel’s get… methods has to variants: The first one takes an integer parameter. This parameter specifies the maximum number of array elements that are read from the server. If the channel’s value is represented by an array with more elements, the remaining elements are simply skipped. The second variant does not take any parameters. It always returns all the elements, regardless of the array’s size.

There are two get… methods for each of the data types defined in ChannelAccessValueType, the only exception being the DBR_PUT_ACKT and DBR_PUT_ACKS types which can only be used in put operations. For each of the seven basic types (DBR_STRING, DBR_SHORT, DBR_FLOAT, DBR_ENUM, DBR_CHAR, DBR_LONG, and DBR_DOUBLE), there are sub-types that provide the alarm status (DBR_STS_…), the time stamp and alarm status (DBR_TIME_…), the alarm status and display information (DBR_GR_…), and the alarm status, display information and control limits (DBR_CTRL_…). In addition to that, there are two special types, DBR_STSACK_STRING and DBR_CLASS_NAME. The DBR_STSACK_STRING type (method getAlarmAcknowledgementStatus) is used to get information about acknowledged and unacknowledged alarms and the configuration regarding the acknowledgement of transient alarms. The DBR_CLASS_NAME type (method getClassName) is used to get the name of the implementation backing the channel. For a regular EPICS IOC, this is the name of the record type (for example ai, ao, bi, bo, calc, longin, or longout).

If we do not want to get a value of a specific type, but rather want to use the type that is native to the server, we can use the channel’s getNative method. There also is a get method taking a parameter of type ChannelAccessValueType. It can be used when a specific type shall be requested but this type is not known at compile-time. For example, an application might be interested in getting a value with the type native to the server, but also wants a time stamp. In this case, the application can use the channel's getNativeDataType to determine the native type and than use the corresponding type that also includes a time stamp:

ChannelAccessValueType nativeType = channel.getNativeDataType();
ChannelAccessValueType timeType;
try {
   timeType = nativeType.toTimeType();
} catch (IllegalArgumentException e) {
    timeType = ChannelAccessValueType.DBR_TIME_STRING;
}
try {
    ChannelAccessGettableValue<?> value = channel.get(timeType).get();
    ChannelAccessTimeValue<?> timeValue = (ChannelAccessTimeValue<?>) value;
    System.out.println("Time stamp is " + timeValue.getTimeStamp().toString());
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

As the exact type cannot be known at compile time, the get method returns a future that in turn returns a value of type ChannelAccessGettableValue. This is the base type implemented by all values that can be returned by read operations. In this example, we can safely cast the value to ChannelAccessTimeValue because we requested a DBR_TIME_… type, so the returned value must implement ChannelAccessTimeValue. The ChannelAccessValue interface provides a method getType that can be used to determine the actual type of a value at run-time. There also is a getGenericValueElement method that can be used to retrieve an element of the value array (specifying the element’s index) without having to know its actual type. Combined with the getValueSize method, this can be used to get all the value elements without knowing their concrete type. However, this involves boxing of primitive values to turn them into objects, making this much less efficient than casting to the correct type and then using the getValue method to get a buffer providing primitives.

Please note that the getNativeDataType method (like the get… methods) can only be used when the channel is connected. Using it when the channel is not connected results in an IllegalStateException being thrown. The get… methods do not throw an IllegalStateException. Instead, the returned future’s get method throws a ChannelAccessException with a status of ECA_DISCONN.

2.5. Writing to a channel

For writing to a channel, the Channel Access protocol provides a “put” operation. Writing a value is very similar to reading one. For example, in order to write a double value, we could use the following code:

try {
    // The get() call on the future is only necessary if we want to wait for the
    // operation to complete or if we want to be notified if it fails.
    channel.putDouble(42.0).get();
} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Like a read operation, a write operation completes asynchronously. The ListenableFuture returned by the put… method can be used to wait for the operation to be completed or to register a listener that is notified when the operation has completed. If we are not interested in waiting for the operation to complete, we can simply ignore the returned future. In this case, however, we will not be notified either when the operation fails. If we are interested in knowing whether the operation has been successful, we have to call the future’s get method (which will block when the future has not completed yet). This method returns null on success and throws an exception on failure.

The ChannelAccessChannel has two put… methods for each of the seven basic data-types described by ChannelAccessValueType (DBR_STRING, DBR_SHORT, DBR_FLOAT, DBR_ENUM, DBR_CHAR, DBR_LONG, and DBR_DOUBLE). For each data type, there is one method taking a single element and one taking an array of elements (for example double and double[]). The DBR_STRING type is an exception because there is an additional method taking a Collection of Strings.

Writing values of a “complex” type (for example a DBR_GR_STRING) is not supported by the Channel Access protocol. However, there are two special types, that can only be used in write, but not in read operations: DBR_PUT_ACKT and DBR_PUT_ACKS. The first one is used for configuring whether transient alarms need to be acknowledged and can be written by using the channel’s putConfigureAcknowledgeTransientAlarms method. The second one is used for acknowledging alarms and can be written by using the channel’s putAcknowledgeAlarm method.

It is also possible to write a value of a type that is only determined at run-time. The channel’s put method takes a parameter of type ChannelAccessPuttableValue. This is a marker interface that is only implemented by types that may be used in write operations. An application must never attempt to supply its own implementation of any of the interfaces in the ChannelAccessValue type hierarchy. Instead, one of the methods provided by ChannelAccessValueFactory should be used to construct an instance. Please refer to Chapter IV, EPICS Jackie Common Components, Section 2, “Channel Access value objects” for more information about the Channel Access value type system.

Please note that the the put… methods can only be used when the channel is connected. When it is used while the channel is not connected (or when the channel disconnects before the operation has finished) the future returned by the put… method throws a ChannelAccessException with a status of ECA_DISCONN.

2.6. Using monitor subscriptions

The Channel Access protocol has a very useful feature called “monitors”. These monitors can be used to be notified by a server whenever a channel’s state or value changes (significantly). This works asynchronously so that it is much more efficient than polling the channel. This helps to reduce latency and to preserve processing power, I/O operations, and bandwidth.

Creating a monitor subscription is very similar to reading from a channel. Monitor subscriptions support exactly the same data types that are also supported for read operations. For example, the following code shows how we can create a monitor subscription for double values that include a time stamp:

ChannelAccessMonitor<ChannelAccessSimpleOnlyDouble> monitor = channel
        .monitorDouble();
monitor.addMonitorListener(
        new ChannelAccessMonitorListener<ChannelAccessDouble>() {
            @Override
            public void monitorError(
                    ChannelAccessMonitor<? extends ChannelAccessDouble> monitor,
                    ChannelAccessStatus status, String message) {
                System.out.println("Error: " + status.toString()
                        + (message == null ? "" : ", " + message));
            }

            @Override
            public void monitorEvent(
                    ChannelAccessMonitor<? extends ChannelAccessDouble> monitor,
                    ChannelAccessDouble value) {
                System.out.println("Value is " + value.getValue().get(0));
            }
        });

Monitor subscriptions differ from read operations in three ways: First, they can always be created, even when the channel is disconnected. Of course, events are only received as long as the channel is actually connected. Second, the monitor… methods return a ChannelAccessMonitor instead of a ListenableFuture. Third, a monitor subscription does not cause just one event, but it causes one event after the monitor is created and one event every time an event is received from the server.

The ChannelAccessMonitorListener has two methods. The monitorError method is called when the server sends an error notification. Typically, a server only sends an error notification when the monitor subscription could not be created for some reason. When the channel is disconnected, this does not cause an error notification because this is not considered a problem: As long as a monitor exists, it will automatically start sending events once the channel is (re-)connected. The monitorEvent method is called every time when an updated value is received from the server. This method is also called once after the monitor subscription has been created (and the first value has been received from the server). It is also called once after the connection with the server has been reestablished because the value on the server might have changed in the meantime.

[Note]Note

In the default implementation of the Channel Access client, the client does not keep a strong reference to a monitor. This means that a monitor will eventually be marked for garbage collection and destroyed if the application does not retain a strong reference to it. This happens even if there are active listeners attached to the monitor.

However, if possible, an application should not rely solely on garbage collection for resource management because garbage collection is inherently unpredictable. The application should call the monitor’s destroy method as soon as it does not need the monitor any longer. This will ensure that network resources associated with the monitor are released immediately (for example, the server can stop sending updates if there are no other monitors sharing the same subscription).

The automatic destruction during garbage collection is only intended as a last resort in case a bug in the application would otherwise cause a resource leak, very similarly to how the Java platform manages I/O resources. Please refer to Section 2.8, “Resource management” for more information about resource management in the EPICS Jackie Client.

For each get… method of the ChannelAccessChannel, there are two corresponding monitor… methods. One of them has the same parameters as the corresponding get… method. The other one takes an additional parameter of type ChannelAccessEventMask. This parameter defines the kind of events for which the server should send a notification. If not specified, the default event mask specified in the ChannelAccessClientConfiguration for the client is used.

There are four basic mask values (DBE_VALUE, DBE_ARCHIVE, DBE_ALARM, and DBE_PROPERTY). These mask values can be combined to build other masks. DBE_VALUE is used for retrieving notifications when the channel’s value changes. DBE_ARCHIVE is also used for notifications on value change, but a server can choose to use a larger dead-band compared to the one used for DBE_VALUE. This means that typically, only larger value changes (that are considered significant enough to be archived) result in a notification. Usually, only one of DBE_VALUE or DBE_ARCHIVE are specified in a mask. DBE_ALARM is used for being notified whenever a channel’s alarm state changes. DBE_PROPERTY is used for being notified when other properties of a channel (for example engineering units or limits) change. The default mask is the combination (binary or) of DBE_VALUE and DBE_ALARM.

Like when reading values, the type that is supposed to be used for a monitor subscription cannot always be determined at compile-time. For these cases, the ChannelAccessChannel provides the monitorNative and monitor methods.

The monitorNative methods create a monitor that always delivers values with the type that is native to the server. As a channel might be disconnected and reconnected to a different server (or the same server using a different configuration), the native type can change over the life-time of the monitor subscription. In this case, the type of the values delivered with monitor events will change accordingly, always representing the current native type.

The monitor method allows the user to specify the data type of the value that the server shall send with monitor events. This must be one of the types that is allowed for a read operation. Please refer to Section 2.4, “Reading from a channel” for details about specifying the data type for a read operation at run-time.

2.7. Access restrictions

The Channel Access protocol allows a server to restrict access to a channel. There can be separate access restrictions for read and write operations. For example, a server might choose to allow read access to all clients but restrict write access to certain clients.

An application can query the access rights for a channel by using the ChannelAccessChannel’s isMayRead and isMayWrite method. The first one returns true if read operations (this means “get” and “monitor” operations) are allowed, the second one returns true if write (“put”) operations are allowed.

The access rights can only be checked when the channel is connected. Calling either of the methods when the channel is disconnected results in an IllegalStateException being thrown. Please note that when a channel is connected, then disconnected, and then connected again, the access rights might have changed compared to the first connection because the channel might now be hosted by a different server (or the same server, but using a different configuration).

Calling one of the get… or put… methods when the client does not have the corresponding access right results in the future returned by the method failing with a ChannelAccessException that has a status code of ECA_NORDACCESS or ECA_NOWTACCESS respectively.

In the same way, a monitor triggers an error notification with a status code of ECA_NORDACCESS when a monitor subscription cannot be created because of insufficient access rights.

2.8. Resource management

The objects provided by the EPICS Jackie Client (the ChannelAccessChannels, the ChannelAccessMonitors, or even the ChannelAccessClient itself), are associated with network resources. For this reason, it is important that these objects are destroyed so that the corresponding network resources can be deleted when they are not needed any longer.

Each of these objects can be destroyed by calling its destroy method. Destroying a channel also destroys all its monitors and destroying a client also destroys all its channels.

Each time the ChannelAccessClient’s getChannel method is called, a separate ChannelAccessChannel instance is returned. Even though the resources of all the instances are shared internally, thus avoiding the redundant allocation of resources, each instance is destroyed separately. This means that the resources are only realeased after all instances of ChannelAccessChannel referring to the same channel name have been destroyed.

The same applies to ChannelAccessMonitors. Each time one of the monitor… methods is called, a new instance is returned. However, those instances internally share resources. This means that several monitor’s created for the same channel name and using the same parameters (monitor event mask, data type, and element count) internally use only one subscription. This subscription is cancelled when the last monitor referring to it is destroyed.

In order to avoid a resource leak when an application erroneously does not destroy a channel or monitor when it does not need it any longer, the default implementation (DefaultChannelAccessClient) does not keep strong references to channels and monitors. This means that a channel or monitor that is not referenced any longer will eventually be marked for garbage collection and destroyed. For this reason, it is important that application code wanting to keep a channel or monitor alive (typically because it has a listener attached to it and wants to keep getting notified of events) has to retain a strong reference to the channel or monitor.

For example, the following code will not work as naively expected:

ChannelAccessMonitorListener listener = ... // The details of the listener are
                                            // not relevant for this example.
// Bad example, do NOT write code like this:
channel.monitorDouble().addMonitorListener(listener);

First, the monitor listener might receive events as expected. After some time, however, the Java Virtual Machine might run the garbage collection, resulting in the monitor instance created by monitorDouble being destroyed because there is no strong reference to it.

Still, an application should not solely rely on garbage collection for destroying channels or monitors. Typically, the Java Virtual Machine initiates a garbage collection when there is demand for it because free memory is becoming scarce. However, channels and monitors not only consume memory, but also consume precious network and I/O resources (for example file descriptors). This means that there could be a situation in which these resource become scarce, but the garbage collection does not run because free memory is still abundant. This is the same reason why Java I/O objects (for example InputStream or OutputStream) should be closed explicitly instead of relying on garbage collection closing them.

When a strong reference to a monitor is kept, it is not necessary to also keep a strong reference to the corresponding channel. Each monitor keeps a strong reference to its channel (which can be retrieved using the getChannel method). For this reason, a channel will only be destroyed by the garbage collection after all monitors referencing it have been destroyed.

As a general guideline, an application should keep a separate channel or monitor instance for each of its components, even if they refer to the same channel. As described earlier, the ChannelAccessClient ensures internal sharing of resources so that the overhead is negligible. The resource management is simplified significantly by this approach: A component can create the channel or monitor when the component is created and destroy it when the component is destroyed.

For example, an application with a graphical user-interface (GUI) might have widgets that refer to channels for displaying purposes. In this case, it makes sense that each of these widgets has its own instance of ChannelAccessChannel. When a widget is destroyed, it can safely destroy its associated channel. This means, for example, that a window being closed would destroy all the widgets used in this window and thus indirectly all the channel instances. If the same channels were also used in other windows, the internal resources would be kept alive and thus the other widgets would continue working as expected. However, when the last window referencing a certain channel was closed, this would result in the immediate destruction of the last ChannelAccessChannel instance, thus immediately freeing the associated resources.

Basically, the same applies to instances of ChannelAccessClient. When a client is not needed any longer, it should be destroyed. However, most applications will need the client for their whole life-time. When the Java Virtual Machine is shutdown, the resources associated with a client are released implicitly, making it not strictly necessary to destroy the client. However, it is still considered good style to destroy the client explicitly when reasonably possible. Having more than one client instance per application should be avoided at all cost. Each client instance consumes precious resources (network sockets and background threads), so operation is most efficient when there is only one client. In addition to that, the sharing of resources for channels and monitors is realized on the client level. This means that if there is more than one client and each of these clients creates a channel instance referring to the same channel name, those channel instances will not share resources, increasing resource consumption significantly. In case of doubt, it is better to only have a single client instance and not being able to properly destroy the client than creating many client instances just for the sake of simpler resource management.

3. Configuration options

The ChannelAccessClient can be configured by supplying an instance of ChannelAccessClientConfiguration. Instances of ChannelAccessClientConfiguration are immutable, so all configuration parameters have to be passed to the constructor. The parameter-less constructor initializes all configuration parameters with default values. When using the explicit constructor, it is possible to only specify a few parameters explicitly. Specifying null for the other parameters has the effect of initializing them with their default values.

In addition to the parameters that can be set directly in the client configuration, the client configuration contains an instance of BeaconDetectorConfiguration and an instance of ChannelNameResolverConfiguration. As far as the user is concerned, these configuration objects can simply be considered a part of the client configuration. They are only separated into separate classes because they provide the configuration for separate components that in this way can avoid any dependency on the general client configuration.

Like the ChannelAccessClientConfiguration, instances of these classes are immutable and configuration parameters have to be specified when constructing the object. Like the client configuration, they both have a default constructor that specifies null for all parameters, thus setting them to their default values.

3.1. Client configuration

The ChannelAccessClientConfiguration is the top object in the configuration hierarchy used by the ChannelAccessClient. It encapsulates all the configuration options needed by a client, including an instance of BeaconDetecorConfiguration (explained in Section 3.2, “Beacon detector configuration”) and an instance of ChannelNameResolverConfiguration (explained in Section 3.3, “Channel-name resolver configuration”).

Character set

The configured character set is used whenever string data is received from the network or sent to the network. This obviously concerns string values, but other values that carry strings in their meta-data (for example the engineering units) and textual error messages sent over the network are affected as well.

The character set specified for the ChannelAccessClientConfiguration is also used by the ChannelNameResolverConfiguration (see Section 3.3, “Channel-name resolver configuration”) unless a ChannelNameResolverConfiguration is specified when creating the ChannelAccessClientConfiguration.

If not specified, the UTF-8 character set is used. In the unlikely event that the UTF-8 character set is not available (it should always be available because it is one of the standard character sets supported by the Java Standard Edition), the platform’s default character set is used.

Host name

The host name is sent to each server when a connection is established. Some servers use the host name in order to decide which access rights (see Section 2.7, “Access restrictions”) should be granted to the client. The host name sent to the server usually is the unqualified host-name (without a domain part).

If not specified explicitly, the host name is determined automatically. Unfortunately, the Java platform specifies no standardized way for reliably determining the host name. For this reason, a multi-step process is used. First, the library tries to call the hostname command and uses its output. If this command is not available or returns with an error, a platform-specific environment variable (HOSTNAME on UNIX-like systems, COMPUTERNAME on Windows systems) is used. If this environment variable is not set either, the library tries to determine the hostname through DNS (by calling InetAddress.getLocalHost().getHostName()). If this fails as well, the library uses the string “<unknown host>” as the host name.

Typically, this setting is chosen so that is has the same value as the corresponding setting in the channel-name resolver configuration.

User name

The user name is sent to each server when a connection is established. Some servers use the user name in order to decide which access rights (see Section 2.7, “Access restrictions”) should be granted to the client.

If not specified explicitly, the user name is determined automatically. Unfortunately, the Java platform specifies no standardized way for reliably determining the user name. For this reason, a multi-step process is used. First, the library tries to use platform-dependent classes from the com.sun.security.auth.module package. If these classes are not available (they are not part of the Java Standard Edition but are only bundled with certain Java run-time environments) or their use results in an exception, the user.name system property is used. If this property is not set, the USERNAME environment variable is used. If this environment variable is not set either, the library uses the empty string as the user name.

Typically, this setting is chosen so that is has the same value as the corresponding setting in the channel-name resolver configuration.

Maximum payload size

The Channel Access protocol works through the exchange of messages. Some of these messages (in particular the ones transporting values for “get”, “put”, and “monitor” operations) have a payload. The size of this payload (and thus the total message size because the header has a fixed size-limit) can be limited. The reason for limiting the payload size is the fact that a received messages has to be buffered completely before it can be decoded. In the same way, a message that is sent is typically completely serialized to a buffer before being sent.

When choosing a large maximum payload size, the potential memory consumption for these buffers increases. When choosing a small maximum payload size, this has the effect that larger messages cannot be sent or received, limiting the maximum number of elements that the value array for a “put” operation (send limit) or the value array for a “get” or “monitor” operation (receive limit) can contain.

Changing the limit on the client side is only effective when the server uses the same or greater limits. If the client uses greater limits than the server, the limits set by the server will effectively limit the maximum size of the payload.

If not configured explicitly, the library does not limit the payload size (except for the technical limit described in the next paragraph). However, if the EPICS_AUTO_ARRAY_BYTES environment variable is set to “NO” (case-insensitive), the value of the EPICS_CA_MAX_ARRAY_BYTES environment variable is used for both the send and receive limit. If that environment variable is not set, a default value of 16384 bytes is used. This value is also the minimum payload size that can be set. The C library from EPICS Base uses the same two environment variables since EPICS Base 3.16.1. Before EPICS Base 3.16.1, the C library did not consider the EPICS_CA_AUTO_ARRAY_BYTES and always acted like that environment variable was set to “NO”.

While the Channel Access protocol in theory allows for payload sizes just short of 4 GB, restrictions imposed by the implementation used in the EPICS Jackie library limit it to slightly less than 2 GB. However, this should not have any practical consequences because the Channel Access protocol is in no way designed to handle large amounts of data efficiently and when values significantly larger than a few megabytes have to be transferred, using a different communication protocol should be considered.

Echo interval

The echo interval is the time between sending two echo requests to a server. Echo requests are sent in order to ensure that the connection is still working. If a beacon packet from the same server is received within this interval, this is considered proof that the network connection is okay and thus no echo request is sent . For this reason, this setting should be chosen to be at least two times as large as the max. interval between sending two beacons. The beacon interval is configured on the server-side (typically by setting the EPICS_CA_BEACON_PERIOD environment variable).

Echo requests are sent for two reasons: First, it allows the client to detect when the network connection has been interrupted and the corresponding channels should be put into the disconnected state. This way, the disconnected state is visible early, even before the operating system closes the TCP socket. Second, sending some data might help the operating system to detect that a TCP connection has been interrupted, thus giving the client a chance to eventually establish a new connection (potentially with a different server now serving the channels).

Typically, this setting is chosen so that is has the same value as the corresponding setting in the channel-name resolver configuration.

If this option is not specified, the value of the EPICS_CA_CONN_TMO environment variable is used. This is the same environment variable that is also used by the C library. If the environment variable is not set, the default value of 30 seconds is used. The minimum interval that can be set is 0.1 seconds.

Default event mask

The default event mask is used when a monitor subscription is created (see Section 2.6, “Using monitor subscriptions”) and no event mask is specified when calling the channel’s monitor… method. If this configuration value is not specified explicitly, a combination of DBE_VALUE and DBE_ALARM (binary or) is used as the default value.

CID block reuse time

The CID block reuse time is specified in milliseconds and defines how long a channel identifier (CID) is blocked from being reused after it has been released.

The CID is a 32-bit integer number that is internally used in the protocol for identifying a certain channel in the communication between client and server. The client assigns a CID when it creates the channel and (aside from other places) uses it in the name-lookup process. The responses received in reply to a name-lookup request only contain the CID, not the channel name. This means that the CID is the only way by which the client can map the response to a channel name.

As name resolution is typically done through the UDP protocol, involving broadcast messages being sent into the network, there is no reliable way of determining when the last response for a certain request has been received. This means that if a CID is reused too early, a response that actually belongs to an earlier request but arrives late (possibly because of a slow server or a high-latency network link) could be wrongly correlated with a more recent request specifying a different channel name.

On the other hand, if the CID block reuse time is chosen too large, this can have the undesirable effect of an increased memory consumption (for maintaining the list of not-yet reusable CIDs) or (in the worst case) even resource exhaustion because of a lack of free CIDs.

If the time is chosen reasonably, this scenario is very unlikely and limited to a scenario in which a client application creates and destroys channels at an unreasonably high rate.

In general, there is no need for an application to modify this value. The default value of 900,000 milliseconds (15 minutes) ensures that late replies do not pose any danger, even in the presence of very slow servers and high-latency links. On the other hand, this interval is so short that a client would need to create and destroy channels at a rate of more than one thousand per second to cause a significant memory footprint.

Error handler

The error handler used by the client is also specified through the ChannelAccessClientConfiguration. However, its details are described in Section 4, “Error handling”.

3.2. Beacon detector configuration

In most environments, the name resolution of channel names relies on UDP broadcast packets being sent to the network. In order to avoid flooding the network with these brodcast packets, a complex algorithm is used that limits the rate of name resolution requests. While the details of this algorithm are rather complex, one of its main effects is that the time interval between two resolution requests for the same channel name is increased with every failed attempt to resolve it. This has the effect that a channel name that can never be resolved (maybe because it has been mistyped) will still not put a significant load on the network. However, there is the undesired side effect that a channel that could not be resolved because its server was offline might take a very long time to be resolved after the server goes online. In order to mitigate this effect, there is a mechanism that (under certain circumstances) can detect when a server that has previously been offline becomes available. This mechanism is called “beacon detection” and works by decreasing the search interval for channels that could not be resolved for a long time when a “beacon anomaly” (an event that might indicate a previously offline server now being online) is detected.

The whole beacon detection and name resolution process is transparent to the user and the client internally handles it. However, there are a few configuration options regarding the beacon detection that can be configured in the BeaconDetectorConfiguration. Typically, the default values of this configuration are fine, an application only has to specify explicit values if it wants to override the default behavior of how the port numbers are determined.

Default server port

The default server port is the port number on which a server sending a beacon message is assumed to accept name-resolution requests. Newer servers typically include this information in a beacon message, but older servers might not. In this case, the port number specified through this option is used. The default implementations of the Channel Access client and name resolver provided by the EPICS Jackie library do not use this information, even if it is supplied by the server. For this reason, there is typically no need for an application to modify this setting.

If not specified explicitly, this setting is initialized with the value of the EPICS_CA_SERVER_PORT environment variable. If this environment variable is not set, the protocol’s default port number (5064) is used.

Repeater port

The beacon detection mechanism relies on the reception of beacon messages sent in UDP broadcast packets. The repeater port is the port on which these broadcast packets are received. The name stems from the fact that there is typically a process called the “repeater” which listens for these packets and forwards them to the various clients running on the same host. Typically, the repeater is started automatically when the first Channel Access client application is started and runs in the background.

The DefaultBeaconDetector from the EPICS Jackie library first tries to connect to an existing repeater so that it can be notied when the repeater receives a beacon message. If it determines that there is no repeater running yet, it tries to start it. If the repeater cannot be started (typically because there is no EPICS Base installation available or it is not in the system path), it starts its own repeater inside the Java Virtual Machine (JVM). The repeater from EPICS Base is preferred over the one bundled with EPICS Jackie because other clients, that are started later, might rely on the existing repeater process and the repeater started inside the JVM is stopped when the JVM is shutdown while the external repeater process is typically kept alive.

If not specified explicitly, the value of the EPICS_CA_REPEATER_PORT environment variable is used as the repeater port. This is the same environment variable that is also used by the C library. If this environment variable is not set, the protocol’s default value (5065) is used.

Error handler

The error handler is used by the beacon detector to log errors that occur during its operation. For details, please refer to Section 4, “Error handling”.

3.3. Channel-name resolver configuration

The channel-name resolver is responsible for resolving channel names. This is the process of finding the server that hosts the channel identified by a certain name. The channel-name resolver is a vital part of the Channel Access client because without it, the client could not know which server(s) it has to connect to or whether a channel even exists at all.

In most environments, UDP is used for name resolution. In very recent protocol versions (since Channel Access version 4.12, introduced with EPICS Base 3.14.12 released in November 2010), name resolution via TCP is supported as well. However, unlike the name resolution via UDP, name resolution via TCP always requires explicit configuration (at least by setting the corresponding environment variable).

The behavior of the channel-name resolver (in particular the port numbers used) can be specified by explicitly constructing a ChannelNameResolverConfiguration and passing it to the ChannelAccessClientConfiguration.

Character set

The configured character set is used when serializing channel names in order to send them over the network inside search requests.

When the ChannelNameResolverConfiguration is created automatically by the ChannelAccessClientConfiguration (because no ChannelNameResolverConfiguration has been specified), it always uses the same character set as the surrounding ChannelAccessClientConfiguration. When creating the ChannelNameResolverConfiguration explicitly, it is possible to specify a different character set then the one used by the ChannelAccessClientConfiguration. However, in most scenarios, using different character sets does not make sense.

If this option is not specified, the UTF-8 character set is used. In the unlikely event that the UTF-8 character set is not available (it should always be available because it is one of the standard character sets supported by the Java Standard Edition), the platform’s default character set is used.

Host name

The host name is sent to each TCP-based name-server when a connection is established. Some name-servers might use the host name in order to decide which channels they should present to the client. The host name sent to the server usually is the unqualified host-name (without a domain part).

If not specified explicitly, the host name is determined automatically. Unfortunately, the Java platform specifies no standardized way for reliably determining the host name. For this reason, a multi-step process is used. First, the library tries to call the hostname command and uses its output. If this command is not available or returns with an error, a platform-specific environment variable (HOSTNAME on UNIX-like systems, COMPUTERNAME on Windows systems) is used. If this environment variable is not set either, the library tries to determine the hostname through DNS (by calling InetAddress.getLocalHost().getHostName()). If this fails as well, the library uses the string “<unknown host>” as the host name.

Typically, this setting is chosen so that is has the same value as the corresponding setting in the client configuration.

User name

The user name is sent to each TCP-based name-server when a connection is established. Some servers might use the user name in order to decide which process variables they should present to the client.

If not specified explicitly, the user name is determined automatically. Unfortunately, the Java platform specifies no standardized way for reliably determining the user name. For this reason, a multi-step process is used. First, the library tries to use platform-dependent classes from the com.sun.security.auth.module package. If these classes are not available (they are not part of the Java Standard Edition but are only bundled with certain Java run-time environments) or their use results in an exception, the user.name system property is used. If this property is not set, the USERNAME environment variable is used. If this environment variable is not set either, the library uses the empty string as the user name.

Typically, this setting is chosen so that is has the same value as the corresponding setting in the client configuration.

Default server port

The default server port is used for two purposes. First, it is used as the target port for search requests when a configured target address does not specify the target port number explicitly. In particular, this port is used when a target address is determined automatically (see the section called “Determine UDP addresses automatically”). Second, the default server port is used when a reply to a search message contains an invalid server port (a port number of 0 or above 65535).

If this option is not specified, the value of the EPICS_CA_SERVER_PORT environment variable is used. This is the same environment variable that is also used by the C library. If the environment variable is not set, the protocol’s default port (5064) is used.

TCP addresses

Recent versions of the Channel Access protocol support the resolution of channel names via TCP. The TCP-addresses option specifies the list of server addresses (combination of IP address and TCP port number) that are contacted for name resolution via TCP.

Please note that name resolution via TCP is only supported in fairly recent versions of the Channel Access protocol (since Channel Access version 4.12).

If this option is not specified, it is initialized from the value of the EPICS_CA_NAME_SERVERS environment variable. This is the same environment variable that is also used by the C library. The value of the environment variable is expected to be a space-separated list of addresses in the form “N.N.N.N:M” or “N.N.N.N”, where “N.N.N.N” is the name server’s IP address and “M” is the optional TCP port number. If the TCP port number is not specified, the default server port is used. If the environment variable is not set, an empty list is used. This has the effect that name resolution via TCP is disabled.

UDP addresses

In most environments, UDP is used for resolving channel names. The UDP-addresses option specified the list of server addresses (combination of IP address and UDP port number) that are contacted for name resolution via UDP. This list may be augmented by a list of automatically determined addresses (see the section called “Determine UDP addresses automatically”).

If this option is not specified, it is initialized from the value of the EPICS_CA_ADDR_LIST environment variable. This is the same environment variable that is also used by the C library. The value of the environment variable is expected to be a space-separated list of addresses in the form “N.N.N.N:M” or “N.N.N.N”, where “N.N.N.N” is the name server’s IP address and “M” is the optional UDP port number. If the UDP port number is not specified, the default server port is used. If the environment variable is not set, an empty list is used. However, automatically determined addresses might still be used. If automatic address detection is also disabled, this effectively disables name resolution via UDP.

Determine UDP addresses automatically

If UDP addresses are set to be determined automatically, the channel-name resolver uses the broadcast addresses of all local interfaces as target addresses for name resolution. When sending messages to these broadcast addresses, the default server port is used. The channel-name resolver periodically checks the list of local interfaces, updating the list of broadcast addresses when new interfaces have been brought up or previously present interfaces have been shutdown.

Typically, there are only two reasons for disabling this option. When name resolution via UDP should be disabled completely in favor of name resolution via TCP, this option has to be disabled in order to avoid using automatically detected addresses. The other reason is when the computer running the client is connected to multiple networks and only servers on some of these networks shall be contacted. In this case, the broacast addresses of the networks that shall be used have to be set manually.

If this option is not specified, it is determined from the EPICS_CA_AUTO_ADDR_LIST environment variable. This is the same environment variable that is also used by the C library. If this environment variable is set to “NO” (not case-sensitive), the automatic address detection is disabled. Otherwise, it is enabled.

Maximum search period

The maximum search period is the maximum amount of time (specified in seconds) that the name-resolver waits before sending a search request for a channel that could not be resolved again. With every failed resolution attempt, the channel-name resolver increases the time between two search requests in order to avoid overloading the network with search requests for channels that might never be found. However, it does not increase this time beyond the limit set by this option.

If this option is not specified, the value of the EPICS_CA_MAX_SEARCH_PERIOD environment variable is used. This is the same environment variable that is also used by the C library. If the environment variable is not set, the default value of 60 seconds is used. This is also the smallest value allowed for this option.

Echo interval

The echo interval is the time between sending two echo requests to a TCP-based name-server. Echo requests are sent in order to ensure that the connection is still working. If a beacon packet from the same server is received within this interval, this is considered proof that the network connection is okay and thus no echo request is sent. For this reason, this setting should be chosen to be at least two times as large as the max. interval between sending two beacons. The beacon interval is configured on the server-side (typically by setting the EPICS_CA_BEACON_PERIOD environment variable).

Typically, this setting is chosen so that is has the same value as the corresponding setting in the client configuration. However, unlike that setting, this setting does not have any effect on the connection state of channels. The only reason why echo requests are also sent for a name-server connection is that actually sending some data might help the operating system to detect that a TCP connection has been interrupted, thus giving the name resolver a chance to establish a new connection.

If this option is not specified, the value of the EPICS_CA_CONN_TMO environment variable is used. This is the same environment variable that is also used by the C library. If the environment variable is not set, the default value of 30 seconds is used. The minimum interval that can be set is 0.1 seconds.

Multicast time-to-live

The multicast time-to-live (TTL) defines the number of hops a multicast packet might take before being delivered to its target. By default, the Channel Access protocol does not use multicast packets, but multicast packets might be sent if the list of UDP name-serves includes multicast addresses. In this case, this configuration option defines the TTL of these packets.

Due to limitations of the Java platform, the multicast TTL cannot be set when running on Java 6. The API needed for setting this option has been introduced with Java 7. When running on Java 6, this option is silently ignored. Even when running on Java 7 or newer, some platforms might not implement this option. In this case, the error handler is notified about the problem, but the name resolver continues operation without this option being set on the UDP socket.

If this option is not specified, the value of the EPICS_CA_MCAST_TTL environment variable is used. This is the same environment variable that is also used by the C library (starting with EPICS Base 3.16.1). If the environment variable is not set, the default value of 1 is used. The minimum value for this option is 1 and the maximum value is 255.

Error handler

The error handler is used by the channel-name resolver to log errors that occur during its operation. For details, please refer to Section 4, “Error handling”.

4. Error handling

The EPICS Jackie Client is designed to handle errors right where they occur. For example, when a read operation fails, the corresponding future throws an exception. If a monitor subscription cannot be created, the corresponding monitor listeners are notified. However, there are certain situations in which an error cannot be associated with a certain context in which the corresponding exception could be thrown. For example, a server might send an error message which is not related to a specific request or a malformed packet might be received on the network port used for name resolution. In these cases, the obvious solution is to write the error to a log and continue operation. However, EPICS Jackie tries to avoid a dependency on a specific logging framework. For this reasons, there is the concept of an error handler, that can be specified when creating the configuration objects ( ChannelAccessClientConfiguration, BeaconDetectorConfiguration, and ChannelNameResolverConfiguration). This error handler is called whenever an exceptional situation that cannot be associated with a specific context is encountered.

Error handlers must implement the ErrorHandler interface. This interface has a single method handleError that is called whenever an otherwise unhandled error is encountered. With each call of this method, the context (in the form of the class in which the error was encountered) is passed. Typically, there also is an exception and sometimes even an error message that is passed to this method, but these two parameters are optional and might be null.

The EPICS Jackie library provides two default implementations of the ErrorHandler interface. JavaUtilLoggingErrorHandler uses the java.util.logging framework for logging the error. If an exception is passed, the error is logged as a warning, otherwise it is logged as an informational message. The NullErrorHandler simply does nothing. This error handler can be used when errors shall not be logged at all.

If no error handler is specified explicitly, the configuration objects are initialized with a JavaUtilLoggingErrorHandler. When the BeaconDetectorConfiguration and the ChannelNameResolverConfiguration are created automatically by the surrounding ChannelAccessClientConfiguration they use the same error handler that is also used by the ChannelAccessClientConfiguration. Otherwise, they use the JavaUtilLoggingErrorHandler by default, even if the ChannelAccessClientConfiguration is set to use a different error handler.

An application will typically supply an error handler that delegates to the logging framework used by the application (for example Apache Commons Logging or SLF4J). If the application uses java.util.logging as its logging framework or java.util.logging is configured to redirect to the application’s logging framework, the JavaUtilLoggingErrorHandler can be used as-is, of course.

5. Listener lock-policy

The listener lock-policy allows a developer to configure how the library deals with locks when removing listeners or destroying channels or monitors. This behavior is configurable because there is a trade-off between having strong consistency guarantees and avoiding deadlocks.

Such a deadlock can occur when a listener acquires a lock that is also acquired by code removing that listener or destroying the object to which the listener is registered (in the case of channels and monitors). This risk of a deadlock stems from the fact that the code calling the listener also acquires an internal lock and holds it while calling the listener. This lock is needed because it is the only way to assure that a listener is not called after the remove or destroy method has returned.

By default, the BLOCK policy is used, which guarantees that a listener is never called after the method removing it or the destroy method has returned. However, this policy has the disadvantage that a deadlock can occur when the listener acquires a lock that is also acquired by code removing the listener or destroying the object to which the listener is registered.

A different behavior is seen when choosing the IGNORE or REPORT policy. In this case, even removing a listener (or destroying the respective object) while holding a lock that is also acquired by the same listener (or any listener registered with the object in case of the destroy method) cannot cause a deadlock. The price one has to pay for this is that it is no longer guaranteed that the listener is not going to be notified after the method removing the listener or the destroy method has returned.

The difference between the IGNORE and the REPORT policy is that the REPORT policy causes the method that removes the listener or the destroy method to throw a ConcurrentNotificationException when it detects that the listener might receive one more notification. In either case, the listener will receive at most one more notification after that method returns, but if the policy is set to REPORT and the method does not throw an exception, it is guaranteed that the listener is not going to be called again – just like when using the BLOCK policy.

Even when using the IGNORE or REPORT policy, having listeners that might potentially block is not without danger. By default, listeners are executed in the I/O thread and thus a listener that blocks will result in the I/O thread being blocked. Please refer to Section 6, “Threading strategy” for details.

6. Threading strategy

The concept of how threads used by the DefaultChannelAccessClient are managed is separated into the ClientThreadingStrategy. The default implementation of this interface, DefaultClientThreadingStrategy, should be suitable for most use cases. However, applications wanting to use a different threading model can easily provide their own implementation of the ClientThreadingStrategy interface.

6.1. DefaultClientThreadingStrategy

The DefaultClientThreadingStrategy is the default implementation used by the DefaultChannelAccessClient if no other threading-strategy is specified. This threading strategy creates one thread that handles beacon detection and name resolution and one additional thread for each connection with a channel server. Channels that are hosted by the same server share a connection, so that the number of server connections is usually rather small.

By default, this threading strategy notifies listeners (like channel connection listeners, future completion listeners, and monitor event listeners) in the same thread that triggers the event. This implementation is very efficient and safe as long as the listeners do not block or need a considerable amount of processing time.

When being constructed explicitly (in contrast to being created implicitly by the DefaultChannelAccessClient), the DefaultClientThreadingStrategy allows for changing this behavior. The user can specify an ExecutorService that is used for calling listener methods. When this option is used, blocking listeners do not pose a threat to the stability of the client, provided that the ExecutorService’s execute method does not block.

Even if such a configuration is used, blocking listeners are still not desirable because they might cause the execution of other listeners to be delayed if a fixed-size thread-pool is used and all threads are occupied by blocking listeners. In addition to that, delegating the execution of listeners to an executor service causes significant overhead for coordination in order to ensure that event notifications are being delivered in the correct order. Last but not least, such a configuration cannot guarantee that a listener will not be called after it has been removed. If events have been queued before the listener is removed, the listener is still going to be called for those events.

For these reasons, the default option of notifying listeners in the calling thread should be preferred. The option of using an executor service should only be considered when implementing all listeners in a way so that they do not block is not feasible.

6.2. Implementing a custom threading strategy

For some applications, the threading model provided by the DefaultClientThreadingStrategy might not be convenient. For example, there might be applications that connect to a large number of servers, but want to keep the number of threads to a minimum. Such an application could easily provide its own implementation of the ClientThreadingStrategy interface that only creates a single thread that takes care of handling all I/O operations, including the beacon detector, the channel-name resolver and all channel-server connections.

Creating such a threading strategy is as simple as creating a SimpleCommunicationController that is repeatedly called from the processing thread. The threading strategy can then simply pass this communication controller to all the CommunicationProcessors used for the beacon detector, the channel-name resolver, and the channel-server connections.

Another threading strategy might want to use a pool of threads for processing the various communication controllers. Such an implementation can also be built based on the SimpleCommunicationController. However, as soon as more than one thread is involved, care must be taken that a single communication processor is not processed by more than one thread because communication processors are not thread-safe.

7. Further customization options

The DefaultChannelAccessClient, which is the default implementation of the ChannelAccessClient interface provided by the EPICS Jackie library, already has plenty of customization options. First of all, many options can be changed by providing a modified configuration as described in Section 3, “Configuration options”. The handling of errors can be customized through the use of error handlers like described in Section 4, “Error handling”. If the default threading model used by EPICS Jackie is not suitable for an application, it can provide its own threading strategy as described in Section 6, “Threading strategy”.

If all these customization options are not sufficient for a specific application, it can customize things even further by providing its own implementation of ChannelAccessClient, ChannelAccessChannel, and ChannelAccessMonitor. All these types are interfaces in order to allow for easy adaption. Such a custom implementation does not have to implement all features from scratch. The implementation of a channel can be derived from AbstractChannelAccessChannel, which already implements most of the methods specified by the interface. The implementation of a monitor can be derived from AbstractChannelAccessMonitor, which implements the whole management of monitor listeners.

The ChannelAccessClient interface is very simple, so there is no abstract base class implementing it. However, the implementation of a client does not have to be written from scratch either: Beacon detection can be delegated to DefaultBeaconDetector, the default implementation of the BeaconDetector interface. Resolving channel names can be handled by DefaultChannelNameResolver, the default implementation of the ChannelNameResolver interface. The ChannelAccessServerConnection class can be used as a base for implementing the communication with Channel Access servers. This base class takes care of establishing the connection (including version negotiation) and encoding and decoding messages.

Finally, the components that are part of the EPICS Jackie Common module provide building blocks that can be used when creating a client implementation and might even be useful for applications beyond the scope of a client. Please refer to Chapter IV, EPICS Jackie Common Components to learn more about the components provided by that module.

Chapter IV. EPICS Jackie Common Components

The EPICS Jackie Common Components module assembles various components that are useful when building software dealing with the Channel Access protocol. These components are not limited to a certain application (for example a client), but can be used to build any kind of software for handling Channel Access, including applications like servers, gateways, or protocol analyzers.

These components can be coarsely separated into four categories: Components dealing with the Channel Access protocol (described in Section 1, “Channel Access message codec”), components dealing with Channel Access values (described in Section 2, “Channel Access value objects”), components for handling asynchronous network I/O (described in Section 3, “I/O support”), and general utilities.

This document does not give a complete description of all the components provided by the library, but is rather intended as an overview and starting point. For a complete description of all components, including detailed API specifications, please refer to the API reference documentation.

1. Channel Access message codec

The primary class for dealing with the Channel Access protocol on a per-message basis is the ChannelAccessMessageCodec. This class provides methods for serializing and deserializing the messages supported by the Channel Access protocol. In its deserialized form, each message is represented by an instance of one of the classes derived from ChannelAccessMessage. For the serialized form, the ChannelAccessMessageCodec uses instances of ByteSink (for serialization) and ByteSource (for deserialization) from the I/O support components.

The ChannelAccessMessageCodec provides serialization and deserialization support for all message types and all parties involved in communication (client, server, repeater, TCP, UDP, etc.). Therefore, it should be sufficient for handling the Channel Access protocol in all situations. Internally, the ChannelAccessMessageCodec makes use of the ChannelAccessValueCodec (see Section 2, “Channel Access value objects”) for serializing and deserializing Channel Access values.

2. Channel Access value objects

The Channel Access protocol has a type system that includes 39 different data-types. Of these data types, thirty can only be used in read operations, two can only be used in write operations, and seven can be used in both read and write operations.

The EPICS Jackie library uses interfaces for representing the various data-types. However, application code should never create implementations of these interfaces. Only the implementations provided by the library (and exposed through ChannelAccessValueCodec and ChannelAccessValueFactory) must be used. The only reason for using interfaces instead of classes is the complex type-hierarchy of the Channel Access type system that cannot be represented adequately without using multiple inheritance.

Figure IV.1. Overview of the ChannelAccessValue class hierarchy
Overview of the ChannelAccessValue class hierarchy

Figure IV.1, “Overview of the ChannelAccessValue class hierarchy” shows the basic outline of the type hierarchy used for the Channel Access value objects. The “ChannelAccess” prefix has been stripped from all class names for brevity. All types inherit from ChannelAccessValue, the types that can be used in read operations inherit from ChannelAccessGettableValue, and the types that can be used in write operations inherit from ChannelAccessPuttableValue. Even though it cannot be seen from this figure, the ChannelAccessClassName, ChannelAccessAcknowledgeAlarm, and ChannelAccessConfigureAcknowledgeTransientAlarms types are special because there are no sub-interfaces derived from them. They directly represent values of the types DBR_CLASS_NAME, DBR_PUT_ACKS, and DBR_PUT_ACKT.

The seven types in the left column of the figure (char, double, enum, float, long, short, string) actually are base-interfaces that are shared by all types having the respective element type. For example, all types that contain elements of type double inherit from ChannelAccessDouble. The four types in the center column and the three types in the right column are the base interfaces shared by all types that have certain properties in common (but may have different element types). The types in the right column differ from the types in the center column in the fact that the types in the right column only have their specific properties, while the ones in the center column might have additional properties that are just not visible in the base interface. For example, an instance of ChannelAccessAlarmValue might actually also implement ChannelAccessGraphicsValue, while a ChannelAccessAlarmOnlyValue is guaranteed to be neither a ChannelAccessTimeValue, nor a ChannelAccessGraphicsValue, nor a ChannelAccessControlsValue.

Consequently, there are interfaces that mix the interfaces from the left column with the interfaces from the center and right columns.

Figure IV.2. Class hierarchy of the ChannelAccessDouble type
Class hierarchy of the ChannelAccessDouble type

Figure IV.2, “Class hierarchy of the ChannelAccessDouble type” shows this sub-hierarchy, using the ChannelAccessDouble type as an example. Again, the “ChannelAccess” prefix has been stripped from all class names for brevity. There are seven interfaces that inherit from ChannelAccessDouble. At the same time, each of these interfaces inherits from one of the base-types that do not depend on the element type.

For numeric element types (char, double, float, long, short), the complete picture is even a bit more complex:

Figure IV.3. Class hierarchy of numeric ChannelAccessValue types
Class hierarchy of numeric ChannelAccessValue types

As shown in Figure IV.3, “Class hierarchy of numeric ChannelAccessValue types”, there are specialized versions of the ChannelAccessGraphicsValue and ChannelAccessControlsValue for numeric element types and even more specialized versions for floating-point element types. The reason for this is that the graphics and controls types for numeric types share a certain common structure of their properties.

For each of the 39 types known by the Channel Access protocol, there is one interface corresponding to this type as listed in Table IV.1, “Channel Access value types and corresponding interfaces”.

Table IV.1. Channel Access value types and corresponding interfaces
Channel Access value typeJava interface
DBR_STRING ChannelAccessSimpleOnlyString
DBR_SHORT ChannelAccessSimpleOnlyShort
DBR_FLOAT ChannelAccessSimpleOnlyFloat
DBR_ENUM ChannelAccessSimpleOnlyEnum
DBR_CHAR ChannelAccessSimpleOnlyChar
DBR_LONG ChannelAccessSimpleOnlyLong
DBR_DOUBLE ChannelAccessSimpleOnlyDouble
DBR_STS_STRING ChannelAccessAlarmOnlyString
DBR_STS_SHORT ChannelAccessAlarmOnlyShort
DBR_STS_FLOAT ChannelAccessAlarmOnlyFloat
DBR_STS_ENUM ChannelAccessAlarmOnlyEnum
DBR_STS_CHAR ChannelAccessAlarmOnlyChar
DBR_STS_LONG ChannelAccessAlarmOnlyLong
DBR_STS_DOUBLE ChannelAccessAlarmOnlyDouble
DBR_TIME_STRING ChannelAccessTimeString
DBR_TIME_SHORT ChannelAccessTimeShort
DBR_TIME_FLOAT ChannelAccessTimeFloat
DBR_TIME_ENUM ChannelAccessTimeEnum
DBR_TIME_CHAR ChannelAccessTimeChar
DBR_TIME_LONG ChannelAccessTimeLong
DBR_TIME_DOUBLE ChannelAccessTimeDouble
DBR_GR_STRING ChannelAccessAlarmOnlyString
DBR_GR_SHORT ChannelAccessGraphicsOnlyShort
DBR_GR_FLOAT ChannelAccessGraphicsOnlyFloat
DBR_GR_ENUM ChannelAccessGraphicsEnum
DBR_GR_CHAR ChannelAccessGraphicsOnlyChar
DBR_GR_LONG ChannelAccessGraphicsOnlyLong
DBR_GR_DOUBLE ChannelAccessGraphicsOnlyDouble
DBR_CTRL_STRING ChannelAccessAlarmOnlyString
DBR_CTRL_SHORT ChannelAccessControlsShort
DBR_CTRL_FLOAT ChannelAccessControlsFloat
DBR_CTRL_ENUM ChannelAccessGraphicsEnum
DBR_CTRL_CHAR ChannelAccessControlsChar
DBR_CTRL_LONG ChannelAccessControlsLong
DBR_CTRL_DOUBLE ChannelAccessControlsDouble
DBR_PUT_ACKT ChannelAccessConfigureAcknowledgeTransientAlarms
DBR_PUT_ACKS ChannelAccessAcknowledgeAlarm
DBR_STSACK_STRING ChannelAccessAlarmAcknowledgementStatus
DBR_CLASS_NAME ChannelAccessClassName

The ChannelAccessAlarmOnlyString and ChannelAccessGraphicsEnum interfaces are special because they are used for more than one Channel Access value type. The reason is that the DBR_STS_STRING, DBR_GR_STRING, and DBR_CTRL_STRING types are completely identical, as are the DBR_GR_ENUM and DBR_CTRL_ENUM types.

For reasons explained earlier, the whole type hierarchy of the Channel Access value types relies on interfaces instead of classes. Unfortunately, the implementations of interfaces cannot be limited to a certain package. Unlike a class, an interface does not have a constructor that could be made package-private. Still, it is very important that an application does not create its own implementations of the interfaces. Many parts of the code involving Channel Access values, in particular code serializing the values, rely on the fact that the values implement the correct interfaces (and not more interfaces). This is particularly important for marker interfaces like ChannelAccessGettableValue and ChannelAccessPuttableValue.

The ChannelAccessValueFactory is the class through which objects implementing the interfaces of the type hierarchy can be created safely. The objects created this way are accepted by ChannelAccessValueCodec and can be serialized correctly.

The ChannelAccessValueCodec is the class responsible for serialization and deserialization of Channel Access values to ByteSinks and from ByteSources. The objects created by the deserialization methods of this class implement the type-hierarchy correctly (actually, they are the same implementations that are also used by ChannelAccessValueFactory) and can also be serialized again.

3. I/O support

The EPICS Jackie library includes a small but powerful framework for creating asynchronous I/O services. This framework is the internal basis for all the network-communication code of the EPICS Jackie Client.

The components provided by this I/O framework can roughly be split into two categories: There are utilities that allow dealing with a (theoratically endless) stream of bytes (described in Section 3.1, “Byte streams”) and there are utilities dealing wih the handling of asynchronous I/O channels (described in Section 3.2, “Asynchronous I/O”).

3.1. Byte streams

There are two types of byte streams: A ByteSource acts as an input stream, providing data that has been read from some source (typically a network socket). Correspondingly, a ByteSink acts as an output stream, accepting data and writing it to some kind of sink (typically, this also is a network socket).

The interface of ByteSource and ByteSink resembles the interface of Java’s ByteBuffer class. The most notable difference is that there are no read and write methods taking an offset. The read methods always consume the bytes right at the beginning of the data remaining in the the source. In the same way, the write methods always append the bytes right at the end of the data already written to the sink.

Sometimes, a read or write operation only makes sense if a whole data-set can be read or written. The ByteSource and ByteSink provide atomic read and write operations for this purpose: When an atomic read operation fails, the source is rolled back to the state it was in before the atomic read operation started, so that the data consumed by the read operation is still available for the next read operation. In the same way, when an atomic write operation fails, the sink is rolled back to the state it was in before the atomic write operation started, so that none of the data written by the write operation is actually written. Internally, this works by using buffers that store the data.

This feature is very useful when processing a protocol that relies on some form of packets or messages. In this case, the code can fail if a message cannot be read or written completely, relieving it from the burden to keep track of which data has already been read or written and having to continue at the right point.

The ByteSource has a remaining method that returns the number of bytes that can be read from the byte source without a BufferUnderflowException being thrown. However, the value returned by this method only is a lower estimate, meaning that there might actually be more data available. Typical implementations will return the number of bytes available in the internal buffer and throw a BufferUnderflowException when reading beyond the limit of this buffer, but some implementations might choose copying additional bytes into the buffer when its end is reached, in which case the estimate might be too small.

The ByteBufferByteSource and ByteBufferByteSink are implementations that internally use a list of ByteBuffers for buffering data. These implemantations are very convenient when used with asynchronous I/O because they allow for an easy integration with Java’s Channel API that also works with ByteBuffers. The ByteBufferByteSource implements the remaining method so that the number of bytes remaining in the buffers is returned (unless this number exceeds Integer.MAX_VALUE in which case that number is returned instead). This means that an application reading from the byte source can check whether the data remaining in the byte source is sufficient for finishing an atomic read operation and can fail early with a BufferUnderflowException if the remaining bytes are not sufficient.

The AbstractSocketChannelConnection class described in Section 3.2, “Asynchronous I/O” provides a convenient way of connecting a SocketChannel with the ByteSource and ByteSink APIs.

3.2. Asynchronous I/O

In Java, asynchronous I/O is typically performed through the facilities of the Java NIO framework. This means that a Selector has to be created and Channels have to be registered with this selector in order to be notified when the operating system is ready for an I/O operation.

Managing the selector and its associated channels can be tedious because it means that the component responsible for a certain channel has to be identified and notified. In addition to that, components often want to perform certain operations in certain time intervals, independent of the I/O state of their channel. In this case, the select operation on the selector has to be limited in time so that the timed operation can be run at the right point in time.

The asynchronous I/O framework of EPICS Jackie provides a component that takes care of all these details. This component is called the CommunicationController. The communication controller allows for both kind of notifications: being notified, when a channel is ready for an I/O operation and being notified when a timer has expired.

The registerChannel method is used to register a ChannelProcessor that is notified when the corresponding Channel is ready for I/O. The return value of this method is a SelectionKey that can be used by the channel processor in order to configure when it wants to be notified. Most processors will want to always be notified when the channel is ready for a read operation (because new data has arrived), but will only be interested to be notified when the channel is ready for a write operation when they actually have data in their write buffer.

The registerTimer method is used to register a TimerProcessor that is notified after a certain time interval has passed.

It is assumed that a communication controller only uses a single thread so that channel and timer processors can be designed without having to worry about thread safety. As one component might register more than one channel or timer processor, a communication controller has to guarantee that this holds true, even for different processors that are registered with the same controller.

The SimpleCommunicationController is an implementation that deals with all the tasks of a communication controller, doing its work in a single method, processTimersAndIO, that has to be called repeatedly by an application thread.

The CommunicationProcessor interface is a simple interface that can be implemented by a component in order to get the CommunicationController injected. This way, a component using a communication controller can be designed without having any dependencies on how the communication controller (and its associated thread) is created or which implementation is used.

The AbstractSocketChannelConnection is a good starting point for writing a component that uses a SocketChannel for asynchronous I/O. Its constructor takes a socket channel and a communication controller and takes care of registering the channel with the communication controller. Internally, it uses instances of ByteBufferByteSource and ByteBufferByteSink that are connected with the channel so that data that arrives on the channel is made available in the byte source and data that is written to the byte sink is written to the channel.

The application code typically only has to implement (some of) the five event handler methods (onConnect, onDestroy, onReceive, onReceiveFinished, onSend) and can simply use the provided byte source and byte sink to read and write data from within these methods.