10 LIST OF FIGURES Figure 1.1: The Mace architectural design Figure 3.1: Bamboo DHT design architecture Figure 3.2: Mace service composition for a DHT application using Recursive Overlay Routing implemented with Bamboo. Shaded boxes (dark for downcall, light for upcall) indicate the interfaces implemented by the service objects Figure 3.3: States and Transitions for Bamboo Service Object Figure 3.4: State machine model for Bamboo. States are shown in light gray; event-transitions are represented by arrows with names in white and actions in dark gray Figure 3.5: Message diagram for global sampling. In response to a scheduled timer, Node A routes a GlobalSample message to an identifier id by sending it to node B, which is forwarded to its owner, node C. Node C responds with a GlobalSampleReply message informing node A about node C, potentially causing a routing table update. 40 Figure 3.6: Local and Distributed inconsistency and failure detection in Mace. 40 Figure 3.7: Percentage of lookups that return a consistent result Figure 3.8: Mean latency for lookups that return consistent results Figure 4.1: This figure shows how timeout periods and triggers interact in detect specifications in Mace. Time is represented by the arrow going to the right. Arrows pointing up indicate triggers executing. 82 Figure 5.1: State exploration: we perform an iterative, bounded depth-first search (BDFS) from the initial state (or search prefix): most periphery states are indeterminate. We execute random walks from the periphery states and flag walks not reaching live states as suspected violating executions Figure 5.2: Compared performance of random walks with and without biasing.115 Figure 5.3: CDF of simulator steps to a live state at a search depth of Figure 5.4: mdb session. Lines with differences are shown in italics ( indicates the error log, + the live log), with differing text shown in bold. The receiver is IP address and the sender is Figure 5.5: Automatically generated event graph for MaceTransport liveness bug x

11 Figure 6.1: System Architecture: first, the unmodified system is run through a set of training runs to determine a set of event duration distributions (EDD). Next, the system, along with the EDD, is fed into the Search algorithm, which will produce one or more anomalous paths. We then use the ExecutionSearch tool to analyze the anomalous path, giving us the most similar normal path. At this point, any debugging tool can be used to compare the two executions until the source of the bug is found Figure 6.2: We first perform an exponential search (E 1 E 5 ) to determine bounds for the divergence point, then a binary search (B 1 B 3 ) to isolate the divergence point xi

12 LIST OF TABLES Table 3.1: Lines of code measured in semicolons for various systems implemented in Mace and other distributions Table 5.1: Example predicates from systems tested using MaceMC. Eventually refers here to Always Eventually corresponding to Liveness properties, and Always corresponds to Safety properties. The syntax allows a regular expression expansion *, used in the AllNodes property Table 5.2: Summary of bugs found for each system. LOC=Lines of code and reflects both the Mace code size and the generated C++ code size. 123 xii

13 ACKNOWLEDGEMENTS I would like to acknowledge all the people who have helped bring this work to fruition. Adolfo Rodriguez, from whom I inherited the MACEDON project which led to the development of Mace, Dejan Kostić, who helped drive many of the early requirements of the project, James Anderson, who helped build Mace and MaceMC, and co-authored each of these papers, Ryan Braud, who helped build Mace, and co-authored that paper, my faculty co-author Ranjit Jhala, and my co-author and advisor, Amin Vahdat. Additionally, this work would not have been so successful without the help of fellow students and users of the Mace toolkit, especially Meg Walraed-Sullivan, Justin Burke, Alex Rasmussen, and Flavio Junqueira, nor without the contributions of various smaller course projects, undergraduate research projects, and Masters projects, especially those of Duy Nguyen, Hakon Vesperej, Jim Hong, and Darren Dao. Thanks also to the shepherds and anonymous reviewers of our various papers, especially to Petros Maniatis of Intel Research. Not to be forgotten also are the staff and systems faculty at UCSD who contributed to the growth of this project and helped make sure the resources were available and functioning properly. Special thanks in this regard to Ken Yocum, Marvin McNett, Chris Edwards, Stefan Savage, Alex Snoeren, and Geoff Voelker. Chapter 3 is an updated and revised copy of the paper by Charles Killian, James W. Anderson, Ryan Braud, Ranjit Jhala, and Amin Vahdat, titled Mace: Language Support for Building Distributed Systems, as it appears in the proceedings of the ACM SIGPLAN Conference on Programming Language Design and Implementation c 2007 ACM DOI The dissertation author was the primary researcher and author of this paper. Chapter 5 is an updated and revised copy of the paper by Charles Killian, James W. Anderson, Ranjit Jhala, and Amin Vahdat, titled Life, Death, and the Critical Transition: Finding Liveness Bugs in Systems Code, as it appears in the proceedings of the 4th USENIX Symposium on Networked Systems Design and Implementation The dissertation author was the primary researcher and author of this paper. xiii

15 Jerry Griggs, Charles Killian, and Carla Savage. Venn diagrams and symmetric chain decompositions in the Boolean Lattice. Electronic Journal of Combinatorics. Volume 11, January 2, (An article about this result appeared in Science, Vol. 299, January 31, 2003 and it was the subject of a front page article in the January 2004 issue of SIAM News. Additionally, this work was featured in the December 2006 issue of the Notices of the AMS.) Charles Killian, and Carla D. Savage. Antipodal Gray Codes. Discrete Math. Vol. 281, Nos. 1-3 (2004) Feiyi Wang, Raghu Uppalli, and Charles Killian. Analysis of Techniques For Building Intrusion Tolerant Server Systems. In proceedings of Military Communications Conference. Oct 13-16, Feiyi Wang, and Charles Killian. Design and Implementation of SITAR Architecture: A Status Report. In proceedings of Intrusion Tolerant System Workshop, Supplemental Volume on 2002 International Conference on Dependable System & Networks. Washington D.C.. June 22-26, (unrefereed) xv

16 ABSTRACT OF THE DISSERTATION Systems and Language Support for Building Correct, High Performance Distributed Systems by Charles Edwin Killian, Jr. Doctor of Philosophy in Computer Science University of California San Diego, 2008 Professor Amin Vahdat, Chair Daily life involves the use of computers for everything from interpersonal communication to banking and transportation. But while everyday computation has become more decentralized and disconnected, advances in programming and debugging have centered on individual processes. It is still very challenging to write correct, highperformance distributed systems. Programmers can choose either to sacrifice correctness by accepting the complexity of building a distributed system from the ground up, or to sacrifice performance by using generic toolkits and languages which provide simplifying functionality like RPC, memory transactions, and serialization, which make it easier to code correct systems. Where performance is deemed higher priority, systems are generally built using C++, and debugged by printing logs at each node and using ad-hoc tools for analysis, leading to complex, brittle implementations. This dissertation posits that language support can significantly simplify the development of distributed systems without sacrificing performance, and can enable analysis to automatically find and isolate deep bugs in implementations affecting both performance and correctness of distributed systems. We focus on finding a middleground between the canonical distributed systems design abstraction, state machines, and the classical programming tools used for modern high-performance distributed systems, C++, awk, sed, and gdb. That middle ground is a C++ language extension wherein users implement a distributed system using syntax yielding the performance xvi

17 and control of C++, but in a restricted programming model forcing them to structure their system as a state machine. This dissertation presents the Mace language extension, runtime, and tools. Mace makes it possible to develop new high-performance distributed systems in a fraction of the time. We implemented more than 10 significant distributed systems using Mace, each both shorter and as fast, or faster, than the original. Structured programming in Mace also enabled development of the first model checker capable of finding liveness violations in unmodified systems code, and an automated performance tester to detect and isolate anomalies. These have been used to find and isolate previously unknown or elusive bugs in mature Mace implementations. Mace has been publicly available for four years, and is used worldwide by academic and industrial researchers in their own research. xvii

18 Chapter 1 Introduction Distributed systems are critically important today. Computers that control everything from stock markets and communication networks to transportation systems and hospitals are increasingly interconnected, with distributed computation and communication critical to their correct functioning. Moreover, these systems must perform not only correctly, but at a level of performance that significantly limits their implementation options. This combination of correct execution at high performance makes them a challenge to program. Despite the best efforts of systems developers over the past 30 years, we still produce systems with latent bugs or issues that are often discovered only by the end-user in production systems. Alternately, the systems may be inefficiently designed or under provisioned, causing them to perform poorly in periods of high load. Failures of these distributed systems, while somewhat rare, are often spectacular and become headline news. There are the cases where distributed systems failures cause the markets to cease trading [And05, Pre06], or their indexes to be incorrectly maintained [PLS07]. There are also cases where communication systems, either telephonic [Gar05] or internet-based [Ara07] suffer complete failure due to bugs. In other cases, network connectivity failures or high load halt transportation systems [Car07, Pre07] and hospitals [Pre03]. 1

19 2 1.1 Today s Distributed Systems Modern distributed systems are about thirty years old. One of the first such systems, Grapevine [SBN83], was an experiment in distributed systems, and provided the first naming and service location service. Since its development, communication has become even faster and more widespread. Before, it would have been inefficient to break a computation or job into small pieces divided across vastly distributed resources, because the communication overhead of the pieces would quickly dominate the overall task, actually causing it to perform slower than traditional computation. But today, with new, fast, widely deployed networks, it makes sense to share the load across resources potentially internationally. Computers and distributed systems play a role in a wide variety of industries, and a whole survey could be written about them. This introduction will focus on just three industries whose distributed systems, and their failures, are well known throughout the world: the financial, communication, and transportation systems. Financial Systems Banking systems are tightly interconnected, including e-check processing systems, credit card processing systems, ATMs, branch offices, and so forth. Before these distributed systems, but after the aforementioned forms of payment were introduced, sellers accepting these payment types were often at risk that the payment would not go through, and the seller would have to track down the buyer to collect their money. Now, distributed systems have been established which connect the sellers and processing systems to the bank accounts. Verification takes place instantly, ensuring that the buyer has the money they are trying to spend, and electronically transferring it into the seller s account when the transaction takes place. These systems are especially critical to program correctly because they are tracking someone s hard-earned money, and errors usually either mean the bank loses or the client loses, because of incorrect balances. Plus, there needs to be a coordination process between these systems, because the customer may be conducting several transactions at once, and expects that there will be a consistent view of what is going on in the bank account. Thus it is important to verify, given all the ways two transactions might interleave, that

20 3 the balances match up with the transaction history. One common problem occurs if two processes read the balance at the same time, each apply their transaction to that balance, then write the value back. If there is no detection of the other transaction, then the first transaction to complete will essentially be lost when the second one completes. But beyond just tracking an individual bank account, today s distributed systems also control the stock market. Stock values, purchase requests, and sell requests are all exchanged in a high-volume trading system around the world, both in private networks for brokerage firms and on the public internet. Having distributed systems that effectively handle the high trading volume is a rather challenging task. Added to this burden is the addition of program trading, which in essence is when a computer program is written to automatically trade stocks under a set of conditions that it then monitors for. These two properties can in fact combine to produce conditions where the market races out of control, with the program traders adding significant load to the system as a whole to create a vicious cycle. Take for example a program trader that detects a significantly declining market and sells off stocks to get out before they decline too much. The program trader is unwittingly contributing to the decline of the market, and thus triggering other program trading with similar policies. This series of events causes the trading volume to increase overall, stressing market systems. Safeguarding our market system against overloaded systems, software bugs, and instabilities in program trading is an immensely complicated task. It has involved backup systems, monitoring systems, over-provisioned networks, and the creation of mechanisms to halt the exchange when trading gets out of control to prevent collapse of the economy. These systems must be maintained, tested, and themselves debugged, to avoid recovery mechanisms that fail as soon as they are deployed, and the software testing process for these systems must aggressively defend against a whole host of possible problems based on seemingly random and unknown user interactions.

21 4 Communication Systems Communication systems supporting person-to-person communication are also a kind of distributed system, and are often dual-purpose to support a wide variety of other distributed systems. These systems can be low-level, like traditional telephony systems, which have dedicated resources for a continuous data flow, or more modern software communication packages, like voice-over-ip provider Skype, which provide streaming media using Internet best-effort service. Once simple, telephone systems connected calls through the use of human operators who physically connected patch cables to connect callers to receivers. Since that time, call traffic has been modified to be transmitted along with other call traffic sharing a fiber-optic channel, and routed using automated electronic switching stations. The software for these systems is notably well tested and the PSTN (public switched telephone network) service generally delivers what is still considered the gold standard of 5-nines (99.999%) of reliability, better than other networks and distributed systems. Yet even in this environment, buggy software still manages to find its way to deployment, as evidenced in the 1990 failure of the AT&T long-distance switching system. In this instance, a bug in the software caused the long-distance switch to reboot, and in doing so caused neighboring switches to suffer the same fate. The result was a nationwide long-distance outage over a period of about 8 hours. The bug was exercised by a certain timing of events that had not been tested, suggesting that existing tools for testing these distributed systems were not considering the range of possible problems. More modern communication systems can deliver the same type of service as traditional telephone service, but do so by splitting communication media into little pieces, and shipping each of the pieces to destinations using the public Internet. The most popular such service is Skype, a voice-over-ip provider with 220 million users, 5 to 6 million of whom are generally online at any given time. To save Skype s bandwidth, and ultimately money, users of Skype donate a portion of their bandwidth to other users, routing calls around circumstances that prevent direct communication between users. Accordingly, the Skype client executes a peer-to-peer membership protocol to learn which peers are online, and to support the location of peers for

22 5 indirect routing. However, the Skype software designers had not properly accounted for how the protocol would tolerate a massive restart of users computers at roughly the same time, both drastically reducing the available network resources and increasing the flood of join attempts. This scenario became tested in deployment after a routine download of updates to Windows boxes through Windows Update caused a large quantity of Skype users to restart their computers at nearly the same time. The result was a Skype outage affecting all users from a Thursday to Saturday. Again, the environment and methodology for testing this software was not adequate to support the scope of scenarios that might exist in deployed environments. Transportation Systems However, distributed systems do not always fail due to software bugs. They can just as easily fail due to misconfiguration or under provisioning. So, it is not sufficient to build tools that search for bugs in the programming itself, but instead the tools must consider how interactions with the environment, variations in the timing of events or the offered load might affect the system. Additionally, tools must be built to enhance our ability to perform post-mortem diagnosis of what happens in the deployed system, since inevitably we cannot solve all problems before deployment. This is the case of two distributed systems supporting transportation systems, which normally manage the safe and efficient transportation of people and cargo over rail and through the air. In both the cases of rail-management systems and flight-tracking systems, operators are concerned with the efficient routing of moving vessels from source to destination, but they are primarily concerned with the safety of those moving vessels and their passengers. Unlike vehicular systems, whose routing and safety decisions are made by independent drivers on the roadways, in railroad and airplane systems these decisions are made under the supervision of a separate authority who handles the routing and scheduling of all vessels through a region of space; therefore, it may make more efficient and globally optimal decisions. Of course, a primary requirement to developing these systems is that the information about where vessels are and where they are going must be made available

