Реализация отправки писем через cron job

Рассмотрим следующую таблицу «pending_send_confirm» в базе данных, используемую для хранения адресов электронной почты, на которые мне нужно отправить электронную почту (конечный пользователь подтвердит свою подписку через это электронное письмо):

id| address |occupied|
0 |[email protected]|0       |
1 |[email protected]|0       |
2 |[email protected]|0       |

Когда пользователь подписывается (например, на мою рассылку), я хочу показать сообщение о том, что почти сразу все прошло нормально. Я не хочу, чтобы он ждал, пока сервер отправит электронное письмо с подтверждением на его адрес, прежде чем показывать сообщение.

По этой причине я настраиваю задание cron, запускаемое каждую минуту, чтобы управлять электронными письмами, которые необходимо отправить. Согласно таблице, которую я придумал (показана выше), это некий псевдокод, отправляющий электронные письма на каждый адрес, 1 на 1:

//child_script.php
//fetch the entries from the database
while ($entry = $result->fetch_assoc()) {
    if ($entry['occupied']) {
        /*
         * Another instance of this script has 'occupied' this address, which
         * means that it is currently trying to send a confirmation email to
         * this address. So you know that another instance is working with this
         * email address, so skip it.
        */
        continue;
    }

    /*
     * Entry is not occupied, occupy it now in order to prevent future instances
     * of this script to attempt to send additional confirmation email to this address
     * while this instance of the script tries to send the confirmation email to this address.
     * occupied=1 means that an attempt to send the confirmation email is under the way
    */

    occupyEntry($entry['id']); //sets 'occupied' to 1

    if (sendConfirmationEmail($entry['address'])) {

        /*
         * Email was sent successfully, move the email address from the 'pending_send_confirm' to the 
         * 'pending_confirmation_from_user' table.
        */

        moveToConfirmPendingFromUserTable($entry['id']);
    } else {

        /*
         * Failed to send the email, unoccupy the entry so another instance of the script
         * can try again sometime in the future
        */

        unoccupyEntry($entry['id']); //sets 'occupied' to 0
    }


}

Код без комментариев для удобства чтения:

//child_script.php
while ($entry = $result->fetch_assoc()) {

    if ($entry['occupied']) {
        continue;
    }

    occupyEntry($entry['id']);

    if (sendConfirmationEmail($entry['address'])) {
        moveToConfirmPendingTable($entry['id']);
    } else {
        unoccupyEntry($entry['id']);
    }
}

Является ли это надежным решением для предотвращения отправки дубликатов электронных писем? Я беспокоюсь, что 2 экземпляра скрипта могут обнаружить «одновременно», что $entry['occupied'] равно 0 для определенного идентификатора, и попытаться отправить электронное письмо на этот адрес.

Другим решением было бы использовать flock ( Как я могу убедиться, что только один экземпляр PHP-скрипта работает через Apache? ), чтобы гарантировать, что только 1 экземпляр моего скрипта работает.

Однако я вижу много проблем с реализацией стада. Например, что произойдет, если мой сценарий выйдет из строя до вызова fclose($fp)? Сможет ли следующий экземпляр моего скрипта продолжить работу или он увидит его как еще один запущенный экземпляр скрипта (т. е. что функция flock вернет новому экземпляру скрипта)? Другая проблема заключается в том, что мой скрипт отправляет электронные письма одно за другим. Это означает, что если у меня есть 100 электронных писем для отправки, и я отправляю их в течение 3,5 минут, то следующий экземпляр запустится через 4 минуты ПОСЛЕ ЗАПУСКА 1-го экземпляра. Это означает, что если подписчик решит подписаться в момент запуска 1-го экземпляра, ему придется ждать более 4 минут, чтобы получить электронное письмо с подтверждением. Если я позволю сценариям работать параллельно, то электронные письма будут рассылаться намного быстрее. Поэтому я бы предпочел иметь несколько экземпляров одного и того же сценария, отправляющих электронные письма одновременно.

