Sending High Volume of Emails with Coldfusion
Asked Answered
M

1

6

This question could probably be related to doing anything high volume, but in this case I am trying to send emails.

I have setup the sending process in a new thread so user doesn't wait, and have overridden the request timeout to an hour.

The problem is that once the process get's up to about 2000 emails sent (looped over the below code about 2000 times) the server runs out of memory, stops responding, and needs a reboot.

Reading other topics on this, CF should be able to handle this volume of emails fine.

One thing I have considered is changing all object calls to straight DB Queries and using the cfmail tag to (I guess) remove all objects from being created and building up on reach request (which I guess is what is happening), but I'm not sure if that would make a difference, and really want to avoid that approach if possible. Something else I considered was splitting it across 3 or 4 seperate threads, but again, not sure if that would solve the problem.

Has anyone faced this problem, and what did you find worked to allow processing to continue without the ram slowly filling up and killing the server?

thread name="sendBroadcastEmail" rc="#rc#" prc="#prc#" filters="#rc.filters#" email="#email#" emailSignature="#emailSignature#"{
    createObject( "java", "coldfusion.tagext.lang.SettingTag" ).setRequestTimeout(javaCast( "double", 3600 ));

        //get profiles that it will be sent to
        var sendToProfiles = profileService.getWithFilters(rc.filters);

        var mailService = getPlugin("MailService");
        var emailSent = false;
        var sentCount = 0;
        var failedCount = 0;

        //send the email (and log in profile events)
        if (listFind(attributes.rc.email.action,'send')){

            for ( i=1; i<=arrayLen(sendToProfiles);i++){
                var profile = sendToProfiles[i];
                try{

                    if (len(trim(profile.getPrimaryEmail()))){

                        var emailBody = profile.processDynamicPlaceholders(attributes.rc.email.body);
                        var emailBody = attributes.emailSignature.getHeader() & emailBody & attributes.emailSignature.getFooter();

                        var sendEmail = mailService.newMail(
                             from = attributes.emailSignature.getEmailAddress(),
                             //to = profile.getPrimaryEmail(),
                             to = Application.settings.testemail,
                             subject = attributes.rc.email.subject,
                             body = emailBody,
                             type="html");

                             sendEmail.addMailParam(disposition='attachment', file=attributes.email.getAttachmentWithPath());
                             mailService.send(sendEmail);

                        //log profile event
                        profile.saveEvent(eventType = 3,
                                        title="Broadcast Email: #attributes.rc.email.subject#", 
                                        description="Broadcast Email Sent: Subject: <br> #attributes.rc.email.subject#",
                                        sentContent=emailBody,
                                        ref2=1);
                    sentCount++;
                    }
                }
                catch (any exception){
                    //log profile event
                    profile.saveEvent(eventType = 3,
                                    title="FAILED Broadcast Email", 
                                    description="<br>Subject: #attributes.email.subject#<br>This email should have been sent to this profile, but the attempted send failed.  The likely cause is a malformed email address.",
                                    sentContent=emailBody,
                                    ref2=0);
                    failedCount++;
                }

            }   
            emailSent = true;

        }


        //persist email object
        if (listFind(attributes.rc.email.action,'save')){
            email.setTstamp(attributes.prc.now);
            email.setSent(emailSent);
            email.setStatsSent(sentCount);
            email.save();
        }

    }//end thread   
