著者は、 Write for DOnations プログラムの一環として、 Girls WhoCodeを選択して寄付を受け取りました。

序章

Linuxでは、用途の広い crontabツールを使用して、特定の時間にバックグラウンドで長時間実行されるタスクを処理できます。 デーモンは反復的なタスクを実行するのに最適ですが、1つの制限があります。タスクを実行できるのは、最小時間間隔1分のみです。

ただし、多くのアプリケーションでは、ユーザーエクスペリエンスの低下を防ぐために、ジョブをより頻繁に実行することをお勧めします。 たとえば、ジョブキューモデルを使用してWebサイトでファイル処理タスクをスケジュールしている場合、大幅な待機はエンドユーザーに悪影響を及ぼします。

もう1つのシナリオは、ジョブキューモデルを使用して、クライアントがアプリケーションで特定のタスク(たとえば、受信者に送金する)を完了した後、テキストメッセージまたは電子メールをクライアントに送信するアプリケーションです。 ユーザーが確認メッセージの配信まで1分待たなければならない場合、ユーザーはトランザクションが失敗したと考え、同じトランザクションを繰り返そうとする可能性があります。

これらの課題を克服するために、crontabデーモンが1分後に再度呼び出すのを待つ間、タスクを60秒間繰り返しループして処理するPHPスクリプトをプログラムできます。 PHPスクリプトがcrontabデーモンによって初めて呼び出されると、ユーザーを待たせることなく、アプリケーションのロジックに一致する期間でタスクを実行できます。

このガイドでは、Ubuntu20.04サーバー上にサンプルcron_jobsデータベースを作成します。 次に、tasksテーブルと、PHPwhile(...){...}ループおよびsleep()関数を使用して5秒間隔でテーブル内のジョブを実行するスクリプトを設定します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

  • root以外のユーザーでセットアップされたUbuntu20.04サーバー。 Ubuntu20.04を使用したサーバーの初期設定ガイドに従ってください。

  • サーバーにセットアップされたLAMPスタック。 Linux、Apache、MySQL、PHP(LAMP)スタックをUbuntu20.04にインストールする方法ガイドを参照してください。 このチュートリアルでは、ステップ4 —Webサイトの仮想ホストの作成をスキップできます。

ステップ1—データベースのセットアップ

このステップでは、サンプルデータベースとテーブルを作成します。 まず、サーバーにSSHして、rootとしてMySQLにログインします。

  1. sudo mysql -u root -p

MySQLサーバーのrootパスワードを入力し、ENTERを押して続行します。 次に、次のコマンドを実行して、cron_jobsデータベースを作成します。

  1. CREATE DATABASE cron_jobs;

データベースのroot以外のユーザーを作成します。 PHPからcron_jobsデータベースに接続するには、このユーザーの資格情報が必要です。 EXAMPLE_PASSWORDを強力な値に置き換えることを忘れないでください。

  1. CREATE USER 'cron_jobs_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
  2. GRANT ALL PRIVILEGES ON cron_jobs.* TO 'cron_jobs_user'@'localhost';
  3. FLUSH PRIVILEGES;

次に、cron_jobsデータベースに切り替えます。

  1. USE cron_jobs;
Output
Database changed

データベースを選択したら、tasksテーブルを作成します。 この表には、cronジョブによって自動的に実行されるいくつかのタスクを挿入します。 cronジョブを実行するための最小時間間隔は1分であるため、後でこの設定をオーバーライドするPHPスクリプトをコーディングし、代わりに5秒間隔でジョブを実行します。

今のところ、tasksテーブルを作成します。

  1. CREATE TABLE tasks (
  2. task_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3. task_name VARCHAR(50),
  4. queued_at DATETIME,
  5. completed_at DATETIME,
  6. is_processed CHAR(1)
  7. ) ENGINE = InnoDB;

タスクテーブルに3つのレコードを挿入します。 queued_at列のMySQLNOW()関数を使用して、タスクがキューに入れられた現在の日時を記録します。 また、completed_at列の場合は、MySQL CURDATE()関数を使用して、デフォルトの時刻を00:00:00に設定します。 後で、タスクが完了すると、スクリプトは次の列を更新します。

  1. INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 1', NOW(), CURDATE(), 'N');
  2. INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 2', NOW(), CURDATE(), 'N');
  3. INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 3', NOW(), CURDATE(), 'N');

INSERTコマンドを実行した後、出力を確認します。

Output
Query OK, 1 row affected (0.00 sec) ...

tasksテーブルに対してSELECTステートメントを実行して、データが適切に配置されていることを確認します。

  1. SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;

すべてのタスクのリストがあります。

