# 定時任務

通常來説，執行定時任務會通過 Linux 的 `crontab` 命令來實現，但現實情況下，並不是所有開發人員都能夠擁有生產環境的服務器去設置定時任務的，這裏 [hyperf/crontab](https://github.com/hyperf/crontab) 組件為您提供了一個 `秒級` 定時任務功能，只需通過簡單的定義即可完成一個定時任務的定義。 

# 安裝

```bash
composer require hyperf/crontab
```

# 使用

## 啟動任務調度器進程

在使用定時任務組件之前，需要先在 `config/autoload/processes.php` 內註冊一下 `Hyperf\Crontab\Process\CrontabDispatcherProcess` 自定義進程，如下：

```php
<?php
// config/autoload/processes.php
return [
    Hyperf\Crontab\Process\CrontabDispatcherProcess::class,
];
```

這樣服務啟動時會啟動一個自定義進程，用於對定時任務的解析和調度分發。   
同時，您還需要將 `config/autoload/crontab.php` 內的 `enable` 配置設置為 `true`，表示開啟定時任務功能，如配置文件不存在可自行創建，配置如下：

```php
<?php
return [
    // 是否開啟定時任務
    'enable' => true,
];
```

## 定義定時任務

### 通過配置文件定義

您可於 `config/autoload/crontab.php` 的配置文件內配置您所有的定時任務，文件返回一個 `Hyperf\Crontab\Crontab[]` 結構的數組，如配置文件不存在可自行創建：

```php
<?php
// config/autoload/crontab.php
use Hyperf\Crontab\Crontab;
return [
    'enable' => true,
    // 通過配置文件定義的定時任務
    'crontab' => [
        // Callback類型定時任務（默認）
        (new Crontab())->setName('Foo')->setRule('* * * * *')->setCallback([App\Task\FooTask::class, 'execute'])->setMemo('這是一個示例的定時任務'),
        // Command類型定時任務
        (new Crontab())->setType('command')->setName('Bar')->setRule('* * * * *')->setCallback([
            'command' => 'swiftmailer:spool:send',
            // (optional) arguments
            'fooArgument' => 'barValue',
            // (optional) options
            '--message-limit' => 1,
        ]),
    ],
];
```

### 通過註解定義

通過 `@Crontab` 註解可以快速完成對一個任務的定義，以下的定義示例與配置文件定義所達到的目的都是一樣的。定義一個名為 `Foo` 每分鐘執行一次 `App\Task\FooTask::execute()` 的定時任務。

```php
<?php
namespace App\Task;

use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Crontab\Annotation\Crontab;
use Hyperf\Di\Annotation\Inject;

/**
 * @Crontab(name="Foo", rule="* * * * *", callback="execute", memo="這是一個示例的定時任務")
 */
class FooTask
{
    /**
     * @Inject()
     * @var \Hyperf\Contract\StdoutLoggerInterface
     */
    private $logger;

    public function execute()
    {
        $this->logger->info(date('Y-m-d H:i:s', time()));
    }
    
    /**
     * @Crontab(rule="* * * * * *", memo="foo")
     */
    public function foo()
    {
        var_dump('foo');
    }
}
```

### 任務屬性

#### name

定時任務的名稱，可以為任意字符串，各個定時任務之間的名稱要唯一。

#### rule

定時任務的執行規則，在分鐘級的定義時，與 Linux 的 `crontab` 命令的規則一致，在秒級的定義時，規則長度從 5 位變成 6 位，在規則的前面增加了對應秒級的節點，也就是 5 位時以分鐘級規則執行，6 位時以秒級規則執行，如 `*/5 * * * * *` 則代表每 5 秒執行一次。注意在註解定義時，規則存在 `\` 符號時，需要進行轉義處理，即填寫 `*\/5 * * * * *`。

#### callback

定時任務的執行回調，即定時任務實際執行的代碼，在通過配置文件定義時，這裏需要傳遞一個 `[$class, $method]` 的數組，`$class` 為一個類的全稱，`$method` 為 `$class` 內的一個 `public` 方法。當通過註解定義時，只需要提供一個當前類內的 `public` 方法的方法名即可，如果當前類只有一個 `public` 方法，您甚至可以不提供該屬性。

#### singleton

解決任務的併發執行問題，任務永遠只會同時運行 1 個。但是這個沒法保障任務在集羣時重複執行的問題。

#### onOneServer

多實例部署項目時，則只有一個實例會被觸發。

#### mutexPool

互斥鎖使用的 `Redis` 連接池。

#### mutexExpires

互斥鎖超時時間，如果定時任務執行完畢，但解除互斥鎖失敗時，互斥鎖也會在這個時間之後自動解除。

#### memo

定時任務的備註，該屬性為可選屬性，沒有任何邏輯上的意義，僅供開發人員查閲幫助對該定時任務的理解。

#### enable

當前任務是否生效。

> 除了 bool 類型，還支持 string 和 array

如果 `enable` 是 `string`，則會調用當前類對應的方法，來判斷此定時任務是否運行

```php
<?php

namespace App\Crontab;

use Carbon\Carbon;
use Hyperf\Crontab\Annotation\Crontab;

/**
 * @Crontab(name="Echo", rule="* * * * * *", callback="execute", enable="isEnable", memo="這是一個示例的定時任務")
 */
class EchoCrontab
{
    public function execute()
    {
        var_dump(Carbon::now()->toDateTimeString());
    }

    public function isEnable(): bool
    {
        return true;
    }
}
```

如果 `enable` 是 `array`，則會調用 `array[0]` 對應的 `array[1]`，來判斷此定時任務是否運行

```php
<?php

namespace App\Crontab;

class EnableChecker
{
    public function isEnable(): bool
    {
        return false;
    }
}
```

```php
<?php

namespace App\Crontab;

use Carbon\Carbon;
use Hyperf\Crontab\Annotation\Crontab;

/**
 * @Crontab(name="Echo", rule="* * * * * *", callback="execute", enable={EnableChecker::class, "isEnable"}, memo="這是一個示例的定時任務")
 */
class EchoCrontab
{
    public function execute()
    {
        var_dump(Carbon::now()->toDateTimeString());
    }

    public function isEnable(): bool
    {
        return true;
    }
}

```

### 調度分發策略

定時任務在設計上允許通過不同的策略來調度分發執行任務，目前僅提供了 `多進程執行策略`、`協程執行策略` 兩種策略，默認為 `多進程執行策略`，後面的迭代會增加更多更強的策略。

> 當使用協程風格服務時，請使用 協程執行策略。

#### 更改調度分發策略

通過在 `config/autoload/dependencies.php` 更改 `Hyperf\Crontab\Strategy\StrategyInterface` 接口類所對應的實例來更改目前所使用的策略，默認情況下使用 `Worker 進程執行策略`，對應的類為 `Hyperf\Crontab\Strategy\WorkerStrategy`，如我們希望更改策略為一個新的策略，比如為 `App\Crontab\Strategy\FooStrategy`，那麼如下：

```php
<?php
return [
    \Hyperf\Crontab\Strategy\StrategyInterface::class => \App\Crontab\Strategy\FooStrategy::class,
];
```

##### Worker 進程執行策略 [默認]

策略類：`Hyperf\Crontab\Strategy\WorkerStrategy`   

默認情況下使用此策略，即為 `CrontabDispatcherProcess` 進程解析定時任務，並通過進程間通訊輪詢傳遞執行任務到各個 `Worker` 進程中，由各個 `Worker` 進程以協程來實際運行執行任務。

##### TaskWorker 進程執行策略

策略類：`Hyperf\Crontab\Strategy\TaskWorkerStrategy`   

此策略為 `CrontabDispatcherProcess` 進程解析定時任務，並通過進程間通訊輪詢傳遞執行任務到各個 `TaskWorker` 進程中，由各個 `TaskWorker` 進程以協程來實際運行執行任務，使用此策略需注意 `TaskWorker` 進程是否配置了支持協程。

##### 多進程執行策略

策略類：`Hyperf\Crontab\Strategy\ProcessStrategy`   

此策略為 `CrontabDispatcherProcess` 進程解析定時任務，並通過進程間通訊輪詢傳遞執行任務到各個 `Worker` 進程和 `TaskWorker` 進程中，由各個進程以協程來實際運行執行任務，使用此策略需注意 `TaskWorker` 進程是否配置了支持協程。

##### 協程執行策略

策略類：`Hyperf\Crontab\Strategy\CoroutineStrategy`   

此策略為 `CrontabDispatcherProcess` 進程解析定時任務，並在進程內為每個執行任務創建一個協程來運行。

## 運行定時任務

當您完成上述的配置後，以及定義了定時任務後，只需要直接啟動 `Server`，定時任務便會一同啟動。   
在您啟動後，即便您定義了足夠短週期的定時任務，定時任務也不會馬上開始執行，所有定時任務都會等到下一個分鐘週期時才會開始執行，比如您啟動的時候是 `10 時 11 分 12 秒`，那麼定時任務會在 `10 時 12 分 00 秒` 才會正式開始執行。
