1

I have a node js service that consumes messages from Kafka and processes it through various steps of transformation logic. During the processing, services use Redis and mongo for storage and caching purposes. In the end, it sends the transformed message to another destination via UDP packets.

On startup, it starts consuming message from Kafka after a while, it crashes down with the unhandled error: ERR_CANNOT_SEND unable to send data(see below picture). restarting the application resolves the issue temporarily. I initially thought it might have to do with the forwarding through UDP sockets, but the forwarding destinations are reachable from the consumer!

I'd appreciate any help here. I'm kinda stuck here.

enter image description here

Consumer code:

const readFromKafka =  ({host, topic, source}, transformationService) => {
    const logger = createChildLogger(`kafka-consumer-${topic}`);
    const options = {
        // connect directly to kafka broker (instantiates a KafkaClient)
        kafkaHost: host,
        groupId: `${topic}-group`,
        protocol: ['roundrobin'], // and so on the  other kafka config.
    };

    logger.info(`starting kafka consumer on ${host} for ${topic}`);
    const consumer = new ConsumerGroup(options, [topic]);
    consumer.on('error', (err) => logger.error(err));
    consumer.on('message', async ({value, offset}) => {
        logger.info(`recieved ${topic}`, value);
        if (value) {
            const final = await transformationService([
                JSON.parse(Buffer.from(value, 'binary').toString()),
            ]);
            logger.info('Message recieved', {instanceID: final[0].instanceId, trace: final[1]});
         
        } else {
            logger.error(`invalid message: ${topic} ${value}`);
        }
        return;
    });
    consumer.on('rebalanced', () => {
        logger.info('cosumer is rebalancing');
    });
    return consumer;
};

Consumer Service startup and error handling code:

//init is the async function used to initialise the cache and other config and components.
const init = async() =>{
    //initialize cache, configs.
}

//startConsumer is the async function that connects to Kafka,
//and add a callback for the onMessage listener which processes the message through the transformation service.
const startConsumer = async ({ ...config}) => {
    //calls to fetch info like topic, transformationService etc.
   //readFromKafka function defn pasted above
    readFromKafka( {topicConfig}, transformationService);
};

init()
    .then(startConsumer)
    .catch((err) => {
        logger.error(err);
    });

Forwarding code through UDP sockets. Following code throws the unhandled error intermittently as this seemed to work for the first few thousands of messages, and then suddenly it crashes

