The present article deals with the latter case; we show how, by writing code that follows a specific pattern and leans on a suitable support library, the benefits of hierarchical exception handling and static exception checking can be achieved. This technique was applied in some parts of Lightstreamer Server with proficiency.
Table of Contents
1. Introduction
Asynchronous programming is a widely adopted technique for the implementation of web services and it is almost mandatory for providing push notifications based on a Comet/WebSocket communication channel. In a nutshell, the technique consists in never using blocking API functions; whenever a wait for a condition is needed at some point in the execution flow, all the remaining part of the processing should be collected in one or multiple callback functions and supplied to a proper asynchronous API function. Therefore, the wait will arise from the delayed invocation of the callback. In pseudocode terms, this means avoiding the form:snippet 1.1
in favour of the form:
snippet 1.2
Hopefully, the non-blocking API will reduce the need for threads (by causing, in a certain way, multiple execution flows, when in waiting state, to share a single blocked thread); this will lead to a better usage of system resources and improve the overall scalability.
By the way, the use of blocking API functions, if a non-blocking equivalent is not available, can be turned into the asynchronous paradigm by wrapping them in dedicated threads or in a thread pool, as in:
snippet 1.3
which mimics snippet 1.2. The use of a thread pool would allow us to configure it in such a way as to pose an upper limit to the resource consumption (in terms of blocked threads) related with myService, as in the well-known Staged Event-Driven Architecture (SEDA). This is certainly an aspect that a sensible asynchronous library, if available, would have taken into account.
The drawback of the asynchronous approach is that the program code has to be spread over multiple callbacks, which affects code readability.
This is particularly true when, from a logical point of view, the execution flow is strictly sequential and lends itself to a top-down representation, made of multiple layers, such as in the following pseudocode:
snippet 1.4
where preStuff and postStuff are both related with layer i scope and should share the same context, hence should be defined as close as possible. If, anywhere within the next layer, the invocation of an asynchronous API were possible, then the above form would be incorrect, as the whole do-layer-i+1 function would have to be considered as asynchronous too; hence, the above pseudocode would have to be transformed into the more complicated form:
snippet 1.5
where an onDone parameter is added to each layer to carry forward the postStuff instructions in a recursive way. As said, some of these layers may involve the invocation of an external API. We can take this into account by extending snippet 1.4 into this general form:
snippet 1.6
whose asynchronous equivalent would become:
snippet 1.7
This code style is quite cumbersome, yet many programmers can use it with proficiency.
The Problem
However, by now focusing on the Java language, the above transformation would deprive us of one of the most useful tools provided by this language: static exception checking. Top-down style code can strongly benefit from static exception checking, which allows different error conditions to be traced back and handled at the proper layer.Hence, sticking to pseudocode, let’s assume that a typical layer has the form:
snippet 1.8
Where, by do-layer-i+1, we represent a new layer (the extended case that includes an external API invocation, as in snippet 1.6, could be considered as well). This form does not have a natural equivalent in the asynchronous case: by providing the next layer with all the handlers in form of callbacks, we would lose the compiler check that each type of exception must be either handled or declared.
Or, we can put it in this way: the onDone operation to be carried forward to do-layer-i+1 should be the whole try-catch-finally check, whose evaluation should be postponed and applied only when the outcome of the asynchronous processing of the try block is determined; something like this:
snippet 1.9
Of course, writing multiple layers in a style like this would be awkward.
Our Approach
When coding Lightstreamer Server in Java, we did face the need to implement top-down logical workflows, which included the invocation of asynchronous APIs, but also a great deal of exception handling, in the most clear, error-safe, and manageable way.So, we decided to adopt the above pseudocode pattern, to ensure correct exception propagation across asynchronous invocations, but struggled to find the best way to write code equivalent to that. The resulting code would feature a separation between lines related with functional aspects and boilerplate lines, related with flow control aspects. In particular, the latter would be similar across the various layers, hence easily recognizable and prone to be just ignored by a knowledgeable programmer when concentrating on functional aspects.
We eventually came up with a bunch of Java support code that allowed us to write our layers in a coding form, which, in our view, achieves a reasonable compromise between code readability and syntax richness.
The produced code consists in a support library, which supplies various classes, required by the coding form, which perform all the needed tricks under the hood, and in a further library of utility classes, which wrap all asynchronous APIs to be used, to make them compliant with our coding form.
We will refer to this coding form and to this technique in general as the Asynchronous Top-Down Chain pattern.
The goal of the remaining part of this post is to illustrate the coding style made possible by our support library and to discuss the related pros and cons; and hopefully, receive some feedback about the approach as well.
On the other hand, a presentation and a description of the support library and utility classes is out of the current scope: in the following code examples, we will take them for granted and just describe shortly their behavior.
2. The Proposed Coding Style
The form in which we write each layer in a possibly long sequence, according to the Asynchronous Top-Down Chain pattern, is the following:snippet 2.1
which, in turn, assumes a similar definition for the subsequent Stage_m.
Note that we keep using function calls to represent (possibly empty) sequences of instructions. Also types, variables, and exceptions (with related catch blocks) indicated are placeholders for (possibly empty) lists of them; obviously, this does not hold for R_n and R_m, the return types of the methods, which however, may represent either a real type or void and, in the latter case, the code shown should be adjusted accordingly. As can be inferred from the code, our naming convention is the following:
- R_n, T_n and v_n represent the return type, argument types and argument variables of Stage_n, as exposed by its invoke method.
- R_m, T_m and v_m represent the return type, argument types and argument variables of the subsequent Stage_m, as exposed by its invoke method.
- EP are exceptions that pass through the layer shown;
- EC are exceptions that are caught in the layer shown;
- EI are exceptions newly introduced in invoke, but outside the try block;
- ES are exceptions newly introduced (and not caught) anywhere in the layer shown.
Accordingly, the subsequent Stage_m.invoke throws the combination of EP and EC, whereas Stage_n.invoke throws the combination of EP, EI, and ES. Note that there is no loss of generality in this representation.
However, the creation of an instance is only possible through the library’s getStage method (to enforce this constraint, the class inherits from FullStage a dummy abstract method that is not visible from application code; hence, the Stage class cannot implement it and must be declared abstract). This allows our library to return, instead of an instance of the Stage class, an instance of a suitable proxy class (which extends and instruments the Stage class), so that the invocation of the layer through invoke() calls the proxy instead. In this way, upon invoke(), the proxy can take care of all the stuff needed to execute the layer in the asynchronous style depicted in snippet 1.9.
To enable the instance generation, the Stage class must declare a suitable constructor, which must be public. On the other hand, the Stage class is not required to provide an empty constructor: the getStage method accepts any arguments after the Stage’s Class specification and looks for a corresponding constructor for these arguments. This even supports Stage classes defined as non-static inner classes, provided that a pointer to an instance of the outer class is supplied to getStage before any other constructor arguments. Obviously, all the conditions involved will not be checked until runtime and exceptions may arise.
Invocations of asynchronous APIs are carried out in a similar way. This is accomplished with the optional chain.setRedirector() call, where different implementations of the Redirector interface can encapsulate the various asynchronous APIs available. When specifying a Redirector just before calling the sub-Stage’s invoke, the proxy will take care of invoking the external API through the Redirector and then the subsequent sublayer in the proper way. So, through a Redirector, the proxy may call an asynchronous API, for instance, to write something, then proceed with the next layer when finished. Another typical case is using a Redirector for scheduling the next layer for running in a specified thread pool.
It is important that chain.setRedirector() immediately preceeds stage.invoke(), because the two calls are strongly coupled. More precisely, it is important that no exceptions or early returns can occur in between; otherwise, it will be the responsibility of the application to ensure the Redirector setting is reset (by calling chain.setRedirector(null)).
Note that, by comparing this form with snippet 1.6, the midStuff (i.e., code to be executed after the invocation of an external API but not pertaining to a sublayer) is missing; this and other expressivity limitations of the proposed coding rules will be discussed later.
Providing general-purpose Redirectors is the job of the utility classes introduced above, although custom Redirectors can be defined as well.
By the way, we chose to use the word “Stage" to qualify layer implementation classes, to recall the SEDA approach. We will also say “Stage” to indicate a layer as implemented in Java through our coding rules.
The Proxy Trick
A key part of the technique is the segregation of all the code up to the invocation of the next Stage in its own method (that we name body), so the method can be overridden by the proxy as well.The rationale is that, upon a call to the proxy’s invoke() (here, think to an invocation of Stage_n as defined in snippet 2.1), only the Stage’s body method will be executed instead, as in snippet 1.9 above. Hence, the Stage’s invoke method (like complete-layer-i in snippet 1.9) has to be kept and performed only after the real termination of body, in whichever thread it occurs; but, in this case, invoke may not call body again: it should only recall the outcome of the body execution (or the exception possibly thrown) and this is what the proxy’s overriding body method allows for and it is supposed to do.
A condition for the application of this trick is that the invoke and body methods share the same argument types, though not necessarily the same exceptions and the same return type. This can be enforced and managed by our support library, as will be detailed below.
As a consequence of the trick, the whole execution of the chain of Stages consists in a forward chain, made of the body executions, followed by a backward chain, made of the invoke executions.
The backward chain will be played by the library upon the first time a body method exits without calling the next Stage.
We chose this form, despite the complexity it introduces, because it features a key property: the code is the same as its synchronous equivalent; in fact, if just getStage returned an instance of the Stage class instead of a proxy and setRedirector should directly invoke the involved external API in a synchronous way, then the whole execution would be sequential. In practice, as far as functional aspects are concerned, we can read the code as though it was synchronous with the only added complexity as a result of the separation of body from invoke, not a big issue indeed.
The following sequence diagrams illustrate the work of the proxies. Diagram 2.1 shows two Stages, S1 and S2, where S1 invokes S2 on a different thread, as exemplified by the asynchronous message 2.1.2.1. The corresponding synchronous equivalent, where proxies are bypassed, is shown next, in diagram 2.2.
diagram 2.1
diagram 2.2
When stage.invoke() redirects the execution of the chain on a different thread, because of a Redirector specified there or in any sub-Stage (as exemplified by arrows 2.1.2.2 to 2.3 in diagram 2.1), the call throws a RedirectedException. This exception should never be caught directly by application code, but it should be allowed to flow back to the caller recursively, as it signals that the current thread is no longer the one that is processing the chain. By the way, note that the library may throw a RedirectedException only upon invoke(), and never upon body(); as a consequence, you can ignore it completely when writing your try-catch-finally statements.
Details of the Support Library Interface
The various classes silently used in snippet 2.1 take care of doing all the needed tricks.In particular, the Chain class is the heart of the implementation. An instance of the Chain class has to be created at the start of the chain processing, then forwarded from each Stage to the next, to keep track of the stack of invoke methods that make up the backward chain. The Chain instance collects the needed information in collaboration with the proxies. It is mandatory that the last parameter in all the custom invoke and body methods carries the involved Chain instance.
The FullStage superclass provides the needed background for the Stage classes. In particular, it provides the getStage factory method and the StageInvocation and StageBody annotation classes, which simplify the definition of the Stage class; in fact, the library identifies the invoke and body methods by their annotations, not by their names, which are actually free.
As anticipated, the proxies carry out two jobs: one is decoupling the invoke and body methods, the other is launching the next Stage through an asynchronous API, when needed, as specified through the Redirector instance supplied to the optional Chain.setRedirector. One point that we have not yet considered is that the execution of an asynchronous API may give rise to its own error conditions, which could pop up asynchronously as well and any of these exceptions have to be handled with suitable callbacks. This complication affects asynchronous programming techniques in general and we also avoided coping with it by extending snippet 1.9 (we just left that to the reader’s imagination).
The Asynchronous Top-Down Chain pattern deals with this aspect by providing extended versions of the Redirector interface.
To show an example, we consider the call of an asynchronous write operation, after which the processing can continue, unless the write fails with an IOException.
The support library allows us to express that as:
snippet 2.2
but, in this case, the call to stage.invoke(), in addition to the exceptions pertaining to the Stage_m.invoke method, may also cause an IOException to be issued to the caller. However, the exception declaration of Stage_m.invoke might not include an IOException; so, to enforce the additional declaration, our trick is to have it declared by the setRedirector call instead (note that setRedirector must always be followed by stage.invoke() immediately). This is achieved by the support library by defining the following overload of setRedirector:
snippet 2.3
where WriteRedirector, which can be supplied by the utility class library, must be written by extending Redirector.With1<IOException>, which is based on the following API:
snippet 2.4
In WriteRedirector, the launch method should start the asynchronous write operation, then take care of ensuring that exactly one among the provided onCompleted, onException1(IOException), and onUnexpectedException is eventually called.
Note the onUnexpectedException method, which allows unchecked exceptions occurred in the processing to be managed; it may be used to forward checked exceptions as well, but in this case, they will get wrapped by a RuntimeException.
With the same idea, any other asynchronous API can be encapsulated.
By the way, allowing this trick is the most frustrating part of the support library coding, because it requires the library to prepare different classes for each different number of exceptions to be expected (so Redirector.With2<E1, E2> and so on), along with all the related overloads of setRedirector.
One clarification may be useful at this point. In the example above, we said that a call to stage.invoke() would “also cause an IOException to be issued to the caller”. This does not mean that stage.invoke() may throw an IOException even though the Stage_m.invoke signature does not allow it. Actually, because a Redirector is involved, the proxy will cause stage.invoke() to just throw a RedirectedException, as explained in the previous paragraph.
A possible IOException would then reappear in a different thread, as the library initiates the backward chain with a call to the outer Stage_n.invoke: there, the included body() invocation will throw the IOException and this will be compliant with the Stage_n.body signature; in fact, this is exactly what the throws clause in setRedirector has forced us to do.
In some cases, the invocation of the asynchronous API through a Redirector is the last action of the chain. But because setRedirector must be followed by a sub-Stage call, we should have to supply a no-op Stage, whose proxy would just call the Redirector and then initiate the backward chain.
This turns out to occur quite often, so, to simplify, a dedicated void method called redirectAndClose is provided, which gets a Redirector as argument. So, issuing chain.redirectAndClose(myRedirector) replaces both the setRedirector and the subsequent invoke(chain) call on a no-op Stage. A similar method, redirectAndReturn, in which the no-op Stage just returns a supplied value is also provided.
To support any kind of Redirector, the various exception-enabled overloads of redirectAndClose and redirectAndReturn are provided as well, similarly to what was done for setRedirector.
So far, we have shown the recursive part of a top-down chain of layers. We also have to provide a starting point. For this, we stick to the classic callback-based form:
snippet 2.5
In fact, the static startChain method and the FirstStage and ChainOutcomeListener interfaces are the only means provided by the support library to initiate a chain (note that an optional logger for the chain’s own logging is also provided here). The Chain.startChain method is the only place where a Chain instance is created and a possible RedirectedException is eventually caught.
A Broad Definition
The coding form shown in snippet 2.1 is very limited and does not allow for the conversion of all possible logical top-town representations of a workflow. In pseudocode terms, it roughly corresponds to the following:snippet 2.6
where, if the invocation of the next layer is omitted, layer-i will represent the innermost layer. The informal restrictions stated for preStuff are needed here to ensure that the whole block, up to and including the invocation of do-layer-i+1, can be segregated into a separate body function (note that “does not change a” only prevents the change of the do-layer-i argument values, not of anything pointed by these values). For the same reason, postStuff is only allowed to use a and c but not b.
On the other hand, the starting point of the chain corresponds to a logical layer that propagates no exceptions, so that its invocation can fit into the form:
snippet 2.7
In practice, the coding will only be straightforward if you can devise your logical layers in such a way as to fit into the above templates.
Presumably, this will occur in few cases, whereas, in most cases, there won’t be a natural fit. In some cases, this might be achieved by refactoring the layers, possibly splitting some layers into an inner and an outer part, but at the cost of separating pieces of code that should better be placed close to each other (more on this later).
However, the template shown in snippet 2.1 was only supposed to be an example and it does not exhaust the whole range of code that can take advantage of our support library. Actually, we would prefer to include in the Asynchronous Top-Down Chain pattern a wider range of possible code, still consistent with the support library, but, unfortunately, we are not able to provide a wider definition of the pattern in terms of formal coding rules.
Nevertheless, once the behavior of the support library is known, along with all the impacts on code semantics, we could just take the library “as is” and design our code by directly leveraging the library for the intended purpose. This might turn out to be the easiest way to proceed in a real scenario and it will also help us overcome the limitations of the basic template.
To resume, all we need is to ensure that the support library can operate the proxy trick (that is, the replacement of the calls to invoke with calls to body and the deferring of the real calls to invoke in the backward chain) by keeping the same semantics as in a proxy-free case.
In particular, claiming that stage.invoke() would be the “last instruction of the block” or placing no instructions before body() served this purpose but it was quite restrictive. The same conditions can be put in a lighter way; all told, our use of proxies poses two main constraints to the final code:
- There should be no instructions preceding any call of body() in the invoke method. This is because any such instruction would be supposed to be executed before body, but, actually, it would be executed after, in the backward chain.
However, the constraint could be relieved by considering that this code misplacement would change the semantics of the program only if side-effects or early returns were involved. So, any initialization code with no such side-effects is actually allowed.
Let’s call the above the Body Constraint.
- There should be no further instructions following any call of stage.invoke() for the next Stage in the body method (and similarly for the FirstStage.invoke method).
This is because the control flow could proceed asynchronously after that call. By the way, in that case, stage.invoke() would issue a RedirectedException and any subsequent instruction would be cut off.
The only instruction supported after stage.invoke() is the return of its result (when it is not void), as shown in the base template; this is why the return type of body was constrained to be the same as the included sub-Stage’s invoke. If invoke is not void, ignoring its result (to yield a void body) is also supported.
However, the actual code that, after stage.invoke(), causes the control flow to exit the method and to possibly return the result is free, provided, again, that no side effects are involved.
Let’s call the above the Substage Constraint.
As a first consequence, according with the Substage Constraint, the body method can contain loops and branches, which may give rise to multiple places in the code in which an invocation of a further Stage is performed, only provided that any such invocation can only occur just before exiting the method. Obviously, some branches may terminate without calling a further Stage, which would end the forward chain processing.
As another consequence, even the restriction that the postStuff executed after the sub- Stage should always be the same can be partly overcome, by placing branches in the try block of invoke, leading to multiple alternative occurrences of body() and postStuff. In fact, in many cases it could be possible to write if statements in the try block in such a way that the Body Constraint would still be satisfied.
With the above considerations in mind, the main problematic points will now be analyzed in detail.
3. Notes on the Usability of the Technique
To what extent can an arbitrary top-down algorithm be made to fit into the Asynchronous Top- Down Chain coding pattern? With reference to snippet 2.6 above, the form of the layers is quite limited, as several parts are missing.We can’t address the problem in formal terms here, but we can enumerate the missing parts in the proposed layer structure as follows:
- missing initializationStuff at the beginning;
- missing midStuff after an external API call;
- lack of support for multiple sublayers and/or external API calls in sequence;
- coding limitations on preStuff;
- no sharing of variables between preStuff and the subsequent parts of the layer;
- lack of support for invoking sublayers and/or external API calls outside the try block.
To see an example of splitting, if some initializationStuff (i.e., code preceding the try- catch-finally statement; see pseudocode snippet 1.8) were needed, we could spawn a previous, yet very simple, layer, as in:
snippet 3.1
Splitting the layers in this way, obviously, worsens the code structure; in this case, we separate the initializationStuff from the other closely-related instructions, which is certainly less readable. Moreover, the separation may give rise to more occurrences of points 4 and 5 above to be overcome.
Point 6 remains to be dealt with. This is an important issue, because, at least, the need to invoke external APIs in a catch or finally block is something to be expected (for instance, to write a proper response to the client upon catching an exception).
Surprisingly enough, there is no such limitation at all. We haven’t introduced that in snippet 2.1 and snippet 2.6 in order to keep them simple, but the use of setRedirector and stage.invoke is not limited to inside body. We will cover this topic and provide all the details in a dedicated section later.
All told, we think we can claim that the limitations of the Asynchronous Top-Down Chain pattern are not in terms of what can or cannot be expressed with it, but rather, in terms of what can be expressed in an acceptable form. It might be viewed as encouraging the fact that, for Lightstreamer Server’s needs, the fit to this coding pattern was quite straightforward.
We will now outline various common coding cases, to see how they can be handled best. Whoever is confident on his ability to produce code compliant with the Body and Substage Constraints can skip the remaining part of this chapter.
Common Initialization for a Layer
As discussed above, a layer cannot include any initializationStuff instructions, as this is forbidden by the Body Constraint. Such initialization code can be useful to provide values that can be shared by all parts of the try-catch-finally statement. It also allows for any preparation stuff that should not be closed by the finally block when not successful.As a corollary, the recently introduced try-with-resource Java language statement is not supported directly by the Asynchronous Top-Down Chain pattern; it would need to be expanded and, eventually, this might also give rise to the situation described here.
We have seen how such initialization can be spawn in a previous layer.
However, the initializationStuff can also be useful to setup variables before entering the try-catch-finally statement; such variables would be handy, to allow the try block to pass values to the catch and finally blocks and, similarly, to allow a catch block to pass values to the finally block. Such variables should be declared as part of the current layer, not a previous one.
Fortunately, as anticipated above, the Body Constraint allows simple local variable declarations before the try block and adding initializers to them is also compliant, provided that they are simple enough not to involve any side-effect or exception. To summarize, the initializers should be constants or, at most, they may only depend on the function arguments, but not on anything pointed to by function arguments; for instance, they might be numerical operations on numerical function arguments.
This should account for most of the needs.
The extended form of the consequent invoke method is shown below:
snippet 3.2
Passing Values Back from the Body
Setting shared variables in invoke still does not allow for passing values back from the preStuff and the sublayer, to be used in the try-catch-finally block (probably the most important cases). In fact, these parts are segregated in the body method, so they cannot share local variables with the other parts in the invoke method.This need can be for the greatest part fulfilled by leveraging the return value of body(), that can be assigned to the common vi in postStuff. Otherwise, side-effects in the body() execution can be used.
The simplest way is to use instance variables of the Stage class itself, to be written in body and read in invoke; the drawback is that a similar Stage class will not be reentrant and the allocation of a dedicated instance may be needed for each execution.
Alternatively, bean objects could be supplied as arguments to body by invoke, so that the former will set values and the latter will get them; here, the problem is that invoke is required to supply its own arguments to body, hence the beans should be created by the previous Stage, to be passed to stage.invoke(), which may be out of context at that point.
Passing Values Back from the Sub-Stage
To pass values back from a sub-Stage, similar considerations apply. When leveraging the sub- Stage’s stage.invoke() return value is not enough, supplying bean objects to invoke might be the best way; but, because this occurs within body, this would only cover half of the trip: these beans would still have to be made visible also to invoke, hence falling into the previous case.Getting values generated by an invocation of an asynchronous API may be needed as well (for instance, upon a socket read operation). The provided Redirector interface does not support this, which means that the specific Redirector implementation used must take care of providing its own interface extension to extract values generated in the call.
These values can be made available to the subsequent sub-Stage, by passing a reference to the Redirector instance itself to stage.invoke(); to make them available also to the current stage’s invoke, the reference to the Redirector instance can be passed back in any of the ways discussed above.
Early Return from a Layer
In a general case (see snippet 1.8), the preStuff instructions may issue a return statement to leave the method successfully without calling the next layer and this would also cause any postStuff and completionStuff to be skipped. This cannot be achieved in our structure, where any return statement within body cannot be distinguished from a normal termination of the body block, so postStuff and completionStuff would not be skipped.Such a behavior, if needed, could be implemented manually by passing flags back from body to invoke as seen above. For more complex cases, it could even be necessary to spawn intermediate Stages.
4. Notes on the Overhead Introduced
The code structure resulting from the application of the Asynchronous Top-Down Chain pattern introduces several redundancies with respect to the logical form.We consider them to be acceptable, as they don't compromise readability.
For instance, the introduction of one Stage class for each layer may even improve readability, by emphasizing the staged structure.
The main complication introduced is the spawning of a body method, which separates most or all of the try block from the other parts of the Stage processing. This, along with the possible splitting of layers discussed in the previous paragraph, gives rise to a scattering of code fragments that negatively affects code readability. When multiple Stage classes are needed to implement a complex layer, we suggest defining them as inner classes of a hosting class, to keep them close to each other; as already pointed out, getProxy supports even non-static inner classes.
The complication as a result of the code scattering is also mitigated by the fact that the correct forwarding of parameters from Stage to Stage is checked by the compiler. And the same holds for exception forwarding. But if you consider that this, actually, applies to asynchronous code, then this property, rather than just a compensation, should be regarded as a worthwhile benefit.
In general, the use of boilerplate code is affordable when it is just a hassle, but it does not introduce risks of more coding errors. In this case, there are many constraints that have to be satisfied to produce correct code, each of which is a potential cause of errors.
Most of these constraints are directly related with the use of the support library.
The library interface has been expressly designed with the aim of limiting the freedom of its use, so that illegal uses can be detected by compiler checks.
But other kinds of illegal uses could not be expressed in form of syntax constraints. Many of them are checked by the library at runtime; detected errors give rise to a PatternException, which is of an unchecked type, or to error-level log. The library does its utmost to detect uncompliant code as early as possible and we expect syntax related issues to be found on the first testing run of the code.
Still, some constraints are under the sole control of the coder and this mainly regards the fact that the invoke and body methods must have the required form.
In particular, it is exclusive responsibility of the coder to ensure that the Body Constraint and the Substage Constraint are satisfied. Failing to do so may cause unexpected behavior that may prove difficult to detect.
The runtime overhead is potentially significant because of the use of reflection methods, but we have not found important impacts on the overall performance in our measurements.
Note that the support library has been designed in such a way that the generation of the synthetic proxy classes is done only once for each Stage class. And this operation is supposed to be the heaviest one involving the reflection package.
By the way, for the generation of the proxy classes, our library takes advantage of the Javassist library. This allowed us to come out with relatively simple coding rules. More complicated but still useful formulations of our pattern could be possible by leaning on the Java reflection package only, or even without any reflection involved.
The runtime overhead added upon each Stage method call consists mainly in a call to java.lang.reflect.Method.invoke. The most annoying aspect of this extra work is probably that it is visible when tracing the execution of the chain step by step through a programming GUI for debugging purpose. It may be advisable that both JDK reflection classes and the support library are configured to be hidden from the tracing. Similarly, the extra calls will overload stack traces and stack dumps.
Some memory overhead is also added, because of the various utility objects needed. However, the support library was crafted with the aim of mitigating this overhead too.
For instance, it is not needed to create a new Stage implementation object upon each invocation of the Stage; on-the-fly creation was used in the basic snippet 2.1, but only for brevity.
In fact, as far as the support library is concerned, Stage implementation objects can be reused by multiple subsequent executions and even used concurrently by multiple threads. So, the level of sharing of the Stage implementation objects is up to their coding; you can choose to use a single static stateless instance for all invocations; or, perhaps, to use multiple dedicated stateful instances, which would allow you to easily pass values from the body method back to the invoke method (as discussed above).
The Redirector types provided by the support library also allow concurrent use of the same instance. The same does not hold for custom Redirector classes defined by extending the basic abstract class provided for this purpose.
5. Extensions to the Basic Rules
Simplified Syntax for Simple Layers
Among the many layers identified in the logical top-down structure of our workflow, some may not need any code, including catch and finally blocks, after the sublayer execution, that is:snippet 5.1
Converting such a layer in the Asynchronous Top-Down Chain format would produce highly redundant code: the invoke method would just call the body method, which would include all the stuff. One might be tempted to merge such a layer with the previous one, by expanding do-layer-i inline in the previous layer.
This would be possible, provided that the previous layer does not involve an external API call; however, we suggest keeping the two layers and the related two Stages in all cases, so as to keep the separation of concerns identified at the logical level. To help with this task, the support library allows for a simpler and less redundant form:
snippet 5.2
Note that we have to assign to invoke the same return type as the sub-Stage’s invoke; in more general terms, the constraint on invoke is the same as the Substage Constraint introduced above. As you may suspect, when a SimpleStage is used instead of a FullStage, the invoke method is executed immediately, in the forward chain (possibly through a Redirector), and nothing is added to the backward chain.
Direct Access to the Backward Chain
There are other cases in which a strict application of the Asynchronous Top-Down Chain pattern is overkill and even less clear than the traditional use of callbacks.A typical one is shown below. Here, there is an initializationStuff, which must precede the try block and which is logically related with both the try and finally block. Suppose that this code cannot be made compliant with the Body Constraint. However, to simplify the scenario, the try block does not alter the variables it shares with the finally block (though it may modify the values pointed to); moreover, the finally block does not throw checked exceptions:
snippet 5.3
Despite its simple form, this layer should still require splitting into two, in order to be converted. To relieve such cases, the support library allows for the resort to the classic callback-based syntax, which, in this case, is handier. This is done by simply generalizing the use of the ChainOutcomeListener introduced before with startChain. In practice, the finally block can be transformed into a callback, which will be added to the backward chain, in this way:
snippet 5.4
which allows the layer to fit into a SimpleStage. Note that, in this way, if any exception is pending after the execution of the forward chain, it is notified to the onClose callback as well.
Calls to addClosingHook can be placed freely within invoke in a SimpleStage and within body in a FullStage. They are not supported within invoke in a FullStage.
Resuming the Chain From Within catch and finally Blocks
In the above description of the Asynchronous Top-Down Chain pattern, the launch of a sublayer, possibly through a Redirector, was only performed from within the try block (hence, from inside the body method, for full Stages).However, as anticipated, there is no such limitation at all: the support library can handle invocations of a sub-Stage also from any part of the invoke method, including the catch and finally blocks, hence “resuming” the chain. In other words, the same code snippet supported in body, that is
snippet 5.5
can be placed anywhere in invoke, provided that a constraint very similar to the Substage Constraint is satisfied:
- There can be no further instructions following a call of stage.invoke() for the next Stage in the invoke method.
This is because the control flow could proceed asynchronously after that point. So, any subsequent instruction executed by the method (for instance, a finally block) might occur at the wrong time.
The only instruction supported after stage.invoke(), unless it occurs in the finally block, is the return of its result (when it is not void); this would constrain the return type of invoke to be the same as the included sub-Stage’s invoke. Ignoring a result (to yield a void invoke) is also supported.
However, the actual code that causes the control flow to exit the method and to possibly return the value is free, provided, again, that no side effects are involved.
Let’s call this the Resume Constraint.
The conditions to satisfy the Resume Constraint depend on the place in which stage.invoke() is issued. Obviously, this call must be the last one in its block; to summarize the various cases:
- Multiple catch blocks may terminate with the launch of a new Stage, but if there is at least one, then there cannot be the finally block, nor any completionStuff.
- If a finally block terminates with the launch of a new Stage, then there cannot be any completionStuff. Moreover, as said, no explicit return statement is allowed for leaving the block. As a consequence, any value returned by stage.invoke should be ignored; but, actually, a stage.invoke method that returns a value is not supported at all; it is only allowed to declare a void return type.
- Launching a new stage at the end of the completionStuff is fully supported.
- Launching a new Stage in the postStuff is not a significant case (there would have to be no try-catch-finally statement at all and no completionStuff; this would end up to be the same
case as launching a new stage in the completionStuff). - Launching a new Stage in the initializationStuff is still not allowed by the Body Constraint.
The further restriction on the finally case (that an included Stage.invoke must declare a void return type) is quite strange and needs a short discussion. It is just originated by a limitation in our support library’s ability to handle the case. It is certainly not an important restriction, as placing return instructions in a finally block is discouraged in general, and ignoring the return value of a call is also not a recommended practice; still, it is error-prone, as the use of a non-void invoke method may come unnoticed. Unfortunately, the case cannot be checked by the support library neither at compile time nor at runtime, and the consequent program behavior may not be as expected.
To alleviate the hassle, the coder has the opportunity to put an explicit check on a Stage used in a finally block, by invoking stage.finallyCheck(), which, in case of a non-void invoke, would throw a PatternException at runtime.
One important point that has not been pointed out yet is that, because all processing done in invoke pertains to the backward chain, the thread in which this processing will occur may not be easily predictable. In fact, the backward chain will be initiated in the thread on which the forward chain will be closed or will get an exception.
So, if any heavy operation has to be made in invoke after the completion of the inner body, and the thread allocation for the various tasks has to be kept under control, then it may be desirable to leverage the resumption support just introduced and to spawn a separate Stage for those operations, so as to redirect all the stuff in the appropriate thread pool.
6. A Non-trivial Example
In the following example, we face a scenario that is still a fake one but that mimics typical situations that can sound familiar to many people involved in asynchronous programming.The code is supposed to do the following:
- getting a socket just opened;
- reading a request;
- parsing the request;
- waiting for some time, if needed (let's suppose that each user is granted a limited number of requests per second);
- invoking an external service;
- writing back the response.
The code should also manage several exception conditions, which can occur in the various tasks, by writing back proper error notifications.
Finally, we want to keep socket management, request parsing, and servicing tasks well separated and encapsulate them into different classes.
The logical structure of the problem is indeed simple and prone to a top-down representation; a blocking version of the code could be like this:
snippet 6.1
where we lean on obvious definitions for the various base classes, exceptions, and external functions mentioned.
The following could be the asynchronous version of the above code based on the Asynchronous Top-Down Chain pattern:
snippet 6.2
The code is significantly longer and still leans on some more classes that have been omitted, that is the various Redirectors (PoolRedirector, TimerRedirector, AsynchReader, and AsynchWriter), which take care of the asynchronous stuff by wrapping the base functions, in a way similar to the included MyServiceExecutor. However, these classes can be defined in a general purpose way and, in Lightstreamer, similar classes are included among the cited collateral library of reusable utility classes.
On the other hand, this code entirely preserves the main structure of the blocking version, which helps identify the role of each piece.
But the main benefit of this code style can be appreciated when a new exception is added to the declaration of any external method used in the processing: the exception will be outlined by the compiler and the development GUI will force the propagation of the exception declaration outward, until a suitable place for its management is reached, hopefully also assisting in the code modification work. This will be the same handy and safe process we are familiar with when dealing with synchronous code, which helps ensuring that all those annoying special cases that crop up when coding will be taken into account.
7. Conclusion
In this article, we have introduced the Asynchronous Top-Down Chain pattern, a coding rule for the invocation of asynchronous methods that, by leaning on a proper support library, hides the use of callbacks.We have shown how, by sticking to this coding rule, a sequential logical flow can be expressed in Java in a synchronous style, fully taking advantage of Java exception handling, even if it includes invocations to asynchronous APIs.
We have then analyzed in detail the benefits and the drawbacks of this approach and we have provided some sample code to see how the result may look like.
An evaluation of whether or not obeying to the restrictions introduced helps in simplifying code writing and management is left to the reader.
Acknowledgements
Thanks to Gianluca Finocchiaro (gianluca.finocchiaro@gmail.com) for reviewing the presentation and suggesting several improvements.*UPDATE* Based on the initial feedback received, we changed the article title from "Exception Handling in Asynchronous Java Code" to "Exploiting Static Exception Checking in Asynchronous Java Code".
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.