Chapter 5.  Debugging Reactive Extensions

Debugging Reactive Extensions (Rx) for .NET means something more than simply using the visual studio debugger. This chapter focuses on how to trace the executing operations against all the sequences that behave within our application and how to use diagnostic-oriented sequences that strongly improve developer debugging times. Here's a short list of arguments we're going to cover in this chapter:

  • Tracing sequences
  • Inspecting sequences
  • Exception handling
  • Playing with sequences

Tracing sequences

The most widely used diagnostic solution is application tracing; in a few words, it is the art of logging the application execution flow with emphasis on the event type and its description.

This is the same in reactive programming, thus, we have the ability to use specific sequences that mark messages with additional metadata or log out messages to analyze (or dump for further usages) the sequence flow in addition to the usual tracing tools available within the .NET world.

More than a simple reactive way of tracing, we can design specific diagnostic sequence flows with related filtering or manipulation, and the ability to use any other reactive operator to help us isolate interesting diagnostic information.

Materialize

The Materialize extension method produces a new sequence for diagnostic purposes that flows messages containing additional metadata information about the messages flowing in the sourcing sequence.

The great advantage of materializing a sequence is the ability to know the message's type (OnNext, OnComplete, or OnError), inspecting its sourcing data or exceptions without having to fit within the original sequence chain. This is  actually, a powerful feature. However, this has a cost in resource needs. This is why we cannot materialize every sequence just to have access to related metadata until we definitely need it.

In other words, Materialize is something similar to what Reflection is for normal types, giving us the ability to write a code to read the object's property by simply looping across all those available because we don't need to write code in a static way. In fact, with reflection, there is huge additional resource cost, and with reflection, we definitely need it often.

You have just another use case to look at before you dive into code: think about the ability to handle any reactive message at a single point by simply analyzing the message metadata and routing to the required concrete handler. Keep the resource usage in mind before you think that this is a great feature at all; although in some cases, this may be definitely a great feature.

The usage is definitely simple. Here's an example:

//a simple sequence of DateTime 
var sequence1 = new Subject<DateTime>(); 
 
//a console observer 
sequence1.Subscribe(x => Console.WriteLine("{0}", x)); 
 
//a tracing sequence 
//of materialized notifications 
IObservable<Notification<DateTime>> tracingSequence = sequence1.Materialize(); 
tracingSequence.Subscribe(notification => 
{ 
    //this represents the operation 
    Console.WriteLine("Operation: {0}", notification.Kind); 
 
    //has a value 
    if (notification.HasValue) 
        Console.WriteLine("Value: {0}", notification.Value); 
 
    //has an exception 
    else if (notification.Exception != null) 
        Console.WriteLine("Exception: {0}", notification.Exception); 
}); 
 
//flows a new value 
sequence1.OnNext(DateTime.Now); 
 
//flows the oncomplete message 
sequence1.OnCompleted(); 
 
Console.ReadLine(); 

The preceding example shows the basic usage of the Materialize operator. This gives us a new sequence of the Notification<T> messages, where T is the original message type. These messages will contain full information/metadata about the flowing message. The Notification<T> method contains everything we need to trace out our messages or dump message data by reading its Kind, Value, or Exception properties as visible in the preceding example.

Regarding performance, the suggestion is to avoid the massive usage of materialized sequences because these are very expensive considering the overall resource usage.

Tip

By using the Sample or the Throttle operator together with the Materialize operator, we can trace messages at a slower rate. This may be an interesting compromise when we want to trace without destroying the overall performance of our application. Their description is available in the Transforming operators or Combining operators sections of Chapter 3, Reactive Extension Programming .

Dematerialize

Once we have a materialized sequence of the Notification<T> messages, if we need to produce a value sequence, we can use the Dematerialize extension method.

Obviously, recreating the original value sequence from a materialized sequence is odd, but it is sometimes necessary. Here's an example:

//a dematerialized sequence 
var valueSequence = tracingSequence.Dematerialize(); 
//a console observer 
valueSequence.Subscribe(x => Console.WriteLine("D: {0}", x)); 

Regarding performance, the usage of the Materialize/Dematerialize sequences produces some visible overheads. Other than when debugging, the suggestion is to avoid the usage of similar sequences.

