How to trigger a gateway timer to trigger immediately but no more than four times per minute?

I have a gateway timer script that is configured to run every 15 seconds.

ds = system.tag.read("[default]Queue").value

if ds.getRowCount() > 0:
	# Get value from queue
	message = ds.getValueAt(0, "message")

	# Complete some arbitrary stuff
	specialFunctions.myFunction(message)

	# Delete value from dataset
	new_ds = system.dataset.deleteRow(ds, 0)

	# Write new dataset back to queue
	system.tag.write("[default]Queue", new_ds)

I have up to 100 tags feeding a queue. The purpose of the gateway timer is to just read values from that queue, take some arbitrary actions, and then delete the value from the queue. This code works ok but if you are the only value in the queue then you might have to wait 14 seconds before the your turn to execute. Is there a good way to execute immediately if you are the first value in the queue?

The tag system is fully multithreaded and only locks what it needs, not what some queuing algorithm needs. You will suffer trying to do this with a memory dataset tag.

Use a proper java concurrent queue. Set it up so that only a single copy is ever constructed (using system.util.getGlobals() and .setdefault()). Run a timer script that polls this queue, with a timeout the same as the fastest pace you want to run.

Only place python or java native data types in such a queue. (I usually use python tuples.)

Well it seems i might be doomed to suffer... can you provide an example or documentation to look at that does a "proper java concurrent queue"? And how would I accomplish this in Ignition?

Would functions readBlocking and writeBlocking help the situation?

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/LinkedBlockingDeque.html

It's essentially a list, but with lots of safety features.

Ok, so I created one in the script console.

from collections import deque

# Create an empty deque
deque_obj = deque()

# Add elements to the deque
deque_obj.append("Element 1")  # Adds element at the end of the deque
deque_obj.appendleft("Element 2")  # Adds element at the front of the deque

# Access elements in the deque
first_element = deque_obj[0]  # Access the first element
last_element = deque_obj[-1]  # Access the last element

# Remove elements from the deque
removed_element = deque_obj.pop()  # Removes and returns the last element
removed_element = deque_obj.popleft()  # Removes and returns the first element

# Check the size of the deque
size = len(deque_obj)

# Check if the deque is empty
is_empty = len(deque_obj) == 0

And I assume it lives in the global namespace (on the gateway???). But then how do I write to it and how do I read it from different locations? For example, I might want to read/write to it from my alarm pipeline and then read/write to it from a gateway timer.

  1. Don't use the Python standard library deque, use the Java standard library one. One concern here is leaking Jython memory as Ignition shuts down and starts up script lifecycles around you.
  2. Creating an instance doesn't do anything, yet. It's just an instance in memory. Like I said, it's just a list with some guardrails. The 'superpower' here is when you create one on the gateway, pairing it with system.util.getGlobals(). That function creates a persistent dictionary that will persist for the entire lifetime of the current local Java process (the designer, or the gateway, or the Vision client). So on the gateway, you create a LinkedBlockingDeque, put it into system.util.getGlobals() at some well-known key, and then anywhere else on the gateway, you have access to a safe concurrent datastructure that you can push values into or pull values from. Phil recommends setdefault for this, because it means you avoid having to think about when the object is initialized.

Something like this:

# in a script library called queue
from java.util.concurrent import LinkedBlockingDeque

def getQueue():
	return system.util.globals.setdefault(LinkedBlockingDeque())

So anywhere on the Gateway, you could then use queue.getQueue() to retrieve _the same instance of a LinkedBlockingDeque() - so you can safely put events into it from any of your 100 tags, and safely retrieve items from it from your timer script.

I usually use this construct at the top level of a script module:

from java.util.concurrent import LinkedBlockingDeque, TimeUnit

# Capacity = 100.  The new instance is thrown away if already in globals.
deque = system.util.getGlobals().setdefault('MySpecialForeverQueue', LinkedBlockingDeque(100))

def someTimerEvent(event):
	next = deque.poll(15000, TimeUnit.MILLISECONDS)
	if next:
		# handle it
# Run the above at a fixed **rate** matching the poll timeout.

Use ConcurrentLinkedDeque if you don't want any capacity. (It'll run a bit faster that a LinkedBlockingDeque that doesn't have a limit, IIRC.)

Queues are a bit faster than Deques, if you don't need bidirectional traffic.

6 Likes

Thanks @pturmel

I was going to throw something like this into a gateway startup event script but I'll implent your timer also into the gateway timer script.

from java.util.concurrent import LinkedBlockingDeque

def getQueue():
    return system.util.globals.setdefault("my_deque_key", LinkedBlockingDeque())

# Check if the deque is already set in the globals dictionary
if "my_deque_key" not in system.util.globals:
    # Create a new deque and set it in the globals dictionary
    system.util.globals["my_deque_key"] = LinkedBlockingDeque()

# Get the deque from the global dictionary
deque = getQueue()

# Add a value to the deque
value = "Example Value"
deque.add(value)

print(deque)

You absolutely may not put this directly into an event script. It must be in a project library script module.

(Paul's is buggy. Use mine. Exactly as shown. Do not use any form of check for existence of the key in globals other than the .setdefault() operation shown.)

You do not need to use a startup event. The timer event will cause this to be created properly on first use.

Sources of queue elements that are outside the script can use path.to.script.deque.offer() to put stuff into the queue safely.

1 Like

If you ever start offering classes on coding I'll be the first to sign up. I will test this out shortly. Thank you.

@pturmel

What do I pass in for the event?

I've been able to add values into the queue but not get them out.

Ah, yes. Timer events don't have an event object. I'm not using it, so just leave it out. Empty parentheses on both ends.

The script console is running in the designer, while your gateway event is running in the gateway. Totally different processes that do not share variables, even on the same machine.

(Don't test gateway scope from the designer script console, unless you are simply using system.util.sendRequest() to trigger a gateway message handler.)

1 Like

I have a gateway script timer executing every 9,000 milliseconds the following:

message = queue.subDeque1()

if message:
	# Send message to radio 
	specialFunctions.text_to_speech(message)

Which is intended to send a message over radio 1. However, when I trigger multiple events there are times where I have to wait up to 24 seconds for the next event.

# Capacity = 100.  The new instance is thrown away if already in globals.
Deque1 = system.util.getGlobals().setdefault('RadioChannel1', LinkedBlockingDeque(100))

##################################################
# Deque 1 functions
# Return value from RadioChannel1
# Call this function at a fixed **rate** matching the poll timeout.
def subDeque1():
	next = Deque1.poll(9000, TimeUnit.MILLISECONDS)
	if next:
		# Write to memory tag.
		queue.updateDeque1()
		return next


# Add value to RadioChannel1 Queue
def addDeque1(channel, message):
	Deque1.add(message)
	
	# Write to memory tag.
	queue.updateDeque1()
	

# Write RadioChannel1 to memory tag RadioChannel1.
def updateDeque1():
	headers = ["message"]
	data = []
	for val in system.util.globals["RadioChannel1"]:
		data.append([val])
		
	ds = system.dataset.toDataSet(headers, data)
	system.tag.write('[AndonCallbox]RadioChannel1', ds)
##################################################

Any idea what the culprit might be?

How are you/what is calling addDeque1 ? And why are you using .add() instead of .offer() as I recommended?

(Get rid of your dataset tag. Iterating over your Deque to populated it might clash with poll().)

An alarm pipeline script block is calling addDeque1.

And why are you using .add() instead of .offer() as I recommended?

I forgot :sweat_smile:

Be sure to check the return value of offer() and provide an alternate path when the deque is full.

1 Like

While it doesn’t actually matter in this case, because scoping, I wouldn’t use next as an identifier: it’s a Python builtin.
It shouldn’t interfere with anything here, but it just feels weird reading next = something.

2 Likes