Frontline Skirmishes, Part 4: Reactive ifology with retries


Introduction

I have recently had to add a simple component to the Angular frontend, which, in addition to displaying a window with a message, was to perform only a single task: ask the backend if there is any new process of a certain type, and if the answer is positive, retrieve the process ID. If the called API method returned an empty string as the ID, the attempt to retrieve the ID was to be repeated every second until success, meaning a non-empty string is returned.

I’ve solved the problem in two different ways. First, let me present the more intuitive and simpler method, to be followed by the more appropriate one, which employs the reactive programming philosophy to the fullest extent.

Solution 1

The first method should check if there is a process to be downloaded and possibly call a method that will retrieve the ID. So it could look like this:

We also need a method that will repeatedly try to retrieve the process ID, which can be done using recurrence with a delay indicated by setTimeout:

By combining the above methods and adding a call for finishProcessing(), which may, for example, end the displaying of the data loading icon, we are left with the following code:

The above snippet consists of just two fairly uncomplicated methods and does the job perfectly well, but that doesn’t mean it’s perfect. First of all, we have a nested observable subscription here, which can be seen as an anti-pattern. The task can be performed in a more ‘reactive’ way by building a single pipeline containing all of the desired logic. Incidentally, our code will also become more DRY (‘Don’t Repeat Yourself’) and more SOLID (in line with the five design principles).

So how do we spice up the above code with some reactivity so that it tastes better to all fans of functional programming and beyond?

Solution 2


A simple pipeline that checks for the existence of a process and retrieves its ID could look like this:

The first challenge is to put a conditional statement in the pipeline which initiates attempts to retrieve the ID only if the process is established as actually existing. The RxJs pipeline alone does not provide any space for conditional querying or skipping its individual steps. However, a conditional statement may be incorporated in at least two ways.


We can inject an ‘If’ statement into one of the methods that make up the pipeline, so that this method returns the specified observable based on certain conditions.

In the example above, an attempt is made to retrieve the process ID provided that the process actually exists. Our method must behave appropriately: it must return an observable of the appropriate type even when the process does not exist. When using the ‘of’ command, the method will return a specific message that the process is not present.


As an alternative to the above method, we can use the iif statement, which will return one of the two observables, depending on the value returned by the predicate. So we obtain the same effect as above without creating an additional dedicated observable to handle the conditional selection logic for the observable to be returned.
If the iif statement is used, the pipeline looks like this:

The second problem is the conditional repetition of attempts to retrieve the process ID. RxJs provides several methods that allow a particular observable to be called repeatedly.

The repeat method repeats the observable subscription to which the pipe had been attached a specified number of times. It does so immediately whenever the observable does not return an error.

The second method, retry, works in the same way as repeat except that it resubscribes whenever, and only if, the observable returns any type of error.

The disadvantage of both methods is that they do not allow conditional repeating of subscriptions nor defining of specific delays between attempts.

So I used another method: retryWhen. It’s the most complicated, but it opens up nearly unlimited possibilities of defining the observable retry strategy.

When discussing the command set for retrying subscriptions, it is also worth mentioning repeatWhen and expand. The first one allows repeating calls according to a specific strategy which does not, however, have access to the values generated on the previous attempt, and so it isn’t helpful in our case. The expand method does allow recurrent calling of the observable, which can be made dependent on the previously returned value. Theoretically, it could be used in my scenario. I didn’t use it, and that’s because of problems with getting it to act in line with the theory.

The retryWhen method, like retry, only repeats the call after an error has been returned from the observable. In my case, such an action is not desirable. On the contrary, we don’t want to retry calls after an error, but only after an empty response is returned. This problem is not difficult to solve though. If we need an error, we can generate it using the throwError instruction. Then, we simply apply the retryWhen method accordingly to repeat the call in the event of a specific error type. The complete solution boils down to validating the response, which involves generating an exception when an unsatisfactory response is obtained. Then, using retryWhen with an appropriate strategy, we only repeat calls in the case of one specific error type.

My validation method throws an exception if the ID is empty. When the ID is valid, which is simply non-empty in my case, the method passes it on.

We create a repetition strategy by defining an object which returns a function that retrieves a parameter in the form of Observable<any> and returns a notification (any value) when the subscription is supposed to retry, or throws an exception if we no longer wish to retry the call.


An example strategy might look like this:

If we wanted to make further repetitions of the current attempt number dependent, we can retrieve information about the attempt number in the same way as error information about (the variable count in the example below):

We can now inject this strategy into the retryWhen method. The following example contains the complete validation and retry logic:

By combining all the above code snippets, the task can be executed reactively, as shown in the example below:

Conclusion

The above design is slightly more complex than the first, less reactive variant, and it may be argued whether it is at all necessary in such a simple case. The reactive solution, however, has the indisputable advantage that it allows further expansion of the logic in a relatively safe manner, which does not significantly deteriorate the readability of the code and minimizes the risk of errors, which would grow exponentially if we were to expand a non-reactive code.


In my opinion, reactive programming is worth mastering, even in such simple cases as the one above, to gain the experience necessary to handle much more complex data flow structures.


Piotr Zyśk

About Piotr Zyśk

Fullstack .Net/Angular developer in Atena since 2019. Previous 16 years spent on IT support for large contact center company, including development and maintenance of local infrastructure. After work I like to spend time on my hobbies, like photography, composing music on my PC or playing virtual racing games. You can also meet me biking in the nearby forests.

Leave a comment

Your email address will not be published. Required fields are marked *