Hello Team,
We have our application hosted on AEM Cloud, where I have set up a scheduler to trigger every 2 hours. The scheduler calls a method to send email notifications using the following command:
emailSenderService.sendExpiryNotification(res.getPath(), 30, expiryMillis)
However, we're experiencing an issue where the email is sent three times, whereas we only need it to be sent once. Our setup consists of one Author instance and two Publish instances. In the Author server run mode, I've disabled the email functionality by setting "smtp.ssl": true in the Day CQ Mail Service configuration. On the Publish instances, we've configured "smtp.ssl": false to allow email delivery.
Despite this setup, the emails are still being sent three times.
Has anyone encountered a similar issue or have suggestions on how to resolve this?
Thanks in advance for your help!
I am attaching a file which has logs. Please kindly check once to see the issue.
Scheduler Code :
Here's my scheduler :
@Designate(ocd=AssetExpiryScheduler.Config.class)
@component(service = Runnable.class,
property = {
"scheduler.runOn=LEADER"
})
public class AssetExpiryScheduler implements Runnable {
@ObjectClassDefinition(name="A scheduled task",
description = "Simple demo for cron-job like task with properties")
public static @interface Config {
@AttributeDefinition(name = "Cron-job expression")
String scheduler_expression() default "0 0 */2 * * ?";
@AttributeDefinition(name = "Concurrent task",
description = "Whether or not to schedule this task concurrently")
boolean scheduler_concurrent() default false;
@AttributeDefinition(name = "Asset Expiry Notification Scheduler",
description = "Can be configured in /system/console/configMgr")
String assetExpiryPath() default "/content/dam";
}
private static final Logger log = LoggerFactory.getLogger(AssetExpiryScheduler.class);
private static final String EXPIRATION_PROPERTY = "prism:expirationDate";
private static final String ASSET_ROOT_PATH = "/content/dam";
// DUPLICATE PREVENTION - Add this thread-safe flag
private static final AtomicBoolean isRunning = new AtomicBoolean(false);
private ResourceResolverFactory resourceResolverFactory;
private AssetExpiryNotificationConfigImpl config;
private EmailSenderService emailSenderService;
private final Logger logger = LoggerFactory.getLogger(getClass());
private String myParameter;
public void run() {
// TEMPORARY: Log instance information
logger.error("Scheduler instance: {}", System.identityHashCode(this));
logger.error("Thread: {}", Thread.currentThread().getName());
// PREVENT DUPLICATE EXECUTION - Add this check at the beginning
if (!isRunning.compareAndSet(false, true)) {
logger.warn("Scheduler is already running, skipping duplicate execution");
return;
}
try {
logger.error("AssetExpirySchedulerTask is now running, myParameter='{}'", myParameter);
logger.error("Asset Expiry Scheduler triggered at {}", LocalDate.now());
Map<String, Object> authMap = new HashMap<>();
authMap.put(ResourceResolverFactory.SUBSERVICE, "workflowUser");
try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(authMap)) {
Resource assetRoot = resolver.getResource(ASSET_ROOT_PATH);
if (assetRoot == null) {
logger.error("Asset root not found at {}", ASSET_ROOT_PATH);
return;
}
LocalDate today = LocalDate.now();
LocalDate oneMonthReminder = today.plusMonths(1);
LocalDate oneWeekReminder = today.plusWeeks(1);
LocalDate finalDayReminder = today.plusDays(1);
// NEW: Maintain a Set of visited asset paths to avoid duplicates within this run
Set<String> visitedAssets = new HashSet<>();
// Start recursive asset traversal with visited assets set
findAssetsRecursively(assetRoot.listChildren(), oneMonthReminder, oneWeekReminder, finalDayReminder, resolver, visitedAssets);
} catch (Exception e) {
logger.error("Error in Asset Expiry Scheduler", e);
}
} finally {
// ALWAYS RESET THE FLAG - Add this in finally block
isRunning.set(false);
}
}
// Modified recursive method with visitedAssets set parameter
private void findAssetsRecursively(Iterator<Resource> resources, LocalDate oneMonthReminder,
LocalDate oneWeekReminder, LocalDate finalDayReminder,
ResourceResolver resolver, Set<String> visitedAssets) {
while (resources.hasNext()) {
Resource res = resources.next();
// Skip if already visited this asset path in this scheduler run
if (visitedAssets.contains(res.getPath())) {
continue;
}
visitedAssets.add(res.getPath());
Resource metadata = res.getChild("jcr:content") != null
? res.getChild("jcr:content").getChild("metadata")
: null;
if (metadata != null) {
ValueMap vm = metadata.getValueMap();
String expirationDateStr = vm.get(EXPIRATION_PROPERTY, String.class);
if (StringUtils.isNotBlank(expirationDateStr)) {
try {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(expirationDateStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
LocalDate expirationDate = offsetDateTime.toLocalDate();
long expiryMillis = offsetDateTime.toInstant().toEpochMilli();
if (expirationDate.equals(oneMonthReminder)) {
logger.error("1-month reminder for asset: {} (expires on {})", res.getPath(), expirationDate);
emailSenderService.sendExpiryNotification(res.getPath(), 30, expiryMillis);
} else if (expirationDate.equals(oneWeekReminder)) {
logger.error("1-week reminder for asset: {} (expires on {})", res.getPath(), expirationDate);
emailSenderService.sendExpiryNotification(res.getPath(), 7, expiryMillis);
} else if (expirationDate.equals(finalDayReminder)) {
// Skip sending 0-day expiry email here because listener/job handles it
logger.error("Skipping 0-day expiry reminder in scheduler for asset: {}", res.getPath());
} else {
logger.error("Asset {} has expiration {}, not matching reminder days", res.getPath(), expirationDate);
}
} catch (Exception e) {
logger.error("Error parsing prism:expirationDate for asset {}", res.getPath(), e);
}
}
}
// Recursively check children
if (res.hasChildren()) {
findAssetsRecursively(res.listChildren(), oneMonthReminder, oneWeekReminder, finalDayReminder, resolver, visitedAssets);
}
}
}
protected void activate(final Config config) {
myParameter = config.assetExpiryPath();
}
}
Topics help categorize Community content and increase your ability to discover relevant content.
Views
Replies
Total Likes
In AEM as a Cloud Service you cannot control individual instances; especially the number of publish instances is dynamic and you cannot designate any of them in a way "special" (for example to execute a specific job and then send you those emails).
In your case it all depends why you send these emails. If you need to run a specific job at every publish at a specific time or interval: Why on publish? What does that job do that it has to run there? And why should the email then just be sent from one publish, but not from the others?
(In other words: the symptoms you described are expected. But I wonder if you have chosen the right solution to your requirement.
Views
Replies
Total Likes
Hi @SampathKu1 ,
This is the limitation of AEMaaCS:
Recommended Solution
References:
https://medium.com/adobetech/handling-sling-schedulers-in-aem-as-a-cloud-service-cb59d5e59e9
Views
Replies
Total Likes
Views
Likes
Replies
Views
Likes
Replies