23 6 to the operators making the decisions. In the case of a June 2007 aviation problem, a computer in the Atlanta-system for processing flight plans and sending them to air-traffic controllers failed. To avoid shutting down, the traffic was diverted to the Salt Lake City center. However, the load proved to be too much for the Salt Lake City center, causing hundreds of flights to be cancelled or delayed. In a similar scenario, an engineer for Caltrain in San Francisco was attempting to complete a network connection to a new central operations facility when the existing connection to the old central operations facility was disrupted. Initially blamed on a computer glitch, the inability to tell where the trains were and to signal them caused them to revert to their failsafe mode, which is to stay in their current position. As a result there were no injuries, but at least 8 trains were stopped on the track during rush-hour commuting. Clearly, failures in existing distributed systems, whatever the cause, have a huge social and financial impact on society. Failures can result from buggy software, hardware, under-provisioning, user error, malice or misconfiguration, or more often, a combination of more than one contributing factor. Consider next some of the specific challenges of building and testing that are unique to distributed systems. 1.2 Challenges There are many challenges in building correct, high-performance distributed systems. Distributed systems must deal with network errors, disconnections, node reboots, and widely varying latencies and throughputs. Each node in a distributed system can only see its local state, and can only query other nodes state through network messaging. This messaging has an inherent delay, so this report may not reflect current state, making it harder to coordinate and collaborate across nodes. Furthermore, as multiple nodes communicate with each other, the orderings of these messages are unpredictable, despite the expectation that the one sent first will be received first. In fact, the triangle inequality also does not apply: given three nodes A, B, and C, if A sends messages to B and C, then B forwards a message to C, the message from A to B to C might arrive before the message from A to C. Each of these unexpected orderings makes it challenging to get the algorithms correct, but

24 7 they also impact performance. Performance problems can be caused by bugs in the code that are exercised when the state is inconsistent across nodes. To add insult to injury, the nature of high-performance and real-time distributed systems also makes it impractical to include even modest amounts of logging overhead in a deployed system, and therefore it is nearly impossible to piece together what happened during execution even with logging enabled Programming Languages and Abstractions Among the many challenges in building these systems is that currently, programmers are forced to choose between simple expression and control over performance. Simple expression is obtained by using high-level programming languages and/or communication services, which simplify the construction of networked systems by general purpose abstractions. However, these general purpose abstractions tend to have lower overall performance, either due to higher overall load or the fact that they have to do more work than necessary to accommodate for all the ways the service might be used. Take, for example, JAVA serialization. JAVA serialization is built under the idea that there is an object stream, and that the process reading from the object stream need not know the type of the object to be received next. In fact the next object could be any JAVA object, even those not familiar to the local JVM. Additionally, each field of the object may be at runtime replaced with another JAVA object that is a derivative of the actual field type. The implication of this design decision is that the serialized object stream must contain complete type information for each object and field. But, for practical distributed systems, this wholly general feature is unnecessary, as the messaging protocol will define with only few options what each field and object in the stream should be, making the actual serialized form much more simple. The more general JAVA form is simpler to code and use, but comes at the cost of larger serialized message sizes, and more CPU cost to serialize and deserialize the object. Simple primitives for shared memory infrastructure or RPC can similarly be used to build a wide variety of communication patterns. But often using these primi-

25 8 tives for building other abstractions is less than ideal from a performance standpoint, for much the same reasons as JAVA serialization is not ideal. So while researchers and prototypes sometimes use these services and languages, current distributed systems programmers often tend to work closer to the other end of the spectrum, choosing a programming language that gives them complete flexibility over the performance and details of each component. But these programming languages require the programmer to focus on every detail, not just the ones that matter for the distributed system. The resulting large body of code leaves many cases where mistakes may be made, which leads to bugs and errors. Plus, much of the code is tedious, following a copy-paste-edit paradigm, where the same pattern is to be used, but must be tailored slightly differently in each case. If a problem is later found in the pattern, each case must be visited and fixed, and missing cases can also lead to bugs. Also, the simpler high-level abstractions are easier to understand and reason about, which helps those implementations be more correct than they would be using low-level abstractions. But the practical systems are generally much more complex, and cannot be reasoned about in any convenient way. It is possible to build tools to process added instrumentation data collected from an actual execution of a specific system, but these tools and systems then have to be organically grown for each component needed, versus a unified framework which is possible for higher level systems. Of course, when it comes to debugging, even the process of adding this instrumentation can cause the result to change, making it impossible to reproduce the problem exactly. So we either seek a way to debug the system without keeping track of what it is doing, or to make the constant instrumentation overhead small enough to not affect the result Runtime Variations Concretely, consider the challenges in building a simple distributed hash-table (DHT). A distributed hash-table distributes the buckets of a hash-table across nodes throughout the network. In doing so, it makes it possible to build a load-balanced system that distributes key-value pairs throughout the network. To tolerate the inherent churn in the network, DHTs replicate data at peer nodes, so that if they

26 9 reboot, they download fresh copies of the data to prevent loss. Furthermore, since the DHTs are designed to be quite large, DHTs maintain routing tables that enable efficient lookup of any particular datum. Generally, this means that from any node in the DHT, any key-value pair can be determined by routing a query through at most lg n peers, where n is the total number of peers. However, there are many problems which can occur that prevent this design from being met. First, consider when a node reboots. It will download a fresh copy of the data from a peer by design, but consider also a simultaneous query. The programmer naturally assumes the copy of the data will be received first, and tends not to consider the rather unlikely event that a query is received before the rebooting node can download the data again. But if the query does arrive first, a naive implementation of the DHT would respond with the incorrect response that the key has no value. This result is due to the fact that the node is a bucket maintainer for that key, and does not have any data for it. In this case, the ordering of messages is critical to whether a bug will be exercised. A programmer gets progressively worse at considering all the possible scenarios as the number of nodes grows larger, causing an exponential state space explosion. Another problem occurs early on in the DHT execution, when the routing tables are incomplete and only basic routing is available. In this event, nodes do not know other good nodes to which to forward data, so the path of these queries can be quite long. The designer s view of this problem is that it is a startup problem only, and in the steady state is not a concern. But, this view overlooks problems where the design for the construction of routing tables takes an excessive amount of time, and becomes a problem for the average case. A final example problem occurs when, due to node churn, the network is left in a bad state, and a routing loop forms. These loops can cause lookup messages to be forwarded indefinitely through the system. Further churn can appear to solve this problem, which makes the difference between what would appear to be a correctness problem and what is instead a significant performance problem. Similarly, if the implementation language has high overhead in terms of CPU or memory usage, or certain machines are under-provisioned or overloaded, the performance of the whole system will degrade. This degradation is because in a distributed system, the

27 10 complete operation involves several serial steps of nodes waiting for other nodes to complete, and the whole chain is brought down by the weakest link. This degradation is more subtly true in parallel systems that must wait for the total response set before continuing the slowest response will dominate the performance of the overall system Heisenberg Uncertainty Principle Determining what is happening in a distributed system is much like locating a particle in a small region of space. Generally, users can only see the macro outputs that is, those outputs that are results of application behavior. To get a better look at what is precisely happening in a distributed system, the state of the art is to add logging to each node in the distributed system. Increasing the amount of logging generally leads to equivalent increases in the visibility into the actions at each node. But adding even small amounts of logging subtly affects the timings of events in the system. This modification may prevent or mask problematic behaviors of the system, making the error condition impossible to reproduce, even if the error condition is observed. Additionally, too much logging can slow down the performance of the system noticeably, an unacceptable outcome for a deployed system. Thus, distributed systems developers expend a lot of energy in making sure their logging has a minimal impact on the system, and can easily be disabled to achieve the best performance. Even after instrumenting distributed systems, figuring out what is happening in the distributed system as a whole is still a challenging problem. Each node generates its own independent log file. These log files are kept on a local disk, so network I/O from logging does not impact system performance. Understanding what happened in a slow query, for example, involves tracing the query message from the source computer s log, matching up each message sent and received, skipping from log to log on different computers, and trying to piece together what happened. Often, the exact trace of the message path is not enough to understand what happened, as more information is needed about the state of each node when it processes messages, and the other things going on simultaneously at the node. Understanding the state of the system often comprises relational properties of the variables

28 11 at each node, collected globally. For example, to see the state of a tree, one would construct a map of which node each node thinks is its parent. Further, since clocks are not synchronized across nodes, it is non-trivial to even produce a consistent snapshot of the system state. 1.3 Hypothesis The hypothesis of this dissertation is that language support can significantly simplify the development of distributed systems without sacrificing performance, and can enable analysis to automatically find and isolate deep bugs in implementations affecting both performance and correctness of distributed systems. This dissertation provides a partial solution to the problem of building correct, high performance distributed systems, that both advances the state of the art, and is a practical solution that can be used in addition to and along with existing tools, systems, and programming experience. First, this dissertation argues that a new programming language can be developed for the domain of distributed systems that allows programmers to easily write simple, efficient implementations that contain semantic information allowing the compiler to handle tedious code, generic tools that provide useful functionality, and automatically instrument the code with event processing and tracing. Second, this dissertation argues that to effectively test distributed systems, it is critical to test liveness properties, which ensure not only that the system never enters a bad configuration, but that it eventually accomplishes its goals. Further, the dissertation argues that it is possible to extend existing model checking techniques to effectively test these properties on unmodified implementations. Using model checking also avoids the problem of determining what happens during execution and understanding log files, because it runs the unmodified code in simulation, where the model checker, and not the performance of the node, controls the interleaving of events.

29 12 Third, this dissertation argues that the ideas and concepts of model checking may be extended to develop a new set of tools which can support the location of performance problems in distributed systems implementations. Specifically, these tools can help find bugs in implementations that are masked by robust engineering, and would otherwise be ignored by correctness checking. To this end, we have developed the Mace programming environment for distributed systems, the MaceMC model checker that can test liveness properties, and extended MaceMC to help find and isolate performance problems in unmodified distributed systems implementations. Figure 1.1 illustrates the Mace architectural design. The user writes the state-machine representation and the interface description of a distributed system service, which the Mace compiler translates into a highperformance implementation, debugging hooks, and its annotated structure. These pieces can then be used equally by user applications and tools designed to work generically with Mace implementations such as the MaceMC model checker and its extensions Mace We developed the Mace programming language, runtime, and toolkit to simplify development of efficient, high-performance distributed systems implementations. Mace is fully operational, has been in development for five years, is publicly available for download, and has been used by researchers at UCSD, HP Labs, MSR-Asia, and a handful of universities worldwide in support of their own research and development. We have implemented more than ten significant distributed systems in Mace, most of which were originally proposed by others. This set includes Distributed Hash Tables [RGRK04a, RD01, SMK + 01], Application Layer Multicast [CDK + 03, JGJ + 00, KBK + 05], and network measurement services [DCKM04] ready to run over the Internet. In each of these cases, the Mace implementation was as fast or faster than the original implementation, and two to ten times shorter in source code. We have also used Mace in multiple graduate and undergraduate courses as a teaching tool, where students successfully used Mace as an implementation language for course projects.

30 13 Figure 1.1: The Mace architectural design MaceMC To accompany the Mace compiler and runtime, we built many tools supporting debugging, each taking advantage of the support available from the domain-specific language. In particular, we built the Mace Model Checker (MaceMC), to support systematic automated testing of unmodified Mace implementations in the many scenarios to which each system could be exposed, simplifying the task of finding bugs. MaceMC can also test whether systems violate liveness properties, enhancing its ability to find bugs. Once a violation is found, MaceMC can identify the specific point where an execution becomes dead, classifying an entire set of executions as liveness-violating, which directs the developer to the specific problem that caused the execution to violate liveness. MaceMC has been used to find more than 50 bugs in

31 14 Mace implementations, and is part of the active development cycle for all core Mace developers MaceMC-Performance We also developed a variant of MaceMC to help the developer search for performance problems. Performance problems are more difficult to detect automatically, because they do not have black-and-white boundaries. They require testing that considers a wide variety of probable or possible timings of events. This new variant takes input timing distributions and generates fully-reproducible time-based executions. It can then take anomalous executions and explore the space of similar executions, helping the developer understand where in the execution a performance problem may have occurred. This new technique for finding performance problems has allowed us to find an interesting new class of bugs correctness bugs that are masked by robust design. For example, a bug sometimes preventing the construction of an overlay tree may get masked by a protocol designed to recover from network partitions. This bug would be overlooked by the original MaceMC, but found by the enhanced version since it would cause anomalous performance. 1.4 Summary Mace, MaceMC and the performance tester have each advanced the state of the art towards easily building correct, high-performance distributed systems. It now takes experienced Mace developers a fraction of the time previously needed to implement a new distributed system once its design is conceived, and debugging and testing are greatly simplified thanks to the model checker and performance tester. Mace represents five years of development work and has been publicly available for four years. In addition to the Mace research contributions, the Mace distribution also represents many of the best-quality publicly-available implementations of the included services, and by itself represents a practical contribution that users worldwide recognize and utilize.

32 15 This dissertation presents Mace, MaceMC, and the performance-testing extension to MaceMC. Chapter 2 explores the space of related work. Chapter 3 describes the design of the Mace language and runtime, while Chapter 4 describes the language in detail. Chapter 5 describes the MaceMC modelchecker, and Chapter 6 describes the variant of MaceMC that can find performance problems. The dissertation concludes with Chapter 7, which describes future directions and open problems.

33 Chapter 2 Related Work The Mace toolkit includes a new domain-specific programming language, libraries, a model checker, and various tools for performance testing and monitoring. It is therefore closely connected to a large body of work in the area of languages, libraries and toolkits for building concurrent systems. We, however, focus our attention on those specific to distributed systems. It is also related to other systems for testing, and in particular, model checkers. 2.1 Related Languages and Toolkits The features developed in the Mace language are not wholly new. Rather, Mace eclectically composes elements of language design available in other languages to develop a language suitable for high-performance distributed systems MACEDON Mace builds upon the earlier MACEDON [RKB + 04] work a domain-specific language for fair comparisons of overlay systems. MACEDON also represents systems as I/O automata, but does not consider how compiler extensions that restrict specifications can support model checking, high performance, debugging, etc. Whereas MACEDON focused on building prototype lab experiments, we designed Mace as a practical, real-world environment for developing deployable high-performance, reliable applications. 16

34 State-Event Systems The state-event-transition model Mace is founded on is closely related to other event-driven languages and libraries. NesC [GLvB + 03] is a language for building sensor networks with limited resources requiring static memory allocation. Broadly speaking, several researchers have investigated providing language support for building concurrent systems out of interacting components, such as Click [KMC + 00] for building routers from modules and the Flux OsKit [FBB + 97] for building operating systems. These approaches, however, target concurrent systems executing within one physical machine rather than distributed systems scattered across a network Declarative Languages P2 [LCH + 05] is a declarative, logic-programming based language for rapidly prototyping overlay networks by specifying data-flow between nodes using logical rules. While P2 specifications are substantially more succinct than those in Mace, we feel the corresponding specification is not as natural to programmers. Additionally, the P2 authors admit their runtime sacrifices performance performing only within an order of magnitude for the best case. Also, while Mace is well-suited for building overlays, its applicability is broader. There is a line of work in the functional programming community for advanced, type-safe languages for distributed computation [SLW + 05]. At the moment these languages are somewhat experimental, emphasizing full understanding of the semantics of high-level constructs for distributed programming and their interplay with the type system rather than enabling the rapid deployment of robust, high-performance distributed systems Library Toolkits Several libraries and toolkits also support many of the common primitives required for building distributed systems. Libasync [Maz01] uses a single-threaded event-driven model that makes extensive use of callbacks. Libasync also provides some compiler support for dealing with remote procedure calls and for generating some of the serialization code for messaging. Another instance, SEDA [WCB01],

35 18 provides an architecture for event-based systems. Both of these systems focus on simplifying the implementation of event-driven code, rather than a structure for distributed systems Aspect-Oriented Programming Mace uses ideas proposed by Aspect-Oriented Programming (AOP) [Kic96]. One of the first examples of AOP was a domain-specific language for writing distributed software [Lop96]. One primary contribution of Mace is identifying the different concerns that comprise a distributed system e.g. the messages, events, transitions, failures, and logging and designing a language that enables programmers to think about these in isolation. The Mace compiler seamlessly puts each of these together to create an efficient implementation of the system. An immediate payoff of this separation is the ease with which a programmer can log and monitor entire event flows without cluttering the code with print statements High-Level languages There are several high-level languages for describing network protocols, rather than entire distributed systems. Some of these, such as LOTOS [BB87] and ES- TELLE [BD87], are intended largely to formally specify protocols using message passing finite state machines. Promela [Hol03] and TLA [Lam02] are two more general languages that can be used to model concurrent systems. Instead of producing executable systems, they compile the description into large finite state machines to exhaustively analyze for errors. RTAG [And88] based on grammars and Prolac [KKM99] based on an object-oriented model are two examples of protocol description languages that actually compile the description into executable code. Mace combines the benefits of both approaches by structuring the description of the system such that the subsequent compiled implementation is amenable to exhaustive analysis.

