Table of Contents

Module Troubleshooting Guide

This guide helps you diagnose and resolve common issues when developing and testing custom modules for the Crosser Node.

Debugging Strategies

1. Start with Local Unit Tests

Always begin troubleshooting with local unit tests using the TestHelper package:

  • Isolate the problem to your module logic
  • Test with known input data
  • Verify expected outputs
  • Check error handling

See testing for detailed guidance.

2. Use Debug Module in Flows

When testing in remote sessions you can include a Debug module.

  • Add it to capture both input and output
  • Monitor message flow and data types in real-time

3. Add Comprehensive Logging

You can use the built-in static ILog instance to log messages at different levels. See logging for more information on logging.

Common Issues and Solutions

Module Failing to Start

Symptoms:

  • Module status shows "Error" in remote sessions
  • Module doesn't process any messages
  • Startup exceptions in logs

Common Causes:

  1. Initialization Issues Check if there might be issues in the Initialization code in the module.

  2. Resource Access Issues

    • Verify credentials are properly configured
    • Check network connectivity for external resources
    • Validate file paths and permissions

No Message Processing

Symptoms:

  • Module starts successfully
  • No output messages produced
  • Debug Module shows no activity

Debugging Steps:

  1. Check Input Messages
    Is the Source Property configured correctly? Add logging to confirm messages are received and have the expected format:

    protected override async Task MessageReceived(IFlowMessage message)
    {
         Logger?.Information("Received message: {@Message}", message);
    
         // Log message properties
         foreach (var prop in message.GetProperties())
         {
              Logger?.Debug("Property {Key}: {Value}", prop.Key, prop.Value);
         }
    
         // Your processing logic here
    }
    
  2. Verify Message Routing

    • Check flow connections in Flow Studio
    • Ensure your module is connected to data sources
    • Verify upstream modules are producing messages
  3. Check Error Handling Make sure exceptions are handled correctly.

    protected override async Task MessageReceived(IFlowMessage message)
    {
        try
        {
            await ProcessMessage(message);
        }
        catch (Exception ex)
        {
             var error = $"The incoming message could not be processed {ex.Message}";
             SetStatus(Status.Warning, error); // Or SetError if so critical that the flow should stop/restart
             message.SetError(error);
        }
        finally
        {
            await Next(message);
        }
    }
    

Performance Issues

Symptoms:

  • Slow message processing
  • Growing message queues
  • High CPU or memory usage

Optimization Strategies:

  1. Async/Await Patterns

    // ❌ Blocking - blocks the thread
    protected override async Task MessageReceived(IFlowMessage message)
    {
        var result = SomeSlowOperation(); // Synchronous call
        await Send(result);
    }
    
    // ✅ Non-blocking - proper async
    protected override async Task MessageReceived(IFlowMessage message)
    {
        var result = await SomeSlowOperationAsync(); // Asynchronous call
        await Send(result);
    }
    
  2. Resource Management

    public class MyModule : FlowModule<MyModuleSettings>, IDisposable
    {
        private readonly HttpClient httpClient;
    
    // ❌ Recreates instance for every message
    protected override async Task MessageReceived(IFlowMessage message)
    {
        this.httpClient = new HttpClient(); // Creates a new instance for every message - bad!
        await this.httpClient.GetAsync("http://example.com");
    }
    
    }
       // ✅ One time initialization
    public override async Task<IError?> Initialize()
    {
        this.httpClient = new HttpClient(); // Create once
        return await base.Initialize();
    }
    
     protected override async Task MessageReceived(IFlowMessage message)
    {
        await this.httpClient.GetAsync("http://example.com");
    }
    
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.httpClient?.Dispose(); // Dispose when module is disposed
        }
        base.Dispose(disposing);
    }
    
    

State Management Issues

Improper state management can lead to unpredictable behavior, especially when multiple instances of the same module exist in a flow or when using timers.

Shared Static State Between Module Instances

Problem: Using static variables causes multiple instances of the same module to share state, leading to unexpected behavior.

// ❌ WRONG - Static variables are shared across all instances
public class CounterModule : FlowModule<CounterModuleSettings>
{
    private static int counter = 0; // This is shared across ALL instances!
    
