Timestamps fetched during a named query run during an SFC are of a different format than the exact same query run from the Script Console

I am trying to run a named query that pulls the last row of data before a timestamp. It is incredibly simple (the CTE format is from when I was trying to do a few and join them together, I know it is unnecessary in this immediate formulation):

WITH tag_history AS
(SELECT *
FROM tag_historian
WHERE t_stamp < :t 
ORDER BY t_stamp DESC
LIMIT 1)
SELECT tag_history.*
FROM tag_history;

When I run this from within the Script Console with runNamedQuery("myQuery", {"t": system.date.now()}), I get a properly behaved object, that string evaluates to something like Tue Oct 24 23:07:43 GMT 2023. When I run it from within a SFC, I get something that string evaluates to something like this: 2023-10-24 22:58:46.561.

Immediately that tells me that there is something different happening under the hood here. I am explictly running the exact same script call, just from within the OnStart method of an Action in a SFC.

Does anyone know what is going on here? It is causing substantial problem in moving data as I must convert the java.sql.Timestamp or java.util.Date object into something serializable in order to send it over the network, and my call to system.date.format(ts, "yyyy-MM-dd HH:mm:ss'Z'") does not work correctly on the object that is being returned from the SFC, for reasons I can not seem to replicate in the script console.

I have confirmed that both java.util.Date and java.sql.Timestamp (the only two possible objects I am working with here) are both formattable using the system.date.format method. All of the evidence is pointing towards something in the SFC engine casting time objects into something incompatible.

Seems like you should be using yyyy-MM-dd HH:mm:ss.SSS if the SFC returned 2023-10-24 22:58:46.561

What lead you to that conclusion? java.util.Date is Java serializable, which would be the only relevant serialization methodology for SFC persistence or for gateway <-> designer comms.

1 Like

I don't want the string to have milliseconds in it. And the main concern is why this is behaving differently when run in an SFC than when run in the Script Console.

def json_encode(myDict, encoding="utf-8", **kwargs):
    import json
    
    try:
        return json.dumps(myDict).encode(encoding)
    except Exception as e:
        print("Exception during json_encode: {}".format(str(e)))
        return None

from java.sql import Timestamp
from java.util import Date

current_date = Date()

javasqlTimestamp = Timestamp(current_date.getTime())
javautilDate = system.date.now()

print(javasqlTimestamp, type(javasqlTimestamp))
json_encode({"t": javasqlTimestamp})

print(javautilDate, type(javautilDate))
json_encode({"t": javautilDate})
>>> 
(2023-10-25 17:33:08.893, <type 'java.sql.Timestamp'>)
Exception during json_encode: 2023-10-25 17:33:08.893 is not JSON serializable
(Wed Oct 25 17:33:08 GMT 2023, <type 'java.util.Date'>)
Exception during json_encode: Wed Oct 25 17:33:08 GMT 2023 is not JSON serializable
>>>

json is a Python standard library module; it has no knowledge of anything in Java.

If you're going to JSON encode things manually, use Ignition's builtin system.util.jsonEncode function. I'm still not sure you actually have to, though, is my point.

I say it every time and I will say it again, it is incredibly frustrating to try to use this mix of java and jython. Anything you guys to do to alleviate this would be a huge benefit from my perspective.

I need to encode the data so that I can send it over TCP to an external service to be processed. While I will test using the builtin system.util.jsonEncode, the virtue of doing this in that python function as above is that it is copy pasted from my external application (with changes to downgrade from python310 to jython27), so I know it will behave within my external systems.

I will repeat the original point of this post: There is a clear difference in behavior between when I run a script in the scripting console and within a SFC action. This is a gigantic red flag for me as we are trying to run our primary control logic through the SFC engine, and I want to understand why they are behaving differently so that I can trust my development work will function when put into production. Can you help me with this problem?

Sorry.
If it helps, don't think of Ignition as a Python execution environment. Think of it as a Java execution environment, where Jython happens to be available as a scripting language. To that end, I personally wouldn't do more than trivial "glue" code in scripting; I personally would write any significant business logic in a strongly typed language like Java or Kotlin and deploy it as a module.

Don't shoot the messenger, but if you're doing this with Python code adapted from a better Python execution environment, you're likely going to end up with memory leaks. Jython's socket implementation isn't great; we've patched it in a few places but it's prone to resource and memory leaks if you're doing stuff with raw sockets.

SFCs are being executed on the gateway JVM. The script console is being run in the local designer JVM. They're using the same Java version, because we take care of that for you, but there's a lot of intermediate steps in terms of string representation of a date object that could be the difference. The only meaningful way to test an SFC is with an actual SFC. In some future version we're planning to add a "Gateway scoped" script console to help alleviate some of these issues, but fundamentally it's just part of the platform - sometimes the scripts you're executing are executing on your local machine, and sometimes they're executing on the gateway, and knowing the difference is just part of learning Ignition.

2 Likes