36 Error Location Tools Our work is related to several techniques for finding errors in software systems. Some of these techniques fall under the broad umbrella of model checking. These can be further classified as classical model checking, model checking by systematic execution, and model checking by abstraction. Another related approach is the use of dataflow-based static analyses and the analysis of program text to find errors Classical Model Checking Model checking, i.e., checking a system described as a graph (or a Kripke structure) was a model of a temporal logic formula independently invented two 1981 research papers [CE81, QS82]. A Turing award was recently awarded for this research result. Advances like Symmetry Reduction, Partial-Order Reduction, and Symbolic Model Checking have enabled the practical analysis of hardware circuits [McM00, AHM + 98], cache-coherence and cryptographic protocols [DDHY92], and distributed systems and communications protocols [Hol97]. SPIN [Hol97] introduced the idea of state-hashing used by MaceMC. However, the tools described above require the analyzed software to be specified in a tool-specific language, using the state graph of the system constructed either before or during the analysis. Thus, while they are excellent for quickly finding specification errors early in the design cycle, it is difficult to use them to verify the systems implementations. MaceMC by contrast tests the C++ implementation directly, finding bugs both in the design and the implementation Model Checking by Random Walks West [Wes86] proposed the idea of using randomization to analyze networking protocols whose state spaces were too large for exhaustive search. Sivaraj and Gopalakrishnan [SG03] propose a method for iterating exhaustive search and randomization to find bugs in cache-coherence protocols. Both of the above were applied to check safety properties in systems described using specialized languages yielding

37 20 finite state systems. In contrast, MaceMC uses randomization to find liveness bugs by classifying states as dead or transient, and further, to pinpoint the critical transition Model Checking by Systematic Execution Two model checkers that directly analyze implementations written in C and C++ are Verisoft [God97] and CMC [MPC + 02]. Verisoft, from which MaceMC takes the idea of bounded iterative depth-first search, views the entire system as several processes communicating through message queues, semaphores, and shared variables visible to Verisoft. It schedules these processes and traps calls that access shared resources. By choosing the process to execute at each such trap point, the scheduler can exhaustively explore all possible interleavings of the processes executions. In addition, it performs stateless search and partial order reduction allowing it to find critical errors in a variety of complex programs. Unfortunately, when we used Verisoft to model-check Mace services, it was unable to exploit the atomicity of Mace s transitions, and this combined with the stateless search meant that it was unable to exhaustively search to the depths required to find the bugs MaceMC found. A more recent approach, CMC [MPC + 02], also directly executes the code and explores different executions by interposing at the scheduler level. However, to avoid re-exploring states, CMC uses state hashing instead of partial-order reductions. CMC has found errors in implementations of network protocols [ME04] and file systems [YTEM04]. JavaPathFinder [HP00] takes an approach similar to CMC for Java programs. Unlike Verisoft, CMC, and JavaPathFinder, MaceMC addresses the challenges of finding liveness violations in systems code and simplifying the task of isolating the cause of a violation. Additionally, these tools uniformly ignore time, and hence cannot find errors related to performance anomalies, as Mace performance tools can Model Checking by Abstraction A different approach to model checking software implementations is to first abstract them to obtain a finite-state model of the program, which is then explored

38 21 exhaustively [Hol00, CDH + 00, CW02, GS97, BR02, CPR06] or up to a bounded depth using a SAT-solver [CKL04, XA05]. This model can be obtained either by restricting the types of variables to finite values, as is done in Feaver [Hol00], Bandera [CDH + 00], Mops [CW02] or by using predicate abstraction [GS97] as is done in Slam [BR00] and Blast [HJMS02]. A related technique is bounded model checking implemented in CBMC,Saturn where all executions up to a fixed depth are encoded as a propositional formula, which is then solved using fast modern SAT solvers to find bugs. Of the above, only Feaver and Bandera can be used for liveness-checking of concurrent programs, and they require a user to manually specify how to abstract the program into a finite-state model Dataflow Based Static Analyses Dataflow based static analyses are typically highly scalable. Examples include Splint [EL02], Cqual [USW01, JW04], MC [ECCH00], and Esp [DLS02], which have found many bugs such as null-pointers, data races and buffer overflows in a variety of large applications. However, these methods are best at finding a particular class of errors. Furthermore, the relatively high rate of false positives prohibits their use for finding the subtle protocol errors targeted by our work Isolating Causes from Violations Naik et al. [BNR03] and Groce [GV03] propose ways to isolate the cause of a safety violation by computing the difference between a violating run and the closest non-violating one. MaceMC instead uses a combination of random walks and binary search to isolate the critical transition causing a liveness violation, and then uses a live path with a common prefix to help the programmer understand the root cause of the bug Model Checking for Real-Time and Hybrid Systems There is a significant body of research on extending temporal logic and automata to account for the passage of time [ACD90, AH92], and for hybrid sys-

39 22 tems [ACH + 95]. Moreover, there are model checkers like Uppaal [BLL + 96] and Hytech [HHWT97] for checking manually constructed models of real-time and hybrid systems. These systems find errors by finding executions that violate timed temporal properties. For example, they might find executions that violate a specification of the form event B must happen no later than 10 seconds after event A. In contrast, our system runs on implementations and finds the common case performance and uses outlier executions to find performance anomalies Performance Error Detection Pip is a concurrent, complementary technique for finding bugs in distributed systems [RKW + 06]. Pip is an annotation language and an expectation checker that can be applied to executions. It provides a way to visualize distributed path-flow in a system and to write expectations to validate system paths. By writing a set of execution validators, the idea is that you can find performance bugs by looking at any non-validated paths. Our model checker is simpler to use and run because it does not require deploying your system, it can automatically test a wide variety of executions, and it does not require careful examination of every possible distributed path-flow. X-Trace [FPK + 07], an effort similar to Pip in many ways, focuses on the tracing of messaging between applications through extensions to the existing protocol stack. It allows tracing across protocol layers and networks, and allows developers to better understand the performance of their system. Like Pip, X-Trace is focused on individual debugging of live executions, whereas our model checker is focused on automatic location and detection of anomalous executions. Finally, Trend-Prof [GAW07] allows users to measure the empirical computational complexity of implementations, by plotting the performance of the system across a range of input sizes. Divergences in expected behavior can pinpoint bottlenecks in the code e.g. functions whose run-times should grow linearly with input size, but instead grow at a faster rate. It is not clear if such techniques can be adapted to the dynamic and uncertain environment of distributed systems.

40 Summary Despite previous and concurrent work in distributed systems toolkits, tools for finding bugs, and performance supporting tools, building distributed systems remains challenging. In light of this work, Mace seeks to bring together the best of these efforts with its own contributions to provide a complete programming environment for building distributed systems.

41 Chapter 3 Mace Design Currently, there are three ways of specifying distributed systems. First, formalisms such as I/O Automata [Lyn96] or the Pi-Calculus [Mil89] can be used to model distributed algorithms as collections of finite-state automata (or processes), one for each node of the system that interact by sending and receiving messages. Though these formalisms succinctly capture the essence of many distributed protocols and algorithms, they abstract away and ignore the low-level implementation details essential to deploying robust, high-performance systems. Second, higher-level programming languages such as Java, Python, and Ruby have eased some of the tedium associated with building distributed systems. However, they often introduce performance overheads and do not significantly simplify the task of ensuring system correctness or identifying inevitable performance problems. Thus, developers seeking efficiency resort to the third option of assembling applications in an ad-hoc, bottom-up manner. While the resulting systems may be fast and reliable, they sacrifice structure, readability, and extensibility. The lack of structure in particular significantly limits the ability to apply automated tools, such as model checkers, to find subtle performance and correctness problems. This chapter demonstrates how programming language, compiler, and runtime support can combine the elegance of high-level specifications with the performance and fault-tolerance of low-level implementations. We seek to drastically lower the barrier to developing, maintaining, and extending robust, high-performance distributed 24

42 25 applications that are readable and amenable to automatic analysis for performance and correctness problems. The main difficulty in building these systems arises from the distributed, concurrent, asynchronous, and failure-prone environment where distributed systems run. System complexity requires that applications be layered on top of fast routing protocols, which are built on top of efficient messaging layers. Concurrency and asynchrony imply that events simultaneously take place at multiple nodes in unpredictable orders. Messages may be delivered in arbitrary orders, dropped completely, or delayed nearly indefinitely. Nodes may at any time have multiple outstanding messages in-flight to other nodes and multiple pending received messages ready to be processed. Further, an arbitrary subset of nodes or links may fail at any time, leaving the system as a whole in a temporarily inconsistent state. These properties make maintaining performance and correctness difficult. A single high-level system request may require communication with many nodes spread across the Internet. Often, even seemingly correct distributed system implementations perform an order of magnitude more slowly than expected. Analyzing executions to find the source of such problems frequently reduces to searching for a needle in a haystack: among (at least) millions of individual message transmissions, algorithmic decisions, and the large number of participating nodes, which network link, computer, or low-level algorithm resulted in performance degradation? While each of these problems has well-known solutions, the task of addressing them simultaneously proves to be quite challenging because of their subtle interactions. For example, object-oriented design is the canonical way to build systems from sub-systems, but for distributed systems hiding internal state from other layers results in serious performance penalties and duplicate effort. Similarly, there are standard ways to detect and handle failures, but the code for doing so must be interspersed (usually repeatedly at multiple points) with the code for handling common case operation, not only obfuscating the code but also eliminating the high-level structure required to use techniques like model checking [God97] and performance debugging [AMW + 03, BIMN03, CKF + 02]. In this chapter, we present Mace, a new C++ language extension and sourceto-source compiler for building distributed systems in C++. Mace seamlessly com-

43 26 bines objects, events, and aspects to simultaneously address the problems of layering, concurrency, failures, and analysis. While these are well known programming language ideas, the key advances of Mace are twofold. First, we unify, in one development environment, the diverse elements required to build robust and high-performance distributed systems. Second, and more importantly, by defining a language extension to write distributed systems, we are able to restrict the ways that such systems can be built. For our domain, this restriction is both expressive enough to permit the compilation of readable high-level descriptions into implementations matching the performance of hand-coded implementations and structured enough to enable the use of automatic, efficient, and comprehensive static and dynamic analysis to locate and understand behavioral anomalies in deployed systems. By using a structured yet expressive approach tailored to distributed systems, Mace provides many concrete benefits: Mace allows the programmer to focus on describing each layer of the distributed system as a reactive state transition system, using events and transitions as the basis for system specification. This explicitly maintains structure given by highlevel formalisms while enabling high-performance implementations. Mace uses the semantic information embedded in the system specification to automatically generate much of the code needed for failure detection and handling, significantly improving readability and reducing the complexity of maintaining internal application consistency. Mace supports automatic profiling of individual causal paths the sequence of computation and communication among nodes in a distributed system corresponding to some higher level operation, e.g., a lookup in a distributed hash table. Mace exports a simple language that allows developers to match their expectations of both system structure and performance against actual system behavior, thereby isolating performance anomalies. Mace s state transition model enables practical model checking of distributed systems implementations to find both safety and liveness bugs. The Mace model checker, MaceMC, successfully finds subtle bugs in a variety of complex

44 27 distributed systems implementations. Most of the bugs were quite insidious, present in mature code, and could not be found without exploiting the structure preserved by Mace. 3.1 Overview Drawing from our experience building a variety of distributed systems based on high-level specifications, we categorize the gap between specification and low-level features essential to real deployments into the following categories: Layers: To manage complexity, network services consist of a hierarchy of layers, where higher level layers are built upon lower levels. The canonical example is the Internet protocol stack where, for example, the physical layer is responsible for modulating bits on a medium; the link layer delivers packets from one node to another on the same physical network; the network layer delivers packets between physical networks; and the transport layer provides higher level guarantees such as reliable, in-order delivery to application end-points. Each layer builds upon well-defined functionality of the layer below it and can typically work on a variety of implementations of the underlying layer s interface. Concurrency: A distributed implementation must properly contend with and exploit concurrency to maximize performance. For example, an overlay routing application must simultaneously contend with the application layer sending requests to the routing layer, the networking layer receiving new messages that must be passed up to the routing layer, and timers executing scheduled tasks. While these events may be interleaved on a single node, they can also be arbitrarily interleaved across nodes as well. Failures: A robust implementation must account for the inevitable failures of different components or nodes of the distributed system. Failures are often difficult to detect; for instance it is impossible to distinguish between a failed node and one that is particularly slow. Further, the remaining nodes must correctly update their state to reflect the new configuration, lest inconsistencies lead to further errors.

45 28 Analysis: Given the complex operating environment, there are always performance bottlenecks and correctness issues that arise because the developers overlooked some subtle scenario or miscalculated some parameter like a message timeout. An implementation must be structured and readable enough to permit the manual and automatic analyses required to fix such performance and correctness problems. While well-known techniques address each of these issues in isolation, the primary challenge in our setting is to devise mechanisms that help the programmer resolve the tensions arising from complex interactions between the four problems. For example, the standard solution to the problem of layering is Object-Oriented design. However, for high-performance applications, treating layers as black boxes that hide their internal state and mask failures leads to performance bottlenecks. For instance, higher level routing algorithms greatly benefit from lower level information about link latencies and knowledge about which nodes or links have failed. Further, multiple sources of concurrency complicate the task of propagating information consistently between layers. Similarly, failures make it difficult to design a layering mechanism. The approach of masking low-level failures while appealing because it simplifies higher layers is insufficient in distributed environments because it sacrifices significant performance gains available from notifying the upper layers of the failure. For instance, the transport layer could mask failures by buffering sent messages and attempting to resend them until it succeeds; however, doing so would prevent higher layers from adjusting their own state to achieve better performance, such as a multicast layer reconfiguring its tree structure for higher throughput after a failure. Unfortunately, the task of notifying the upper layers is complicated by the fact that failures can happen concurrently with other system events. Further, concurrency makes it tricky to cleanly separate the failure detection and handling code from the rest of the commoncase code, obfuscating the resulting system and destroying structure. Standard techniques such as profiling to find performance bottlenecks and model checking to find pernicious bugs typically cannot be applied to distributed systems implementations. In ad-hoc implementations, the code that handles concurrency and failures obscures code structure making manual and automated reasoning

46 29 DHT Application Recursive Routing Bamboo UDP Routing TCP Routing OS Kernel Figure 3.1: Bamboo DHT design architecture. difficult (but essential). The principle technique used by developers to analyze deployed systems is tedious ad-hoc logging that clutters the code and often delivers only limited value because the programmer must manually stitch together spatially and temporally scattered logs. Finally, concurrency makes it difficult to reason about or to even replicate behaviors (due to non-deterministic factors like network latencies and scheduling decisions), thereby severely increasing the time and effort required to find complex bugs via testing. In our experience, particularly subtle bugs may remain latent for weeks in a deployed system. Further, because these bugs often result from inconsistencies between the state at multiple nodes, the subsequent departure or failure of a node after the bug manifests itself can push the system back into a consistent state, masking the bug and making it even more difficult to track. Thus, to develop high-performance systems from high-level specifications, we must devise techniques to architect the system, to determine when failures have occurred, and to propagate and exploit information throughout the architecture. These techniques must operate in a concurrent setting, enable modularity and reuseability, and explicate the high-level structure of the algorithm, thereby enabling manual and automatic system level analyses.