Output
+---------+-----------+---------------------+---------------------+--------------+ | task_id | task_name | queued_at | completed_at | is_processed | +---------+-----------+---------------------+---------------------+--------------+ | 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 00:00:00 | N | | 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 00:00:00 | N | | 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 00:00:00 | N | +---------+-----------+---------------------+---------------------+--------------+ 3 rows in set (0.00 sec)

completed_at列の時間は00:00:00に設定されており、次に作成するPHPスクリプトによってタスクが処理されると、この列が更新されます。

MySQLコマンドラインインターフェイスを終了します。

  1. QUIT;
Output
Bye

これで、cron_jobsデータベースとtasksテーブルが配置され、ジョブを処理するPHPスクリプトを作成できるようになりました。

ステップ2—5秒後にタスクを実行するPHPスクリプトを作成する

このステップでは、PHP while(...){...}ループとsleep関数の組み合わせを使用して、5秒ごとにタスクを実行するスクリプトを作成します。

nanoを使用して、Webサーバーのルートディレクトリにある新しい/var/www/html/tasks.phpファイルを開きます。

  1. sudo nano /var/www/html/tasks.php

次に、<?phpタグの後に新しいtry {ブロックを作成し、手順1で作成したデータベース変数を宣言します。 EXAMPLE_PASSWORDをデータベースユーザーの実際のパスワードに置き換えることを忘れないでください。

/var/www/html/tasks.php
<?php
try {
    $db_name     = 'cron_jobs';
    $db_user     = 'cron_jobs_user';
    $db_password = 'EXAMPLE_PASSWORD';
    $db_host     = 'localhost';

次に、新しいPDO(PHPデータオブジェクト)クラスを宣言し、属性ERRMODE_EXCEPTIONを設定してPDOエラーをキャッチします。 また、ATTR_EMULATE_PREPARESfalseに切り替えて、ネイティブMySQLデータベースエンジンがエミュレーションを処理できるようにします。 プリペアドステートメントを使用すると、SQLクエリとデータを別々に送信して、セキュリティを強化し、SQLインジェクション攻撃の可能性を減らすことができます。

/var/www/html/tasks.php

    $pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);       

次に、$loop_expiry_timeという名前の新しい変数を作成し、現在の時刻に60秒を加えた値に設定します。 次に、新しいPHPwhile(time() < $loop_expiry_time) {ステートメントを開きます。 ここでの考え方は、現在の時刻(time())が変数$loop_expiry_timeと一致するまで実行されるループを作成することです。

[label /var/www/html/tasks.php]       

    $loop_expiry_time = time() + 60;

    while (time() < $loop_expiry_time) { 

次に、tasksテーブルから未処理のジョブを取得する準備済みのSQLステートメントを宣言します。

[label /var/www/html/tasks.php]   

        $data = [];
        $sql  = "select 
                 task_id
                 from tasks
                 where is_processed = :is_processed
                 ";

SQLコマンドを実行し、tasksテーブルからis_processed列がNに設定されているすべての行をフェッチします。 これは、行が処理されないことを意味します。

[label /var/www/html/tasks.php]  

        $data['is_processed'] = 'N';  

        $stmt = $pdo->prepare($sql);
        $stmt->execute($data);

次に、PHP while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {...}ステートメントを使用して取得した行をループし、別のSQLステートメントを作成します。 今回は、SQLコマンドにより、処理されたタスクごとにis_processed列とcompleted_at列が更新されます。 これにより、タスクを複数回処理しないようになります。

[label /var/www/html/tasks.php]  

        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 
            $data_update   = [];         
            $sql_update    = "update tasks set 
                              is_processed  = :is_processed,
                              completed_at  = :completed_at
                              where task_id = :task_id                                 
                              ";

            $data_update   = [		          
                             'is_processed' => 'Y',                          
                             'completed_at' => date("Y-m-d H:i:s"),
                             'task_id'      => $row['task_id']                         
                             ];
            $stmt = $pdo->prepare($sql_update);
            $stmt->execute($data_update);
        }

注:処理するキューが大きい場合(たとえば、1秒あたり100,000レコード)、MySQLよりも高速であるためRedisサーバーでジョブをキューに入れることを検討してください。ジョブキューモデルの実装になります。 それでも、このガイドではより小さなデータセットを処理します。

最初のPHPwhile (time() < $loop_expiry_time) {ステートメントを閉じる前に、sleep(5);ステートメントを含めて、ジョブの実行を5秒間一時停止し、サーバーリソースを解放します。

ビジネスロジックとタスクの実行速度に応じて、5秒の期間を変更できます。 たとえば、タスクを1分間に3回処理する場合は、この値を20秒に設定します。

} catch (PDOException $ex) { echo $ex->getMessage(); }ブロック内のPDOエラーメッセージをcatchすることを忘れないでください。

[label /var/www/html/tasks.php]         
        sleep(5); 

        }       
		
} catch (PDOException $ex) {
    echo $ex->getMessage(); 
}

