java - 防止从Google App Engine发送重复的每日报告电子邮件

标签 java email google-app-engine google-cloud-datastore sendgrid

我们出现了问题。我们的客户抱怨他们的收件箱中收到重复的电子邮件。几天中,完全相同的电子邮件最多可能有5或6个实例。我们不明白为什么。该代码已至少被重写过一次,但问题仍然存在。

我会尽力解释这一点...但是有点复杂:O(

我们希望每天晚上(清晨)向用户发送包含使用情况统计信息的每日报告。因此,我们有一项计划工作:

<cron>
    <url>/redacted/report/url</url>
    <description>Send out daily reports to active subscribers</description>
    <schedule>every 2 hours</schedule>
</cron>


cron作业点击servlet get方法:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AccountFilter filter = AccountFilter.forWebSafeName(req.getParameter("filter"));
    createTasks(filter, null);
}


使用空游标调用createTasks方法:

private void createTasks(AccountFilter accountFilter, String cursor) {
    try {
        PagedResults<Account> pagedAccounts = accountRepository.getAccounts(accountFilter.getFilter(), 50, cursor);
        createTaskBatch(pagedAccounts);

        // If there are still more results in cursor, then send cursor back to this servlet's doPost method so we don't hit the request time limit
        if (pagedAccounts.getCursor() != null) {
            getQueue(QUEUE_NAME).add(withUrl(WORKER_URL).param(CURSOR_KEY, pagedAccounts.getCursor()).param(FILTER_KEY, accountFilter.getWebSafeName()));
        }
    } catch(Exception ex) {
        logger.log(Level.WARNING, "Problem creating daily report task batch for filter " + accountFilter.getWebSafeName(), ex);
    }
}


它将抓取50个帐户并对其进行遍历,从而为此时应发送的电子邮件创建新的排队作业。在创建新的排队任务之前,有代码可以明确检查上次报告发送的时间戳并更新时间戳。这应该在不发送报告而不是发送重复报告的一边犯错:

private void createTaskBatch(PagedResults<Account> pagedAccounts) {
    // GAE datastore query might return duplicate results?!
    List<Account> list = pagedAccounts.getResults();
    Set<Account> noDuplicates = new HashSet<>(list);
    int dups = list.size() - noDuplicates.size();
    if ( dups > 0 ){
        logger.warning ("Accounts paged results contained " + dups + " duplicates!");
    }
    for (Account account : noDuplicates) {
        try {
            if (lastReportSentOver12HoursAgo(account)) {
                List<Parent> parents = parentRepository.getVerifiedParentsForAccount(account.getId());
                if (eitherParentSubscribed(parents)) {
                    List<AccountUser> users = accountUserRepository.listUsers(account.getId());
                    List<Device> devices = getUserDevices(account, users);
                    if (!devices.isEmpty()) {
                        DateTimeZone tz = getMostCommonTimezone(devices);
                        if ( null == tz ){
                            logger.warning("No timezone found for account: " + account.getId() );
                        }
                        else{
                            // Send early in the morning as the report contains the previous day's stats
                            if (now(tz).getHourOfDay() < 7) {
                                // mark sent now because queue might not be processed for a while
                                // and the next cursor set might contain some of the same accounts
                                accountRepository.markReportSent(account.getId(), now());
                                getQueue(QUEUE_NAME).add(withUrl(DailyReportServlet.WORKER_URL).param(DailyReportServlet.ACCOUNT_ID, account.getId()).param(DailyReportServlet.COMMON_TIMEZONE, tz.getID()));
                            }
                        }
                    }
                }
            }
        } catch(Exception ex) {
            logger.log(Level.WARNING, "Problem creating daily report task for " + account.getId(), ex);
        }
    }
}


Servlet POST方法负责通过游标方法处理结果的后续页面:

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    AccountFilter accountFilter = AccountFilter.forWebSafeName(req.getParameter(FILTER_KEY));
    logger.log(Level.INFO, "doPost hit from task queue with filter " + accountFilter.getWebSafeName());
    String cursor = req.getParameter(CURSOR_KEY);
    createTasks(accountFilter, cursor);
}


还有另一个处理每个报告任务的servlet,它仅创建电子邮件内容并在com.sendgrid.SendGrid类上发送呼叫。

最终可能会在Datastore中保持一致性,但是应该在几秒钟内解决该问题,但我看不出这如何解决抱怨的客户数量和某些客户看到的重复数量。

救命!有任何想法吗?我们在某个地方呆呆吗?

更新

为了清楚起见...电子邮件发送任务队列最终使用此方法,该方法可以捕获异常并将其报告给我们。对于重复的案例,我们没有看到例外:

