Unexpected Variable Value

We have a function that we use for handling errors with our code. The intent is that an email is generated if we encounter an unexpected issue, and the email is sent both to the original developer and as well as to a group email inbox (Ignition) so that we can monitor issues with code across all locations and developers.

Occasionally we will have a very fast repeating error that will generate many emails in a very short period of time. When this happens, the group email inbox address is often added multiple times to the email even though we are confident that this is not in the code. Here is an example where the group email address (Ignition) is added multiple times to an email:

If there is a delay between the errors, this problem seems to go away and the emails only include one instance of the Ignition email address. Does Ignition, Java, or Jython somehow retain the email variable from the exception function and reuse it when that function is run again?

Here is a simplified version of our code. In the example below, we would see this error if we ran the code() function over again over again in a very short period of time. The send_email function at the end of the exception function is where we actually send the email.

GLOBAL_VARIABL = 'email@address.com'

def exception(email):
	if type(email) is list:
		# Add the group email distribution list to the variable
		to.append('group_email@address.com')
	elif type(email) is str:
		# A single email address (string) was supplied to the function; convert 'to' to a list and add the group email distribution list to the variable
		to = [email,'group_email@address.com']
	# Send the email
	send_email('exception error', to)

def code():
	try:
		print(a)
	except:
		exception(GLOBAL_VARIABL)

Your exception function is never initializing to to any default value, so you run the risk of forever appending to the previous to value until the interpreter instance closes.

Your code should look something more like

def exception(email):
	to = ['group_email@address.com']
	if type(email) is list:
		# Add the group email distribution list to the variable
		to.extend(email)
	elif type(email) is str:
		# A single email address (string) was supplied to the function; convert 'to' to a list and add the group email distribution list to the variable
		to.append(email)
	# Send the email
	send_email('exception error', to)

Also, I understand if you omitted the name of the exception for display here but if not you should not be mirroring the same name as a built in function/type.

I apologize. That was actually due to an error in my example code. I was trying to cut out a lot of other error handling in the exception function that isn't relevant to this issue.

Here is the actual version of the exception function with a few minor edits to remove sensitive email addresses.

def pyException(to=None,object=None,loop=None):
	# 'to' is the email address that should be copied when this function runs.
	# 'object' is the object that caused the exception; this should be 'self' when used on a button
	
	# Import other functions
	import linecache
	import sys
	
	exc_type, exc_obj, tb = sys.exc_info()
	f = tb.tb_frame
	lineno = tb.tb_lineno
	filename = f.f_code.co_filename
	linecache.checkcache(filename)
	line = linecache.getline(filename, lineno, f.f_globals)
	exception = 'EXCEPTION IN ({}, LINE {} "{}"): {}'.format(filename, lineno, line.strip(), exc_obj)
	
		
	try:
		# try to log the user where the error occurred
		user = object.session.props.auth.user.userName
	except:
		# The user variable could not be set, so it will be set to none
		user = None
		
	try:
		# try to log the location where the error occurred
#		location = object.view.id
		location = 'Client: ' + str(object.session.props.host) + '; Connected to ' + object.view.id
#		location = object.session.getInfo()
	except:
		# The location variable could not be set, so it will be set to none
		location = None

	# Send an email if the to parameter is not null
	if to == None:
		# no one is on the email list; only send it to the ignition inbox
		sendEmail('pyException Error',exception,'ignition@address.com')
	else:
		if type(to) is list:
			# A list of email addresses was supplied to the function; add 'ignition@address.com' to the list 
			to.append('ignition@address.com')
		elif type(to) is str:
			# A single email address (string) was supplied to the function; convert 'to' to a list and add the email address and 'ignition@address.com' to the list
			to = [to,'ignition@address.com']
		# Send the email
		sendEmail('pyException Error',exception,to)
	# Check to see if this error is being generated by a function that could cause a loop
	if loop == None:
		# log the exception to the database
		log(exception, user, location)

It's these, default values for function arguments can be a major footgun.

1 Like

Can you elaborate on this? I had already thought of a workaround to my specific issue, but I want to understand what exactly is happening so that we don't have similar issues in other code without realizing it.

I'm trying to find a good post that explains it and make sure that's really it, but it was the first thing that stood out to me. Hang on...

1 Like

Is it related to using mutable default values? If so, I may have just found an example: Use mutable default value as an argument in Python - GeeksforGeeks

1 Like

None is a safe default value, as are primitives and strings, as they are immutable. Collections of any kind are unsafe.

3 Likes

Yeah, hmm...

I don't know. I've stared at it and don't see anything.

I'd start with logging the recipients in the sendEmail function just to rule out any weird SMTP/outlook problems entirely. If this function logs the duplicate recipients then you know some caller of it is providing multiple.

1 Like

Does it have to do with what type the their global variable is?

SOME_GLOBAL_VAL = ["startingString"]

def globalModifier(param1=None):
	if isinstance(param1, str):
		param1 = [param1, "someOtherString"]

	elif isinstance(param1, list):
		param1.append("someOtherString")
	print param1
	return

for x in xrange(3):
	globalModifier(SOME_GLOBAL_VAL)
	print SOME_GLOBAL_VAL

nets

['startingString', 'someOtherString']
['startingString', 'someOtherString']
['startingString', 'someOtherString', 'someOtherString']
['startingString', 'someOtherString', 'someOtherString']
['startingString', 'someOtherString', 'someOtherString', 'someOtherString']
['startingString', 'someOtherString', 'someOtherString', 'someOtherString']

If the global value is a list, it is modified each time the function is called in the loop, but if it's a string it retains its original value each call.

If whatever is calling their pyException is in a loop, or somehow using the same interpreter instance as previous calls, it could be modifying the global variable's value.

@rbachman what happens if you add from copy import deepcopy and then add the line to = deepcopy(to) as one of the first lines in the pyException function?

Adding that to my example function prevents it from modifying the global variable's value during the loop.

Additionally, is this pyException being defined in a project library script? If so you should move your import calls outside of your function.

I suspect (but could be wrong) it's something to do with what's listed on this page. I don't know if this is python specific or doesn't even apply, but could it be based on how it's called and not the function itself? Look at the highest rated answer here:

If that's the case, the better solution would be to use a new list initialized by the function with your group email as the first email, then extend it with the to list. (I suspect this may only be happening when passing a list to the function and if that variable to list is the same variable and not a different instance, they keep getting appended to the end.)