Rule of thumb to make it as painless as possible when engineering a solution -

  1. Always check if there is a first party function available from IA, some system.* call, in this case system.util.encode
  2. check the java library (or this forum for recommended java libraries) that can help you solve your problem
  1. last resort if you can use a mix of the first two easily is to use a built-in jython library (anything like import something where something is not something you coded and not a java or Ignition library)

It will help you avoid running into unexpected jython issues.

Sorry.
If it helps, don't think of Ignition as a Python execution environment. Think of it as a Java execution environment, where Jython happens to be available as a scripting language. To that end, I personally wouldn't do more than trivial "glue" code in scripting; I personally would write any significant business logic in a strongly typed language like Java or Kotlin and deploy it as a module.

I'm coming to understand that the python is a very thin veneer. Might want to update the marketing material to make this more clear. And writing an entire module is a large jump in scope from just using Ignition as delivered. I am not a Java or Kotlin developer and one of the reasons we chose Ignition as our Scada system was the percieved accessibility using just python scripting, plus the apparent value of the SFC environment for developing processes.

Don't shoot the messenger, but if you're doing this with Python code adapted from a better Python execution environment, you're likely going to end up with memory leaks. Jython's socket implementation isn't great; we've patched it in a few places but it's prone to resource and memory leaks if you're doing stuff with raw sockets.

You think the simple json.dumps.encode is going to cause memory leaks? Most of my code is not imported from an external service, its developed for and in Ignition's scripting engine. This is one of the few exceptions. I am aware that there are message size limits in jython's socket implementation, and I am not excited to run into them. Does Ignition have a supported external messaging system? Sending TCP messages and recieving responses is a pretty useful feature.

SFCs are being executed on the gateway JVM. The script console is being run in the local designer JVM. They're using the same Java version, because we take care of that for you, but there's a lot of intermediate steps in terms of string representation of a date object that could be the difference. The only meaningful way to test an SFC is with an actual SFC. In some future version we're planning to add a "Gateway scoped" script console to help alleviate some of these issues, but fundamentally it's just part of the platform - sometimes the scripts you're executing are executing on your local machine, and sometimes they're executing on the gateway, and knowing the difference is just part of learning Ignition.

Excuse me if I have misunderstood, but you're effectively telling me I can never trust that my system will work in production as programmed in Designer, and that I will be wrestling with SFC weirdness ad infinitum? Love to hear it. I have the same feeling about that as if someone had told me "sometimes the compiler just reads the code wrong". Do you have suggestions for how I can alleviate this problem? The best I have so far is to put logger statements everywhere.

All that said, I am still very disturbed that a Ignition function system.date.format does not work on an object coming out of a Ignition Transaction Group administered database via a Ignition Named Query while running in the Ignition SFC Engine. I was hoping to resolve this on the forums, but I will need to open a support ticket to deal with this now.

No. Encoding to and from JSON is easy. The section of what you're doing that is liable to cause issues is the 'sending over TCP' part. The "official" way to send socket data in Ignition would be the TCP driver, but it's a very leaky abstraction. If you've already got Python socket code, it really isn't a significant uplift to rewrite it to Java's idioms:

The key insight is to use Java libraries - you can still write things in Python, so it's not false to say that the scripting you write in Ignition is Python. It is also true that it's a thin veneer, because it's a scripting language - we're not writing Ignition itself in Python, we're writing it in Java.

That's not remotely what I told you. Nothing in Ignition is non-deterministic. I am trying to make sure you understand that calling a script from the script console is not the same as calling a script in an SFC. Those two things are different from one another - but they are not going to be different between themself. If you want to test a script you are executing in an SFC, you absolutely can test isolated pieces of it via the script console provided you understand the differences implied by the different execution scope. The point I was trying to make is that you cannot say "oh, it worked in the script console, therefore it will work in an SFC" in all scenarios - they are different execution scopes, fundamentally.

Can you export a simple, self contained example SFC that is demonstrating this problem?

No. Encoding to and from JSON is easy. The section of what you're doing that is liable to cause issues is the 'sending over TCP' part. The "official" way to send socket data in Ignition would be the TCP driver, but it's a very leaky abstraction. If you've already got Python socket code, it really isn't a significant uplift to rewrite it to Java's idioms:

Ill look into smoothing this out, but Ill reiterate that it sucks to not be able to do basic network messaging. I realize it's a java problem, but Ignition is a java application so :man_shrugging:

The key insight is to use Java libraries - you can still write things in Python, so it's not false to say that the scripting you write in Ignition is Python. It is also true that it's a thin veneer, because it's a scripting language - we're not writing Ignition itself in Python, we're writing it in Java.

I understand. Hopefully someday you guys upgrade to cPython or even pypy. I say that knowing it is not trivial.

That's not remotely what I told you. Nothing in Ignition is non-deterministic . I am trying to make sure you understand that calling a script from the script console is not the same as calling a script in an SFC . Those two things are different from one another - but they are not going to be different between themself . If you want to test a script you are executing in an SFC, you absolutely can test isolated pieces of it via the script console provided you understand the differences implied by the different execution scope. The point I was trying to make is that you cannot say "oh, it worked in the script console, therefore it will work in an SFC" in all scenarios - they are different execution scopes, fundamentally.

