Expand my Community achievements bar.

Submissions are now open for the 2026 Adobe Experience Maker Awards.

Issue with Receiving Duplicate Emails in AEM Cloud Scheduler

Avatar

Level 1

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);

 

    @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();

    }

}

Topics

Topics help categorize Community content and increase your ability to discover relevant content.

2 Replies

Avatar

Employee

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.

 

Avatar

Community Advisor

Hi @SampathKu1 ,

This is the limitation of AEMaaCS:

  • Code running in AEM as a Cloud Service must be cluster-aware. There are always more than one instance. Any scheduler you write may run on more than one node. 
  • Adobe explicitly recommends not relying exclusively on Sling Commons Scheduler for critical or business-important tasks because it can lead to duplicate execution (since scheduler runs in all nodes unless constrained).
  • Adobe recommends using Sling Jobs / the job queue for background / scheduled processing rather than per‐instance schedulers.

Recommended Solution

  • Use Sling JobManager and ScheduledJobInfo / scheduled jobs via JobManager to schedule rather than using @Scheduler (Commons Scheduler). 
  • Check for existing scheduled jobs (avoid scheduling duplicates) via jobManager.getScheduledJobs(...) before adding a job. This prevents multiple jobs being scheduled for the same topic.
  • Make your code resilient: tasks should be idempotent, able to handle being run again, handle instance restarts or failures.
  • Use run modes or other config to limit scheduling or task execution to particular contexts if needed. (Though the stronger pattern is via jobs + persistence rather than depending on single node behavior.)

References:

https://medium.com/adobetech/handling-sling-schedulers-in-aem-as-a-cloud-service-cb59d5e59e9

https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/dev...  

Shiv Prakash