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
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
Related
I am trying to download data from Azure blob in chunk and then trying to upload same chunk to aws s3 bucket.
While uploading I am getting "Your proposed upload is smaller than the minimum allowed size"exception. One thing I noticed, in upload response I am getting 0 content length. Data size I am trying is more than 300MB.
Any pointers what could be wrong here?
Below is my code snippet :
var remainingLength = blob.Properties.Length;
long startPosition = 0;
List<UploadPartResponse> uploadResponses = new List<UploadPartResponse>();
int i = 1;
string uploadId = string.Empty;
//Step 1: build and send a multi upload request
var initiateRequest = new InitiateMultipartUploadRequest
{
BucketName = existingBucketName,
Key = "firstobj"
};
var initResponse = client.InitiateMultipartUpload(initiateRequest);
uploadId = initResponse.UploadId;
do
{
var blockSize = Math.Min(segmentSize, remainingLength);
using (var ms = new MemoryStream())
{
blob.DownloadRangeToStream(ms, startPosition, blockSize);
//Step 2: upload each chunk (this is run for every chunk unlike the other steps which are run once)
var uploadRequest = new UploadPartRequest
{
BucketName = existingBucketName,
Key = "firstobj",
UploadId = uploadId,
PartNumber = i,
PartSize = ms.Length,
InputStream = ms
};
// Upload part and add response to our list.
var temp = client.UploadPart(uploadRequest);
uploadResponses.Add(temp);
}
//Step 3: build and send the multipart complete request
if (blockSize < segmentSize)
{
var completeRequest = new CompleteMultipartUploadRequest
{
BucketName = existingBucketName,
Key = "firstobj",
UploadId = uploadId,
};
completeRequest.AddPartETags(uploadResponses);
client.CompleteMultipartUpload(completeRequest);
}
startPosition += blockSize;
remainingLength -= blockSize;
i++;
}
while (remainingLength > 0);
After banging my head a lot, I got solution for this. It was in step 2 just before uploading part to AWS we should set stream position to 0.
uploadRequest.InputStream.Position = 0;
Can someone tell me what's wrong with my code here... I'm always getting this exception on the first call to contentService.read(...) after the first fetchMore has occurred.
org.apache.ws.security.WSSecurityException: The security token could not be authenticated or authorized
// Here we're setting the endpoint address manually, this way we don't need to use
// webserviceclient.properties
WebServiceFactory.setEndpointAddress(wsRepositoryEndpoint);
AuthenticationUtils.startSession(wsUsername, wsPassword);
// Set the batch size in the query header
int batchSize = 5000;
QueryConfiguration queryCfg = new QueryConfiguration();
queryCfg.setFetchSize(batchSize);
RepositoryServiceSoapBindingStub repositoryService = WebServiceFactory.getRepositoryService();
repositoryService.setHeader(new RepositoryServiceLocator().getServiceName().getNamespaceURI(), "QueryHeader", queryCfg);
ContentServiceSoapBindingStub contentService = WebServiceFactory.getContentService();
String luceneQuery = buildLuceneQuery(categories, properties);
// Call the repository service to do search based on category
Query query = new Query(Constants.QUERY_LANG_LUCENE, luceneQuery);
// Execute the query
QueryResult queryResult = repositoryService.query(STORE, query, true);
String querySession = queryResult.getQuerySession();
while (querySession != null) {
ResultSet resultSet = queryResult.getResultSet();
ResultSetRow[] rows = resultSet.getRows();
for (ResultSetRow row : rows) {
// Read the content from the repository
Content[] readResult = contentService.read(new Predicate(new Reference[] { new Reference(STORE, row.getNode().getId(), null) },
STORE, null), Constants.PROP_CONTENT);
Content content = readResult[0];
[...]
}
// Get the next batch of results
queryResult = repositoryService.fetchMore(querySession);
// process subsequent query results
querySession = queryResult.getQuerySession();
}
I have the code below. I would expect it to run daily at 17:00 if the config setting was not set otherwise the config setting will be used. So far no issue, the variable is set correctly. However: Instead of daily the job gets executed every minute and I cant figure out why. Is the scheduler not set up correctly?
TimeSpan timeOfExecution;
if (!TimeSpan.TryParse(ConfigurationManager.AppSettings["TimeOfExecution"], out timeOfExecution))
{
timeOfExecution = new TimeSpan(17, 0, 0);
}
var job = JobBuilder.Create<DailyReportJob>()
.WithIdentity("DailyReportJob")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("DailyReportTrigger")
.WithDailyTimeIntervalSchedule(s => s.OnEveryDay().StartingDailyAt(new TimeOfDay(timeOfExecution.Hours, timeOfExecution.Minutes)))
.Build();
Scheduler.ScheduleJob(job, trigger);
Scheduler.ListenerManager.AddJobListener(AutofacJobListener);
Scheduler.Start();
The default time for this trigger is every minute, since you haven't specified otherwise.
You can check all the intervals using this code:
var dt = trigger.GetNextFireTimeUtc();
for (int i = 0; i < 10; i++)
{
if (dt == null)
break;
Console.WriteLine(dt.Value.ToLocalTime());
dt = trigger.GetFireTimeAfter(dt);
}
If you want to schedule your job to run once a day at 5pm, you can change your code adding a 24 hour interval:
var trigger = TriggerBuilder.Create()
.WithIdentity("DailyReportTrigger")
.WithDailyTimeIntervalSchedule(s => s.OnEveryDay().StartingDailyAt(new TimeOfDay(timeOfExecution.Hours, timeOfExecution.Minutes)))
.WithIntervalInHours(24)
.Build();
This thread was useful in finding out the next run-time for a scheduled task.
How do I find out the next run time for a Scheduled Task?
But, is there also a way to simply get the next scheduled task due to run?
If I can get the date and name of the next task due to run, I can plug that date into a jQuery countdown timer, which will display a countdown to the next scheduled task, something like:
TaskABC due to run in:
12 03 20
hrs min sec
. This is for an admin interface in case you're wondering how geeky can people get:-)
EDIT
I had the same thought as Bill. But was curious if there was another way.
I poked around and apparently the internal Scheduler class maintains a list of upcoming tasks. The list is private, but you can use the same reflection technique to access it. Interestingly the list also includes system tasks like the mail spooler, session/application trackers, watchers, etecetera. So you must iterate through it until you find a "scheduled task" ie CronTabEntry
Below is a very lightly tested function that seems to do the trick in CF9. (Note, includes the CreateTimeStruct function from http://www.cflib.org).
Rules:
Returns a structure containing the name and time remaining until the next task. If no tasks were found, result.task is an empty string.
Excludes paused tasks
Usage:
result = new TaskUtil().getNextTask();
WriteDump(result);
CFC
component {
public struct function getNextTask() {
// get list of upcoming tasks from factory (UNDOCUMENTED)
local.scheduler = createObject("java", "coldfusion.server.ServiceFactory").getSchedulerService();
local.taskField = local.scheduler.getClass().getDeclaredField("_tasks");
local.taskField.setAccessible( true );
local.taskList = local.taskField.get(local.scheduler);
// taskList contains system jobs too, so we must iterate
// through the tasks to find the next "scheduled task"
local.nextTask = "";
local.tasks = local.taskList.iterator();
while ( local.tasks.hasNext() ) {
local.currTask = local.tasks.next();
local.className = local.currTask.getRunnable().getClass().name;
// exit as soon as we find a scheduled task that is NOT paused
if (local.className eq "coldfusion.scheduling.CronTabEntry"
&& !local.currTask.getRunnable().paused) {
local.nextTask = local.currTask;
break;
}
}
// if we found a task, calculate how many days, hours, etcetera
// until its next run time
local.details = { task="", remaining={} };
if ( isObject(local.nextTask) ) {
local.secondsToGo = (local.nextTask.getWhen() - now().getTime()) / 1000;
local.details.task = local.nextTask.getRunnable().task;
local.details.remaining = createTimeStruct(local.secondsToGo);
local.details.nextDate = dateAdd("s", local.nextTask.getWhen() / 1000
, "January 1 1970 00:00:00" );
}
return local.details;
}
/**
* Abbreviated version of CreateTimeStruct by Dave Pomerance
* See http://www.cflib.org/index.cfm?event=page.udfbyid&udfid=421
*
* #param timespan The timespan to convert.
* #return Returns a structure.
* #author Dave Pomerance
* #version 1, January 7, 2002
*/
public struct function CreateTimeStruct(required numeric timespan) {
var timestruct = StructNew();
var mask = "s";
// only 4 allowed values for mask - if not one of those, return blank struct
if (ListFind("d,h,m,s", mask)) {
// compute seconds
if (mask eq "s") {
timestruct.s = (timespan mod 60) + (timespan - Int(timespan));
timespan = int(timespan/60);
mask = "m";
} else timestruct.s = 0;
// compute minutes
if (mask eq "m") {
timestruct.m = timespan mod 60;
timespan = int(timespan/60);
mask = "h";
} else timestruct.m = 0;
// compute hours, days
if (mask eq "h") {
timestruct.h = timespan mod 24;
timestruct.d = int(timespan/24);
} else {
timestruct.h = 0;
timestruct.d = timespan;
}
}
return timestruct;
}
}
My first thought is to iterate Leigh's getNextRunTime(string taskName) function over the collection of tasks. You can get an array of structs containing the details of all scheduled tasks using taskArray = createobject("java","coldfusion.server.ServiceFactory").getCronService().listAll();
The key in the struct containing the task name is "task". So you can extract all the task names as an array for example, run Leigh's function on each element and determine which one will run next.
In ColdFusion 9, is there a quick way to find out the next time that a scheduled task will attempt to run?
I would rather call a lower level API or such to have CF calculate it
in the same way it normally would. I've dumped the various services
and see no obvious methods to call that would help.
AFAIK, there is no one line solution. The main method CF uses to calculate the dates is CronTabEntry.NextRunTime. The CronTabEntry class represents the settings for a single task. The NextRunTime method(s) calculate the next potential run date, based on the task's settings. Handling of expired and paused tasks is done elsewhere, at runtime.
To duplicate the results you need to call NextRunTime and add a bit of logic to handle expired tasks. While NextRunTime method is private, it can still be accessed via reflection, with the help of Method.setAccessible(boolean).
I threw together the function below to demonstrate. The bulk of it is the reflection call (which is a bit more verbose in CF than its java equivalent). Anyway, it should return the same dates used by the CF scheduler.
Rules:
If the task end date/time has passed, returns an emtpy string ""
If it is a one-time task, that already executed, returns an empty string ""
For all other tasks (including paused tasks), returns the next scheduled date
Usage:
result = new TaskUtil().getNextRunTime("SomeTask");
WriteDump(result);
CFC
component {
public struct function getNextRunTime(required string scheduledTaskName) {
// load task settings from factory
local.cron = createobject("java","coldfusion.server.ServiceFactory").getCronService();
local.task = local.cron.findTask( arguments.scheduledTaskName );
// abort if we cannot find the task ..
if ( isNull(local.task) ) {
throw ("The specified task was not found: ["& arguments.scheduledTaskName &"]");
}
// Calculate the next POTENTIAL schedule date
local.isRecurring = listFindNoCase("daily,weekly,monthly", local.task.interval);
local.dateClass = createObject("java", "java.util.Date").getClass();
local.longClass = createObject("java", "java.lang.Long").TYPE;
local.stringClass = createObject("java", "java.lang.String").getClass();
// Construct the appropriate arguments
// Note: must load arguments / class types into arrays for java reflection
if (local.isRecurring) {
local.args = [ local.task.getStartDate(), local.task.getStartTime(), local.task.interval ];
local.types = [ local.dateClass, local.dateClass, local.stringClass ];
}
else {
local.args = [ local.task.getStartDate(), local.task.getStartTime(), local.task.getEndTime(), javacast("long", val(local.task.interval) * 1000) ];
local.types = [ local.dateClass, local.dateClass, local.dateClass, local.longClass ];
}
// Call CF's internal method to calculate the next date (UNDOCUMENTED)
local.internalMethod = local.task.getClass().getDeclaredMethod("NextRunTime", local.types );
local.internalMethod.setAccessible( true );
local.nextRunOnDate = local.internalMethod.invoke( local.task, local.args );
// Determine if the task will be rescheduled
local.isExpired = false;
if ( local.task.interval != "once" && structKeyExists( local.task, "end_date") ) {
// It is non-recurring, so determine if it already expired
local.expiresOnDate = local.task.mergeDates( local.task.getEndDate(), local.task.getEndTime() );
local.isExpired = dateCompare( local.nextRunOnDate, local.expiresOnDate, "s") >= 0;
}
else if ( local.task.interval == "once" && local.task.disabled) {
// It is a one time task that already executed
local.isExpired = true;
}
// construct and return results
local.result = {};
local.result.name = local.task.task;
local.result.isPaused = local.task.paused;
local.result.isExpired = local.isExpired;
local.result.nextRunTime = local.isExpired ? "" : local.nextRunOnDate;
return local.result;
}
}