valand.dev

Complex Software: Asynchronous Machines

Complex software can be daunting to author. I will dissect and reintroduce the concept of asynchronous machines, why we need it in the first place, and how delving into the concept might help conquer complex software.

Software Complexity

Software complexity is the number of components inside a software, assuming that a single component has a single responsibility. A software with a single component that does a lot of things is regarded as not only complex but also amess, but I'm not going to visit that idea. I don't stick to the strict definition of single-responsibility principle that comes from OOP, therefore a component is not necessarily a class, a struct. It's just one part of a software of many with a clear distinction from the others.

Async Machines

"Machine is a mechanical structure that uses power to apply forces and control movement to perform an intended action." - wikipedia.com

Why are we mentioning "machine" at all?

Computer is an automaton, a self-operating machine that follows a predetermined sequence of operations. Those operations are what we call a program. Being Turing complete, a computer can simulate itself, therefore a program can be a machine too.

A video game runs, you play it. While it is running, it reads inputs from peripherals. Those inputs alter the internal state. At one point, the video game can reach a particular state that prompts it to quit. That video game is a machine. Another example, NodeJS. NodeJS loads and interprets JavaScript code and executes it. In this situation, NodeJS is the machine, while the JavaScript code is the program. This is why NodeJS is a VM.

A program can be as boring as receiving input, processing it, and spewing output, but each passing day that simple thing does not suffice. Computers gain new capabilities, file systems, networking, multi-threading, GPU processing, and those are made available through hardware with different speeds. And not to forget the most important part of the realm of computer, human, its user, doesn't like to be kept waiting. That's why a simple input, computation, output doesn't suffice. Everything around the program, hardware interfaces, humans, are all working and reacting at different paces. Inputs and outputs are needed at different intervals. This is the original reason why the concept of asynchronous machines exists.

Essentially, the concept of asynchronous machines is to make all parties, which are the I/O edges of the program, the happiest they can be.

A case: A browser is a complex system. It is responsible for many things, connecting to and loading from servers, running web pages, sandboxing tabs, synchronizing web storage to disks, etc. In it are asynchronous machines, each responsible for different parties:

  • A tab is running a page. Inside a page is HTML DOM, CSS style computation and JavaScript code execution. This is a human facing machine. It must always be responsive to user input. You can find evidence that unresponsiveness is an intolerable behavior for this machine, such as calling a long-running JavaScript operation will trigger a prompt that says "page is unresponsive", offering the user an escape hatch, killing the page, to make sure that the user has options all the time.
  • A tab is running a page. Inside a page is HTML DOM, CSS style computation and JavaScript code execution. It is a human facing. It must always be responsive to user input. Unresponsiveness is intolerable for it. You can find evidence of that anywhere. For example, calling a long-running JavaScript operation will trigger a prompt that says "page is unresponsive". Then it offers the user an escape hatch: to kill the page.
  • A storage-facing program is "happy" when it efficiently reads and writes to storage.
  • And many more...

Messaging is the most important basis of asynchronous machines. Messaging happens at small scale, like communication between threads, as well as at large scale, like blockchain mining. Synchronous APIs directed towards asynchronous machines are either wrapper for request/response with messaging underneath, or fire-and-forget message.

Messaging involves message queues at the receiver's side, and sometimes at the sender's side too. A buffer at the I/O to hardware is a message queue. Hardware and a program are separate asynchronous machines. They need buffers to store unprocessed messages.

Queues are abstracted out for a software developer most of the time. For example, NodeJS has a lot of APIs implementing stream. NodeJS's stream is actually a buffer and its processor behind the scene, which triggers events. NodeJS users only have to understand which events do what.

Many projects I have worked on involve writing two or more asynchronous machines at the same time. Usually one is the UI layer and the other is a layer that interacts with storage or network. Most of the time queues need to be handwritten because it needs a custom behavior, such as merging message duplicates, has a custom backpressure mechanism, etc. Then, because the queue is handwritten, the queue runner also needs to be handwritten. The queue is a "singleton function", in which only one instance of it can exist per queue. It runs at the start of the queue appendage and stops when the queue is empty. Then, at the caller's side, a wrapper is written to make an "illusion" of synchronous calls. Most of the time it is fun.

