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   
Jason
  • 1,957
  • 2
  • 20
  • 34
  • 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. – Matt Busche Jun 23 '14 at 00:45
  • Do any of the emails get sent? – Matt Busche Jun 23 '14 at 00:46
  • 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! – Jason Jun 23 '14 at 00:50
  • Yes, about 2000 get sent fine, then process just slows down and server crashes. – Jason Jun 23 '14 at 00:50
  • Which version of ColdFusion? (pls update tagging accordingly) – Adam Cameron Jun 23 '14 at 06:03
  • 3
    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. – Jeremy Halliwell Jun 23 '14 at 07:39
  • 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! – Jason Jun 23 '14 at 10:46
  • 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. – Jeremy Halliwell Jun 23 '14 at 11:03
  • When your server gets overwhelmed and stops responding, you don't have to re-boot it. You can simply re-start ColdFusion. – Dan Bracuk Jun 23 '14 at 12:44
  • Related: http://stackoverflow.com/questions/10193325/coldfusion-mail-sending-capacity – Fish Below the Ice Jun 23 '14 at 15:34
  • 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! – Jason Jun 24 '14 at 00:50
  • 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. – David Faber Jul 01 '14 at 17:08

1 Answers1

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
Jeremy Halliwell
  • 3,295
  • 1
  • 15
  • 13
  • @Jason I've adapted your example, obviously not tested so feel free to edit if there's a mistake – Jeremy Halliwell Jun 24 '14 at 08:46
  • 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. Jason – Jason Jun 24 '14 at 22:53