Best practices and pitfalls with Tag Event Scrips

In summary, my question is:
What is best practices, and what to avoid when writing Tag Event Scripts?

Background and more details regarding question:

From what I have read, the Tag Event Scripts are running in fixed size thread pool (default 3 threads). This would mean that if you have multiple Tag Event Scripts that does slow/blocking operations, it could block any other tag event scripts from running.

This would presumably mean that running e.g. network calls like OPC calls, or other potentially time consuming calls from these scripts risky.

But what about performing reads and writes to the tag system?
I would assume that you should in that case prefer async version, to reduce risk of potential lockup. I note that in the script examples in Tag Event Scripts | Ignition User Manual is actually using blocking version. Furthermore Kevin writes that you should be careful with async invocations in
Tag Event Scripts, ref. Tag Event Scripting Performance - #5 by Kevin.Herron . He was talking about system.util.invokeAsynchronous() but presumably we could extrapolate the same to apply to the async version of tag read/write.

The reason my attention was drawn to this, was due to noticing some issues where the gateway could get locked up when tag was restarting (got some blank entries stuck in "running scripts", there the elapsed time just kept counting and counting). Digging into this, I found some code that had been written to dynamically enable/disable parts of a tag based on value from PLC. This is also using the depreciated system.tag.write and read, so that may also play into this.
But before I start rewriting these, and other scripts, I would like to try and collect some information on what is best practice when it comes to Tag Event Scripts.

For reference, one of the scripts in question, running on a derived tag with an integer describing number of packs:

def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
	val = currentValue.getValue()
	
	for i in range(1, 33):
		packPath = str(tagPath).replace("Battery/inNumberOfPacks","Pack{:02d}".format(i))
		packNumber = system.tag.read(packPath + "/Parameters.PackNumber").value  
		if val >= packNumber: 
			system.tag.write(packPath + ".Enabled", 1)
		else:
			system.tag.write(packPath + ".Enabled", 0)	

I use system.util.invokeAsynchronous() extensively in tag change scripts, as we were running into performance issues. The pitfalls Kevin mentioned about the guaranteed order of events does not apply to my use case.

Your example may be massive improved if refactored with the new reading functions to only read (blocking) and write (async) once.

Pseudo-code:

val = currentValue.value
readlist = []
for i in range(1, 33):
                packPath = str(tagPath).replace("Battery/inNumberOfPacks","Pack{:02d}".format(i))  + "/Parameters.PackNumber"
		readlist.append(packPath)

values = system.tag.readBlocking(readlist)

writelist = []
writevalues = []
for i in range(1, 33):
      packNumber = values[i].value
      writelist.append(packPath + ".Enabled") #build packpath again
      writevalues.append(1 if val>=packNumberelse 0)
system.tag.writeAsync(writelist,writevalues)

This way you iterate quickly to build the list of tags to read, read blocking once, iterate through the tags read to build the results, and finally writing the results once. This assumes you don't have to wait after the write.

1 Like

3 threads to execute tag events, and a queue of up to 5 events per tag. If the 3 events being processed are long running, and 6 new events come in for a single tag, the last one will be dropped.

1 Like

system.tag.write() is Asynchronus, system.tag.read() is blocking. The example script is blocking 32 times. Not good (particularly in a Tag Value Change Script).

The example script would be more efficient if written like this:

def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
	val = currentValue.getValue()
	tagPaths = [str(tagPath).replace("Battery/inNumberOFPacks","Pack{:02d}".format(i)) for i in xrange(1,33)]
	readPaths = [path + "/Parameters.PackNumber" for path in tagPaths]
	tagValues = [qv.value for qv in system.tag.readBlocking(readPaths)]
	writeValues = [currentValue.value >= packNum for packNum in tagValues]
	system.tag.writeAsync([path + ".Enabled" for path in tagPaths], writeValues)

But IMO, should still not be in a Tag Value Changed Event Script.

Best practices are as you've already said:

No long running operations. Scripts should execute in single digit ms. This means anything that touchs a DB that isn't a Store and Foreward operation or anything that requires network traffic (such as gateway messages, OPC operations, etc..) should not be executed in these scripts.

They should be very short, very quick scripts.

The higher your tag count gets, the more important this will become.

I avoid Tag Value Change Scripts at all costs. If I need a tag change event, I use a Gateway Tag Change Event. I have yet to find a need for a script that wasn't driven by a project.

1 Like

Blocking tag reads are safe in tag event scripts, since they never go outside the gateway to get a value. Anything that is subscribed, they just get the most recent value.

Blocking tag writes are safe in tag event scripts only for memory tags. Any other kind of tag will delay for confirmation of the write.

2 Likes