Global objects in Perspective Scope

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
3 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()

Oh okay, I didnt realize that the section that creates the singleton lock just executes when the project script is referenced, so it would ensure that it exists and I could access it that way.

Would there be any benefit to adding that content into the __enter__ and __exit__ methods on the singleton class so that I could just do
with MyClass as instance instead of having to get the lock and then the key? Or would it cause an issue if the gateway restarted and potentially the code inside the __enter__ and __exit__ methods had potentially changed?

No, those are instance methods and you can’t get the instance before you lock.

1 Like

@pturmel Thank you for your detailed explanation here. Can you confirm I am understanding this correctly:

  • The script containing the class definition stores a lock in globals if none exists.
  • The script containing the class definition instantiates the class and stores it in globals, “inheriting” values from the old instance if it exists in globals.
  • When using this class, for example in an event script, we should import the lock from the script containing the class definition, causing that script to run, replacing the class instance in globals. Then we can pull the class instance from globals and use it.

Yes, except you don’t import the lock. You just refer to it with a fully-qualified name–that is enough for Ignition to ensure it is imported. You should never import from any Ignition script library. Only import standard python modules (sys, traceback, etc.) and external stuff you’ve placed in pylib.

1 Like

Okay that makes sense. Thank you for your help.

Why should you never do this? I had an app that we needed to test the code with pytest, and so we ended up importing any modules we used as aliases instead of code.py. What this meant is that anything in a folder could not be fully qualified in the test

I.e: General.Logging needed to just be imported as Logging due to pytest not understanding the folder structure and the alias not allowing a package name.

Is there a technical reason why not to do this? Or just a “you shouldn’t need to do it in 99% of scenarios so just don’t ever do it”

There is a scenario (race condition) where an import in one script module could capture old code when multiple scripts were updated.

If you need a module to appear under another name, use assignment, not import, just before you use it. In function scope, don’t rely on such from outer scopes. Only a reference to the fully-qualified name will ensure you get the latest code.

{ I wouldn’t be surprised if jython 2.7 improved this situation, but I don’t think it is gone. }

1 Like