Skip to content

Commit

Permalink
post 29: the plan-execute pattern (#75)
Browse files Browse the repository at this point in the history
Closes #67.
  • Loading branch information
roman-kashitsyn authored Jun 20, 2024
1 parent dc7c0e2 commit 1c330a4
Show file tree
Hide file tree
Showing 7 changed files with 795 additions and 5 deletions.
4 changes: 2 additions & 2 deletions about.tex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ \section{about-author}{About the author}
Hi there!

My name is Roman Kashitsyn.
I'm a software engineer at \href{https://chainlinklabs.com/}{ChainLink Labs}, where I work on \href{https://chain.link/cross-chain}{\sc{ccip}}.
Before ChainLink Labs, I did hard-core software engineering at \href{https://dfinity.org}{\sc{dfinity}} and worked on large-scale distributed systems at \href{https://shopping.google.com/}{Google.Shopping} and \href{https://yandex.ru/maps}{Yandex.Maps}.
I'm a software engineer at \href{https://chainlinklabs.com/}{ChainLink Labs}, where I work on \href{https://chain.link/cross-chain}{\textsc{ccip}}.
Before ChainLink Labs, I did hard-core software engineering at \href{https://dfinity.org}{\textsc{dfinity}} and worked on large-scale distributed systems at \href{https://shopping.google.com/}{Google.Shopping} and \href{https://yandex.ru/maps}{Yandex.Maps}.

\section{about-site}{About this website}

Expand Down
274 changes: 274 additions & 0 deletions images/29-dynamic-planning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions images/29-just-do-it.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 177 additions & 0 deletions images/29-plan-execute.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/29-source.afdesign
Binary file not shown.
6 changes: 3 additions & 3 deletions posts/28-enlightenmentware.tex
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ \section{bazel}{Bazel}
After my deep dive into \code{make}, I often fiddled with build systems at work:
I introduced \href{https://cmake.org/}{CMake} to my first C++ project to replace complex and scarily incorrect \code{Makefile} files and
replaced an inflexible \href{https://ant.apache.org/}{Ant}-based build system in a 500 \textsc{kloc} Java project with \href{https://gradle.org/}{Gradle} scripts that everyone on the team could contribute to.
But all of the tools I tried, including \href{https://cmake.org/}{CMake}, \href{https://ant.apache.org/}{Ant}, \href{https://maven.apache.org/}{Maven}, \href{https://gradle.org/}{Gradle}, \href{https://www.scons.org/}{SCons}, and \href{https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html}{autotools} left me deeply unsatisfied.
They were clanky, awkward, and hard to extend and compose.
But all the tools I tried, including \href{https://cmake.org/}{CMake}, \href{https://ant.apache.org/}{Ant}, \href{https://maven.apache.org/}{Maven}, \href{https://gradle.org/}{Gradle}, \href{https://www.scons.org/}{SCons}, and \href{https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html}{autotools} left me deeply unsatisfied.
They were clunky, awkward, and hard to extend and compose.

In 2016, I joined Google in Zurich.
I heard about Google's internal build tool, \code{blaze}, and couldn't wait to lay my hands on it.
Expand All @@ -223,7 +223,7 @@ \section{bazel}{Bazel}
\code{blaze} made correct and fast builds not just easy, but \emph{boring} in the good sense.
Only a few years later, when I attempted to use \href{https://bazel.build/}{Bazel}---the open-source version of \code{blaze}---for a toy personal project, did I have to understand the underlying model.

Bazel was the final piece of the puzzle, together with Haskell's typeclasses, \href{https://research.google/pubs/flumejava-easy-efficient-data-parallel-pipelines/}{Flume pipelines} interface, and the \href{https://www.tensorflow.org/}{TensorFlow} 1.0 execution model, that made me understand the ubiquitous plan-execute pattern\sidenote{sn-build-systems-a-la-carte}{
Bazel was the final piece of the puzzle, together with Haskell's typeclasses, \href{https://research.google/pubs/flumejava-easy-efficient-data-parallel-pipelines/}{Flume pipelines} interface, and the \href{https://www.tensorflow.org/}{TensorFlow} 1.0 execution model, that made me understand the ubiquitous \href{/posts/29-plan-execute.html}{plan-execute pattern}\sidenote{sn-build-systems-a-la-carte}{
The \href{https://www.microsoft.com/en-us/research/uploads/prod/2018/03/build-systems.pdf}{Build Systems à la Carte} article by Andrey Mokhov, Neil Mitchell, and Simon Peyton Jones explains how various build system designs map to Haskell typeclasses.
Thomas Leonard's \href{https://roscidus.com/blog/blog/2019/11/14/cicd-pipelines}{CI/CD pipelines: Monad, Arrow or Dart?} blog post is also a great read on this topic.
}.
Expand Down
219 changes: 219 additions & 0 deletions posts/29-plan-execute.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
\documentclass{article}

\title{The plan-execute pattern}
\subtitle{A ubiquitous pattern you won't find in your textbook.}
\date{2024-06-20}
\modified{2024-06-20}
\keyword{programming}
\keyword{software-design}

\begin{document}
\section*

I feel uneasy about design patterns.
On the one hand, my university class on design patterns revived my interest in programming.
On the other hand, I find most patterns in the \href{https://www.goodreads.com/book/show/85009.Design_Patterns}{Gang of Four} book to be irrelevant to my daily work;
they solve problems that a choice of programming language or paradigm creates.

My litmus test of a good design pattern is its cross-disciplinary applicability.
I'm more likely to accept an idea that pops up in fields beyond software engineering.
And the most convincing patterns are the ones that help me in everyday life.

This article describes a universal pattern that billions of people rely on daily, but software engineers rarely discuss---the plan-execute pattern.

\section{background}{Background}

In early August 2020 I was working on \textsc{dfinity}'s \href{/posts/02-ic-state-machine-replication.html#incremental-sync}{incremental state synchronization protocol}.
The protocol brings a stale replica up to date by computing and applying state deltas on top of an existing state snapshot.

One major challenge with this protocol was testing.
If the protocol implementation is a black box that outputs the final state, then how do I check that it took the most efficient path to reconstruct the checkpoint?
Simply observing the output won't work: it might have decided to fetch the entire new state instead of re-using locally available pieces.

To address this problem, I factored the implementation into stages:
\begin{enumerate}
\item Acquire all the information required for synchronization: the bill of materials for the local and target replica states.
\item Build a \emph{plan}---a data structure encapsulating the protocol decisions: what data to fetch, what to copy from the local state, and how to assemble the pieces.
\item Execute: fetch the data and schedule disk writes according to the plan.
\end{enumerate}

Since then, the pattern has become one of my favorite techniques.
I used it a few more times with great success and started recognizing it in other software.

\section{plan}{Plan}

\epigraph{
Planning can be seen as reformulating a problem in a simpler problem space, solving it in the simpler space, and then trying to generalize that approach to the real problem space.
}{Scott H. Young, ``Get Better at Anything''}

There are two ways to get from Washington, \textsc{dc}, to New York City by car.
One approach is getting in your vehicle, starting the engine, and following the road signs until you reach the destination.
I call this approach ``just do it.''
Alternatively, you can sit down with a map and plan your itinerary, deciding which routes you will take and where you'll stop for a break, and then follow the planned route.
That's the plan-execute approach.

In software engineering, we often deal with complex algorithms that can take many paths to reach their goals.
A ``just do it'' implementation freely interleaves decisions and actions, hiding the execution details from observers.

\begin{figure}[grayscale-diagram]
\marginnote{mn-just-do-it}{
The ``just do it'' implementation of an algorithm hides all decisions it takes.
}
\includegraphics{images/29-just-do-it.svg}
\end{figure}

The plan-execute pattern tackles the problem in two stages.
The planning stage takes the inputs and produces a plan: a data structure encapsulating all the decisions the algorithm should make.
The execution stage realizes the plan.

\begin{figure}[grayscale-diagram]
\marginnote{mn-plan-execute}{
The plan-execute pattern consists of two stages: the planning stage outputs the decisions as a data structure, and the execution stage realizes the plan.
}
\includegraphics{images/29-plan-execute.svg}
\end{figure}

This approach allows for more comprehensive testing of the decision-making part.
It also extends the system's debugging capabilities: since the plan is a data structure, humans can inspect it to understand what the system is about to do without causing \href{https://en.wikipedia.org/wiki/Side_effect_(computer_science)}{side effects}.

\section{execution}{Execution}

\epigraph{If you want to make God laugh, tell him about your plans.}{Woody Allen}

In some cases, the execution part of the pattern is straightforward: traverse the plan data structure and run predefined steps:
Turn left, turn right, \href{https://xkcd.com/461/}{take the ferry across the lake}.
That works well if the execution environment is predictable.

When driving a car, however, we might have to take an unplanned route because of traffic, weather conditions, missed turns, and construction work.
Similarly, when execution steps can involve concurrency or potential failure, our programs must adjust the plan as they realize it.

My favorite way to deal with uncertainty is to split the execution stage further into a state machine and a driver loop.
The driver feeds external events into the state machine, and the state machine emits actions that the driver must take.
The original plan becomes an initializer for the state machine starting state.

\begin{figure}[grayscale-diagram]
\marginnote{mn-dynamic-planning}{

}
\includegraphics{images/29-dynamic-planning.svg}
\end{figure}

The beauty of this approach is that the state machine doesn't depend on messy execution details,
so we can test it exhaustively and thoroughly,
potentially applying \href{https://propertesting.com/book_state_machine_properties.html}{property-based testing techniques}.

\section{build-system-example}{Build system example}

The following simplistic build system design demonstrates how all the pattern pieces fit together.
The inputs for a build are the build graph, the set of outputs the caller requested to materialize, and the build cache state.
The plan is the list of tasks we need to execute to materialize the desired artifacts, in the topological order.

\begin{code}[rust]
struct Node {
id: NodeId,
inputs: Vec<ArtifactId>,
outputs: Vec<ArtifactId>,
transform: Command,
}

struct BuildGraph(\ldots)

type BuildPlan = Vec<Node>;

fn plan(
g: &BuildGraph,
cache: &BuildCache,
targets: &[ArtifactId],
) -> Result<BuildPlan, BuildError> { \ldots }
\end{code}

The most straightforward execution function would go over the tasks in the plan and execute them sequentially.

\begin{code}[rust]
fn run_build_sequentially(
plan: &BuildPlan,
cache: &BuildCache,
sandbox: &Sandbox,
) -> Result<(), BuildError> {
for node in plan {
sandbox.execute(node, cache)?;
}
Ok(())
}
\end{code}

A more efficient approach is to implement pipeline parallelism and execute independent tasks concurrently.
This execution model adds a lot of complexity, which the build plan alone doesn't address.
So we introduce a state machine that keeps track of unfinished work and adjusts the plan as the execution unfolds.

\begin{code}[rust]
struct ExecutionState {
graph: BuildGraph,
todo: Vec<NodeId>,
in_progress: Vec<NodeId>,
depcount: HashMap<NodeId, usize>,
}

enum Action {
Schedule(Vec<Node>),
Finish,
}

impl ExecutionState {
\emph{// Returns the first action to take.}
fn init(&self) -> Action { \ldots }

\emph{// Indicates that the task corresponding to the node is in progress.}
fn started(&must self, node: NodeId) { \ldots }

\emph{// Indicates that the task corresponding to the node has completed.}
\emph{// Returns the next action to take.}
fn completed(&mut self, node: NodeId) -> Action { \ldots }
}
\end{code}

The \code{run\_build} function is the driver loop that spawns processes and feeds their results to the state machine, which either outputs more tasks or reports that the build has finished.

\begin{code}[rust]
fn run_build(
cache: &BuildCache,
sandbox: &Sandbox,
state: ExecutionState,
) -> Result<(), BuildError> { \ldots }
\end{code}

This design splits a hard problem into smaller pieces that are easy to understand.
The most algorithm-heavy portions (planning and execution control) do not require any I/O and are easy to test.
Only the \code{run\_build} function needs to interact with the operating system.
The planning function is also helpful in implementing the \code{--dry-run} feature.

\section{instances-relatives}{Instances and relatives}

The \href{https://en.wikipedia.org/wiki/Query_plan}{\textsc{rdbms} query planner} is a supreme example of the pattern.
There are many ways to execute an \textsc{sql} query, and the database engine should always use the most efficient one.
Query plans give us insight into which path the engine is going to take; they are paramount for understanding and tweaking database performance.

The \href{https://en.wikipedia.org/wiki/Interpreter_pattern}{interpreter pattern} is a special case of the plan-execute pattern,
where plans are syntactic trees expressing the intent,
and the execution stage is their interpretation.

The \href{https://sans-io.readthedocs.io/how-to-sans-io.html}{Sans-\textsc{i/o}} protocol implementations provide a blueprint for extracting execution state machines from input/output driver loops
as discussed in the \href{#execution}{Execution} section.
This practice goes back to functional programming folklore;
\href{https://www.destroyallsoftware.com/talks/boundaries}{Gary Bernhardt} called it ``functional core, imperative shell''.

Finally, all programs are plans: a programmer makes all decisions in advance and encodes them as a byte array,
leaving it to the computer to take care of the execution.
``Programming'' was a synonym of ``planning'' before computers conquered the world.

\section{conclusion}{Conclusion}

This article presented two approaches to implementing complex algorithms:
the ``just do it'' approach where decisions and actions are intertwined,
and the plan-execute pattern that separates decisions from actions.

The former approach is the natural first choice that leads to straightforward code.
As our systems grow and become more complex,
the plan-execute pattern becomes a viable alternative that helps separate concerns and test our code more easily and thoroughly.

\end{document}

0 comments on commit 1c330a4

Please sign in to comment.