const udpSender = (msg, destinations) => {
    return Object.values(destinations)
        .map(({id, host, port}) => {
            return new Promise((resolve) => {
                dgram.createSocket('udp4').send(msg, 0, msg.length, port, host, (err) => {
                    resolve({
                        id,
                        timestamp: Date.now(),
                        logs: err || 'Sent succesfully',
                    });
                });
            });
        });
};
Pancham Kumar
  • 145
  • 1
  • 10
  • Total shot in the dark here, but is it this an issue of race conditions? See this github comment for what I'm thinking: https://github.com/nodejs/help/issues/2484#issuecomment-590944091 I don't know enough about socket initialization but it sounds as if trying to send on the socket before you've ensured the socket is listening is at least causing your real issue to be hidden. – user3781737 Apr 05 '21 at 15:11
  • I'm not sure that comment helps my case or not, as I'm opening a separate socket for each destination for sending the msg to the destination, so it is possible that at a certain instant, there might be about 100 separate sockets created, but each socket will be bound to a random port (as I've not called `socket.bind()`). – Pancham Kumar Apr 05 '21 at 17:59
  • Besides, an error callback should be getting called which in turns resolve the promise with error logging., if I'm correct? – Pancham Kumar Apr 06 '21 at 07:29
  • A larger stack trace as text would be more helpful, but if your error callback is working correctly than that should be what’s logging the `SOCKET_CANNOT_SEND` you’re seeing in the stack trace at all. What I’m saying is, if I’m reading the comment I linked correctly, when you call `socket.send` on an unbound socket, Node will implicitly handle binding for you. However, if that bind/send errors, for whatever reason internally the implicit binding handler will obfuscate the real error behind the `SOCKET_CANNOT_SEND` error. Again, if I’m reading the comment correctly. – user3781737 Apr 06 '21 at 18:19
  • You may instead then want to change your UDP code to something like `var mySock = dgram.createSocket('udp4'); mySock.bind(port, host, () => { mySock.send(msg, 0, msg.length, (err) => { resolve({ id, timestamp: Date.now(), logs: err || 'Sent succesfully', }); }); });` which may change your error message into something more helpful and not so ambiguous, is what I’m hoping – user3781737 Apr 06 '21 at 18:27
  • Unfortunately, the logs snapshot that I've added is all that gets printed on the server console. Anyways, I understand your point and will test it out and update it. THanks – Pancham Kumar Apr 06 '21 at 18:29
  • Correction to the above as I misunderstood the client and server roles. It should be: `var mySock = dgram.createSocket('udp4'); mySock.bind(0, "0.0.0.0", () => { mySock.send(msg, 0, msg.length, port, host, (err) => { resolve({ id, timestamp: Date.now(), logs: err || 'Sent succesfully' }); }); });` Not sure it'll actually affect the error message, but it should at least result in the same code. – user3781737 Apr 06 '21 at 18:56
  • Scratch everything prior, as after properly re-reading your question, I'm now able to reproduce this issue after some research and changing my version of node. You say this sends thousands of messages... are you creating new socket each time you send a batch of messages? And are you making sure to clean up and close your sockets after they're no longer necessary? Because I'm able to reproduce this error in node v10 by opening a ridiculous number of (like 2**15) sockets concurrently. In later versions of node, you get an error that more clearly outlines the lack of available ports/sockets. – user3781737 Apr 06 '21 at 21:12
  • I'm calling `dgram.createSocket('udp4')` for every msg destinations, If this call opens a socket every time then yes, I'm opening a new socket every time. – Pancham Kumar Apr 07 '21 at 05:34

1 Answers1

1

Based on our comment exchange, I believe the issue is just that you're running out of resources.

Throughout the lifetime of your app, every time you send a message you open up a brand new socket. However, you're not doing any cleanup after sending that message, and so that socket stays open indefinitely. Your open sockets then continue to pile up, consuming resources, until you eventually run out of... something. Perhaps memory, perhaps ports, perhaps something else, but ultimately your app crashes.

Luckily, the solution isn't too convoluted: just reuse existing sockets. In fact, you can just reuse one socket for the entirety of the application if you wanted, as internally socket.send handles queueing for you, so no need to do any smart hand-offs. However, if you wanted a little more concurrency, here's a quick implementation of a round-robin queue where we've created a pool of 10 sockets in advance which we just grab from whenever we want to send a message:

const MAX_CONCURRENT_SOCKETS = 10;

var rrIndex = 0;

const rrSocketPool = (() => {
    var arr = [];
    for (let i = 0; i < MAX_CONCURRENT_SOCKETS; i++) {
        let sock = dgram.createSocket('udp4');
        arr.push(sock);
    }
    return arr;
})();

const udpSender = (msg, destinations) => {
    return Object.values(destinations)
        .map(({ id, host, port }) => {
            return new Promise((resolve) => {
                var sock = rrSocketPool[rrIndex];
                rrIndex = (rrIndex + 1) % MAX_CONCURRENT_SOCKETS;
                
                sock.send(msg, 0, msg.length, port, host, (err) => {
                    resolve({
                        id,
                        timestamp: Date.now(),
                        logs: err || 'Sent succesfully',
                    });
                });
            });
        });
};

Be aware that this implementation is still naïve for a few reasons, mostly because there's still no error handling on the sockets themselves, only on their .send method. You should look at the docs for more info about catching events such as error events, especially if this is a production server that's supposed to run indefinitely, but basically the error-handling you've put inside your .send callback will only work... if an error occurs in a call to .send. If between sending messages, while your sockets are idle, some system-level error outside of your control occurs and causes your sockets to break, your socket may then emit an error event, which will go unhandled (like what's happening in your current implementation, with the intermittent errors that you see prior to the fatal one). At that point they may now be permanently unusable, meaning they should be replaced/reinstated or otherwise dealt with (or alternatively, just force the app to restart and call it a day, like I do :-) ).

user3781737
  • 932
  • 7
  • 17