Asynchronous UI with stateless appserver

Posted by ian colenutt on 05-Mar-2020 17:58

Hi all - I need some advice on introducing async remote procedure calls in to ABL webclient GUIs ( or telling whether what I'm trying to achieve is actually advisable / possible with our current architecture ).

It's an OE 11.7 webclient which re-uses a single session managed connection to a stateless classic Appserver. Every RPC from any client window is done on a handle to the same connection.

I am able to make an ASYNC call ON LEAVE of a mobile phone field, set a flag with the reponse and later validate this flag on OK press. It looks something like what the documentation suggests:

 https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/pasoe-migrate-develop%2Fa-complete-asynchronous-request-example.html%23

However I'm having a few issues and I don't think I fully understand what's happening...

  • The ON LEAVE event seems to be interupted by the WAIT-FOR and it tabs to somewhere else on the screen until the async response comes back, when the focus returns to the expected next field in the tab order. Something similar happens for a button press where the focus returns to the button. Perhaps there's something I need to do to allow the original event to continue and not disrupt the UX.

  • If I create the async call on the client's existing connection it obviously prevents any regular RPCs. While unlikely since the async response typically takes < 1000ms it means I'd have to check the async-request-count before all potential RPC calls to prevent the user encountering a a 9004 errors. Not ideal.

  • To get around this I can either create a new connection for each async call, maintain a separate connection for any async calls in the screen or the entire client, or connect to an entirely separate session-free appserver for async calls. I'm not sure which is best or typical practice here.

  • In all of my testing with the above connections & state modes I've encountered STOP errors when submitting any more than 1 simultanous async requests. This may just be an issue with the scoping of my async-request or server handle but I'm finding it difficult to debug or find out why the STOP is happening.

Apologies for the extended question but any advice much appreciated :)

All Replies

Posted by Laura Stern on 05-Mar-2020 18:31

1. The example that you site shows the syntax of the operation.  But you should never write code this way.  You already have a WAIT-FOR in effect for the GUI.  You do NOT want another one.  You should do the asynchronous RUN and just return back to the UI.  When the response comes in, the one WAIT-FOR you already have will detect it and process it by running your PROCEDURE-COMPLETE event (which obviously has to be in scope, i.e., in a persistent procedure).  I think this doc should be fixed to explain that in a real application you would not have a WAIT-FOR statement there.

I think that will take care of your first bullet.  There will be no WAIT-FOR when you do the async RUN, so it won't interrupt anything.

2. I believe your 2nd & 3rd bullets are correct.  You will have to look to others for advice on that.

3. You should not get any error or STOP condition because you've run more than 1 async request.  If the server is busy, they will get queued up.  If you can say specifically what error message you are getting, I might be able to be more helpful.

Posted by Laura Stern on 05-Mar-2020 18:32

P.S.  I am going to log a doc bug, based on my previous comment.

Posted by Patrick Tingen on 06-Mar-2020 08:54

To avoid the wait-for, as mentioned in the docs, I used a temp-table. I had a situation where I needed to do some calculation for a lot of products. I used an async stateless appserver for this, along this lines (more or less pseudo-code)

DEFINE TEMP-TABLE ttTodo NO-UNDO
  FIELD cItemNr  AS CHARACTER
  FIELD hRequest AS HANDLE.

/* Create a to-do list of all items */
FOR EACH bItem NO-LOCK:
  CREATE bTodo.
  ASSIGN bTodo.cItemNr = bItem.itemNr.
END.

CREATE SERVER hServer.
hServer:CONNECT("-URL slater:OuterLimits64@zeus:8810/.../apsv").

/* Start all processes */
FOR EACH bTodo:
  RUN getItemData.p ON hServer 
    ASYNCHRONOUS SET ttTodo.hRequest
    EVENT-PROCEDURE "RequestComplete" IN THIS-PROCEDURE 
      (INPUT ttTodo.cItemNr, OUTPUT TABLE ttItemData APPEND).
END.

/* Wait for all processes to complete */
DO WHILE TEMP-TABLE ttTodo:HAS-RECORDS:
  FOR EACH bTodo:
    IF bTodo.hRequest:COMPLETE THEN DELETE bTodo. 
END.
PROCESS EVENTS.
END.
hServer:DISCONNECT().

PROCEDURE RequestComplete:
DEFINE INPUT PARAMETER TABLE FOR ttItemData.
/* do some interesting stuff */
END PROCEDURE.

Posted by ian colenutt on 06-Mar-2020 12:03

Ahh thanks [mention:04fbfb2e92784123a464ff2aade602b1:e9ed411860ed4f2ba0265705b8793d05] that WAIT-FOR in the documentation example completely threw me. As you say it's a little misleading - ideally there could be a separate example specific to not locking up a UI (since this is a common use case for Async).