47 30 Mace Design Principles To address the challenges posed by the domain of distributed applications, we base Mace on three fundamental concepts. Objects: Mace structures systems as a hierarchy of service objects connected via explicit interfaces. We use an object to implement each layer of the system running on an individual node. The interface for each layer specifies both the functionality provided by that layer as well as any requirements that must be satisfied to use that layer. Events: Mace uses events as a unified concurrency model for all levels of the system: within an individual layer, across the layers at a single node, and across the nodes comprising the entire system. Each event corresponds to a method implemented by a service object. Aspects: Mace provides aspects to describe computations that cut across the object and event boundaries; in particular, aspects define tasks that need to be performed when particular conditions become satisfied. While each of these ideas have been studied extensively in isolation, we demonstrate that they combine synergistically to preserve the high-level structure of the distributed system and simultaneously address the complexity and challenges of building robust, high performance implementations. We use a popular Distributed Hash Table (DHT) to illustrate the challenges associated with building distributed systems and our approach to addressing these challenges. DHTs support put and get operations on a logical hash table whose actual storage is spread across multiple physical machines, and thus form a convenient abstraction for building higher-level applications like distributed file systems [DKK + 01, MMGC02]. The key properties of a DHT implementation are scalability and robustness to failure. We consider a DHT built on the Bamboo routing protocol [RGRK04a] (similar to Chord [SMK + 01] or Pastry [RD01]).

48 31 Layers Nodes in Bamboo self-organize into a structure that enables rapid routing of messages using node identifiers. This protocol forms a single layer of the DHT shown in Figure 3.1. Bamboo is built on top of a TCP subsystem that maintains network connections and delivers messages and a UDP subsystem that sends latency probes. A recursive routing subsystem routes messages to the node owning a given key by asking Bamboo for the next hop towards the destination. The DHT application layer uses the lower layers to store and retrieve data. Mace enables programmers to build layered systems by using objects to implement individual layers and events to facilitate interaction across layers. For each layer, the programmer writes interfaces specifying the events that may be received from or sent to the layers both above and below. A layer s implementation consists of a service object that must be able to receive and may send all the events specified in the interfaces. Thus, Mace combines objects and events to enable programmers to build complex systems out of layered subsystems, thereby abstracting functionality into layers with specified interfaces and allowing the safe reuse of different implementations (meeting the same interface) of a particular layer in different systems. Concurrency In Bamboo, a key challenge is to provide fast message routing while simultaneously dealing with node churn i.e. the arrival and departure of nodes from the system. To achieve this goal, the system must concurrently process network errors, messages from newly created nodes, and periodically perform maintenance to ensure routing consistency. In Mace, each service object consists of a state-transition system beginning in some initial state. Each node progresses by sequentially processing external events originating from the application, the physical network layer, or self-scheduled timers. Upon receiving an event, the service object executes a corresponding transition to update its state, during which it may transitively send new events to the layers above and below, each of which are processed synchronously without blocking until completion. Furthermore, a transition may queue new external events locally by

49 32 scheduling timers and remotely by sending network messages. Once processing for a given external event completes, the node picks the next queued external event and repeats. The Mace event-driven model provides a unified treatment of the diverse kinds of concurrency that must be handled in an efficient implementation: the reception of messages from other nodes (via the transport layer), the reception of high-level application requests, timers firing, and cross-layer communication all correspond to events that the relevant layers must handle via appropriate transitions. Additionally, Mace ensures that the transitions execute without preemption, freeing the programmer from worrying about exponential interleavings of concurrent executions. Finally, because Mace automatically dispatches events through a carefully tuned scheduler, Mace systems can achieve the throughput necessary for high performance applications with minimal programmer involvement. Thus, objects, events, and aspects enable Mace to describe each layer of a complex application with the simplicity and conciseness of high-level models. When combined with the modular layering mechanism, Mace provides a succinct representation of the entire computation stack for each node of the distributed system. Failures Bamboo builds an overlay network forming a logical ring among the nodes. To create and maintain this topology, each node keeps references to its adjacent peers in the ring. If one node fails, the application-level state corresponding to the relationships between that node and its neighbors may become inconsistent, breaking the overlay structure. Mace uses aspects to cleanly specify how to consistently update local state in response to a variety of cross-layer events, such as: node arrivals, departures, and application-level failures. The developer can specify predicates over the variables of a given node that test for programmer-specified inconsistencies. Mace generates code to evaluate the predicate whenever the relevant variables change and to execute the aspect when the predicate is satisfied. Aspects provide an ideal mechanism for specifying and detecting failures and inconsistencies, because without them, the developer would have to undertake the tedious and error-prone task of manually placing check-

50 33 ing code throughout the system, additionally reducing readability. When a failure occurs, the Mace runtime sends notification events to the appropriate layers. Upon receiving these events, the system executes recovery transitions. Thus, Mace combines objects, events, and aspects to provide clean mechanisms for specifying, notifying, and handling various types of failures and for maintaining the consistent internal state necessary for fault-tolerant implementations. Analysis Bamboo routes messages through several intermediary nodes, so tracing the forwarding path for a specific message manually, for instance to debug the timing of a request, involves inspecting multiple physically scattered log files. Mace simplifies such analysis tasks through preservation of the explicit high-level structure of the distributed application with three techniques. First, Mace uses aspects to separate code needed to log statistics, progress, or debugging information from the actual event handling implementation. By removing the distracting logging statements, Mace keeps the system code readable. Second, Mace exploits the structuring of the computation into causally related event chains to generate event logs, which may be spatially and temporally scattered. These event logs can be automatically aggregated into flows describing high-level tasks, extracting the events at individual nodes corresponding to some higher-level operation. The structure preserved in the flows allows developers to use automated analysis techniques to find and fix performance anomalies. Third, the modular structure of Mace applications enables developers to test the system using simulated network layers that facilitate deterministic replay, as described in 5. MaceMC combines these deterministic layers with a special scheduler that iterates over all possible event orderings. MaceMC systematically explores the space of possible executions to find subtle bugs in the system. The event-driven nature of Mace applications reduces the number of interleavings that must be analyzed, enabling MaceMC to search deep into the execution space. Thus, objects, events, and aspects combine to structure Mace implementations that enable automated analysis techniques to improve the performance and reliability of the distributed application.

51 Mace We now describe the details of how Mace combines objects, events, and aspects to generate high performance, fault-tolerant implementations from high-level specifications. Our C++ language extensions structure each service object as a state machine template with blocks for specifying the interface, lower layers, messages, state variables, inconsistency detection, and transitions containing C++ code implementing event handlers. The template syntax allows the Mace compiler to enforce the architectural design by performing a high-level validation of the service object. Additionally, the structure gives the Mace compiler the necessary information to automatically generate efficient glue code for a variety of tasks that in previous, ad-hoc implementations, had to be manually inserted by the developer. This section discusses how Mace addresses many of the challenges associated with building distributed systems Layers To specify a distributed system in Mace, the programmer simply specifies the set of layered service objects (abbreviated to services) that comprise a single node and the implementation of the required interface for each service object. Figure 3.2 depicts a more detailed view of the Bamboo architecture (shown earlier in Figure 3.1), including the interfaces. Interfaces. An interface comprises a set of downcall events and a set of upcall events. Upper layers send downcalls received by lower layers. Lower layers send upcalls received by the upper layers. We model events using methods sending corresponds to calling the appropriate method, and receiving corresponds to executing the method. In Figure 3.2 on the left, we show two interfaces: Overlay and Route. For each interface, the top half (lightly shaded box) corresponds to the upcall events, and the lower half (darkly shaded box) shows the downcall events. (Note that the syntax shown is for illustrative purposes only. The actual syntax is described in )

52 35 interface Overlay { upcalls { void notifysuccessors(nodeset successors); void notifysuccessoradded(nodekey id); void notifysuccessorremoved(nodekey id); void notifyidspacechanged(keyrange range); } downcalls { bool idspacecontains(nodekey id); NodeSet getsuccessors(); NodeKey getnexthop(nodekey dest); } }; NodeKey getnexthop(nodekey dest, NodeKey& overlayid); DHT Application Mace Overlay Bamboo Service RecursiveOverlayRoute Service Overlay Route Route interface Route { upcalls { void deliver(nodekey source, NodeKey destination, string s); } downcalls { bool route(nodekey dest, string s); } }; Route UdpTransport Service OS Kernel Route TcpTransport Service Figure 3.2: Mace service composition for a DHT application using Recursive Overlay Routing implemented with Bamboo. Shaded boxes (dark for downcall, light for upcall) indicate the interfaces implemented by the service objects. Architecture. Developers layer service objects implementing higher layers on top of service objects implementing lower layers. To facilitate modular design and seamless replacement of one service object with another, we specify for each service object the set of lower-level interfaces it uses and the upper-level interface it provides. Used Interfaces: When specifying a service, the developer declares each lowerlevel service with a name and an interface. The service may send any of the downcall events specified in the interface to any of the lower layers, and it must implement all the upcall events to receive any callbacks. Bamboo uses two lower-level services of type Route, which it binds to local names TCP and UDP. The Bamboo implementation can call downcall route for the TCP or UDP service object implementing the lower level, as route is a downcall event in the used interface. Similarly, the lower-level TCP and UDP

53 36 services can invoke the deliver callback on Bamboo, as it is an upcall in the Route interface. Provided Interface: When writing a service, the developer specifies how upper layers can use the service via a provides interface. The service must implement all downcall events specified in the provides interface and may also send callback events to upper layers, typically in response to some prior request. Figure 3.2 shows that all arrows pointing to Bamboo have type Overlay, indicating that Bamboo provides the Overlay interface to upper-level services. Thus, the Bamboo service object must be able to receive the getnexthop event from the upper layers, as it is a downcall event in the Overlay interface. Likewise, the Bamboo service may send an upcall notifyidspacechanged event, which must be implemented by any upper layers using Bamboo. Static Checking. The Mace compiler performs two compile time checks to enforce that each service object meets the requirements of the interfaces that it uses and provides. First, the compiler checks that the object implements methods corresponding to all the upcall events in the used interfaces and the downcall events in the provided interface. Second, the compiler checks that the object only calls methods corresponding to downcall events in the used interfaces, and the upcall events in the provided interface. The service specification explicitly names lower-level services because this knowledge is required to build the service. For example, Bamboo requires two transports: one for sending protocol related messages (TCP by default) and one for probing (UDP by default). However, any upper layers that use a given service are unknown when specifying the service, hence those need not and cannot be explicitly named. We observe that the upcall events sent to upper level services are in response to previous requests made by those services. Thus, the Mace compiler automatically generates code such that every downcall is accompanied by a reference to the source of the downcall, and the service employs this reference to determine the destination of the subsequent upcall. By explicitly decomposing the whole system using layers and interfaces, Mace allows implementations of subsystems to be easily reused across different systems, as

54 37 any service implementation that meets the statically checked interface specifications can be used as the subsystem. For example, our DHT application works equally well by replacing the Bamboo service object with service objects implementing the Chord or Pastry algorithms, which also provide the Overlay interface Concurrency The standard way of modeling distributed algorithms at a high-level is with state-transition systems. Mace enables developers to reap the many benefits of this structured approach by requiring them to specify each service object as a state transition system where the transitions represent the execution of the methods corresponding to received events. Given specifications for individual service state machines, Mace can automatically compose layers to obtain an efficient, structured system implementation. A state machine specification comprises two basic entities: states and transitions. States. States are a combination of the finite high-level control states of the service protocol, along with the (possibly infinite) data states corresponding to values taken by variables such as routing tables, peer sets, and timers. Figure 3.3 shows how the programmer specifies the high-level states and state variables of the Bamboo service. The finite high-level states, init, prejoining, Joining, and Joined correspond to the four stages of joining the system. The state variables myhash, myleafset, mytable, and range correspond to the node s unique identifier, the set of peers and routing table maintained by the node, and the space of keys assigned to the node. In addition, Bamboo uses two timers, one of which is automatically rescheduled at MAINTENANCE TIMEOUT intervals by Mace compiler generated code. Transitions. There are four kinds of transitions and corresponding events: upcalls received from lower layers, downcalls received from upper layers, scheduler events received from self-scheduled timers, and aspect transitions which occur immediately following the other event types. Methods implement the transitions and update the state upon receipt of the corresponding event. Figure 3.3 shows different kinds of transitions corresponding to events the Bamboo service object may receive. The full syntax is described in 4.2.3, but briefly, a keyword labels each method and indicates

58 41 To understand how the programmer structures the code for each service into events and transitions, consider the high-level state machine for the Bamboo object illustrated in Figure 3.4. The figure shows what happens when Bamboo receives events such as application join requests, network messages, or timers causing reattempted joins. The system begins in the init state, and transitions to the prejoining state upon receiving a downcall event init from the application. When it subsequently receives the downcall event joinoverlay, it transitions to the joining state if it is not its own peer (captured via a predicate guarding the event), or to the joined state otherwise, in either case sending appropriate notification events and scheduling timers. In the joining state it periodically sends join messages to other nodes requesting to join the system, and finally, when it receives a deliver(leafsetpull) message from another node, it moves into the joined state. The rest of a node s life is spent in the joined state, where it periodically globally samples the other nodes to improve its local routing information. Sequence diagrams are an informal technique programmers use to reason about low level interactions, such as those taking place while in the joined state. Figure 3.5 shows a sequence diagram depicting the interaction between nodes performing global sampling to improve routing tables. The periodically scheduled global maintenance timer fires on node A causing it to select a random routing identifier id, and to then send a GlobalSample message to the node B, which is the next hop along the route. This message gets (transitively) forwarded by B until it reaches C, which actually owns the identifier id. C then sends a GlobalSampleReply message back to A, which, upon receiving the reply, may update its routing information. Once the programmer has worked out the details of the protocol using the sequence diagram, it is straightforward to code in Mace using transitions and events. Figure 3.3 shows (in order) how the events corresponding to (i) the firing of the global maintenance timer at A, (ii) the forwarding of the GlobalSample message to B and C, (iii) the final delivery message to the destination C, and (iv) the delivery of the GlobalSampleReply back to A, are implemented as transitions in the Bamboo service object. The bodies of the respective transitions implement the actions taken upon receiving the corresponding events shown in Figure 3.5.

59 Failures Mace s use of service objects and events greatly simplifies the task of detecting, notifying, and handling failures and inconsistencies. While layering is essential for building complex services, the information hiding endemic to layered systems often makes it difficult to deliver the best performance or the most agile fault handling. For example, when a DHT application s socket breaks due to a node failure, the TCP transport layer could attempt to mask the error. However, doing so may prevent Bamboo from being able to route around the failed node, leading to degraded performance or incorrect message delivery. Rather, the TCP transport layer must propagate the error to Bamboo so that it can update its routing table and leafset. Bamboo, in turn, further propagates the error to the DHT application, so that it can redistribute the keys stored on the failed node. Mace provides clean mechanisms for layering network services while also making it easy to deliver error notifications automatically from one layer to another when required for performance or fault tolerance. Mace employs upcalls to signal higher layers of potential performance and correctness issues. It may be possible to correctly handle an issue entirely at a lower layer, but with suboptimal performance. If this is acceptable to upper layers, then they can simply ignore the corresponding upcall. However, for best performance it may be necessary to register handlers for such upcalls. While such cross-layer communication usually obfuscates code and eliminates many of the benefits of layering, we leverage our event-based structure to cleanly separate the notification and recovery code from the rest of the system that executes in the non-exceptional case. Mace addresses the remaining challenge of providing programmers with a succinct but flexible mechanism for detecting both failures and inconsistencies through the use of aspects. Aspects provide a unified way to maintain consistent state, regardless of whether the state needs to be updated in response to an expected protocol event, such as a node arrival, or an unexpected event, such as a failure. Mace aspects check for two types of inconsistency/failure detection: those that involve purely local state and those that involve multiple nodes.

