Part 1 of this article: https://medium.com/software-engineering-problems/what-represents-the-past-present-and-future-the-future-1-665966610825
Active process
In the previous simple process, the coroutine is passive. It waits for future instruction and just does what was told. This contradicts with the requirement document, which normally describes a task as if it is alive. It knows what it is doing, and it knows what it should do next. In this chapter, we are going to explore the ways to make the future representation more active.
Time scheduler
The first example is about sleep.
lua version: https://tio.run/
es2017 version: https://tio.run/
Call with continuation
coroutine.yield('sleep', {seconds=2})
is awkward. Why sub1_task can not put itself into sleep mode directly? Because it does not have reference to itself. Some language provides “call with continuation” which essentially allow the coroutine to get its remaining calculation as a continuation, and pass that to another function as callback. We can simulate that in lua
lua version: https://tio.run/
“Future” concept is introduced, it wraps the task continuation along with its requirement. When the requirement fulfilled, the task execution will be resumed.
Async and await
Notice that we did not give es2017 version in above example. Because es2017 has its own version of “future” builtin.
es2017 version: https://tio.run/
Although the code looks very different. The underlying mechanism is actually very similar. The compiler/interpreter of es2017 will turn “await” into a continuation passed to the promised given.
We can change the lua code to work like the javascript promise. It just returns a function called “resolve” to capture the task continuation and register it to somewhere, then yield execution.
Network client
Time scheduler is simple. It does not need to return value back into the task. If the coroutine needs to play the role of a network client. It needs a way to get the result back.
lua version: https://tio.run/
Returning value is not a hard job as it turns out. The caller and callee share a “future” object so that they can send request and response via this object. With es2017 await syntax, return value back into coroutine is much simpler.
es2017 version: https://repl.it/@taowen/es2017-client
Network server
At this point, we have already seen how coroutine can compute, sleep and call external service. Now, we need to make the coroutine a server. We only present es2017 version here, the lua code will be pretty much the same as “network client example”.
es2017 version: https://repl.it/@taowen/es2017-server
The difference between client and server is, for the server, it needs two await, but for the client, it only needs one await. One for retrieving request from the scheduler, one for sending back a response. For TCP socket, writing can be blocked by the kernel when the buffer is full. So sending back response is also an async operation.
Passive Process V.S. Active Process
We have checked out several examples of the active process. It can do the following things:
- sleep
- as a client to call another service
- as a server to be called by another service
Compared to the passive process from the first chapter, the active process is indeed very active. It has control of its own destiny. For a direct comparison, let’s say we have two steps, and one sleep between them.
Passive version: https://tio.run/
Active version: https://tio.run/
It is really no different from “passive” v.s. “active” sense. From sub1_task point of view, the passive version use “coroutine.yield” directly to wait for the scheduler to wake it up, the active version use “sleep” which in turn uses “coroutine.yield” to wait for the scheduler. Both versions essentially do the same thing, they call "yield" to park itself.
The difference is about predefined protocol. The active version has a protocol between scheduler and task. If the task park itself to “sleep_future”, the scheduler has the responsibility to wake it up in the specified time. Just like you use os.execute(“sleep 1”), the operating system has the responsibility to wake your os thread up 1 second later.
The benefit of the active process is the scheduler become infrastructure, all unstable business logic is isolated in the task. For the passive process, both sub1_task and schedule will need to be modified if the process needs to be updated.
The scheduler becomes an extensible platform, the tasks are plugins to reuse platform capability.
Coroutine UI
The task parked in the scheduler is anonymous. This is a problem. If we do not know what are you waiting for, how can we know what to give? We know the sub1_task want to know if step1_add or step2_add. This is a “User Interface Problem” essentially.
Here is an example process we want to describe
When the task is launched, it will present the user with two options, either to add or sub. Then the calculation result will be displayed back. One second later, the UI will change back to the two options to let the user do the second calculation. This forms an infinite loop.
This process can be represented by this code
es2017 version: https://jsfiddle.net/taowen/L0p516xv/56/
Just like the scheduler can expose API “sleep”, “recv”, “reply”, it can also provide “user_input” as a reusable infrastructure. Of course, the scheduler can not know what the user interface will look alike, so it takes the argument to define the UI component to use and the model to render the view. Here is the source code of scheduler
The user_input the render the view by setting three variables:
- step_ui: which component to render the view
- step_ui_input: the model of the view
- step_callback: what to do when the user submitted the form
As we have already learned, how “await” “promise” and “resolve” works, it is obvious when the user submitted the form, resolve callback will resume the execution of sub1_task. The actually view rendering and form submission implementation is irrelevant to the topic here. You can play with code at https://jsfiddle.net/taowen/L0p516xv/56/
The scheduler.user_input is provided as a generic tool. It can be used regardless of the business logic we need to describe. The vocabulary of “active process” is expanded. There will be more “future” can be represented by coroutine.
Coroutine hibernation
We can not put the coroutine UI into production, because there is a serious flaw. The coroutine needs to stay alive in computer memory for the whole time. If the user decided to click the button tomorrow, we can not keep the process alive that long. We need to click the “hibernate” button so that the state can be dumped to disk so that we can save resources only resuming the process when needed.
This example requires a special version of lua interpreter: https://github.com/fnuecke/eris
We have expanded what the scheduler can do further. This time, the “hibernate” function allow the task to dump itself into the disk. The representation of the future is totally encapsulated into one coroutine “fib”.
Coroutine database
The hibernation implementation is also seriously flawed. The “continuation.data” is written in an alien binary format. Represent the future using an object such as “order” does not have this problem. We normally would create a table called “order” to save the unfinished order processes.
To represent the order process using coroutine, it would look like:
The order_status column of the order table is a cursor tracking the position of execution. It is essentially the position of “coroutine.yield” in the coroutine. So we can represent the order_status with lua label. When persisting the coroutine, the “program counter” can be translated to “order_status”, and mapped to a database column.
Summary
Now, we can represent calculation logic, UI and database by the active process. The coroutine yield control to its scheduler for reusing predefined and stable atomic operation. All those unstable business logic can be encapsulated in a single coroutine.
Using the active process to describe “long-term future” is unconventional. Looking a coroutine to “invoke” a UI or database is mind-blowing. But it is a plausible representation, and sometimes come in handy.
See also