Skip to content

Introduce a way to interrupt `diagnostics_channel` flow

Rodrigo Muino Tomonari requested to merge github/fork/simon-id/interrupt_dc into main

This PR introduces a way to interrupt the flow of diagnostics_channel.

Problem

Sometimes, a subscriber to a channel will want to stop further execution of the event. In a usual context this would be done by throwing an exception. However, diagnostics_channel has a mecanism to catch any exception coming from subscribers, and redirect it to the process event uncaughtException. This makes sense because we don't want a publisher to crash only because a subscriber is doing something wrong. But a way to circumvent this catch-all is necessary for some use cases.

Use cases

An example of a very real use case would be to have a monitoring subscriber that would collect metrics and monitor the health of the app, and could decide to interrupt an action if it exceeds certain limits, contains invalid or malicious data, etc...

Another possibility is that a user would want to not go forward with a certain action if it is not succesfully consummed by a subscriber.

The general idea is that we should let the user decide where an exception thrown inside a subscriber should go, by having a safe default (ie: uncaughtException) and an explicit bypass (ie: passed to the publisher).

Solution

The proposed mecanism would rethrow a subscriber exception if it is an instance of DCInterruptError (taking suggestions for a better name), not call the uncaughtException event, and not execute the subsequent subscribers.

The public API is very simple (more details available in doc/api/diagnostics_channel.md

const dc = require('diagnostics_channel');

const channel = dc.channel('test');

channel.subscribe((data) => {
  throw new dc.DCInterruptError('Subscriber Interruption');
});

try {
  channel.publish({ foo: 'bar' });
} catch (err) {
  // DCInterruptError: Subscriber Interruption
}

Here is an alternative approach to the public API addition (courtesy of @Qard), that I could implement instead, if requested:

/////////////////////////////
// diagnostics_channel.js
// export an `interrupt` function that adds a symbol to a provided error object
const SymbolInterrupt = Symbol('DCInterrupt');

module.exports.interrupt = function interrupt (err) {
  err[SymbolInterrupt] = true;
  return err;
};

////////////////////////////
// app.js
channel.subscribe((data) => {
  throw dc.interrupt(new Error('Subscriber Interruption'));
});

////////////////////////////
// diagnostics_channel.js:ActiveChannel.publish()
// check if error contains the special symbol
try {
  onMessage(data);
} catch (err) {
  if (err[SymbolInterrupt]) throw err;
  // etc...
}

Since the diagnostics_channel module is still in experimental status, and early in its ecosystem adoption, I think it's a good idea to iterate on the idea now more than later.

Merge request reports

Loading