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.
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)
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.
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']
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.
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
@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…
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