mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-11 11:02:05 +02:00
fresh build
This commit is contained in:
parent
c7c4bff5d6
commit
aa7276828e
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
164
docs/API.en.html
164
docs/API.en.html
@ -624,7 +624,7 @@ ul.references li p a.back-anchor {
|
||||
<p>What differs between a Roman aqueduct and a good API is that in the case of APIs, the contract is presumed to be <em>programmable</em>. To connect the two areas, <em>writing some code</em> is needed. The goal of this book is to help you design APIs that serve their purposes as solidly as a Roman aqueduct does.</p>
|
||||
<p>An aqueduct also illustrates another problem with the API design: your customers are engineers themselves. You are not supplying water to end-users. Suppliers are plugging their pipes into your engineering structure, building their own structures upon it. On the one hand, you may provide access to water to many more people through them, not spending your time plugging each individual house into your network. On the other hand, you can't control the quality of suppliers' solutions, and you are to blame every time there is a water problem caused by their incompetence.</p>
|
||||
<p>That's why designing an API implies a larger area of responsibility. <strong>An API is a multiplier to both your opportunities and your mistakes</strong>.</p><div class="page-break"></div><h3><a href="#intro-api-solutions-overview" class="anchor" id="intro-api-solutions-overview">Chapter 3. Overview of Existing API Development Solutions</a><a href="#chapter-3" class="secondary-anchor" id="chapter-3"> </a></h3>
|
||||
<p>In the first three sections of this book, we aim to discuss API design in general, not bound to any specific technology. The concepts we describe are equally applicable to web services and, let's say, operating systems (OS) APIs.</p>
|
||||
<p>In the first three sections of this book, we aim to discuss API design in general, not bound to any specific technology. The concepts we describe are equally applicable to, let's say, web services and operating system (OS) APIs.</p>
|
||||
<p>Still, two main scenarios dominate the stage when we talk about API development:</p>
|
||||
<ul>
|
||||
<li>developing client-server applications</li>
|
||||
@ -632,7 +632,7 @@ ul.references li p a.back-anchor {
|
||||
</ul>
|
||||
<p>In the first case, we almost universally talk about APIs working atop the HTTP protocol. Today, the only notable examples of non-HTTP-based client-server interaction protocols are WebSocket (though it might, and frequently does, work in conjunction with HTTP) and highly specialized APIs like media streaming and broadcasting formats.</p>
|
||||
<h4>HTTP API</h4>
|
||||
<p>Though the technology looks homogenous because of using the same application-level protocol, in reality, there is significant diversity regarding different approaches to realizing HTTP-based APIs.</p>
|
||||
<p>Although the technology looks homogeneous because of using the same application-level protocol, in reality, there is significant diversity regarding different approaches to realizing HTTP-based APIs.</p>
|
||||
<p><strong>First</strong>, implementations differ in terms of utilizing HTTP capabilities:</p>
|
||||
<ul>
|
||||
<li>either the client-server interaction heavily relies on the features described in the HTTP standard (or rather standards, as the functionality is split across several different RFCs),</li>
|
||||
@ -641,14 +641,14 @@ ul.references li p a.back-anchor {
|
||||
<p>The APIs that belong to the first category are usually denoted as “REST” or “RESTful” APIs. The second category comprises different RPC formats and some service protocols, for example, SSH.</p>
|
||||
<p><strong>Second</strong>, different HTTP APIs rely on different data formats:</p>
|
||||
<ul>
|
||||
<li>REST APIs and some RPCs (JSON-RPC, GraphQL, etc.) use the JSON format (sometimes with some additional endpoints to transfer binary data);</li>
|
||||
<li>GRPC and some specialized RPC protocols like Apache Avro utilize binary formats (such as Protocol Buffers, FlatBuffers, or Apache Avro's own format);</li>
|
||||
<li>REST APIs and some RPCs (JSON-RPC, GraphQL, etc.) use the JSON format (sometimes with some additional endpoints to transfer binary data)</li>
|
||||
<li>GRPC and some specialized RPC protocols like Apache Avro utilize binary formats (such as Protocol Buffers, FlatBuffers, or Apache Avro's own format)</li>
|
||||
<li>finally, some RPC protocols (notably SOAP and XML-RPC) employ the XML data format (which is considered a rather outdated practice by many developers).</li>
|
||||
</ul>
|
||||
<p>All the above-mentioned technologies are operating in significantly dissimilar paradigms — which arise rather hot “holy war” debates among software engineers — though at the moment this book is being written we observe the choice for general-purpose APIs is reduced to the “REST API (in fact, JSON-over-HTTP) vs. GRPC vs. GraphQL” triad.</p>
|
||||
<p>All the above-mentioned technologies operate in significantly dissimilar paradigms, which give rise to rather hot “holy war” debates among software engineers. However, at the moment this book is being written we observe the choice for general-purpose APIs is reduced to the “REST API (in fact, JSON-over-HTTP) vs. GRPC vs. GraphQL” triad.</p>
|
||||
<h4>SDKs</h4>
|
||||
<p>The term “SDK” is not, strictly speaking, related to APIs: this is a generic term for a software toolkit. As with “REST,” however, it got some popular reading as a client framework to work with some underlying API. This might be, for example, a wrapper to a client-server API, or a UI to some OS API. The major difference from the APIs we discussed in the previous paragraph is that an “SDK” is implemented for a specific programming language and platform, and its purpose is translating the abstract language-agnostic set methods (comprising a client-server or an OS API) into concrete structures specific for the programming language and the platform.</p>
|
||||
<p>Unlike client-server APIs, such SDKs can hardly be generalized as each of them is developed for a specific language-platform pair. There are some interoperable SDKs, notable cross-platform mobile (React Native, Flutter, Xamarin) and desktop (JavaFX, QT) frameworks and some highly-specialized solutions (Unity).</p>
|
||||
<p>The term “SDK” is not, strictly speaking, related to APIs: this is a generic term for a software toolkit. As with “REST,” however, it got some popular reading as a client framework to work with some underlying API. This might be, for example, a wrapper to a client-server API or a UI to some OS API. The major difference from the APIs we discussed in the previous paragraph is that an “SDK” is implemented for a specific programming language and platform, and its purpose is translating the abstract language-agnostic set methods (comprising a client-server or an OS API) into concrete structures specific for the programming language and the platform.</p>
|
||||
<p>Unlike client-server APIs, such SDKs can hardly be generalized as each of them is developed for a specific language-platform pair. There are some interoperable SDKs, notably cross-platform mobile (React Native, Flutter, Xamarin) and desktop (JavaFX, QT) frameworks and some highly-specialized solutions (Unity).</p>
|
||||
<p>Still, SDKs feature some generality in terms of <em>the problems they solve</em>, and Section V of this book will be dedicated to solving these problems of translating contexts and making UI components.</p><div class="page-break"></div><h3><a href="#intro-api-quality" class="anchor" id="intro-api-quality">Chapter 4. API Quality Criteria</a><a href="#chapter-4" class="secondary-anchor" id="chapter-4"> </a></h3>
|
||||
<p>Before we start laying out the recommendations, we ought to specify what API we consider “fine,” and what the benefits of having a “fine” API are.</p>
|
||||
<p>Let's discuss the second question first. Obviously, API “finesse” is primarily defined through its capability to solve developers' and users' problems. (One could reasonably argue that solving problems might not be the main purpose of offering an API to developers. However, manipulating public opinion is not of interest to the author of this book. Here we assume that APIs exist primarily to help people, not for some other covertly declared purposes.)</p>
|
||||
@ -659,7 +659,7 @@ ul.references li p a.back-anchor {
|
||||
<li>ideally, developers should be able to understand at first glance, what entities are meant to solve their problem</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>the API must be readable;
|
||||
<li>the API must be readable
|
||||
<ul>
|
||||
<li>ideally, developers should write correct code after just looking at the methods' nomenclature, never bothering about details (especially API implementation details!)</li>
|
||||
<li>it is also essential to mention that not only should the problem solution (the “happy path”) be obvious, but also possible errors and exceptions (the “unhappy path”)</li>
|
||||
@ -674,45 +674,45 @@ ul.references li p a.back-anchor {
|
||||
<p>However, the static convenience and clarity of APIs are simple parts. After all, nobody seeks to make an API deliberately irrational and unreadable. When we develop an API, we always start with clear basic concepts. Providing you have some experience in APIs, it's quite hard to make an API core that fails to meet obviousness, readability, and consistency criteria.</p>
|
||||
<p>Problems begin when we start to expand our API. Adding new functionality sooner or later results in transforming once plain and simple API into a mess of conflicting concepts, and our efforts to maintain backward compatibility will lead to illogical, unobvious, and simply bad design solutions. It is partly related to an inability to predict the future in detail: your understanding of “fine” APIs will change over time, both in objective terms (what problems the API is to solve, and what is best practice) and in subjective terms too (what obviousness, readability, and consistency <em>really mean</em> to your API design).</p>
|
||||
<p>The principles we are explaining below are specifically oriented towards making APIs evolve smoothly over time, without being turned into a pile of mixed inconsistent interfaces. It is crucial to understand that this approach isn't free: the necessity to bear in mind all possible extension variants and to preserve essential growth points means interface redundancy and possibly excessive abstractions being embedded in the API design. Besides, both make the developers' jobs harder. <strong>Providing excess design complexities being reserved for future use makes sense only if this future actually exists for your API. Otherwise, it's simply overengineering.</strong></p><div class="page-break"></div><h3><a href="#intro-api-first-approach" class="anchor" id="intro-api-first-approach">Chapter 5. The API-first approach</a><a href="#chapter-5" class="secondary-anchor" id="chapter-5"> </a></h3>
|
||||
<p>Today, more and more IT companies accept the importance of the “API-first” approach, i.e., the paradigm of developing software with a heavy focus on developing APIs.</p>
|
||||
<p>However, we must differentiate the product concept of the API-first approach from a technical one.</p>
|
||||
<p>Today, more and more IT companies are recognizing the importance of the “API-first” approach, which is the paradigm of developing software with a heavy focus on APIs.</p>
|
||||
<p>However, we must differentiate between the product concept of the API-first approach and the technical one.</p>
|
||||
<p>The former means that the first (and sometimes the only) step in developing a service is creating an API for it, and we will discuss it in “The API Product” section of this book.</p>
|
||||
<p>If we, however, talk about the API-first approach in a technical sense, we mean the following: <strong>the contract, i.e. the obligation to connect two programmable contexts, precedes the implementation and defines it</strong>. More specifically, two rules are to be respected:</p>
|
||||
<p>If we talk about the API-first approach in a technical sense, we mean the following: <strong>the contract, i.e. the obligation to connect two programmable contexts, precedes the implementation and defines it</strong>. More specifically, two rules must be respected:</p>
|
||||
<ul>
|
||||
<li>the contract is developed and committed in a form of a specification before the functionality is implemented;</li>
|
||||
<li>if it turns out that the implementation and the contract differ, it is the implementation to be fixed, not the contract.</li>
|
||||
<li>the contract is developed and committed to in the form of a specification before the functionality is implemented</li>
|
||||
<li>if it turns out that the implementation and the contract differ, the implementation is to be fixed, not the contract.</li>
|
||||
</ul>
|
||||
<p>The “specification” in this context is a formal machine-readable description of the contract in one of the interface definition languages (IDL) — for example, in a form of a Swagger/OpenAPI document or a <code>.proto</code> file.</p>
|
||||
<p>The “specification” in this context is a formal machine-readable description of the contract in one of the interface definition languages (IDL) — for example, in the form of a Swagger/OpenAPI document or a <code>.proto</code> file.</p>
|
||||
<p>Both rules assert that partner developers' interests are given the highest priority:</p>
|
||||
<ul>
|
||||
<li>rule #1 allows partners for writing code based on the specification without coordinating the process with the API provider;
|
||||
<li>rule #1 allows partners to write code based on the specification without coordinating the process with the API provider
|
||||
<ul>
|
||||
<li>the possibility of auto-generating code based on the specification emerges, and that might make development significantly less complex or even automate it;</li>
|
||||
<li>the code might be developed without having an access to the API;</li>
|
||||
<li>the possibility of auto-generating code based on the specification emerges, which might make development significantly less complex and error-prone or even automate it</li>
|
||||
<li>the code might be developed without having access to the API</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>rule #2 means partners won't need to change their implementations should some inconsistencies between the specification and the API functionality pop up.</li>
|
||||
<li>rule #2 means partners won't need to change their implementations should some inconsistencies between the specification and the API functionality arise.</li>
|
||||
</ul>
|
||||
<p>Therefore, for your API consumers, the API-first approach is a guarantee of a kind. However, it only works if the API was initially well-designed: if some irreparable flaws in the specification surfaced out, we would have no other option but break rule #2.</p><div class="page-break"></div><h3><a href="#intro-back-compat" class="anchor" id="intro-back-compat">Chapter 6. On Backward Compatibility</a><a href="#chapter-6" class="secondary-anchor" id="chapter-6"> </a></h3>
|
||||
<p>Backward compatibility is a <em>temporal</em> characteristic of your API. An obligation to maintain backward compatibility is the crucial point where API development differs from software development in general.</p>
|
||||
<p>Of course, backward compatibility isn't an absolute. In some subject areas shipping new backwards-incompatible API versions is a routine. Nevertheless, every time you deploy a new backwards-incompatible API version, the developers need to make some non-zero effort to adapt their code to the new API version. In this sense, releasing new API versions puts a sort of a “tax” on customers. They must spend quite real money just to make sure their product continues working.</p>
|
||||
<p>Large companies, which occupy firm market positions, could afford to charge such a tax. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.</p>
|
||||
<p>From our point of view, such a practice cannot be justified. Don't impose hidden levies on your customers. If you're able to avoid breaking backward compatibility — never break it.</p>
|
||||
<p>Of course, maintaining old API versions is a sort of a tax either. Technology changes, and you cannot foresee everything, regardless of how nice your API is initially designed. At some point keeping old API versions results in an inability to provide new functionality and support new platforms, and you will be forced to release a new version. But at least you will be able to explain to your customers why they need to make an effort.</p>
|
||||
<p>Therefore, for your API consumers, the API-first approach is a guarantee of a kind. However, it only works if the API was initially well-designed. If some irreparable flaws in the specification surface, we would have no other option but to break rule #2.</p><div class="page-break"></div><h3><a href="#intro-back-compat" class="anchor" id="intro-back-compat">Chapter 6. On Backward Compatibility</a><a href="#chapter-6" class="secondary-anchor" id="chapter-6"> </a></h3>
|
||||
<p>Backward compatibility is a <em>temporal</em> characteristic of an API. The obligation to maintain backward compatibility is the crucial point where API development differs from software development in general.</p>
|
||||
<p>Of course, backward compatibility isn't absolute. In some subject areas shipping new backward-incompatible API versions is routine. Nevertheless, every time a new backward-incompatible API version is deployed, developers need to make some non-zero effort to adapt their code to the new version. In this sense, releasing new API versions puts a sort of “tax” on customers who must spend quite real money just to ensure their product continues working.</p>
|
||||
<p>Large companies that occupy solid market positions could afford to charge such a tax. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.</p>
|
||||
<p>From our point of view, such a practice cannot be justified. Don't impose hidden levies on your customers. <strong>If you can avoid breaking backward compatibility, never break it</strong>.</p>
|
||||
<p>Of course, maintaining old API versions is a sort of tax as well. Technology changes, and you cannot foresee everything, regardless of how nicely your API is initially designed. At some point keeping old API versions results in an inability to provide new functionality and support new platforms, and you will be forced to release a new version. But at least you will be able to explain to your customers why they need to make an effort.</p>
|
||||
<p>We will discuss API lifecycle and version policies in Section II.</p><div class="page-break"></div><h3><a href="#intro-versioning" class="anchor" id="intro-versioning">Chapter 7. On Versioning</a><a href="#chapter-7" class="secondary-anchor" id="chapter-7"> </a></h3>
|
||||
<p>Here and throughout this book, we firmly stick to <a href="https://semver.org/">semver</a> principles of versioning.</p>
|
||||
<p>Here and throughout this book, we firmly adhere to <a href="https://semver.org/">semver</a> principles of versioning.</p>
|
||||
<ol>
|
||||
<li>API versions are denoted with three numbers, e.g., <code>1.2.3</code>.</li>
|
||||
<li>The first number (a major version) increases when backwards-incompatible changes in the API are introduced.</li>
|
||||
<li>The second number (a minor version) increases when new functionality is added to the API, keeping backward compatibility intact.</li>
|
||||
<li>The first number (a major version) increases when backward-incompatible changes in the API are introduced.</li>
|
||||
<li>The second number (a minor version) increases when new functionality is added to the API while keeping backward compatibility intact.</li>
|
||||
<li>The third number (a patch) increases when a new API version contains bug fixes only.</li>
|
||||
</ol>
|
||||
<p>Sentences “a major API version” and “new API version, containing backwards-incompatible changes” are therefore to be considered equivalent ones.</p>
|
||||
<p>It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., <code>1.2.3</code>) or a reduced one (<code>1.2</code> or just <code>1</code>). Some systems support more sophisticated schemes of defining the desired version (for example, <code>^1.2.3</code> reads like “get the last stable API release that is backwards-compatible to the <code>1.2.3</code> version”) or additional shortcuts (for example, <code>1.2-beta</code> to refer to the last beta release of the <code>1.2</code> API version family). In this book, we will mostly use designations like <code>v1</code> (<code>v2</code>, <code>v3</code>, etc.) to denote the latest stable release of the <code>1.x.x</code> version family of an API.</p>
|
||||
<p>The sentences “a major API version” and “a new API version, containing backward-incompatible changes” are considered equivalent.</p>
|
||||
<p>It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., <code>1.2.3</code>) or a reduced one (<code>1.2</code> or just <code>1</code>). Some systems support more sophisticated schemes for defining the desired version (for example, <code>^1.2.3</code> reads like “get the last stable API release that is backward-compatible to the <code>1.2.3</code> version”) or additional shortcuts (for example, <code>1.2-beta</code> to refer to the last beta release of the <code>1.2</code> API version family). In this book, we will mostly use designations like <code>v1</code> (<code>v2</code>, <code>v3</code>, etc.) to denote the latest stable release of the <code>1.x.x</code> version family of an API.</p>
|
||||
<p>The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the <a href="#back-compat-statement">“Backward Compatibility Problem Statement”</a> chapter.</p><div class="page-break"></div><h3><a href="#intro-terms-notation" class="anchor" id="intro-terms-notation">Chapter 8. Terms and Notation Keys</a><a href="#chapter-8" class="secondary-anchor" id="chapter-8"> </a></h3>
|
||||
<p>Software development is characterized, among other things, by the existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book, we are deliberately avoiding using terms like “method,” “object,” “function,” and so on, using the neutral term “entity” instead. “Entity” means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think is right).</p>
|
||||
<p>Software development is characterized, among other things, by the existence of many different engineering paradigms, whose adherents are sometimes quite aggressive towards other paradigms' adherents. While writing this book, we are deliberately avoiding using terms like “method,” “object,” “function,” and so on, using the neutral term “entity” instead. “Entity” means some atomic functionality unit, like a class, method, object, monad, prototype (underline what you think is right).</p>
|
||||
<p>As for an entity's components, we regretfully failed to find a proper term, so we will use the words “fields” and “methods.”</p>
|
||||
<p>Most of the examples of APIs will be provided in a form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A <code>GET /v1/orders</code> endpoint call could easily be replaced with an <code>orders.get()</code> method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.</p>
|
||||
<p>Most of the examples of APIs will be provided in the form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A <code>GET /v1/orders</code> endpoint call could easily be replaced with an <code>orders.get()</code> method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.</p>
|
||||
<p>Let's take a look at the following example:</p>
|
||||
<pre><code>// Method description
|
||||
POST /v1/bucket/{id}/some-resource⮠
|
||||
@ -742,16 +742,16 @@ Cache-Control: no-cache
|
||||
<li>a specific <code>X-Idempotency-Token</code> header is added to the request alongside standard headers (which we omit);</li>
|
||||
<li>terms in angle brackets (<code><idempotency token></code>) describe the semantics of an entity value (field, header, parameter);</li>
|
||||
<li>a specific JSON, containing a <code>some_parameter</code> field and some other unspecified fields (indicated by ellipsis) is being sent as a request body payload;</li>
|
||||
<li>in response (marked with an arrow symbol <code>→</code>) server returns a <code>404 Not Founds</code> status code; the status might be omitted (treat it like a <code>200 OK</code> if no status is provided);</li>
|
||||
<li>in response (marked with an arrow symbol <code>→</code>) the server returns a <code>404 Not Found</code> status code; the status might be omitted (treat it like a <code>200 OK</code> if no status is provided);</li>
|
||||
<li>the response could possibly contain additional notable headers;</li>
|
||||
<li>the response body is a JSON comprising two fields: <code>error_reason</code> and <code>error_message</code>; field value absence means that the field contains exactly what you expect it should contain — so there is some generic error reason value which we omitted;</li>
|
||||
<li>if some token is too long to fit a single line, we will split it into several lines adding <code>⮠</code> to indicate it continues next line.</li>
|
||||
<li>if some token is too long to fit on a single line, we will split it into several lines adding <code>⮠</code> to indicate it continues next line.</li>
|
||||
</ul>
|
||||
<p>The term “client” here stands for an application being executed on a user's device, either a native or a web one. The terms “agent” and “user agent” are synonymous to “client.”</p>
|
||||
<p>The term “client” here stands for an application being executed on a user's device, either a native or a web one. The terms “agent” and “user agent” are synonymous with “client.”</p>
|
||||
<p>Some request and response parts might be omitted if they are irrelevant to the topic being discussed.</p>
|
||||
<p>Simplified notation might be used to avoid redundancies, like <code>POST /some-resource</code> <code>{…, "some_parameter", …}</code> → <code>{ "operation_id" }</code>; request and response bodies might also be omitted.</p>
|
||||
<p>We will be using sentences like “<code>POST /v1/bucket/{id}/some-resource</code> method” (or simply “<code>bucket/some-resource</code> method,” “<code>some-resource</code>” method — if there are no other <code>some-resource</code>s in the chapter, so there is no ambiguity) to refer to such endpoint definitions.</p>
|
||||
<p>Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like one, since types are omitted. We assume such imperative structures are readable enough to skip detailed grammar explanations.</p><div class="page-break"></div><h2><a href="#section-2" class="anchor" id="section-2">Section I. The API Design</a></h2><h3><a href="#api-design-context-pyramid" class="anchor" id="api-design-context-pyramid">Chapter 9. The API Contexts Pyramid</a><a href="#chapter-9" class="secondary-anchor" id="chapter-9"> </a></h3>
|
||||
<p>We will use sentences like “<code>POST /v1/bucket/{id}/some-resource</code> method” (or simply “<code>bucket/some-resource</code> method,” “<code>some-resource</code>” method — if there are no other <code>some-resource</code>s in the chapter, so there is no ambiguity) to refer to such endpoint definitions.</p>
|
||||
<p>Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like one since types are omitted. We assume such imperative structures are readable enough to skip detailed grammar explanations.</p><div class="page-break"></div><h2><a href="#section-2" class="anchor" id="section-2">Section I. The API Design</a></h2><h3><a href="#api-design-context-pyramid" class="anchor" id="api-design-context-pyramid">Chapter 9. The API Contexts Pyramid</a><a href="#chapter-9" class="secondary-anchor" id="chapter-9"> </a></h3>
|
||||
<p>The approach we use to design APIs comprises four steps:</p>
|
||||
<ul>
|
||||
<li>defining an application field</li>
|
||||
@ -2681,7 +2681,97 @@ const pendingOrders = await api.
|
||||
<p>Mathematically, the probability of getting the error is expressed quite simply. It's the ratio between two durations: the time period needed to get the actual state to the time period needed to restart the app and repeat the request. (Keep in mind that the last failed request might be automatically repeated on startup by the client.) The former depends on the technical properties of the system (for instance, on the replication latency, i.e., the lag between the master and its read-only copies) while the latter depends on what client is repeating the call.</p>
|
||||
<p>If we talk about applications for end users, the typical restart time there is measured in seconds, which normally should be much less than the overall replication latency. Therefore, client errors will only occur in case of data replication problems / network issues / server overload.</p>
|
||||
<p>If, however, we talk about server-to-server applications, the situation is totally different: if a server repeats the request after a restart (let's say because the process was killed by a supervisor), it's typically a millisecond-scale delay. And that means that the number of order creation errors will be significant.</p>
|
||||
<p>As a conclusion, returning eventually consistent data by default is only viable if an API vendor is either ready to live with background errors or capable of making the lag of getting the actual state much less than the typical app restart time.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Chapter 19. Asynchronicity and Time Management</a></h3><div class="page-break"></div><h3><a href="#chapter-20" class="anchor" id="chapter-20">Chapter 20. Lists and Accessing Them</a></h3><div class="page-break"></div><h3><a href="#chapter-21" class="anchor" id="chapter-21">Chapter 21. Bidirectional Data Flows. Push and Poll Models</a></h3><div class="page-break"></div><h3><a href="#chapter-22" class="anchor" id="chapter-22">Chapter 22. Organization of Notification Systems</a></h3><div class="page-break"></div><h3><a href="#chapter-23" class="anchor" id="chapter-23">Chapter 23. Atomicity</a></h3><div class="page-break"></div><h3><a href="#chapter-24" class="anchor" id="chapter-24">Chapter 24. Partial Updates</a></h3><div class="page-break"></div><h3><a href="#chapter-25" class="anchor" id="chapter-25">Chapter 25. Degradation and Predictability</a></h3><div class="page-break"></div><h2><a href="#section-4" class="anchor" id="section-4">Section III. The Backward Compatibility</a></h2><h3><a href="#back-compat-statement" class="anchor" id="back-compat-statement">Chapter 26. The Backward Compatibility Problem Statement</a><a href="#chapter-26" class="secondary-anchor" id="chapter-26"> </a></h3>
|
||||
<p>As a conclusion, returning eventually consistent data by default is only viable if an API vendor is either ready to live with background errors or capable of making the lag of getting the actual state much less than the typical app restart time.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Chapter 19. Asynchronicity and Time Management</a></h3>
|
||||
<p>Let's continue working with the previous example. Let's imagine that the application retrieves some system state upon start-up, perhaps not the most recent one. What else does the probability of collision depend on, and how can we lower it?</p>
|
||||
<p>We remember that this probability is equal to the ratio of time periods: getting an actual state versus starting an app and making an order. The latter is almost out of our control (unless we deliberately introduce additional waiting periods in the API initialization function, which we consider an extreme measure). Let's then talk about the former.</p>
|
||||
<p>Our usage scenario looks like this:</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrders.length == 0) {
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
// App restart happens here,
|
||||
// and all the same requests
|
||||
// are repeated
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders(); // → []
|
||||
if (pendingOrders.length == 0) {
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
</code></pre>
|
||||
<p>Therefore, we're trying to minimize the following interval: network latency to deliver the <code>createOrder</code> call plus the time of executing the <code>createOrder</code> plus the time needed to propagate the newly created order to the replicas. We don't control the first summand (but we might expect the network latencies to be more or less constant during the session duration, so the next <code>getOngoingOrders</code> call will be delayed for roughly the same time period). The third summand depends on the infrastructure of the backend. Let's talk about the second one.</p>
|
||||
<p>As we can see if the order creation itself takes a lot of time (meaning that it is comparable to the app restart time) then all our previous efforts were useless. The end user must wait until they get the server response back and might just restart the app to make a second <code>createOrder</code> call. It is in our best interest to ensure this never happens.</p>
|
||||
<p>However, what we could do to improve this timing remains unclear. Creating an order might <em>indeed</em> take a lot of time as we need to carry out necessary checks and wait for the payment gateway response and confirmation from the coffee shop.</p>
|
||||
<p>What could help us here is the asynchronous operations pattern. If our goal is to reduce the collision rate, there is no need to wait until the order is <em>actually</em> created as we need to quickly propagate the knowledge that the order is <em>accepted for creation</em>. We might employ the following technique: create <em>a task for order creation</em> and return its identifier, not the order itself.</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrders.length == 0) {
|
||||
// Instead of creating an order,
|
||||
// put the task for the creation
|
||||
const task = await api
|
||||
.putOrderCreationTask(…);
|
||||
}
|
||||
// App restart happens here,
|
||||
// and all the same requests
|
||||
// are repeated
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
// → { tasks: [task] }
|
||||
</code></pre>
|
||||
<p>Here we assume that task creation requires minimum checks and doesn't wait for any lingering operations and therefore is created much faster. Furthermore, this operation (of creating an asynchronous task) might be isolated as a separate backend service for performing abstract asynchronous tasks. Meanwhile, by having the functionality of creating tasks and retrieving the list of ongoing tasks we might significantly narrow the “gray zones” when clients can't learn the actual system state precisely.</p>
|
||||
<p>Thus we naturally came to the pattern of organizing asynchronous APIs through task queues. Here we use the term “asynchronous” logically meaning the absence of mutual <em>logical</em> locks: the party that makes a request gets a response immediately and does not wait until the requested procedure is carried out fully being able to continue to interact with the API. <em>Technically</em> in modern application environments, locking (of both the client and server) almost universally doesn't happen during long-responding calls. However, <em>logically</em> allowing users to work with the API while waiting for a response from a modifying endpoint is error-prone and leads to collisions like the one we described above.</p>
|
||||
<p>The asynchronous call pattern is useful for solving other practical tasks as well:</p>
|
||||
<ul>
|
||||
<li>caching operation results and providing links to them (implying that if the client needs to reread the operation result or share it with another client, it might use the task identifier to do so)</li>
|
||||
<li>ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter)</li>
|
||||
<li>naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the <a href="https://en.wikipedia.org/wiki/Token_bucket">“token bucket”</a> technique</li>
|
||||
<li>organizing interaction in the cases of very long-lasting operations that require more time than reasonable timeouts (which are tens of seconds in the case of network calls) or can take unpredictable time.</li>
|
||||
</ul>
|
||||
<p>Also, asynchronous communication is more robust from a future API development point of view: request handling procedures might evolve towards prolonging and extending the asynchronous execution pipelines whereas synchronous handlers must retain reasonable execution times which puts certain restrictions on possible internal architecture.</p>
|
||||
<p><strong>NB</strong>: in some APIs, an ambivalent decision is implemented where endpoints feature a double interface that might either return a result or a link to a task. Although from the API developer's point of view, this might look logical (if the request was processed “quickly”, e.g., served from cache, the result is to be returned immediately; otherwise, the asynchronous task is created), for API consumers, this solution is quite inconvenient as it forces them to maintain two execution branches in their code. Sometimes, a concept of providing a double set of endpoints (synchronous and asynchronous ones) is implemented, but this simply shifts the burden of making decisions onto partners.</p>
|
||||
<p>The popularity of the asynchronicity pattern is also driven by the fact that modern microservice architectures “under the hood” operate in asynchronous mode through event queues or pub/sub middleware. Implementing an analogous approach in external APIs is the simplest solution to the problems caused by asynchronous internal architectures (the unpredictable and sometimes very long latencies of propagating changes). Ultimately, some API vendors make all API methods asynchronous (including the read-only ones) even if there are no real reasons to do so.</p>
|
||||
<p>However, we must stress that excessive asynchronicity, though appealing to API developers, implies several quite objectionable disadvantages:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>If a single queue service is shared by all endpoints, it becomes a single point of failure for the system. If unpublished events are piling up and/or the event processing pipeline is overloaded, all the API endpoints start to suffer. Otherwise, if there is a separate queue service instance for every functional domain, the internal architecture becomes much more complex, making monitoring and troubleshooting increasingly costly.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For partners, writing code becomes more complicated. It is not only about the physical volume of code (creating a shared component to communicate with queues is not that complex of an engineering task) but also about anticipating every endpoint to possibly respond slowly. With synchronous endpoints, we assume by default that they respond within a reasonable time, less than a typical response timeout (which, for client applications, means that just a spinner might be shown to a user). With asynchronous endpoints, we don't have such a guarantee as it's simply impossible to provide one.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Employing task queues might lead to some problems specific to the queue technology itself, i.e., not related to the business logic of the request handler:</p>
|
||||
<ul>
|
||||
<li>tasks might be “lost” and never processed</li>
|
||||
<li>under the task identifier, wrong data might be published (corresponding to some other task) or the data might be corrupted.</li>
|
||||
</ul>
|
||||
<p>These issues will be totally unexpected by developers and will lead to bugs in applications that are very hard to reproduce.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>As a result of the above, the question of the viability of such an SLA level arises. With asynchronous tasks, it's rather easy to formally make the API uptime 100.00% — just some requests will be served in a couple of weeks when the maintenance team finds the root cause of the delay. Of course, that's not what API consumers want: their users need their problems solved <em>now</em> or at least <em>in a reasonable time</em>, not in two weeks.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Therefore, despite all the advantages of the approach, we tend to recommend applying this pattern only to those cases when they are really needed (as in the example we started with when we needed to lower the probability of collisions) and having separate queues for each case. The perfect task queue solution is the one that doesn't look like a task queue. For example, we might simply make the “order creation task is accepted and awaits execution” state a separate order status, and make its identifier the future identifier of the order itself:</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrders.length == 0) {
|
||||
// Don't call it a “task”,
|
||||
// just create an order
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
// App restart happens here,
|
||||
// and all the same requests
|
||||
// are repeated
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
/* → { orders: [{
|
||||
order_id: <task identifier>,
|
||||
status: "new"
|
||||
}]} */
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: let us also mention that in the asynchronous format, it's possible to provide not only binary status (task done or not) but also execution progress as a percentage if needed.</p><div class="page-break"></div><h3><a href="#chapter-20" class="anchor" id="chapter-20">Chapter 20. Lists and Accessing Them</a></h3><div class="page-break"></div><h3><a href="#chapter-21" class="anchor" id="chapter-21">Chapter 21. Bidirectional Data Flows. Push and Poll Models</a></h3><div class="page-break"></div><h3><a href="#chapter-22" class="anchor" id="chapter-22">Chapter 22. Organization of Notification Systems</a></h3><div class="page-break"></div><h3><a href="#chapter-23" class="anchor" id="chapter-23">Chapter 23. Atomicity</a></h3><div class="page-break"></div><h3><a href="#chapter-24" class="anchor" id="chapter-24">Chapter 24. Partial Updates</a></h3><div class="page-break"></div><h3><a href="#chapter-25" class="anchor" id="chapter-25">Chapter 25. Degradation and Predictability</a></h3><div class="page-break"></div><h2><a href="#section-4" class="anchor" id="section-4">Section III. The Backward Compatibility</a></h2><h3><a href="#back-compat-statement" class="anchor" id="back-compat-statement">Chapter 26. The Backward Compatibility Problem Statement</a><a href="#chapter-26" class="secondary-anchor" id="chapter-26"> </a></h3>
|
||||
<p>As usual, let's conceptually define “backward compatibility” before we start.</p>
|
||||
<p>Backward compatibility is a feature of the entire API system to be stable in time. It means the following: <strong>the code that developers have written using your API continues working functionally correctly for a long period of time</strong>. There are two important questions to this definition and two explanations:</p>
|
||||
<ol>
|
||||
|
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@ -2682,7 +2682,97 @@ const pendingOrders = await api.
|
||||
<p>Математически вероятность получения ошибки выражается довольно просто: она равна отношению периода времени, требуемого для получения актуального состояния к типичному периоду времени, за который пользователь перезапускает приложение и повторяет заказ. (Следует, правда, отметить, что клиентское приложение может быть реализовано так, что даст вам ещё меньше времени, если оно пытается повторить несозданный заказ автоматически при запуске). Если первое зависит от технических характеристик системы (в частности, лага синхронизации, т.е. задержки репликации между мастером и копиями на чтение). А вот второе зависит от того, какого рода клиент выполняет операцию.</p>
|
||||
<p>Если мы говорим о приложения для конечного пользователя, то типично время перезапуска измеряется для них в секундах, что в норме не должно превышать суммарного лага синхронизации — таким образом, клиентские ошибки будут возникать только в случае проблем с репликацией данных / ненадежной сети / перегрузки сервера.</p>
|
||||
<p>Однако если мы говорим не о клиентских, а о серверных приложениях, здесь ситуация совершенно иная: если сервер решает повторить запрос (например, потому, что процесс был убит супервизором), он сделает это условно моментально — задержка может составлять миллисекунды. И в этом случае фон ошибок создания заказа будет достаточно значительным.</p>
|
||||
<p>Таким образом, возвращать по умолчанию событийно-консистентные данные вы можете, если готовы мириться с фоном ошибок или если вы можете обеспечить задержку получения актуального состояния много меньшую, чем время перезапуска приложения на целевой платформе.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Глава 19. Асинхронность и управление временем</a></h3><div class="page-break"></div><h3><a href="#chapter-20" class="anchor" id="chapter-20">Глава 20. Списки и организация доступа к ним</a></h3><div class="page-break"></div><h3><a href="#api-patterns-push-vs-poll" class="anchor" id="api-patterns-push-vs-poll">Глава 21. Двунаправленные потоки данных. Push и poll-модели</a><a href="#chapter-21" class="secondary-anchor" id="chapter-21"> </a></h3><div class="page-break"></div><h3><a href="#chapter-22" class="anchor" id="chapter-22">Глава 22. Варианты организации системы нотификаций</a></h3><div class="page-break"></div><h3><a href="#chapter-23" class="anchor" id="chapter-23">Глава 23. Атомарность</a></h3><div class="page-break"></div><h3><a href="#chapter-24" class="anchor" id="chapter-24">Глава 24. Частичные обновления</a></h3><div class="page-break"></div><h3><a href="#chapter-25" class="anchor" id="chapter-25">Глава 25. Деградация и предсказуемость</a></h3><div class="page-break"></div><h2><a href="#section-4" class="anchor" id="section-4">Раздел III. Обратная совместимость</a></h2><h3><a href="#back-compat-statement" class="anchor" id="back-compat-statement">Глава 26. Постановка проблемы обратной совместимости</a><a href="#chapter-26" class="secondary-anchor" id="chapter-26"> </a></h3>
|
||||
<p>Таким образом, возвращать по умолчанию событийно-консистентные данные вы можете, если готовы мириться с фоном ошибок или если вы можете обеспечить задержку получения актуального состояния много меньшую, чем время перезапуска приложения на целевой платформе.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Глава 19. Асинхронность и управление временем</a></h3>
|
||||
<p>Продолжим рассматривать предыдущий пример. Пусть на старте приложение получает <em>какое-то</em> состояние системы, возможно, не самое актуальное. От чего ещё зависит вероятность коллизий и как мы можем её снизить?</p>
|
||||
<p>Напомним, что вероятность эта равна она равна отношению периода времени, требуемого для получения актуального состояния к типичному периоду времени, за который пользователь перезапускает приложение и повторяет заказ. Повлиять на знаменатель этой дроби мы практически не можем (если только не будем преднамеренно вносить задержку инициализации API, что мы всё же считаем крайней мерой). Обратимся теперь к числителю.</p>
|
||||
<p>Наш сценарий использования, напомним, выглядит так:</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrder.length == 0) {
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
// Здесь происходит крэш приложения,
|
||||
// и те же операции выполняются
|
||||
// повторно
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders(); // → []
|
||||
if (pendingOrder.length == 0) {
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
</code></pre>
|
||||
<p>Таким образом, мы стремимся минимизировать следующий временной интервал: сетевая задержка передачи команды <code>createOrder</code> + время выполнения <code>createOrder</code> + время пропагации изменений до реплик. Первое мы вновь не контролируем (но, по счастью, мы можем надеяться на то, что сетевые задержки в пределах сессии величина плюс-минус постоянная, и, таким образом, последующий вызов <code>getOngoingOrders</code> будет задержан примерно на ту же величину); третье, скорее всего, будет обеспечиваться инфраструктурой нашего бэкенда. Поговорим теперь о втором времени.</p>
|
||||
<p>Мы видим, что, если создание заказа само по себе происходит очень долго (здесь «очень долго» = «сопоставимо со временем запуска приложения»), то все наши усилия практически бесполезны. Пользователь может устать ждать исполнения вызова <code>createOrder</code>, выгрузить приложение и послать второй (и более) <code>createOrder</code>. В наших интересах сделать так, чтобы этого не происходило.</p>
|
||||
<p>Но каким образом мы реально можем улучшить это время? Ведь создание заказа <em>действительно</em> может быть длительным — нам нужно выполнить множество проверок и дождаться ответа платёжного шлюза, подтверждения приёма заказа кофейней и т.д.</p>
|
||||
<p>Здесь нам на помощь приходят асинхронные вызовы. Если наша цель — уменьшить число коллизий, то нам нет никакой нужды дожидаться, когда заказ будет <em>действительно</em> создан; наша цель — максимально быстро распространить по репликам знание о том, что заказ <em>принят к созданию</em>. Мы можем поступить следующим образом: создавать не заказ, а задание на создание заказа, и возвращать его идентификатор.</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrder.length == 0) {
|
||||
// Вместо создания заказа
|
||||
// размещаем задание на создание
|
||||
const task = await api
|
||||
.putOrderCreationTask(…);
|
||||
}
|
||||
// Здесь происходит крэш приложения,
|
||||
// и те же операции выполняются
|
||||
// повторно
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
// → { tasks: [task] }
|
||||
</code></pre>
|
||||
<p>Здесь мы предполагаем, что создание задания требует минимальных проверок и не ожидает исполнения каких-то длительных операций, а потому происходит много быстрее. Кроме того, саму эту операцию — создание асинхронного задания — мы можем поручить отдельному сервису абстрактных заданий в составе бэкенда. Между тем, имея функциональность создания заданий и получения списка текущих заданий, мы значительно уменьшаем «серые зоны» состояния неопределённости, когда клиент не может узнать текущее состояние сервера точно.</p>
|
||||
<p>Таким образом, мы естественным образом приходим к паттерну организации асинхронного API через очереди заданий. Мы используем здесь термин «асинхронность» логически — подразумевая отсутствие взаимных <em>логических</em> блокировок: посылающая сторона получает ответ на свой запрос сразу, не дожидаясь окончания исполнения запрошенной функциональности, и может продолжать взаимодействие с API, пока операция выполняется. При этом технически в современных системах блокировки клиента (и сервера) почти всегда не происходит и при обращении к синхронным эндпойнтам — однако логически продолжать работать с API, не дождавшись ответа на синхронный запрос, может быть чревато коллизиями подобно описанным выше.</p>
|
||||
<p>Асинхронный подход может применяться не только для устранения коллизий и неопределённости, но и для решения других прикладных задач:</p>
|
||||
<ul>
|
||||
<li>организация ссылок на результаты операции и их кэширование (предполагается, что, если клиенту необходимо снова прочитать результат операции или же поделиться им с другим агентом, он может использовать для этого идентификатор задания);</li>
|
||||
<li>обеспечение идемпотентности операций (для этого необходимо ввести подтверждение задания, и мы фактически получим схему с черновиками операции, описанную в главе <a href="#api-design-describing-interfaces">«Описание конечных интерфейсов»</a>);</li>
|
||||
<li>нативное же обеспечение устойчивости к временному всплеску нагрузки на сервис — новые задачи встают в очередь (возможно, приоритизированную), фактически имплементируя <a href="https://en.wikipedia.org/wiki/Token_bucket">«маркерное ведро»</a>;</li>
|
||||
<li>организация взаимодействия в тех случаях, когда время исполнения операции превышает разумные значения (в случае сетевых API — типичное время срабатывания сетевых таймаутов, т.е. десятки секунд) либо является непредсказуемым.</li>
|
||||
</ul>
|
||||
<p>Кроме того, асихнронное взаимодействие удобнее с точки зрения развития API в будущем: устройство системы, обрабатывающей такие запросы, может меняться в сторону усложнения и удлинения конвейера исполнения задачи, в то время как синхронным функциям придётся укладываться в разумные временные рамки, чтобы оставаться синхронными — что, конечно, ограничивает возможности рефакторинга внутренних механик.</p>
|
||||
<p><strong>NB</strong>: иногда можно встретить решение, при котором эндпойнт имеет двойной интерфейс и может вернуть как результат, так и ссылку на исполнение задания. Хотя для вас как разработчика API он может выглядеть логично (смогли «быстро» выполнить запрос, например, получить результат из кэша — вернули ответ; не смогли — вернули ссылку на задание), для пользователей API это решение крайне неудобно, поскольку заставляет поддерживать две ветки кода одновременно. Также встречается парадигма предоставления на выбор разработчику два набора эндпойнтов, синхронный и асинхронный, но по факту это просто перекладывание ответственности на партнёра.</p>
|
||||
<p>Популярность данного паттерна также обусловлена тем, что многие современные микросервисные архитектуры «под капотом» также взаимодействуют асинхронно — либо через потоки событий, либо через асинхронную постановку заданий же. Имплементация аналогичной асинхронности во внешнем API является самым простым способом обойти возникающие проблемы (читай, те же непредсказуемые и возможно очень большие задержки выполнения операций). Доходит до того, что в некоторых API абсолютно все операции делаются асинхронными (включая чтение данных), даже если никакой необходимости в этом нет.</p>
|
||||
<p>Мы, однако, не можем не отметить, что, несмотря на свою привлекательность, повсеместная асинхронность влечёт за собой ряд достаточно неприятных проблем.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Если используется единый сервис очередей на все эндпойнты, то она становится единой точкой отказа. Если события не успевают публиковаться и/или обрабатываться — возникает задержка исполнения во всех эндпойнтов. Если же, напротив, для каждого функционального домена организуется свой сервис очередей, то это приводит к кратному усложнению внутренней архитектуры и увеличению расходов на мониторинг и исправление проблем.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Написание кода для партнёра становится гораздо сложнее. Дело даже не в физическом объёме кода (в конце концов, создание общего компонента взаимодействия с очередью заданий — не такая уж и сложная задача), а в том, что теперь в отношении каждого вызова разработчик должен поставить себе вопрос: что произойдёт, если его обработка займёт длительное время. Если в случае с синхронными эндпойнтами мы по умолчанию полагаем, что они отрабатывают за какое-то разумное время, меньшее, чем типичный таймаут запросов (например, в клиентских приложения можно просто показать пользователю спиннер), то в случае асинхронных эндпойнтов такой гарантии у нас не просто нет — она не может быть дана.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Использование очередей заданий может повлечь за собой свои собственные проблемы, не связанные с собственно обработкой запроса:</p>
|
||||
<ul>
|
||||
<li>задание может быть «потеряно», т.е. никогда не быть обработанным;</li>
|
||||
<li>под идентификатором задания могут быть по ошибке размещены неправильные данные (соответствующие другому заданию) или же данные могут быть повреждены.</li>
|
||||
</ul>
|
||||
<p>Эти ситуации могут оказаться совершенно неожиданными для разработчиков и приводить к крайне сложным в воспроизведении ошибкам в приложениях.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Как следствие вышесказанного, возникает вопрос осмысленности SLA такого сервиса. Через асинхронные задачи легко можно поднять аптайм API до 100% — просто некоторые запросы будут выполнены через пару недель, когда команда поддержки, наконец, найдёт причину задержки. Но такие гарантии пользователям вашего API, разумеется, совершенно не нужны: их пользователи обычно хотят выполнить задачу <em>сейчас</em> или хотя бы за разумное время, а не через две недели.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Поэтому, при всей привлекательности идеи, мы всё же склонны рекомендовать ограничиться асинхронными интерфейсами только там, где они действительно критически важны (как в примере выше, где они снижают вероятность коллизий), и при этом иметь отдельные очереди для каждого кейса. Идеальное решение с очередями — то, которое вписано в бизнес-логику и вообще не выглядит очередью. Например, ничто не мешает нам объявить состояние «задание на создание заказа принято и ожидает исполнения» просто отдельным статусом заказа, а его идентификатор сделать идентификатором будущего заказа:</p>
|
||||
<pre><code>const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
if (pendingOrder.length == 0) {
|
||||
// Не называем это «заданием» —
|
||||
// просто создаём заказ
|
||||
const order = await api
|
||||
.createOrder(…);
|
||||
}
|
||||
// Здесь происходит крэш приложения,
|
||||
// и те же операции выполняются
|
||||
// повторно
|
||||
const pendingOrders = await api.
|
||||
getOngoingOrders();
|
||||
/* → { orders: [{
|
||||
order_id: <идентификатор задания>,
|
||||
status: "new"
|
||||
}]} */
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: отметим также, что в формате асинхронного взаимодействия можно передавать не только бинарный статус (выполнено задание или нет), но и прогресс выполнения в процентах, если это возможно.</p><div class="page-break"></div><h3><a href="#chapter-20" class="anchor" id="chapter-20">Глава 20. Списки и организация доступа к ним</a></h3><div class="page-break"></div><h3><a href="#api-patterns-push-vs-poll" class="anchor" id="api-patterns-push-vs-poll">Глава 21. Двунаправленные потоки данных. Push и poll-модели</a><a href="#chapter-21" class="secondary-anchor" id="chapter-21"> </a></h3><div class="page-break"></div><h3><a href="#chapter-22" class="anchor" id="chapter-22">Глава 22. Варианты организации системы нотификаций</a></h3><div class="page-break"></div><h3><a href="#chapter-23" class="anchor" id="chapter-23">Глава 23. Атомарность</a></h3><div class="page-break"></div><h3><a href="#chapter-24" class="anchor" id="chapter-24">Глава 24. Частичные обновления</a></h3><div class="page-break"></div><h3><a href="#chapter-25" class="anchor" id="chapter-25">Глава 25. Деградация и предсказуемость</a></h3><div class="page-break"></div><h2><a href="#section-4" class="anchor" id="section-4">Раздел III. Обратная совместимость</a></h2><h3><a href="#back-compat-statement" class="anchor" id="back-compat-statement">Глава 26. Постановка проблемы обратной совместимости</a><a href="#chapter-26" class="secondary-anchor" id="chapter-26"> </a></h3>
|
||||
<p>Как обычно, дадим смысловое определение «обратной совместимости», прежде чем начинать изложение.</p>
|
||||
<p>Обратная совместимость — это свойство всей системы API быть стабильной во времени. Это значит следующее: <strong>код, написанный разработчиками с использованием вашего API, продолжает работать функционально корректно в течение длительного времени</strong>. К этому определению есть два больших вопроса, и два уточнения к ним.</p>
|
||||
<ol>
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user