С другой стороны, если мой сценарий, использующий метод «оккупировать», работает нормально, смогу ли я использовать другой сценарий для управления количеством одновременных экземпляров, которые будут запущены? Например, смогу ли я сделать следующее? :

//master_script.php

/*
 * Launch many instances of child_script.php.
 * If there are 901 to 1000 emails, then start 10 instances etc
*/

launch_n_instances( ceil(number_of_non_occupied_entries()/100.0) );

Как правильно решить эту проблему?


person hytromo    schedule 05.02.2015    source источник


Ответы (2)


В другой таблице создайте поле с именем «cron_running» или что-то подобное и установите для него значение true в верхней части скрипта cron. В конце скрипта установите для поля значение false. Это позволит вам проверить, работает ли скрипт cron, если он перекрывается и не продолжается.

person Mick    schedule 05.02.2015

При чтении о вашей проблеме возникает много вопросов:

Ты говоришь:

Я не хочу, чтобы он ждал, пока сервер отправит электронное письмо с подтверждением на его адрес, прежде чем показывать сообщение.

Во-первых, задержка, добавляемая отправкой письма с подтверждением, должна быть очень короткой.

Во-вторых, подписка на информационный бюллетень кажется мне чем-то вроде «электронной почты». Как вы можете сказать, что «подписка прошла успешно», если вы не получили подтверждения?

Другая проблема, о которой вы упоминаете, на самом деле сводится к тому, как вести учет некоторого статуса — в данном случае «отправлено электронное письмо» — для нескольких процессов в базе данных. Если ваша база данных имеет соответствующий уровень изоляции, то есть вы можете читать только то, что не при изменении база данных позаботится об этом.

Вместо этого ваше поле «оккупировать» может называться email_status со значениями,

0 - new
1 - processing
2 - created
3 - confirmed 

Последний момент заключается в том, что эта бухгалтерия — всего лишь переходный статус. Вы не хотите хранить все эти избыточные «подтвержденные» записи в своей базе данных, просто чтобы иметь информацию о возможных ошибках на пути от «новых» к «подтвержденным».

Таким образом, вы можете даже использовать отдельную таблицу, состоящую из идентификатора и статуса, где последним шагом обработки является их удаление. Таким образом, ваша дополнительная таблица будет содержать только не полностью обработанные идентификаторы.

person nlu    schedule 05.02.2015
comment
Я не говорю, что подписка прошла нормально. Я просто хочу показать сообщение о том, что электронное письмо было действительным, что они еще не являются подписчиками и что мы приняли к сведению их запрос. И нет, отправка электронной почты мгновенно добавляет достаточную задержку для моей системы виртуального хостинга, так что это нежелательно. - person hytromo; 05.02.2015
comment
И мой вопрос остается без ответа: возможно ли, чтобы 2 экземпляра скрипта пытались отправить одно и то же письмо дважды? - person hytromo; 05.02.2015
comment
На это дан ответ в части о бухгалтерии: если вы используете для этого базу данных и она имеет правильный уровень изоляции, непротиворечивость данных гарантируется базой данных: если один процесс установил статус в 1, никто другой не может прочитать 0. - person nlu; 05.02.2015
comment
Очевидно, что если один устанавливает 1, другой не может прочитать 0. Предположим, что мне нужно отправить 2 электронных письма. Первый процесс сначала пытается отправить 1-е электронное письмо, поэтому для его поля обработки устанавливается значение 1. Он успешно отправляет его, и в этот момент начинается 2-й процесс. 1-й процесс теперь обработал 1-е электронное письмо, в то время как 2-й процесс видит только одно электронное письмо (2-е электронное письмо, потому что 1-е было обработано из 1-го процесса). Таким образом, оба процесса переходят ко второму электронному письму ТОЧНО в одно и то же время. Таким образом, они оба будут читать статус 0 (не обрабатываются). Возможен ли такой сценарий? - person hytromo; 05.02.2015
comment
В mysql вы можете создать эксклюзивный доступ с выбором для обновления: dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html - person nlu; 05.02.2015