60 43 Local Failure Detection. Failures occurring in distributed systems can be characterized via inconsistencies in the values of state variables. A local failure occurs when the values of state variables at a single node are inconsistent. For example, in our DHT application built on top of Bamboo, the data that each node is responsible for depends on the key space specified by the range variable. In other words, the views of the range of the DHT layer and the Bamboo layer must be synchronized, and if they are not, recovery action must be taken so that the DHT relocates the data according to the new range, potentially involving communication with remote system nodes. Such failures can be specified using a predicate that characterizes the inconsistent values, i.e., which becomes true when the values of the variables are inconsistent. Thus, such failures can be locally detected by monitoring the predicate, and firing an event when the predicate becomes true. In Mace, the programmer specifies how failures should be detected and how to react to the failure using an aspect transition. A failure occurs when this predicate is true, which fires an error event and notifies the upper layers of the inconsistency. Consider the example in the top of Figure 3.6, showing a local detection aspect that specifies that an inconsistency failure occurs when the value of the variable range changes (the aspect only fires when the monitored variables change, so no additional guard function is needed). When the change occurs, Mace sends a notifynewrange event to the upper DHT layer indicating that its portion of the key space has changed and prompting it to reorganize stored data appropriately. This aspect will correctly react to all events that change the range, whether it be the arrival of a new peer adhering to the Bamboo protocol or an unexpected peer failure. The local detection aspect checks the predicate only at transition boundaries, avoiding notification of state that may become temporarily inconsistent in the middle of a transition. Thus, aspects and events provide a clean way to separate the failure detection, notification, and handling from the rest of the common-case code. We implement detection by keeping a shadow copy of monitored state variables, checking and updating them after each transition. In ad-hoc implementations, built without language support, the programmer would have to manually insert the check and notification each time the variables might be modified. In addition to greatly reducing

61 44 readability, this task is error-prone, especially as the code evolves or is maintained by multiple programmers. Distributed Failure Detection. A distributed failure occurs when the values across two or more nodes are inconsistent. For example, in Bamboo, each node maintains the set of its immediate peers in the state variable myleafset. Each such peer, in turn, must include the node in its own set of known peers. A distributed failure occurs if some element of a node s leafset does not include the node in its own set of peers. As a node cannot directly access the other nodes internal state, the only way to determine the presence of such a failure is to actively exchange information across nodes, checking that the received information is consistent, and if so, returning messages acknowledging consistency. If the originating node receives the acknowledgment before a timeout occurs, it confirms that no failure has occurred. The programmer specifies how to detect and react to distributed failures in Mace using a detection aspect, defining elements for the aspect, as shown at the bottom of Figure 3.6. The nodes are the set of nodes monitored by the aspect. In this example, it is the nodes stored in the set myleafset. The interval trigger method indicates how the probes are sent to the elements of nodes. Here, the node sends LeafsetPush messages with the current value of myhash and myleafset state variables to the elements of myleafset, once every 5 seconds. Finally, a suppression transitions block indicates the response expected from the other nodes, together with a timeout before which the response must arrive. Here, it stipulates that the node must receive a LeafsetPull message from each of the other nodes in myleafset before a timeout period of 5 minutes elapses. Mace generates extra state and the code needed to keep track of the last time it has heard from each monitored node. It sets a timer to fire sometime after it expects to receive an acknowledgment of a particular remote configuration. If the timeout occurs and the guard is true, then Mace calls the event specified in the timeout trigger, notifying upper layers of the failure. As in the case of the local failures, the transition corresponding to the error event corresponds to the code that implements the recovery mechanism. Likewise, Mace simplifies the detection of distributed failures by separating the detection code into an aspect and automating the process of sending the probe messages and detecting timeouts.

62 Analysis By using objects and events to preserve the high-level structure of the distributed system, Mace can automate a variety of post-development analyses that find performance or correctness problems. Execution Logging and Debugging. Mace uses aspects to generate debugging and logging code without cluttering the service specification. Mace exploits the preserved structure to enable different levels of automatic logging. First, with event-level logging, the generated program logs the beginning and end of each high-level event. This process captures the order and timing of events at each node. With state-level logging, every time a transition finishes, the generated program logs the node s complete state, which indicates the change caused by the transition. Finally, with message-level logging, the generated program additionally logs the content and transmission time for each message sent from or received by the node. We have used the automatically generated logs to implement mdb, a replay debugger for Mace distributed applications. mdb collects all individual node log files centrally and allows the developer to single step, forward and backward, through the execution of individual nodes. The developer may move from node to node, inspecting global system state, in a manner similar to traditional single process debuggers. Causal-Paths. Mace provides a more advanced form of logging that aggregates execution events distributed across multiple nodes into a set of causal paths. Each path starts at a given node, with a particular seed event, and contains the sequence of all events that are causally, transitively related to the seed event. For example, if the seed event is a request generated by a particular node, then the causal path includes the sequence of messages (and resulting events and transitions) that span the different nodes until the response returns to the requester. To obtain such causal paths in a Mace application, the programmer specifies the seed event where the path begins and the event that ends the path. Mace tags all the relevant, causally related activity that occurs between the seed and the end (i.e., all events, transitions, messages sent and received) with a dynamically generated path identifier and generates logs such that events distributed across multiple nodes can be collected using their shared path identifier. As a result, Mace enables logging

63 46 System Mace Distribution Bamboo BulletPrime Chord Overcast 450 NA Pastry Scribe SplitStream Vivaldi Table 3.1: Lines of code measured in semicolons for various systems implemented in Mace and other distributions. at a semantic-level and allows programmers to understand and analyze the behavior of the system at a high level. In addition, previous work [RKW + 06] describes how the causal-path logging done by Mace can be automatically mined to find and fix performance anomalies, by comparing the causal paths resulting from actual executions with programmer-specified high-level expectations. While this earlier work on the benefits of causal paths to performance debugging is independent of Mace, it requires significant manual logging in standard, unstructured C++ applications. We have found that more than 90% of the logging required for causal path analysis can be automatically inserted by the Mace compiler, significantly lowering the barrier for leveraging the benefits of such performance debugging tools. Model Checking. A high-level model of a distributed system enables exhaustive analyses like model checking to find subtle bugs in either the protocol or implementation of a distributed system. Mace allows developers to use the same analysis to find subtle errors in the actual implementation of the system by making it easy to systematically explore the space of executions of the implementation. The full details of the model checker, MaceMC, are found in 5.

66 Experiences In this section, we outline some of our experiences developing distributed applications with Mace. Mace itself is implemented as a source-to-source compiler in Perl using a recursive descent parsing module. The Mace compiler emits C++ code, which is then compiled using any C++ compiler such as g++. We have implemented over nine substantial distributed systems in Mace, many of which we have run across the Internet, including on testbeds such as PlanetLab [PACR02]. In addition to the Bamboo implementation discussed here, we have also implemented the systems shown in Figure 3.1. These systems include Chord [SMK + 01], Pastry [RD01], Scribe [RKCD01], SplitStream [CDK + 03] (from the FreePastry [fre06] distribution), BulletPrime [KBK + 05] (from the MACEDON [RKB + 04] distribution), Overcast [JGJ + 00] (not available to us for line counting), and Vivaldi [DCKM04]. Excepting BulletPrime (which was written in the MACEDON language), each of these services were originally developed in unstructured C++ or Java. The Mace compiler eliminates many tedious tasks that must otherwise be hand-implemented to achieve high performance, such as message serialization and event dispatch, and correspondingly drastically reduces the implementation size. A Mace service object implementation contains a block for specifying message types (essentially a struct with optional default values), for each of which the compiler generates a class containing optimized methods to serialize and deserialize the message to and from a byte string that can be sent across the network. The Mace compiler also generates methods to automatically perform event sequencing and dispatch. The generated code selects the next pending event, performs locking to prevent preemption, evaluates any guard tests for the transition, executes the appropriate method implementing the event handler (assuming the guards succeeded), tests any aspect predicates that might have been updated by the transition, and finally releases the acquired locks. Overall, we find that the structure imposed by Mace greatly simplifies the implementation by allowing the programmer to focus only on the essential elements, without compromising performance or reliability.

67 Performance Evaluation To evaluate the performance of Mace systems, we compare our Bamboo implementation in Mace with its well-tested counterpart [RGRK04b]. To distinguish the two versions, for this section we will refer to our implementation as Mace-Bamboo. We chose Bamboo because of its excellent performance, detailed published performance evaluation, and its publicly available and well documented code base. Bamboo is a highly optimized Java implementation of a distributed hash table, based originally on Pastry [RD01]. We compare behavior of node lookups under churn. Lookups operate by forwarding a message using increasing prefix matching to nodes whose identifiers are progressively closer to the key. Bamboo explores the limitations of previous protocols in providing consistent routing in the presence of node churn, and proposes several modifications to Pastry to allow it to deliver high consistency and low latency even when nodes are entering and leaving the system at a high rate. Consistency is a measure that captures whether different nodes routing to the same identifier will reach the same destination. This is the most important requirement for correct performance of applications using a DHT, since they rely on being able to share data by using the same identifier to store and retrieve values. Our exercise of re-implementing Bamboo serves to show the simplicity of implementing distributed systems in Mace and our ability to generate robust, efficient, and high performance code. Two experienced Mace developers implemented the primary Bamboo algorithms in twelve hours (excluding the reliable UDP transport), starting from an existing Mace Pastry implementation. To compare against published Bamboo experimental results (we attempted to reproduce the published results but could never achieve them, most likely due to having fewer machines), we prepare a framework that matches, to the best of our ability, the original experimental conditions. The experiment consists of 1000 Bamboo nodes organized into groups of 10 performing simultaneous lookups of random keys. A lookup result is considered consistent if a majority of the 10 nodes return the same result. Each group of 10 nodes performs lookups according to a Poisson process with an average inter-lookup delay of 1 second. For the runs, we vary the median churn

68 51 rate also according to a Poisson process, ranging from on average 8 deaths per second to 1 death per second. We run 1000 Bamboo instances on 16 physical machines (the published Bamboo results used 40 machines), using the ModelNet [VYW + 02] network emulator with a single FreeBSD core. Each of the physical machines is a dual Xeon 2.8Mhz processor with 2GB of RAM. During the runs, load averages ranged from 0.5 to 1.5. The emulated topology consists of an INET network with 10,000 nodes, 9,000 of them routers. Client bandwidths on the topologies ranged from 2-8Mbps. To start the experiment, nodes were staggered, starting one on each machine each second for a minute. The churn and lookup schedules began as soon as all nodes were live. This experimental setup differs from the published Bamboo experiments in that the stagger-start is at a much faster rate, we do not wait for the network to settle after starting all nodes, we run with 1/3 the number of machines, and our request load is 10 times higher. Figure 3.7 shows the consistency numbers for the Java-Bamboo and Mace- Bamboo. The published consistency values demonstrate near-perfect consistency at all churn levels. While still above 92% consistent, Mace-Bamboo nodes are slightly less consistent than their counterpart, though they track its performance closely. However, as shown in Figure 3.8, the Mace-Bamboo latency outperforms Java-Bamboo at each of these churn levels, and by a factor of 5 at high churn levels Undergraduate Course To aid in the evaluation and development of Mace, we have used it in two undergraduate networking courses, and it has also been used in several graduate course projects. During the spring quarter of 2005 and the spring quarter of 2006, students in advanced undergraduate networking classes at UCSD were asked to program in Mace for a class project. None of the students enrolled in the class had been exposed to Mace previously. The project involved implementing a peer-to-peer file sharing program loosely based on the popular FastTrack protocol. The protocol includes a number of distributed concepts, such as: flood-based searching, distributed election, and random network walks. To prepare for the project, students were given a one-

69 52 hour introduction to Mace, a list of protocol messages (to support inter-operation), and a skeleton template for a basic Mace service. 90% of the students successfully completed the project and a majority expressed a preference for programming in Mace relative to Java or C Summary In this chapter, we argued for the benefits of language support to construct robust, high-performance distributed systems. The principle challenge in this environment is resolving tensions between the tasks of developing a clean layering system, handling concurrency and failures, and preserving enough structure to enable automated performance and correctness analyses. The key insight behind Mace is that objects, events, and aspects can be seamlessly combined to simultaneously address the intertwined challenges. Mace s language structure and restrictions enable a number of important features that are otherwise difficult or impossible to express in existing languages: language support for failure detection, causal path performance and correctness debugging, and model checking unmodified Mace code. We have employed Mace to build more than ten significant distributed applications, which have been successfully deployed over the Internet. Others are using Mace to support their own independent research and development. Using automated debugging tools that exploit the Mace structure to find and fix problems, exploiting the flexible architecture to reuse optimized subsystems across applications, and leveraging the uniform and efficient eventdriven concurrency model, Mace system specifications were about a factor of five smaller than original versions in Java and C++, while delivering better performance and reliability. 3.5 Acknowledgement Chapter 3 is an updated and revised copy of the paper by Charles Killian, James W. Anderson, Ryan Braud, Ranjit Jhala, and Amin Vahdat, titled Mace: Language Support for Building Distributed Systems, as it appears in the proceedings of

70 53 the ACM SIGPLAN Conference on Programming Language Design and Implementation c 2007 ACM DOI The dissertation author was the primary researcher and author of this paper.

71 Chapter 4 Lessons of the Mace Language In the prior chapter, we described the design, implementation, and evaluation of the Mace programming language extension. In this chapter, we describe in detail the grammar of the Mace programming language extension to C++, and some lessons we learned in the process. Some code examples were given in the prior chapter, and some more short examples will be found here. Full examples of services implemented in Mace can be found in its HOWTO documentation, and also from the public release of the code. Implementing a system in Mace entails breaking it up into a layered, asynchronous, state-event-system. Each layer should be divided by a clean separation of concerns, providing a generic service interface to higher level services. The interface must be designed such that the operation requested can complete without having to block, requiring such operations to be delayed for a future callback. The only way to access the state of any given layer is through an event-method from its interface, protecting its state from outside harm. This chapter will describe how this is accomplished in the Mace language. Programming in mace is described in the following sections. Section 4.1 describes the architectural design grammar, which describes the layering of services. Section 4.2 describes the individual components of the Mace language grammar for services, while Section 4.3 describes the set of options that can be supplied to the compiler in the grammar in various places. Finally, Section 4.4 gives a few notes about programming in Mace. 54

72 Architecture Design The complete structure of a Mace specification is given in 4.2. But the first part of this specification syntax describes the definition of the architectural design of the service both the interface it provides, and the layering of services atop other services. This section first describes the syntax of the header files that we use to define the interfaces ( 4.1.1), and then the syntax for this first part of the component architecture ( 4.1.2) Interfaces The first task to implement any system is to break it up into its interface and layer design. For a DHT application, this interface will generally be directly tied to the service it provides. In this case, the interface in question should have a method to get data based on a key, to put data based on a key, to check whether the DHT contains a given key (without retrieving the data), and to remove the given key. In Mace, we ll use the MaceKey type to represent keys or addresses, which can support a variety of types of keys. This can generally be written in a C++ header file as: class DHTInterface { public: virtual string get(const MaceKey& key); virtual bool containskey(const MaceKey& key); virtual void put(const MaceKey& key, const string& data); virtual void remove(const MaceKey& key); }; Unfortunately, this cannot be directly used in Mace, as some implementations of a DHT will likely not be able to respond to get or containskey requests without checking with other nodes. Lesson 1: In designing an interface, we must strike a balance between the perfect interface for the service implementation we have in mind, and a general implementation of that interface. When designing a new interface, we should ask