Medusa answered 22/6, 2014 at 23:21 Comment(12)
Are you saying you're creating 2000 threads? If you're sending a single email it shouldn't take even close to a second for the code to run. Your issue could simply be that i is not scoped.Acolyte
Do any of the emails get sent?Acolyte
No, one thread, but looping over the send email process inside that thread 2000+ times, and sending 2000+ emails (see the for loop on line 15). When you say it may be that it is not scoped, what is not scoped? Thanks Matt!Medusa
Yes, about 2000 get sent fine, then process just slows down and server crashes.Medusa
Which version of ColdFusion? (pls update tagging accordingly)Keffiyeh
I do something similar and I've found that generating the emails (rather than sending) is the most resource-intensive part, so you could try doing it in timed batches. I set a batch limit of 1K emails, then sleep the thread for 2mins before sending the next batch.Partitive
Jeremy, thanks. That seems like a solid approach. I've been playing around all day with different approaches, such as splitting core processing into separate function to ensure memory is cleared at end of function call and adding all objects and vars to a struct, then doing structClear at end of each loop.. and more.. each gave slight improvements, but memory still running out at about 2,500 emails. Do you find letting the thread sleep has the same effect on releasing memory as if the thread ended? Does all the memory from each batch get cleared in the 2 min sleep? Thanks!Medusa
Yes it does seem to help regulate the memory usage, we were observing massive spikes before trying it, and it's easy to dial the sleep intervals and batch sizes up or down according to need.Partitive
When your server gets overwhelmed and stops responding, you don't have to re-boot it. You can simply re-start ColdFusion.Kravits
Related: #10193825Heist
Jeremy, I've found of general optimization has given improvements, but your suggestion of batching and sleeping the thread as a way of sending large volumes of emails without impacting server performance is what I was looking for here. Could you post your solution as an answer, with an example if possible, and I will flag as the answer. Thanks!Medusa
Sorry if this has already been mentioned, but are you using a Windows SMTP server in order to accomplish this? The company for which I work sends large quantities of email and we found Windows wholly inadequate for this task. We currently use postfix running on Linux for SMTP while still generating the emails on Windows. Part of the problem was that Windows SMTP could not send out emails quickly enough to prevent the spool folder from getting overwhelmed.Inesita
P
3

One approach would be to generate the emails in timed batches to spread the load evenly. The batch size and delay between batches can be adjusted to suit your environment.

    thread name="sendBroadcastEmail" rc="#rc#" prc="#prc#" filters="#rc.filters#" email="#email#" emailSignature="#emailSignature#"{
    createObject( "java", "coldfusion.tagext.lang.SettingTag" ).setRequestTimeout(javaCast( "double", 3600 ));

            // set thread to a lowish prority
            var currentThread = CreateObject( "java","java.lang.Thread" ).currentThread();
            var priority = currentThread.getPriority();
            currentThread.setPriority( 3 );

        //get profiles that it will be sent to
        var sendToProfiles = profileService.getWithFilters(rc.filters);

        var mailService = getPlugin("MailService");
        var emailSent = false;
        var sentCount = 0;
        var failedCount = 0;

        //send the email (and log in profile events)
        if (listFind(attributes.rc.email.action,'send')){

            var emailsPerBatch = 1000; // divide into batches, set size here
            var batchcount = Ceiling( ArrayLen( sendToProfiles ) / emailsPerBatch ); // number of batches
            var batchdelay = 120000; // set delay between batches (ms)
            // initialise first batch
            var firstitem = 1;
            var lastitem = emailsPerBatch;

            for( var batch=1; batch<=batchcount; batch++ ) {
                if( batch > 1 ){
                    // delay sending next batch and give way to other threads
                    currentThread.yield();
                    currentThread.sleep( batchdelay );
                }

            for ( var i=firstitem; i<=lastitem;i++ ){
                var profile = sendToProfiles[i];

                            // generate emails ...

            }


            // initialise next batch
            firstitem = lastitem++;
            lastitem += emailsPerBatch;
            if( lastitem > ArrayLen( sendToProfiles ) ) {
                // last batch
                lastitem = ArrayLen( sendToProfiles );
            }

            }
            emailSent = true;
        }

            currentThread.setPriority( priority ); // reset thread priority


    }//end thread
Partitive answered 24/6, 2014 at 8:45 Comment(2)
@Medusa I've adapted your example, obviously not tested so feel free to edit if there's a mistakePartitive
Thanks Jeremy, for anyone finding this in future, other things I did to improve things were: 1. Ensure correct scoping of variables; 2. put the processing inside the loop to an external function and call that (apparently this ensures memory is released after each loop); 3. replaced some object calls with direct queries to database. 1&2 game slight improvement.. 3 gave a big improvement. Biggest came from Jeremy's suggestion on sleeping the loop. JasonMedusa

© 2022 - 2024 — McMap. All rights reserved.