Sending messages inside callback functions

I have been working on a custom node for what seems like an eternity (worse it is yet another scheduling node :crazy_face:. I have it close to where I want it but have come across one interesting problem.

The node uses a callback function to fire time based events. Inside the callback function I don't do anything special, just send on the message with node.send(msg). However in the most basic test configuration (scheduling one event after another), only the LAST firing of the callback sends the msg.

I know the previous fired events contain the msg because it is output to the console correctly at the correct time.

I am hoping I have left something simple out like node cleanup etc, but any clues as to where to start looking would be appreciated.

Thanks
Bob

Hey Bob,
that should work what you are doing. Perhaps you should share a code snippet ...
Here you can see that I do something similar in my node-red-contrib-msg-resend node. For example:

var yourTimerId = setInterval(function() {
   node.send( {payload: "test"});
}, 5000);

Make sure that you create a NEW message every time the callback is triggered! Because when you resend the same message over and over again, you will get weird results (as soon as the next nodes in the flow start changing the content of that single message over and over again)...

And don't forget to stop your timer, as soon as your node is cleaned up:

node.on("close", function() {
    clearInterval(yourTimerId );
}

Otherwise you will end up with multiple timers running in parallel ...
Bart

Thanks for the tips Bart. Unfortunately I'm still at a loss to understand where I have made the mistake, or perhaps I am misunderstanding something fundamental regarding sending messages.

A code snippet probably won't help you much but it will clearly demonstrate the issue:

                    var j = schedule.scheduleJob(jb, date, function () {

                        var id_status = context.get('cancel-date');

                        console.log('single date - ', jb);
                        console.log('message = ', msg);
                        send(msg);
                    });

The above is a call made to the node-schedule node - https://github.com/node-schedule/node-schedule.

What I don't understand is the output from the console is always correct. There is a msg displaying the correct msg_id and info when the event is fired. There is just no output at the debug node from send(msg).

Maybe a silly question, but you just call send(msg);. Where/how is send defined in your code?

The send function is available on the enclosing function that defines your node. So unless you do something like let send = this.send; beforehand, that code will not work, because send is not in the closure of your callback function.

Thinking even further, just calling send() may mess up the send function itself, because this will not be bound to the node function. You'd have to do something like

let node = this;

var j = schedule.scheduleJob(jb, date, function () {

                        var id_status = context.get('cancel-date');

                        console.log('single date - ', jb);
                        console.log('message = ', msg);
                        node.send(msg);
                    });

1 Like

No silly questions, except perhaps mine.

I "upgraded" the node to be compliant with NR 1.x via https://nodered.org/docs/creating-nodes/node-js#sending-messages.

Prior to that I was using node.send(msg) with same results.

Yes, but the new Send API only applies to nodes with an input using the on('input') event.

In your case you are generating the messages yourself I suspect, so you would have to use node.send() anyway.

I would need to see more of your node's code to find the problem. Is it shared on Github?

Not as yet no. But if you are willing to take a look I will gladly get that done.

I am out of ideas and js knowledge :grimacing:

Working from your example, your code should look something like the following. Unless I completely misunderstand what your node is supposed to do. :nerd_face:

function MyNode(config) {
    RED.nodes.createNode(this, config);

    const node = this;

    var j = schedule.scheduleJob(jb, date, function () {

        var id_status = context.get('cancel-date');

        let msg = {payload: 'foo'};

        console.log('single date - ', jb);
        console.log('message = ', msg);
        node.send(msg);
    });
}

Another question... where do you get the message object from? Or are you trying to schedule from within a on('input') event??? That would be a completely different case.

Ok, so it is a fairly basic daily scheduler node using the node-schedule node. I am aware that that particular nodejs node does have a few bugs but se la vì.

Either the schedule can be passed in, or created in the node's editor. I then add msg.alarm_date.xxx which forms the scheduled date for the callback function to fire.

Shall I just post the code here?

Yes, that would be the easiest way. I have a suspicion what might be the problem, but I would need to see the code for that. :nerd_face:

Remember this is primarily a self-learning exercise, as well as providing something which I would find useful, should it ever work properly :grin:

Debugging code of other people is part of the learning exercise. As most people with a computer science background, we love to solve problems. :grin:

1 Like
var schedule = require('node-schedule');

module.exports = function (RED) {
    "use strict";
    function DailySchedulerNode(config) {
        RED.nodes.createNode(this, config);
        var node = this;
        var context = this.context().flow;

        this.on('input', function (msg) {

            this.hour = config.hour;
            this.month = config.month;
            this.day = config.day;
            this.minute = config.minute;
            this.recur = config.recur;
            this.year = config.year;
            this.desc = config.desc;
            this.early = config.early;
            this.idx = config.idx;

            var editor, inputmsg = false;

            if (msg.hasOwnProperty("alarm_date") && msg.alarm_date.hasOwnProperty("hours") && msg.alarm_date.hasOwnProperty("years") &&
                msg.alarm_date.hasOwnProperty("months") && msg.alarm_date.hasOwnProperty("date") && msg.alarm_date.hasOwnProperty("desc")) {

                // ***************************************************
                // INPUT FROM MSG.ALARM_DATE

                this.status({ fill: "green", shape: "dot", text: "ok" });

                inputmsg = true;

                console.log('INCOMING MSG');

            } else if (node.month != '' && node.hour != '' && node.minute != '' && node.day != '' && node.desc != '') {

                // ***************************************************
                // INPUT FROM EDITOR

                console.log('The EDITOR worked.');

                this.status({ fill: "green", shape: "dot", text: "ok" });

                editor = true;

                msg.alarm_date = {};

                msg.alarm_date.years = Number(node.year);
                msg.alarm_date.months = Number(node.month);
                msg.alarm_date.date = Number(node.day);
                msg.alarm_date.hours = Number(node.hour);
                msg.alarm_date.minutes = Number(node.minute);
                msg.alarm_date.early = Number(node.early);
                msg.alarm_date.idx = node.idx;
                msg.alarm_date.desc = node.desc;
                msg.alarm_date.recur = node.recur;

            } else {
                msg.error = 'Date configuration error in Daily Scheduler node';
                console.log('Date configuration error in Daily Scheduler node');

                this.status({ fill: "red", shape: "dot", text: "config error" });
            }

            if (editor || inputmsg) {

                var jobname;

                if (msg.hasOwnProperty("alarm_date") && msg.alarm_date.hasOwnProperty("idx") && msg.alarm_date.idx != '') {
                    jobname = msg.alarm_date.idx;
                } else { jobname = msg._msgid; msg.alarm_date.idx = jobname; }

                if (msg.hasOwnProperty("alarm_date") && msg.alarm_date.hasOwnProperty("hours") && msg.alarm_date.hours != '') { }
                else { msg.alarm_date.hours = 0; }


                let jb = String(jobname);

                console.log(jb);

                if (msg.alarm_date.early > 0 || msg.alarm_date.recur) { // process for early reminders OR monthly recurring

                    var rule = new schedule.RecurrenceRule();
                    var earlyrem = msg.alarm_date.early + 1;

                    // change from minutes to dates *****************************....
                    var currentdate = new Date(msg.alarm_date.years, msg.alarm_date.months,
                        msg.alarm_date.date, msg.alarm_date.hours,
                        msg.alarm_date.minutes, 0, 0);

                    var earlydate = new Date(msg.alarm_date.years, msg.alarm_date.months,
                        msg.alarm_date.date, msg.alarm_date.hours,
                        msg.alarm_date.minutes - msg.alarm_date.early - 1, 0, 0);


                    if (msg.alarm_date.recur) { rule.month = [0, new schedule.Range(0, 11)]; } // monthly recurring

                    rule.date = msg.alarm_date.date;

                    rule.hour = msg.alarm_date.hours;

                    var j = schedule.scheduleJob(jb, { start: earlydate, end: currentdate, rule }, function () {

                        var id_status = context.get('cancel-date');

                        if (id_status == jb) {
                            schedule.scheduledJobs[jb].cancel();
                            console.log('CANCELLED ... early or recurring - ', jb);
                        } else {
                            earlyrem = earlyrem - 1;
                            console.log('early or recurring - ', jb);
                            console.log('Left - ', earlyrem);
                            msg.alarm_date.earlyrem = earlyrem;
                            node.send(msg);
                        }
                    });

                } else {  // change to standard single date event

                    console.log('SINGLE DATE TRIGGER .....');

                    var date = new Date(msg.alarm_date.years, msg.alarm_date.months, msg.alarm_date.date,
                        msg.alarm_date.hours, msg.alarm_date.minutes, 0);


                    var j = schedule.scheduleJob(jb, date, function () {

                        /**************** MISSING MESSAGES */
                        var id_status = context.get('cancel-date');

                        console.log('single date - ', jb);
                        console.log('message = ', msg);
                        node.send(msg);

                    });

                }

            }

            node.send(msg);

        });

    }

    RED.nodes.registerType("daily-scheduler", DailySchedulerNode);
}

Okay... so it isn't the thing I suspected regarding the binding of this.

But I see that you send the same message instantly and reuse it in your callbacks. By resending the same message reference you are in for trouble. It may work in simple cases, but since your are setting lots of message properties and rely on them for your scheduling, you should clone the message using RED.util.cloneMessage(msg);. Otherwise the message will be modified by subsequent nodes in the meantime until your callback gets fired.

I would create the clone early in your on('input') function and use that clone in the scheduler callback.
The original message can then be sent instantly (the last node.send(msg); in your code)

I did actually try cloning but did it in the callback. I shall try early as you suggest. Thanks.

If you clone it within the callback, the original message is likely to be modified already.

So the general order should be

  1. clone original
  2. register/create callbacks, use cloned message within
  3. send original message

No luck with cloning unfortunately.

So the test scenario is (using the nodes editor), set a schedule and deploy. Set another schedule and deploy. The first schedule msg gets sent to debug, the second (third etc) does not.

If you are still interested in solving the mystery I will stick my node up on github.

Cheers

By debug you mean a debug node connected to your node?

One other thing, do you get the console.log() output from your scheduler callbacks? I'm curious if the callback gets fired at all :thinking:

Yes indeed. My first thought was that the events were just not being fired, but the console logs show they are, with the correct msg id and data.

And yes, with a debug node at the end of my node.

Another piece to the puzzle. The order in which the events are deployed (as per my testing scenario) affects which msg gets sent to debug (ie last deployed event will trigger debug only).