73 56 ourselves what requirements the interface design itself places on the implementation of the service. If an interface lends itself to only a single implementation, it is probably not the right interface. In this case, the best thing to do is to separate the request from the result, allowing the result to be asynchronously called back or upcalled. A service that does provide immediate availability of the result can simply perform the upcall immediately. Mace uses two types of interface files to describe service interfaces. An interface for a service (to be used by applications or higher-level services) is called a service class, while the upcall interface (or call back interface) of the service is defined by the set of handlers that are registered with the service. This relationship can be seen in Figure 3.2 (though it uses a different syntax than the one shown below). The downcalls make up the the service class, while the upcalls make up the handlers. A handler syntax file looks like this: Handler: handler Name { HandlerStatement* } Name: (a token/word that gives the name of the Handler) HandlerStatement: CppStatement Function CppStatement: (C++ statement for constants, enumerations, etc.) Function: virtual (?) FunctionDecl FunctionImpl FunctionDecl : (C++ function declaration without terminator) FunctionImpl : ; { (C++ function body) } The handler filename should be NameHandler.mh, where Name is replaced by the name of the handler. It will generate a C++ class whose name is NameHandler. C++ statements that are parsed will be pasted into the generated C++ class, and can be used to define constants or enumerations. Each of the functions defined will also be included in part of the generated C++ class. The function can either be declared (ending with a semicolon), or defined (ending with a method body), and can be virtual, or non-virtual. As with C++, virtual methods can be overridden by implementing service handlers, and non-virtual methods cannot (the latter generally are useful in an interface only to transform parameters and call another virtual method). Methods cannot be defined as pure virtual this is because handler classes must have default implementations for any method, for the null reference handler im-

74 57 plementation. The default implementation for a declared (but not defined) method will therefore be to abort execution at runtime. Another transformation the Mace compiler makes will be to add a registration uid t parameter to each method. The registration uid t parameter is used to identify a specific registration of a specific handler with a specific implementation of a service class. In a handler method, it tells the called function which service instance is calling the method. All service class functions also have a registration uid t parameter added to them, and they tell the implemented service which registered handler should receive upcalls related to this function call. The handler interface of the DHT service, therefore, is used to asynchronously deliver the result of calls from its primary interface. For example, a call to a get method will be returned by a call to the handler s dhtgetresult method, and a call to a containskey method will be returned by a call to the handler s dhtcontainskeyresult method. The put and remove calls, in our design, provide no feedback when they eventually complete, and therefore do not have a callback. We call the DHT handler object the DHTData handler, and define it as such: handler DHTData { virtual void dhtcontainskeyresult(const MaceKey& key, bool result) { } virtual void dhtgetresult(const MaceKey& key, const mace::string& value, bool found) { } }; The service class interface therefore is largely the same as the desired interface, but does not immediately return the value. The service class interface is defined in NameServiceClass.mh, and its syntax is the following: ServiceClass: serviceclass Name { ServiceClassStatement* } Name : (a token/word that gives the name of the service class) ServiceClassStatement : CppStatement Function Handlers MaceBlock CppStatement: (C++ statement for constants, enumerations, etc.) Function: virtual (?) FunctionDecl FunctionOptions(?) FunctionImpl FunctionDecl : (C++ function declaration without terminator) FunctionOptions : [ (key) = (value) ( ; (key) = (value) )* ] FunctionImpl : ; { (C++ function body) } Handlers : handlers HandlerName (, HandlerName)* ;

75 58 MaceBlock : mace WhenToInsert { MacFileBlocks } WhenToInsert : provides services MacFileBlocks : (mac file syntax blocks) This is much like the handler interface description, except that it allows two additional types of statements, and synchronous options on functions. The first additional type of statement is the Handlers statement. It tells which Handler interface files are upcall handlers associated with this service class. For each handler in the list, the generated service class C++ object will contain a registerhandler method, which allows a Handler object of the given type to be registered to receive upcalls in response to the downcalls made to the service class. In the DHT case, it will list DHTData. For any DHT service implementation, a DHTDataHandler should first be registered for a particular registration uid, then calls made on the DHT service should pass in the same registration uid, telling it which registered handler to return upcalls to. This way, the same service can be shared by multiple higher level services, using the registration uids to keep their data distinct. The second additional type of statement is a MaceBlock element, which allows the interface designer to add blocks of code to services that either provide this interface, or use a lower level service that does. The syntax of these will be described later, but this block can for example be used to (1) indicate strings that should be generally deserialized into messages through method remappings (see TransportServiceClass.mh), or (2) add transitions to every implementation such as to support automatic bootstrapping on initialization (see OverlayServiceClass.mh). The function options for service classes specify synchronous options that can be used to make it easier to use a service from code that does not wish to use it in an asynchronous manner. By defining these options, an extra class is generated in the NameServiceClass.h file that automatically blocks the calling thread until the appropriate upcall is made, resuming its operation and filling in the functional parameters. To take advantage of these options, the interface designer should specify 4 things: syncname The name of the function that will provide synchronous behavior callback The function of the handler that is the upcall response for this downcall.

76 59 id The parameter of the function that can be used to match the request and response (the function listed in callback must have the same parameter of the same type), type The type of synchronous behavior to use. Implemented options include block and broadcast. Under block, only one outstanding operation for any given id can be outstanding, and subsequent ones will block waiting for the first. Under broadcast, the first outstanding operation for any given id will cause a call to the implementation, and others will just wait. When a response is received for the first, the result will be delivered to all waiting threads. The DHT service class interface is defined as follows: serviceclass DHT { virtual void containskey(const MaceKey& key) [syncname=synccontainskey; type=block; id=key; callback=dhtcontainskeyresult]; virtual void get(const MaceKey& key) [syncname=syncget; type=block; id=key; callback=dhtgetresult]; virtual void put(const MaceKey& key, const mace::string& value); virtual void remove(const MaceKey& key); } handlers DHTData; Here you will notice essentially the same four functions, though in each case void is returned since a callback provides the result. containskey and get both define synchronous options for callbacks, to make it easy to use DHT from C++ threads that may block. An example of how to use the generated syncget method is shown here: pair<string, bool> get(const string& k) { boost::shared_ptr<synchronousdht::syncgetresult> p = dht->syncget(macekey(sha160, k)); return pair<string, bool>(p->value, p->found); } // get The generated syncget method takes the same parameters as the original method, waits until the result is available, and returns a generated object that contains each of the parameters from the matched callback method.

