[IGN-5244]Persist Python Class Object

I am working in Perspective, but this probably applies to other modules as well. I have created user defined classes in the project scripts and I am instantiating them from a view in Perspective. Is there a place I can persist the instance of the object for use between events by the user?

Right now I have it serializing to a property on the view, but when I want to work with the object again I have to de-serialize the object in order to work with it again. Having to write custom de-serialize method seems like extra work if there is a way to persist the object for the client.

1 Like

You can use the system.util.getGlobals() function to get access to a persistent, dictionary. Be careful with this - it’s pretty easy to end up with persistent objects that consume memory and never release it. Also, keep in mind that since this object is getting created per-scope, it’s going to be the same object (on your gateway server) between Perspective sessions - you’ll need to use a unique identifier for your session as the key.

That sounds risky. Would you say that writing a de-serialized method would be the preferred way to handle this in Ignition/Perspective?

Would this be a good use case for Pickle? https://docs.python.org/2/library/pickle.html

Well, it heavily depends on what you’re actually doing with these objects.
Also, FWIW - I have no idea how well pickle works inside the Jython environment. It’s probably fine, but there may be issues if you end up with Java objects mixed into your Python stuff - such as when working with Ignition dates or Perspective’s object wrappers.

What’s the use case? I understand if you’re cagey about doing something proprietary, but maybe there’s a different method to entirely avoid doing whatever you’re doing?

I have an object which holds the state of the view. When the user interacts with the view I want to call a method on that state object that will, depending on the action the user took, update and return the new state object.

Right now on the views startup I have it initialize an object, lets call the object “state”, then the startup script serializes the object “state” to a custom property on the root of the view. Now when the user clicks a button I want to take the object “state” and call the method “buttonClicked” like so state.buttonClicked(). But before I can do that I need to make a instance method that will take the json representation of “state” and turn it back into a python class object before I can call the method buttonClicked.

Wondering if there is any updated best practices on this. I have a class I built that interacts with our MES. Not sure what classifies as a large class, but its doing TCP communication, read/write xml, usually the request/response is fairly small, but could be up to a meg or more. I re-wrote all the code recently to be class based as before it was all single functions and was growing way to unmanageable for even a minor edit. Additionally when the class is initialized we pass the perspective session so we can gather auth info and write back results from the mes to session.custom.

I was hoping on view load, I could just load the class into a view.custom, but it looks like it is just storing the str representation. So that wont work.

I can init the class on each event (binding/script/etc) that needs to use the class, but that just seems crazy to keep re-initializing each time something needs to happen. But maybe this is the safest as regular GC will clean up after me?

@PGriffith I’m a bit worried about how these are very persistent. If I scope each instance of the class to a perspective session id, then just run a cleanup once an hour or so to clear out any orphaned classes, would this keep things safe?

@pturmel will correct me if I’m wrong, but I think as of 8.1.x it’s ‘safe’ to use system.util.getGlobals - I would probably keep a dictionary of sessionId: handlerObjects in a handlers key under getGlobals; then create a project script that works something like this:

def getHandler(sessionId):
    return system.util.globals["handlers"].setdefault(sessionId, __createHandler())

def __createHandler():
    return HandlerClass()

So, in your script(s) you just always use projectLibrary.getHandler(self.sessionId), and at the use site you don’t have to care if it’s initialized or not - but it will be automatically (lazily) created when needed. You can make a best effort to clean it up on session shutdown via the script hook, but a periodic cleanup is probably also a good idea.

Not so much that it’s safe, but that it persists like v7.9. I temporarily patched that up in v8.0 with my now-obsolete LifeCycle module. (Not so obsolete, after all.)

The question of safety is not the lack of persistence, but the persistence of old code, and persistence of cached objects for discarded sessions.

To avoid keeping (leaking) old code, the script module that contains the class definition should have top-level code that retrieves the cache from getGlobals() and replaces all instances with new ones. The class’s __init__ method should have a form that copies state from another (old) instance.

To avoid keeping instances for dead sessions, the cache itself should be based on a weak-keyed dictionary where each key is the session object itself (is session really that?). When that is garbage collected, the associated class object will drop out of the cache automatically.

1 Like

Can class objects be stored in system.util.getGlobals? Or do the objects need to be serialized? Based on @PGriffith code it looks like he is storing a class object.

Nothing in globals needs to be serializable; it’s local to the lifetime of the current JVM. You only need to worry about serialization if you’re handing things between client/designer/gateway.

Jython class objects are code, so the comment above about replacing instances in .getGlobals() when script modules reload does apply. I can’t emphasize enough how important this is to avoid huge memory leaks.

Are you saying there should be code outside of the class but inside the script file that goes through the global variables and looks for instances of the class and instantiates new objects based on the old ones. Then replace the old instances with the new instances in the system.util.globals?

something like this?

class TestClass(object):	
	def __init__(self,
		param1,
		param2
	):
		self.param1=param1
		self.param2=param2
		
	@classmethod
	def reInit(cls,
		testClassObject	
	):
		return cls(
			param1=testClassObject.param1,
			param2=testClassObject.param2
		)
		
dictGlobals = system.util.getGlobals()
if("testObjects" in dictGlobals.keys()):
	dictTestObjects = dictGlobals["testObjects"]
					
	for key, value in dictTestObjects.items():
		dictTestObjects[key] = TestClass.reInit(value)
2 Likes

Precisely.

Does system.util.globals synchronize across a redundant pair of gateways?

No.

Technically when doing this with self.session on a button or something, it actually gives you a wrapper to the session, and somehow still with self.session.getSession() you still just get another wrapper.

Since that wrapper gets destroyed, you lose the data in the weak-keyed ref, any idea how to get a reference to the actual session object for this?

Hmmm. Experiments are in order.

I have been trying each of the values in dir(), and amazingly the ones for getSession() and session gives you a new wrapper object each time, with a different place in memory.

Not sure if there is a function somewhere in a script that could be called with this as a parameter to extrapolate the actual content of the wrapper

Contents of dir for reference:
  "__class__",
  "__copy__",
  "__deepcopy__",
  "__delattr__",
  "__doc__",
  "__ensure_finalizer__",
  "__eq__",
  "__format__",
  "__getattribute__",
  "__hash__",
  "__init__",
  "__ne__",
  "__new__",
  "__reduce__",
  "__reduce_ex__",
  "__repr__",
  "__setattr__",
  "__str__",
  "__subclasshook__",
  "__unicode__",
  "class",
  "close",
  "equals",
  "getClass",
  "getInfo",
  "getName",
  "getPage",
  "getPages",
  "getProject",
  "getProjectInfo",
  "getPropertyTreeOf",
  "getSession",
  "getView",
  "hashCode",
  "info",
  "name",
  "notify",
  "notifyAll",
  "page",
  "pages",
  "print",
  "project",
  "projectInfo",
  "refreshBinding",
  "session",
  "toString",
  "view",
  "wait"

There’s a lot of trickery going on to prevent the scripting system from getting unwrapped session objects. Don’t fight it.

1 Like

Would you say that the easiest way here would be to use sessionId’s and then add a garbage collection script to remove old sessionIds?

I hate to add more erroneous scripting on timers, but if its the only way than it is what it is.