はじめに

複雑な 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.ymlswiftmailer の設定も以下のように変更しておきます。

# 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/ にリダイレクトされます。

image

この時点では、sendmail という名前のキューにジョブが 1 つ挿入されている状態です。 しばらく待ってからページをリロードすると、以下のように状態が変わっているはずです。

image

sendmail キューを処理する Worker の、Processed (処理済み) の欄にジョブが移りました。 正常にジョブが消化されたので、メールが届いているはずです。

おわりに

この例で実装したソースは こちら で公開していますので、よろしければご参考にどうぞ。

  • app/config/parameters.yml
  • src/Quartet/Bundle/JobQueTestBundle/Controller/DefaultController.php

は環境に合わせて修正が必要ですのでご注意を。