Working to understand and find the Concurrent Modification Exception

I have seen a few references to this exception before but I believe in the context of the base product. I am working on improving a module we developed a while back to clean up exceptions, improve comments, and remove code that was derived from playing and understanding the SDK. In that effort, I am really struggling with a ConcurrentModificationException that keeps cropping up in my tagChanged hook.

Here is the full exception that I am getting. The log has highlighted the “at com.inductiveautomation.ignition.gateway.tags.subscriptions.ProviderSubscriptionManagerImpl$TagChangePublish.run(ProviderSubscriptionManagerImpl.java:1138)” line.

java.util.ConcurrentModificationException: null

at java.base/java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.base/java.util.ArrayList$Itr.next(Unknown Source)
at com.jnjdodev.ignition.modules.websocket.UtilSingleton$1.tagChanged(UtilSingleton.java:78)
at com.inductiveautomation.ignition.gateway.tags.subscriptions.ProviderSubscriptionManagerImpl$TagChangePublish.run(ProviderSubscriptionManagerImpl.java:1138)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)

Below is the code for the tagChanged hook we built. I’m very sure that more information might be needed to help find the issue, but I’m not sure what others might find relevant so I thought we would start at the stacktrace line indicated in the error

public class UtilSingleton {

	private static UtilSingleton single_instance = null;
	private GatewayContext context;
	private ArrayList<WebSocketHandler> socketHandle;
	private Logger logger;
	private Server server;
	private List<TagPath> tagSubscriptions;
	private ArrayList<TreeNode> tagSubscriptionsTree;
	private com.inductiveautomation.ignition.common.tags.model.event.TagChangeListener tcl;
	private WebSocketSettingsRecord settings = null;
	private Thread th = null;
	
	private UtilSingleton() {
		socketHandle = new ArrayList<WebSocketHandler>();
		setTagSubscriptions(new ArrayList<TagPath>());
		tagSubscriptionsTree = new ArrayList<TreeNode>();

		// A reference of the TagChangeListener thread
		tcl = new com.inductiveautomation.ignition.common.tags.model.event.TagChangeListener() {
			@Override
			public void tagChanged(com.inductiveautomation.ignition.common.tags.model.event.TagChangeEvent e) throws InvalidListenerException {
				LogInfo("Tag Changed Fired for Tag: " + e.getTagPath() + " Value:"+ e.getValue().toString(),false);
				try {
					Gson g = new Gson();
					// Gets the value of this.socketHandle from the parent class
					if (getSocketHandle() != null) {
						// Creates an object structure that will be converted to JSON for my apps to consume
						ReportingTag rt = new ReportingTag(e.getValue(), e.getTagPath());						
						// Loop our socket clients to send them messages
						for (WebSocketHandler webSocketHandler : getSocketHandle()) {							
							// Evaluate if a client has a specific subscription to a tagpath (specific PLC) or wants all updates
							if (webSocketHandler.subscriptionPath != "") {
								if (e.getTagPath().toString().toUpperCase().startsWith(webSocketHandler.subscriptionPath.toUpperCase())) {
									LogInfo("Sending Explicit Subscription Tag Change to " + webSocketHandler.hostname + " (" + webSocketHandler.ipAddy + ")", false);
									webSocketHandler.SendMessage(g.toJson(rt));
								}
							// Send all tag updates
							} else {
								LogInfo("Sending Tag Change to " + webSocketHandler.hostname + " (" + webSocketHandler.ipAddy + ")", false);
								webSocketHandler.SendMessage(g.toJson(rt));
							}
						}
					}
				} catch (NullPointerException ex) {
					// STUB Still trying to understand why and when this exception fires
					LogError("Null Pointer in tag change event", ex);
				} catch (Exception ex) {
					LogError("Error sending tag change event", ex);
				}
			}
		};
	}

I am suspecting that my loop is taking long enough that the same tag change is firing in that time and that’s what is causing the exception.

My plan, even if it doesn’t fix this exception, is to move this looping and message sending logic to a blocking collection in a different thread so that the tag change from ignition can fire on through and I can send messages, in order, on the side.

Curious on the thoughts from others what I should maybe look for or do as I transition this.

I can’t match up your line numbers, but I suspect the problem is very simple: you are modifying the list returned by getSocketHandle() in one thread while another thread is iterating over it.

You either need to add some kind of synchronization to prevent this or use a concurrent/thread-safe collection like CopyOnWriteArrayList.

Good point. Even if it doesn't correct the exception that could happen where a new client connects and is added to the array while the array is iterated. I've done a standard for loop that loops down from the current size of the array. It seemed the simple solution that should work since it's not a true iterate of that array.

FINALLY I FOUND THE NULL POINTER ISSUE! Throw enough (a gross amount) of try catches at the wall something will stick I guess.

Turns out when I see these it’s because the tagChanged events qualified value’s value object returns a null value event.[QualifiedValue]getValue().[Object]getValue()

Wowza that was obnoxious ha ha ha!

The concurrent errors appear to have gone away. Not sure if that’s because of the change in the for loop or that we threaded the message sending in a blocking queue outside the tagChanged event. Regardless both needed to be done.

Lesson here, if anyone hooks tagChanged to use the actual value value of the qualified value, make sure it’s not null before you try to use it :smiley:

1 Like

So… I tried fixing it up stream by not allowing it to resolve to null. But this query tag is still sending null through tagChanged. Anyone with insight as to why that might be happening.

That null exception is being caught here in the hook

I don’t know why you’re getting a null value there, but you should certainly be prepared to receive null values without blowing up.

It might be interesting to see what TagChangeEvent::isInitial returns when the value is null.

I plan to leave the exception but switch it to a warning so I can keep an eye on them, but yeah at least it no go boom!

Sorry can you explain what you mean a bit more on this comment? I don't appear to be able to access isInitial from where I'm at in this hook.