Messaging can either be brokered or not brokered. A simple brokered messaging would be Mutex that guards access from multiple threads, or Kafka sending messages from one service to another. A message broker is essentially the party responsible for transporting messages from one party to the other. A message broker is essentially the party responsible for transporting messages from one party to the other. Meanwhile, unbrokered messaging is when two parties don't need another party to manage their messages. Peer-to-peer communication is one of those. Being brokered and unbrokered is not a hard 1 or 0 but a spectrum. There can be a messaging mechanism in the middle of the spectrum. Shared storage is, for example, is where multiple parties store data at the same site. The shared storage acts as a passive broker. The difference between brokered and not brokered lies in how a message gets transported and how the receiver and sender must do additional process before and after the transportation, like serialization, validation, etc.

Knowing the existence of a message broker is crucial when you design objects' lifecycle. The message broker must live before the components that rely on it. It also must die after them. This concept seems obvious, but it's not rare for projects to fail this aspect. An easy practice is to structure code chronologically, both for declarative and imperative styles.

// In declarative language/framework:
// example: react + jsx
(
	<ComponentA> {/* ComponentA is mounted first */}
    	<ComponentB> {/* ComponentB is mounted second */}
            <ComponentC /> {/* ComponentC is mounted last and dismounted first */}
        </ComponentB> {/* ComponentB is dismounted second */}
    </ComponentA> {/* ComponentA is dismounted last */}
)
// ComponentA is mounted first and dismounted last
// ComponentB is mounted second and dismounted second
// ComponentC is mounted last and dismounted first
// In imperative language
// example: Rust
fn some_function(){
    let lock = Arc::new(Mutex::new(0_u32)); // <- mutex, the broker, is created first
   	let lock1 = Arc::clone(&lock);
   	let lock2 = Arc::clone(&lock);
    let thread1 = thread::spawn(move || {   // <- thread1 the consumer, is created after mutex
        // do stuff with lock1
    });
    let thread2 = thread::spawn(move || {   // <- thread2 the consumer, is created after mutex
        // do stuff with lock2
    });

    thread1.join(); // thread1 ends here
    thread2.join(); // thread2 ends here
    // lock is destroyed here because of Rust's awesome lifetime
}

The Line Separating FP vs OOP War Lies At The Edges of Asynchronous Machines

Let's talk functional programming, or rather, programming with functional style. FP is often regarded as the opposite of OOP, while I think it's not the exact opposite. Sometime in the past birthed a language which forces you to write everything with class. A class has methods, which has call syntax subject.predicate(). subject in a sentence is active, an agent. This paradigm introduces a side effect. In the perspective of the programmer, a program written in an OOP style has many agents, whereas not every kind of program needs multiple agencies.

In an environment where there are no other machines with different paces, a machine only needs one agent, which is itself. The machine receives input and produces output and maybe some side effects. All components inside the machines work at the same pace, so there's no need for sub-agency, message passing, nor buffering. It is like a librarian with one mind and two hands cataloging and organizing books. The program manages data. It makes functional programming a prime paradigm in building a machine. Its principles like immutability over mutability, pure functions, recursion, higher-order functions, result in elegant code.

At the same time, two asynchronous machines have two agencies. Each has their memory, works at their own pace, acts on their own data, and occasionally communicates with each other. This is where the concept of multiple agencies needs to be introduced. This is, where I think, the original concept of OOP (not nowadays' OOP) makes sense. Alan Kay describes OOP as if computers are cells, and they occasionally send messages to each other. This is also why sometimes people throw jokes at functional programming for being difficult to do side-effects on.

The line that separates FP and OOP paradigms, moreover how programmers perceive machines because of the two, lies at the edges of asynchronous machines.

"I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages" - Alan Kay

Coda

Understanding the concept of asynchronous machines is a huge help to me if we're talking about system design. While this piece of writing is a big idea presented in a rough format, I hope you get something out of it as much as I did.