A similar feature is available when debugging with Visual Studio under the name IntelliTrace. With this feature, Visual Studio logs all that is happening in our executing code with the ability to replay specific code portions or verify already handled exceptions that don't bubble up in the debugger. The same feature is obviously available when programming with Rx, but Materialize/Dematerialize gives a coarse-grained ability to reproduce the message flow of a production environment without the cost of having a debugger online, as IntelliTrace needs.

TimeInterval

Although we have already seen this sequence in Chapter 4, Observable Sequence Programming , kindly consider that its usage in debugging is visibly useful. Here's a short example.

Let's assume that we're writing an application that reads data from an IO port as a serial or TCP port. We want the messages to always come on time. In other words, we cannot accept delays of more than a specific duration because we're writing a real-time application.

Tip

Bear in mind that a real-time application must have a deterministic execution time in addition to fast/immediate reactions to events/input.

We can use the TimeInterval operator to skip messages that are outside our timing policy and produce a log of these messages for further analysis or simply as a waste sequence.

Here's an example:

var r = new Random(DateTime.Now.GetHashCode());
 
//an infinite message source 
var source = Observable.Interval(TimeSpan.FromSeconds(1)) 
    .Select(i => 
    { 
        //let's add some random delay 
        Thread.Sleep(r.Next(100, 1000)); 
 
        return DateTime.Now; 
    }); 
 
//the timing tracing sequence 
var timingsTracingSequence = source.TimeInterval(); 
 
//valid messages 
var validMessagesSequence = timingsTracingSequence.Where(x => x.Interval.TotalMilliseconds <= 1200); 
//var exceeding messages 
var exceedingMessagesSequence = timingsTracingSequence.Where(x => x.Interval.TotalMilliseconds > 1200); 
 
//some console output 
validMessagesSequence.Subscribe(x => Console.WriteLine(x.Value)); 
exceedingMessagesSequence.Subscribe(x => Console.WriteLine("Exceeding timing limits for: {0} ({1:N0}ms)", x.Value, x.Interval.TotalMilliseconds)); 
 
Console.ReadLine(); 

Do

The Do extension method is very similar to the Materialize one with a single great difference. The Materialize method produces a new sequence of Notification<T> that will add message-related metadata information to each sourcing message, while the Do method will return the same sourcing sequence, registering and passing each message of each type to the proper C# handler (Action or Action<Exception>).

In other words, the Do method gives us the ability to specify an external message handler in a way similar to CLR event handling with the usage of delegates (Action, Action<Exception>).

Although this option may seem more comfortable to any experienced state-driven programmer, here, in any reactive programming project, it may bring more issues than it resolves. This is because of the different programming approach that is very useful for diagnostics needs and contemporarily very dangerous in production time execution because it may lead the developer into an undesired Action at a distance anti-pattern.

Here's a complete example:

static void Main(string[] args)
{ 
    var source = Observable.Interval(TimeSpan.FromSeconds(1)) 
        .Select(x => DateTime.Now) 
        .Take(5) 
        .Select(x => 
        { 
            if (x.Second % 10 == 0) 
                throw new ArgumentException(); 
 
            return x; 
        }) 
        .Do(OnNext, OnError, OnCompleted) 
                 
        .Catch(Observable.Empty<DateTime>()); 
 
    //starts the source 
    source.Subscribe(); 
 
    Console.ReadLine(); 
} 
 
private static void OnError(Exception ex) 
{ 
    Console.WriteLine("-> {0}", ex.Message); 
} 
 
private static void OnCompleted() 
{ 
    Console.WriteLine("-> END"); 
} 
 
private static void OnNext(DateTime obj) 
{ 
    Console.WriteLine("-> {0}", obj); 
} 

The example is very simple: we have a sourcing sequence of no more than 5 messages. In addition, we will raise an error when the timestamp's seconds are divisible by 10.

The Do method expects up to three handlers for the three message types.

Kindly consider, anything that executes within the handlers, actually executes within the sequence change pipeline in a synchronous way.

Tip

A side effect happens anytime we produce an action at a distance by editing some state or by executing some logic outside our function. In a reactive world, our function is our sequence chain. We can do anything we want within our chain, but when we interact outside the chain within a single chain node or message transformation/filtering node, we are causing a side effect.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.58.194