77 Component Architecture The first part of a Mace specification describes the component architecture of the service. The syntax is: ComponentArch: Name Provides(?) Registration(?) GenOpts(?) Services(?) Name: service (token giving the name of the service)(?) ; Provides: provides ServiceClass (, ServiceClass )(*) ; ServiceClass: (the name of a defined service class interface) Registration: registration = RegType ; RegType: static dynamic < (registration object type name) > GenOpts: Trace(?) Time(?) Trace: trace = ( off manual low med high ) ; Time: time = ( uint64_t MaceTime ) ; Services: services { ServiceVariable(*) } ServiceVariable: InlineFinal(?) ServiceClass Handlers(?) VarName SVOptions SVImpl(?) ; InlineFinal: inline final Handlers: [ Handler (, Handler)(*) ] Handler: (the name of a defined handler interface) VarName: (token for the variable name of the service class) SVOptions: RegistrationUid(?) DynamicRegistration(?) RegistrationUid: :: (the number of a fixed registration uid) DynamicRegistration: < (the type name of the registration type > SVImpl: = ( (another service variable name) (service name) ( (parameters) ) ) The second line, required, gives the name of the service. The name itself is optional, and if omitted, will be the basename of the file. If provided, it should match the basename of the file for proper integration with the build system. The next line, the provides line, tells the service class this service provides. This service should implement each of the methods in the interface as downcall transitions. Then, the compiler adds a set of upcall helper methods to return results to the higher-level service. Recall that in each ServiceClass, a set of handlers may be listed. Given each method methodname in some handler listed, a method upcall methodname is created, and when called, will return an upcall to a higher-level service. The signature of the generated upcall method matches exactly the method generated from the handler interface, including the generated registration uid parameter. That registration uid parameter is used to determine which of the registered handlers to actually

78 61 deliver the upcall. If the provides line is omitted, the default is to provide the Null service, which contains no additional methods other than maceinit and maceexit. For each service class listed in the provides line, a function of the name new service serviceclass will be generated, with the parameter list based on the services and constructor parameters ( 4.2.2). These functions will be declared in the file named ServiceName-init.h. This file can be included without including any actual code from the service itself, which helps reduce compile-time and code protection/isolation, as no actual details of the service need be known except for the constructor list and the service class it provides. Lesson 2: Mace provides a sort of extension of the OSI layered model. On top of the transport layers, which are provided to services through the transport services, each distributed system will provide its own set of layers through the component architecture. However, unlike OSI, the purpose of each layer above the transport will vary for each distributed system, and no total-ordering is possible for all interfaces, as they may be composed over each other in creative ways. The registration line determines whether handlers are registered with this service statically or dynamically, based on the RegType grammar element. Statically registered handlers are the default, and provide basic operation. But consider, for example, the case of the Overlay and Group service classes. These are essentially the same services, with both providing join and leave methods. The major difference between these interfaces is that the group interface provides a MaceKey parameter and can be used for multiple groups simultaneously. The present design of these interfaces is to use static registration. Services that can distinguish groups (such as Scribe) use the group interface, while other services (such as Overcast) would use the overlay interface. However, both of these services are implementations of a tree protocol. To enable modularity, the Tree service class has to include a groupid parameter, which is summarily ignored by services that don t support groups. These interfaces were designed before dynamic registration was an option, and have remained to support existing services. However, dynamic registration presents a new design option to consider.

79 62 A dynamic registration is one in which the same Handler object can be dynamically registered with multiple registration uids, in contrast with a static registration which is either fixed or based on instantiation order. A dynamic registration associates the registered object at both levels with a registration uid and a static handler registration. The new registration uid is computed as the hash of the concatenation of the static registration uid and the object being registered, allowing deterministic registration uids based on the object. This is critical, because the same object registered dynamically with the same static registration must result in the same registration uid, to support coordination across nodes. Consider using dynamic registration to merge the Overlay and Group interfaces become merged. Then, implementations of a Group service declare: registration=dynamic<macekey>; This allows them to associate a registration uid for each group key. Then, calls to join and leave use the dynamically allocated registration uid, and either service can query to find out the key associated with the registration. Tree services then need not contain a groupid parameter in the method signatures, instead getting it from the dynamically registered object. Dynamic registration is a relatively new language feature, and is still being evaluated. It enables services with mostly similar interfaces to actually provide the same interface, but requires a dynamic check to make sure the supplied service actually provides the same registration. Furthermore, as dynamic registrations have to match up between the higher and lower level services, care must be taken to have a small number of dynamic registration options, lest modularity cease to be useful. In that sense, a small sacrifice in interface design may be desirable over too much flexibility that prevents modularity. Next in the language definition comes two optional generation options. The first defines the trace level of the generated code (how verbose the auto-generated tracing should be). The manual option is not yet fully supported, and is identical in practice to off. The difference by design is that under the off option, all logging using the mace logging facility is removed from the generated code, including manual log messages added by users, whereas the manual option copies manually inserted

80 63 user logs into generated code, but adds no instrumentation by the Mace compiler. Both currently behave as manual. The remaining options auto-generate increasingly verbose trace logs. Under low, a statement is logged when each transition, routine, or auto type method is called, logging the values of parameters (except for Message parameters, for which only the name of the message is logged). Under med, message details are logged as well, and additionally the beginning and ending of each transition, routine, and auto type are logged, allowing a sort of stack trace view of the log file, seeing where each message was logged from. Finally, under high, in addition to the logs of med, the state of a service is logged at the end of any non-const transition, to support seeing how the state of the system changes over time. Lesson 3: Between the compiler s ability to automatically instrument a service with logging, and the logging infrastructure s ability to enable and disable logs at a fine grained level, the user needs to write fewer and fewer of their own logs. Many services can be built and debugged without the user ever writing a single log message. This is because most user debug logging is just to indicate the state of a given variable, the occurrence of an event, or the parameter to a method. The compiler understands these at the granularity of events, which makes it possible to generate an appropriate degree of logging automatically. The other generation option that can be specified is what should be the type used to represent timestamps. The two options are uint64 t, which is slightly more efficient, and MaceTime, which is modelchecker-friendly. If a service will be model checked, and it contains any time-based non-determinism (comparing the latency of operations, for example), then MaceTime should be used. Functionally, the primary difference is that when using MaceTime, helper functions (and not standard operators) should be used to compare two timestamps, or to combine/scale timestamps. These functions expose to the model checker the operations of time, helping it distinguish between a deterministic time value and a non-deterministic time value, allowing it to properly explore non-deterministic comparisons of time. During actual execution, the system runs as though with real time values.

81 64 The final portion of the architecture section of a Mace implementation is the set of lower level services a service will use. The services block can contain any number of service variables. Lesson 4: Though simplistic layered systems, like a tree service running over just a transport service, assume a single stack of layered service objects, this is neither a limitation nor always practical. Mace instead supports a directed acyclic graph (DAG) of services, which preserves the notion of layering, but greater flexibility and modularity. One such DAG was shown in Figure 3.1. Each service may use multiple lower level services, and provide multiple interfaces and be used by multiple higher-level services. This requires a bit of extra effort when coding some services, as the service must be shareable, but the benefit is greater reusability. Each service variable is declared to provide a given service class interface. For every method meth in the service class interface, a method is generated for the service of the name downcall meth, allowing the service to make calls into the lower level service. As with the upcall generated methods, the signature matches the generated signature exactly (modulo method remappings, 4.2.5), including the generated registration uid parameter. The variable name of the service variable represents the statically registered uid, and can be passed as the final parameter to the downcall method to tell it on which service variable to make the call. If there is only one service variable that provides the given function, and it is not being used with dynamic registration, or if a default has been set explicitly, this parameter may be omitted, and the Mace generated code will figure out on which service variable to make the call. Service variables represent the statically allocated registration uid they have received. The registration uid can be manually set using the RegistrationUid syntax, and should be done if non-parallel service instances need to talk to each other. As an example, for a client and server to talk over a shared transport, they must each allocate it the same registration uid. As registration uids are allocated sequentially increasing starting at 0, generally large integers should be used for fixed registration ids. To indicate that the service variable will be used with dynamic registration, use

82 65 the DynamicRegistration syntax, which specifies the type for registration. A runtime check will assure that the types match up with what the service provides. Then, in addition to the declared service class, a dynamic registration service can be used with the DynamicRegistrationServiceClass<typename T> service defined in the DynamicRegistration.h file of the interfaces directory under the services directory within Mace. Downcall methods will be generated for each of these as well. Next, for each handler listed for a service class of a named service variable, the service implementation will generate appropriate methods for receiving callbacks from the service variable, and will generate the code for registering the service with the lower level service as a handler. The service should therefore implement each of these methods as upcall transitions. The set of handlers can be restricted using the Handlers syntax, which will let the service register only the specified set of handlers with the service variable. Restricting the handlers will reduce the generated code size, reduce the number of warnings from the Mace compiler about unimplemented interface methods, and can in rare circumstances be used to share services in unconventionainterface methods ways. Each service listed in the services block will be included in the constructor for a service, unless modified by either inline or final, according to the InlineFinal syntax. This allows any service to be overridden at construction time by passing in a different implementation of the service class. The inline keyword prevents the variable from being part of the constructor, and will even prevent the service variable from being an actual variable of this service, but instead is only constructed inline as needed to pass it into lower level services. No methods, handlers, etc. will be generated as a result of inline services. The purpose of inline services is to be able to configure the lower level services, beneath the services used by this service. For example, to make a transport that is shared by two lower level services, make it inline, then pass it into both of the services. The final keyword also prevents a service from being part of the constructor, but otherwise it behaves like any other service. That is to say, the implementation or construction specified by the SVImpl syntax is the final choice of what will implement that service, and cannot be overridden at construction time.

83 66 Finally, the syntax describes the default (or final) implementation of a service should an overriding value not be available. This can be omitted, but then must be provided at construction time 1. Options for an implementation are to either provide another service variable, in which case a dynamic cast will be performed at construction time to ensure a match, or a service name, with its constructor parameters and services. This will cause the method new service serviceclass() to be called with these same parameters, to construct the given service. Lesson 5: Service construction can be one of the hardest parts of building services in Mace, because by design it is highly modular. The component architecture provides a flexible language for defining how to construct lower-level services, including declaring some as inline or final. All other services may be overridden by higher-level services, or by the application creating the services. A helpful tip when creating your service is to diagram the service DAG used in your application to help you construct all the services appropriately. 4.2 Service Specification The specification of a Mace service is given by: MaceService: ComponentArch ServiceBlocks(*) ServiceBlocks: TypeDefinitions PersistentState Execution Debugging Miscellaneous TypeDefinitions: Typedefs AutoTypes Messages PersistentState: Constants ConstructorParameters States StateVariables Execution: Transitions Routines Detect Debugging: Properties StructuredLogging Miscellaneous: MethodRemappings MInclude Anything that appears before the ComponentArch block is copied into the generated output files. This therefore is an ideal place to put include statements for necessary C++ header files. The service blocks may appear in any order, but no more than once in the main specification file. The Transitions block is required to 1 This is commonly done for simulated application services, as the modelchecker requires a service to avoid default construction anyway.

84 67 be present, but the others are all optional. The ComponentArch was described in 4.1.2, but the others are described in the following sections TypeDefinitions There are a three types of type definitions commonly used in distributed systems implementations: Typedefs, AutoTypes, and Messages. Typedefs The first of these are basic C++ typedefs, which are essentially used to give a new name to an existing type. The syntax is Typedefs: typedefs { Typedef(*) } Typedef: typedef ExisitngType NewTypeName ; The syntax is identical to a simple renaming typedef in C++. Typedefs in Mace cannot map new structure or class definitions to a typename, as was commonly done in older C designs. In Mace, we find that these typedefs generally serve two common usages, (1) to give names to various integral types that represent something specific, such as registration uid t, or (2) to map complex STL types or template types into more conveniently typed names. In addition to standard C++ types, the typedefs block may reference earlier defined typedefs, or auto-types. AutoTypes Auto types in Mace are a convenient place to create all kinds of struct or class types, while letting the compiler generate formulaic domain specific code for you. The syntax is AutoTypes: auto_types { AutoType(*) } ; AutoType: TypeName TypeOpts { Typedef(*) Field(*) CppConstructor(*) Method(*) } ; (?) TypeOpts: attribute(( TypeOpt (, TypeOpt)(*) )) TypeOpt: AttributeName ( SubTypeOpts(?) ) SubTypeOpts: SubTypeOpt ( ; SubTypeOpt)(*) ; (?) SubTypeOpt: (Value Key = Value) Field: Type VarName TypeOpts(?) ( = DefaultValue )(?) ;

85 68 Method: MethodDecl MethodOptions(?) MethodImpl MethodDecl : (C++ function declaration without terminator) MethodOptions : [ (key) = (value) ( ; (key) = (value) )* ] MethodImpl : { (C++ function body) } There may be any number of auto types, and they may reference previouslydefined auto-types, or types defined in the typedefs block. The common use for an auto-type behaves essentially like a struct each field listed will be copied as a public member of the output class. Several auto-generated methods will be generated: (1) a default constructor which initializes each parameter to its default value, or Type() otherwise; (2) a constructor with each parameter as a field (in the same order) with default values as given for fields, which sets the value for each field based on what s passed in; (3) accessor methods of the form get fieldname which return a copy of the field (these can be used as function pointers to STL algorithms or collection methods taking a function pointer to compute over the objects; (4) a method to print the object by printing each of its fields, and (5) methods to serialize and deserialize the object according to the Serializable interface, by serializing and deserializing each object in turn, which includes normal and XMLRPC serialization. An example auto types block might look like: auto_types { VersionedData { int32_t version; string data; }; } This defines a class called VersionedData in the scope of the service, with public fields version and data, auto-serialization and printing methods, a default constructor, and a constructor which takes each of the fields. Many other examples of auto types can be found in the downloadable Mace code. now. There are many ways to customize an auto-type, and these will be described Typedefs As with the typedefs block, new types may be named within the context of an auto-type. These will be created as inner-types of the auto-type, and serve the same purpose as the typedefs block.

86 69 CppConstructors You can define any number of C++ constructors for your autotype, which can override the default constructors that are otherwise autodefined. The syntax for these is as it would be for standard constructors in C++. Methods You can define any number of C++ methods as member functions of the auto-type. These use the normal syntax for defining methods in C++, but may contain an optional MethodOptions block between the method signature and the method body, which may contain a trace option that changes the trace level for this function only. These methods cannot directly access variables or methods of the service itself, though the auto-type is declared as a friend of the service instance. The auto-type may contain a pointer to the service instance, which can be passed in by the constructor, and this pointer can be used to access variables and methods of the service. FieldOptions Each field of the auto-type can be given a variety of options that control how they are generated within auto-types. These options overlap substantially with other fields in the specification, and so are described in detail in Serialize The serialize option with sub-option no (serialize(no)), can be specified for the auto-type, which causes it not to have the serializable methods generated for it. Comparable Auto-type objects are not, by default, comparable meaning there are no equality or comparison operators defined automatically. The comparable option can be specified, and admits three sub-options: equals, lessthan, and hashof. The value provided for each should be default, other options are not presently supported. If equals is supplied, then an operator== method will be defined. If lessthan is supplied, then an operator< method will be defined, and equals will be implied. Finally, hashof will cause a hash template to be generated for use in hash maps. A full specification would be comparable(equals=default; lessthan=default; hashof=default). The

87 70 generated methods will consider the fields in order, omitting those fields that are marked notcomparable. Private Giving the private option with the sub-value yes has the effect of making the fields of the auto-type private, instead of the public default, while leaving the auto-type methods as public. This is to allow normal C++-style protection for parameters. Node The node attribute tells the Mace compiler this auto-type represents a node. This causes it to add several auto-generated features for the type. First, an id field is generated for the object, which is hidden and protected. The value can be retrieved using getid(), which will return a copy of the id. The id field will be the first parameter to any constructor, and must be set at the time of construction. The purpose of setting the node attribute is twofold. First, future compiler options can take advantage of types are known to represent nodes, in the same way that it takes advantage of the MaceKey and NodeSet types, but with more richness. Second, for use with the mace::nodecollection data type, which is a hybrid map and set type. Nodes in the collection can be located and referenced using only their MaceKey, as with a map, but the key is also automatically a field of the mapped type, or value type, as though it were a set. Part of this support for node collections is the addition of a getscore function, which is the default function pointer used with the greatestscore and leastscore functions of the node collection. By default, getscore simply returns 0.0, but by giving the name of a field as the score sub-option, it can return that field instead. (example node(score=delay)). Messages The messages block allows one to define messages for sending to other nodes. The syntax is described below: Messages: messages { Message(*) } ; Message: TypeName TypeOpts { Fields(*) } ; (?) Fields: Type VarName TypeOpts(?) ; Example:

88 71 messages { JoinRequest { MaceKey source; } JoinReply attribute((number(6))) { NodeSet peers; } } This example contains two messages, one named JoinRequest, and the other Join- Reply. Each message has a single field. The JoinReply message will be allocated message number 6 while JoinRequest will get its number by sequential assignment. The only valid type option for message fields is the dump option, which can be used to restrict how a message field is printed in its output methods. The suboptions and values for dump are described in Serialization is automatically generated for each message, and cannot be modified. Methods similarly cannot be added to messages. To support distinguishing the messages during deserialization, each message of a service is assigned a sequential number in the order listed, from an int8 t numerical space. To support sharing messages across services, the number type option may be set for the message type, with a sub-value of the number to set it to. Subsequent messages will be one greater than this message. (Example: number(5)). The serialized form, then, will contain the number at the beginning of the bytestream, and is automatically handled for dispatch and deserialization by deserialization code. Messages are highly optimized for minimizing copies of data, and therefore cannot be stored past their stack-lifespan. The message itself is a set of const references, which either point to the variables passed in by constructor, or to the variables of a separate struct generated just for the message. The former is used for serializing a message, while the latter for de-serializing a message. Messages therefore should not be passed by copy or stored, as these references will become invalid and cause memory problems. In some cases, it is desirable to save a message, so as to preserve its contents for future delivery. Presently, there are two ways to do this. First, you can create an auto-type with the same fields, and either send the auto-type in the message (and

89 72 store the auto-type), or write a constructor for the auto-type that takes the message and initializes all the fields. The second way to do this is to serialize the message into a string, so it may be deserialized later. If the goal will be to re-deliver the message, the latter option may be preferred, as the string can be delivered directly by calling a service s own deliver method, passing in the string. In addition to serialization and logging code, messages are handled specially by the Mace compiler, as will be described in In general, however, it is to leverage the fact that the Mace compiler knows, for each service, the complete set of messages it may send and receive, and so generates appropriate code to handle multiplexing and demultiplexing of the messages efficiently rather than trying to deserialize an arbitrary object, as happens in other languages like Java Persistent State Few distributed systems are stateless. Thus, the Mace specification language has to provide mechanisms for storing the state of each node in the system. I have identified four types of persistent state a service maintains (1) compile-time constants, which support compile-time optimization of the service based on the constant value, (2) run-time constants, which are set during the service initialization by its constructor s parameters, (3) high-level states, which are used to break-up the implementation into phases, and (4) detailed state variables, which let the service maintain specific information in addition to its current phase of operation. Constants Syntax: Constants: constants { CppInclude(*) Variable(*) } Variable: Type Varname = Value ; The constants block contains two portions. First, a set of C++ header files that may be needed to define types for constants. To make compilation more efficient, there is a file ServiceName-constants.h generated for defining the constants, and this file only includes MaceBasics.h. Thus, if the type is not defined there, it will need to be #include-d inside the constants block. Two cases where this is necessary

90 73 are constant std::string variables and constant mace::macetime variables. The syntax of a CppInclude are the same as in standard C++. The variables themselves are defined as other C++ variables are defined. Each must have a value provided, which will be the compile-time constant used for the variable. Constructor Parameters Both constants and constructor parameters are constants at runtime. The distinguishing feature of these is that constructor parameters may be set at runtime rather than compile time, limiting optimizations the compilers may make. Any parameter listed here will be placed, in order, in the initialization function of a service so it may be over-ridden at instantiation time. Syntax: Constants: constructor_parameters { Variable(*) } Variable: Type Varname = Value ; Unlike the constants block, there is no need for C++ includes in this block, as it will enjoy the set of includes the rest of the service code does as well, including any files included from the constants block. Each variable in this block must be given a default value, to support a parameter-less instantiation of the service. In actuality, two initialization functions will be generated for services that use both lower level services and which have constructor parameters. The first will have the list of services first, and the second will have the list of constructor parameters first. This supports default initialization of either set while only passing in the other. In fact, there will only be one constructor generated, and it will contain no default parameters. This was done for simplicity in code generation and shortness of compilation all the default value mapping happens in the initialization functions, which are defined in ServiceName-init.cc and declared in ServiceName-init.h. By doing this, no service implementation file depends directly on another service, so when modifying one service, only the initialization functions of dependent services need be recompiled.

91 74 States and State Variables The remaining state is the mutable persistent state. It comes in two types high level states, and lower level state variables. To understand the distinction, consider a TCP implementation. The TCP implementation defines a state-transition diagram, which includes the states CLOSED, LISTEN, SYN-RCVD, SYN-SENT, ES- TAB, FIN-WAIT-1, FIN-WAIT-2, CLOSING, CLOSE-WAIT, TIME-WAIT, LAST- ACK, and CLOSED. These determine the phase of the transport protocol, and determine how to handle incoming packets at a high-level. But nodes also maintain some specific information, such as the congestion window, and the local and remote sequence numbers, which are not encoded in the state-transition diagram. This dichotomy captures exactly the design difference between states and state variables. Lesson 6: While the high-level state could be captured as an enumeration in a normal state variable, it is more natural to use the states block to define these high-level states. Doing so also exposes to the Mace compiler and tools that these state transitions are more significant to the operation of the service, and this information can be used to better visualize the execution or search the state space in model checking tools. Lesson 7: Some services, because they provide logical service for multiple purposes, would be better served by a high-level state that is not only service specific, but also specific to a group or registration id. These services currently tend not to use the high-level state variable, to the detriment of automated understanding. We are currently considering new language features to preserve this dichotomy, even for these kinds of services. States Syntax: States: states { StateName ; (StateName ; )(*) } In addition to the states listed as StateName(s), two states are automatically added to every service: init and exited. The service will begin in the init state. When the service is exiting, though calls to the maceexit event handler, the service will automatically transition to the exited state. Once in the exited state, no transitions

92 75 may occur except maceexit, so a service should only transition itself to the exited state if it wishes to prevent all future transitions. Usually this is not done by the programmer, as it is handled automatically on the call to maceexit. In each service, the state of the service can be checked and modified through the state variable. An enumeration is created from the set of states, so the state variable may be compared to the names of the states, or assigned a new value. To capture the lower-level states of the service, these variables are defined as part of the StateVariables block. StateVariables Syntax: StateVariables: state_variables { Variable(*) } Variable: Type VarName TypeOpts(?) ( = InitialValue )(?) ; TypeOpts: attribute(( TypeOpt (, TypeOpt)(*) )) TypeOpt: AttributeName ( SubTypeOpts(?) ) SubTypeOpts: SubTypeOpt ( ; SubTypeOpt)(*) ; (?) SubTypeOpt: (Value Key = Value) These variables generally share the standard C++ syntax for declaring variables, and are generated as private member variables of the service. They may only be accessed or referenced from code within the service specification such as transitions, routines, or as discussed in auto-type methods and may not be externally influenced. This supports encapsulation and checking because only by calling a transition may the state of the service be affected. These variables can also be affected by a variety of attribute field options, which are described in detail in The Mace compiler does not enforce, but it is strongly urged, that no pointer or reference variables are placed in a state variables block, because it may violate the encapsulation that is assumed by MaceMC and other Mace tools. State variables may be set to an initial value, or will be initialized to Type() otherwise. Variables can be of any standard C++ type, a type named in a typedefs block, or an auto-type. They should not, as described earlier, be a message type. One state variable type in particular deserves special mention, because it is treated specially by the Mace compiler. This is the timer type. In its basic form, a user can define a timer as such: timer printer;

93 76 This would create a new timer, with the name printer. Timers are used in Mace to schedule future callbacks as a timed event rather than in response to an external stimulus. There are two main kinds of timers: plain and multi-timers. Plain timers can be recursive or not, depending on the type options. Recursive timers are simply plain timers that automatically reschedule themselves as they expire. The service must schedule them the first time to start the sequence, and it will fire until cancelled. Multi-timers cannot be recursive, and have the property that they may be scheduled multiple-times simultaneously. Timers may also contain a set of variables that are passed into the schedule methods, and returned during the expire transition. Because of the various specializations, each timer is generated individually. Example timer definitions are: timer plain; timer<int, float> plainwithvariables; timer recursive5sec attribute((recur( ))); timer multi attribute((multi(yes))); timer<int, float> multiwithvariables attribute((multi(yes))); Timers support these methods: schedule() The schedule method takes a uint64 t parameter that is the number of microseconds until the timer should expire. Additionally, there is a parameter for each value to be stored with the timer, which the timer will make a copy of for safekeeping. In a multi-timer, the schedule method will return the exact timestamp the timer is scheduled to expire, which is used as a key for other methods. In a plain timer, schedule will assert that the timer is not scheduled. reschedule() This call is only generated for plain timers. It differentiates itself from schedule only by the fact that it will cancel this timer if already scheduled before scheduling it as requested. isscheduled() This call with no parameters returns boolean whether this timer is presently scheduled. For multi-timers, you can pass in a specific timestamp, which you get from the return value of a call to schedule, and the timer will tell you if there is a timer scheduled with that timestamp. Timers with variables will also implement an isscheduled method that takes the variable list, and will return true if there is a scheduled timer with that set of variables.

94 77 nextscheduled() This call, for all timers, returns the first time this timer is scheduled to expire. cancel() The cancel method can be used to cancel timer expirations. The form without parameters, for both plain and multi-timers, cancels all outstanding expirations of the timer. For multi-timers, there will be two additional forms. One takes a timestamp, and cancels only that specific expiration. The other takes the set of timer variables, and cancels all matching timer expirations. The transitions that occur when a timer expires are discussed in Execution There are three places in the service where designers can place executable code that executes directly in the scope of the service itself. These are transitions, which are event handlers, routines, which are private methods of the service, and detect transitions and triggers, which are normal transitions, but separated for convenience. It should be stressed that nowhere in Mace execution code within a service context should the thread be allowed to block, as this would prevent other threads from executing their events in the current implementation. Transitions Transitions are the key of all Mace services it is through transitions that all useful work of the service occurs. There are four kinds of transitions: upcalls, downcalls, scheduler, and aspect transitions. Upcalls are transitions that come from lower layers, namely the registered handlers of services in the services block. Downcalls are transitions that come from higher layers: interfaces in the provides statement of this service. Scheduler transitions are expiration transitions of a timer variable, and aspect transitions are those that occur atomically at the end of a transition in which a monitored state variable is modified. Syntax: Transitions: transitions { GuardedTransitionSet } GuardedTransitionSet: guard Guard { GuardedTransitionSet } Transition(*)

95 78 Guard: ( (const boolean expression) ) Transition: TransitionType Guard(?) ReturnType(?) TransitionName ParameterList MethodOptions { CppMethodBody } TransitionType: upcall downcall schedule AspectType AspectType: aspect < Variable (, Variable)(*) > TransitionName: (function name from the valid interface methods) ParameterList: (Type)(?) VarName (, (Type)(?) VarName)(*) In the execution of a service, event processing happens by threads acquiring the privilege to execute a given event, then calling that event handler. Because different threads are competing to fire events, the order of events is unpredictable, and fair-sharing across threads is handled by operating system primitives. With each transition is associated a stack of guard boolean expressions, which defaults to a single true if empty. This is the stack parsed from the nested GuardTransitionSet blocks into the specific transition Guard. For example, consider this block: transitions { downcall maceinit() { /* event body */ } guard (state == waiting) { upcall (src == me) deliver(src, dest, const JoinRequest& r) { //error? } } } It shows a maceinit event, whose guard stack defaults to < true >, and a deliver event for a JoinRequest message. The guard stack for the deliver event is < (state == waiting), (src == me) >. When a thread receives permission to execute its event, it will find the first matching and enabled transition, and execute that transition. An enabled transition is one whose guard stack returns true. In the case of the deliver event, the guards stack would be true if the state is the waiting state and the source is the local node. If no matching and enabled transition is found, the default behavior from the interface description will be used instead. Lesson 8: There is functionally no difference between defining multiple transitions with different guards, and having a single transition with an if /else if /.... The latter can even be more efficient due to limitations of the current implementation. Despite this, the guards are recommended, because they allow the user

96 79 to group transitions by something other than transition type, the compiler to better understand how the system is structured, and the analysis tools to make better use of the guard information. There are a few caveats and exceptions to the default behavior, which have to do with the transition options described in Basically, transitions may be declared as pre or post transitions, and may be marked as only executing once. Marking a transition as execute-once is equivalent to adding a guard that checks if the execution has happened before, and return false if so. Otherwise, all pre transitions will be considered for execution, based on their guard and will execute in the other they appear in the specification. Then, after all pre transitions execute, the first matching normal transition will execute, followed by all matching and enabled post transitions, in the order they appear. Finally, aspects and deferred actions will be executed. Since these can in turn cause other things to execute by calling other methods, their interaction is not well understood, so care must be taken when combining all of these features. Transition code is written in C++, and can make calls to other services (using upcall and downcall functions), to routines, or asynchronous, non-blocking libraries. Transitions can also defer certain routines, downcalls, and upcalls until the end of events, by prefixing those calls with defer. To defer an upcall deliver, for example, the programmer would write defer upcall deliver. Note that no return value may be collected from those calls, as they are not immediately executed. The specification of transition signatures allows most types to be omitted. The return-type may always be omitted, and will be taken from the interface definition. Also, the types of the parameters may be omitted, as long as they are not needed to distinguish which interface it should match. The parameters of a transition must match exactly the type listed in the interface definition. However, there are a few exceptions. First, the registration uid parameter common to upcalls and downcalls may be omitted, though it should not be omitted if it is a downcall and the variable needs to be saved or used for an eventual related upcall. Second, the parameters of a scheduler transition are taken from the template parameters of the timer type, except that they are passed to the scheduler

97 80 transition as reference parameters. Finally, the parameters of an aspect method are constant reference parameters to variables of the same type as the template variables of the aspect. An aspect transition is declared as an aspect that monitors the changes of a set of state variables. The compiler checks this list to make sure they are all state variables, and bases the parameters of the transition on the types of the state variables. An aspect will detect when a state variable changes, and if any of the flagged state variables change, the guard will be checked to see if it is enabled. If so, the prior value will be passed in as a parameter to the aspect transition, by constant reference. Routines Syntax: Routines: routines { (Routine RoutineObject)(*) } Routine: Method RoutineObject: ( class struct ) TypeName { CppBody } ; Routines are nothing more than private member functions of a service. Routines may not be called from outside the service, thus restricting entry points into the service to transitions. Routines are defined using normal C++ syntax for methods, though they may contain MethodOptions between the signature and the method body. As a special case, the routines block may also contain a class or struct, who s implementation will be copied verbatim from the routines block to the service. This use is deprecated, but remains to support defining a functor for use as a secondary sort function on objects that have a different primary sort. Detect The detect block is a shortcut for a common setup in distributed systems. The basic idea is that for a node or group of nodes, often a protocol wants to exchange periodic information, and will declare the remote peer failed if there is no response in an appropriate amount of time. This is often implemented as a set of timers, keeping

98 81 track of the last time a node was heard from, and messages to exchange information, resetting the time when an appropriate response from a peer is seen. This block was developed in Mace as an experiment to clean the code related to this. Syntax: Detect: detect { DetectSpec(*) } DetectSpec: Id { DetectBody } DetectBody: DWho DTimerPeriod(?) DWait(?) DInterval(?) DTimeout(?) DWTrigger(?) DITrigger(?) DTTrigger(?) SuppressionTransitions(?) DWho: ( node nodes ) = state-variable ; DTimerPeriod: timer_period = Expression ; DWait: wait = Expression ; DInterval: interval = Expression ; DTimeout: timeout = Expression ; DWTrigger: wait_trigger ( varname ) { MethodBody } DITrigger: interval_trigger ( varname ) { MethodBody } DTTrigger: timeout_trigger ( varname ) { MethodBody } SuppressionTransitions: suppression_transitions { GuardedTransitionSet } In the detect block, multiple detect specifications (DetectSpec) can be defined. For each specification, there is a name name for the specification, and either a node or set of nodes must be defined. Given the name of the specification, a method called reset name is generated, which takes a MaceKey parameter. Whenever the method is called, that node is marked as being having responded, and will reset the node clock. The node set of nodes listed in the syntax refers to a state variable, and for each node listed, a virtual clock is started. A timer is generated for the detect specification, which expires at the interval specified by timer period. Whenever the timer expires, each node s virtual clock is consulted, and timers fired as appropriate. The triggers specified are executed according to the chart in Figure 4.1. First, a wait period occurs before any trigger. Then, the wait trigger is executed, followed by the first interval trigger. Then, every time the interval passes, the interval trigger is executed again. Finally, if timeout time elapses on the virtual clock from its start, the timeout trigger is executed. As a convenience, transitions may be defined as part of the detect specification. These transitions are equivalent to transitions in the transitions block, and will be

99 82 Figure 4.1: This figure shows how timeout periods and triggers interact in detect specifications in Mace. Time is represented by the arrow going to the right. Arrows pointing up indicate triggers executing. included as though implemented there, though they are automatically marked as pre transitions, to prevent conflicting with transitions in the rest of the specification. The idea is that as a convenience, suppression can often be express in separate pretransitions, to keep these concerns separate. Many of the detect declarations are optional and have default values. These are currently set as follows: timer period. By default, the timer will expire every 1 second, measured in microseconds, so the specific value is wait period. The default wait period is 0, meaning the intervals begin immediately. interval period. The interval period defaults to the setting of the timer period. timeout period. There is no timeout by default intervals continue until the peer clock is reset. Triggers can also be left undefined, causing no action to occur when the timeouts are passed Debugging There are two parts of a Mace specification that are focused on debugging: the definition of properties and structured logging. Properties are conditions for verifying the correctness of a service, and structured logging is the idea of logging by calling

TECHNICAL NOTE VMware Infrastructure 3 SAN Conceptual and Design Basics VMware ESX Server can be used in conjunction with a SAN (storage area network), a specialized high speed network that connects computer

New Generation of Software Development Terry Hon University of British Columbia 201-2366 Main Mall Vancouver B.C. V6T 1Z4 tyehon@cs.ubc.ca ABSTRACT In this paper, I present a picture of what software development

Trace Driven Analysis of the Long Term Evolution of Gnutella Peer-to-Peer Traffic William Acosta and Surendar Chandra University of Notre Dame, Notre Dame IN, 46556, USA {wacosta,surendar}@cse.nd.edu Abstract.

What Is Specific in Load Testing? Testing of multi-user applications under realistic and stress loads is really the only way to ensure appropriate performance and reliability in production. Load testing

Service Virtualization: Reduce the time and cost to develop and test modern, composite applications Business white paper Table of contents Why you need service virtualization 3 The challenges of composite

Spreadsheet Programming: The New Paradigm in Rapid Application Development Contact: Info@KnowledgeDynamics.com www.knowledgedynamics.com Spreadsheet Programming: The New Paradigm in Rapid Application Development

A New vision for network architecture David Clark M.I.T. Laboratory for Computer Science September, 2002 V3.0 Abstract This is a proposal for a long-term program in network research, consistent with the

Copyright www.agileload.com 1 INTRODUCTION Performance testing is a complex activity where dozens of factors contribute to its success and effective usage of all those factors is necessary to get the accurate

Load Balancing in Distributed Data Base and Distributed Computing System Lovely Arya Research Scholar Dravidian University KUPPAM, ANDHRA PRADESH Abstract With a distributed system, data can be located

Principles and characteristics of distributed systems and environments Definition of a distributed system Distributed system is a collection of independent computers that appears to its users as a single

THE WINDOWS AZURE PROGRAMMING MODEL DAVID CHAPPELL OCTOBER 2010 SPONSORED BY MICROSOFT CORPORATION CONTENTS Why Create a New Programming Model?... 3 The Three Rules of the Windows Azure Programming Model...

Introduction Computer Network. Interconnected collection of autonomous computers that are able to exchange information No master/slave relationship between the computers in the network Data Communications.

Facility Usage Scenarios GDD-06-41 GENI: Global Environment for Network Innovations December 22, 2006 Status: Draft (Version 0.1) Note to the reader: this document is a work in progress and continues to

Tools Page 1 of 13 ON PROGRAM TRANSLATION A priori, we have two translation mechanisms available: Interpretation Compilation On interpretation: Statements are translated one at a time and executed immediately.

Achieving Nanosecond Latency Between Applications with IPC Shared Memory Messaging In some markets and scenarios where competitive advantage is all about speed, speed is measured in micro- and even nano-seconds.

Monitoring Traffic manager eg Enterprise v6 Restricted Rights Legend The information contained in this document is confidential and subject to change without notice. No part of this document may be reproduced

An Integrated CyberSecurity Approach for HEP Grids Workshop Report http://hpcrd.lbl.gov/hepcybersecurity/ 1. Introduction The CMS and ATLAS experiments at the Large Hadron Collider (LHC) being built at

For personal use only, not for distribution. 333 16.1 MAPREDUCE Initially designed by the Google labs and used internally by Google, the MAPREDUCE distributed programming model is now promoted by several

Chapter 1 1.1Reasons for Studying Concepts of Programming Languages a) Increased ability to express ideas. It is widely believed that the depth at which we think is influenced by the expressive power of

Software testing cmsc435-1 Objectives To discuss the distinctions between validation testing and defect testing To describe the principles of system and component testing To describe strategies for generating

An Easier Way for Cross-Platform Data Acquisition Application Development For industrial automation and measurement system developers, software technology continues making rapid progress. Software engineers

Spot server problems before they are noticed The system s really slow today! How often have you heard that? Finding the solution isn t so easy. The obvious questions to ask are why is it running slowly

Rapid Bottleneck Identification A Better Way to do Load Testing An Oracle White Paper June 2009 Rapid Bottleneck Identification A Better Way to do Load Testing. RBI combines a comprehensive understanding

Automatic Service Migration in WebLogic Server An Oracle White Paper July 2008 NOTE: The following is intended to outline our general product direction. It is intended for information purposes only, and

The Association of System Performance Professionals The Computer Measurement Group, commonly called CMG, is a not for profit, worldwide organization of data processing professionals committed to the measurement

1. Comments on reviews a. Need to avoid just summarizing web page asks you for: i. A one or two sentence summary of the paper ii. A description of the problem they were trying to solve iii. A summary of

Designing a Cloud Storage System End to End Cloud Storage When designing a cloud storage system, there is value in decoupling the system s archival capacity (its ability to persistently store large volumes

The EMSX Platform A Modular, Scalable, Efficient, Adaptable Platform to Manage Multi-technology Networks A White Paper November 2002 Abstract: The EMSX Platform is a set of components that together provide

High Availability Essentials Introduction Ascent Capture s High Availability Support feature consists of a number of independent components that, when deployed in a highly available computer system, result

Welcome to this introduction to application performance testing and the LoadRunner load testing solution. This document provides a short overview of LoadRunner s features, and includes the following sections:

White Paper The Ten Features Your Web Application Monitoring Software Must Have Executive Summary It s hard to find an important business application that doesn t have a web-based version available and

Application Note 122 Improving Test Performance through Instrument Driver State Management Instrument Drivers John Pasquarette With the popularity of test programming tools such as LabVIEW and LabWindows

An Oracle White Paper February 2010 Rapid Bottleneck Identification - A Better Way to do Load Testing Introduction You re ready to launch a critical Web application. Ensuring good application performance

This book is provided FREE with test registration by the Graduate Record Examinations Board. Graduate Record Examinations This practice book contains one actual full-length GRE Computer Science Test test-taking

152 APPENDIX 1 USER LEVEL IMPLEMENTATION OF PPATPAN IN LINUX SYSTEM A1.1 INTRODUCTION PPATPAN is implemented in a test bed with five Linux system arranged in a multihop topology. The system is implemented

Software Engineering Architectural Design 1 Software architecture The design process for identifying the sub-systems making up a system and the framework for sub-system control and communication is architectural

FINANCIAL SERVICES OVERVIEW Nine Use Cases for Endace Systems in a Modern Trading Environment Introduction High-frequency trading (HFT) accounts for as much as 75% of equity trades in the US. As capital

High Availability and Disaster Recovery Solutions for Perforce This paper provides strategies for achieving high Perforce server availability and minimizing data loss in the event of a disaster. Perforce