I didn't suggest calling a script the same environment would produce different results, nor did I infer that. I did not accuse Ignition of being atomically indeterministic, nor do I believe that it is. It is as atomically deterministic as anything running in the JVM can be. I said that I can't trust that "my system will work in production as programmed in Designer", but I suppose I should have said that I can't trust that "my system will work in gateway execution as programmed in my local designer instance. I see them as the same thing, since I can't test script execution in the gateway without running through the rigamarole of building gateway scripts and calling them via message handlers, and I can't build an abstract one of these to handle any method passed to it. I tried, but I suppose the java backend doesn't abstract the same way as normal python.

Those two things are different from one another

The development evironment is different from the production environment. While that is normally true in many instances of software development, this is an "All in One" suite, and I would not expect this to be so aggressively true here.

provided you understand the differences implied by the different execution scope

they are different execution scopes, fundamentally.

I understand this. I am frustrated that I can not trust that they behave similarly. I struggle to understand how you do not already have a reliable method to develop scripts for the gateway scope, considering so many things run there in production. Debugging in the SFC engine is awful and developing there primarily instead of in the relatively well behaved scripting enviornment is a massive pain. The errors are hard to get access to, hard to read, and not usually as helpful as the ones delivered in the Scripting Console.

Can you export a simple, self contained example SFC that is demonstrating this problem?

Possibly, I will look into it. It is quite deep in a larger system, and interacts with a database and a dozen functions.

@PGriffith Can I ask for a timeline on the scripting console upgrade to allow running in the gateway?

Major feature development has paused for 8.1, with focus being on 8.3. The first version of 8.3 is supposed to come out ~end of year 2024 (more details). The 'gateway script console' concept is not marked as a must-have feature for 8.3.0, so the absolute earliest timeline I would give you is '2025'. It is possible it would make it into the first 8.3.0 release, but unlikely.

Something like that is on our list (trust me, none of us like being stuck with Python 2.7 compatibility), but it's way further out on the timeline. The fact is that folks like you, coming from a more traditional software development mindset, almost always run into an impedance mismatch with our (heavy airquotes) "IDE" and want it to be better/more than it is... and then there's folks coming from the other direction, who've been using X/Y/Z old SCADA system where VBA is the hot new thing and think "gee, wow, Python, this sure is complicated". Keeping both groups of people happy at the same time is hard. I have an especially vested interest in the scripting experience in Ignition, and I know it better than almost anyone alive...and I fully recognize that it sucks to folks used to an actual IDE.

Last note: fundamentally, this is the hard thing. To get you a more "true" development experience, we'd have to embed the entire SFC engine into the designer and run it all locally. That's just not practical - you'd still get frustrated, it would just be from all the edge case things it didn't do quite the same as the real engine. Piping errors, stacktraces, execution states, etc over the network implies serialization, and that's a security trap and a maintenance boondoggle all on its own - as you've already encountered.

One thing that may help our case - in 8.3 (.0), we're planning a more fully featured concept of "deployment modes" for a gateway, so that you can have 'dev', 'prod', test', etc modes on the same gateway backup, making it easier to develop and test changes on an isolated gateway instance and then roll those same changes into your production system. So instead of the "dev" vs "prod" being "gateway" and "designer", dev and prod are "gateway a" and "gateway b". It's still going to be largely the same experience in the actual designer, but you'll at least have more confidence in your testing not affecting your real system.

1 Like

While this would be really nice, it also scares me a little especially for large systems where performance is important to manage. I presume it'll be possible to turn these other non-prod instances "off"?

They would be independent gateways - just managed under one "config" bundle (think an evolution of the .gwbk format) - so one config definition that outlines "I have ten devices, three DB connections, and five projects" - and then individual "overrides" that point the prod mode gateway at one set of IPs, and the dev mode gateway at localhost, for instance.

Or something like that; specifics are a bit hand-wavy at the moment :slight_smile:

2 Likes

Okay. Thank you for the details, I can pass that uphill. Do you / will you have a public 8.3 feature roadmap?

trust me, none of us like being stuck with Python 2.7 compatibility

I 100% believe you.

Last note: fundamentally, this is the hard thing. To get you a more "true" development experience, we'd have to embed the entire SFC engine into the designer and run it all locally.

See, I'd suggest doing the opposite -- move the full designer engine into the Gateway and have the local client be a window to it. Or at least have that be an available mode. Sure, you lose offline development capability, but its not like you can do a ton when you're offline anyway. And having the entire gateway be containerizable means you can do the sort of Minikube thing of having a local mini-gateway for "offline" development.

One thing that may help our case - in 8.3 (.0), we're planning a more fully featured concept of "deployment modes" for a gateway

This sounds good. Love the idea of having a twin system to work with.

Not in any great detail yet, and not at a level of granularity that would capture something like the proposed gateway script console. I would expect more marketing material to start coming out midway through next year.

1 Like