Skip to content

Commit 95898a7

Browse files
committed
Merge branch 'feature-more-stats-executors' into releases/v7.x
2 parents 31205d3 + f9b16b6 commit 95898a7

2 files changed

Lines changed: 309 additions & 13 deletions

File tree

system/async/executors/Executor.cfc

Lines changed: 295 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ component accessors="true" singleton {
1515
*/
1616
property name="name";
1717

18+
/**
19+
* The created date time
20+
* This is set when the executor is created
21+
*/
22+
property name="created" type="date";
23+
24+
/**
25+
* The last activity date time
26+
* This happens when a task is submitted
27+
*/
28+
property name="lastActivity" type="date";
29+
30+
/**
31+
* Task submission count
32+
*/
33+
property name="taskSubmissionCount" type="numeric" default=0;
34+
1835
/**
1936
* The native Java executor class modeled in this executor
2037
*/
@@ -59,6 +76,15 @@ component accessors="true" singleton {
5976
}
6077
};
6178

79+
variables.healthThresholds = {
80+
"poolUtilization": { "degraded": 75, "critical": 95 },
81+
"threadUtilization": { "degraded": 75, "critical": 95 },
82+
"queueUtilization": { "degraded": 70, "critical": 95 },
83+
"taskCompletionRate": { "degraded": 50, "critical": 25 },
84+
"inactivityMinutes": 30,
85+
"minimumTasksForCompletion": 10
86+
};
87+
6288
/**
6389
* Constructor
6490
*
@@ -80,6 +106,9 @@ component accessors="true" singleton {
80106
variables.debug = arguments.debug;
81107
variables.loadAppContext = arguments.loadAppContext;
82108
variables.shutdownTimeout = arguments.shutdownTimeout;
109+
variables.created = now();
110+
variables.lastActivity = variables.created;
111+
variables.taskSubmissionCount = 0;
83112

84113
return this;
85114
}
@@ -109,6 +138,10 @@ component accessors="true" singleton {
109138
[ "java.util.concurrent.Callable" ]
110139
);
111140

141+
// Update the last activity
142+
variables.lastActivity = now();
143+
variables.taskSubmissionCount++;
144+
112145
// Send for execution
113146
return new coldbox.system.async.tasks.FutureTask( variables.native.submit( jCallable ) );
114147
}
@@ -157,6 +190,17 @@ component accessors="true" singleton {
157190
return variables.native.isShutdown();
158191
}
159192

193+
/**
194+
* Check if executor is healthy (simple boolean check)
195+
* Uses current stats to determine health
196+
*
197+
* @return boolean True if status is "healthy" or "idle"
198+
*/
199+
boolean function isHealthy(){
200+
var stats = getStats();
201+
return listFindNoCase( "healthy,idle", stats.healthStatus ) > 0;
202+
}
203+
160204
/**
161205
* Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or
162206
* the current thread is interrupted, whichever happens first.
@@ -312,20 +356,260 @@ component accessors="true" singleton {
312356
* @return struct of data about the executor and the schedule
313357
*/
314358
struct function getStats(){
315-
return {
359+
var uptimeSeconds = dateDiff( "s", variables.created, now() );
360+
var stats = {
361+
"created": dateTimeFormat( variables.created, "iso" ),
362+
"features" : variables.features[ variables.native.getClass().getSimpleName() ],
363+
"lastActivity": dateTimeFormat( variables.lastActivity, "iso" ),
364+
"lastActivityMinutesAgo" : dateDiff( "n", variables.lastActivity, now() ),
365+
"lastActivitySecondsAgo" : dateDiff( "s", variables.lastActivity, now() ),
316366
"name" : getName(),
317-
"poolSize" : getPoolSize(),
318-
"maximumPoolSize" : getMaximumPoolSize(),
319-
"largestPoolSize" : getLargestPoolSize(),
320-
"corePoolSize" : getCorePoolSize(),
321-
"completedTaskCount" : getCompletedTaskCount(),
322-
"taskCount" : getTaskCount(),
323-
"activeCount" : getActiveCount(),
367+
"thresholds" : variables.healthThresholds,
368+
"type" : variables.native.getClass().getName(),
369+
"uptimeDays" : dateDiff( "d", variables.created, now() ),
370+
"uptimeSeconds" : uptimeSeconds,
371+
// Pool Stats
372+
"corePoolSize" : 0,
373+
"largestPoolSize" : 0,
374+
"maximumPoolSize" : 0,
375+
"poolSize" : 0,
376+
"poolUtilization" : 0,
377+
// Task Stats
378+
"activeCount" : 0,
379+
"allowsCoreThreadTimeOut" : false,
380+
"averageTasksPerMinute" : uptimeSeconds > 0 ? ( variables.taskSubmissionCount / uptimeSeconds ) * 60 : 0,
381+
"averageTasksPerSecond" : uptimeSeconds > 0 ? variables.taskSubmissionCount / uptimeSeconds : 0,
382+
"completedTaskCount" : 0,
383+
"keepAliveTimeoutInSeconds" : 0,
384+
"taskCompletionRate" : 0,
385+
"taskCount" : 0,
386+
"taskSubmissionCount" : variables.taskSubmissionCount,
387+
"threadsUtilization" : 0,
388+
// States
389+
"isShutdown" : isShutdown(),
324390
"isTerminated" : isTerminated(),
325391
"isTerminating" : isTerminating(),
326-
"isShutdown" : isShutdown(),
327-
"type" : variables.native.getClass().getName(),
328-
"queue" : getQueue().toString()
392+
// Queue Stats
393+
"queueCapacity": 0,
394+
"queueIsEmpty":0,
395+
"queueIsFull":0,
396+
"queueRemainingCapacity":0,
397+
"queueSize":0,
398+
"queueType":0,
399+
"queueUtilization" : 0
400+
};
401+
402+
// Pool Stats
403+
if( hasFeature( "pool" ) ){
404+
stats[ "corePoolSize" ] = getCorePoolSize();
405+
stats[ "largestPoolSize" ] = getLargestPoolSize();
406+
stats[ "maximumPoolSize" ] = getMaximumPoolSize();
407+
stats[ "poolSize" ] = getPoolSize();
408+
stats[ "poolUtilization" ] = getMaximumPoolSize() > 0 ? ( getPoolSize() / getMaximumPoolSize() ) * 100 : 0;
409+
}
410+
411+
// Task Stats
412+
if( hasFeature( "taskMethods" ) ){
413+
stats[ "activeCount" ] = getActiveCount();
414+
stats[ "allowsCoreThreadTimeOut" ] = variables.native.allowsCoreThreadTimeOut();
415+
stats[ "completedTaskCount" ] = getCompletedTaskCount();
416+
stats[ "keepAliveTimeoutInSeconds" ] = variables.native.getKeepAliveTime( this.$timeUnit.get( "seconds" ) );
417+
stats[ "taskCompletionRate" ] = getTaskCount() > 0 ? ( getCompletedTaskCount() / getTaskCount() ) * 100 : 0;
418+
stats[ "taskCount" ] = getTaskCount();
419+
stats[ "threadsUtilization" ] = getPoolSize() > 0 ? ( getActiveCount() / getPoolSize() ) * 100 : 0;
420+
}
421+
422+
// Queue Stats
423+
if( hasFeature( "queue" ) ){
424+
stats[ "queueCapacity" ] = getQueue().size() + getQueue().remainingCapacity();
425+
stats[ "queueIsEmpty" ] = getQueue().isEmpty();
426+
// Check unbounded queues
427+
stats[ "queueIsFull" ] = stats.maximumPoolSize >= 2147483647
428+
? false
429+
: ( getQueue().remainingCapacity() == 0 );
430+
stats[ "queueRemainingCapacity" ] = getQueue().remainingCapacity();
431+
stats[ "queueSize" ] = getQueue().size();
432+
stats[ "queueType" ] = getQueue().getClass().getSimpleName();
433+
stats[ "queueUtilization" ] = stats.queueCapacity > 0 ? ( stats.queueSize / stats.queueCapacity) * 100 : 0;
434+
}
435+
436+
// Add health status to stats (this must come last after all stats are calculated)
437+
stats[ "healthStatus" ] = getHealthStatus( stats );
438+
stats[ "healthReport" ] = getHealthReport( stats );
439+
440+
return stats;
441+
}
442+
443+
/**
444+
* This functions needs to use standard heuristics to determine a health status of the executor.
445+
* It should return a value of "healthy", "degraded", "critical", "draining".
446+
*
447+
* @return string The health status of the executor
448+
*/
449+
private function getHealthStatus( required struct stats ){
450+
var thresholds = variables.healthThresholds;
451+
var criticalIssues = [];
452+
var degradedIssues = [];
453+
454+
// Shutdown/termination states (highest priority)
455+
if ( arguments.stats.isTerminated ) return "terminated";
456+
if ( arguments.stats.isShutdown ) return "shutdown";
457+
if ( arguments.stats.isTerminating ) return "draining";
458+
459+
// Critical health issues
460+
if ( arguments.stats.queueIsFull ) {
461+
criticalIssues.append( "queue_full" );
462+
}
463+
464+
if ( arguments.stats.poolUtilization > thresholds.poolUtilization.critical ) {
465+
criticalIssues.append( "pool_exhausted" );
466+
}
467+
468+
if ( arguments.stats.threadsUtilization > thresholds.threadUtilization.critical ) {
469+
criticalIssues.append( "threads_exhausted" );
470+
}
471+
472+
// Queue utilization critical
473+
if ( arguments.stats.queueUtilization > thresholds.queueUtilization.critical ) {
474+
criticalIssues.append( "queue_near_full" );
475+
}
476+
477+
// Task completion rate critical
478+
if ( arguments.stats.taskCount >= thresholds.minimumTasksForCompletion &&
479+
arguments.stats.taskCompletionRate < thresholds.taskCompletionRate.critical ) {
480+
criticalIssues.append( "task_completion_critical" );
481+
}
482+
483+
// Degraded health issues
484+
if ( arguments.stats.poolUtilization > thresholds.poolUtilization.degraded ) {
485+
degradedIssues.append( "high_pool_usage" );
486+
}
487+
488+
if ( arguments.stats.threadsUtilization > thresholds.threadUtilization.degraded ) {
489+
degradedIssues.append( "high_thread_usage" );
490+
}
491+
492+
// Queue utilization degraded
493+
if ( arguments.stats.queueUtilization > thresholds.queueUtilization.degraded ) {
494+
degradedIssues.append( "queue_backing_up" );
495+
}
496+
497+
// Task completion rate degraded
498+
if ( arguments.stats.taskCount >= thresholds.minimumTasksForCompletion &&
499+
arguments.stats.taskCompletionRate < thresholds.taskCompletionRate.degraded ) {
500+
degradedIssues.append( "task_completion_degraded" );
501+
}
502+
503+
// Check for idle state (no activity, no work)
504+
if ( arguments.stats.lastActivityMinutesAgo > thresholds.inactivityMinutes &&
505+
arguments.stats.activeCount == 0 &&
506+
arguments.stats.queueSize == 0 &&
507+
!arguments.stats.isShutdown &&
508+
!arguments.stats.isTerminating ) {
509+
return "idle";
510+
}
511+
512+
// Return status based on issues found
513+
if ( criticalIssues.len() > 0 ) return "critical";
514+
if ( degradedIssues.len() > 0 ) return "degraded";
515+
516+
return "healthy";
517+
}
518+
519+
/**
520+
* Provides a comprehensive health report with detailed analysis, issues, and recommendations
521+
* Uses the provided stats to generate the report
522+
*
523+
* @stats struct The stats structure to analyze for health report
524+
*
525+
* @return struct Detailed health report
526+
*/
527+
struct function getHealthReport( required struct stats ){
528+
var status = arguments.stats.healthStatus;
529+
var thresholds = variables.healthThresholds;
530+
var issues = [];
531+
var recommendations = [];
532+
var alerts = [];
533+
534+
// Analyze pool utilization
535+
if ( arguments.stats.poolUtilization > thresholds.poolUtilization.critical ) {
536+
issues.append("Critical pool utilization: #numberFormat(arguments.stats.poolUtilization, '0.0')#%");
537+
recommendations.append("Immediately increase maximum pool size or reduce workload");
538+
alerts.append({ "level": "critical", "metric": "poolUtilization", "value": arguments.stats.poolUtilization });
539+
} else if ( arguments.stats.poolUtilization > thresholds.poolUtilization.degraded ) {
540+
issues.append("High pool utilization: #numberFormat(arguments.stats.poolUtilization, '0.0')#%");
541+
recommendations.append("Consider increasing maximum pool size");
542+
alerts.append({ "level": "warning", "metric": "poolUtilization", "value": arguments.stats.poolUtilization });
543+
}
544+
545+
// Analyze thread utilization
546+
if ( arguments.stats.threadsUtilization > thresholds.threadUtilization.critical ) {
547+
issues.append("Critical thread utilization: #numberFormat(arguments.stats.threadsUtilization, '0.0')#%");
548+
recommendations.append("All threads busy - consider increasing pool size or optimizing tasks");
549+
alerts.append({ "level": "critical", "metric": "threadsUtilization", "value": arguments.stats.threadsUtilization });
550+
} else if ( arguments.stats.threadsUtilization > thresholds.threadUtilization.degraded ) {
551+
issues.append("High thread utilization: #numberFormat(arguments.stats.threadsUtilization, '0.0')#%");
552+
recommendations.append("Monitor thread usage patterns and consider capacity planning");
553+
alerts.append({ "level": "warning", "metric": "threadsUtilization", "value": arguments.stats.threadsUtilization });
554+
}
555+
556+
// Analyze queue health
557+
if ( arguments.stats.queueIsFull ) {
558+
issues.append("Queue is full: #arguments.stats.queueSize#/#arguments.stats.queueCapacity# capacity");
559+
recommendations.append("Queue rejecting tasks - increase capacity or improve processing speed");
560+
alerts.append({ "level": "critical", "metric": "queueFull", "value": true });
561+
} else if ( arguments.stats.queueUtilization > thresholds.queueUtilization.critical ) {
562+
issues.append("Queue near capacity: #numberFormat(arguments.stats.queueUtilization, '0.0')#% (#arguments.stats.queueSize#/#arguments.stats.queueCapacity#)");
563+
recommendations.append("Queue filling up - monitor for processing bottlenecks");
564+
alerts.append({ "level": "critical", "metric": "queueUtilization", "value": arguments.stats.queueUtilization });
565+
} else if ( arguments.stats.queueUtilization > thresholds.queueUtilization.degraded ) {
566+
issues.append("Queue utilization elevated: #numberFormat(arguments.stats.queueUtilization, '0.0')#% (#arguments.stats.queueSize#/#arguments.stats.queueCapacity#)");
567+
recommendations.append("Monitor queue growth trends");
568+
alerts.append({ "level": "warning", "metric": "queueUtilization", "value": arguments.stats.queueUtilization });
569+
}
570+
571+
// Analyze task completion rates
572+
if ( arguments.stats.taskCount >= thresholds.minimumTasksForCompletion ) {
573+
if ( arguments.stats.taskCompletionRate < thresholds.taskCompletionRate.critical ) {
574+
issues.append("Very low task completion rate: #numberFormat(arguments.stats.taskCompletionRate, '0.0')#%");
575+
recommendations.append("Investigate task failures or performance issues");
576+
alerts.append({ "level": "critical", "metric": "taskCompletionRate", "value": arguments.stats.taskCompletionRate });
577+
} else if ( arguments.stats.taskCompletionRate < thresholds.taskCompletionRate.degraded ) {
578+
issues.append("Low task completion rate: #numberFormat(arguments.stats.taskCompletionRate, '0.0')#%");
579+
recommendations.append("Monitor task success patterns");
580+
alerts.append({ "level": "warning", "metric": "taskCompletionRate", "value": arguments.stats.taskCompletionRate });
581+
}
582+
}
583+
584+
// Analyze activity patterns
585+
if ( status == "idle" ) {
586+
issues.append("Executor idle for #arguments.stats.lastActivityMinutesAgo# minutes");
587+
recommendations.append("Verify if inactivity is expected or indicates a problem");
588+
}
589+
590+
// Performance insights
591+
var insights = [];
592+
if ( arguments.stats.averageTasksPerSecond > 0 ) {
593+
insights.append("Processing rate: #numberFormat(arguments.stats.averageTasksPerSecond, '0.00')# tasks/second");
594+
}
595+
if ( arguments.stats.uptimeDays > 0 ) {
596+
insights.append("Uptime: #arguments.stats.uptimeDays# days");
597+
}
598+
599+
// Resource efficiency analysis
600+
if ( arguments.stats.poolSize > 0 && arguments.stats.averageTasksPerSecond > 0 ) {
601+
var tasksPerThread = arguments.stats.averageTasksPerSecond / arguments.stats.poolSize;
602+
insights.append("Efficiency: #numberFormat(tasksPerThread, '0.00')# tasks/second per thread");
603+
}
604+
605+
return {
606+
"status": status,
607+
"summary": issues.len() == 0 ? "Executor operating normally" : "#issues.len()# issue#issues.len() == 1 ? '' : 's'# detected",
608+
"issues": issues,
609+
"recommendations": recommendations,
610+
"alerts": alerts,
611+
"insights": insights,
612+
"lastChecked": dateTimeFormat(now(), "iso")
329613
};
330614
}
331615

system/async/executors/ScheduledExecutor.cfc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ component extends="Executor" accessors="true" singleton {
6060
this.$timeUnit.get( arguments.timeUnit )
6161
);
6262

63+
// Update the last activity time
64+
variables.lastActivity = now();
65+
variables.taskSubmissionCount++;
66+
6367
// Return the results
6468
return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture );
6569
}
6670

6771
/**
68-
* Creates and executes a periodic action that becomes enabled first after
69-
* the given initial delay, and subsequently with the given period;
72+
* Creates and executes a periodic action that becomes enabled first after the given
73+
* initial delay, and subsequently with the given period;
7074
* that is executions will commence after delay then delay+every, then delay + 2 * every,
7175
* and so on.
7276
*
@@ -101,6 +105,10 @@ component extends="Executor" accessors="true" singleton {
101105
this.$timeUnit.get( arguments.timeUnit )
102106
);
103107

108+
// Update the last activity time
109+
variables.lastActivity = now();
110+
variables.taskSubmissionCount++;
111+
104112
// Return the results
105113
return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture );
106114
}
@@ -140,6 +148,10 @@ component extends="Executor" accessors="true" singleton {
140148
this.$timeUnit.get( arguments.timeUnit )
141149
);
142150

151+
// Update the last activity time
152+
variables.lastActivity = now();
153+
variables.taskSubmissionCount++;
154+
143155
// Return the results
144156
return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture );
145157
}

0 commit comments

Comments
 (0)