I have configured a scheduler in AEM6.5 and the idea is that it runs ONCE a night at 2AM
@Designate(ocd = TagVerificationScheduledTask.Config.class)
@Component(service = {Runnable.class})
public class TagVerificationScheduledTask implements Runnable {
@ObjectClassDefinition(name = "scheduled task that verifies tags in blog articles",
description = "A scheduled task that verifies tags in blog articles")
public @interface Config {
@AttributeDefinition(name = "Cron-job expression. Default: run every night at 2:00 AM.")
String scheduler_expression() default "0 0 2 * * ?";
The job runs on all three instances (one author and two publishers) and I want it to run on only one instance i.E. author.
How can I configure that?
O tried @Property(name="scheduler.runOn", value="SINGLE"); but it does not work. I also tried @AttributeDefinition(name = "Run On") String scheduler_runOn() default "SINGLE"; but that also did not work.
Solved! Go to Solution.
Views
Replies
Total Likes
The scheduler runs independently on each AEM instance; they don't know of each other (except for the replication, but that's a different story). That means every instance thinks of itself as a SINGLE instance, which at the same time is also a Cluster LEADER.
That means in your setup you implement this at best on the author only (make this service only running on author using OSGI configuration, see https://cqdump.joerghoh.de/2019/10/14/how-to-use-runmodes-correctly/)
Hi @anasustic,
I guess that you have multiple author and publish instances where the scheduler is running, possibly three instances in your case. This is why the email is sent three times at 2 AM.
Thanks a lot for your reply.
I have one Author and two Publish instances.
Is there a way to configure the job to run only once and only on one instance?
Please check the discussion here
https://experienceleaguecommunities.adobe.com/t5/adobe-experience-manager/aem-cloud-is-scheduler-run...
Regards,
Divanshu
Hi @divanshug
Thanks a lot for your reply. I am not sure how to configure it to run only on one node (i.E. author instance) using the documentation you provided:
@Property(name="scheduler.runOn", value="LEADER");
or
@Property(name="scheduler.runOn", value="SINGLE");
The scheduler runs independently on each AEM instance; they don't know of each other (except for the replication, but that's a different story). That means every instance thinks of itself as a SINGLE instance, which at the same time is also a Cluster LEADER.
That means in your setup you implement this at best on the author only (make this service only running on author using OSGI configuration, see https://cqdump.joerghoh.de/2019/10/14/how-to-use-runmodes-correctly/)
Hello @Jörg_Hoh
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!
Attaching the logs as well for reference
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);
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Reference
private AssetExpiryNotificationConfigImpl config;
@Reference
private EmailSenderService emailSenderService;
private final Logger logger = LoggerFactory.getLogger(getClass());
private String myParameter;
@Override
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);
}
}
}
@Activate
protected void activate(final Config config) {
myParameter = config.assetExpiryPath();
}
}
Views
Replies
Total Likes
Views
Likes
Replies
Views
Likes
Replies