Товарищи, господа, пацаны и т. д. Есть вопрос. Если мы отправляем почту функцией mail(), то всё происходит быстро (как и надо), но проконтролировать отправку письма мы не можем. Или можем?! Можем, но геморройно. Так же функция mail() завязана на локальный sendmail и многое зависит от его настройки. Если мы отправляем почту с помощью сокетов через какой-нибудь SMTP-сервер, то всё просто великолепно, мы читаем ответы сервера, отвечаем ему и можем контролировать отправку писем. Но, если у нас начинаются проблемы со связью, или удалённый SMTP-сервер не отвечает по каким-либо причинам, то скрипт начинает подвисать, правильно?! Я ещё не проверял, но подвисать должен, по идеи. А подвисать он будет на попытке соединения с SMTP-сервером. И подвисать он должен круто. А в чём проблема?! А есть скрипт, который должен быстро отрабатывать и ещё оправлять почту. Если в скрипте мы отправляем почту функцией mail(), то проблем нет. Письмо улетает в очередь локального сендмейла и уже он разруливает его отправку. Но если мы юзаем сокеты, то появляется проблема. Мы не можем даже гарантировать, что скрипт отработает. Может просто отвалиться по таймауту. Устанет ждать ответа от SMTP-сервера и отвалится. И будет жопа. Вот и вопрос. Что делать. Я хочу сокеты. Думаю надо всю корреспонденцию класть в базу - в очередь на отправку. А рассылать каким-нибудь скриптом-демоном, или по крону каждую минуту дёргать скрипт. Но есть ещё одна идея. Пишем скриптик mail.php и кладём его куда-нибудь, да в тот же корень сайта. Во время работы скрипта, когда надо отправить почту, мы кладём письмо в базу и дёргаем наш скрипт. file_get_contents('./mail.php?pid=PostID'); Скрипт получает ИД письма, идёт за ним в базу и потихонечку отправляет. Но, как быть если письмо не получится отправить, да, придётся делать так, чтобы скрипт ещё и не отправленные пытался отправить снова. Блин, устал думать... Кто знает решение?! Колитесь. =)
Hight Да не дергнет file_get_contents файл. Где ты такое вычитал? Это же не wget в линухе. file_get_contents тупо прочитает файл без его запуска.
письма сразу должны отправляться или это какая то рассылка? вешай на сокет таймаут, выкидывай эксепшен и отлавливай не отправленные письма в базу, потом проходись чем нибудь по базе на предмет таких писем если без базы то можно написать демона что бы он свой лист с такими письмами обрабатывал сам сразу из памяти
Hight Если хочется отсылать не задерживая скрипт, то делай очередь в базе и сделай демон, который будет делать рассылку - благо на PHP это плёвое дело, а с PHP 5.3 с менеджментом памяти стало ажурно - главное не забывать делать unset почаще. К тому же у сокетов есть настройки таймаутов - юзай В последствии можно сделать многозадачность и прочие плюшки. Я вот вообще для линяги с pnctl_fork замутил демона, который делает целую кучу обработок - работает как часы - главное логгинг и аккуратная обработка возможных проблемных мест. Исключения рулят безбожно. Апельсин file_get_contents, запрашивающий скрипт через http тработает так же, как бы отработало с браузером - для веб сервера и PHP никакой разницы.
Hight Ну так как у меня Yii, то я использую компоненту CConsoleCommand и на базе неё просто довернул демонизацию - но по сути тоже самое можно сделать и без Yii - в данном случае тут удобство запуска и функционал AR и прочие плюшки - костяк же сам по себе вполне жить может: PHP: <?php /** * Created by PhpStorm. * User: psihius * Date: 2010.5.10 * Time: 15:59:29 * To change this template use File | Settings | File Templates. */ /** * @class LaunchCommand * @extends CConsoleCommand */ class XDaemonCommand extends CConsoleCommand { private $args; private $_pid_file; private $_pid; private $_run; private $_daemon_name; private $_logs; private $processors; /** * Initialize the daemon * @param $args * @return void */ public function run($args) { $this->args = $args; $this->_daemon_name = str_replace('Command', '', __CLASS__); $this->initDirs(); $this->log('Starting daemon...', true); $this->_run = true; $this->processors = array(); if (!$this->isRunning()) { // Start the daemon $this->demonize(); } else { $this->log('Daemon is running, exiting...', true); } } final private function demonize() { $this->log('Forking...', true); $this->_pid = pcntl_fork(); if ($this->_pid === -1) { $this->log('Can\'t fork!', true); exit(0); } elseif ($this->_pid > 0) { $this->log('Sucessfully spawned with ID '.$this->_pid, true); Yii::app()->end(); exit(0); } if (posix_setsid() < 0) { $this->log('Failed to make child as master process...'); Yii::app()->end(); exit(-1); } Yii::getLogger()->autoFlush = 1; if (function_exists('setproctitle')) { setproctitle($this->_daemon_name); } // Save PID file_put_contents($this->_pid_file, getmypid()); // Register signal handler pcntl_signal(SIGTERM, array($this, 'signalHandler')); $last_shedulue = time() - (24 * 3600); // Go into daemon loop while ($this->_run) { try { // Make criteria to fetch daemon process queue $criteria = new CDbCriteria(array( 'condition' => 'start_on <= NOW()', 'order' => 'start_on ASC', )); /** @var $queue DaemonQueue[] * Fetch transactions */ $queue = DaemonQueue::model()->with('transaction')->findAll($criteria); if (is_array($queue)) { /** @var $item DaemonQueue */ foreach ($queue as $item) { $class = ucfirst($item->keyword).'Processor'; try { /** @var $processor Processor */ $processor = new $class(); $processor->run($item->transaction, $item, $this); $processor->cleanup(); unset($processor); } catch (ProcessorException $e) { // Catch and log non-critical, application-logic specific errors if ($item->transaction instanceof Transactions) { Yii::log('Transaction #'.$item->transaction->id.PHP_EOL.$e->getMessage().$e->getDetails(), 'warning'); $item->transaction->status = Transactions::SUPPORT; $item->transaction->save(); Operations::addEntry($item->transaction, $e->getMessage()); if (!$item->delayed()) { $item->delete(); } } else { Yii::log($e->getMessage().$e->getDetails(), 'warning'); } } catch (CException $e) { // This is some serious stuff, include any info we can get if ($item->transaction instanceof Transactions) { Yii::log('Transaction #'.$item->transaction->id.PHP_EOL.$e->getMessage().PHP_EOL.$e->getTraceAsString(), 'error'); $item->transaction->status = Transactions::SUPPORT; $item->transaction->save(); Operations::addEntry($item->transaction, $e->getMessage()); if (!$item->delayed()) { $item->delete(); } } else { Yii::log($e->getMessage().PHP_EOL.$e->getTraceAsString(), 'error'); } } unset($item); if (!$this->_run) { break; } } } $time = time(); if ($time - $last_shedulue > 300) { $last_shedulue = $time; $processor = new StaleTransactionProcessor(); $processor->run(); unset($processor); } unset($criteria, $queue); // Sleep for 0.5 second, so CPU doesn't get loaded 100% if ($this->_run) { usleep(500000); } } catch (Exception $e) { $this->_run = false; $this->log($e->getMessage().PHP_EOL.$e->getTraceAsString()); } } $this->log('Shutdown complete, cleaning up...'); foreach ($this->processors as $k => $v) { unset($v); $this->processors[$k] = null; } if (file_exists($this->_pid_file)) { unlink($this->_pid_file); } } final public function signalHandler($signo) { switch($signo) { case SIGTERM: $this->log('SIGTERM received, starting shutdown...'); $this->_run = false; break; default: break; } } /** * @return void */ final private function initDirs() { // Init PID file path $pid_dir = Yii::app()->getRuntimePath().DIRECTORY_SEPARATOR.'pids'.DIRECTORY_SEPARATOR.$this->_daemon_name.DIRECTORY_SEPARATOR; if (!file_exists($pid_dir)) { mkdir($pid_dir, 0644, true); } $this->_pid_file = $pid_dir.DIRECTORY_SEPARATOR.md5(__CLASS__.implode('', $this->args)).'.pid'; // Init LOG dir $this->_logs = Yii::app()->getRuntimePath().DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR; if (!file_exists($this->_logs)) { mkdir($this->_logs, 0644, true); } } /** * @return bool */ final private function isRunning() { $running = false; if (file_exists($this->_pid_file)) { // Get the PID $this->_pid = file_get_contents($this->_pid_file); if (file_exists('/proc/'.$this->_pid) || posix_kill($this->_pid, 0)) { $running = true; } else { $this->pidRemove(); } } return $running; } /** * @param $message * @param bool $echo * @return void */ final private function log($message, $echo = false) { $error = posix_get_last_error(); if ($error !== 0) { $message .= PHP_EOL.'Posix error description: '.posix_strerror($error); } $message .= PHP_EOL; error_log('['.date('Y-m-d H:i:s').'] '.$message, 3, $this->_logs.'daemon.log'); if ($echo) { echo $message; } } final private function pidRemove() { if (!unlink($this->_pid_file)) { $this->log('Error while deleting PID file!', true); } } } В нутри while ($this->_run) 98% кода application specific. Общая идея в PHP реализуется буквально 10-15 строками - остальное это красивости и удобство для логгирования. Вообще собираюсь свою наработку допилить и предложить свою компоненту CConsoleDaemon
Psih Угу, спасибо. Всё понятно. Насколько я понимаю у тебя можно прямо в админке на кнопочку нажать и консольная команда запустит скрипт. Там же можно и посмотреть работает ли демон? Занятно. А я как-то всё скриптами в init.d запускаю, демоны ведь. Привычка. Ещё раз спасибо за код, появились новые идеи.
Hight Ну не совсем так, в админке я не делал ничего такого. Но по сути стопать/запускать можно и по кнопке - были бы у WEB права делать "system('php '.$path_to_daemon.'/console.php daemonname')" и "system('kill -9 '.get_file_contents($path_to_pid_file))" - всё дорабатывается по мере надобности. Вообще запуск через init.d штука хорошая - я собираюсь доработать это дело в ближайшую неделю-две - всё же удобнее администрировать и перезапускать. В общем мысль в том, что делается всё крайне легко если есть желание разобраться
У меня PHP-FPM от юзверя работает. Если им cmd дёрну, то и скрипт от этого юзверя заведётся, всё должно быть хорошо. Да, по идее всё должно быть круто, попробую сделать в админке интерфейс для запуска и остановки демонов, удобно ведь. Всё само заводит при старте сервера, можно легко остановить демон, перезапустить. Класс.
igordata Расслабься, я с разбега не смог с решением определиться. В бошке куча мусора, понимаешь, трудно что-то одно выбрать.
Hight я в таких случаях забиваю на пару дней - благо неотложных дел хватает. а потом... Семен Семеныч! я честно говоря не понимаю вобще что тебя смущает. Было спрошено (как я понял): скрипт, который отправляет через сокеты и авторизацию может недождаться ответа и отпасть. Было предложено и мной и падабу метить неотправленные и пытаться снова. Можно сделать счетчик попыток. Я так понял что ты хочешь гарантировать себе отправку почты - каждого письма без исключения. Чем тебе не нравится поставить их в очередь к почтовику - я не понял. Ну видимо есть на то причины. Но даже если ты отправишь письмо точно, то не факт что оно дойдет. По сути гарантировать доставку ты не можешь в принципе. Если только не сам себе их шлешь. Т.е. ты можешь гарантировать себе отправку, договорившись с сервером лично. Но какая нафик разница от отправки через mail() - нипанятна. По сути сам факт правильной настройки сервера уже должен тебя достаточно успокоить. Почта не протухнет, если даже инет пропадет. Отправится когда восстановится. Случаи, когда почта не отправилась с настроенного сервера редки и ими можно пренебречь, т.к. неработающий сервер - это и без того великая проблема. В любом случае, т.к. письма которые не удалось отправить ты можешь отметить при сокетном способе отправки, то ты можешь после устранения неполадок возобновить и таки отправить почту. в чем беда? или я дебил, и ктонить мог бы мне сказать в чем косяк.
igordata Ты немного не понял. Я знаю как работает почта, хорошо знаю. Я знаю, что не могу точно проверить дошло письмо или нет. Но это меня не волнует. Я хочу сделать велосипед, свой "сендмейл". С блэкджеком и шлюхами. Чтобы не запариваться с настройкой постфикса на серваке. Мне нужно независимое решение, гибкое, своё. Вопрос в том как лучше это сделать. Лично я сейчас думаю так. Есть скрипт, он отрабатывает, чота там делает и кладёт в БД письма. Есть демон, он работает всегда, каждую секунду-2-3 он ломится в БД и ищет там не отправленные письма, выбирает, отправляет, помечает как отправленные. Шикарно правда. Почему я задумался. У меня есть класс для работы с почтой. Он умеет отправлять письма, но только с помощью функции mail(). Я его хочу научить отправляет письма с помощью сокетов. Если бы я в метод отправки письма встроил бы функциональность с сокетами, это бы привело к пиздецам. Как я уже говорил, функция mail() отрабатывает мгновенно, она просто вызывает локальный сендмейл и отдаёт ему письмо, а он уже разруливает его отправку. Но это не прокатывает с сокетами, там возможны задержки, крутые тормоза... Значит эту задачку надо выносить куда-то, чтобы общение с SMTP-сервером происходило не в методе отправки почты, а где-то в другом месте, где пофигу тормоза. Вот и получается, что я делаю в админке флаг - юзать SMTP-сервер, там же скармливаю в конфиг параметры сервера (адрес, логин, пароль)... В методе отправки почты смотрю как отправлять, если флага нет - юзать функцию mail(), флаг есть - класть письмо в БД, пусть демон отправки разруливает отправку. А?!
Hight кстати, возми Zend_Mail - там все есть: атачи, html и.т.д. и реюз смтп соединения для батч отправки
Psih У меня тоже есть аттач и хтмл. У меня всё есть, мне извращений не хватает. Что-то вот меня на них потянуло =)
Hight тогда вот тебе такое извращение: не демон а либо плодящийся демон, клонящийся. либо крон - экземпляр скрипта каждую секунду. новорожденный скрипт лезет в бд, включает транзакцию, берет сотню писем, помечает их флагом "забрал на отсыл", коммитит транзакцию и рассылает себе, вылетая с таймаутами даже - метит что "не удалось" и мол надо еще разок. если отослал - помечает что "ушло" или удаляет. если он ковыряется долго - не страшно. Следующий скрипт лезет в бд и выбирает письма, которые еще не "забрали на отсыл" или не "ушло"... ну ты понел. если случился форс-мажор и сервак внезапно ребутнулся - надо предусмотреть таймаут, при котором "забранные на отсыл" письма требуется слать заново. при таком раскладе не страшно, если демоны будут друг друга перекрывать. Не страшно, если кто-то из них подвисает на отсылке. Не страшно даже если умрет. это как муровьи - они будут приходить, брать, срать. Т.е. слать.