Skip to content

Latest commit

 

History

History
5109 lines (4237 loc) · 268 KB

thesis.mdk

File metadata and controls

5109 lines (4237 loc) · 268 KB
Title : Concurrent Programming with Actors and Microservices Subtitle : Author : Maximilian Irro Copyright : Maximilian Irro, 2018 Title Footer : 2018 Subtitle EN : &subtitle; Subtitle DE : &subtitle; Author Gender : male Author Pretitle : Author Posttitle : BSc BSc Author Address : Pfalzstra{\ss}e 10, 5282 Ranshofen Regnumber : 01026859 Curriculum EN : Software Engineering \& Internet Computing Curriculum DE : Software Engineering \& Internet Computing Advisor : Franz Puntigam Advisor Gender : male Advisor Pretitle : Ao.Univ.Prof. Dipl.-Ing. Dr. Advisor Posttitle : Subject : Concurrent Programming Keywords : concurrent programming, actor model, microservice architecture Submission Day : 30 Submission Month : 09 Submission Year : 2018 Thesis Type : master Bachelor Thesis : false Master Thesis : true Doctor Thesis : false Master Degree : dipl. Doctor Degree : Script : style/mathjax-font.js Toc depth : 3 Cite All : False Bibliography : dipl Csl Style : style/custom.csl Name References : Bibliography Name Figures : List of Figures Name Tables : List of Tables [INCLUDE=style/vutinfth] top: ⊤ rightleftharpoons: ⇌ rightleftarrows: ⇄ Tex Header : \mdDefineUnicode{8868}{\ensuremath{\top}} \mdDefineUnicode{8652}{\ensuremath{\rightleftharpoons}} \mdDefineUnicode{8644}{\ensuremath{\rightleftarrows}} Package : epigraph Tex Header : \epigraphsize{\small\itshape} \setlength\epigraphwidth{6cm} \setlength\epigraphrule{0pt} Epigraph { replace:"~ Begin TexOnly&nl;\ ~ Begin TexRaw&nl;\ \epigraph{\textit{&source;}}{--- &caption;}%mdk&nl;\ ~ End TexRaw&nl;\ ~ End TexOnly&nl;\ ~ Begin HtmlOnly&nl;\
&nl;\

&source;

&nl;\ — &caption;&nl;\
&nl;\ ~ End HtmlOnly" } .epigraph { max-width:50%; margin-left: auto; margin-right: 0; margin-top: 30px; margin-bottom: 70px; } toc.toc-figures { - } toc.toc-tables { - } dt { margin-top:1ex } dd { margin-bottom:1ex } blockquote { font-style: italic; } .tocblock2 { margin-bottom:1em } .figureline { display: none } .findings { border: solid black 1px; padding:1rem } .findings-title { font-family: sans-serif; font-weight: bold; margin-left:1rem; } .findings-list { margin-top: 1ex; padding-top: 0ex; margin-right:1rem; } .example-title { font-family: sans-serif; font-weight: bold; } .acronym { font-family: sans-serif; font-weight: bold; width:15ex; } .acronym-table { list-style-type:none; margin-left:0; paddding-left:0; tex-env: "itemize[leftmargin=*]" } @if tex { dt { font-family: sans-serif } .tocitem { color:black } Bibliography { tex-cmd-outer-before: "\mdsetrefname{\textsf{&name-references;}}%mdk&nl;" } } CSS Header : /* @import url(http://spratt.github.io/Computer-Modern/cmserif.css); @import url(http://spratt.github.io/Computer-Modern/cmsans.css); */ @import url(https://cdn.rawgit.com/dreampulse/computer-modern-web-font/master/fonts.css); body.madoko { font-family: "Computer Modern Serif", serif; font-size: 16px; max-width: 39em; /* reduce to: 34em */ } .madoko code, .madoko pre { font-family: "Computer Modern Typewriter", monospace; font-size: 1rem !important; } .h1, .h2, .h3, .h4, .h5, .h6, .title, dt, .acronym-item, .findings-title, .example-title { font-family: "Computer Modern Sans" !important; } .madoko .math-rendering { color: black; } hr.figureline.madoko { display: none; } a, a:visited, .toc a, .toc a:visited { color: #0000EE; } .titleheader { max-width: 80%; } .epigraph, figure { -webkit-margin-start: 0; -webkit-margin-end: 0; -moz-margin-start: 0; -moz-margin-end: 0; } .epigraph-body, .figure > p, .findings > p { -webkit-margin-before: 0; -webkit-margin-after: 0; } .acronym-table { -webkit-padding-start: 0; -moz-padding-start: 0; } .findings > ul { -webkit-margin-after: 0; } .epigraph-footer { float:right; font-style:normal; } .li { text-align: justify; } .toc>h1 { text-align: start; } .madoko .footnotes code { font-size: small !important; } ~ Begin HtmlOnly [TITLE] [TOC] ~ End HtmlOnly ~ Begin TexOnly ~ Begin TexRaw \begin{danksagung*} ~ End TexRaw ~ End TexOnly ~ Begin HtmlOnly # Danksagung { -; toc:clear } ~ End HtmlOnly An dieser Stelle möchte ich einigen wenigen meinen Dank aussprechen: * Meiner Familie, die mich in meinem Studium immer unterstützt haben, obwohl ich viel zu selten anrufe. * Meinem Betreuer, Franz Puntigam, für die guten Ratschläge durch welche diese Arbeit zustande kam. * Theresa und Alex, die sich die Mühe gemacht haben diese Arbeit Korrektur zu lesen. ~ Begin TexOnly ~ Begin TexRaw \end{danksagung*} ~ End TexRaw &pagebreak; ~ Begin TexRaw \begin{abstract} ~ End TexRaw ~ End TexOnly ~ Begin HtmlOnly # Abstract { -; toc:clear } ~ End HtmlOnly Common problems require applications to manage multiple concerns simultaneously. A convenient approach is the concept of *concurrent programming*. In this thesis, we investigate two different models for introducing concurrent computational units into software architectures. One approach is the *actor model* that defines theoretically well-known constructs supporting concurrent, parallel and distributed execution in a transparent way. The other approach is an architectural style based on *microservices*, a recent trend that gained academic and industrial popularity. Microservices facilitate many principles of the old Unix philosophy by composing complex functionality through small, independent, highly cohesive and loosely coupled executables. These programs interoperate via lightweight, technology-heterogeneous messaging channels. The deployment modality of microservices conceives concurrent execution through the operating system scheduler. This thesis compares the programming of concurrent computation through actors and microservices with respect to a non-trivial concurrent system scenario. We argue that both approaches share many conceptual similarities and show few but significant differences. Both models have the same expressive capabilities regarding concurrent programming concerns like communication and scalability, but are subject to different trade-offs. We provide implementations of the system scenario based on actor and microservice architectures. Benchmark results of these implementations suggest that actors provide better system efficiency through a smaller codebase. Microservice architectures consume significantly more system resources and suffer especially from purely synchronous communication mechanisms. ~ Begin TexOnly ~ Begin TexRaw \end{abstract} \begin{kurzfassung} ~ End TexRaw ~ End TexOnly ~ Begin HtmlOnly # Kurzfassung { -; toc:clear } ~ End HtmlOnly Applikationen benötigen häufig eine simultane Bearbeitung mehrerer Aufgaben. *Nebenläufige Programmierung* ist hierfür ein verbreitetes Konzept. Diese Arbeit beschäftigt sich mit zwei Modellen zur Definition nebenläufiger Programmeinheiten innerhalb von Softwarearchitekturen. Eines dieser Modelle ist das *Actor Model*. Es definiert theoretisch wohlfundierte Konstrukte, welche transparent eine nebenläufige, parallele und verteilte Ausführung ermöglichen. Bei dem anderen Modell handelt es sich um einen relativ neuen Architekturstil unter Verwendung von *Microservices*, welche sich kürzlich im akademischen und industriellen Umfeld großer Beliebtheit erfreuen. Microservices bauen auf viele Prinzipien der alten Unix-Philosophie, indem sie komplexe Funktionalität durch den Zusammenschluss kleiner, unabhängiger, kohäsiver und lose gekoppelter Programme konzipieren. Diese Programme interagieren über leichtgewichtige, auf Nachrichtenaustausch basierende, technologisch heterogene Kommunikationskanäle. Microservices unterliegen einer implizit nebenläufigen Ausführungsmodalität durch den Prozess-Scheduler des Betriebssystems. Diese Arbeit vergleicht die Programmierung von nebenläufiger Ausführung mittels Actors und Microservices relativ zu einem nichttrivialen Fallbeispiel eines nebenläufigen Systems. Wir argumentieren, dass beide Ansätze viele Gemeinsamkeiten und wenige aber wichtige konzeptionelle Unterschiede besitzen. Beide Modelle haben gleichwertige Möglichkeiten um typische Anliegen der nebenläufigen Programmierung wie Kommunikation und Skalierbarkeit auszudrücken. Jedoch unterliegen die Modelle unterschiedlichen Trade-offs. Wir stellen Implementierungen des Fallbeispiels bereit, welche jeweils auf Actors bzw.\ Microservices basieren. Die Resultate eines Benchmarkings dieser Implementierungen legen nahe, dass Actors eine bessere Systemeffizienz verbunden mit einer kleineren Codebasis ermöglichen. Microservice-Architekturen hingegen konsumieren erheblich mehr Systemresourcen und leiden vor allem unter den Auswirkungen rein synchroner Kommunikationsmechanismen. ~ Begin TexOnly ~ Begin TexRaw \end{kurzfassung} % Select the language of the thesis, e.g., english or naustrian. \selectlanguage{english} ~ End TexRaw &pagebreak; ~ Begin TexRaw \vspace*{\fill} ~ End TexRaw ~ End TexOnly ~ Begin Center *To whom it may concern* ~ End Center ~ Begin TexOnly ~ Begin TexRaw \vspace*{\fill} ~ End TexRaw [TOC] &pagebreak; ~ Begin TexRaw % Switch to arabic numbering and start the enumeration of chapters in the table of content. \mainmatter ~ End TexRaw ~ End TexOnly # Introduction ~ Epigraph { caption: "Butler Lampson"} I think the computer is the world's greatest toy. You can invent wonderful things and actually make them happen. ~ The physical world is a composition of simultaneous activities. Programmers experience nature as a concurrent environment. As such, the idea of simultaneous actions has not been absent from the intangible world of computer programming. Numerous models to conceive concurrent execution have been proposed over the decades. Now that manycore machines are widespread and distribution is popular in the current trend of *cloud computing*, concurrent programming techniques have become essential. Many of the proposed models are therefore now heavily applied in practice. In this thesis, we pay attention to two approaches toward concurrent programming. The first approach is the *actor model* [@Hew73], a decade-old model dedicated to express concurrency. The second approach is based on the *microservice paradigm* [@Fow14], originally an architectural style for software systems that adds concurrent execution implicitly. ## Problem Statement Dragoni *et al.*\ [@Dra17a] point out that there is yet a gap in the literature that emphasizes the connections of the actor model and the microservice model. This work aims to fill this gap, with a focus on the concurrent programming aspects of these two concepts. Specifically, we ask the following research questions: |:~~{width:8ex}~~~|:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | __RQ1__ | Why do actors and microservices qualify for programming concurrency? | { } |:~~{width:8ex}~~~|:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | __RQ2__ | How do the actor and the microservice model facilitate concurrent execution? | { } |:~~{width:8ex}~~~|:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | __RQ3__ | What are the expressive capabilities of actors and microservices regarding | | | concurrent programming concerns? | { } |:~~{width:8ex}~~~|:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | __RQ4__ | How does the performance of actors and microservices compare in a multi- | | | core environment relative to a concurrent system scenario? | { } ## Methodological Approach We conduct our research using the following methodological steps: 1. Identify the key characteristics and resulting model semantics of actors and microservices through literature review. 2. Define a non-trivial scenario for a concurrent system. 3. Develop two implementations of this scenario. One implementation is based on actors and the other implementation is based on microservices. 4. Using the knowledge gained from implementing the systems, evaluate the expressive capabilities of both models. 5. Perform an efficiency benchmark of both system implementations. 6. Evaluate the benchmark results, and answer the research questions. ## Structure of the Thesis This thesis has the following structure: Chapter [#ch-concurrency] discusses *concurrency* in general with a focus on the concerns relevant for our subsequent discussion. Chapter [#ch-actor-model] introduces the *actor model of computation* and subsequently Chapter [#ch-microservice-paradigm] the *microservice architecture style*. Chapter [#ch-implementation] concerns programming with actors and microservices, where Section [#sec-scenario] describes a scenario system we implement, Section [#ch-actor-impl] the implementation strategies with actors, and Section [#ch-microservice-impl] the implementation strategies with microservices. Chapter [#ch-evaluation] evaluates both programming models based on the implementations, where Section [#sec-eval-expressiveness] concerns the expressiveness of the models, and Section [#sec-eval-efficiency] their efficiency. Chapter [#ch-conclusion] gives our conclusive view. # Concurrent Computation {#ch-concurrency} ~ Epigraph { caption: "Rich Hickey"} What matters for simplicity is that there's no interleaving. ~ Computation is conceived through the execution of instructions. We call activities *sequential*, if their list of atomic statements execute in a sequential manner. Given two or more sequential activities are executing either pseudo-simultaneously (in alternation on a single processing unit), or truly simultaneously (on multiple processing units), they interleave and we therefore call these activities *concurrent*. Interleaving weakens the total ordering from sequential actions to a merely partial ordering. As a result, concurrency is *nondeterministic*. Repeated invocations on the same input can result in different outputs in general [@Ben90;@Sco06;@Agh93]. In this chapter, we cover the foundational concerns of concurrency, the distinction to parallel and distributed computing, correctness properties, and different kinds of programming abstractions. The overall requisite for every kind of concurrency is the simultaneous presence of multiple active computational units. Depending on the context in which scholars discuss concurrency, they established different terminologies. The programming language level often uses the term *thread* for the concurrent unit. Concurrency theory uses the *process* construct in general [@Sco06]. However, the term *process* interferes with other notions of executable units we discuss in due course. In order to be able to refer to different concepts without aliasing among the terminologies, we follow the suggestion of Ben-Ari [@Ben90] and denote abstract units of activity as *tasks* throughout the remainder of this thesis. This designation is an homage to the Ada programming language, where *task* refers to an activity associated with a concurrent unit. The term is well-known and offers a neutral way to refer to every kind of concurrently executed computation within a logical unit. ## Foundational Issues {#sec-foundational-issues} Many different notions of concurrency exist. Regardless of the chosen approach, we always have to pay attention to three basic concerns [@And83]: * Expression of concurrent execution : Concurrent computation must be *indicated*. The literature proposes various abstractions and subsequently numerous implementations exist in practice. We discuss some of these abstractions in due course. In general, all concurrency abstractions need to provide the possibility to define tasks as well as manage the tasks [@Bac03]. Examples are *channels*, *coroutines*, *fork and joins*, *futures* and *threads*. The interfaces for the creation of tasks can be arbitrary, e.g.\ as primitives directly within a programming language, as libraries and through operating system calls. * Communication : Tasks must be able to interact and cooperate. *Communication* allows tasks to influence each other [@And83]. The *shared state* communication style rests on commonly accessible memory (e.g.\ variables, objects, etc.). Several tasks then interact by reading and writing to the same state. In contrast, *message passing* communication forgoes any access to shared data. Instead, it builds on the exchange of messages (fixed, immutable data) sent through communication *links*. The links are additionally required elements. Shared memory is only possible among tasks which gain access to a common memory section. A shared physical machine is the most basic form to get access to shared memory between tasks. A network can also simulate a mutual memory region. Message passing on the other hand is not even concerned by the locality or remoteness of memory. Messages can transit numerous links between sender and recipient. Therefore, messages easily bypass machine boundaries [@Fel90]. Message passing can happen in either *synchronous* fashion (messages are sent and the execution delays until the response is available) or *asynchronous* fashion (the execution resumes immediately after sending a message) [@And83]. * Synchronization : Although concurrent execution has merely a partial ordering, communication still requires some ordering constraints. We must perform an action before we can detect its effect. *Synchronization* refers to mechanisms we use to ensure such constraints [@And83;@Mey97]. *Semaphores*, *locks* and *transactional memory* are prominent examples. The literature mostly discusses synchronization techniques regarding the shared state scenario, since the communication requires state modification by the sender before the receiver is allowed to read the information. Also, only one task can modify state at a time to avoid unexpected behavior due to low-level data races. The modification or evaluation of shared state occurs within a *critical section* or *critical region*. Synchronization mechanisms realize *mutual exclusion* where no task can access the shared state while another is within its critical region [@Ben90;@Sco06]. : Message passing on the other hand provides an implicit form of synchronization. Intrinsically, a message must be sent before it can be received. As a result, the semantics of message passing constrains the order by design [@And83]. Synchronous passing additionally constraints the sender from resuming its computation until it receives an answer. : There are two kinds of synchronization mechanisms. *Enforced primitives* guarantee no access to the state outside the primitive, thus ensuring order. *Unenforced primitives* grant a certain degree of freedom in their usage and therefore provide no guarantee of mutual exclusion, as do e.g.\ semaphores [@Fel90]. ## Concurrency, Parallelism and Distribution So far, we have discussed concurrency as a generic term that denotes a simultaneous execution of activities. Yet there are more diverse notions that regard the implementation of concurrent execution. A strict distinction in our terminology is therefore in order. We use *concurrency* to refer to the multiplexing of multiple tasks among one or more processors. We cannot make more specific assumptions in general. On a single *central processing unit* (CPU), the interleaving of computation is merely pseudo-simultaneous via time slicing, since all computation is still sequential [@Roe15]. *Parallelism* refers to truly simultaneous execution on different CPUs. Whether parallel execution is possible depends on the concurrency abstraction and its implementation [@Bac03]. On a programming language level, referencing components is usually subject to physical limitations regarding the program's memory. For example, objects can only reference other objects that are inside the memory space of the same program in general [@Mey97]. We regard a notion of concurrent objects that is able to surmount this restriction in due course. This limitation on memory space does not prevent us from writing concurrent code in general. But the limitation certainly complicates the writing of parallel code, e.g.\ when we use shared state communication. Parallel execution requires code execution on different CPU cores *at the same time*, which usually means distinct process memory boundaries. Inter-component communication must happen across memory and process boundaries. If a programming language uses a *virtual machine* (VM) to execute code, we can charge transparent inter-component communication across boundaries to this VM. For example, the *Java Virtual Machine* (JVM) has different approaches to implement threads. One is to map Java threads to system processes for parallelization [@Hal09]. The JVM hides the resulting gap in memory sections transparently. Writing explicit parallel code, e.g.\ with a *Fork/Join* framework, can be painful and requires us to explicitly prepare code segments and data that we can process in parallel [@Sco06;@Lea00]. *Distributed computation* is regarded by the literature as its own research discipline separate from parallel computation. However, both concepts build on the same fundamental idea: Truly concurrent execution (as in *at the same time*) of physically distributed tasks [@Agh99]. Agha, Frølund and Kim formulated a simple and sound argumentation [@Agh93]: > "In a parallel computation some actions overlap in time; by implication these events must be distributed in space." This argument suggests that every parallel task is also a distributed task in a certain sense. The major distinction is that we expect parallel tasks to be physically closer to each other (same CPU) than distributed tasks (distinct CPUs and machines). Due to this distance, distributed tasks cannot share main memory directly [@Bus90;@Agh99]. Distribution therefore relies on message passing communication over the network. Of course, we can use the network to create abstractions for shared memory, so-called *spaces*. One example for a space is the *Linda* model [@Ben90;@Sco06]. Due to the physical separation, a subset of all distributed tasks executes on different locations (host machines) in general. We also refer to these hosts as *nodes* [@Ben90]. A single node can have one or more processors on which one or more tasks run concurrently. As a result, we make three fundamental observations about the interrelations of concurrency, parallelism, and distribution: 1. Concurrent systems can be parallel and distributed. 2. Parallel and distributed systems are inherently concurrent. 3. Distributed systems with two or more nodes are parallel. Baeten [@Bae05] gives a general definition that incorporates these interrelations and which reflects our view on concurrency within this thesis: > "Concurrency theory is the theory of interacting, parallel and/or distributed systems." In subsequent sections, we pay attention to two selected task abstractions. Both conceive concurrent computation in general. Additionally, they are able to provide parallelization and even distribution in a transparent way. ## Correctness Properties On a fundamental level, the basic purpose of every program is the computation of a result. From a sequential program, we always expect the same results for the same inputs[^fn-sequential-side-effects]. We can verify the correctness of a program using theoretical methods, although these methods are not widely adopted in practice. For concurrent programs, many standard verification techniques do not hold anymore, due to the intrinsic interleaving of computations [@Ben90]. [^fn-sequential-side-effects]: All sorts of side effects, like IO, are also forms of input to a program and must be stable as well. Many different issues regarding concurrent computation are well-known in the literature. Examples are deadlocks, livelocks, starvation, race conditions, mutual exclusion and fairness. Due to the high-level view on concurrency in this thesis, we do not immerse into detailed discussions on each of these issues, as we often find it in other literature that concerns concurrency concepts. Here, simply two types of properties are relevant to us. Both types have an effect on the correctness of concurrent programs. We can classify all the issues we gave above in terms of these two property types [@Bus90;@Swa14;@Sin09]: * Safety : asserts the operations that are allowed (safe) to be performed. As a result, given correct inputs result in correct outputs, while the computation never enters an undesired state. Examples of safety properties are race conditions and mutual exclusion. Informally, safety guarantees that "nothing bad will happen". * Liveness : asserts the operations that have to be performed, such that a certain state will be reached eventually (progress). In other words, if we provide correct inputs, we have the guarantee for correct outputs in finite time (cf.\ termination in sequential programs). Examples of liveness properties are fairness, starvation, and reliable communication. Informally, liveness guarantees that "something good will happen". Safety is *invariant*, such that a property `P` holds in *every* state of *every* execution. In contrast, liveness of `P` demands that the property holds in *some* state of *every* execution. As a result, safety and liveness have a so-called *duality* relationship. The negation of a member of one type is a member of the other [@Ben90]. Safety relates to *partial correctness* (the result is correct if the program terminates). Liveness on the other hand relates to *total correctness* (the programs terminates with a correct result) [@Bus90]. We have found *deadlocks* (blocking operations which for some reason do not unblock) to have a controversial role. On the one hand, an execution path must not lead into a deadlock (safety), while a deadlock also threatens the progression of a program, thus its liveness. In contrast, so-called *livelocks* (loops never meeting their termination condition) are, as the name suggest, clearly relate to liveness. The operations of a livelock are safe while the program does not progress. ## Programming Abstractions {#sec-concurrency-programming-abstractions} Most programs are concurrent in some way. An example of *implicit concurrency* is *input/output* (IO). There, we trigger IO devices to perform operations simultaneous to the executing program [@Bus90]. Also, compilers and interpreters exploit the concurrency inherent to a language's constructs. On the other hand, *explicit concurrency* must be indicated. We require appropriate programming abstractions. In general, we require concurrency abstractions to be powerful and expressive models, fit harmoniously into the programming language in terms of their interface, and exploit the underlying hardware resources efficiently [@Shi97]. ### Language-Construct Approach {#sec-concurrency-language-level} Many different approaches to explicitly express concurrent computation on a programming language level were proposed over the decades and are now in use. A programming language either provides concurrency constructs by design, or we can utilize such constructs through libraries and frameworks [@Ben90;@Sco06]. Therefore, most concurrent task abstractions are available in most programming languages. The implementation part of this thesis in Chapter [#ch-implementation] is in the context of Java and its virtual machine. A brief discussion of Java's basic approach towards concurrency is in order, since alternative abstractions have to build on it. #### Case Study: Concurrency in Java Java is an object-oriented programming language with a C-inspired syntax for the JVM. The language expresses concurrency via threads and offers basic concepts to manage the access to shared resources. We define concurrent computation through the `Runnable`{language:java} interface. The default implementation of `Runnable`{language:java} is available in the `Thread`{language:java} class [@Goe06;@Gos15]. The following example illustrates the principle approach: ```{language:java} class State { public int x = 0; } final State s = new State(); final Runnable task = () -> { final String name = Thread.currentThread().getName(); synchronized(s) { s.x += 1; out.println(name + " " + s.x); } }; new Thread(task).start(); new Thread(task).start(); ``` The `synchronized`{language:java} primitive allows us to express mutual exclusion to a shared resource whenever concurrent modification to this resource is possible. In this example, `s` denotes some state. Two threads have access to `s` through the scope of the `Runnable`{language:java} lambda. Note that though we declared `s` as `final`{language:java}, its publicly visible member `x` remains mutable. The mechanism behind Java's synchronization is *locking* on a common reference among all involved threads, the so-called *monitor* object [@Goe06;@Gos15]. When we use `synchronized`{language:java} as a block construct, we must provide this monitor as an argument. In our example, the state variable simply doubles as the monitor in addition to being the shared resource. Alternatively, we could have used the `synchronized`{language:java} keyword also as a part of the signature of a method in `State`{language:java} which holds the logic. A `synchronized`{language:java} method signature is equal to `synchronized(this)`{language:java} around the whole method's body, where `this`{language:java} refers to the object `s`. The method's object reference then acts as the monitor, just as in our example. A more modern alternative towards synchronization is the `Lock`{language:java} interface. The previous `synchronized`{language:java} keyword is an enforced synchronization primitive. The locks of `synchronized`{language:java} are always exclusive to one thread at a time. In contrast, the various implementations of `Lock`{language:java} do not need to be enforced. `Lock`{language:java}s can therefore offer fine-grained options to control the locking, e.g.\ for simultaneous access of multiple readers [@Sub11;@Goe06]. To provide this degree of freedom, Java neither detects shared state nor requires its synchronization per se. As a result, programmers can easily introduce data races when they simply omit access control. Alternative concurrency abstractions for Java, e.g.\ through libraries and frameworks, always have to take this into account. Expressing concurrency on the programming language level has its perils due to the overlapping of language concepts. We have already demonstrated the introduction of mutable state via scopes and visibility. Many concepts have an influence on concurrency considerations. Shared mutability and synchronization especially require utmost care of the programmer for handling access to the data. ### Operating System Approach {#sec-concurrency-os-level} Operating systems (OS) use the *process* as their computational model. A process describes the instantiation of a program image with associated *resources* (e.g.\ memory). Processes express dynamic execution. In the most basic case, a single processor alternately executes these computational units. *Scheduling* is the activation and passivation of processes and it is in the responsibility of the operating system. Scheduling results in a quasi-parallel execution of active processes. If we utilize multiple processors, the execution is truly parallel [@Bac03;@Sco06]. As a result, we can state that processes are inherently concurrent units due to their execution modality. In contrast, *threads* are tasks *inside* a process. One process can have numerous threads which all share the same memory space [@Tan07]. Since threads within the same process have access to the same memory locations, we are free to introduce shared state among them. In the Java case study, we already demonstrated that shared state requires synchronization. In contrast to the JVM, an operating system strictly enforces the memory boundaries between processes. Communication between two processes requires either an explicit arrangement of so-called *shared memory*, or another means of message passing communication which we subsume as *inter-process communication*[^fn-ipc] (IPC) [@Bac03;@Lov07]. We extend our focus on OS-conceived concurrent tasks which rely on IPC in due course. A consolidating example for future reference is in order. [^fn-ipc]: In concurrency theory, *process* is also the general term for a concurrent unit, as we have mentioned. Therefore, the literature often denotes all communication between concurrent units as *inter-process communication*. To avoid confusion, we use the IPC designation only for communication mechanisms between OS processes. #### Case Study: Concurrent Processes in C {#sec-concurrent-c} Only with C11[^fn-c11] did the programming language add native support for the expression of concurrency via threads. Prior to this, programmers had to use more operating system depending approaches like the POSIX[^fn-posix] threads binding `pthreads`. An additional strategy was to compose concurrent computation in an ad hoc way by relying on the operating system's scheduler to assign processes to processors in a concurrent way [@Bac03; @Fel90]. [^fn-c11]: The C standard revision of 2011, specifically *ISO/IEC 9899:2011*. It succeeds C99, the 1999 revision. [^fn-posix]: __P__ortable __O__perating __S__ystem __I__nterface, a collection of standardized programming interfaces for operating systems. The __X__ is a remnant from the original draft name *IEEE-IX*. An operating system allows a process to spawn new processes. The process we call the *parent* uses system calls that the OS provides to spawn a new process we call the *child*. For example, the `exec`-family of Unix calls allows us to instantiate arbitrary processes from executables we reference through a filesystem path. However, the new child replaces the parent process. `exec`-calls alone are therefore not insufficient to compose concurrency [@Lov07]. The expedient path is the alternative `fork`-call. It replicates the current process's address space into a separate address space of a new child [@Bac03; @Fel90]. The following example illustrates the control flow: ```{language:cpp} void parentBehavior(int fd[]); void childBehavior(int fd[]); int main(void) { int fd[2]; pipe(fd); pid_t pid = fork(); if (pid == 0) childBehavior(fd); else parentBehavior(fd); } ``` Both processes are based on the same program image. The parent receives the *process identifier* (PID) of the child process as the result of `fork()`. The child does not receive its own PID information. We can therefore use the PID to distinguish the further program flow of both processes. This mechanism effectively supports two separate behaviors. Consecutive forks are possible of course. By using additional Unix system calls, we install a so-called *pipe* as the IPC between the two processes. Pipes are a form of byte stream across the memory boundaries of the respective processes [@Bac03]. Since Unix follows an *everything is a file* design principle, two file descriptors symbolize the endpoints of the pipe. This principle makes the interfaces simple and consistent [@Shi97]. The first descriptor `fd[0]`{language:cpp} is in read mode and the second `fd[1]`{language:cpp} in write mode. For example, the `parentBehavior` writes data to `fd[1]`{language:cpp} and the `childBehavior` subsequently reads this data from `fd[0]`{language:cpp}. Hence, the data crosses memory boundaries. Pipes are a communication link for message passing which avoid the critical region problem. ### Network Approach {#sec-concurrency-network-level} As we have outlined, distribution is another approach to the conception of concurrent execution within a system. Aside from the lack of shared memory, the distinguishing characteristic between parallel and distributed computing is the geographical distance between tasks. Therefore, the communication between distributed tasks happens via networked message passing mechanisms. Networks introduce a wide range of perils. We can neither assume that the communication links are reliable nor static. Also, messages are more costly in terms of time (latency) and effort (e.g.\ due to data serialization) [@Agh99]. The famous *Fallacies of Distributed Computing* by Deutsch subsume many of the problematic aspects [@Tan07]: * Fallacy 1: *the network is reliable*. * Fallacy 2: *latency is zero*. * Fallacy 3: *bandwidth is infinite*. * Fallacy 4: *the network is secure*. * Fallacy 5: *topology doesn't change*. * Fallacy 6: *there is one administrator*. * Fallacy 7: *transport cost is zero*. * Fallacy 8: *the network is homogeneous*[^fn-8th-fallacy]. { list-style-type:none } Fallacies 4 and 6 are outside the scope of this thesis. The remaining aspects are relevant to some concepts we have already discussed or will soon discuss. For example, Fallacy 2 affects synchronous communication. Asynchronous messaging does not concern the latencies which delay the travel time of messages. Time constrains synchronous communication and therefore the network-induced latencies also affect synchronization. [^fn-8th-fallacy]: Fallacy 8 was not part of Deutsch's original proposal, but later added by Gosling. Hence, the literature sometimes refers to merely seven fallacies. All concurrency through distribution is subject to these fallacies in general. Network programming interfaces depend on the concrete network mechanism. A very basic example is the concept of *sockets* that Unix introduced. The standard interface of sockets provides the means to write data to the network. Alternatively, we use a socket to receive data from the network [@Tan07;@Bac03]. Practically every operating system provides sockets and bindings exist for almost all programming languages. Therefore, sockets are a technology-heterogeneous mechanism, although rather low-level (transport layer). More high-level is for example HTTP (__H__yper__t__ext __T__ransfer __P__rotocol), a generic, originally stateless and text-based communication protocol that provides platform-neutral data transfer on the application-level [@Cou05]. # Actor Model {#ch-actor-model} ~ Epigraph { caption: "C.A.R. (Tony) Hoare"} Some problems are better evaded than solved. ~ In this thesis, we particularly focus on one of the traditional models of concurrent computation. In the 1970s, Hewitt *et al.*\ [@Hew73] formulated the *actor model of computation*. As the name suggests, this model builds upon the concept of *actors* as basic building blocks. In this chapter, we describe the model's properties, its unified abstraction, different implementations of the actor model, and its combination with other models of concurrent computation. Agha [@Agh90] describes actors as self-contained, interactive and independent components that communicate via asynchronous message passing. He also reformulated the actor model into three basic primitives an actor can perform upon receiving a message [@Agh85a]: 1. Send a finite number of messages to itself and other actors. 2. Create a finite number of new actors. 3. Substitute its current behavior with a *replacement behavior*. In order to send a message, we must know the unique *address* of an actor. The underlying *actor system* delivers every message. In general, the order of delivery is nondeterministic. We can announce an actor's addresses by sending the address as a message. This method of propagating location information provides the ability of *dynamic reconfiguration* [@Agh90]. An actor processes one message at a time. Every message gets buffered in the so-called *mailbox*, if an actor is not able to process an incoming message immediately, because it is already engaged in a message handling operation. Access to the mailbox is race-free, and therefore safe [@Hal09]. ## Message Passing and Encapsulation {#sec-actor-messaging-encapsulation} The actor concept defines that the only possible form of communication between actors is the exchange of messages. This restriction implies that there is no directly shared state between actors. An actor *encapsulates* its entire state exclusively. To access an actor's state, we must send a message that requests the state information. Actors process messages in a serialized fashion. This provides the basis for *isolation* [@Les09]. All state modifications an actor does while it processes a single message appear to be *atomic*. New messages do not interrupt an actor that currently processes a message [@Agh99], because the mailbox buffers all messages. It is important to realize that the state information we request from an actor is a mere snapshot of the actor's state at a specific point in time (the point when this actor processed the respective message). When we receive a snapshot answer, we must be aware that this information is already outdated in general [@And83]. On the other hand, the isolation of state frees actors from the implications of shared state and resource handling, like the bottlenecks that sequential locking introduces [@Agh90]. We must either copy, proxy, or make the messages otherwise immutable in order to ensure that the snapshots do not violate the encapsulation semantics and prevent that we accidentally expose a direct access to internal state or resources [@Kos16]. This immutability guarantee enables save coordination at a distance [@Hel15]. Conceptually, we realize the internal state changes within an actor through the third of the basic primitives: behavior replacement. In general, this primitive changes the behavior of the actor entirely. The actor taxonomy also calls this to *become* different operations for all following messages. However, the behavior can also become the same operations, but for a different state [@Weg90]. It is important though that behavior replacement does not break the *referential transparency* of the actor's address [@Agh90]. Therefore, changing actor internals has no effect on its reachability for other actors. The actor logically stays the same, but behaves differently for the next message. This strict encapsulation of state and decoupling via immutable and asynchronous message passing leads to a strong form of *isolation* between actors [@Agh14]. State within an actor is mutable, but isolated from the outside and only available through immutable snapshots. Additionally, an actor only changes state while it processes a message. Therefore, as De Koster *et al.*\ [@Kos16] illustrate in detail, we can view the processing of a single message as an isolated operation. This is important when we reason about actors, because we always have to reason with respect to the single-threaded semantics that provides the granularity of a turn[^fn-isolated-turn-principle] [@Ber14]. We call this the *isolated turn principle*. This principle guarantees the safety of actors, because they are free of low-level data races. However, high-level races (depending on the interleaving of messages within the mailbox) can still occur. The isolated turn principle also guarantees computational progress with each turn [@Agh97], and thus liveness. [^fn-isolated-turn-principle]: In this context, *turn* refers to the processing of a single message. The literature defines various terminologies. A good overview of actor model taxonomy and the equivalence of various terms gives [@Kos16]. ## Unified Abstraction Until now, we have described the actor model as a general model of computation. The abstraction of actors provides a strict separation of component states, as well as a loose coupling via asynchronous message passing. Actors encapsulate not only data and functionality, but also their own *thread of control*, making actors autonomous [@Agh99]. This autonomy enables the concurrent execution of actors, effectively turning the actor abstraction into a model of *inherent concurrent computation* [@Agh91]. There are numerous models which are able to provide inherent concurrency, e.g.\ logic programming or functional programming. The benefit of the actor concept however is its support for the direct expression of state [@Agh91]. This state is only mutable within an actor and while the actor processes a message. Each turn is atomic. Omitting to share state and only communicating information via asynchronous message passing greatly improves the safety of actors, as it eliminates a whole class of errors, the *race conditions* [@Cha13]. The dynamic data flow of messages is the primary source of nondeterminism. We get no guarantee on the order of messages when various actors send messages to the same actor. Yet the actual order of processing the messages affects the behavior, resulting in nondeterminism. A not enforced message order however eliminates unnecessary synchronization overhead [@Kar09; @Agh90; @Agh99]. The actor model provides a strict concept of isolated and decoupled components. The only link between actors is the delivery of messages, based on their addresses. These addresses are virtual since they do not expose physical location information [@Ber14; @Tas13]. Addresses therefore do not restrict actors to physical locations. As a result, we do not require the concurrent units to be inside the same process boundaries, nor the same host machine. The addresses bridge the gap in physical distance. *Location transparency* is the general term for separating the virtual and physical location [@Kar09;@Agh99]. The concurrent components of the actor model inherently support parallel component execution, since they can be transparently assigned to processor cores. Additionally, location transparent addresses enable us to reference actors even outside the scope of a CPU. We get the foundation for distributed execution on different nodes [@Kar09]. We refer to the execution on the same CPU as *intra-node* parallelism, and to the distributed execution as *inter-node* parallelism [@Reh13]. Intra-node components are still physically close, and we can assume that their communication channel is reliable. Inter-node components have no guarantee on the safety and reliability of their communication channel. The messages must travel via network links (cf.\ Fallacy 1: *the network is reliable*). The only valid assumption is that communication is more costly and volatile in any case [@Agh99]. A particular characteristic of the actor model is therefore the facilitation of one and the same primitive for task unit communication in concurrent, parallel and distributed execution contexts. ## Actor Systems and Variations {#sec-actor-systems} Actors are autonomous computational entities, but not individually deployable on operating systems in general. They require a runtime environment, the so-called *actor system*, to exist within. Actor systems have two general concerns: they provide the linguistic support to utilize the actors (programming interface and model semantics) and they realize efficient actor execution [@Kar09]. Depending on the underlying programming model, the actor concept and primitives can pose a challenge for programmers. The model primitives provide a very low-level abstraction to express computation in an (almost) pure communication paradigm. Therefore, actor systems aim for additional, more high-level primitives [@Agh90;@Weg90], e.g.\ to express various other communication patterns. Efficiency can pose a challenge, since the runtime must use the idioms of the underlying system or platform to map concurrency to the actor abstraction. In thread-based environments like Java's virtual machine, we are forced to execute the relatively lightweight actor constructs on top of the relatively heavy JVM threads. One implementation for JVM threads is the direct mapping of a thread to an OS process. In this case, each actor is executed as a system process. Haller & Odersky [@Hal09] call this an *impedance mismatch* between message passing (event-based) and thread-based programming. In this concrete example, a runtime can mitigate the negative impact by not assigning one dedicated thread per actor. Instead, the runtime can employ a scheduling strategy similar to operating systems. With scheduling, many actors share the resources that fewer threads provide [@Kar09]. Numerous actor system implementations do exist. All diverge in term of features and model semantics realization. We have identified three that merit special attention: * Erlang : A programming language dedicated to actor-based programming is *Erlang* [@Arm93;@Vin07]. It is well-known for introducing the programming with actors to a broader industrial application. Ericsson first used Erlang in 1986 to build telecommunication infra\-structure. In contrast to most programming languages, an Erlang program has the main focus on its so-called *process* constructs (actors), rather than e.g.\ objects or functions. Erlang is designed to meet challenges like high concurrency, availability, fault-tolerance and live-upgrades [@Vin07;@Kos16]. * Akka : Released in 2009, *Akka* [@LightbendAkka] is the most important actor framework for the JVM today. It offers bindings for the Java and Scala programming languages. Akka is highly inspired by Erlang and facilitates many of the same conceptualities in order to meet similar concerns. Examples are scalability, fault tolerance and remoting/distribution. As a library, Akka faces conceptual difficulties which endanger the actor model semantics. Ecosystems dedicated to the actor model typically avoid these dangers, as does Erlang with its virtual machine, the *BEAM*. Section [#ch-actor-impl] concerns Akka and the challenges that the JVM presents as the target platform in more detail. * Orleans : A recent variant of an object-oriented interpretation of actors called *active objects* is *Orleans* [@Ber14]. Microsoft Research constructed Orleans in 2011 to meet the requirements of highly distributed deployment setups, currently referred to as *cloud computing*, on the .NET platform. Orleans facilitates what it calls the *virtual actor model*. A virtual actor (called a *grain*) does not exist *physically* as an entity all the time. The actor runtime only (re-)instantiates a grain on demand automatically. In contrast to most other actor variants including Erlang and Akka, this omits the need for lifecycle management. The *virtuality* characteristic turned out to be more suitable in high-scale dynamic workloads of today's cloud computing deployment setups. To our knowledge, Erlang, Akka and Orleans have the most significance in industrial applications. In the remainder of this thesis, we refer to individual characteristics of all three actor variants to point out relevant differences and noteworthy capabilities. ## Active Objects {#sec-active-objects} As we pointed out, actor systems often aim to provide a more high-level interface than the mere basic primitives to express concurrency. One specific way to realize a higher level is through the concept of *objects* we know from the *object-oriented programming* (OOP) paradigm. Objects encapsulate state and offer operations on this data [@Agh90; @Weg90]. In the terminology of the Smalltalk programming language, we invoke an operation by sending a so-called *message* to an object. This terminology already points out the conceptual resemblance between actors and objects in general [@Sco06]. Yonezawa *et al.*\ [@Yon86] were the first to introduce classic *passive* objects extended by their own *thread of control* in a programming language they call ABCL/1[^fn-abcl1]. The state of an object in ABCL/1 is only accessible from within the object itself. We access or modify the state through the invocation of public interface methods of the object. However, the objects are not idle by default and only perform operations when we call their methods. ABCL/1 objects are *active* on their own, since they live in their own thread. Hence comes their name: *active objects* (AO). When an active object's method is invoked, the actual method execution is decoupled and performed concurrently. A *proxy object* realizes the method invocation on the client side (invoker). The proxy merely mirrors the AO's public interface and handles the message dispatch. The actual computation runs inside a server object on a separate thread [@Lav95]. Meyer [@Mey97] points out one general notion of the concurrency conception of active objects that emphasizes a viewpoint which we pay more attention to in due course: > "Each object is a single, identifiable process-like entity (not unlike a Unix process) with state and behavior." [^fn-abcl1]: __A__ctor-__B__ased __C__oncurrent __L__anguage. The */1* indicates that it is merely the first of a whole family of languages. We have found it often omitted in the literature. Consecutive versions do not follow a sequential numbering, e.g.\ *ABCL/R*, *ABCL/f* or *ABCL/c+*. AOs aim for a purely object-oriented version of actors. Scholars have argued that actors themselves already represent the very essence of object-orientation [@Agh90]. There is a decade-old debated about the fundamental concepts of object-orientation. The author of this thesis came to the conclusion that the only truly undisputed characteristic seems to be the encapsulation of state [@Kni96] coupled to a set of operations which share this data [@Weg90]. Additionally, actors share object concepts like the ability to be created, having a unique identity (address) and a public interface [@Sny93]. As a result, it has often been argued that either actors are convenient for the foundation of active objects, or that AOs are suitable to implement actors [@Lav95]. It is worth to point out that due to this similarity of the actor construct with the object essence, scholars do not use the terminologies consistently. We have found that the literature often uses *active object* interchangeably with the *actor* term. In this thesis, we use *actor* to refer to Hewitt's concept that Agha later refined. Subsequently, we use *active object* for the object pattern of Yonezawa to abstract the actor semantics into a classic object API. The following example illustrates the subtle difference in the behavior of classic versus active objects: ``` {language:java} class Fnord { private int a = 1; void add(int b) { a += b; } int get() { return a; } } ``` Using the above class `Fnord`{language:java}, we create the following simple procedure: ``` {language:java} 1 final Fnord f = ...; // obtain reference to an instance 2 f.add(1); 3 print(f.get()); ``` In a single-threaded program, once the execution reaches line 3, we can safely assume that the addition finished and the internal field `a` has the new value. The `get()` call subsequently results in `2`. Now we alter the definition to `class Fnord extends ActiveObject`{language:java} with some arbitrary base class `ActiveObject`{language:java} that turns `Fnord`{language:java} into an active object implementation. Then, the previous observation does not hold anymore. When the execution reaches line 3, we have no guarantee that the addition was already executed. Line 2 only dispatched the message and returned to leave the activity to its own flow of execution. Line 3 then blocks (because `get()` has a return value) and waits until we receive an answer. But we cannot assume that we receive the value `2` anymore. The active object can receive other messages and process them between our `add(1)` and `get()` messages. The nondeterminism we introduce through the concurrent behavior hinders us to safely reason about our result value. We see, although active and passive objects offer the same interface, they do not provide the same degree of safety. The author of this thesis believes that this safety mismatch is dangerous for programmers in general. The classic actor variants regarded by the author (Erlang, Akka) do not provide interfaces that we can mistake for non-concurrent entities. Therefore, these actors do not offer us a false sense of safety. However, there is one major benefit of active objects compared to classic actors. The object interface provides *type safety*. Messages to AOs are strongly typed [@Lav95]. In contrast, we can send arbitrary types of messages to ordinary actors. Only when an actor processes a message at runtime, it decides whether the behavior is actually able to understand the message type. Actors therefore perform *dynamic type checking* at runtime -- even in statically typed programming languages. On the other hand, active objects provide ordinary object-like interfaces. We send a message to an AO when we call a method of the object with a fixed signature (the proxy respectively). We are only able to call the methods of the object's public interface. A compiler statically ensures the message validity at compile time. Due to the nature of AO interfaces, they only provide message passing in a point-to-point communication style. Broadcasting messages hypothetically requires one method call to address several objects. This behavior is not intended by the object abstraction [@Yon86]. The active object method signatures do not only define communication with a certain degree of static type safety. Every signature also influences the behavior of an object's thread of control. Therefore, signatures constrain synchronization [@Lav95]. In the original work of Yonezawa *et al.*, they introduce multiple types of message passing for method invocation [@Yon86; @Kos16]: * Past Type : The message is dispatched and the sending object's thread of control immediately continues. The thread does not wait until the message has been processed by the receiver. This behavior is equal to message passing in the classic actor model. * Now Type : The message is dispatched and the sender waits for a result. Its thread of control blocks until the receiver processed the message and replies with the result. This behavior is equal to a method call (with a return value) on passive objects. ~ TexOnly &pagebreak; ~ * Future Type : The sender's thread of control immediately returns with a reference to the result that will be available at some point in the future. The actual result becomes available once the receiving object has processed the message and replies with a result. The example above illustrates two of these behaviors for method invocations on active objects. `void add(int)`{language:java} only dispatches a message and immediately returns (past type). In contrast, `int get(void)`{language:java} actually waits for a result (now type). Using the future type requires us to include an additional model of concurrency, the *future*. ## Integration of other Concurrency Abstractions {#sec-actor-concurrency-combination} The actor model is a mature, general purpose model for expressing concurrent computation. It has some clear principles which we must uphold in order to ensure the intended semantics. Besides these principles, the model does not make additional assumptions and restrictions. This makes actors flexible and applicable for general purpose computation. Concurrency, or the suitability for it, is basically an inherent side-effect. As a result, we can combine the model with additional, arbitrary approaches to express computation. Even concurrent models are possible, as long as every concept we introduce does not jeopardize the actor semantics. As a result, mixing actors with other forms of concurrency was always common. The reasons for introducing additional abstractions are manifold. Tasharofi *et al.*\ [@Tas13] empirically found that programmers think that the major inadequacies of actor systems are their lack of efficient support for blocking operations (especially IO). Also, many communication protocols are hard to express in an asynchronous messaging style. In order to overcome these shortcomings, actor systems interweave with additional concurrency models. We come back and take a closer look at the two deficiencies -- efficient IO and communication styles -- in Section [#ch-actor-impl]. Before, we must know the requirements for two concurrency models to be *composable* without inconsistencies. Swalens *et al.*\ [@Swa14] regard two concurrency models as composable if their integration does not result in new effects regarding their *safety* and *liveness* that have not been there before. The isolated turn principle of the actor model already gives a strong boundary to ensure these properties [@Kos16]. Added concurrency concepts must neither weaken these boundaries nor the model properties. Especially, the introduction of low-level data races is very easy for new abstractions and we must therefore carefully avoid any race conditions [@Tas13]. ### Futures The traditional notion of a procedure call is that the execution flow only continues once the invoked computation has finished. Of course, the procedure we call can dispatch a concurrent execution and return without a result. The flow of execution then resumes before the computation we called executes. However, if the procedure provides a return value, we expect the control flow to *block* until the respective value is available [@Tan07]. In many cases however, we do not immediately require the result for the subsequent computation. The control flow is able to continue without accessing the value for some time. Therefore, it is possible to resume the caller's activity, while the called procedure executes concurrently in a separate thread of control. The procedure initially returns a simple placeholder that will contain the actual result value at some point in the future [@Wel05; @Fla99]. We use such values in a *semi-synchronous* fashion. The value calculation runs *asynchronously* in general. The calling and the called thread once again *synchronize* when we access the placeholder for the actual result. We also call this *touching* or *claiming* the value. Attempting to access a placeholder expands to blocking the current control flow if the result is not yet available [@Wel05; @Tas13]. The literature does not use a uniform name for the concept of eventually retrievable values. Baker & Hewitt [@Bak77] describe the concept of a *future* which delivers the result of an expression eventually. Liskov & Shrira [@Lis88] extend this idea and introduce a data type they call *promise* for result values that we single-assign at some point in the future. More seldomly have we found the terms *eventual*, *delay* or *deferred* [@Pra18]. *Call-by-future* [@Bak77] or *call-by-need* [@Agh85a] express the kind of evaluation order of these concepts. Some programming languages, among them Java and Scala, have a special view on eventual values. These languages support both `Future`{language:java}s as well as `Promise`{language:java}s. A `Future`{language:java} represents a read-only container we use as the placeholder for an eventually available value. In contrast, a `Promise`{language:java} is a single-assignment variable we use to explicitly set a value at *some* point in time, i.e.\ to complete a `Future`{language:java}[^fn-java-promise]. In other words, a `Future`{language:java} refers to a `Promise`{language:java} that we keep eventually [@Hal18; @Pra18]. Though today we find all designations interchangeably used and they refer to roughly the same idea [@Swa14], we confine to the term *future*. Java and Scala use this name and we rely on the specific future semantics of these two languages in due course. [^fn-java-promise]: Therefore Java calls it `CompletableFuture`{language:java}, instead of `Promise`{language:java} as Scala does. There is a long tradition of combining actors with futures. Agha [@Agh85a] describes that actors often model call-by-need computation, which is essentially a future. We also trace future combination back to ABCL/1 and its active object notion [@Yon86]. We have already discussed three kinds of message passing for AOs. The example then merely demonstrated two (past type and now type). The third, coincidentally called *future type*, is actually the result of combining actor concurrency semantics with the future concurrency abstraction [@Tas13]. Orleans uses promises/futures as the only form of method calls for all active objects [@Ber14]. For a complete formal definition of future semantics we refer the interested reader to Flanagan & Felleisen [@Fla99]. ### Software Transactional Memory The asynchronous messaging style of actors becomes a burden when we need some sort of consensus between several actors. The model does not provide an adequate mechanism to abstract operations involving several messages [@Sub11]. We need an additional high-level model on top of the messages. The *transaction* is a well-known concept to provide a single-threaded appearance to the access of state or memory that is concurrently accessible. A transaction encapsulates a computation that does not have to be atomic by itself (e.g.\ code block). The effects of the computation still logically appear to happen within a single instant in time [@Les09]. Therefore, all memory modifications done inside the transaction become atomic from the outside perspective. If a transaction becomes invalid, the transaction roles back all state modifications across the entire code segments involved in the transaction. Write collisions are one reason for transactions to become invalid. Upon a collision, the transaction cannot guarantee the isolation anymore [@Sub11]. *Software transactional memory* (STM) refers to transactional semantics realized in software[^fn-hardware-transaction]. In the scope of this thesis, STM is the only considered transaction mechanism. [^fn-hardware-transaction]: Originally, the concept was proposed for supporting transactions in functional languages, but in hardware. Hence the distinction. Combining transactions with actors can have a huge impact on performance. Especially write collisions raise the amount of coordination we require. Though all required coordination can happen transparently through an actor system, it always means additional message processing for the involved actors, which potentially turns into a bottleneck [@Sub11]. # Microservice Paradigm {#ch-microservice-paradigm} ~ Epigraph { caption: "Groucho Marx"} These are my principles. If you don't like them, I have others. ~ *Microservice architecture* (MSA) is a recent software architecture style for designing software applications as systems of loosely coupled, independently deployable, single purpose services [@Fow14]. In this chapter, we outline the motivation behind microservice architectures, the component properties and the resulting concurrent nature, problems arising from the terminology, and the service programming model. In contrast to a monolithic application, for a microservice architecture we split the application logic into a suite of small parts. We implement each part as a dedicated program and design it to provide a single task of the business logic. We call these programs, which pose as the application's components, *microservices* (MS). The microservice style is open for every programming language and paradigm. All services communicate with message passing semantics on the OS or network level. Therefore, we can conceive the various services in different programming languages and technologies. As long as every microservice exposes the interface that the architecture requires, the service is a suitable component [@Dra17a;@Mon16a]. Unlike the actor model, microservices were not invented as a specific model of computation. Instead, the microservice paradigm emerged from the industrial need to break the scalability barrier that monolithic[^fn-monolith] applications inevitably reach [@Bon17]. Only later did the scientific community gain interest in this paradigm. The consequence was an explosion of contributions on this concept in recent years. Yet, academia still has some troubles to settle on a common basic definition of the essential concepts which determine this paradigm [@Has16]. Fowler & Lewis [@Fow14] give the seminal review of the microservice concept. Their work is therefore the original source most scholars refer to. But Fowler & Lewis focus on the common characteristics of microservices from a more practical engineering perspective. We refer to Dragoni *et al.*\ [@Dra17a] for further reading as well, since they give an extensive conceptual description. An overview of the publication trends is given in [@Fra17b]. [^fn-monolith]: The term originates from the *monolithic kernel* style of operating system architectures. Such is a sole binary running in kernel mode and providing the process- and memory management, file system, etc. ## Limits of Centralization Historically, software systems are implemented in a so-called *monolithic* way. All the source code compiles into one single, central artifact that we execute on one machine. This centralization originates from the level of abstraction mainstream programming languages provide to break down the complexity of the programming task. The general term for these abstractions is the *module*, and they allow us to logically separate concerns. Yet, the transformation of modules from program code to machine code leads to a result where all modules merge into one unified construct: the (monolithic) *executable* [@Sco06]. By inversion of argument, a monolith is an application of modules that we cannot execute independently [@Dra17a]. Modules naturally introduce a relatively strong form of coupling. In-memory call communication is a cheap and direct way to address components and we therefore apply it heavily within monoliths and their modules [@Bon17]. Every application that is subject to the tight coupling of its modules suffers from certain issues [@Sal16;@Dra17c;@Dra17a]: * The components are less reusable. * Increased interleaving becomes hard to maintain. * We need to upgrade all modules simultaneously and are limited in the technologies we can use. * Evolution is generally hindered due to the growing complexity. Most importantly, the main argument against monoliths is scalability [@Dra17a]. Each single instantiation of a program executable is intrinsically only able to run on a single machine. Hence, there is a natural upper bound to the application's performance due to the hardware limitations. Many approaches to overcome this limit(s) were proposed over the years. Previously, the *service-oriented architecture* (SOA) approach gained popularity. SOA builds on the idea of combining the capabilities of multiple monoliths -- either of the same or different program images -- and integrating them through a uniform communication channel like an *enterprise-service bus* [@Fow14;@Sal16]. This approach allows us to link heterogenous technologies and enables independent deployment, since we do not require in-memory calls between these services anymore. However, SOA still facilitates large monolithic applications which are only able to scale through duplicated instances of the entire application [@Dra17b]. In order to overcome these limitations, the microservice paradigm aims for a separation of the modules into small service programs. SOAs are called the *first generation* services, while microservices are subsequently the *second generation* of services. Because microservices evolve from SOA, they are also part of the general *service-oriented computing* (SOC) paradigm [@Maz16;@Dra17a]. For an in depth review on how microservices historically emerged from a distributed systems perspective, starting with the client-server paradigm, to mobile agents technology and service-oriented architectures, we refer the interested reader to Salah *et al.*\ [@Sal16]. ## Term Ambiguity The literature uses the *service* term in an overloaded manner. We identify two general meanings attributed to the term. On the one hand, the term refers to a computational task unit, i.e.\ a process, as part of a service-oriented architecture. This is the predominant intention when authors refer to a *microservice*. On the other hand, a service also describes a specific functionality that we can utilize through an interface. We know this notion from object-orientation, where objects provide services in the form of procedures through their public interface [@Sny93]. In order to avoid confusion, some scholars propose a clear distinction. For example, Xu *et al.*\ [@Xu16] use the term *service* exclusively for the functionality part and refer to the component as *agent*. We follow the example of Guidi & Montesi [@Gui09] and distinguish between the *service engine*, that is the component we deploy as a process, and the *service behavior* for the functionality that the service engine offers. This terminology suits us, because it highlights the resemblance between an actor's behavior and a microservice's behavior. ## Independence and Interaction From the separation of modules into dedicated service engines comes a high cohesion within the modules as well as a loose coupling among them. As a result, microservices are highly independent [@Dra17a;@Dra17b;@Gui17]. The inherent fact that each service engine is a separate application implies that the engines are independently deployable [@Fow14]. The decoupling of services also affects their state, since the state becomes conceptually isolated. Therefore, we require that services refrain from sharing any resources related to memory persistence. A database for example introduces a notion of implicit communication among all the services with access to this database. An essential principle of the microservice style is therefore the commitment to provide every service engine with its own exclusive database instance(s) [@Fow14;@Dra17a;@New15]. Consequently, all communication between microservices happens across the boundaries of the service engine processes. We already know this concept from Section [#sec-concurrency-os-level] as inter-process communication. Various forms of IPC channels exist. Example mechanisms are a shared memory section between two processes within the same operating system, or Unix pipes. The microservice idiom specifies the following requirements on service engine IPCs [@Fow14]: 1. Communication channels should be open and provide well-defined interfaces, such that heterogenous technologies are able to use them. 2. Communication channels should be lightweight, such that they are cheap mechanisms without much additional functionality besides basic message transportation. 3. Communication channels should only act as message routers, such that they transport immutable messages and do not apply data processing logic on their own. From the two example IPC mechanisms above, Unix pipes and shared memory, only the pipes qualify for a valid microservice communication mechanism. Data in the form of text strings represents serialized state information of a service. The pipe transports this data in an immutable way between the endpoints of the pipe. Raymond describes this in his Unix *rule of composition* as [@Ray03]: > "Text streams are to Unix tools as messages are to objects in an object-oriented setting. The simplicity of the text-stream interface enforces the encapsulation of the tools." This satisfies the channel requirements given above. Therefore, pipes are a valid communication mechanism. However, pipes do not offer a specific structuring of the byte stream. Programmers have to specify an application-level protocol on top of the pipe mechanism [@Bac03]. Shared memory, on the other hand, faces several conceptional problems regarding a microservice communication mechanism. Services would send messages by modifying memory both services have access to. Yet the memory is not necessarily exclusive to both services. A third party that also has access to the memory can intercept a message by getting a lock to the shared state before the intended recipient. This third party is subsequently able to modify the message. Shared memory does not guarantee the delivery of the original message itself. We require that the synchronization mechanism enforces the semantically correct order of state access. This risk to correct message delivery is another argument why microservices do not use shared state communication. ## Concurrent and Distributed Building Blocks As independently deployable applications, each microservice is by design a dedicated process. These are inherently concurrent on the operating system level and also facilitate parallelization on multiple cores transparently. Every communication mechanism must be able to send data across the distinct address spaces which strictly isolate the states. Recall the case study of Section [#sec-concurrency-os-level] on system process programming in [C.]{tex-cmd-after:"\,"} Fork/pipe-based applications utilize Unix pipes to cross memory boundaries. Like microservices, their components execute concurrently through the process scheduling of the OS. It is therefore worth to debate whether these systems qualify as microservice architectures. The fork pattern spawns processes in a tree-like fashion. All components rely on a shared ancestor. Every child can replace itself by an arbitrary other program image using `exec`. However, the setup of the communication routes relies on an instantiation of the pipe *before* the fork. Only then have both the parent and the child access to the pipe. This fact limits the possibility to take down or add new components independently. Hence, pipes are rather restrictive and present a certain degree of coupling [@Bac03]. It is difficult to replace the system's components independently. To overcome this restriction, more modern Unix variants introduce the concept of *named pipes*. This kind of IPC allows processes to hook into a common pipe without a shared ancestor [@Bac03]. However, every communication route we fix at compile time -- generally called *static channel naming* -- limits the ability for changing topologies [@And83]. In any case, the pipe mechanism definitely does not provide communication outside the boundary of the common operating system. Pipes therefore limit scalability since they restrict the service architecture onto a single host machine -- at least for the subset of services that facilitates this mechanism. Network-based IPC mechanisms overcome this restriction inherently and we subsequently prefer them for MSAs in general. Network IPCs provide higher degrees of freedom regarding deployment and heterogenous technology integration at the price of more costly data transfers (cf.\ Fallacy 7: *transport cost is zero*) [@Cou05]. As a result, we always assume that microservices are concurrent components which support parallel execution. However, whether they qualify as distributed components within their respective architecture is subject to the communication mechanism(s). ## Size, Scope and Granularity {#sec-ms-granularity} Bonér [@Bon17] criticizes the term *micro* since it encourages us to debate the actual *size* of a service. Every size definition requires a metric for comparison. Only with a metric can we debate up to which limit we call an application a *micro*service. Therefore, developers focus on metrics like *lines of code* up to more obscure ones, e.g.\ the reported *two pizza team* size, where a service is written and maintained by a team that we can feed with two pizzas[^fn-two-pizza-team] [@Fow14]. These discussions are irrelevant. [^fn-two-pizza-team]: Assuming an arbitrary pizza size, this either suggests very small teams, or really big pizzas. Instead, Bonér argues, a notion of size should refer to the *scope of responsibility*. A guideline towards this is once again the Unix philosophy [@Ray03] we already referred to. The philosophy suggests that programs should have a well-defined and limited scope. We realize more complex functionality by composing multiple of these simple programs. This concept is also found in object-oriented design, where we know it as the *single responsibility principle* of objects [@Gui17]. In the microservice context however, authors do not refer to the single responsibility principle a lot. Instead, they tend to phrase it *bounded context* [@Dra17a;@Dra17c;@Maz16]. Authors argue that services should be an aggregation of functionality around a single business capability. This divergence of granularity is one of the major evolutionary changes from SOA to microservices [@Mon16a;@Gui17]. However, a too-fine granularity becomes an issue. In a distributed setting, granularity is always a balance between the number of messages that we send versus the performance implications we expect by every message. We must consider latencies of network channels, processing time of addressed services, and delay penalties that result from service unavailability when we design the granularity of microservices [@Sha17a]. In general, we expect Fallacies 1-3, 5 and 7 to contribute to bounded context considerations. ## Service-oriented Programming Until now, we have regarded microservices in the light of a software architectural style. Within this context, the building blocks are all truly isolated components we instantiate from independently executable artifacts. We merely link the artifacts through rather loosely coupled message passing communication channels. Scholars argue that due to these characteristics the perspective in the literature (both academic as well as industrial) has a focus on the *deployment*, which is the operation of a service engine [@Gui17;@Xu16]. After all, the deployment context of MSA is the origin of the concurrent nature, that is independent processes of the OS or network. In this section however, we favor an argumentation towards a linguistic viewpoint on microservices as a programming model instead. Programming paradigms build upon respective conceptual constructs, e.g.\ objects in OOP or functions in *functional programming*. In more recent time, a new paradigm called *service-oriented programming* (SOP) [@Gui09;@Mon14] emerged. It builds upon the service-oriented computing approach that SOA and subsequently microservices emphasize and introduces it into the design of programming languages. Services become first-class entities of the language and are the smallest programmable constructs. Instead of a single executable artifact, service-oriented programs compile into multiple executables, one for each of the service constructs in the source code. Initially, the conception of SOP aimed for an evolutionary step. The idea was to combine the object-oriented notion with the SOA paradigm to program distributed systems. When the microservices principles finally distilled, it transpired that the compilation of SOP languages essentially produces microservice architectures. Instead of the total technology freedom of the mainstream MS paradigm, SOP languages are a separate and more restricted approach towards the microservice style [@Mon14;@Gui17]. Various prototype languages facilitate the service-oriented programming model. Two SOP languages merit attention: * CAOPLE : One attempt of a service-oriented programming language is *CAOPLE* [@Xu16]. It calls its basic programming constructs *agents* (microservices). Agents provide a very strict notion of state encapsulation, autonomy, and well-defined communication interfaces. Unlike traditional microservices, agents do not execute directly on the host's OS, but run on a dedicated virtual machine called *CAVM-2* instead. CAOPLE's VM focuses on providing a lightweight dynamic deployment of services compared to container technology. Additionally, the VM is optimized for running large quantities of services in a lightweight manner, as well as abstracting the network distribution of agents across host machines. * Jolie : Currently the most advanced and scientifically best described service-oriented language is *Jolie* [@Mon14;@Mon16b;@Gui17;@Min17;@Gui09]. We therefore use Jolie as the primary linguistic reference throughout the remainder of this thesis. A Jolie program defines services which describe two basic aspects. First, the *behavior* expresses all functionality that the service offers. The behavior makes this functionality available on the so-called *communication ports*. Second, the *deployment* describes how a service is used, i.e.\ the communication technology, addresses of the exposed functionality, and data protocols. These two main parts of every Jolie program, behavior and deployment, indicate the strong *separation of concerns* between what we designate service behavior and service engine. Even the root level syntax expresses this separation in the program structure: : ~ center `Program \(void|::=\) D \(return|main\) { B }`{language:"java"; class:"pretty"} ~ : The language's syntactic rules prevent us from introducing the deployment expression (`D`{.pretty}) into a behavioral expression (`B`{.pretty}) and vice versa. The only connection between behavior and deployment are the communication ports. The behavior abstractly uses the communication ports, and the deployment concretely defines the ports. Hence, behavior and deployment are complemental. As a scientific prototype language, Jolie incorporates many interesting concepts. There is only an implicit notion of concurrency from the service execution as OS processes, and the `concurrent`{color:blue} primitive as one option for the so-called *execution modality*. In this case, upon receiving a message on a communication port, a Jolie service spawns a dedicated process such that the behavior executes in a separate local memory space (cf.\ the fork approach in the C processes case study of Section [#sec-concurrency-os-level]). The primitive hereby allows concurrent message processing. Among other concepts, Jolie supports complex message routing through *correlation sets*, and facilitates transparent delegation through the so-called *aggregation* primitive, which extends a service's interface with the interfaces of other services. Microservices share many similarities with objects in general [@Dra17a]. Therefore, service-oriented languages tend to have many analogies to object-oriented languages, as the computational units in both paradigms provide functionality via a public interface [@Xu16;@Sny93]. However, this marks also the most important difference to objects. In general, objects facilitate information hiding and encapsulation in a *shared memory* setup. Microservices on the other hand solely rely on *message passing* [@Dra17a]. Besides this difference, advanced object-oriented concepts can also be part of a language's service constructs. For example, Jolie has static type checking for service interfaces [@Min17], and CAOPLE even supports polymorphism via an inheritance mechanism. # Implementation {#ch-implementation} ~ Epigraph { caption: "Eric S. Raymond"} Every good work of software starts by scratching a developer's personal itch. ~ In this chapter, we cover the practical aspects of programming with actors and microservices. Section [#sec-scenario] describes a scenario for a concurrent system. Section [#ch-actor-impl] covers the strategies we apply to implement this scenario using the actor model. Subsequently, Section [#ch-microservice-impl] describes the implementation of the scenario using the microservice model. ## Concurrent System Scenario {#sec-scenario} In this section, we outline a domain-specific search engine we call *Echo*[^fn-echo-name]. This search engine is our non-trivial scenario of a concurrent system that serves us as the reference for evaluating the programming of concurrent systems with actors and microservices. [^fn-echo-name]: We chose the name "Echo" for its wonderful characteristics of providing a short package name and the analogy to *recalling spoken words*. Search engines are a composition of rather loosely coupled and independent subsystems [@Cou05]. Users interact with a search engine by submitting search requests in the form of so-called *queries*. The search engine then presents respective results to the user. This functionality however is merely the so-called retrieval phase performed by the *retrieval subsystem*. As the name indicates, this phase retrieves information. By implication, the information must have been collected and stored beforehand. A second so-called *indexing subsystem* is responsible for gathering the information and storing it in a form that is optimized for searching, the so-called *reverse index* [@Lil09;@Ped06]. The reverse index maps a *document-term* relationship -- where documents are arbitrary text collections -- into a *term-document* structure [@Man08]. Several factors contribute to the fact that search engine architectures are suitable for concurrent programming research. First, both subsystems are mostly independent. They merely make use of a common information index, where the indexing subsystem is exclusively adding information and the retrieval subsystem is exclusively reading the information. Hence, the subsystems are independent and can run concurrently. Second, since many kinds of search engines regard very large amounts of data, their construction was always led by the effort to leverage concurrency in order to improve their scalability. Especially the parallel and distributed computing research merits attention to search engines, for example to explore cluster architectures [@Ped06;@Man08]. Additionally, our specific domain we outline below is also very suitable for concurrent processing. The design of search engine architectures is generally led by two basic requirements [@Man08]: * Effectiveness : The quality of search results is the *effectiveness* of a search engine. Effectiveness is the sole concern of the scientific discipline called *information retrieval* (IR). *Precision* and *recall* are the two metrics that IR defines in order to assess the effectiveness. * Efficiency : Factors like the response time and the throughput determine the *efficiency* of a search engine. These factors are highly affected by the concurrent processing capabilities of the system. The optimization of effectiveness is not within the scope of this thesis. We merely apply a basic scoring method of the utilized information retrieval library. Our sole goal is to increase the efficiency of the system by leveraging concurrent programming techniques. ### Domain Description We build our domain-specific search engine for the *podcast* domain. On the one hand, the term refers to content, that is an episodic series of digital media. The media is usually audio, more seldomly video. On the other hand, *podcast* can also refer to the distribution mechanism. The distribution builds upon XML (E__x__tensible __M__arkup __L__anguage) web feeds. RSS 2.0 [@Win03] (__R__ich __S__ite __S__ummary) and Atom [@RFC4287] are the established syndication formats. Since RSS 2.0 has always been the more dominant format, we simply refer to *RSS feeds* from here on. Both formats gained popularity in the 2000s as an effective, decentralized mechanism to publish the updates to a website's content. Podcasts build upon the same principle. Yet they utilize an otherwise optional field for items of an RSS feed, the `` tag. This tag provides an URL (__U__niform __R__esource __L__ocator) to the media file. Subscribers of the feed download the file behind the URL and watch/listen to the media, usually through a specialized software application. The `` is therefore the main content of each item in a podcast RSS feed. Additionally, there are other fields within the feed. Some of these fields contain human readable information about the linked media file, so-called *metadata* [@Pat06]. Appendix [#apx-feed-structure-example] gives an example RSS feed structure with dummy metadata. Our search engine is designed to regularly analyze RSS feeds of podcasts. The metadata allows us to add information for every media file to the search index. Although we do not analyze the media itself, we can still provide search functionality based on the metadata information. The domain is very suitable for concurrent processing, since the RSS feeds are decentralized. Every podcast content creator is publishing a separate feed. There is no interrelation between feeds. We can process each feed separately and therefore concurrently. ### System Components At the core, our basic architecture and the components are inspired by the work of Brin & Page [@Brin98] on large scale web search engine "anatomy", as well as more modern interpretations of associated design principles given in [@Cou05] and [@Ped06]. The two high-level subsystems we have given above, indexing and retrieval, are internally composed of several smaller components. We specify that each of these components has to be a concurrent task unit of the programming model, i.e.\ an actor or a microservice. The respective units are: * CatalogStore (C) : holds a catalog of all metadata information we gather about podcasts, their feeds and episodes. The Store persists this information in a relational database. * IndexStore (I) : holds the data structure we use for searching (reverse index). Registered information entities are called *documents*. Each document relates to one podcast or episode. The IndexStore documents are merely the part of the metadata we need to match search queries to matching results. * Web Crawler (W) : acquires the information that the search engine stores by downloading data from URLs. These URLs relate to feed files. * Parser (P) : transforms the XML data into internal representation formats. This extracted data is what we consider when running search queries and subsequently display in the Web application. * Searcher (S) : performs the search operations. This component applies some basic query pre-processing and delegates the retrieval of relevant documents to the IndexStore with its inverted index. The Searcher communicates the results from the IndexStore back to the Gateway. * Gateway (G) : provides the link between the Web UI and the system-internal capabilities. The Gateway exposes a REST interface to the outside and uses respective mechanisms to interact with other internal system components. The REST interface allows us to request cataloged information and perform searches. * Updater (U) : determines which feeds to re-download next in order to register new episodes, and update existing metadata. The CatalogStore and the IndexStore are stateful, all other task units are stateless. The complete search engine architecture is the composition of all these components according to the interaction model shown in Figure [#fig-task-units]. ~ Figure { #fig-task-units; \ caption: "Complete interaction model of the task units in the Echo search engine"; \ width:100%; page-align:here; } ![img-task-units] ~ [img-task-units]: graphics/interaction-model.[svg,png] "Image about task units and message flow" { height:4.2cm; vertical-align:middle; padding-bottom:1em; padding-top:0; margin-top:0; } When we give some basic dataflow examples in due course, we use the following shorthand notation for arbitrary components `X` and `Y`, where `X` and `Y` get substituted by the component abbreviations (`C`, `I`, `W`, `P`, `S`, `G`, `U`). `X` → `Y` expresses `X` sending a message to `Y` (push). `X` ← `Y` denotes `X` fetching a message from `Y` (pull). `X` ⇄ `Y` is short for `X` sending a request message to `Y` with a direct response (synchronous remote procedure call, RPC). The system shown in Figure [#fig-task-units] merely forms the concurrent indexing and retrieval system. It is therefore a backend application only. In order to actually use the search engine, we provide the backend with a web-based user interface (UI). This *Web* application is based on the Angular [@GoogleAngular] framework. The actor and microservice implementations of the backend have to provide a REST interface within the Gateway component to allow interaction from the outside. The Web UI serves us as the proof of concept for the desired functionality of the engine's backend implementations. Since our scientific focus is on the concurrent programming aspect and not the information retrieval aspect, we want to implement the domain-specific logic only once. Therefore, we provide each backend with a common *Core* library written in Java. The Core offers most domain-specific functionality, so that each backend codebase can focus on the concurrent execution and interaction. For example, the actual searching is done through a specialized data structure, the reverse index. We use Lucene [@ApacheLucene] to create this structure. Lucene offers a Java interface that is interoperable with most JVM-based programming languages. RSS/Atom feed parsing is done using ROME [@ROME], enriched by an extension we wrote to support additional *Simple Chapter* [@PodloveSimpleChapters] metadata information. ### Processing Pipelines {#sec-subsystem-pipelines} In this section, we give a brief outline of the data processing pipelines which make up the indexing and the retrieval subsystem. The processing pipelines are the result of the composition of the architecture components. Note that Figure [#fig-task-units] shows an interaction between the Gateway and the CatalogStore. The pipelines below do not mention this interaction. The Web UI can display the entire metadata of an item. Therefore we must retrieve the complete metadata from the CatalogStore. For search requests, the retrieval subsystem merely produces the reduced metadata that is stored in the search index. We nevertheless show the `G` ⇄ `C` call for completeness. #### Indexing Pipeline We process feeds either when they are new to us (initial indexing), or to check for new episodes (update). Hence, there are two cases when the indexing pipeline gets triggered. Either we add a new feed, or the Updater determines that a feed requires a check for new episodes. In order to determine which feeds require an update, the Updater regularly inquires the database of the CatalogStore. The Updater passes the update candidates to the Web Crawler. The Crawler retrieves the XML data of the feed via HTTP. Then the Crawler passes the raw feed data to the Parser. The Parser extracts the podcast and episodes metadata from the XML into domain objects. The Parser forwards all metadata objects to the CatalogStore. The database of the Store persists the complete metadata. The Catalog also sends the search-relevant part[^fn-relevant-metadata] of the metadata to the IndexStore, which adds the data to the Lucene reverse index data structure. The overall flow is: `U` → `C` → `U` → `W` → `P` → `C` → `I` [^fn-relevant-metadata]: Some parts of the metadata, like the byte size or MIME type of the `` file, is important to determine new entries. Therefore, the CatalogStore persist this data. This metadata is however hardly relevant for search queries, therefore we do not include it in the search index. ~ Figure { #fig-indexing-pipeline; \ caption: "The indexing pipeline: The Updater (`U`) uses the CatalogStore's (`C`) metadata to determine feeds that require updating (`U` → `C` → `U`). The Web Crawler (`W`) loads the XML from the web, the Parser (`P`) transforms the feed data to domain objects. The CatalogStore persists the data and forwards selected metadata to the IndexStore (`I`)"; \ width:100%; page-align:here; } ![img-indexing-pipeline] ~ [img-indexing-pipeline]: graphics/pipeline-indexing.[svg,png] "Image about indexing pipeline" { height:3.1cm; vertical-align:middle; padding-top:1em; padding-bottom:1em; } #### Retrieval Pipeline The essential purpose of the engine is search. The Web UI offers an interface similar to well-known search providers on the world wide web. The Gateway registers search requests from the UI on the REST interface and forwards the request to a Searcher (`G` → `S`). This Searcher is doing some basic query processing and then forwards the resulting query to an IndexStore (`S` → `I`). The IndexStore propagates the search results back via the Searcher (`I` → `S`) and the Gateway (`S` → `G`) to the Web [UI]{tex-cmd-after:"\,"}. We require this flow to complete in a timely manner, thus synchronous. The complete flow is: `G` ⇄ `S` ⇄ `I`. ~ Figure { #fig-retrieval-pipeline; \ caption: "The retrieval pipeline: The Gateway (`G`) registers requests, forwards each query to the Searcher (`S`), who retrieves data from the IndexStore (`I`). The respective results travel back from `I` via `S` to `G`"; \ width:100%; page-align:here; } ![img-retrieval-pipeline] ~ [img-retrieval-pipeline]: graphics/pipeline-retrieval.[svg,png] "Image about retrieval pipeline" { height:1.3cm; vertical-align:middle; padding-top:1em; padding-bottom:1em; } ## Actor-based Implementation {#ch-actor-impl} This section covers the strategies we apply when we program with the actor model. We implement the backend of the concurrent system which we outlined in Section [#sec-scenario]. All concepts we discuss are with respect to the specific actor variant we use. The focus is on the linguistic support provided by the actor library. Efficiency considerations are part of Chapter [#ch-evaluation]. It is important to realize that although there is *the* conceptual actor model, there are numerous system implementations available through various forms of interfaces, either integrated into the programming language or as a library [@Kos16]. These systems are all based on the theoretical model, but can choose to compromise some of the semantic properties in order to increase their efficiency [@Kar09]. Such considerations are relevant when we evaluate the linguistic support. We build our Echo implementation with an actor variant called *Akka* [@LightbendAkka]. We have mentioned Akka already in Section [#sec-actor-systems] alongside Erlang and Orleans. The Akka library is available for the JVM through bindings for Java and Scala, but was later ported to other ecosystems such as .NET and JavaScript runtimes (through Scala.JS). The .NET variant (called *Akka.NET* [@AkkaDotNet]) is to our knowledge not able to interweave with the original JVM version at the moment. Because our solution is solely based on the JVM, all following discussions refer to the capabilities of Akka's original variant. Akka theoretically builds on Agha's [@Agh85a] vision of the actor model and harnesses its potential for distributed problem solving [@Kos16]. An archetype has been Erlang. Akka is designed as a toolkit collection consisting of several libraries. We can use these libraries in arbitrary combination based on our actual need of them. The actor runtime system is a lightweight execution environment based on work stealing thread-pools with local task queues which schedule the actor execution [@Hal12]. As of Scala version 2.10, Akka replaces the default actor implementation that Scala originally offered [@Hal12]. We therefore refer to the former as *Scala actors* in contrast to *Akka actors*. Among the reasons were the better performance, transparent actor addresses, expressing resilience as well as fault tolerance [@Tas13]. In fact, [@Reh13] found that Akka actors have up to 10 times higher message throughput and a network latency under 1ms, in contrast to the 0.2 seconds of Scala actors. ### Striving for Isolation {#sec-actor-isolation} While actors encapsulate state conceptually, in practice their *full isolation* must be ensured to avoid accidentally sharing state. This is essential to guarantee the safety properties, that is prevent data races and state modifications [@Hal12;@Kan12]. Akka offers interfaces for Scala and Java. Both languages support object-orientation in an imperative programming style -- even though Scala is primarily a functional programming language. Since Akka is not directly integrated into either of these two languages[^fn-akka-language-integration], it cannot ensure isolation by itself. This restriction is true for most library-based actor systems running on execution environments that support shared-memory multithreading like the JVM [@Kos16]. Therefore it is especially interesting how Akka handles the isolated turn principle, because, as was outlined in Section [#sec-actor-messaging-encapsulation], internal state of an actor must be mutable exclusively from within the actor itself to preserve the model semantics. [^fn-akka-language-integration]: In Scala, Akka is the built-in actor *library*, but not a language feature. #### Issue of Data Hiding Akka's actor runtime provides a transparent interface for component communication which exist either within the same local scope (same JVM) or remote scope (distinct JVMs). In the first case, we must take different notions of state into account. Kniesel [@Kni96] defines *weak state* as the state given through an object's instance variables. *Strong state* is the combination of *local state* (the object's instance variables) and *transient state* (the state of objects referenced by instance variables). Actor semantics implies the need for a strict conception of encapsulation where the strong state is exclusive to the actor. We must not expose mutable local state outside the actor's scope, nor import mutable transient state into the scope of the actor. Violation of this requirement leads to the overlapping (sharing) of mutable state, which is in contrast to the message passing semantics of the model. *Visibility* is a property of the variables and methods of an object which are part of the interface of the object [@Kni96]. Visibility is a concern for encapsulation and subsequently shared mutability [@Sub11; @Goe06]. Java for example offers multiple granularities for visibility of class fields. The following code snippet illustrates the resulting problem: ``` {language:java} public class Foo extends UntypedActor { public String bar; public static Props props() { return Props.create(Foo.class, () -> new Foo()); } @Override public void onReceive(Object msg) { /* handle msg */ } } ``` We extend `UntypedActor`{language:java}, the base class for classic actors which do not provide type-safety for messages. In contrast, Akka's `TypedActor`{language:java} is the base class for active objects, which do provide type-safety for messages [@Sub11]. We give the field `bar` in class `Foo`{language:java} *external visibility* by declaring it `public`{language:java}. The field is therefore part of every object of type `Foo`{language:java} and influences the object's encapsulation [@Kni96]. From visibility follows accessibility, such that `bar` is also accessible from outside the scope of `Foo`{language:java}. Since `bar` is not `final`{language:java}, we can also modify `bar` from outside the object's scope. External modifications violate the requirement on exclusive state mutability of the actor semantics. All Java-based actor implementations therefore face the problem that custom-written actor classes can easily break the required model semantics. In order to cope with this problem, object-oriented implementations can offer us APIs where we do not issue interactions with an actor instance directly through the instance's method interfaces, but instead via proxy constructs like [@Hal12]: ``` {language:java} final ActorRef foo = system.actorOf(Foo.props()); ``` We do not directly create an instance of `Foo`{language:java} using the `new`{language:java} keyword as it is custom in Java. Instead, we use the Akka system's factory method `actorOf` that hides the actual instantiation. The `create` method of `Props`{language:java} takes a Java 8 lambda as an actor object factory. The lambda and the `create` call are commonly wrapped in a `props` method of the actor class. Java lambdas are basically functional closures[^fn-clojure] and only allow us to access effectively `final`{language:java} fields inside the lambda's scope. This restriction prevents us from exposing mutable state to the constructor of an actor class. Of course, this is only true for the `final`{language:java} fields themselves, but not their members (cf.\ Java case study in Section [#sec-concurrency-language-level]). [^fn-clojure]: Not to be confused with *Clojure*, a dialect of the programming language Lisp for the JVM. The actor system only exposes a proxy object of type `ActorRef`{language:java} to the user. An `ActorRef`{language:java} instance does not have the external interface of the actor class it represents. The `ActorRef`{language:java} merely offers a variety of methods for sending messages to its actor. Messages sent through these methods are delivered by the actor system and then consumed by the actor through the `onReceive` method [@Sub11]. The use of `ActorRef`{language:java}s has the benefit that no direct contact with an actor instance object is possible. This lack of contact prevents both visibility and accessibility to any actor object fields or method calls. Additionally, `ActorRef`{language:java} proxies enable *location transparency* [@AkkaActor]. #### References and Immutability Preventing visibility of actor object fields and methods is not sufficient for guaranteeing the required strong state encapsulation on the JVM. The method signature of `onReceive`{language:java} indicates that messages are received with type `Object`{language:java}. Though Java has pass-by-value method parameters, variables with a non-primitive type (all besides `byte`{language:java}, `int`{language:java}, `char`{language:java}, etc.) are actually reference variables storing the address to their objects. A passed-by-value parameter is therefore a copy of the object-address [@Gos15]. By implication, each message sent between actors contains a copy of the reference to the object representing the message[^fn-java-pass-by-value]. In general, we must expect that the reference we send and the reference we receive point to *one and the same* object. Akka only serializes messages in case both counterparts are not within the same JVM [@Sub11]. In case both actors are on the same local JVM, a given message object is therefore in the scope of both the sending and the receiving actor. This message introduces shared state between these two actors, which is in contrast to the strong state encapsulation requirement. [^fn-java-pass-by-value]: This causes the illusion that Java has pass-by-reference parameters. It does not. However, messages are meant to represent snapshot information of a state at a given point in time. Therefore, shared state is not a problem if it refers to immutable snapshots, such that there is no memory with read-write or write-write access by two distinct actors [@Kos16]. Then the facts cannot be modified by either of the holders. The encapsulation requirement explicitly refers to *mutable* strong state, as immutability avoids what Akka calls the *shared mutable state trap* [@AkkaJMM]. One option for Scala is to use `case class`{language:scala} constructs, which are immutable by default except for the transient state of the constructor parameters [@AkkaActor]. Java offers less syntactic support for expressing immutability. The property is neither formally defined in the *Java Language Specification* nor the *Java Memory Model* [@Gos15; @Goe06]. However, the basic requirement is to have `final`{language:java} fields only[^fn-java-immutability]. This means that the transient state through internally referenced objects must be `final`{language:java} too. In the author's experience, libraries which facilitate source-level annotation processing[^fn-immutables-lib] provide useful tools for generating immutable value objects. These libraries use annotated `interface`{language:java} declarations to generate consistent implementations offering builders and factory methods for instantiation [@Goe06]. [^fn-java-immutability]: From a technical point of view, a class can have non-`final`{language:java} fields and still instantiate immutable objects. `String`{language:java} is a prominent example. However, deeper insight into the Java Memory Model is required. Goetz gives an outline of the principal approach [@Goe06, p.47]. [^fn-immutables-lib]: Such as for example. All the restrictions we discussed so far still cannot prevent all obstacles Java and Scala offer to break the actor model semantics. Nothing can hinder an actor from sending a message to another actor containing the `this`{language:java} reference of its object. `this`{language:java} within an Akka actor is the standard self-reference object pointer and therefore not equal to *self* from the theoretical actor model. Akka provides us with the `self()` method that returns an `ActorRef`{language:java} inside the actor for when we need to communicate the actor's location. But having access to the `this`{language:java} reference of another actor breaks location-transparent access to the respective actor. Additionally, Java access modifiers are on class level instead of object level. If the recipient is of the same dynamic type as the `this`{language:java} reference sender, then the recipient (after the corresponding typecast) has access to all `private`{language:java} fields of the corresponding actor object. Though this visibility feature completely bypasses the encapsulation principle, it is intended behavior of the Java language design. We see, a library-based actor variant like Akka cannot enforce strict actor semantics by itself, if the programming language offers ways to break the semantics to the programmers. Only a programming language itself can enforce a strict notion of the actor semantics, as does for example Erlang. However, in general it is sufficient if programmers comply to coding conventions specific to the language to avoid shared state by accident [@Tas13]. Conventions come with the burden of ensuring immutable value objects or manually deep-copying messages. Other actor frameworks like Orleans always provide deep-copied messages automatically, which comes with a performance penalty [@Ber14]. It is worth pointing out that though the actor model is Scala's standard concurrency variant, the language was not like Erlang designed to enforce strict actor semantics. Instead it accepts the perils that come with a library-based implementation. The arguments for a library are [@Hal09; @Tas13]: * A library does not require special support by the compiler, JVM or extra syntax. * A library can be easily extended, adapted, and even replaced. This has already happened, when the standard Scala actors have been replaced under the hood by Akka actors. * A library can break the actor semantics *intentionally*, e.g.\ to introduce an additional concurrency abstraction, as the next section demonstrates. ~ Findings [Main findings]{.findings-title} + Programming language features can jeopardize actor state encapsulation. + Library-based actor systems cannot ensure isolation by themselves. + Guaranteeing message immutability is the obligation of the programmer. {.findings-list} ~ ### Utilizing other Concurrency Constructs Section [#sec-actor-concurrency-combination] motivated why we can combine the actor model with other abstractions of concurrency, as long as the actor semantics is not jeopardized. Akka offered support for several additional concurrency models. With version 2.3 however, Akka dropped the combination of actors and software transactional memory into so-called *transactors*. In principle, transactors have been useful for coordinating computations which span over the scope of multiple actors and require consensus between all of them [@Sub11]. However, transactional memory usage has never been able to abstract distribution transparently in Akka, since STM requires shared memory which is difficult across JVMs [@Swa14]. Transactor support was removed eventually. Besides STM, much more prominently used is the future concept. Futures allow us to define concurrent computation *inside* an actor [@AkkaFuture]. However, futures are not without perils of their own, as the following example illustrates: ```{language:scala} var a = 0 override def receive = { case _ => implicit val ec: ExecutionContext = context.dispatcher Future { a += 1 } a -= 1 print(a) } ``` First of all, Akka requires a so-called `ExecutionContext`{language:scala} in the actor's scope to run the future [@Hal18]. The example uses the actor's `Dispatcher`{language:scala}, which represents the thread-pool on which the actor runtime executes the actor. We can also specify a separate thread-pool instead [@AkkaFuture]. Most importantly however, we can misuse futures to introduce nondeterminism into the scope of an actor. The example defines a mutable state variable. Upon receiving an arbitrary message, we dispatch a `Future`{language:scala} with the task to modify the state variable `a`. Concurrently, the actor continues to process the message and attempts to also modify the very same state variable. Due to the nondeterministic nature of the underlying thread-pool, multiple orders of execution are possible, and therefore also multiple results for the output statement. This is possible because Java as well as Scala do not provide any kind of guarantee regarding the safety of data inside the scope of a `Future`{language:java} that exceeds the regular notion of safety of the respective language [@Wel05]. The isolated turn principle demands a guarantee that nothing interferes with the internal state of an actor except the actor itself, at the very least while processing a message. Yet futures have the potential to violate this constraint, thus breaking the actor semantics. Once again, Akka can neither check nor prevent this kind of concurrent modification. The programming languages visibility concepts simply allow us to pass mutable state into the scope of the futures. Then the safety notion permits the mutation of this state. Again, it is up to the programmer to ensure that only *immutable* state is introduced into the scope of a future [@Sub11; @AkkaJMM]. There are also less expected issues related to futures. The Crawler retrieves the XML feeds via HTTP. In principle, HTTP is a synchronous communication protocol, such that there is always a response to every request[^fn-synchronous-http]. Most APIs are therefore blocking as they abstract over remote procedure call semantics. However, some APIs allow us to handle requests asynchronously by providing a future result. The author expected to improve the throughput of the Crawler when it retrieves feeds via an asynchronous handling of HTTP requests. The basis for this assumption was that HTTP connections to remote servers pose as potential bottlenecks (unknown server response time, network latency), thus reducing the liveness of the actor. However, this approach dispatches great many `Future`{language:java}s simultaneously. Feed endpoints have a wide variation in response times and an asynchronous API allows us to start requests before the previous request has finished. All these futures stress the thread-pool of the Crawlers. We have experienced temporary starvations due to a lack of available threads in the Crawler's thread-pool. The author observed this effect with both asynchronous client APIs of the *Akka HTTP* [@AkkaHTTP] module and the *Apache HTTP Components* [@ApacheHttpComponents] library. Akka HTTP also provides a *flow*-based variant, where the concept of *backpressure* known from *stream*-based programming should limit throughput accordingly. However, the author experienced that the underlying *super connection pool flow* also introduces a limit to the amount of concurrent requests to a single host [@AkkaHTTP]. Feed publishers nowadays often choose to distribute their feeds via dedicated providers. As a result, a great amount of feeds are centralized on a small amount of hosts, rendering Akka's flow variant inapplicable. [^fn-synchronous-http]: The most basic form is merely a status code, e.g.\ the famous 404. Although simple RPC-styled retrieval did limit throughput, we have experienced this limitation as an advantage. The limitation puts a uniform and more predictable stress on the thread-pool, avoids problems like actor starvation and maintains their overall liveness. There are however still other cases where futures can come into play. The following section continues discussing future usage in the light of communication. ~ Findings [Main findings]{.findings-title} + We can combine actors with additional compatible concurrency models. + As with isolation, programming language features can jeopardize actor semantics when applying alternative concurrency constructs. + Combining futures with actors can have a negative impact on performance. {.findings-list} ~ ### Communication Abstractions {#sec-actor-communication-abstractions} The actor model is solely built on the concept of *asynchronous* message passing. Akka provides a method called `tell`, with an additional alias `!`\ for Scala, on `ActorRef`{language:scala}. We use the method to send a message object to an actor. However, many real-life scenarios expect synchronous communication. Echo faces this problem whenever a user requests information, i.e.\ a search request through the Web application (`G` ⇄ `S` ⇄ `I`) or metadata retrieval from the CatalogStore (`G` ⇄ `D`). Fortunately, we can model synchronous communication with an asynchronous information flow [@Agh97]. #### Future-based Messaging Akka provides a primitive to introduce a synchronous information flow. In addition to the asynchronous `tell [!]` command, `ActorRef`{language:scala} also offers the `ask` method, with alias `?`\ in Scala [@AkkaActor]. We use `ask` to model request/reply-style communication [@Hal09]. An `ask`-call resembles a `tell`-call in that it dispatches the method's argument as a message to the actor behind the reference. However, `ask` offers a result value which is the expected result of a synchronous call wrapped in a `Future`{language:scala}. The caller of `ask` is free to either proceed its computation, or go directly into blocking until the `Future`{language:scala} is resolved. This semantics resembles the future type message passing of active objects. ~ Figure { #fig-pipeline-search-future; \ caption: "Example flow of a future-based synchronous call in the retrieval phase"; \ width:100%; page-align:here; } ![img-pipeline-search-future] ~ [img-pipeline-search-future]: graphics/search-future.[svg,png] "Image about pipeline of a search request using futures" { height:1.1cm; vertical-align:middle; padding-top:1em; padding-bottom:1em; } A search request is the prime example of a synchronous call. Figure [#fig-pipeline-search-future] shows how a request travels from the Gateway to the Searcher and finally to the IndexStore. The results travel back from `I` via `S` to `G`. On the client-side, the results are wrapped in a `Future`{language:scala} until they become available. We can resolve a `Future`{language:scala} inside an actor (e.g.\ a Searcher) in a waiting fashion, which causes the actor to block. The actor cannot process other requests until it receives the answer from the IndexStore. However, we need to avoid blocking if we expect the Searcher to process messages in reasonable time. We want to improve throughput. To prevent unnecessary blocking, Scala provides monadic methods for the `Future`{language:scala} trait that we use to define subsequent computation once the results are available. We can also utilize these methods when we dispatch several synchronous messages inside actors. Scala even offers specialized syntax through the so-called *for-comprehension* [@Hal18]: ```{language:scala} val f1: Future[Int] = actor1 ? msg1 val f2: Future[Int] = actor2 ? msg2 val f3: Future[Int] = actor3 ? msg3 val r = for { r1 <- f1 r2 <- f2 r3 <- f3 } yield (r1 + r2 + r3) ``` It is important however that we dispatch the messages prior to the `for`{language:scala}-block's scope. Otherwise, it enforces sequential composition, if the `ask`-calls are inlined into the block scope [@AkkaFuture]. This is because for-comprehension unfolds to monadic combinator usage of `flatMap` and `map`, which are sequential by nature [@Hal18]. The example above becomes: ```{language:scala} val r = f1.flatMap(r1 => f2.flatMap(r2 => f3.map(r3 => r1 + r2 + r3))) ``` It is clear to see that if the `ask`-calls are inlined into the `for`{language:scala}-block, then the second message only gets dispatched once the first `Future`{language:scala} is resolved. Yet if used correctly, we can harness futures to preserve the single-threaded semantics of actors and still leverage parallel computation inside an actor. However, this approach has two downsides. First, it is a load on resources, since every `Future`{language:scala} also stresses the actor's thread pool. Second, there is always the risk of accidentally passing the actor's internal mutable state into the `Future`{language:scala}'s scope, thus introducing race conditions [@Sub11]. `ask` per se is therefore not ideal, but using futures with actors still has a long tradition [@Tas13]. #### Delegation-based Messaging One of the basic actor primitives allows an actor to spawn new actors. We leverage this ability to model synchronous request handling. Hereby, we relocate the result handling to a dedicated child actor, individually spawned for each request. We create a child in Akka by: ```{language:scala} val handler = context.actorOf(ReponseHandler.props()) index.tell(msg, handler) ``` Using `context.actorOf` instead of `system.actorOf` makes the response handler a direct descendent of the current actor. Providing the obtained `ActorRef`{language:scala} as a second argument to `tell`[^fn-2nd-tell-argument] sets the response handler as the official sender of the message. This way, we set the handler as the recipient to the response of the message. This dynamically created actor poses as a temporary component in the architecture: * Response Handlers (H) : exist with the sole purpose of posing as the original sender of a simple `tell` message dispatch and eventually receiving an answer in a purely asynchronous fashion. Upon message reception, a response handler passes on the result and deconstructs. In the retrieval subsystem, the actual information flow turns from the concept definition `G` ⇄ `S` ⇄ `I` into the concrete realization `G` → `S` → `I` → `H` → `G` (Figure [#fig-pipeline-search-delegation]). Altering the reply destination is a form of the *delegation* concept known from object-orientation [@Yon86]. The overall approach is sometimes referred to as *cameo pattern* and mostly used for brief and simple interactions between actors [@All13]. The delegation pattern provides an asynchronous composition style to handle synchronous communication requirements. The approach is also more implementation independent, if the actor variant does not offer a handy concept like futures. [^fn-2nd-tell-argument]: Note that the `!`\ alias of `tell` does not allow more than one parameter. ~ Begin Figure { #fig-pipeline-search-delegation; \ caption: "Example flow of a delegation-based synchronous call in the retrieval phase"; \ width:100%; page-align:here; } ![img-pipeline-search-delegation] ~ End Figure [img-pipeline-search-delegation]: graphics/search-delegation.[svg,png] "Image about pipeline of a search request using delegation" { height:2.7cm; vertical-align:middle; padding-bottom:1em; } Time in general constrains synchronous communication. It is important that we process the messages for a cameo delegation swiftly. The actor model's mailbox construct however buffers all incoming messages to an actor in a strict FIFO (__F__irst __I__n __F__irst __O__ut) order. Large mailboxes with many messages queued up prevent timely message processing. A common property of actor systems is the ability to influence the order of message reception [@Kos16]. Akka provides the concept of a `PriorityMailbox`{language:scala}, which is utilizing the pattern matching syntax of Scala to assign priority levels to messages based on their type. IndexStores and CatalogStores facilitate priority mailboxes to process all messages of synchronous flows first, regardless of the current mailbox size. #### Modelling Timeouts Synchronous information flow requires a mechanism to implement timeouts in order to prevent starvation. Akka supports a special timing mechanism. Actors, e.g.\ newly spawned delegation-slaves, register to receive a timeout message after a given time period. The actor then simply needs to provide an appropriate message behavior to handle the timeout message. When the actor receives the expected message of the synchronous flow before the timeout message, the actor cancels the dispatch of the timeout message. However, if the actor receives the timeout message prior to the expected response message, then the timeout occurred and the actor performs a timeout reaction [@All13; @Roe15]. It is interesting how timed messages are introduced into the actor system. Timers require some sort of concurrent thread that constantly checks the current time and performs registered trigger actions. Dedicated thread-based concurrency is somewhat opposed to the actor model and message passing in general, where each action happens as a reaction to a received message, decoupled from a notion of time. Therefore, offering solutions for timer mechanisms is a concern of many actor systems, e.g.\ also Erlang [@Arm93] and Orleans [@Ber14]. To avoid interference of outside threads with actor states, Akka provides a special `Scheduler`{language:scala} instance that is unique for each runtime system. Actors register a timed message sending operation with this `Scheduler`{language:scala}: ```{language:scala} val messenger: Cancellable = context.system.scheduler .scheduleOnce(5.seconds) { self ! TimeoutMessage } ``` The provided `Cancellable`{language:scala} reference allows us to prevent the trigger from firing by calling `messenger.cancel`{language:scala}. This scheduling mechanism introduces a notion of time into the actor semantics that feels natural to the actor model [@AkkaScheduler]. In combination with the cameo pattern, we can implement synchronous information flow semantics including time constraints by using purely asynchronous message passing operations. #### Type-restricted Messages and Compatibility {#sec-type-restricted-messages-and-compatibility} One of the basic actor primitives allows actors to send messages to other actors. However, in general this does not define restrictions on the types of messages that are sent. Messages are untyped in the theoretical actor model. The behavior of an actor decides at runtime whether it is able to handle the message. The active object concept aims to provide a higher-level abstraction and provides static guarantees for message types. Active objects leverage the method signatures of the standard object model for static type checking. However, the active object model also uses the method's signature to define the method's dispatch semantics. The work of Waldo *et al.*\ [@Wal96] describes in detail when and why this abstraction becomes problematic, especially in a context with transparent distribution. In short, the abstraction provided by the object model does not incorporate some effects of distribution well. Examples for such effects are latency, memory access (via pointers), partial failure and concurrency effects in general. The transparent abstraction of local and remote execution is therefore problematic with active objects. The object model must explicitly distinguish local and remote interaction to be robust and reliable. The classic actor abstraction on the other hand is resilient without explicitly distinguishing between local and remote message recipients. Current developments in Akka therefore address the challenge of offering some level of type safety for messaging without using Akka's `TypedActor`{language:java}s for active objects. The APIs are summarized under the name *Akka Typed* [@AkkaTyped]. One part of Akka's isolation strategy is to never expose a reference to an actual actor instance directly. Instead, all communication happens via the messaging interfaces of the `ActorRef`{language:scala} proxies (`tell`, `ask`). These interfaces take arbitrary types as messages. Akka Typed introduces a generic type parameter to the address, i.e.\ `ActorRef[U]`{language:scala}. The range of accepted messages is then limited to `U`{language:scala}-typed objects. The signatures of the messaging methods changes from `tell(msg: Any)`{language:scala} to `tell(msg: U)`{language:scala}. It is worth pointing out that in Scala the actor messages have `Any`{language:scala} as the most general type. In Java however, `Object`{language:java} is the most general message type. This discrepancy in the typization is somewhat counterintuitive, since Akka offers compatible bindings for both languages. The type systems of Java and Scala deviate however. Java distinguishes between reference types with `Object`{language:java} as ⊤ in the type hierarchy and primitive types (`int`{language:java}, `char`{language:java}, etc.) which are not subtypes of `Object`{language:java}. Hence, we cannot use primitive types as messages. In Scala on the other hand, all types have `Any`{language:scala} as their unified ⊤. The direct descendent `AnyVal`{language:scala} is the supertype of all value types (`Int`{language:scala}, `Char`{language:scala}, etc.), while `AnyRef`{language:scala} corresponds to Java's `Object`{language:java} and is therefore also supertype to all non-value types. If we send an `AnyVal`{language:scala} from a Scala to a Java actor, the corresponding primitive type's wrapper class (`Integer`{language:java}, `Character`{language:java}, etc.) is received and vice versa. In any case, type-restricted actor addresses are not without drawbacks. The third basic model primitive states that actors can change their behavior. When we restrict accepted messages by an `ActorRef[U]`{language:scala} that is hiding behavior changes transparently, we must also constrain a new behavior to process messages of type `U`{language:scala} exclusively. Otherwise, the address of the actor represented by the `ActorRef[U]`{language:scala} breaks the semantics and becomes invalid [@AkkaTyped]. To ensure address and behavior compatibility at compile time, we must define type-restricted actors through a behavior function that is also restricted by a type parameter: ```{language:scala} val behavior: Behavior[U] = Actor.immutable[U] { (_, msg) => // process msg Actor.same } ``` In this example, the typization `msg:U`{language:scala} is guaranteed. The `Actor.immutable[U]`{language:scala} factory prevents the constructed behavior from holding and passing over mutable state [@AkkaTyped]. Every behavior has to specify the replacement behavior for the next message explicitly. In this example, `Actor.same`{language:scala} declares that the replacement behavior stays the same. Though Akka Typed provides static type safe messaging, it still has its limits. For example, Akka Typed is not able to statically ensure that a behavior is in a certain state. The association of an actor's address and behavior is a fundamentally dynamic property of the actor model [@AkkaTyped]. We have found that compile-time message safety restrictions, apart from active objects, are a rare capability among actor systems. However, the Akka Typed library is in the current Akka version 2.5 still under active research. It is marked as "may change" and to be considered experimental. ~ Findings [Main findings]{.findings-title} + Abstractions for synchronous-styled actor communication require additional concurrency models (futures) or additional actors (delegation). + An explicit sense of time is contrary to the actor concept. The runtime must handle time and communicate it in terms of messages. + The actor model intrinsically types all received messages dynamically. Type-restricting the actor addresses constrains the range of acceptable messages. { .findings-list } ~ ### Supervision and Monitoring In Section [#sec-actor-systems] we have pointed out that actor systems aim for higher-level constructs than the low-level basic model primitives. One example for higher-level abstractions are the different messaging styles Akka facilitates. Another important reason for providing more expressive constructs is the encapsulation of faults [@Agh90]. Being aware of the possibility of faults and handling them adequately is key, especially in a distributed context [@Cou05]. Orleans for example directly reports exceptions back to the message sender [@Ber14]. The key to handling faults in Akka is its concept of *supervision*. No actor exists for itself, but is always a subordinate to its supervising actor. We call this dependency relationship a *supervision hierarchy*. The actor system provides a default supervisor at the top-level, which has the eventual supervision of all other actors [@AkkaSupervision]. The hierarchical relationship is fairly simple: a supervisor delegates work to its subordinate children, but has to *monitor* each child in return. Monitoring is the concept of getting notified of a subordinate's failures, and in turn reacting to these failures. A fault can be of arbitrary nature, i.e.\ an unhandled exception or invalid state. An actor automatically suspends itself and all its subordinates upon the occurrence of a failure. The runtime then notifies the superior, which has to provide a response to the failure [@All13;@Roe15]. The signaling of an occurred failure is not communicated via a "normal" actor message, but on a side channel [@AkkaSupervision]. A so-called *supervison strategy* handles the failure notifications. This strategy can take one of four possible actions: * Action 1: Resuming the suspended child, when the fault can be safely ignored. * Action 2: Restarting the suspended child, if its internal state is invalid. * Action 3: Stopping the child completely by not continuing its execution. * Action 4: Escalating the failure, hence the supervisor fails itself. { list-style-type:none } A supervisor's failure notification has the sole form of an exception. The actor system does not offer state information that puts this exception into context. The reason is that state should only belong to and be processed by one actor exclusively. Let us assume that a failure results in the propagation of state from the child to the supervisor. Then a part of the child's implementation logic leaks into its supervisor. The supervisor requires the knowledge to interpret the state in order to evaluate it in a meaningful way. However, Akka isolates the failure in the child. In case the child gets resumed (Action 1), the cleanup falls to the child itself. The runtime does not re-introduce the message on which the fault occurred into the mailbox, to avoid fault-reoccurrence. Actions 2-4 discard the child's state in any case [@Roe15;@AkkaSupervision]. ### Information Routing and Delivery Reliability We must frequently send a message not to one actor specifically, but distribute the message among a set of equivalent instances. Akka provides an appropriate concept called *routers*: ~ TexOnly &pagebreak; ~ ```{language:scala} val router: Router = { val routees = Seq(parser1, parser2, parser3) Router(RoundRobinRoutingLogic(), routees) } router.route(xmlData, sender()) ``` When a Web Crawler needs to pass on the XML feed data to the Parser, the Crawler does not directly select the recipient. The selection of recipients follows a strategy that depends on the used `RoutingLogic`{language:scala}. The `RoundRobinRoutingLogic`{language:scala} of the example redirects the message to the next routee in a cyclic order. Many alternative routing strategies are available, e.g.\ `RandomRoutingLogic`{language:scala} for a random recipient, and `BroadcastRoutingLogic`{language:scala} to distribute the message to all routees [@All13]. Conceptually, we can do the routing directly inside the sending actor by replacing the `tell(m,s)` call with `router.route(m,s)`. Alternatively, we can also employ an intermediate actor. The decision is based on the burden of managing the routees. We have to manage the set of routees and keep it up to date, since actors can fail and we must not send any message to a terminated actor. If the routees are children of the routing actor, then the router gets informed through its supervision obligation in the case of a subordinate's demise. A supervision-managed set of routees is called a *pool* [@Roe15]. Alternatively, routees are part of a so-called *group* when the routing actor does not supervise the routees [@Roe15]. Akka provides the so-called *actor selection* mechanism to send a message to an address matching a certain pattern. When no supervision relationship exists, message delivery reliability becomes a special concern. Actor selection does not guarantee that a recipient for a selection pattern exists. Therefore, a message becomes a so-called *dead letter* when the runtime cannot deliver the message. Delivery reliability is also a general concern of sending messages to actors, besides the context of routing. The theoretical actor model guarantees that all messages are always delivered [@Agh85b]. Conceptually, this insurance is important since it implies that no actor can permanently starve [@Kar09]. In practice however, we cannot safely assume perfect delivery reliability to hold. Because we consider actors in a potentially distributed context, message delivery can be subject to a network link. Recalling Fallacy 1: *The network is reliable* warns us that we cannot trust the network to transport the data in general. As a result, neither can we assume actor message delivery. Akka therefore provides a weaker insurance regarding message delivery reliability than the theoretical actor model. Particularly, all messages are merely delivered *at-most-once*. Additionally, when several actors send messages to the same recipient, there is no guarantee of a general order of the messages in the mailbox. The runtime merely enqueues the messages of each particular sender into the receivers mailbox in the same order as the sender dispatched the messages (FIFO order) [@AkkaMDR]. In contrast, other actor systems like Orleans provide *at-least-once* delivery. They resend messages that were not acknowledged within a certain timeframe [@Ber14]. No message is ever lost but it can emerge duplicate instead. As a consequence, the application logic of actors with at-least-once delivery must cope with the fact that the actor can receive one and the same message several times. ~ Findings [Main findings]{.findings-title} * Actors exist within a hierarchical supervision structure. * Failure is communicated along side the hierarchy while state does not leave the boundary of an actor. * Supervision is also useful for complex message routing logics. { .findings-list } ~ ### Persistence and IO {#sec-actor-persistence} We must employ some kind of data persistence mechanism, e.g.\ a database, in order to persist the internal states of actors. Echo's CatalogStore is a prime example. Due to the single-threaded semantics of actors, only a single interaction with the database is possible at the same time. This is inefficient, since database access is input/output and therefore a performance limiting factor in general [@Sub11]. Theoretically, the single-threaded semantics of actors makes database transactions obsolete, but the author found that common APIs demand an active transaction in any case, e.g.\ with providers of the __J__ava __P__ersistence __A__PI (JPA). We can overcome the single-threaded limitation when we utilize a non-blocking API for database connections. Such APIs provide a `Future`{language:scala} reference to an eventual result at the cost of additional stress to the actor's thread-pool. With the monadic methods of `Future`{language:scala} we define further computation on the result once it becomes available. The price is the immanent risk of accidental data races when we pass mutable state into the `Future`{language:scala}'s scope. An example for a situation where we cannot apply a non-blocking API is the experiment for the benchmark we describe later on in Section [#sec-experiment-setup]. We need an alternative strategy. The delegation principle we discussed for synchronous message handling is also applicable for database interaction. We can handle several database interactions concurrently by having as many actors communicate with the database at the same time. ~ Figure { #fig-persistence-actor; \ caption: "Example of a stateful actor sharing its persistent state with several child actors: The store delegates all database interactions to the child actors"; \ width:100%; page-align:here; } ![img-persistence-actor] ~ [img-persistence-actor]: graphics/persistence-actor.[svg,png] "Image about actor persistence" { width:5cm; vertical-align:middle; padding-bottom:1em; } Each CatalogStore actor has a database. Conceptually, we use the database to persist the state of a single actor. The state is not exclusive anymore, if several actors manage the same database. Yet we need several actors for concurrent interaction. Hence, we have to intentionally weaken the encapsulation principle. Although the database conceptually belongs to the CatalogStore, the store actor delegates all database interactions to it's children (Figure [#fig-persistence-actor]). A `RoundRobinRoutingLogic`{language:scala} distributes the database interaction operations between the child actors. Now, the CatalogStore architecture component consists of several task units of the actor model. All these actors conceptually share a single persistent state. We must not break the isolation of each actor however. Therefore, these children neither share the same database connection interface object, nor any other mutable data. We merely loosen up the restriction on encapsulating the persistent state inside one single actor. A narrow group of actors is managing the persistent state instead. All these actors must utilize the database system's transaction mechanism. We use a dedicated dispatcher for all actors involved in the logical encapsulation of the persistent state. The underlying thread-pool uses a fixed number of threads. This provides a predictable impact on performance, in contrast to Akka's default dynamically sized thread-pools. Dynamic pools add threads when demand is high, i.e.\ due to many blocking operations, and can therefore consume a lot of system resources [@All13]. The idea of actor-managed databases is not novel. Shah & Salles give an entire manifesto on what they call *actor database systems* [@Sha17c]. ~ Findings [Main findings]{.findings-title} + Efficient handling of persistence and IO in general uses the same strategies as for synchronous actor communication. + Concurrent database interaction via delegation forces us to intentionally weaken the conceptual encapsulation of actor state. {.findings-list} ~ ## Microservice-based Implementation {#ch-microservice-impl} This section covers the strategies we apply when we program with the microservice model. Again, we implement the backend of the concurrent system which we outlined in Section [#sec-scenario]. All concepts we discuss are with respect to a specific technology stack. The focus is on the linguistic support provided by the technology stack. Efficiency considerations are part of Chapter [#ch-evaluation]. ### Service Technology Stack Service-oriented programming languages seem to be a good choice for a microservice architecture. Though some languages are theoretically matured, from a practical point the available SOP languages are as of yet still at an early prototypical stage. Jolie for example still misses an ecosystem and tool support we are looking for in comparison to Akka. Therefore, we refrained from using Jolie to implement Echo. Instead, we compose microservices using a more traditional technology stack. We use Java as the programming language for all services and the *Spring* [@PivotalSpring] framework, most notably its *Spring Boot* [@SpringBootDoc] module, for application fundamentals. Additionally, we apply many libraries of the *Spring Cloud* [@SpringCloudDoc] collection, which have proven very effective for microservice development in industrial applications [@Car17]. For the webserver, we configure Spring to use *Undertow* [@JBossUndertow]. Spring is based around the concept of *inversion of control* (IoC). Spring's IoC variant applies *dependency injection*. We do not instantiate objects directly, but instead define for certain classes what kinds of dependencies they need (i.e.\ we declare fields but do not initialize them directly). These kinds of classes are called *beans*. A so-called IoC *container* instantiates these beans and *injects* their dependencies through constructor arguments, factory methods or setter methods. An IoC container's primary responsibility is therefore the management of beans[^fn-managed-objects]. We call this *lifecycle management*. The actual execution logic of a bean, i.e.\ the scheduling on a thread-pool, is also left to the IoC container [@SpringDoc;@Wal07]. Therefore, Spring's IoC container is the internal concurrency managing system of each of our microservices. Conceptually, IoC management has a certain resemblance to the execution of actors by an actor runtime [@Hal06]. [^fn-managed-objects]: Sometimes Spring beans are therefore referred to as *managed objects*. Spring Boot is primarily an application skeleton based on the Spring framework. The skeleton is a fully executable application without domain-specific functionality. We use Spring beans to add custom configuration and the functionality we want the application to fulfill [@SpringBootDoc]. In other words, Spring Boot provides us with a general foundation for the microservice's engine, and we as programmers merely add the specific service behavior through custom beans. Spring Cloud is a set of libraries which focus on features like data access, messaging, streams, discovery, load balancing, circuit breaker, and many more [@SpringCloudDoc]. The term *cloud* indicates that the libraries are intended for cloud computing scenarios. This makes them useful to us since the industry uses microservice architectures for cloud deployment scenarios [@Bon17;@Dra17b]. As we did with the actor implementation, in subsequent sections we pay attention to the linguistic support provided by the framework regarding the expression of service requirements. ~ Findings [Main findings]{.findings-title} + We can leverage inversion of control frameworks to reduce the effort that comes with writing many separate applications for MSAs. + Programming with IoC transfers the management of concurrent execution to the IoC container. {.findings-list} ~ ### Internal Service Concurrency {#sec-internal-service-concurrency} The microservice model paradigm does not dictate restrictions on the internal service structure. A service is free to apply concurrency internally, and to utilize every concurrency mechanism it sees fit (including actors). We build the Echo services on top the Spring framework and utilize the concurrent programming structure that Spring's IoC container provides. Spring-based microservices receive requests as method calls to beans. We discuss the concrete communication mechanisms in Section [#sec-ms-communication-mechanisms] below. The respective bean classes have a so-called *stereotype annotation*[^fn-spring-stereotype] decoration (e.g.\ `@Component`, `@Controller`, etc.). The IoC container turns each of these method calls into a so-called *task* by wrapping the call into a `Runnable`{language:java}. Every task gets appended to a task queue. We have already demonstrated how to wrap method calls into a `Runnable`{language:java} in the Java concurrency case study of Chapter [#ch-concurrency]. Spring's `TaskExecutor`{language:java} constantly processes the task queue. An `Executor`{language:java} is Java's variant of a thread-pool. An allocated thread of the thread-pool eventually executes each task [@SpringDoc]. [^fn-spring-stereotype]: Stereotypes in Spring describe certain patterns for beans. Hence, we expect special behaviors of stereotype-annotated beans. Since microservices are concurrent internally, we have to pay attention to their shared internal resources. All shared resources must be either immutable as the messages in Akka, or we must synchronize the access to the resource. Spring uses software transactions for synchronization. Spring provides linguistic support in a *declarative programming* style for many strategies and mechanisms. This is particularly interesting since the declarative style is not intrinsic to Java's imperative programming concept. However, the language allows us to introduce declarative programming through *annotations*, so that the IoC container then applies appropriate behavior. In contrast to annotation processors, where the compiler reads annotations to influence the compilation process (e.g.\ to generate class implementations), Spring uses *reflection* at runtime. The result is a form of *aspect-oriented programming*. We leverage the declarative style for synchronization by defining software transactions on method calls using the `@Transactional` annotation. Spring also extends the STM to database transactions transparently [@SpringDoc;@Wal07]. While Spring enqueues each request into a concurrent task queue automatically, we as programmers also want to leverage this technique to achieve a higher degree of concurrency inside a microservice. The `@Async` annotation allows us to declare methods that we want to dispatch asynchronously. When we call an `@Async`-annotated method, Spring wraps the call into a `Runnable`{language:java} and enqueues the resulting task into the task queue of the `TaskExecutor`{language:java}. Since the method executes at an unknown point in time for the caller, we cannot expect a result value directly. An `@Async` method is therefore `void`{color:blue} in general. Alternatively, we return the expected result wrapped in a `Future`{language:java} [@SpringDoc]. We have discussed this idea for active objects and their future type methods already. The future enables us to return an intermediate result and provides an interface to check whether the actual result is yet available. The author has experienced that Spring's default `AsyncTaskExecutor`{language:java} does neither handle nor log the occurrence of exceptions. We have found this factor troubling for development. Therefore, we use a custom asynchronous task executor implementation capable of handling exceptions to fix this flaw. Although Spring's declarative programming style for concurrency is very powerful, we have also experienced some limitations. The `@Async` and `@Transactional` annotations only show effect on `public`{language:java} methods. Additionally, self-invocation of an `@Async` method does not spawn a new asynchronous task, but instead executes within the same task in a synchronous fashion. Combining `@Async` and `@Transactional` is also possible. However, the asynchronously dispatched method does not run within the same transaction as the dispatching method, but in a fresh transaction instead [@SpringDoc]. ~ Findings [Main findings]{.findings-title} + The IoC container builds on Java's standard threads, but exposes a completely different programming interface (implicit concurrency, `@Async`, STM). + A declarative programming style can simplify the handling of concurrency issues in an imperative programming language. {.findings-list} ~ ### Isolation and Persistence The incarnation of a microservice is by definition a system level process. The foundation of the isolation of services is the strict memory boundary of every process that the operating system enforces. By convention, services refrain from implementing shared memory sections among them. We therefore never require synchronization among the components. The principle must also extend to the persistence of state. We can persist information with many different strategies. Database systems are one well established approach. If we share a database between several microservices, every service gets access to all other component's persistent state. Shared databases are a simple way to skip isolation mechanisms and bypass the service interfaces, thus breaking the encapsulation principle. Therefore, one convention of the microservice principles is that each service owns its databases exclusively [@Dra17a]. Sharing a persistence mechanism conceptually relates to sharing state, which introduces an implicit form of shared state communication [@Cou05]. As a consequence, we must provide each component with its very own database instance if we require persistence. We deploy every CatalogStore instance with a dedicated database instance, and every IndexStore has a separate Lucene reverse index data structure. Persistence is a form of IO and a potentially performance limiting factor. Hence, database management systems usually support concurrent access to the database. As with actors, concurrent connections increase the throughput of the component. The *Spring Data* module offers us a good interface as well as a transparent abstraction to interact with the database in a concurrent way [@Wal07]. ~ Figure { #fig-persistence-microservice; \ caption: "Example of a stateful microservice maintaining several concurrent connections to an exclusive database"; \ width:100%; page-align:here; } ![img-persistence-microservice] ~ [img-persistence-microservice]: graphics/persistence-microservice.[svg,png] "Image about microservice persistence" { width:5cm; vertical-align:middle; padding-bottom:1em; } Since Spring executes every request concurrently inside a `Runnable`{language:java} task on a thread-pool, concurrent database interactions are implicitly available. Every thread of the pool can interact with the database at the same time. The transactional memory of Spring extends to database transactions. Programmers do not have to pay heed to or apply additional strategies to leverage efficient persistency through database concurrency. Hence, we expect a microservice to have several database connections in place at the same time (Figure [#fig-persistence-microservice]). ~ Findings [Main findings]{.findings-title} + State is exclusive to the entire microservice. + Isolating state must extend to the isolated persistence of state. + Concurrent access to persistent state is transparently possible. {.findings-list} ~ ### Communication Mechanisms {#sec-ms-communication-mechanisms} Communication in microservice architectures happens via inter-process communication mechanisms. Various kinds of interfaces are possible. While communication for actors happens in a uniform way, microservices in general face more challenges. The freedom in the design of services does not dictate a specific communication interface. The only restriction regarding the interaction is that we need to omit shared memory between services. Solely relying on message passing mechanism makes services cohesive and loosely coupled. We have found scholars to give REST (__Re__presentational __S__tate __T__ransfer) as the prime (and often sole) example of valid communication channels throughout the literature. However, the author experienced that REST is practical only in certain situations. Since REST builds upon synchronous HTTP, it is a good solution for synchronous requirements. Echo facilitates REST for search requests through the Web application (`G` ⇄ `S` ⇄ `I`), as well as metadata retrieval from the CatalogStore (`G` ⇄ `D`). As even Fowler & Lewis [@Fow14] in their seminal work on microservices point out, other mechanisms are applicable as well, as long as they are lightweight and do not apply logic of their own. The indexing subsystem is more predestined for an asynchronous workflow. Therefore, we desire a message queue-like mechanism. JMS (__J__ava __M__essage __S__ervice) [@Cou05] is a prominent example among JVM technologies. However, JMS is also limited *to* the JVM, which contradicts the open and well-defined interface principle of microservices. We require a technology-heterogeneous message queue standard. AMQP (__A__dvanced __M__essage __Q__ueuing __P__rotocol) [@AMQP] is an open specification for asynchronous messaging. While JMS only defined the interfaces, AMQP also defines the message format. Therefore, different implementations exist which we can interchange freely. Echo builds upon *RabbitMQ* [@PivotalRabbitMQ], a messaging system that proved to integrate well into MSAs, according to the literature [@Dra17c]. A message queue conceptually introduces a new concurrent component into the architecture: * Message Queue (Q) : is a distributed point-to-point communication channel. The queue offers message delivery from a sender to a qualified receiver (possibly unknown to the sender), decoupled in time and space (asynchronous) [@Cou05]. The queue becomes an intermediate for all asynchronous messages. Senders push messages to the queue, and receivers subsequently pull those messages from the queue. For example, we do not send a message directly from a Web Crawler to a Parser (`W` → `P`), where the active component is only `W`. Instead, the Crawler pushes a message the queue (`W` → `Q`). At some later point and idle Parser pulls this message from the queue (`P` ← `Q`). The message travels asynchronously. The actively communicating components are `W` and `P`. The queue merely performs internal routing logic and reacts to requests from others. The queue also decouples the sender `W` from the actual receiver `P`, i.e.\ `W` does not know which concrete `P` receives the message. In general, a service can have several different interfaces, based on heterogenous technologies. As a result, this allows the service to provide the *same* functionality on *different* interfaces. Since all interfaces of Echo's microservices produce and consume messages in JSON (__J__ava__S__cript __O__bject __N__otation) format, there are no data type incompatibility problems. Echo's microservices provide a REST interface for every message a service consumes from the AMQP queue. We can therefore also send a message to a service directly via HTTP. The additional option to invoke service functionality turned out especially useful for testing and debugging purposes when we implemented Echo. This suggests that it is valuable to maintain different interfaces for development, production, and maintenance. #### Programming Abstractions {#src-ms-programming-abstractions} We have already seen that Spring provides a declarative programming style through annotations. The IoC container applies a behavior to a bean based on an annotation using reflection. The benefit of reflection is that we can still apply deployment configuration without the need to recompile, which is especially useful for communication configuration. The downside is additional runtime overhead and the lack of static compatibility checking. For example, a Searcher queries an IndexStore using a synchronous REST call (`S` ⇄ `I`). We use the Spring binding for *Feign* [@OpenFeign], a library dedicated to annotation-based decorations for Java interfaces. Clients consume RESTful endpoints using a dynamic interface implementation. We express the example in the Searcher through: ```{language:java} @FeignClient(name = "index") public interface IndexClient { @GetMapping("/query") List query(@RequestParam("q") String q); } ``` The stereotype annotation `@FeignClient` is for REST clients. Feign automatically generates an implementation class of the given `IndexClient`{language:java} interface. Spring's IoC then instantiates a bean of this implementation class. Calling the `query`{language:java} of this bean dispatches the REST call in a blocking fashion. The method only returns once the result from the IndexStore is available. Mapping the HTTP body content (in JSON format) of the response to domain objects happens transparently, provided that we configured a JSON serializer for the IoC container. As a result, we can use every domain object for the method result type. The IndexStore receives the request by declaring an appropriate REST-endpoint using a similar annotation driven implementation approach. `@RestController` is the stereotype annotation for beans that receive REST calls: ```{language:java} @RestController public class IndexResource { @GetMapping("/query") public List query(@RequestParam("q") String q) { // query reverse index for phrase q } } ``` This approach models a remote procedure call between the two components. The call `query("TU Wien")`{language:java} of the `IndexClient`{language:java} in the Searcher results in a call of `query(String)`{language:java} method with argument `"TU Wien"`{language:java} of the IndexStore's `IndexResource`{language:java}. The service's programmer invokes the method on the client-side. Then, the receiver's inversion of control container registers the request on the transparently exposed REST interface and calls the method on the server-side. We can express message queue interaction in a similar fashion through respective AMQP stereotype annotations. The resulting beans interact then via RabbitMQ. While the above REST example declares a synchronous API, the AMQP annotations declare asynchronous APIs. An interface method call on the client-side returns before the server-side receives and processes the message. #### Service Discovery Message queues decouple the sender from the receiver. Therefore, the sender neither requires nor knows the address of the actual receiver. For direct communication like REST however, we require the actual address of a recipient. Yet in certain deployment scenarios this information is not statically available. We apply the concept of *service discovery* known from SOA [@Cou05] to bridge this lack of static information. The so-called *registry* is a dedicated service component that provides binding information about other services. We merely predefine the connection to the registry statically and are obligated to ensure the availability of the connection at runtime. Microservices then register with the registry service, in order to be discoverable by others [@Mon16a]. This dedicated service adds a new concurrent component into the architecture: * Discovery Registry (D) : is a centralized service and provides address information for dynamic connections. Other services register with the registry service under a name and their address. Clients lookup the current address of registered services for a given name. The `name` argument of the `@FeignClient` annotation in the REST example we gave before relates to the name we use to register the IndexStore unit. The advantage of Feign is that it automatically integrates with discovery mechanisms. Examples for service registry technologies are *Consul* [@HashicorpConsul], a standalone registry service solution, and *Eureka* [@NetflixEureka], a module of Spring Cloud to add registry capabilities to custom applications. Echo supports Consul, but uses a dedicated service based on Eureka by default. The author of this thesis experienced Consul as very resource demanding in comparison. ~ Figure { #fig-service-discovery; \ caption: "Example of service discovery usage in the retrieval phase: The consecutive lookups delay the overall synchronous communication"; \ width:100%; page-align:here; } ![img-service-discovery] ~ [img-service-discovery]: graphics/discovery-1.[svg,png] "Image about service discovery" { height:3.2cm; vertical-align:middle; padding-bottom:1em; } Discovery mechanisms impact the response times of services. It can become necessary to lookup an address before a service is able to make a request. The service must then make additional RPCs for registry lookups, in the worst case for each of the involved services. Figure [#fig-service-discovery] shows the order of interactions in the worst case for search requests in our scenario. When all location information is outdated, then `G` must first lookup `I` with `G` ⇄ `D` (1) before it can do `G` → `S` (2). Subsequently, `S` must lookup `I` with `S` ⇄ `D` (3) before it can send `S` → `I` (4). This dampens the liveness of the request flow for our search results (`G` ⇄ `S` ⇄ `I`). We also need to ensure that the information in the registry is correct and up to date. *Health checks* are a common feature of discovery services to determine if their registrees are actually up and alive [@Dra17c]. In contrast, message queues have the major benefit that a sender dispatches a message to the queue without needing the address of the receiver. This circumstance provides a lower degree of coupling as well as a notion of location transparency between sender and receiver. Therefore, when we use message queue channels, we do not need service discovery technology if we statically know the queue address. Otherwise, the queue needs to register with the discovery mechanism. The clients then simply retrieve the queue's address dynamically. #### Load Balancing {#sec-ms-load-balancing} The idea of distributing work (*load*) between several instances of the same task unit is called *load balancing* (LB). The goal is to optimally utilize the resources of all instances and prevent that a single unit is overloaded. Load balancing maximizes throughput and minimizes response time of the overall system [@Ben90]. There are two directions towards load balancing. A central supervising entity that distributes the work between receiving services is balancing load *server-side*. Spring Cloud offers *Zuul* [@NetflixZuul] to create balancing server. In contrast, a service that distributes the work itself is doing *client-side* balancing [@Cou05]. Echo's microservices use the Spring Cloud module *Ribbon* [@NetflixRibbon] for client-side load balancing. The main reason for Eureka over Consul as the Discovery service is that Ribbon integrates with Eureka. A Ribbon client does not dispatch a message directly to an address. Instead, Ribbon uses a static name to lookup the current address of a server from the discovery service. Ribbon can then balance individual requests directly on the client-side across server instances, as it cooperates with Eureka to maintain a set of valid instances of a static name [@Car17]. This name is in fact the `name` argument for the `@FeignClient` annotation of the declarative REST interface, since Ribbon integrates transparently into Feign. ~ Findings [Main findings]{.findings-title} + Different communication styles require different communication channels. + Microservices are free to serve the same functionality on different communication interfaces and technologies at the same time. + Microservices always serialize and exchange data in a technology neutral format (e.g.\ JSON or XML). This prevents data type compatibility issues. + Location transparency is not intrinsically available in MSAs. Network communication coupled with discovery technology adds this feature. {.findings-list} ~ # Evaluation {#ch-evaluation} ~ Epigraph { caption: "Jeannette M. Wing" } Thinking like a computer scientist means more than being able to program a computer. It requires thinking at multiple levels of abstraction. ~ In Chapters [#ch-actor-model] and [#ch-microservice-paradigm] we introduced the concepts of actors and microservices. Chapter [#ch-implementation] described the strategies of each model to implement concurrent systems like the Echo scenario. We discussed each model separately and focused on the individual concepts of each model. In this chapter, we compare and evaluate both models relative to each other. As we have demonstrated, both actors and microservices qualify for expressing concurrent computation. Their mechanisms and abstractions also support parallel computation on multicore processor as well as distributed execution on multiple hosts. Several authors [@Kan12;@Kar09;@Agh99] suggest that programming models regarding parallel and distributed contexts should be evaluated based on two objectives: *expressiveness* for programmers and *efficiency* of execution. Section [#sec-eval-expressiveness] compares the key properties that are the foundation of the concurrent execution of both models and the resulting capabilities. The model capabilities allow us to evaluate the expressiveness of each programming model. Section [#sec-eval-efficiency] provides a benchmark for the actor- and microservice-based implementations of the Echo scenario. The results of this benchmark evaluate the efficiency of the programming models. ## Expressiveness and Capabilities {#sec-eval-expressiveness} In this section, we evaluate and compare the key properties of the actor and microservice programming models, as well as the capabilities we can express with these models. Programming language theory knows a concept called *expressiveness* or *expressive power*. This concept also becomes more and more relevant in the light of concurrency theory. There, the *relative expressive power* is used to compare two formal concurrency models [@Fel91;@Gor16]. Evaluations on a strictly formal level require the rules and boundaries of formal frameworks. Therefore, often programming languages are analyzed. Concurrency theory focuses on *process languages*. These languages are founded on the formal frameworks of so-called *process calculi* [@Per16], which we briefly discuss in due course. Here, we do not concern ourselves with formal proofs of behavioral equivalence however. Instead, we revert to informal discussions about the observational equivalence of concepts we can express in the actor and microservice model, as has been often done in the literature before [@Fel91]. All strategies that we express in both models are essentially encodings of ideas [@Gor16]. Ultimately, we are interested if actors and microservices have the expressive power to convey the same ideas. The two models are equally capable to express a concept if the solution of one model produces an equally powerful functionality (informally) as the solution of the other model. ### Encapsulation and Isolation Actor and microservice semantics rely on the strict separation of component states. We must ensure that state is conceptually well encapsulated within a component, and practically isolated from the outside. Encapsulation is a concept we know from object-oriented programming. As Snyder [@Sny93] points out, OOP usually offers mechanisms to restrict the access to an object's state. A client gets access to an object's state by issuing requests for the *services* an object offers. These services -- not to be confused with the concept of a *microservice* -- are what Meyer [@Mey97] calls *well-defined interfaces* in the form of *routines*. Meyer considers these services a necessity for encapsulation. Both actors and microservices offer well-defined interfaces in their own way. For an actor, the interface is the sum of the messages the actor understands through its behavior. For microservices, the interface is based on the sum of the facilitated communication channels, e.g.\ the REST interfaces the service exposes and the messages it consumes from a message queue. Only through the interfaces can we access or modify the service's state. Table [#tab-comparison-encapsulation] at the end of this section summarizes the encapsulation-related matters as they are facilitated by Akka actors and Spring-based microservices. #### Shared and Mutable State One fundamental characteristic of both actors and microservices is their notion of shared state. Summarizing Chapter [#ch-actor-model], the actor model encapsulates state exclusively within an actor. Therefore, the state is only accessible to and modifiable by the actor itself. Additionally, actors provide single-threaded semantics while processing messages. An actor processes only one message at a time. The isolated turn principle eliminates the need for synchronization, since message are free of low-level data races, and a turn has exclusive access to the current state. The actor's state is fully isolated. On the other hand, we cannot apply the same reasoning for microservices in general. The paradigm states nothing about how state has to be handled internally. Depending on the programming paradigm we use to implement the service, state is not necessarily exclusive internally. In OOP for example, several objects can access the same memory location. Furthermore, concurrent access to the state is also possible, e.g.\ as a reaction to several invocations of the service's interface within a short time span. Microservices do not ensure single-threaded semantics. In general, we therefore assume that we must synchronize the access to the service's internal state. Additionally, the microservice paradigm dictates that we must avoid shared memory *between* services, as well as all kinds of shared resources in general. Since every service runs within a dedicated system process, avoiding shared memory implies no direct intersection between service process boundaries. Typical communication channels satisfying the requirements given in Section [#sec-independence-and-interaction] also prevent reference sharing to joined mutable data. All viable channel technologies provide some sort of message passing designed to transfer information between the different memory spaces of distinct processes [@Les09]. Actors generally face more challenges when it comes to truly ensuring state separation. The components can exist within the same process boundaries and access the same memory locations. Depending on the programming paradigm we use to implement the actors, exposing shared state to others can be rather easy. At the same time, exposed state is not necessarily apparent to the programmer. Think of the example we discussed for Akka. We can use an arbitrary object as a message. If we do not construct an object in an immutable fashion, the message transfer shares the object's state. Concurrent access and modification to this shared state causes unpredictable runtime issues. Especially the imperative programming style leans on mutable state. Actors in imperative languages therefore require extra care to preserve the model semantics. The functional paradigm tends to avoid these problems inherently. Functional languages model the behavior as a function and only this function is modifiable exclusively by the actor. If the actor model is integrated into the programming language directly, the language is able to enforce restrictions preventing shared mutability problems by design. Erlang is the prime example [@Vin07]. However, library-based actor implementations cannot ensure full isolation by themselves [@Kos16]. #### Persistence and IO {#sec-comparison-persistence-io} The encapsulation and isolation principle of actors and microservices has one more important implication. Recall that state is exclusive to a single component. Hence, each component must also take exclusive care if we require durable state across the component's lifetime. In our scenario, every CatalogStore must have its exclusive database and every IndexStore its exclusive reverse index data structure. However, neither an actor runtime nor a microservice technique can enforce this persistence restriction. The obligation of correct configuration lies solely with the programmer. We also desire concurrent interaction to persistence mechanisms to increase throughput. Microservices easily leverage the concurrent interaction capabilities of a database management system via to the service's internal concurrent structure. The thread-pool strategy of Spring as well as the software transaction mechanism transparently extend to database interactions. Actors face more problems, since they are not concurrent internally. Like with all actions in the actor model, database interactions must execute in a turn-based fashion and are therefore sequential. We have outlined that we can apply the same strategies as for synchronous communication to improve the persistence-efficiency of actors. Either we employ additional concurrency constructs (futures) or we delegate interactions to child actors. Futures always have the potential to violate the isolated turn principle. Delegation forces us to share the database between several actors, violating the exclusive ownership of (shared) state. #### Cohesion, Coupling and Independence Message passing interfaces and strong encapsulation make actors and microservices *cohesive*. Bonér [@Bon16] also defines truly isolated components combined through message passing communication as *decoupled* in two dimensions. On the one hand, the components are decoupled in *time*, which is the requisite for concurrent execution. On the other hand, the components are decoupled in *space*, and therefore we can execute them remotely and even move them between locations. Regarding time, actors facilitate asynchrony intrinsically through the model design. Nevertheless, actor systems tend to offer synchronous primitives on top of the asynchronous style. Programmers receive better abstractions, at the cost of increased coupling. Microservices are free to choose the IPC style, as long as the IPC mechanism is based on message passing rather than shared memory, which further reduces the coupling. Regarding space, actors are conceptually fully isolated. In practice, ensuring true isolation is difficult, especially for library-based actor implementations. We have discussed the conceptional problems for Akka in detail in Section [#sec-actor-isolation]. In the end, the programmer has to guarantee the isolation by complying to programming conventions. Infringing these conventions introduces or exposes shared mutable state, which is a violation of the actor model. Also, shared mutability increases the coupling to the sharing component, both in time and in space. Microservices have an inherent advantage regardless of the chosen programming model. The only true paradigm requirement is the avoidance of shared memory sections with other processes. Either the operating system enforces the memory boundaries, or the hardware separation resulting from distribution guarantees spacial decoupling. Besides, their distinct codebases further decouple the microservices. Only a shared library increases their coupling. For example, we facilitate a custom Core library in Echo. Core increases the service's coupling relative to the library. We have implemented all domain-specific functionality, as well as *data transfer objects* (DTO) within the Core. The DTOs are the domain objects we use as messages. Therefore, all microservices are coupled among each other by the DTO classes. Actors suffer from the coupling problem unequally more. The actor runtime intrinsically binds every actor. Different interfaces to an actor system can exist. Akka demonstrates interface diversity for Java and Scala. Yet, the actors cannot escape the coupling to Akka's codebase. Additionally, Echo's actors are also coupled by the DTOs in Core, just like the microservices. These two notions, high cohesion and low coupling, allow us to reason about the *independence* of task units. Independence is one of the primary concepts of the microservice paradigm. The literature describes independence as a direct consequence of high cohesion and low coupling [@Dra17a;@Dra17b;@Dra17c;@Sha17b]. We deem the strong process-based form of independence superior to the notion provided by the actor construct. Actors are passive tasks which react to messages. An actor can only perform actions when the runtime executes the actor. Microservices can show active behavior on their own. ~ Begin Center ~ Begin TableFigure { #tab-comparison-encapsulation; \ caption: "Comparison of encapsulation-related matters in Akka and a Spring-based MSA"; \ page-align:here; } |----------------|------------------------------------|------------------------------| | Characteristic | Akka | Spring MSA | +----------------+------------------------------------+------------------------------+ | Shared state | Not enforced, obligation of the | Ensured through processes | | | programmer | memory boundaries | | ---------------|------------------------------------|------------------------------| | | Single-threaded semantics, free of | Concurrent access inside the | | State mutation | synchronization, threatened by | service is possible, but | | | language features | requires synchronization | | ---------------|------------------------------------|------------------------------| | | Single-threaded semantics | Concurrent interaction with | | Persistence/IO | dampens throughput, improved | outside freely possible | | | with futures or delegation | | | ---------------|------------------------------------|------------------------------| | Cohesion | High cohesion through strong encapsulation and message passing || | ---------------|------------------------------------|------------------------------| | Coupling | Coupled to common codebase | Low coupling through | | | and the runtime | independent codebases | | ---------------|------------------------------------|------------------------------| | Independence | Passive units, subject to runtime | Active units, subject to OS | | ---------------|------------------------------------|------------------------------| ~ End TableFigure ~ End Center ### Communication and Message Routing According to the general concerns of concurrent computation, tasks without mutable shared memory require other kinds of communication links which facilitate message passing instead. Since both the actor- and the microservice model strictly omit shared state, they too require what we call *communication channels* for messaging. These channels transport information from a source to a destination. A *sender* writes data to the channel, and subsequently the *receiver* reads the data from the channel. Independent of the concrete channel technology, message passing is a form of implicit synchronization of the information, since the event of reading a message can intrinsically only occur *after* the message was sent. In contrast, shared state explicitly requires a defined order of accessing the information [@And83]. We have identified various forms of information flows offered by channel concepts throughout the literature [@Cou05;@Mon14;@Gui09;@Spe90;@Roe15;@Agh99;@Tan07;@Bac03]. Generally, the flows can be distinguished alongside two dimensions: number of recipients and response coupling. Authors declare varying taxonomies for the resulting combinations. Table [#tab-communication-styles-overview] provides an overview of the terminology we use in the remainder of this work. Subsequently, Table [#tab-communication-styles-comparison] summarizes the communication capabilities of Akka actors and Spring-based microservices regarding these communication styles at the end of this section. ~ Begin Center ~ Begin TableFigure { #tab-communication-styles-overview; \ caption: "Communication Styles"; \ page-align:here;} |------------------|--------------------------|---------------------------| | | One-to-One | One-to-Many | +------------------+--------------------------+---------------------------+ | **Synchronous** | Request/response | — | | -----------------|--------------------------|---------------------------| | **Asynchronous** | Notification | Publish/subscribe | | | Request/async.\ response | Publish/async.\ responses | |------------------|--------------------------|---------------------------| ~ End TableFigure ~ End Center Asynchronous one-to-one messaging is inherent to the actor model. The message-sending primitive realizes notification style communication. Responses are asynchronous too. Akka provides the `sender()` method within actors. The method produces the `ActorRef`{language:scala} of the originator of the current turn's message. Microservices achieve asynchrony via message queues. Responding depends on the channel protocol. AMQP does not transmit the sender's location. If we must send a response, we need to include the location information into request messages individually. We can model synchronous one-to-one messaging on top of the actor messaging primitive. Every synchronous communication can be expressed using asynchronous constructs in general, and vice versa [@Agh97]. Akka provides linguistic support for request/response messaging through the `ask [?]` method of `ActorRef`{language:scala}. Microservices utilize network mechanism dedicated to the synchronous interaction pattern. In the service context, REST is the most prominent example. One-to-many communication is neither inherent as a primitive to actors nor microservices, and therefore requires additional effort. Conceptually, we model the communication style by sending a message to each intended recipient in a notification fashion (message broker). Akka has message broker capability in their message router constructs through the `BroadcastRoutingLogic`{language:scala}. Echo implements the routing logic inside a separate actor. In Echo's MSA, the RabbitMQ service does not suffice for one-to-many message distribution. AMQP only supports what the JMS terminology calls *queue* semantics (one-to-one), but not the JMS *topics* (one-to-many). We must employ another messaging technology. *Kafka* [@ApacheKafka] is a widely adopted publish/subscribe streaming system with very lightweight message constructs. One important realization is the difference in messaging interfaces. While Akka provides few but homogenous interfaces (`tell [!]`, `ask [?]`, and `RoutingLogic`{language:scala}s) across all language bindings, the microservice model does not enforce any kind of interface. REST, AMQP and Kafka all have different interfaces. Since all Echo services are based on Spring, at least we express the interaction with each communication mechanism in the same way within each service. But the microservice paradigm is also open for arbitrary technology stacks, and every stack provides its own interface for each mechanism. ~ Begin Center ~ Begin TableFigure { #tab-communication-styles-comparison; \ caption: "Comparison of communication styles and their implementation constructs as we express them in Akka and a Spring-based MSA"; \ page-align:here; } |-------------------------|---------------------------------------------|---------------------------| | Communication Style | Akka | Spring MSA | +-------------------------+---------------------------------------------+---------------------------+ | Request/response | `ask` method of `ActorRef`{language:scala}, | Remote procedure call | | | delegation pattern | with REST | | ------------------------|---------------------------------------------|---------------------------| | Notification | `tell` method of `ActorRef`{language:scala} | Message queue service | | | | like RabbitMQ | | ------------------------|---------------------------------------------|---------------------------| | Request/async response | Request and response with | Request and response | | | `tell` method of `ActorRef`{language:scala} | through message queue | | ------------------------|---------------------------------------------|---------------------------| | Publish/subscribe | Router with | Message broker service | | | `BroadcastRoutingLogic`{language:scala} | like Kafka | | ------------------------|---------------------------------------------|---------------------------| | | `BroadcastRoutingLogic`{language:scala} | Request through message | | Publish/async responses | for request, responses with | broker, responses through | | | `tell` method of `ActorRef`{language:scala} | message queue | | ------------------------|---------------------------------------------|---------------------------| ~ End TableFigure ~ End Center ### Conception of Concurrent Execution {#sec-conception-concurrent-execution} Actors and microservices are both concurrently executed components within their system architectures. However, their execution-modalities are fundamentally different. Hence, both constructs have different *notions* of concurrency. Each notion is a direct result of the underlying concepts. #### Continuations, Threads and Processes Scala's original actors provide two different execution semantics [@Hal09]. One of the semantics schedules the actors on threads. These actors are executed in an inversion of control manner [@Hal06], similarly to the strategy pursued by the Spring framework. The other of the two semantics is purely event-based and therefore without IoC. Thread-based actors are invoked by their current thread to execute a turn. Upon completion, the actor returns to the calling thread. On the other hand, event-based actors do not have (or need) a dedicated thread and therefore cannot return to one. Instead, they facilitate a much cheaper concept called *continuation-passing style* known from functional programming. A function refrains from returning a computed result and calls a subsequent function instead, the so-called *continuation closure*. Akka extends this approach and defines a single closure for all messages until we replace the behavior. This approach is more effective [@Hal12]. We already gave an example of a continuation with `Actor.same`{language:scala} for Akka's type-restricted behaviors. `Dispatcher`{language:scala}s influence the thread assignment strategy for concurrently running the behavior closures [@Hal12]. Although it uses threads, we still consider the continuation closure approach as event-based, since a thread can be seen as merely a trajectory in continuation space [@Shi97]. Nevertheless, threads are conceptually similar to processes, except that several threads exist within a single process [@Bac03]. There, the actor thread-pools live in one or few processes and many actors share the same memory boundaries of their respective process. Although they execute on top of threads, the isolated turn principle essentially defines actors as single-threaded entities. The combination with asynchronous message passing allows the runtime to concurrently execute these logical single-threaded components. In general, an actor has no notion of concurrency at all. However, we have demonstrated how to introduce additional concurrency constructs into the scope of actors, as long as these additional constructs do not break the actor model semantics. Section [#ch-actor-impl] demonstrated futures with their pitfalls as one option. In combination with the continuation abstraction, Akka actors are powerful and still extremely light-weighted constructs. On the other hand, microservices are concurrent distributed processes. The model paradigm tolerates a more widespread notion of concurrency than actors do. Internally, services do not have a counterpart to the isolated turn principle. Though a service also receives messages via a public API and reacts to them, the MSA style permits design flexibility regarding internal task unit concurrency. For example, a widespread strategy in the domain of distributed systems is to utilize several threads to perform blocking operations [@Tan07]. A microservice applies this strategy to perform blocking operations without blocking the service's entire process, and react to numerous messages simultaneously. Spring's IoC container provides this strategy automatically for requests through its thread-pool. The drawback of this degree of freedom is the set of issues internal concurrency introduces. In general, accessing state is not a safe operation anymore, if the respective state is read- and writable across threads. We must use synchronization then, e.g.\ in the form we have demonstrated in the Java case study. When we program with microservices, we are therefore not free from the many hassles of low-level concurrency per se. We have seen this in Echo's MSA variant, where we use transactional memory for synchronization. However, a service's scope of responsibility limits the service's size. Consequently, the size also limits the internal concurrency considerations of a service relative to the overall system. As a result, linguistic approaches to SOC tend to avoid internal concurrency considerations completely by applying an idea resembling the C processes case study of Section [#sec-concurrency-os-level]. For example, Jolie offers the `concurrent`{color:blue} primitive as one option for the execution modality of services. The primitive spawns dedicated processes to concurrently execute the service behavior in response to messages [@Mon16b]. This idea has close resemblance to the cameo delegation pattern of actors. Of course, the design freedom of the MSA style also allows actor-based concurrency internally. The benefits of synchronization-free programming can be harvested by microservices too. #### Distribution and Location Transparency Communication via message passing has one fundamental property: no mutable memory is shared between the communicating components. Conceptually, message passing does not require the components within the same memory space. As a result, it does not matter whether the components run on the same core, different processors or even different host machines [@Fel90]. In short, message passing enables distribution. The actor model builds upon message passing to share state information between actors. Additionally, actors are well isolated from each other. Based on these properties, Agha [@Agh85a] recast the initial notion of actors in the light of distributed computation. All actor addresses, and therefore also Akka's `ActorRef`{language:scala}s, make the location of the underlying actor transparent. A runtime system that is part of a cluster handles the actual message delivery on the same local node and on remote nodes. Addresses provide a uniform interface free of location considerations. Microservices intrinsically fall into the domain of distributed systems as well, if the services leverage a network-based communication mechanism. There is no guarantee of a unified channel interface for all services and mechanisms. We have given Unix pipes as an illustrative mechanism. Pipes facilitate the file descriptor interface that Unix promotes. This interface is limited to local node interaction, that is the memory boundaries of a single host OS. Channels operating on the network level are distributed communication mechanisms. Network channels facilitate message passing and provide a uniform interface for remote as well as local communication [@Spe90]. Sockets are the most basic example. The socket interface is homogenous regarding whether a socket is on the same local or a remote node. However, sockets still require the *concrete* address [@Bac03]. In order to have the recipients' location transparent, the microservice paradigm requires additional effort in the form of discovery mechanisms. We have demonstrated how Echo integrates discovery based on Feign and Eureka directly into the client-side communication interface using Spring's declarative programming style. #### Fairness and Resource Consumption Actors and microservices represent concurrent building blocks of equal status in their respective architectures. It is important to reason about their chances to make even progress, since we have a uniform view on concurrent execution [@Bus90]. Our view does not distinguish quasi-simultaneous execution on a single processor, truly parallel execution on multiple CPUs, and distributed execution among several host machines. The property of uniform progress is called *fairness* and is closely related to the liveness of concurrent programs and systems, i.e.\ to avoid starvation [@Agh85a;@Agh99]. Actors are entities inside an actor runtime. Their scheduling is the responsibility of this runtime system, viz.\ the system's execution strategy. As passive components, actors have no proactive sense and therefore merely react to events (received messages). The actor system delivers the message and assures that the receiving actor processes the message eventually. Therefore, the runtime must schedule every actor regularly to prevent starvation [@Kan12;@Kar09]. The major benefit is that actor systems can greatly reduce processing resource consumption. Given an actor's mailbox is empty, the runtime does not have to invoke the actor, since there is no work to do [@Agh14]. On the other hand, scheduling is a nonconcern for microservices. Every MSA is a composition of concurrent distributed processes. The MSA implicitly delegates the scheduling to the host operating system(s) -- namely their *scheduling policies* [@Bac03]. We cannot make specific assumptions on execution rates in general, but require these rates to be positive. This *finite progress assumption* [@And83] is the foundation of liveness for every microservice. However, there is one drawback to the lack of scheduling concern in MSAs. The architecture cannot influence the resource consumption based on a service's actual demand. An operating system always allocates resources towards every process. Therefore, every task unit of an MSA also stresses its hosts processing power on a regular basis, at least to a small amount. This drain on resources exists even when the services neither have pending requests nor perform active behaviors of their own. In these cases, every process activation is simply a waste of energy and host resources [@Tan07]. Table [#tab-fundamental-concurrency-issues] gives an overview of how actors and microservices meet fundamental issues of concurrent programming. ~ Begin Center ~ Begin TableFigure { #tab-fundamental-concurrency-issues; \ caption: "Comparison of Akka actors and Spring-based microservices meeting fundamental issues of concurrent execution"; \ page-align:here; } |-----------------|--------------------------------|---------------------------------| | Issue | Akka | Spring MSA | +-----------------+--------------------------------+---------------------------------+ | Expression | Actor object, concurrent | Service program execution, | | | execution by runtime | concurrent scheduling by OS | | ----------------|--------------------------------|---------------------------------| | | Message passing primitives | Message passing IPC mechanisms, | | Communication | (e.g.\ `tell`, `ask`), uniform | no shared memory, no uniform | | | interface across all actors | interface across all services | | ----------------|--------------------------------|---------------------------------| | | Implicit among actors due to | Implicit among services due to | | Synchronization | message passing, single- | message passing, potentially | | | threaded semantics internally | required internally | | ----------------|--------------------------------|---------------------------------| | Progress | Guaranteed by runtime | Expected from operating system | | ----------------|--------------------------------|---------------------------------| ~ End TableFigure ~ End Center ### Scalability and Modularity {#sec-scalability-modularity} Their ability to *scale* is arguably one of the most relevant reasons given in the literature to utilize actor- and microservice-based architectures [@Tas13;@Hal09;@Mon16a;@Dra17c;@Sal16;@Che17;@Sal16;@Dra17b]. Based on the definition given by Bondi [@Bon00], *scalability* is an attribute that influences the performance of networks, systems and processes in general. From the empirical knowledge of industrial Erlang applications it has been suggested that scalability is more important than raw system performance [@Hal12]. Many different aspects influence scalability. From a concurrent point of view, every influence hindering parallelism has a negative impact. Examples are synchronization (cf.\ Java case study in Section [#sec-concurrency-language-level]) and (temporal) deadlocks. The strong isolation and message passing principles of actors and microservices (isolated turn principle, avoidance of shared memory) reduce coordination and contention cost. Therefore, message passing and isolation have a positive influence on scalability capabilities by limiting safety and liveness issues [@Bon16]. #### Forms of Scalability Many different forms and classification approaches for scalability exist. Two merit attention here. *Load scalability* refers to a steady performance if the demand or work increases. *Structural scalability* refers to the ability of the topology to change the amount of components, in this case concurrent task units [@Bon00]. Two notions of scaling a system are relevant to us. *Vertical scalability*, or simply *scaling up*, refers to an increase of resource utilization, especially multiple cores and memory on a single host. The influencing factors are asynchronous messaging, refrain from blocking, and synchronization. Actors conceptually support these requirements well, as long as the units refrain from blocking inside their turns. The microservice approach meets the requirements also quite well, when services back asynchronous communication mechanisms and refrain from RPCs. In general though, the internal concurrency capability introduces the risk of facilitating potential hindrances of scaling up. Besides, the scheduling efforts of actor runtimes and operating systems aim for an optimal utilization of available resources [@Bac03]. *Horizontal scalability*, also *scaling out*, *distance scalability* or *geographical scalability*, refers to the utilization of additional hardware resources (hosts). Distribution capability is the prerequisite. The uniform abstraction of concurrent and distributed execution of actors as well as the process nature of microservices combined with network IPCs provide the foundation to scale out [@Bon00;@Dra17c;@Dra17b;@AkkaMDR;@Agh99;@Tan07]. One approach to achieve scaling out is the concept of load balancing we have already discussed. Akka actors build upon the same conceptual ideas using the router constructs. They allow us to distribute work in various strategies, e.g.\ round-robin or broadcast. Round-robin is one example of a load distribution strategy. Server- and client-side load balancing is merely the distinction of a `Router`{language:scala} within the sending actor itself, or inside an intermediate routing actor. Microservices either use dedicated balancing services for server-side balancing, or client-side mechanisms like Ribbon. In one way or another, load balancing is a valuable concept for both actors and microservices, e.g.\ to avoid overflowing mailboxes and to enable timely responses to requests. Some task units are naturally well suited for load balancing capabilities [@Mon16a;@Car17], e.g.\ Echo's API Gateway. The major difference lies in the trade-off that comes with load balancing in each model. As we have mentioned, a `Router`{language:scala} is a supervision-managed set of routees and therefore brings all obligations of actor supervision. Microservices are more loosely coupled and do not know the concept of supervision in general. Hence, load balancing for microservices does not come with additional obligations, except the operation of the additional server-side balancing service. A conceptual disadvantage of load balancers is that the balancers do not know about the progress of recipients by default. The work gets distributed regardless of the current capacity of the receivers. Load balancers can take work load metrics into account of course, as does for example the `SmallestMailboxRoutingLogic`{language:scala} of Akka. This routing logic aggregates the mailbox capacities of all routees first. With this information, the logic then forwards the message to the actor with the smallest mailbox. This capacity aggregation *before* the message dispatch increases the overall processing time [@Roe15;@All13]. Another kind of load balancers are message queue channels. The receiving components actively pull messages from the queue when they have computation capacity. Therefore, the tasks distribute load among themselves based on their demand [@Cou05;@Dra17c]. #### Dynamic Reconfiguration *Dynamic reconfiguration* relates to a change in a system's topology at runtime. We can add, remove or relocate task units divergent from the static initialization configuration [@Agh85b]. Actors support dynamic reconfiguration inherently through the primitive that allows actors to spawn new actors. We already demonstrated this ad hoc instantiation with the dynamically created response handlers that the Searcher spawns for the delegation-based synchronous communication. The loose coupling of microservices also provides opportunities for changing topologies. Discovery registries and asynchronous messages through intermediate message queues provide the foundation. Reconfiguration greatly effects scalability, because it increases the optimal utilization of hardware resources [@Kan12]. A general prerequisite for dynamic reconfiguration is location transparency. Actors as well as microservices have sophisticated solutions. Two additional properties, *mobility* and *elasticity*, become possible as a result. Mobility refers to the relocation of components between nodes [@Agh99;@Kan12;@Tan07]. Elasticity is a form of scalability summarizing the ability of a system to scale the number of components dynamically depending on the current demand. Hence, elasticity improves the load scalability while minimizing the resource consumption. We can scale actors and microservices in a non-uniform way due to their component properties. Each individual component type allows us to incarnate many instances without the requirement to duplicate the residual component types as well (in contrast to classic monolithic applications) [@Dra17b;@Dra17c]. Echo does not provide mobility nor elasticity. However, the general approach is conceptually identical for actors and microservices, therefore we present a short outline. In principle, we are able to create and terminate stateless task units easily on demand (elasticity). Since these units have no state that requires relocation, this reconfiguration also doubles as mobility. Akka does not provide strong mobility that requires the migration of state. Other runtimes like Orleans do provide state migration [@Ber14]. Stateful Stores are a bigger concern. In general, when we (re-)created a stateful unit, it is not safe to assume that its persistent state (database, reverse index) is up to date. Therefore, we must update the state with regard to all outstanding modifications. *Event sourcing* is a convenient concept which persist all modifying commands to Stores in a so-called *event log* [@Ber16;@New15]. A new or reactivated Store requests the history of modifications prior to the unit's existence. Message brokers have the potential to double as event logs. Kafka offers optional persistence support for messages. Akka's *Persistence* module is also convenient to introduce persistence of routed messages. In both cases, it is within the obligation of the Store units to persist a counter or reference to the last received event separately. This reference is needed to determine the required partial history. In general, we deem the concerns related to dynamic reconfiguration more easily met with actors. Spawning new units is a core concept of the basic model primitives. Microservices themselves have no general notion of other services beyond interaction. We need an additional layer to manage changing topologies. *Cloud management frameworks* are a category of tools that seem convenient. Table [#tab-scalability-compatibility-matrix] summarizes the capabilities for each scalability variant provided through Akka and Spring. ~ Begin Center ~ Begin TableFigure { #tab-scalability-compatibility-matrix; \ caption: "Capability matrix of scalability variants and their support by Akka actors and Spring-based microservices"; \ page-align:here; } |------------------------|-----------------------------------|---------------------------------| | Scalability Form | Akka | Spring MSA | +------------------------+-----------------------------------+---------------------------------+ | | Very efficient resource | Limited by the synchron- | | Vertical scalability | utilization if no blocking | ization of the internal service | | | inside actor | concurrency (STM) | | -----------------------|-----------------------------------|---------------------------------| | Horizontal scalability | Akka cluster | Requires network IPC | | -----------------------|-----------------------------------|---------------------------------| | Load scalability | Server-side load balancing | Client-side load balancing | | | with routing logic | with Ribbon, message queues | | -----------------------|-----------------------------------|---------------------------------| | Structural scalability | Location transparency | Requires discovery mechanism | | | inherent in actor addresses | for location transparency | | -----------------------|-----------------------------------|---------------------------------| | Dynamic | Inherent through basic | Requires service registry for | | reconfiguration | model primitive | integration | | -----------------------|-----------------------------------|---------------------------------| | Mobility | Weak mobility (no relocation of persistent state of Stores) || | -----------------------|-----------------------------------|---------------------------------| | | Requires resource control | Requires appropriate cloud | | Elasticity | of cluster (see [@Moa17]), not | management framework, | | | supported by Echo | not supported by Echo | | -----------------------|-----------------------------------|---------------------------------| ~ End TableFigure ~ End Center #### Extensibility and Technology Diversity {#sec-technological-heterogeneity} Another result of the reconfiguration capability is that actor and microservice architectures are also open for extension. In contrast to dynamic reconfiguration, where we add or remove instances of existing task units at runtime, we use *extensibility* to refer to the introduction of either new versions of existing components (update) or new components entirely (addition). Extensibility in general benefits from high cohesion and low coupling of the components. There are two different kinds of extensibility [@Agh85a;@Bac03]: * static, where we adapt the architecture's code, recompile and then redeploy * dynamic, where we add a new component to the architecture at runtime The independent deployment capability of each single service engine allows us to simply add new components at runtime. The reconfiguration does not impact existing services. New services simply consume the existing services. We then gradually update old services to let them integrate with new components [@Mon16a]. Actors face more challenges. Every actor requires an actor system to exist within. Erlang and its runtime system were tailored to support actors. As a result, the *BEAM* virtual machine supports code loading and replacement for live upgrades [@Vin07;@New15]. The JVM does not support similar features. Therefore, Akka requires the restart of the system to introduce new kinds of actors. The same program structure defines all actors (monolith). Subsequently, the actors compile into a single monolithic executable, which limits Akka actors to static extensibility. In a clustered setup however, we do not need to introduce a new component into all cluster nodes at once. Therefore, the cluster is able to retain its uptime, while we upgrade and redeploy the individual cluster nodes. Besides differences in the VM optimization, Erlang's upgrade strategy is conceptually similar. The BEAM does not support the replacement of single actors, but merely of entire code modules [@New15]. Another interesting concern is the technology limitations regarding the conception of new components. An actor is bound to its runtime, which is free to provide interface bindings for numerous programming languages, e.g.\ as Akka does for Java and Scala. We are able to use the Java binding from other JVM languages as well, as [@Sub11] demonstrates for Groovy and JRuby. However, we cannot overcome the JVM as the target platform. Interoperability with Akka.NET would broaden our possibilities, but to our knowledge interoperability is not available. The strong memory boundaries and open communication interfaces of microservices provide a whole different level of flexibility. We can conceive a service using every programming language and technology we desire, as long as the tools are able to interact with the open communication channels. Echo's services facilitate Java and the Spring framework for all microservice components. However, we could have also written every service with a different technology stack. Even now, we can replace existing services by new versions using different technologies. ~ Begin Center ~ Begin TableFigure { #tab-modularity; \ caption: "Comparison of modularity capabilities of Akka actors and Spring-based microservices"; \ page-align:here; } |-------------------------|----------------------------------|----------------------------------| | Characteristic | Akka | Spring MSA | +-------------------------+----------------------------------+----------------------------------+ | Extensibility | Static (phased restarts) | Dynamic | | ------------------------|----------------------------------|----------------------------------| | Technological | Compatible to the Akka interface | Open for arbitrary technology | | Diversity | (JVM technology), no compatible | stacks due to open interfaces | | | runtimes available today | and communication channels | | ------------------------|----------------------------------|----------------------------------| ~ End TableFigure ~ End Center ### Integrating Actors and Microservices {#sec-actor-ms-integration} So far, our evaluation has shown that we can express the same capabilities with both actors and microservices. Both constructs isolate state, communicate through a wide range of interaction styles, and their execution modality enables parallelization and distribution in a transparent way. The major benefit of all these capabilities is a high degree of modularity as well as great flexibility for scaling. Each model brings its own set of trade-offs, and we as programmers must accept one of these sets in order to leverage the capabilities. As a final capability evaluation, we want to reason whether all these properties enable actors and microservices to integrate as equal concurrent task units. We have already seen that the combination of concurrency models is a common and important practice. Actors often incorporate futures, and microservices use and mix arbitrary concurrency constructs internally. We will entertain this integration thought first with some theoretical considerations and then discuss the practical approach regarding Akka and Spring. As we have pointed out, there are two kinds of correctness properties: safety properties and liveness properties. The combination of two concurrency models must not weaken the safety and liveness properties. There are also two approaches to assess the correctness of a computation: *testing* and *verification*. From a theoretical point of view, we can violate every safety property by a finite execution. Hence, we can test for safety at runtime. On the other hand, liveness properties cannot be violated by some finite execution in general. Even if an arbitrary finite execution causes a violation, there is some continuation of the same execution for which the property still holds eventually [@Siv99]. Thus, we cannot test for liveness at runtime. This theoretical limitation raises the need for other concepts to either guarantee or at least inspect the liveness of tasks. We require strict theoretical frameworks to apply formal verification techniques [@Sin09]. For actors, the theoretical actor model provides this framework. However, we have seen that rather pragmatic rules define the microservice model in comparison. We yet lack a formal framework. #### Actor Model and Process Calculi Among the theories of formulating concurrent computation we find a family called *process calculi* or *process algebras*. These calculi define formal models composed of so-called *processes*[^fn-process-concurrency-theory] which communicate within the laws and conditions laid out by their theory. Baeten [@Bae05] defines *process* as any kind of *behavior* of a *discrete event system*, such that it is observable through discrete actions. These actions include interactions with other discrete event systems. The other systems then react to these interactions. Therefore, Baeten terms all interacting systems as *reactive systems*, which are the base for parallel and distributed computing. As a result, one approach towards concurrency theory is the path of process algebras. [^fn-process-concurrency-theory]: These are the processes of concurrency theory we have mentioned before. We must not confuse them with operating system processes. Some process calculi gained considerable prominence. Examples are Milner's *Calculus of Communicating Systems* (CCS) [@Mil80] that was the initial work in this domain. Hoare's *Communicating Sequential Processes* (CSP) [@Hoa78] was the first to introduce message passing instead of global variables for process communication. For our considerations, the *π-calculus* [@Mil92], also by Milner, merits special attention. The π-calculus has a notion of process networks, including mobility and dynamic reconfiguration [@Bae05;@Mon97]. Much work was done on the field of process algebras, since they allow us to express a theoretical basis for arbitrary domains and requirements. In fact, more practical approaches to express interacting processes frequently have their foundation in a process calculus. Some calculi also suit well for microservices. The Jolie language for example is based on a calculus dedicated to service-orientated computing called *SOCK* (__S__ervice __O__riented __C__omputing __K__ernel) [@Gui06]. In turn, SOCK is inspired by CCS and the π-calculus. This relation of process algebra and microservices is especially interesting, since the actor model and process calculi share a long history. Hewitt as well as Milner published their initial works on the actor model and CCS in 1973. Since then, these two approaches mutually influenced and inspired the scientific development of each other. Fitting examples are the *Asynchronous Sequential Processes* (ASP), which bear close resemblance to active objects, but with a more coarse granularity [@Kos16]. Scholars have long tried to formulate a theoretical link between actors and various calculi, with mixed and mostly limited success. To our knowledge, to most promising approaches in the literature so far merely succeeded at describing interoperability between actors and some calculi that show a strong similarity. For example, Montanari & Talcott [@Mon97] demonstrate the cooperation of actors and agents of the π-calculus. As Agha *et al.*\ [@Agh97] discuss in an extensive work, a true equivalence theory among a formal *actor calculus* and some process calculus requires the formulation of a *simulation relation* among the primitives. This was done among different process calculi but is yet unfound regarding actors. Agha *et al.*\ state as the foundational challenges: > "Three points of contrast between the basic actor model and process calculi are: the choice of communication model, the choice of communicable values, and the issue of fairness." Note that we have discussed these issues as concerns in varying degrees throughout this thesis. Eventually, Agha *et al.*\ argue that instead of trying to find an analogy between an actor- and a π-calculus, it is expedient to engage in the definition of high-level semantics for programming languages. Then, we are able to reason about program equivalence of actors- and π-programs. #### Combining Akka Actors and Spring Microservices {#sec-combining-akka-spring} Since we did not implement our Echo microservices in a SOP language like Jolie, we did not provide them the formal foundation of a process calculus. We would not be able to reason about the correctness properties after integration anyway, because there is no known theoretical link between the actor model and a SOC calculus yet. Nevertheless, we have seen that scholars are still motivated to describe interoperability between actors and calculus-based processes. Therefore, we think about the approach to integrate our Akka actors and Spring-based microservices. From a theoretical point of view, two concurrent units require a shared communication channel in order to interact [@Agh97]. The microservice principles allow the services a rather high degree of freedom regarding their communication. REST-based state transfer via HTTP is a popular choice. We have demonstrated that AMQP is also a viable channel. Actors, on the other hand, are more restricted, since all actors must receive the *message* constructs in a uniform way. By implication, this restriction means that we can still use different communication channels as long as the channels comply to a homogenous message receiving abstraction. *Akka HTTP* [@AkkaHTTP] is part of the Akka library collection. It provides a full client- and server-side HTTP stack on top of the basic actor construct. This way, actors receive HTTP requests like ordinary messages, just as if the messages were sent by other actors: ```{language:scala} override def receive = { case SomeMessage(_) => // handle message case HttpResponse(status, headers, entity, _) => // process response } ``` A unique address identifies every actor. This address is specific to a concrete actor system and not conform to a URI (__U__niform __R__esource __I__dentifier) as is required by HTTP in general. We must utilize an integration layer between a HTTP-addressable unit and the actual actor [@AkkaHTTP]. Though this style of receiving messages is in principle conform to the actor abstraction, there are certain limits. By implication, HTTP endpoints also do not comply to actor addresses in general. The standard *send* mechanism through `tell` and `ask` therefore do not qualify to dispatch HTTP requests. We require an alternative interface. The example below uses `pipeTo` to have the integration layer reroute the response as an `HttpResponse`{language:scala} message to the sending actor (delegation): ```{language:scala} Http().singleRequest(HttpRequest(URI(s"http://example.com") .withQuery(s"name", s"value")) .pipeTo(self) ``` We see, not all the basic actor primitives suit for usage outside the actor construct. However, well introduced abstractions like Akka HTTP allow actors to provide their *actor behavior* to the outside in a microservice-like *service behavior* fashion. Communication between actors and microservices is possible in principle. Hence, we can construct systems which distribute tasks between actors and microservices. The general integration idea is not novel. We have mentioned the theoretical foundation from [@Mon97] for incorporating actors and agents of the π-calculus. They also use so-called *actor-π coordinators* to translate between communication channels. These coordinators are similar to the integration layer of Akka HTTP. However, interoperability is merely a concern of static configuration, because processes have only a static notion of interconnection topology in general. This static notion violates the dynamic reconfiguration inherent to actors [@Akm90;@Agh85b]. In order to overcome this limitation, microservice architectures utilize bridge technologies like service discovery. Hence, we require actors to integrate into this discovery mechanism, effectively rendering each actor into a mere microservice. This unified approach is not quite common yet, but technologies start to emerge which build on this idea. An example is *Lagom* [@LightbendLagom], a reactive microservice system framework built on top of Akka. ~ Findings [Main findings]{.findings-title} + Process calculi provide the theoretical frameworks for formal microservice specifications and service-oriented computing languages. + The theoretical link between actors and process calculi is yet an open scientific question. + Actors and microservices can integrate without a formal basis. However, the correctness properties of the models are then not guaranteed. {.findings-list} ~ ### Software Artifact Analysis Sections [#ch-actor-impl] and [#ch-microservice-impl] covered the solution strategies when we program with actors and microservices. The previous sections of this chapter compared the resulting capabilities. Now we give attention to the effects that the programming models have on the resulting software artifacts when we express these capabilities. We concern ourselves with three metrics: the *lines of code* (LoC) it takes to express the respective functionality, the *size* of the resulting artifacts, and the *startup time* of each artifact. All Akka actors are compiled into a single monolithic application. Therefore, the respective Akka metrics refer to the resulting monolith. The Spring-based microservices are independent programs, so we give the metrics for each service engine separately. The LoC give us an indication of the programming effort is takes to implement a given functionality. We count the LoC using the CLOC [@CLOC] tool. Since Spring Boot relies heavily on configuration to adapt it's default behavior, we count the content of configuration files as source code. We measure size in terms of the bytes of each JAR (__J__ava __Ar__chive) file. There are several ways to package a JAR. Two kinds are relevant to us. We define a *skinny JAR* (sJAR) as the archive that contains merely the bytecode and direct resources (e.g.\ property files) of a program's source code. Subsequently, we use *fat JAR* (fJAR) for an archive that contains the data of the skinny JAR version, together with its direct dependencies (e.g.\ other libraries in their skinny JAR version), and the deployment information that a standard Java runtime environment requires to execute the application. Fat JARs are therefore executable artifacts. The startup time is the time from process incarnation until the application is fully operational. Table [#tab-artifact-metrics] gives the metrics for our Echo artifact implementations. As a reminder, the Core artifact is the library that implements the domain-specific logic. Hence, Core's skinny JAR is part of all fat JARs of the microservice engines as well as the monolithic Akka backend. The Web application is the frontend client that connects to both Echo backend implementations. We give the LoC metric of the Web application merely as a reference relative to the other artifacts. ~ Begin Center ~ Begin TableFigure { #tab-artifact-metrics; \ caption: "Lines of code, bytecode sizes, and startup times of software artifacts"; \ page-align:here } |-------------------|------|-----------| ----------| --------------| | Artifact | LoC | sJAR (KB) | fJAR (KB) | Startup (sec) | +-------------------+-----:+----------:+----------:+--------------:+ | Akka backend | 4487 | 1004.3 | 76 775.1 | 5.5 | | ------------------|------|-----------|-----------|---------------| | CatalogStore (MS) | 1838 | 56.1 | 89 225.8 | 14.6 | | ------------------|------|-----------|-----------|---------------| | IndexStore (MS) | 724 | 23.8 | 83 518.2 | 8.8 | | ------------------|------|-----------|-----------|---------------| | Searcher (MS) | 656 | 22.2 | 81 754.4 | 8.1 | | ------------------|------|-----------|-----------|---------------| | Web Crawler (MS) | 716 | 23.5 | 83 517.9 | 9.2 | | ------------------|------|-----------| ----------|---------------| | Parser (MS) | 703 | 24.2 | 83 519.1 | 8.6 | | ------------------|------|-----------| ----------|---------------| | Registry (MS) | 334 | 9.9 | 90 699.7 | 9.4 | | ------------------|------|-----------| ----------|---------------| | Gateway (MS) | 889 | 30.5 | 83 655.1 | 9.7 | | ------------------|------|-----------|-----------|---------------| | Updater (MS) | 693 | 23.9 | 83 518.3 | 8.7 | | ------------------|------|-----------| ----------|---------------| | Core (library) | 5203 | 323.1 | — | — | | ------------------|------|-----------| ----------|---------------| | Web (frontend) | 3144 | — | — | — | | ------------------|------|-----------| ----------|---------------| ~ End TableFigure ~ End Center The Akka backend engine has of course by far the most lines of code. A monolith implements the whole system within a single artifact, while each microservice merely implements a part of the overall system. Note that the Core library implements the DTO classes we use to send asynchronous messages via [AMQP.]{tex-cmd-after:"\,"} The microservices do not implement these DTOs themselves within their codebases. The resulting overall LoC sum of all microservices is 6553. This makes the microservice codebase about 46\ % larger than the actor codebase. We see, although Spring provides very expressive declarative programming APIs, the Akka interface is still less verbose. However, the more compact syntax of Scala compared to Java also contributes to the difference. Even more interestingly, each microservice engine takes considerably more time to startup than the entire Akka backend engine. The inversion of control model that powers the declarative programming style of Spring adds considerable runtime overhead. The sum of skinny JAR sizes of all microservice codebases is 241.1 [KB.]{tex-cmd-after:"\,"} This size is considerably less compared to the 1004.3 KB of the skinny Akka backend. We have analyzed the content of the respective skinny archive files and made the following observations that affect the difference in bytecode size: * The actor codebase is written in Scala, and we make heavy use of `case class`{language:scala} constructs. Scala's `case class`{language:scala}es are very compact class definitions that are mostly written in a single line of code. Though these class definitions are compact, they still compile to dedicated `.class` files. Instantiated objects are immutable, hence they suit very well for the objects we exchange as messages in actor programming. Messages in the actor model are not only for transferring data, but also to transmit commands. Thus, we use very fine-grained message types, and subsequently a lot of `case class`{language:scala} definitions. Few lines of code for these definitions let the compiler produce lots of separate `.class` files. In contrast, all the DTO classes for the messages between microservices are part of the shared Core library. The bytecode sizes of these classes consequently do not participate to the sJAR size of each microservice's artifact. * Scala is primarily a functional programming language. Hence, we make frequent use of so-called *anonymous functions*. Since Scala targets the JVM, the compiler evaluates these anonymous functions as instance creation expressions of the `Function`{language:scala} class. Subsequently, every anonymous function compiles into a dedicated `.class` file too [@Ode16]. * Spring provides a lot of utility functionality transparently, for example DTO class to JSON marshalling. Akka HTTP requires us to use the *Spray* [@LightbendSpray] library to define custom JSON serializer classes manually. These serializers compile to relatively significant bytecode sizes (e.g.\ merely 31 LoC produce about 200 KB[^fn-uncompressed-class-file]). [^fn-uncompressed-class-file]: A JAR file is essentially a compressed archive file. The direct size of a `.class` file does not contribute at a ratio of 1:1 to the sJAR size. The fat JAR sizes show the impact of the declarative programming style of Spring. The price for the relatively low LoC of each service (and subsequently the reduced effort to write many distinct programs) is that the compiler adds lots of dependencies into the fJAR executables. These dependencies come into play at runtime to realize the declared functionality. As a result, every microservice engine is larger (in bytecode size) and upon incarnation also considerably slower than the entire actor engine. ~ Findings [Main findings]{.findings-title} + Programming actors demands less program code than microservices. Even compact programming styles cannot compensate the overhead in LoC from several codebases. + Spring produces huge executable artifacts. Their bytecode size and the overhead at startup is the price for the reduced programming effort. + Microservice architectures are drastically larger in terms of executable component sizes compared to a monolithic actor architecture. {.findings-list} ~ ## Efficiency and Benchmark {#sec-eval-efficiency} We have compared the expressiveness and conceptual capabilities of actors and microservices, and demonstrated that both are able to meet similar concerns. Now we are interested in the efficiency we leverage from each programming model. The Echo system implementations provide us with the foundation for a benchmark of the two models. ### Performance Metrics A benchmark requires measurable and comparable metrics. As we have already mentioned, information retrieval traditionally uses precision and recall metrics to evaluate search engines. However, precision and recall assess the effectiveness of the retrieval techniques. IR effectiveness is not within the scope of this thesis. We require metrics which reflect efficiency. These metrics must be applicable to actors, microservices, and our scenario. *Savina* is a benchmark suit specifically designed to evaluate actor libraries [@Ima14]. Profiling studies used Savina to gather detailed metrics for Akka [@Ros16a;@Ros16b]. However, the benchmark suit as well as the profiling tools are actor model and Akka specific. Hence, the metrics provided by the profiling are tailored to the actor model. Recent works point out that there is still a lack of microservice benchmarks [@Zho18]. Due to the technological diversity, there is also no general microservice profiling tool available. Hence, the literature does not establish widely agreed upon metrics yet. We must revert to model unspecific metrics. Besides a lack of common metrics established between actors and microservices, there is also no general simulation approach for MSAs as of yet [@Gri17]. Additionally, we have to design a custom experiment too. Lillis *et al.*\ [@Lil09] and Pedzai & Suleman [@Ped06] each use techniques we discussed in this work to improve the performance of search engines. Examples are synchronous/asynchronous and one-to-one/one-to-many communication styles, message brokers, and lifecycle management (cf.\ Akka runtime, Spring IoC). Among other things, they evaluate the performance by measuring the time it takes a system to index a given number of documents. The retrieval subsystem's performance is the time it takes to process a given number of queries. We take this experiment design and use it to assess the efficiency of our actor and microservice implementations. ### Simulation Workloads Echo has two essential subsystems: the indexing subsystem and the retrieval subsystem. Since search requests to the retrieval subsystem do not affect the indexing subsystem (and vice versa), we can evaluate both subsystems separately [@Lil09]. Each subsystem requires a different kind of input data. For benchmarking a subsystem, we need to simulate a workload scenario with appropriate input data. Although we are not interested in evaluating the effectiveness, we can still look to information retrieval for workload data. IR uses standardized dataset collections for evaluations. These dataset collections usually consist of three parts [@Man08]: * Part 1: Set of documents * Part 2: Set of queries * Part 3: Relevance assessment between queries and documents { list-style-type:none } We are interested in Part 1 as the input for the indexing subsystem and in Part 2 as the input for the retrieval subsystem. Effectiveness evaluation uses Part 3, hence we do not require this data. To our knowledge, there is only one available dataset collection provided by Spina *et al.*\ [@Spi17] for the podcast domain. This collection contains audio files, manual and automatic audio transcripts, queries, and relevance assessments. Since the collection misses RSS feed data, the input documents are inadequate for the Echo implementations. Therefore, we must design our own dataset collection. In general, we face two problems: selecting documents (RSS feeds) and determining suitable queries. The execution time of operations, e.g.\ parsing a feed, affects the performance. XML parsing time depends on the document size. The literature therefore usually assesses execution time with respect to the input size. Real world RSS feeds have arbitrary data and we have no control over the input size per feed. Since we do not mind the actual information within either the feeds, the queries, nor the quality of search results, we can simply create custom feeds and queries using placeholder text. Appendix [#apx-feed-structure-example] shows the feed structure we use for evaluation. We have analyzed 500 arbitrary feeds from the Fyyd Podcast Directory [@FyydDirectory] and found that the average feed has 70 episodes. To make the simulation workloads more realistic, the test feeds also have 70 elements. ### Experiment Setup {#sec-experiment-setup} We measure all evaluation results on a multicore platform (Intel Kaby Lake Core i5 3.1\ GHz with 2 cores, 64\ MB of eDRAM, 4\ MB shared level 3 cache, 16\ GB of 2133\ MHz LPDDR3 SDRAM, Java HotSpot 64-bit server VM build 25.172-b11, macOS 10.13.6, 1\ TB SDD flash storage, APFS file system). The actor implementation uses Akka version 2.5.11 and the microservices build on Spring Boot version [1.5.10.RELEASE.]{tex-cmd-after:"\,"} All code is compiled with Java compiler version 1.8.0 update 172 and Scala compiler version 2.12.6. The components with persistent states are deployed with an H2 [@Mue04] database engine version 1.4.196 (in-memory persistence mode) and Lucene [@ApacheLucene] version 7.2 respectively. All measurements we report are for cold starts of each system. We restart all involved JVMs for each simulation. The indexing experiments start on an empty catalog/index. The retrieval experiments start on a filled index. Crawlers load the benchmark feeds from the local file system, and are therefore not subject to network induced latencies. To eliminate a threat to the validity of the benchmark (discussed in Section [#sec-threats-to-validity] below), each programming model's CatalogStore implementation uses the Spring Data JPA library for database interaction. Usually, Spring Data JPA expects a Spring IoC container to handle concurrent connections and transaction management. To handle concurrent database interaction directly via actors, the Akka implementation uses Spring Data JPA without an IoC container. The respective CatalogStore therefore has to manage transactions manually. We assign each architecture component with a fixed number of threads to ensure that both implementation variants have the same resources for concurrent execution available to them. The Akka components use a `Dispatcher`{language:scala} backed by a `ThreadPoolExecutor`{language:java} (in contrast to the default `ForkJoinExecutor`{language:scala} with a dynamic pool size). The Spring IoC containers of the microservices are configured to use a `ThreadPoolTaskExecutor`{language:java}, where the `corePoolSize` is equal to the `maxPoolSize`. We use 16 threads per component. This way, the benchmark results indicate which programming model better utilizes the available thread resources. ### Benchmark Results To reduce the effects of outliers, we have conducted each experiment 3 times. Each data point in the following results is the mean of the three measured values. Note that all experiments are for static system configurations. Therefore, the results do not reflect any forms of structural scalability (mobility, elasticity). Also, due to the single multicore host, we cannot draw conclusion to the horizontal scalability behavior of the architectures. #### Experiment 1: Indexing Subsystem Figure [#fig-eval-index-overall] shows the benchmark results of the indexing subsystems for the overall time it takes the implementations to process the workload with respect to the input size. The Akka implementation shows better performance for all input sizes. ~ Figure { #fig-eval-index-overall; \ caption: "Benchmark results for the overall processing time of the indexing subsystem"; \ width:100%; page-align:here; } ![img-eval-index-overall] ~ [img-eval-index-overall]: graphics/eval-index-overall.[pdf,png] "Image about eval-index-overall" { width:8cm; vertical-align:middle; padding-bottom:1em; } The figure also describes the load scalability behavior of the systems. With an increasing load for the indexing subsystem, both implementations scale uniformly, which is the desired behavior. This uniform behavior is the result of good vertical scalability, since the architectures are able to uniformly leverage the resources of the single multicore host used for the benchmark. In Section [#sec-conception-concurrent-execution] we discussed the issue of fairness and the implications on resource consumption. Figure [#fig-eval-index-mem] shows the mean memory resource consumption of the Echo implementations in the indexing phase. We measured the memory usage (heap memory $+$ non-heap memory) using the `java.lang.management.MemoryMXBean`{language:java} that every JVM provides. The results illustrate how every microservice consumes separate system resources, even those who do not perform any work in the indexing phase (Gateway, Searcher). The Akka backend that implements the entire Echo system consumes only slightly more memory resources as a single Spring-based microservice. The CatalogStore MS in an exception. The author suspects that the reason for the CatalogStore service's memory demand is that the IoC container of this service has to extend the STM to the database. The entire MSA has a considerably higher memory requirement than the entire actor-based system. The figure also illustrates the impact of the JVM as a microservice platform. The JVM is a relatively heavy-weight VM. We must deploy every process incarnation of a Java-based microservice in its own separate VM, which poses a considerable impact on the system resources. The operating system always allocates resources towards every process on a regular basis. In contrast, Akka actors, besides being more lightweight constructs in general, only get scheduled and thus only consume resources when they have messages in their mailbox. ~ Figure { #fig-eval-index-mem; \ caption: "Memory consumption of the executable artifact VMs in the indexing phase"; \ width:100%; page-align:here; } ![img-eval-index-mem] ~ [img-eval-index-mem]: graphics/eval-index-mem.[pdf,png] "Image about eval-index-mem" { width:8cm; vertical-align:middle; padding-bottom:1em; } #### Experiment 2: Retrieval Subsystem Figure [#fig-eval-search-rtt-overall] shows the results of the experiment to assess the retrieval subsystem's performance. It clearly indicates that the request/asynchronous response style of Akka actors is superior to the synchronous REST-based communication of the microservices. Since the Akka implementation processes heavy loads of requests considerably faster, this subsystem is more available and thus has better liveness. The figure also describes the load scalability behavior of the retrieval subsystems. Both implementations scale uniformly as desired. Again, we trace this result back to the fact that the implementations leverage the available system resources of the single multicore host well (vertical scalability). However, the microservice variant shows considerably lower overall efficiency. Section [#sec-scalability-modularity] gave the synchronous nature of RPC as a major factor limiting the scalability of an MSA. The results of our benchmark support this claim. The request/asynchronous response style of Akka is clearly more efficient than REST-based communication. ~ Figure { #fig-eval-search-rtt-overall; \ caption: "Benchmark results of the overall processing time for the retrieval subsystem"; \ width:100%; page-align:here; } ![img-eval-search-rtt-overall] ~ [img-eval-search-rtt-overall]: graphics/eval-search-rtt-overall.[pdf,png] "Image about eval-search-rtt-overall" { width:8cm; vertical-align:middle; padding-bottom:1em; } In Section [#sec-actor-communication-abstractions] we have actually discussed two variants to model request/asynchronous response communication with Akka. One variant is based on futures, the other on custom child actors and delegation. The author expected that the delegation approach would show better performance, since a future always stresses the thread-pool, while the actor runtime must only schedule a delegation child once the response is available in the mailbox. The Akka result in Figure [#fig-eval-search-rtt-overall] therefore shows the performance when the retrieval subsystem uses delegation. To evaluate the future- and delegation-based modelling of (semi-)synchronous communication, we have conducted the retrieval experiment also with futures. Figure [#fig-eval-search-comparison-akka-delegation-future] illustrates the future results in contrast to the delegation results. ~ Figure { #fig-eval-search-comparison-akka-delegation-future; \ caption: "Comparison of the benchmark results for the retrieval subsystem using either delegation or futures for request/response communication in the Akka-based implementation"; \ width:100%; page-align:here; } ![img-eval-search-comparison-akka-delegation-future] ~ [img-eval-search-comparison-akka-delegation-future]: graphics/eval-search-comparison-akka-delegation-future.[pdf,png] "Image about eval-search-comparison-akka-delegation-future" { width:8cm; vertical-align:middle; padding-bottom:1em; } We see, neither of the two request/async.\ response styles show a considerable performance advantage. Recall that the future and delegation strategies are also applicable for database interaction and IO. The results of Figure [#fig-eval-search-comparison-akka-delegation-future] suggest that each strategy is also equally efficient for database interaction. Therefore, we assume that the actor implementation does not suffer from a negative impact because it has to use the Spring Data JPA library for database access in these benchmark experiments. ### Relevance of the Benchmark To the authors knowledge, there exists no benchmark comparing Akka actors and Spring-based microservices yet. We were only able to find one project on GitHub[^fn-msa-framework-benchmark] which benchmarks popular microservices frameworks. The benchmark results include Spring Boot with the Undertow webserver and Akka HTTP. The experiment setup is rather simple. The frameworks merely serve "*Hello World!*" on a REST interface as the workload, which does not resemble a real-world workload scenario. [^fn-msa-framework-benchmark]: The literature reports especially a lack of different interaction modes in microservice architecture benchmarks [@Zho18]. Most available benchmarks merely focus on one interaction mode, while this literature also reports that MSA-related problems originate from asynchronous and mixed communication setups. Echo's subsystems engage this circumstance, since we have modeled the indexing subsystem in an asynchronous fashion, and the retrieval subsystem in a synchronous fashion. Our experiences do not reflect problems with the asynchronous style. In contrast, we have found that the synchronous style is considerably less performant than the asynchronous style. Interestingly, Bonér, the creator of Akka, advocates in his recent works [@Bon16;@Bon17] for what he calls *reactive microservices*. Essentially, a reactive microservice is a service that orients itself on the actor principles, especially asynchronous messaging and the lack of global consistency (cf.\ eventual consistency). Bonér argues that reactive microservices are more performant than tightly-coupled synchronous services facilitating global consistency (e.g.\ via a transactions mechanism). However, he provides no benchmark results to back his claim. The subsystems of our microservice implementation reflect a reactive microservice style (indexing pipeline) and a more "traditional" synchronous style (retrieval pipeline). The Akka implementation provides the reference to a purely reactive (asynchronous) system. Our benchmark results support Bonér's argumentation that the reactive style is more performant. ### Threats to Validity {#sec-threats-to-validity} Like all experiments, we are also subject to some factors threatening the validity of our results. #### External Threats to Validity The external threats concern how much our results are generalizable. The domain-specific actions of our scenario have an influence on the performance. Examples are the HTTP retrieval of RSS feeds, XML parsing and database IO. These actions dampen e.g.\ the throughput. The utilized underlying technologies (HTTP library, XML parser, database system) influence the performance of these actions. This threatens the comparability of the benchmark metrics across systems, if the implementations apply different technologies. We mitigated this threat by founding all task units on the JVM. We provide all components with the same Core library, which implements the domain-specific functionality. The CatalogStores access the same kind of database system, and both implementations use the Spring Data JPA library for database interaction. Platform- and domain-specific effects are therefore uniform across the system implementations. Nevertheless, due to the domain influence, no general statement about the performance of Akka or Spring-based services is possible. Other metrics are not even measurable in the scenario. Examples are the creation time and maximum process support as suggested in [@Val18]. The static configuration of our scenario does not intend elasticity, i.e.\ a dynamic creation of task units. Hence these metrics require an experiment outside the bounds of our scenario. Additionally, we conducted the experiments merely on a single multicore machine. The experiments therefore only incorporate the effects of vertical scalability we leverage from general concurrency and parallelism on one single machine. Also, the multicore machine has a very small number of cores. The test setup does not consider the horizontal scalability effects of distribution-induced concurrency. To eliminate the threat of selection bias, we did not use real world RSS feeds for simulation. Instead, we used test data with uniform size and feed structure. #### Internal Threats to Validity The internal threats to the validity of this benchmark concern the accuracy of our data collection tool. Since we did not find a tool that is sufficient to collect the data for Akka actors and Spring-based microservices, we developed a custom benchmark framework. We implemented this toolkit to the best of our ability. But we have to assume that the toolkit's efficiency is not state of the art. This threat is mitigated since both systems are subject to the same potential inefficiency, which does not distort the relative comparison of metrics. An additional threat results from the fixed amount of threads we provide for each component in the benchmark. Actor-based components can fully leverage all these threads, e.g.\ for child actors or futures. We have discussed in Section [#sec-internal-service-concurrency] that the microservices use a different `TaskExecutor`{language:java} for asynchronous tasks than the standard thread-pool of Spring's IoC container. To ensure the overall thread limit for each component, we have to split up the threads between the two thread-pools. There is the threat that a service's implementation does not distribute the computational load equally between the two thread-pools. We did not measure the thread-partitioning required for ideal thread utilization for each microservice. Instead, we distributed the available threads evenly between both pools for all services. This threat is only mitigated for services which do not apply asynchrony internally. Then there is only the standard `TaskExecutor`{language:java} with the full amount of threads in place. # Conclusion {#ch-conclusion} ~ Epigraph { caption: "Peter Landin"} Most papers in computer science describe how their author learned what someone else already knew. ~ In this chapter, we draw our conclusive view. We give answers to the research questions, summarize the contributions, and finally motivate some future work. ## Research Questions Revisited In the beginning of this thesis we asked a set of research questions. It is now time to revisit and answer these questions. ### RQ1: Why do actors and microservices qualify for programming concurrency? { -; toc:clear } Actors and microservices encapsulate their state exclusively and all their communication solely facilitates message passing semantics. These properties make the task units highly cohesive and provide a temporal and spacial decoupling. The resulting independence is the foundation that enables an actor runtime or an operating system to execute the task units in a concurrent fashion implicitly. ### RQ2: How do the actor and the microservice model facilitate concurrent execution? { -; toc:clear } The execution modality of actors and microservices already introduces concurrency among the task units. Both models can also utilize additional sources of concurrency. The actor model has a long tradition of using futures, e.g.\ for (semi-)synchronous communication abstractions. In general, actors can be combined with every other concurrency model, as long as the combination does not introduce new safety or liveness issues. Microservices are free to employ internal concurrency using every model available to their technology stack. Unlike actors, microservices are therefore not free of internal synchronization in general. Our scenario implementation leverages the implicit scheduling of requests on thread-pools through an inversion of control container. Software transactional memory controls the required synchronization. ### RQ3: What are the expressive capabilities of actors and microservices regarding concurrent programming concerns? { -; toc:clear } We have shown that both models apply different implementation strategies, but in general achieve the same capabilities for concerns like parallelization/distribution, communication, isolation, location transparency, persistence/IO, and scalability. The two implementation strategies come with different trade-offs for each model. In general, we expect larger executable artifacts and more resource consumption from the microservice approach, since the model produces independent programs. The author initially expected that the microservice style would also show a significantly higher programming effort, e.g.\ in lines of code. The suspected reason was the need to maintain a dedicated codebase for each service. Indeed, our overall microservice codebase is about 46\ % larger compared to the actor codebase, although the declarative programming style we applied for the microservices already reduced our programming effort significantly. The price for this reduction is the resulting size of the executable artifacts. The actor model achieves all capabilities in a single codebase. This single codebase implies that at least a portion of the actors (in a distributed cluster deployment) exist within the same process and memory boundaries. Much of the programmer's effort is the liability to guarantee that no mutable state is accessible through reference sharing among any two actors in order to ensure the model semantics. ### RQ4: How does the performance of actors and microservices compare in a multi-core environment relative to a concurrent system scenario? { -; toc:clear } The benchmark results show that, with respect to our non-trivial scenario, the performance of an actor-based system is generally better than the performance of a microservice system. Since we have ensured that the domain-induced impact on the performance is uniform between both system implementations, the difference in the performance results from the efficiency we leverage from the underlying concurrent programming model. We have also shown that microservices which facilitate an asynchronous communication style merely have a slight overhead compared to actors. However, the benchmark also exposed that strictly synchronous communication (request/response) among services is clearly inferior to the request/asynchronous response style that actors facilitate. It is therefore surprising to the author that the scientific literature emphasizes REST as the primary microservice IPC mechanism. ## Contributions In this thesis, we compared the programming of concurrent computation with the actor and microservice model. We have explored the interrelations of the two models and filled a gap in the literature. In order to answer our research questions, we designed Echo, a non-trivial scenario for a concurrent domain-specific search engine prototype. We also provide an actor implementation based on Akka, as well as a microservice implementation based on the Spring framework of this system scenario. We evaluated the capabilities we are able to express with each model regarding concurrent programming concerns like parallelization/distribution, communication, persistence/IO, location transparency, isolation/independence, and scalability. Finally, we reported the results of an efficiency benchmark of the system implementations. Further materials related to this work are available at: ~ Center ~ ## Future Work The benchmark in this thesis is limited to a single multicore host. We did not benchmark the effects of distribution and horizontal scalability due to our limited access to hardware resources. Also, although we have motivated the integration of Akka actors and Spring-based microservices in Section [#sec-actor-ms-integration], we did not investigate the effects on efficiency within a mixed architecture integrating actors and microservices as equal concurrent task units. From our literature readings and practical experience, it is our believe that one major limitation of both actors and microservices is the lack of checkable compatibility (design by contract). Some work on static analysis for actors has been done in the literature. The work by D'Osualdo *et al.*\ [@DOs13] for example defines a checkable model for Erlang-styled concurrency. This model can be also expressed as processes of a suitable calculus. Recall that service-oriented programming languages incorporate the microservice model into the language level, and sometimes build upon a process calculus. We deem it worth to fathom into contract verification techniques (especially behavioral types [@Per16;@Hut16]) for the actor model and SOC calculi, for example towards a more theoretical foundation for integrating actors and microservices. From our experiences in this work, it is the author's general believe that the microservice styles suffers from the lack of a theoretical foundation. We therefore think that service-oriented programming languages are a highly promising evolutionary step worth of further investigation. ~ Begin TexRaw \appendix ~ End TexRaw # Feed Structure Example { @h1="A"; #apx-feed-structure-example; before:"Appendix "; } An RSS 2.0 [@Win03] feed is an XML document. The feed's top level element is the `` tag. A feed becomes an Atom [@RFC4287] feed if the `` element additionally has the XML namespace `xmlns:atom="http://www.w3.org/2005/Atom"`. The `` encloses the metadata entries of the feed (``, `<link>`, `<description>`, etc.) as well as a set of `<item>` elements. Each item has it's own set of metadata elements (`<title>`, `<pubDate>`, `<description>`, `<guid>`, etc.). The most important element of each item of a podcast feed is the `<enclosure>`, which provides the URL to the media file. Some feeds of the domain also apply additional XML namespaces to provide a wider range of metadata. Two prominent examples of such namespaces we also support in Echo's feed parsers are: * `xmlns:itunes="<a href="http://www.itunes.com/dtds/podcast-1.0.dtd"`">http://www.itunes.com/dtds/podcast-1.0.dtd"`</a> for metadata used in the iTunes Podcast Directory [@iTunesDirectory]. * `xmlns:psc="<a href="http://podlove.org/simple-chapters/"`">http://podlove.org/simple-chapters/"`</a> for chaptermark metadata information in the Simple Chapters [@PodloveSimpleChapters] format. Below is a complete example of an RSS 2.0 feed. It provides some few metadata elements. The feed merely has a single `<item>` element. In general, a feed has several items. All metadata in this example is text of the so-called *Lorem ipsum*[^fn-lorem-ipsum], a well-known placeholder text snippet. The text is not intended to transport any meaning. Instead, the design and publishing industries use the text to demonstrate a text structure or visual form. Hence, the Lorem ipsum is also suitable to illustrate the RSS feed structure, without providing actual meaningful content. [^fn-lorem-ipsum]: <https://lipsum.com> ~ TexOnly &pagebreak; ~ ```{language:html} <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <rss> <channel> <title>Lorem ipsum https://lorem.ispum.fm en-us Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua Lorem ipsum https://lorem.ispum.fm/li001 Fri, 13 Jul 2018 16:35:13 +0000 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ``` # List of Acronyms and Abbreviations { - } * [AMQP]{.acronym} Advanced Message Queuing Protocol * [AO]{.acronym} Active Object * [API]{.acronym} Application Programming Interface * [CPU]{.acronym} Central Processing Unit * [DTO]{.acronym} Data Transfer Object * [FIFO]{.acronym} First In First Out * [fJAR]{.acronym} fat JAR * [HTTP]{.acronym} Hypertext Transfer Protocol * [IPC]{.acronym} Inter-Process Communication * [IO]{.acronym} Input/Output * [IoC]{.acronym} Inversion of Control * [IR]{.acronym} Information Retrieval * [JAR]{.acronym} Java Archive * [JSON]{.acronym} JavaScript Object Notation * [JVM]{.acronym} Java Virtual Machine * [LB]{.acronym} Load Balancing * [MS]{.acronym} Microservice * [MSA]{.acronym} Microservice Architecture * [OOP]{.acronym} Object-oriented Programming * [OS]{.acronym} Operating System * [PID]{.acronym} Process Identifier * [REST]{.acronym} Representational State Transfer * [RPC]{.acronym} Remote Procedure Call * [RSS]{.acronym} Rich Site Summary * [sJAR]{.acronym} skinny JAR * [SOA]{.acronym} Service-oriented Architecture * [SOC]{.acronym} Service-oriented Computing * [SOP]{.acronym} Service-oriented Programming * [STM]{.acronym} Software Transactional Memory * [URL]{.acronym} Uniform Resource Locator * [VM]{.acronym} Virtual Machine * [XML]{.acronym} Extensible Markup Language { .acronym-table } ~ Begin TexOnly ~ Begin TexRaw \cleardoublepage ~ End TexRaw ~ End TexOnly # List of Figures { - } ~ Begin TexRaw \listoffigures \cleardoublepage ~ End TexRaw ~ Begin HtmlOnly [TOC=figures] ~ End HtmlOnly # List of Tables { - } ~ Begin TexOnly ~ Begin TexRaw \listoftables ~ End TexRaw ~ End TexOnly ~ Begin HtmlOnly [TOC=tables] ~ End HtmlOnly [BIB]