Featured

Running Whole Workflows At Once

Adding Workflows and Running Them

A workflow here is an Action that takes nothing and is handed nothing: whatever it drives, it builds, and whatever it builds, it closes. That is what lets one workflow run Chrome on OttoMagic while the next runs FireFox on Selenium, because nothing is copied from anywhere. Run returns once they have all finished. The out overload hands back one WorkflowRun per workflow, in the order they were added, saying whether it finished, how long it took, and what it threw if it threw. A workflow that throws is reported and the rest carry on, because a site that was down is no reason to lose the other nine.

GPAL.Workflow

.WithWorkflow(() =>

{

IBrowser chrome = GPAL.Browser

.WithBrowserType(BrowserType.Chrome)

.WithAutomationEngine(AutomationEngine.OttoMagic)

.GoTo("https://www.ebay.com")

.ToGPALObject();


chrome.WithSelector("#gh-ac").FillInFrom("laptop").SendKey(GPAL.VK_RETURN);

chrome.Close(true);

})

.WithWorkflow(() =>

{

IBrowser firefox = GPAL.Browser

.WithBrowserType(BrowserType.FireFox)

.WithAutomationEngine(AutomationEngine.Selenium)

.GoTo("https://www.walmart.com")

.ToGPALObject();


firefox.Close(true);

})

.WithMaxAtOnce(2)

.Run(out List<WorkflowRun> runs);


foreach (WorkflowRun run in runs)

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, $"Workflow [{run.Index}] succeeded [{run.Succeeded}] in [{run.Duration}]");

NOTE

There is a second WithWorkflow with the same name on the browser itself, and it is the ordered one. browser.WithWorkflow adds a workflow to that one browser and browser.Run executes them in the order they were added, one after another, each handed the browser you built. It is the same idea as a Gherkin scenario without the syntax to go with it: steps in the order you wrote them, named by what they do. The list is also what gets repeated, so While and Until turn it into a loop, and a page of results with a Next button becomes one workflow run until the button is gone. GPAL.Workflow is the one that runs workflows at the same time; this is the one that runs them in a row.

Throttling and Staggering the Starts

WithMaxAtOnce is a throttle rather than a mode: one, the default, runs them one after another, and a site that objects to four browsers at once is the reason it exists. WithStaggerStart is the least time between two workflows starting, a quarter second by default. Workflows that build a browser spend their first seconds asking the machine for a free port and starting a process, and a dozen doing that in the same instant is a dozen asking the same question before any of them has answered it. The stagger is applied where a workflow is let through rather than where it was created, so one that waited on the throttle is spaced from the workflow before it too, not just from the one it was created after. Zero starts them all together, which is what to use when the workflows drive nothing external.

GPAL.Workflow

.WithWorkflow(FirstWorkflow)

.WithWorkflow(SecondWorkflow)

.WithWorkflow(ThirdWorkflow)

.WithMaxAtOnce(2) // never more than two in flight

.WithStaggerStart(500) // at least half a second between starts

.Run();

Bounding the Wait, and What Run Blocks

Without WithTimeout, Run waits forever, and a workflow that never returns is a caller that never returns. Give it a bound and Run comes back when the clock runs out, reporting the unfinished workflows as failed with a TimeoutException in WorkflowRun.Failure and a WARNING saying how many were still going. Understand what that buys and what it does not: giving up is not stopping. A workflow takes nothing to say it should quit, so an abandoned one keeps running on its thread with its browser open. What you get back is a result and a stack you can read, instead of an application that has stopped responding.

GPAL.Workflow

.WithWorkflow(FirstWorkflow)

.WithWorkflow(SecondWorkflow)

.WithMaxAtOnce(2)

.WithTimeout(120_000) // stop waiting after two minutes

.Run(out List<WorkflowRun> runs);


foreach (WorkflowRun run in runs.Where(r => false == r.Succeeded))

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, $"Workflow [{run.Index}] did not finish",

run, GPALObjectType.Other);

WARNING

Run blocks the thread it was called on until every workflow has finished or the timeout expires. Call it from a button handler on a GPALForm and the form stops painting for the whole run, and if one workflow wedges without a timeout there is nothing left on screen to press. Start it on a thread of its own and marshal the results back when it returns. For the same reason a workflow body must never read a form control: the control getters invoke onto the UI thread, and that is the thread sitting inside Run waiting for the workflows. Read the controls before the run and hand the values in.

💬 Ask GPAL