はじめに
複雑な Web アプリを作っていると、結果をリアルタイムで返す必要のない処理や、返したくても同期で実行するには時間が掛かりすぎるような重い処理を行いたい場合が出てきます。 こういう場合は、レスポンスはすぐに返してしまって、サーバ側にジョブとして溜めておいてバックグラウンドで処理するのが定石です。
このような手法を ジョブキューイング とか タスクキューイング などと呼びます。
Symfony2 + BCCResqueBundle
Resque は、バックエンドに Redis を利用する GitHub 製のジョブキューサーバです。 Resque はもともと Ruby のライブラリですが、PHP でも実装されていて、Symfony2 のバンドルも何種類か実装されています。
今回はその中で、BCCResqueBundle を使った実装方法を紹介したいと思います。 例として、ジョブキューを使ってメール送信を行ってみましょう。
Redis をインストール
まずはサーバマシンに Redis をインストールしておきます。
Mac OS X 環境であれば brew で簡単にインストールできます。 異なる環境の方は環境に合わせて適切にインストールしてください。
$ brew install redis
Symfony2 プロジェクトの準備
次に、Symfony2 のプロジェクトを準備しましょう。 とりあえずインストールして動かしてみるまでの手順は Symfony2入門 にまとめてありますので、Symfony2 に慣れていない方は参考にしてみてください。
SMTP の設定
メールを送信するので、app/config/parameters.yml
で適切に SMTP を設定しておいてください。
ここでは Gmail のサーバを使う例を示します。
parameters:
mailer_transport: gmail
mailer_host: smtp.gmail.com
mailer_user: your_account@gmail.com
mailer_password: your_password
locale: ja
secret: ThisTokenIsNotSoSecretChangeIt # 30 文字のランダム文字列に置き換える.
BCCResqueBundle をインストール
composer でインストール
BCCResqueBundle をインストールします。Packagist に居る ので普通に composer でインストールできます。
$ php composer.phar require bcc/resque-bundle dev-master
dev-master
を使いたいので、composer.json
の"minimum-stability"
は"dev"
にしておいてください。
AppKernel に追加
AppKernel にバンドルを登録します。
<?php
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new Quartet\Bundle\JobQueTestBundle\QuartetJobQueTestBundle(),
new BCC\ResqueBundle\BCCResqueBundle(),
);
}
}
バンドルの設定を追記
app/config/config.yml
に以下を追記します。詳細は こちら を参考にしてください。
# BCCResqueBundle
bcc_resque:
class: BCC\ResqueBundle\Resque
vendor_dir: %kernel.root_dir%/../vendor
redis:
host: localhost
port: 6379
database: 0
auto_retry: [0, 10, 60]
また、ジョブから mailer サービスでメールを送信するための準備として、同じく app/config/config.yml
で swiftmailer
の設定も以下のように変更しておきます。
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
# spool: { type: memory }
spool:
type: file
path: %kernel.cache_dir%/swiftmailer/spool
ルーティングの読み込み
app/config/routing.yml
に以下を追記します。
# BCCResqueBundle
BCCResqueBundle:
resource: "@BCCResqueBundle/Resources/config/routing.xml"
prefix: /resque
ジョブクラスを作成
ジョブのクラス (バックグラウンドで実行したい処理を行うクラス) を作成します。
ここでは、src/ベンダ名/Bundle/バンドル名/Job/
配下に作ってみます。
ジョブクラスは、BCC\ResqueBundle\Job
または BCC\ResqueBundle\ContainerAwareJob
を継承して作成します。
ジョブの中で Symfony2 のサービスコンテナを使用したい場合は、BCC\ResqueBundle\ContainerAwareJob
を継承している必要があります。
<?php
namespace Quartet\Bundle\JobQueTestBundle\Job;
use BCC\ResqueBundle\ContainerAwareJob as BaseJob;
class SendMailJob extends BaseJob
{
public function run($args)
{
$container = $this->getContainer();
$message = \Swift_Message::newInstance()
->setFrom($args['from'], $args['from_name'])
->setTo($args['to'])
->setSubject($args['subject'])
->setBody($args['body'], $args['is_html'] ? 'text/html' : 'text/plain')
;
$container->get('mailer')->send($message);
// Job からだと spool に溜まったメールが自動で送られないので, 手動で送信.
$container->get('swiftmailer.spool')->flushQueue($container->get('swiftmailer.mailer.default.transport.real'));
}
}
run
メソッドをオーバーライドして、ジョブの処理を実装します。
mailer サービスを使ってメールを送信したいので、ContainerAwareJob
を継承しました。
呼び出し元はまだ実装していませんが、from
, from_name
, to
, subject
, body
, is_html
というパラメータが渡ってくることを想定しています。
# 最後の一行はおまじないということで説明は割愛させてくださいm(_ _)m
とりあえず Default コントローラからジョブをエンキュー
とりあえず、src/ベンダ名/Bundle/バンドル名/Controller/DefaultController.php
にアクションメソッドを一つ作って、GET アクセスしただけでジョブをエンキューするようにしてみます。
<?php
namespace Quartet\Bundle\JobQueTestBundle\Controller;
use Quartet\Bundle\JobQueTestBundle\Job\SendMailJob;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DefaultController extends Controller
{
/**
* @Route("/")
*/
public function queueAction()
{
$job = new SendMailJob();
$job->queue = 'sendmail'; // キュー名. あとで Worker プロセスを起動するときに使います.
$job->args = [
'from' => 'no-reply@test.com',
'from_name' => '差出人名',
'to' => 'target@test.com',
'subject' => '件名',
'body' => '本文',
'is_html' => false,
];
$this->get('bcc_resque.resque')->enqueue($job);
// Job 管理画面へリダイレクト.
return $this->redirect($this->generateUrl('BCCResqueBundle_homepage'));
}
}
とりあえずの例なので、エンキューしたら BCCResqueBundle が用意してくれている Job 管理画面にリダイレクトさせています。
Redis サーバおよび Worker プロセスを起動
さて、ここまでで実装は完了です。ジョブを処理してくれるサーバを起動しましょう。
Redis サーバ
まずは Redis サーバを起動します。
$ redis-server /usr/local/etc/redis.conf
Worker プロセス
Worker プロセスとは、ジョブキューのシステムにおいて、サーバ側でジョブを消化してくれるプロセスのことです。 BCCResqueBundle では、以下のコマンドによって起動します。
$ php app/console bcc:resque:worker-start "sendmail" --quiet
"sendmail"
の部分はキュー名を表します。
コントローラでジョブをエンキューするときに
$job->queue = 'sendmail';
としたので、これと同じ名前の Worker を起動しています。
--quiet
は、ログ (app/logs/resque.log
) に出力する情報を少なくするためのオプションです。
コマンドの詳細については こちら を参照してください。
ちなみに、Worker プロセスを終了する場合は
$ php app/console bcc:resque:worker-stop
で、起動中の Worker 一覧を確認し、
$ php app/console bcc:resque:worker-stop [Worker 名]
で、指定した Worker を終了します。
起動中の全ての Worker を終了したい場合は、
$ php app/console bcc:resque:worker-stop -a
で OK です。
動かしてみる
準備が整ったので、実際に動かしてみましょう。
ブラウザで localhost/アプリ名/web/app_dev.php/
にアクセスしてみてください。
queueAction
が実行され、localhost/アプリ名/web/app_dev.php/resque/
にリダイレクトされます。
この時点では、sendmail
という名前のキューにジョブが 1 つ挿入されている状態です。
しばらく待ってからページをリロードすると、以下のように状態が変わっているはずです。
sendmail
キューを処理する Worker の、Processed (処理済み) の欄にジョブが移りました。
正常にジョブが消化されたので、メールが届いているはずです。
おわりに
この例で実装したソースは こちら で公開していますので、よろしければご参考にどうぞ。
app/config/parameters.yml
src/Quartet/Bundle/JobQueTestBundle/Controller/DefaultController.php
は環境に合わせて修正が必要ですのでご注意を。