    protected override async Task MessageReceived(IFlowMessage message)
    {
        counter++; // All instances increment the same counter
        var result = new FlowMessage();
        result.SetValue("count", counter);
        await Send(result);
    }
}

Issue: If you have two CounterModule instances in the same flow, they will share the same counter variable, causing unpredictable counting behavior.

Solution: Use instance variables instead:

// ✅ CORRECT - Each instance has its own state
public class CounterModule : FlowModule<CounterModuleSettings>
{
    private int counter = 0; // Each instance has its own counter
    
    protected override async Task MessageReceived(IFlowMessage message)
    {
        this.counter++; // Only this instance's counter is incremented
        var result = new FlowMessage();
        result.SetValue("count", this.counter);
        await Send(result);
    }
}

Best Practices for State Management

  1. Avoid Static Variables: Always use instance variables unless you specifically need shared state across all module instances in a flow.

  2. Use Thread-Safe Collections: When using timers or background tasks, protect shared state with locks or use concurrent collections:

    private readonly ConcurrentQueue<IFlowMessage> messageQueue = new();
    
  3. Separate Concerns: Use different variables for different purposes (input storage, processing state, output generation).

  4. Clean Up Resources: Always dispose of timers and other resources properly.

  5. Test with Multiple Instances: When testing your module, try using multiple instances in the same flow to verify they don't interfere with each other.

Advanced Debugging

Performance Profiling

Use profiling tools to identify bottlenecks:

  1. Built-in Metrics:

    • Monitor processing times in logs
    • Track message queue sizes
    • Watch memory usage patterns
  2. Custom Metrics:

    private readonly Stopwatch processingTimer = new();
    
    protected override async Task MessageReceived(IFlowMessage message)
    {
        this.processingTimer.Restart();
    
        try
        {
            await ProcessMessage(message);
        }
        finally
        {
            this.processingTimer.Stop();
            Logger?.Debug("Processing took {ElapsedMs}ms", this.processingTimer.ElapsedMilliseconds);
        }
    }
    

Memory Leak Detection

Watch for common memory leak patterns:

  1. Event Handler Leaks:

    // ❌ Can cause memory leaks
    public MyModule(ILog logger) : base(FlowModuleType.Function)
    {
        SomeService.OnEvent += HandleEvent; // Never unsubscribed
    }
    
    // ✅ Proper cleanup
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SomeService.OnEvent -= HandleEvent;
        }
        base.Dispose(disposing);
    }
    
  2. Collection Growth:

    // ❌ Unbounded collection growth
    private readonly List<ProcessedData> allProcessedData = new();
    
    // ✅ Bounded collection with cleanup
    private readonly Queue<ProcessedData> recentData = new();
    private const int MAX_ITEMS = 1000;
    
    private void AddProcessedData(ProcessedData data)
    {
        this.recentData.Enqueue(data);
        while (this.recentData.Count > MAX_ITEMS)
        {
            this.recentData.Dequeue();
        }
    }
    

Testing Checklist

Use this checklist to systematically test your modules:

Unit Testing

  • [ ] Module instantiates correctly
  • [ ] Handles valid input data
  • [ ] Produces expected output format
  • [ ] Handles null/empty input gracefully
  • [ ] Throws appropriate exceptions for invalid input
  • [ ] Resource cleanup works properly

Integration Testing

  • [ ] Module starts in Node environment
  • [ ] Processes messages from upstream modules
  • [ ] Outputs connect properly to downstream modules
  • [ ] Performance is acceptable under normal load
  • [ ] Logging provides useful information
  • [ ] Error conditions are handled appropriately

Edge Case Testing

  • [ ] Very large messages
  • [ ] High message frequency
  • [ ] Network connectivity issues (if applicable)
  • [ ] Node restart/reconnection
  • [ ] Memory pressure conditions
  • [ ] Long-running stability

Getting Help

If you're still experiencing issues:

  1. Check the Logs: Node logs contain detailed error information
  2. Test Isolation: Use unit tests to isolate the problem
  3. Simplify: Create a minimal reproduction case
  4. Documentation: Review the SDK documentation and examples
  5. Community: Reach out to the Crosser community for support

Remember: good logging is your best friend when troubleshooting module issues!