I've removed the WAIT-FOR and can now make multiple async requests, with the EVENT-PROCEDUREs completing in time. I now just need to either tidy up after myself or do something persistent as I'm getting 8982 errors if I close the window when there are outstanding async requests.

Ideally I want to create an isolated resuable piece of code that any window can access in order to:

- create a new connection for async requests ( if there isn't one already for the window )
- call remote procedues from various GUI triggers and store the reponse in a global variable / property
- access the variable / property e.g. on OK press was the mobile phone validation
- process events & disconnect when closing a screen to prevent any errors

I will update the thread with my progress but any other advice much appreciated :)

Posted by ian colenutt on 06-Mar-2020 12:18

Thanks [mention:e666fffb14004b29b4bad87b731999a8:e9ed411860ed4f2ba0265705b8793d05] I think I can see the benefit of storing the async-requests for the window in a single TT rather than separate objects. 

If I understand it right it's an elegant way of keeping track and destroying the request objects, after the responses are definitely in?

Posted by Rutger Olthuis on 11-Mar-2020 13:05

Hi Patrick,

I found this thread while looking for a simular setup. I also found the wait-for suggestion in the documentation, while a long time ago someone in our team created a repeat + process events loop.

What would be the reason not use the wait-for solution? I found the solution below (with and without process events) working fine. I'm not a big fan of a continuous repeat myself.
But on the other hand, thats probably happening in the background while using a wait-for.

do i = 1 to 3:
    log-manager:WRITE-MESSAGE ("open requests? " + string(hServer:ASYNC-REQUEST-COUNT)).
    //process events. 
    WAIT-FOR PROCEDURE-COMPLETE OF hRequest[i].
    DELETE OBJECT hRequest[i] NO-ERROR.
end.

Posted by Laura Stern on 11-Mar-2020 13:40

Glad you brought this up because I was already thinking of responding to the PROCESS EVENTS example.  PROCESS EVENTS should follow the same rules as a WAIT-FOR.  They are both what we refer to as IO-Blocking statements.

As I said, if you are running in a GUI app, you already have a WAIT-FOR.  You don’t need another one,  Yes, it will usually work.  But historically you couldn’t use a WAIT-FOR everywhere (e.g, not in functions), so people got into trouble.  That restriction has almost entirely gone away, but there is still one case left if you are using .NET for your UI.  And if you’re not doing that...?  Why not use the prescribed model?  .Net has the same model and enforces it.  We don’t enforce it.  It is up to the developer to do the right thing. Remember, if you have multiple WAIT-FORs you need to ensure that they are terminated in the correct order.  Otherwise you will get a Stop condition. So I believe it is less bug prone and cleaner to do it the right way.  You just need to get used to the idea.

And if you are doing this on an AppServer, you’d have to be running async requests on another server.  That is the only time you should do this as there is no other WAIT-FOR.  And presumably you are doing this to parcel out requests to different sessions simultaneously.  Otherwise there’s no point in using async.  And if you have this model, WAIT-FOR is much more efficient than PROCESS EVENTS.

Posted by ian colenutt on 11-Mar-2020 14:20

Thanks Laura

That ties in with the "one WAIT-FOR rule" referred to in various forums e.g. 

knowledgebase.progress.com/.../P12116


So to clear things up...

For me with my regular GUI example (not .NET) I can't WAIT-FOR after each request in my trigger blocks as it annoyingly delays the trigger APPLY and could lead to STOP errors. However since the request objects are scoped to my window procedure I need to process them all before window close to prevent 8982 errors on reponse. Are you saying these days I can use either WAIT-FOR or PROCESS-EVENTS to safely do this? Or is there some way of letting the existing GUI WAIT-FOR them before it lets the window close?

For Patrick / Rutger on the Appserver it seems that using the WAIT-FOR between each request would effectively be the same as running each request synchronously so as you say fairly pointless. But you're saying after the requests have been sent they're better off using WAIT-FOR if required as it's more efficient than PROCESS-EVENTS and totally safe in backend code?

Posted by Laura Stern on 11-Mar-2020 16:13

Your problem is that the PROCEDURE-COMPLETE event  handlers are scoped to the “window procedure”.  I assume you mean the procedure where your extra WAIT-FOR currently is.  You need to put it into a persistent procedure.  When the event fires, we make the async request handle available  (as SELF I believe).  Once you’ve processed all requests, you can delete the persistent procedure.  I.e., it can delete itself inside the handler.  Then you don’t need to block with a WAIT-FOR to keep everything from going out of scope. You can just return back to your existing WAIT-FOR.  Try it out.

Correct on your 2nd paragraph.

This thread is closed