From 578b9934f5ec67de8d3020d63b2ad27af32ef6b9 Mon Sep 17 00:00:00 2001 From: shibafu Date: Mon, 10 Aug 2020 13:32:47 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=A1=E3=82=BF=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AE=E8=A8=98?= =?UTF-8?q?=E9=8C=B2=E3=81=A8=E3=83=AA=E3=83=88=E3=83=A9=E3=82=A4=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=81=AE=E9=81=A9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Metadata.php | 58 ++++++++++++++ .../ResolverCircuitBreakException.php | 16 ++++ .../UncaughtResolverException.php | 10 +++ app/Services/MetadataResolveService.php | 80 ++++++++++++++----- 4 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 app/MetadataResolver/ResolverCircuitBreakException.php create mode 100644 app/MetadataResolver/UncaughtResolverException.php diff --git a/app/Metadata.php b/app/Metadata.php index 546f155..61ee308 100644 --- a/app/Metadata.php +++ b/app/Metadata.php @@ -2,6 +2,8 @@ namespace App; +use Carbon\CarbonInterface; +use GuzzleHttp\Exception\RequestException; use Illuminate\Database\Eloquent\Model; class Metadata extends Model @@ -19,4 +21,60 @@ class Metadata extends Model { 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 ?? ''; + } } diff --git a/app/MetadataResolver/ResolverCircuitBreakException.php b/app/MetadataResolver/ResolverCircuitBreakException.php new file mode 100644 index 0000000..748c545 --- /dev/null +++ b/app/MetadataResolver/ResolverCircuitBreakException.php @@ -0,0 +1,16 @@ +formatter = $formatter; } + /** + * メタデータをキャッシュまたはリモートに問い合わせて取得します。 + * @param string $url メタデータを取得したいURL + * @return Metadata 取得できたメタデータ + * @throws DeniedHostException アクセス先がブラックリスト入りしているため取得できなかった場合にスロー + * @throws UncaughtResolverException Resolver内で例外が発生して取得できなかった場合にスロー + */ public function execute(string $url): Metadata { // URLの正規化 @@ -34,34 +45,61 @@ class MetadataResolveService throw new DeniedHostException($url); } - return DB::transaction(function () use ($url) { + DB::beginTransaction(); + try { + $metadata = Metadata::find($url); + // 無かったら取得 // TODO: ある程度古かったら再取得とかありだと思う - $metadata = Metadata::find($url); - if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) { + if ($metadata == null || $metadata->needRefresh()) { + if ($metadata === null) { + $metadata = new Metadata(['url' => $url]); + } + + if ($metadata->error_count >= self::CIRCUIT_BREAK_COUNT) { + throw new ResolverCircuitBreakException($metadata->error_count, $url); + } + try { $resolved = $this->resolver->resolve($url); - $metadata = Metadata::updateOrCreate(['url' => $url], [ - '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を残す + } catch (\Exception $e) { 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; - }); + } catch (UncaughtResolverException $e) { + // Metadataにエラー情報を記録するため + DB::commit(); + throw $e; + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } } }