Ask Reuben

How to allow the user to safely stop a long running operation.

How to allow the user to safely stop a long running operation?

How to interrupt a FOR, WHILE, or FOREACH loop?

How to interrupt a long running database query?

When a program performs something that takes some time to run, such as a loop, a report, a long running database query etc, the front-end has no control.  The runtime is not sitting waiting for an INPUT, MENU etc to be responded too.  However you might want to allow the user to stop the report, database query, batch update etc, typically in case they have entered the wrong parameters, or it is taking longer than expected.

The technique is to have an action-view, typically a button, with the special action name “interrupt”.  When the runtime is executing the long running processes, this button will become active.  The user can then click the button, and an asynchronous interruption request will be sent to the program.

When the run-time receives this event, it will set the INT_FLAG variable to TRUE, and so your code can test if an interrupt has occurred by checking the value of the INT_FLAG variable.

This is covered in the documentation in this section on User Interruption Handling.

I like to create a library to handle the interruption window.  So I’d start with a little test like this …

#! interrupt.per

LAYOUT
GRID
{
[l01                                 ]
[b01                                 ]
}
END
END
ATTRIBUTES
LABEL l01 = formonly.txt, JUSTIFY=CENTER;
BUTTON b01: interrupt, TEXT="Stop", IMAGE="stop";
#! interrupt.4gl

FUNCTION open(title STRING)
    OPEN WINDOW interrupt WITH FORM "interrupt" ATTRIBUTES(STYLE="dialog", TEXT=title)
    CALL ui.Interface.refresh()
END FUNCTION

FUNCTION display(txt STRING, refresh BOOLEAN)
    DISPLAY BY NAME txt
    IF refresh THEN 
        CALL ui.Interface.refresh()
    END IF
END FUNCTION

FUNCTION close()
    LET int_flag = 0
    CLOSE WINDOW interrupt
    CALL ui.Interface.refresh()
END FUNCTION
#! main.4gl

IMPORT FGL interrupt
MAIN
    DEFER INTERRUPT
    MENU ""
        COMMAND "Run"
            CALL do_loop()
        COMMAND "Exit"
            EXIT MENU
    END MENU
END MAIN

FUNCTION do_loop()
DEFINE l_count INTEGER = 0

    CALL interrupt.open("Interrupt infinite loop test")
    WHILE TRUE
        CALL interrupt.display(SFMT("%1 seconds have elapsed", l_count), TRUE)
        IF int_flag THEN
            -- User has pressed interrupt button
            EXIT WHILE
        END IF
        LET l_count = l_count + 1
        SLEEP 1
    END WHILE
    CALL interrupt.close()
    
    CALL FGL_WINMESSAGE("Info", SFMT("Loop interrupted after %1 seconds", l_count), "info")
END FUNCTION

… the key things to note include …

  • the button in interrupt.per has the special name interrupt
  • the use of ui.Interface.refresh() in interrupt.4gl so that the window is open and closed immediately
  • the use of DEFER INTERRUPT in our main program so that we can respond to the int_flag being set
  • the use of IF int_flag to test if the user has clicked the interrupt button
  • setting the int_flag back to 0 inside the close() function in the library.  You could allow the developer to set it back each time outside the library but they tend to forget.  Alternatively you could set it to 0 as part of the open().  It is upto you as to what fits best in your coding standard.

By setting in a library like I have done, you can reuse in many places easily.  Wether that is for loops,  while loops, foreach loops, or an SQL operation.

I would expect most Genero applications to need and have this ability to interrupt something long running.