Global objects in Perspective Scope

Hi everyone, I am trying to figure out how global objects in Perspective Scope work.
I have a view with a numeric entry field that will receive the input of the user. I also have a class TestGlobalScope which will take the value of the numeric entry field when its value has changed.

class TestGlobalScope:
      def setSetPoint(self, s):
        self.setpoint = s

On event configuration of the numeric field, I have configured it to initiate an instance of TestGlobalScope in the onStartup event. I also updated the instance object’s value when the input value had changed in onActionPerformed. But when I tried to execute the script, it raised the error ‘global name ‘g’ is not defined’.

From the document [Perspective Event Types Reference] (Perspective Event Types Reference - Ignition User Manual 8.0 - Ignition Documentation), it says that onStartUp and onShutDown events in System Events are similar to the componentDidMount and componetWillUnmount methods in React. I suppose that creating a global object via Python syntax in component events is actually creating a distinctive object in JavaScript. Because the events are separate, they don’t share info with each other.
I also know a way to create a global object in Perspective via perspective. session.custom. However, we don’t want to assign the object in our Python scripts to it directly in some cases.
I have tried another way to create a global TestGlobalScope object in python script. Then I tried to call it inside the events above. And it worked. I don’t really find the relation between the python code in the script project with the one in the component’s event.

image

Thank you for your time!

Putting global data anywhere but tags, session props, or system.util.globals() / system.util.getGlobals() is either asking for trouble or simply won’t work.

(worth noting session props aren’t true global like the other 2 options if that’s not obvious)

1 Like

Is it generally safe to put custom class instances inside getGlobals(), or do they need to be serializable?

1 Like

Looks okay to me, they aren’t ever persisted as far as I can tell, and they aren’t part of redundancy in any way.

No. Unless the project script that defines the class includes code to replace any persistent instances whenever the script restarts. Jython class instances reference the code that defines them, which will be old code after the defining script restarts. That old code lives in an old interpreter, which leaks huge chunks of memory. Be absolutely sure you replace custom class instances in any persistent use. That isn't limited to just .getGlobals() or my system .util.persistent() function, but would include anywhere you've added a listener into Ignition's infrastructure.

2 Likes

Oh, and I don’t think you can serialize jython class objects.

Ooph, Phil I think you are correct. Don’t listen to me :slight_smile:

1 Like

Interesting,

So then that seems to get rid of any opportunities for singletons right? Or would it be possible to trigger off the script restart and replace the singleton with a new instance from the potentially updated code?

EDIT: Would checking the type like this work in a way that verifies its accurate to the current version prior to use? Obviously some work would be needed to make sure properties of the new one can be re-built from properties of the old one

def getMyClass():
	if 'myClassInstance' not in system.util.globals:
		print("Adding new instance")
		system.util.globals['myClassInstance'] = Test.myClass()
	elif not isinstance(system.util.globals['myClassInstance'], Test.myClass):
		print("Replacing existing instance")
		system.util.globals['myClassInstance'] = Test.myClass(system.util.globals['myClassInstance'])
		
	return system.util.globals['myClassInstance']
1 Like

You have to end up with no references to the old instance. Anywhere. A singleton sounds like a good solution. The key is to not accidentally hang on to references elsewhere. The weakref module can help in some cases.

Does the edit I added above make sense to cover this issue?

In a project I would likely do this by overriding __new__ but I made this a function here for easy example

Glad you’re helping clarify this, as I was about to need to add this in a project!

No idea. Test. Or better, just replace it unconditionally in the top level of the defining script. If that runs, you know it will be new code.

I did test it before I asked, was mostly asking from a memory leak standpoint. It works from a code standpoint.

I am not exactly sure how Jython handles memory in this case, or if I am missing some other horrible practice here.

Replace it unconditionally in the top level of the defining script. Something like this:

class myClass():
	__init__(self, prior):
		# Copy important values from prior version, as long as they
		# are platform data types.  Stop any threads stored in prior
		# and remove any of its listeners from the platform.
		pass

# Get the persistent global dictionary
_g = system.util.getGlobals()

# Protect the swap with a lock.  Locks are defined by
# the jython stdlib, so do not have to be replaced on restarts.
# Use setdefault for assignment to ensure atomicity.
singletonLock = _g.get('singletonLock', None)
if singletonLock is None:
	import thread
	singletonLock = _g.setdefault('singletonLock', thread.allocate_lock())

# Swap old and new under the lock.  This prevents
# races amongst multiple scripting contexts importing this script
# simultaneously.  Just note that a new version might be replaced by
# another script context as soon as the lock is released.
with singletonLock:
	_g['singletonKey'] = myClass(_g.get('singletonKey', None))

# Use the following pattern in functions where you need to use the
# singleton.  Whatever you do, never sleep or do long calculations
# while holding the lock.

# with singletonLock:
#     c = _g[singletonKey]
#     # Do something with c
4 Likes

@pturmel , @Kevin.Herron , @kgamble Thank you very much for the information.
However, I still don’t have the full answer to my question. Why did it work in my
second scenario?

My guess is that its related to scripting restarts during a save/preview update and something to do with local variables in the runScript functions, vs using the actual global library to set the variable inside the runScript function vs not.

However I am not really positive, that’s just a guess incase one of the others tagged knows a more detailed answer…

1 Like

A post was split to a new topic: Adding listeners to Ignition

Where should the singleton lock code execute? Inside the actual init method of the class? I see that you didnt put it in there in this example.

Use it everywhere you work with the singleton class. That prevents replacement of the instance you are working with until you release the lock. As long as every locked section leaves the singleton in a consistent state, you can’t break it.

The lock is not needed in the init method of the class because that is only ever called from the bit of code inside the lock at the top level of the script module. Only one instance is allowed to exist–that’s what a singleton is.

Okay, so say I want to use it in a function, is below a close representation of how I would get the instance and verify it isn’t locked? Then I presume if it is I would need to try again in X seconds, or raise an exception, or something.

def myFunction():
	_g = system.util.getGlobals()
	singletonLock = _g.get('singletonLock', None)
	if singletonLock is None:
		singletonLock = _g.setdefault('singletonLock', thread.allocate_lock())
		with singletonLock:
			_g['singletonKey'] = myClass(_g.get('singletonKey', None))
                        
			myInstance = _g['singletonKey']
			myInstance.performSomeAction()
	else:
		# Singleton is currently in use
		# try this again in a sec to see if it gets unlocked

You’re making it too hard. Just this, when the function is in the same project script:

def myFunction():
	with singletonLock:
		myInstance = _g['singletonKey']
		myInstance.performSomeAction()

Anywhere else, do this:

with path.to.script.singletonLock:
	myInstance = path.to.script._g['singletonKey']
	myInstance.performSomeAction()