What represents the past, present, and future: the future(3)

Wen Tao
Software Engineering Problems
5 min readNov 3, 2018

Chapter 1 of this article: https://medium.com/software-engineering-problems/what-represents-the-past-present-and-future-the-future-1-665966610825

Chapter 2 of this article: https://medium.com/software-engineering-problems/what-represents-the-past-present-and-future-9a8f8bbde4b6

We have already seen how powerful single coroutine can be. But compact everything in a single coroutine is not an option. From a complexity management perspective, we have to dive and conquer. Also, the future is not single threaded, we have to describe how things happen concurrently.

Multi-tasking

The first step is to make the scheduler supporting more than one running task. The operating system is an os thread scheduler, which supports more threads than CPU count. But the operating system is a low-level preemptive scheduler, which is fairly complex. Here, what we are going to implement is a simple cooperative scheduler.

lua version: https://tio.run/

To make the scheduler generic, the tasks need to be maintained in a lua table

lua version: https://tio.run/

Combine sleep future with multi-tasking, we have

lua vesion: https://tio.run/

es2017 await/async internally has a ready_tasks. resolve put the task into this list. We can say es2017 has schedule loop for multi-tasking built-in.

Decomposition

The scheduler has been extended to support multiple running tasks. The next step is to decompose one business process into many tasks so that we can better manage the complexity. Leaving the question of task granularity aside, there are three ways to decompose a long process:

Every box here is a coroutine. Orchestration is the simplest form:

lua version: https://tio.run/

The upside of orchestration is simplicity. Following the call graph of A, we can see everything. The downside of orchestration is coupling. If step2 or step3 is not “your business”, it is annoying to invoke them and wait for their return. Choreography is helpful in this kind of case.

lua version: https://tio.run/

This form of choreography does not really remove the coupling. The A is aware of B, and B is aware of C. To decouple them, we need a storage service in between, such as message queue or database.

lua version: https://tio.run/

“A_subscribers” is not an exact representation. The actual role message queue played is complicated. We will make a more accurate representation later.

Coordination

The goal of decomposition is to split one linear process into smaller blocks. Visually, the blocks come one after another. Unlike decomposition, coordination handles the case where blocks “overlap” with each other. There are broadly two types:

Fork/Join

Fork/join is still orchestration. Say, the boss of A assign a job to A. A splits the job into two jobs, and delegate to his direct report B and C. B and C does the job concurrently, then A collects the results and report back. In this case, B and C are responsible, and A is accountable. The signature of orchestration is clear ownership, the guy on the very top is accountable for everything.

es2017 version: https://tio.run/

Fork/join does not necessarily wait for all of them. If one document needs a signature from one VP. The request can send to all VP. If one of them approved, the flow can proceed.

es2017 version: https://tio.run/

There are more questions we can chase:

  • What if we want to wait 2 out of 3?
  • When promise.race returned, what will happen to other still running coroutine? Can we cancel it?
  • What if more than one coroutine await for the same promise?

Communication

Another form of coordination is multiple concurrent tasks collaborate equally. Effective collaboration is hard. Having multiple peer teams working together without a boss orchestrating them often leads to chaos. Effective collaboration builds on top of communication.

One coroutine should be able to “call” another coroutine.

es2017 version: https://tio.run/

A park itself at scheduler. So that when B calls A, the scheduler can wake it up. However the assumption, the server will start before the client is not valid. What if B calls A before A is ready to serve? We need to park the client at scheduler in this case.
Complete Implementation: https://github.com/dexscript/scheduler.ts/blob/master/src/scheduler.ts

Now, we have a way to do synchronous “RPC” between coroutines. To identify the server, we have upgraded the nameless coroutine to “actor” with an identifier. The current implementation is single threaded, as javascript is single threaded. However, the “RPC” can be extended to call an actual remote actor on a remote machine.

The “scheduler.stub” is a syntax sugar to make the call looking nicer.

A quick recap

  • Function/Object/Coroutine to describe a process
  • Active process is a coroutine with well-defined scheduler protocols
  • Active process is very expressive that can represent a complete long-running business process
  • A proper scheduler can do cooperative multitasking
  • Upgrade active process to “actor” that can RPC between each other

In the next chapter, we are going to represent complex collaboration with actors

See also

What represents the past, present, and future: the future(2)

What represents the past, present, and future: the future(1)

What makes code unreadable

What makes good software architecture

3 Roles of Programming Languages

--

--