system.net.httpClient async use with callbacks v8.0.12

I’m having some trouble with the Callback use.

	url = self.parent.parent.getChild('txt_URL_All').props.text
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})
	
	system.perspective.print("Assigning callback for call to: " + url)
	promise.whenComplete(recvDataCallback)
	#promise.get()
	system.perspective.print('Callback attached and we should get a triggered soon')


	
def recvDataCallback(arg):
#def recvDataCallback():
	system.perspective.print("Callback triggered")
	## do something out of band
	msgType = "ReadAllData"

I’m expecting to see that callback fired after 3 seconds, am I just doing this completely wrong?

I just think the print isn’t working because when the callback completes on a different thread there’s no implicit Perspective session for the print call to use.

If you stored the sessionId and then specified it when calling system.perspective.print it might work.

Unfortunately, that didn’t work either

	sessionId = self.session.props.id
	system.tag.write("localSessionId",sessionId)
	system.perspective.print("SessionId:" + sessionId)
	
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})
	
	system.perspective.print("Assigning callback for call to: " + url)
	promise.whenComplete(recvDataCallback)
	
	#promise.get()
	system.perspective.print('Callback attached and we should get a triggered soon')


	
def recvDataCallback(arg):
	sessionId = qv = system.tag.read("localSessionId")
	system.perspective.print("Callback triggered", sessionId=sessionId)

and in the Firefox dev tools, the same thing
image

for interest sake, i replaced the memory tag with a Session Custom Property. No change in functionality, but probably should have used that in the first place.

Just to update, I’ve also tried writing to a memory tag in the callback with no luck.

This should do what you are looking for, when you are reaching the promise.whenComplete it is already finished evaluating and should return immediately, you are looking for your response.

promise.get() will block at then end though and wait for the response before continuing to execute

Also no need to worry about a tag, as you can use the sessionid as a parameter to your callback.

	def recvDataCallback(sessionId):
		system.perspective.print("Callback triggered", sessionId=sessionId)
			
	sessionId = self.session.props.id
	system.perspective.print("SessionId:" + sessionId)
		
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})
	
	system.perspective.print("Something after the initial call")
	
	response = promise.get()
	
	if response.isGood():
		recvDataCallback(sessionId)

I tested this out in Chrome and the Designer on a button

If you call promise.get() in the same thread as the .getAsync(), you’ve just converted the operation to synchronous. Equivalent to just using the blocking .get() method. If someone actually needs asynchronous operation, they can’t do this.

@pturmel is correct, what I provided will only work if you want to execute more scripting before dealing with your return in the same thread

If you want it to fully execute asynchronously then you are able to do that with the .getFuture method on the promise.

A few notes though:

  1. You require a page ID in your callback, which I dont 100% understand since its in the same session?
  2. I tried passing the function a parameter on the future.thenRun() however, Ignition does not like this and will give you a Null Pointer Exception (I assume it has something to do with the fact thenRun is expecting a java runnable, and not a python function call?)

Client script:

	def recvdCallbackFunction():
		tags = system.tag.readBlocking(["[default]localSessionId", "[default]localPageId"])
		system.perspective.print("Callback Triggered by function in Script", sessionId=tags[0].value, pageId=tags[1].value)
		system.perspective.print("Callback Triggered by function in Script without PageId", sessionId=tags[0].value)
	
	sessionId = self.session.props.id
	pageId = self.page.props.pageId
	system.tag.writeBlocking(["[default]localSessionId", "[default]localPageId"],[sessionId, pageId])
	system.perspective.print("SessionId:" + sessionId + ",  PageId: " + pageId)
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})

	system.perspective.print("Assigning callback for call to: " + sessionId)

    #Get the CompletedFuture java class for the promise
	future = promise.getFuture()
    #Set the callback for your future response
	future.thenRun(recvdCallbackFunction)
	

Interesting update, it looks like if you reference the variables in your python script within the callback, they are still present and so you can actually execute this without tags involved. However I am not sure if this is intended functionality or not.

	def recvdCallbackFunction():
		system.perspective.print("Callback Triggered", sessionId=sessionId, pageId=pageId)
	
	sessionId = self.session.props.id
	pageId = self.page.props.pageId
	system.perspective.print("SessionId:" + sessionId + ",  PageId: " + pageId)
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})

	system.perspective.print("Assigning callback for call to: " + sessionId)

	future = promise.getFuture()
	
	future.thenRun(recvdCallbackFunction)
	

It's because they're going to execute in different threads; httpClient async actions run in a dedicated pool of threads per-scope, and direct Perspective scripts run in a distinct pool of threads. Perspective scripting actions 'keep track' of what page, session, view, etc they're operating on via Java ThreadLocals - so as soon as you 'lose' the thread, you need to maintain that state yourself.

  • I tried passing the function a parameter on the future.thenRun() however, Ignition does not like this and will give you a Null Pointer Exception (I assume it has something to do with the fact thenRun is expecting a java runnable, and not a python function call?)

I'm interested in what this NPE is; Jython usually tries to automatically cast SAM (single abstract method) classes from functions. Either way, I'm not sure why Promise.then() or whenComplete() wouldn't work for these purposes; remember that your callback function should accept some argument, even if it's going to be ignored.

That's intentional/part of the way Python does closures, yes. Another option if you want to be explicit about providing values to 'callback' functions that you don't have control of the call site is functools.partial.

1 Like

When I try to use

promise.then(recvdCallbackFunction) 
promise.whenComplete(recvdCallbackFunction)

They just never execute the function, error out, or create any gateway logs.

Here is a script that throws it (It is extremely long), I just put it on a button in an 8.0.12 view. Interestingly it also executes the callback immediately without waiting for the response. You will see that all three statements are printed before the error happens.

	def recvdCallbackFunction(paramSessionId, paramPageId):
		system.perspective.print("Callback Triggered by function in Script", sessionId=paramSessionId, pageId=paramPageId)

	sessionId = self.session.props.id
	pageId = self.page.props.pageId
	system.perspective.print("SessionId:" + sessionId + ",  PageId: " + pageId)
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3",params={"a": 1, "b": 2})

	system.perspective.print("Assigning callback for call to: " + sessionId)

	future = promise.getFuture()
	
	future.thenRun(recvdCallbackFunction(sessionId, pageId))

Because you're calling the recvdCallbackFunction and passing its return result (nothing) to thenRun.

1 Like

I never updated, but when I played with this last week my takeaway was that the http client async functions work totally fine, but I never got system.perspective.print to work from another thread, even if I had the session id.

The callbacks were definitely happening though.

One of the fundamental problems with OP’s original code was that he defined the callback function after referencing it, which everyone in subsequent posts, including me, seems to have fixed without mentioning explicitly.

1 Like
	sessionId = self.session.props.id
	pageId = self.page.props.pageId
	
	def recvdCallbackFunction(result):
		system.perspective.print("Callback Triggered by function in Script", sessionId, pageId)
	
	system.perspective.print("SessionId:" + sessionId + ",  PageId: " + pageId)
	
	client = system.net.httpClient()
	promise = client.getAsync("https://httpbin.org/delay/3", params={"a": 1, "b": 2})
	
	system.perspective.print("Assigning callback for call to: " + sessionId)
	
	promise.then(recvdCallbackFunction)

Works for me, without having to explicitly call anything blocking in the main Perspective worker thread.

1 Like

Thanks everyone, I was so focused on the callback side of things, I made an assumption about declaring the def in the wrong spot.

Kevin.Herron and PGriffith both made excellent points, and now I have what I wanted working. I’m extremely grateful for the assistance and hopefully this helps some other poor beginner too.

1 Like