完全なtasks.phpファイルは次のようになります。

/var/www/html/tasks.php
<?php
try {
    $db_name     = 'cron_jobs';
    $db_user     = 'cron_jobs_user';
    $db_password = 'EXAMPLE_PASSWORD';
    $db_host     = 'localhost';

    $pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);               

    $loop_expiry_time = time() + 60;

    while (time() < $loop_expiry_time) { 
        $data = [];
        $sql  = "select 
                 task_id
                 from tasks
                 where is_processed = :is_processed
                 ";

        $data['is_processed'] = 'N';             

        $stmt = $pdo->prepare($sql);
        $stmt->execute($data);

        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 
            $data_update   = [];         
            $sql_update    = "update tasks set 
                              is_processed  = :is_processed,
                              completed_at  = :completed_at
                              where task_id = :task_id                                 
                              ";

            $data_update   = [		          
                             'is_processed' => 'Y',                          
                             'completed_at' => date("Y-m-d H:i:s"),
                             'task_id'      => $row['task_id']                         
                             ];
            $stmt = $pdo->prepare($sql_update);
            $stmt->execute($data_update);
        }   
       
        sleep(5); 

        }       
		
} catch (PDOException $ex) {
    echo $ex->getMessage(); 
}

CTRL + XYENTERの順に押してファイルを保存します。

/var/www/html/tasks.phpファイルのロジックのコーディングが完了したら、次のステップで1分ごとにファイルを実行するようにcrontabデーモンをスケジュールします。

ステップ3—1分後に実行するPHPスクリプトのスケジュール

Linuxでは、crontabファイルにコマンドを入力することにより、規定の時間後に自動的に実行されるようにジョブをスケジュールできます。 このステップでは、毎分後に/var/www/html/tasks.phpスクリプトを実行するようにcrontabデーモンに指示します。 したがって、nanoを使用して/etc/crontabファイルを開きます。

  1. sudo nano /etc/crontab

次に、ファイルの最後に次を追加して、1分ごとにhttp://localhost/tasks.phpを実行します。

/ etc / crontab
...
* * * * * root /usr/bin/wget -O - http://localhost/tasks.php

ファイルを保存して閉じます。

このガイドは、cronジョブがどのように機能するかについての基本的な知識があることを前提としています。 Cronを使用してUbuntuでタスクを自動化する方法に関するガイドを読むことを検討してください。

前に示したように、cronデーモンは1分ごとにtasks.phpファイルを実行しますが、ファイルが初めて実行されると、開いているタスクをさらに60秒間ループします。 ループ時間が経過するまでに、cronデーモンがファイルを再度実行し、プロセスが続行されます。

/etc/crontabファイルを更新して閉じた後、crontabデーモンはtasksテーブルに挿入したMySQLタスクの実行をすぐに開始する必要があります。 すべてが期待どおりに機能しているかどうかを確認するには、次にcron_jobsデータベースにクエリを実行します。

ステップ4—ジョブの実行を確認する

このステップでは、データベースをもう一度開いて、tasks.phpファイルがcrontabによって自動的に実行されたときにキューに入れられたジョブを処理しているかどうかを確認します。

rootとしてMySQLサーバーに再度ログインします。

  1. sudo mysql -u root -p

次に、MySQLサーバーのルートパスワードを入力し、ENTERを押して続行します。 次に、データベースに切り替えます。

  1. USE cron_jobs;
Output
Database changed

tasks テーブルに対してSELECTステートメントを実行します。

SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;

次のような出力が表示されます。 completed_at列では、タスクは5秒間隔で処理されています。 また、is_processed列がY、つまりYESに設定されているため、タスクは完了としてマークされています。

Output
+---------+-----------+---------------------+---------------------+--------------+ | task_id | task_name | queued_at | completed_at | is_processed | +---------+-----------+---------------------+---------------------+--------------+ | 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 06:30:01 | Y | | 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 06:30:06 | Y | | 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 06:30:11 | Y | +---------+-----------+---------------------+---------------------+--------------+ 3 rows in set (0.00 sec)

これにより、PHPスクリプトが期待どおりに機能していることが確認されます。 crontabデーモンによって設定された1分間の制限をオーバーライドすることにより、より短い時間間隔でタスクを実行しました。

結論

このガイドでは、Ubuntu20.04サーバーにサンプルデータベースをセットアップしました。 次に、テーブルにジョブを作成し、PHPwhile(...){...}ループおよびsleep()関数を使用して5秒間隔で実行します。 次に、タスクを1分間に複数回実行する必要があるジョブキューベースのアプリケーションを実装する場合は、このチュートリアルのロジックを使用してください。

その他のPHPチュートリアルについては、PHPトピックページをご覧ください。