メタデータ取得エラーの記録とリトライ制限の適用
This commit is contained in:
parent
4acebcec7e
commit
578b9934f5
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Metadata extends Model
|
class Metadata extends Model
|
||||||
@ -19,4 +21,60 @@ class Metadata extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany(Tag::class)->withTimestamps();
|
return $this->belongsToMany(Tag::class)->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function needRefresh(): bool
|
||||||
|
{
|
||||||
|
return $this->isExpired() || $this->error_at !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->expires_at !== null && $this->expires_at < now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeException(CarbonInterface $error_at, \Exception $exception): self
|
||||||
|
{
|
||||||
|
$this->prepareFieldsOnError();
|
||||||
|
$this->error_at = $error_at;
|
||||||
|
$this->error_exception_class = get_class($exception);
|
||||||
|
$this->error_body = $exception->getMessage();
|
||||||
|
if ($exception instanceof RequestException) {
|
||||||
|
$this->error_http_code = $exception->getCode();
|
||||||
|
} else {
|
||||||
|
$this->error_http_code = null;
|
||||||
|
}
|
||||||
|
$this->error_count++;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeError(CarbonInterface $error_at, string $body, ?int $httpCode = null): self
|
||||||
|
{
|
||||||
|
$this->prepareFieldsOnError();
|
||||||
|
$this->error_at = $error_at;
|
||||||
|
$this->error_exception_class = null;
|
||||||
|
$this->error_body = $body;
|
||||||
|
$this->error_http_code = $httpCode;
|
||||||
|
$this->error_count++;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearError(): self
|
||||||
|
{
|
||||||
|
$this->error_at = null;
|
||||||
|
$this->error_exception_class = null;
|
||||||
|
$this->error_body = null;
|
||||||
|
$this->error_http_code = null;
|
||||||
|
$this->error_count = 0;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareFieldsOnError()
|
||||||
|
{
|
||||||
|
$this->title = $this->title ?? '';
|
||||||
|
$this->description = $this->description ?? '';
|
||||||
|
$this->image = $this->image ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
16
app/MetadataResolver/ResolverCircuitBreakException.php
Normal file
16
app/MetadataResolver/ResolverCircuitBreakException.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 規定回数以上の解決失敗により、メタデータの取得が不能となっている場合にスローされます。
|
||||||
|
*/
|
||||||
|
class ResolverCircuitBreakException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(int $errorCount, string $url, Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct("{$errorCount}回失敗しているためメタデータの取得を中断しました: {$url}", 0, $previous);
|
||||||
|
}
|
||||||
|
}
|
10
app/MetadataResolver/UncaughtResolverException.php
Normal file
10
app/MetadataResolver/UncaughtResolverException.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetadataResolver内で未キャッチの例外が発生した場合にスローされます。
|
||||||
|
*/
|
||||||
|
class UncaughtResolverException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
@ -5,14 +5,18 @@ namespace App\Services;
|
|||||||
use App\Metadata;
|
use App\Metadata;
|
||||||
use App\MetadataResolver\DeniedHostException;
|
use App\MetadataResolver\DeniedHostException;
|
||||||
use App\MetadataResolver\MetadataResolver;
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\MetadataResolver\ResolverCircuitBreakException;
|
||||||
|
use App\MetadataResolver\UncaughtResolverException;
|
||||||
use App\Tag;
|
use App\Tag;
|
||||||
use App\Utilities\Formatter;
|
use App\Utilities\Formatter;
|
||||||
use GuzzleHttp\Exception\TransferException;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class MetadataResolveService
|
class MetadataResolveService
|
||||||
{
|
{
|
||||||
|
/** @var int メタデータの解決を中断するエラー回数。この回数以上エラーしていたら処理は行わない。 */
|
||||||
|
const CIRCUIT_BREAK_COUNT = 5;
|
||||||
|
|
||||||
/** @var MetadataResolver */
|
/** @var MetadataResolver */
|
||||||
private $resolver;
|
private $resolver;
|
||||||
/** @var Formatter */
|
/** @var Formatter */
|
||||||
@ -24,6 +28,13 @@ class MetadataResolveService
|
|||||||
$this->formatter = $formatter;
|
$this->formatter = $formatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メタデータをキャッシュまたはリモートに問い合わせて取得します。
|
||||||
|
* @param string $url メタデータを取得したいURL
|
||||||
|
* @return Metadata 取得できたメタデータ
|
||||||
|
* @throws DeniedHostException アクセス先がブラックリスト入りしているため取得できなかった場合にスロー
|
||||||
|
* @throws UncaughtResolverException Resolver内で例外が発生して取得できなかった場合にスロー
|
||||||
|
*/
|
||||||
public function execute(string $url): Metadata
|
public function execute(string $url): Metadata
|
||||||
{
|
{
|
||||||
// URLの正規化
|
// URLの正規化
|
||||||
@ -34,34 +45,61 @@ class MetadataResolveService
|
|||||||
throw new DeniedHostException($url);
|
throw new DeniedHostException($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($url) {
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$metadata = Metadata::find($url);
|
||||||
|
|
||||||
// 無かったら取得
|
// 無かったら取得
|
||||||
// TODO: ある程度古かったら再取得とかありだと思う
|
// TODO: ある程度古かったら再取得とかありだと思う
|
||||||
$metadata = Metadata::find($url);
|
if ($metadata == null || $metadata->needRefresh()) {
|
||||||
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
|
if ($metadata === null) {
|
||||||
|
$metadata = new Metadata(['url' => $url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($metadata->error_count >= self::CIRCUIT_BREAK_COUNT) {
|
||||||
|
throw new ResolverCircuitBreakException($metadata->error_count, $url);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$resolved = $this->resolver->resolve($url);
|
$resolved = $this->resolver->resolve($url);
|
||||||
$metadata = Metadata::updateOrCreate(['url' => $url], [
|
} catch (\Exception $e) {
|
||||||
'title' => $resolved->title,
|
|
||||||
'description' => $resolved->description,
|
|
||||||
'image' => $resolved->image,
|
|
||||||
'expires_at' => $resolved->expires_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tagIds = [];
|
|
||||||
foreach ($resolved->normalizedTags() as $tagName) {
|
|
||||||
$tag = Tag::firstOrCreate(['name' => $tagName]);
|
|
||||||
$tagIds[] = $tag->id;
|
|
||||||
}
|
|
||||||
$metadata->tags()->sync($tagIds);
|
|
||||||
} catch (TransferException $e) {
|
|
||||||
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
|
|
||||||
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
|
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
|
||||||
throw $e;
|
|
||||||
|
// TODO: 何か制御用の例外を下位で使ってないか確認したほうが良い?その場合、雑catchできない
|
||||||
|
$metadata->storeException(now(), $e);
|
||||||
|
$metadata->save();
|
||||||
|
throw new UncaughtResolverException(implode(': ', [
|
||||||
|
$metadata->error_count . '回目のメタデータ取得失敗', get_class($e), $e->getMessage()
|
||||||
|
]), 0, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$metadata->fill([
|
||||||
|
'title' => $resolved->title,
|
||||||
|
'description' => $resolved->description,
|
||||||
|
'image' => $resolved->image,
|
||||||
|
'expires_at' => $resolved->expires_at
|
||||||
|
]);
|
||||||
|
$metadata->clearError();
|
||||||
|
$metadata->save();
|
||||||
|
|
||||||
|
$tagIds = [];
|
||||||
|
foreach ($resolved->normalizedTags() as $tagName) {
|
||||||
|
$tag = Tag::firstOrCreate(['name' => $tagName]);
|
||||||
|
$tagIds[] = $tag->id;
|
||||||
|
}
|
||||||
|
$metadata->tags()->sync($tagIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
return $metadata;
|
return $metadata;
|
||||||
});
|
} catch (UncaughtResolverException $e) {
|
||||||
|
// Metadataにエラー情報を記録するため
|
||||||
|
DB::commit();
|
||||||
|
throw $e;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user