在這個章節,我們會使用到 User Service 與 Main App,請參考第四章節所提到的內容建立你的本地開發環境。
延續前幾章,你可以將專案內的 init.php
加入下述程式碼,將 MainApp 也宣告進來:
//省略其他服務組態設定
ServiceList::addLocalService(
name: "MainApp",
address: "127.0.0.1",
port: 80,
isHttps: false
);
當一個服務呼叫另一個服務時,有需多可能的原因會導致呼叫失敗,例如:網路延遲、服務超時,或大量的資源競爭等。此時,重試(Retry)機制就顯得尤為重要。若我們僅因為一次的請求失敗,就放棄了整個請求的機會,是一件很可惜的事情。在很多情況下,嘗試重新發送該請求也是一種開發成本較低的方案:
Action 也支援 Retry 的設定,首先請在你的開發環境中建立一個名為 errorendpoint.php
的檔案並鍵入以下內容:
<?php
$errorLimit = $_GET["error_limit"] ?? 0;
$filePath = __DIR__ . "/error_count.txt";
if (!file_exists($filePath)) {
file_put_contents($filePath, '0');
}
$count = (int)file_get_contents($filePath);
$message = "";
if ($count >= $errorLimit) {
http_response_code(200);
$message = "OK";
$count = 0;
} else {
http_response_code(500);
$message = "ERROR";
$count++;
}
file_put_contents($filePath, $count);
file_put_contents(__DIR__ . "/error_log.txt", date("Y-m-d H:i:s") . " " . $count . " " . http_response_code() . PHP_EOL, FILE_APPEND);
echo $message . PHP_EOL;
這是一個模擬 API 失敗與成功的端點,當你呼叫這個端點時,會隨著你所傳入的 error_limit
決定在第幾次呼叫時回傳 HTTP 200 的成功訊息。而上述測試用端點會將每次的請求資訊寫入一個 error_log.txt
檔案中。
接著,我們來建立本次測試的主要檔案:
<?php
require_once './init.php';
use SDPMlab\Anser\Service\Action;
$errorLimit = $_GET["error_limit"] ?? 0;
$action = (new Action(
serviceName: "MainApp",
method: "GET",
path: "/errorendpoint.php"
))->setOptions([
"query" => [
"error_limit" => $errorLimit
]
])->setRetry(retry: (int)$errorLimit, retryDelay: 1.0);
$token = $action->do();
header("Content-Type: application/json");
echo json_encode([
"status" => "OK"
]) . PHP_EOL;
這部份的程式碼將會呼叫剛才我們建立的測試用端點,並包含以下特性
setRetry()
允許你傳入下列參數:
int $retry
:重試最大次數,本次設定為 error_limitfloat $retryDelay
:重試間隔秒數,本次設定為每次重試間隔 1 秒打開你的 Postman 試試看執行這個 PHP 檔案,我們將 Query Params 的 error_limit
設定為 3
:
你會發現,響應的處理時間耗時了 3 秒鐘,此時你的開發環境應該會多出一個檔案叫做 error_log.txt
:
沒錯,正如我們的預期,每間隔一秒鐘會重複呼叫相同的服務端點,直到到達 error_limit
的次數,再以 200
的狀態碼進行最後回傳。
如同上述的例子所示,透過簡單的設定,我們可以確保在面對臨時的服務中斷或其他非預期的錯誤時,請求仍然有機會被正確地處理。
在 Anser 的微服務的架構中,每一個服務呼叫或資料交換都被視為一次「行動(Action)」。但這些動作,往往不只是單純的傳送請求和接收回應,還可能涉及一系列的前置和後置工作。這就是「過濾器」或「Filter」發揮其作用的地方。
首先,我們先從一個常見的場景著手:使用者驗證。在許多服務中,我們經常需要驗證使用者的身份,以確定他們是否有權限進行某些操作。而這個驗證過程,通常是透過 API key 或 token 來進行。
首先,請在你的開發環境中建立一個檔案 UserAuthFilters.php
並貼上以下內容:
<?php
namespace Filters;
require_once './vendor/autoload.php';
use SDPMlab\Anser\Service\FilterInterface;
use SDPMlab\Anser\Service\ActionInterface;
class UserAuthFilters implements FilterInterface
{
public function beforeCallService(ActionInterface $action)
{
//get in request Header Authorization
$apiKey = $_SERVER['HTTP_AUTHORIZATION'] ?? "";
if (empty($apiKey)) {
http_response_code(401);
header("Content-Type: application/json");
echo json_encode([
"code" => 401,
"status" => "error",
"message" => "Unauthorized, The authorization header is missing."
]);
exit;
}
$headersOptions = $action->getOption('headers') ?? [];
$headersOptions["Authorization"] = $apiKey;
$action->addOption("headers", $headersOptions);
}
public function afterCallService(ActionInterface $action)
{
//do nothing
}
}
UserAuthFilters
實作了 SDPMlab\Anser\Service\FilterInterface
,在 Action 執行前你可以透過 beforeCallService()
取得 Action 的實體;之後,在 HTTP 200 響應且 doneHandler()
執行前,你可以透過 afterCallService()
處理尚未經過任何修改過的 Action。
而上述過濾器專門用於驗證使用者的 API key,當 Action 被執行前,它會首先檢查請求標頭中是否包含有效的 API key,如果缺少或無效,它將直接回傳一個錯誤訊息,而不進行後續的服務呼叫。
若本次的請求符合我們的需要,那麼就會透過即將被執行的 Action 實體,取出裡面的 Request Option 設定符合 API 需要的正確標頭,再將他設定回 Action 實體中。
接著,我們再建立一個新的檔案:
<?php
require_once './init.php';
require_once './UserAuthFilters.php';
use SDPMlab\Anser\Service\Action;
use SDPMlab\Anser\Exception\ActionException;
use Psr\Http\Message\ResponseInterface;
use Filters\UserAuthFilters;
$action = (new Action(
serviceName: "UserService",
method: "GET",
path: "/api/v1/user"
))
->setFilter(className: UserAuthFilters::class)
->doneHandler(static function(
ResponseInterface $response,
Action $runtimeAction
){
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
$runtimeAction->setMeaningData([
"code" => 200,
"msg" => "success",
"data" => $data['data']
]);
})->failHandler(function (
ActionException $e
) {
if($e->isClientError()){
$msg = $e->getAction()->getResponse()->getBody()->getContents();
$error = json_decode($msg, true)['error'];
$e->getAction()->setMeaningData([
"code" => $e->getAction()->getResponse()->getStatusCode(),
"msg" => $error
]);
}else if ($e->isServerError()){
$e->getAction()->setMeaningData([
"code" => 500,
"msg" => "server error"
]);
}else if($e->isConnectError()){
$e->getAction()->setMeaningData([
"code" => 500,
"msg" => $e->getMessage()
]);
}
});
header("Content-Type: application/json");
if($action->do()->isSuccess()){
echo json_encode($action->getMeaningData()) . PHP_EOL;
exit;
}
$error = $action->getMeaningData();
http_response_code($error['code']);
echo json_encode([
"code" => $error['code'],
"msg" => $error['msg']
]) . PHP_EOL;
exit;
關注上述程式碼第 15 行的位置,你可以透過 setFilter(string $className)
傳入完整的類別名稱字串(包含 Namespace 的類別名稱),將所需的 Filter 設定進 Action 之中。若你重複呼叫 setFilter(string $className)
就可以將多個 Filter 給設定進 Action ,而這些 Filter 將會依序執行。
當然,你也可以單獨使用 addBeforeFilter(string $className)
或 addAfterFilter(string $className)
單獨將過濾器類別中的前濾器或後濾器單獨設定至 Action 中。
你可以打開 Postman 試試,首先我們先不傳入任何 API Key:
如同我們的預期,再未傳入任何 API Key 的情況下我們成功透過前濾器阻止了 Action 的執行。
接著,我們來試試看傳入 API Key 的結果:
一切都符合我們的預期,API Key 被正確傳遞給 User Service 了!
Filter 機制提供了一個靈活的工具,開發者能夠輕鬆地進行前置和後置的工作,無論是驗證或日誌記錄,透過這種機制,我們可以將業務邏輯和非業務邏輯(例如驗證)區分開來,使代碼更為模組化,並提高其可維護性和可讀性。
本章,我們了解了服務重試和過濾器的概念與應用,Anser Action 在基於 Guzzle7 程式庫下,整合了一些常用的元件與設計模式,希望能滿足開發時所需的模組化與可用性等特性。期待透過這些工具和策略,能夠使開發人員確保服務能穩定運行且能快速應對變化。