private void sendReport(Account account, DateTimeZone tz) throws IOException, EntityNotFoundException {
    try {
            boolean sent = false;
            Map<String, Object> root = buildEmailData(account, tz);
            for (Parent parent : parentRepository.getVerifiedParentsForAccount(account.getId())) {
                if (parent.getEmailPreferences().isSubscribedReports()) {
                    emailBuilder.send(account, parent, root, "report", EmailSender.NOTIFICATION);
                    sent = true;
                }
            }
            if ( sent ){
                accountRepository.markReportSent(account.getId(), now());
            }
    } catch (Exception ex) {
        String message = "Problem building report email for account " + account.getId();
        logger.log(Level.WARNING, message, ex);;
        new TeamNotificationEvent( message + " : exception: " + ex.getMessage()).fire();
        throw new IOException(message, ex);
    }
}


附加调试日志记录后的更新2

我看到两个POSTS同时进入具有相同光标的相同任务队列:

09:35:08.397 2015-04-30 200 0 B 3.78s / ws / notification / daily-report-task-creator
  0.1.0.2--[30 / Apr / 2015:01:35:08 -0700]“ POST / ws / notification / daily-report-task-creator HTTP / 1.1” 200 0“ http://screentimelabs.appspot.com/ws/notification/daily-report-task-creator”“ AppEngine-Google; (+ http://code.google.com/appengine)“” screentimelabs.appspot.com“ ms = 3782 cpu_ms = 662 queue_name = dailyReports task_name = 8168414365365326983 instance = 00c61b117c33a909790f0d1882657e04f40b2c7e app_engine_release = 1.9.20
    09:35:04.618 com.screentime.service.taskqueue.reports.DailyReportTaskCreatorServlet createTasks:调用过滤器的createTasks:带有游标的ACTIVE:E-ABAIICO2oQc35zY3JlZW50aW1lbGFic3InCxIHQWNjb3VudCIaamFybW8ua2AcCbbA8B2B3B

09:35:08.432 2015-04-30 200 0 B 8.84s / ws / notification / daily-report-task-creator
  0.1.0.2--[30 / Apr / 2015:01:35:08 -0700]“ POST / ws / notification / daily-report-task-creator HTTP / 1.1” 200 0“ http://screentimelabs.appspot.com/ws/notification/daily-report-task-creator”“ AppEngine-Google; (+ http://code.google.com/appengine)“” screentimelabs.appspot.com“ ms = 8837 cpu_ms = 1348 queue_name = dailyReports task_name = 50170612326424582061 instance = 00c61b117c2bffe8de313e96fea8aeb813f4b20f app_engine_release = 1.9.20 trace_id = 7e5c034f2e6e
    09:34:59.608 com.screentime.service.taskqueue.reports.DailyReportTaskCreatorServlet createTasks:调用过滤器的createTasks:带有游标的ACTIVE:E-ABAIICO2oQc35zY3JlZW50aW1lbGFic3InCxIHQWNjb3VudCIaamFybW8ua2AcCbBn8A2A2C

搜索1个特定的帐户ID,我看到以下请求:

09:35:08.397 2015-04-30 200 0 B 3.78s / ws / notification / daily-report-task-creator

09:35:08.432 2015-04-30 200 0 B 8.84s / ws / notification / daily-report-task-creator

09:35:08.443 2015-04-30 200 0 B 6.73s / ws / notification / daily-report-task-creator

2015-04-30 09:35:10.541 200 0 B 4.03s / ws / notification / daily-report-task-creator

09:35:10.690 2015-04-30 200 0 B 11.09s / ws / notification / daily-report-task-creator

2015-04-30 09:35:13.678 200 0 B 862ms / ws / notification / daily-report-worker

2015-04-30 09:35:13.829 500 0 B 1.21s / ws / notification / daily-report-worker

2015-04-30 09:35:14.677 200 0 B 1.56s / ws / notification / daily-report-worker

09:35:14.961 2015-04-30 200 0 B 346ms / ws / notification / daily-report-worker

有些具有重复的游标值。

最佳答案

我会猜测,因为我看不到任务队列代码。您可能没有正确处理任务队列中的错误。如果任务队列因错误而结束,则gae将对其重新排队。因此,如果已经发送了一些电子邮件,该任务仍将再次运行。您需要一种方法来记住您在任务队列中已处理的内容,因此重试不会重新处理这些内容。

关于java - 防止从Google App Engine发送重复的每日报告电子邮件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29940899/

相关文章:

java - 为什么 HashSet 没有稳定的序列化?

java - 将多个 JAXB 元素片段组合成一个根节点?

ios - pkpass 无法在 iOS 上从电子邮件附件中打开

ruby-on-rails - 如何仅从电子邮件内容中获取文本? Ruby on Rails - 用于 Ruby Gem 的 GMail

java - 四舍五入 BigDecimal 除法流结果时的 IntelliJ 提示

java - 消息队列和命令队列类似吗?

android - 尝试将创建的文本文件作为电子邮件附件发送 - 从默认文件夹

python - App Engine channel API 不返回任何消息

java - 应用引擎 : Copy live Datastore to local Dev Datastore

google-app-engine - 可持续的每秒查询 [QPS],不会产生超时错误