Skip to content

Commit 0e56e8d

Browse files
committed
外部リンクチェック : 低速サーバー向けに、試行を重ねるたびにアクセス方法とタイムアウト時間を変えるようにした #1669
1 parent c3eaa66 commit 0e56e8d

1 file changed

Lines changed: 31 additions & 7 deletions

File tree

.github/workflows/script/link_check.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import glob
33
import re
44
import os
5+
import socket
56
import urllib.request, urllib.error, urllib.parse
67
import urllib3
8+
import urllib3.util.connection
79
import requests
810
import sys
911
import time
@@ -26,15 +28,29 @@ def retry_sleep(retry: int) -> None:
2628
time.sleep(backoff + random.uniform(0, 15)) # 0〜15秒のジッタ
2729

2830
def check_url(url: str, retry: int = MAX_OUTER_LINK_RETRY) -> tuple[bool, str]:
31+
# 試行を重ねるごとにアクセス方法を変えて (別経路で) 再試行する。1回目で失敗する
32+
# 理由は「リンク切れ」以外に「HEAD非対応・低速サーバ」「一時的なネットワーク不調」
33+
# などがあり、方法を変えると通ることが多いため。
34+
# method : 試行ごとにHEADとGETを交互に切り替える (偶数回目=HEAD, 奇数回目=GET)。
35+
# HEADに未対応・不安定なサーバはGETで、GETが重いだけのサーバはHEADで通る。
36+
# (例: www.dre.vanderbilt.edu はHEADが失敗しGETは通るが応答が非常に遅い)。
37+
# timeout : 試行を重ねるごとに増やし、低速サーバに徐々に猶予を与える。
38+
# IPv4固定はcheck()側でallowed_gai_familyにより行う (下記コメント参照)。
39+
attempt = MAX_OUTER_LINK_RETRY - retry # 0, 1, 2, 3
40+
method = "HEAD" if attempt % 2 == 0 else "GET"
41+
# 2回目 (最初のGET) の時点で低速サーバを取りこぼさない余裕を持たせる。
42+
# 例: www.dre.vanderbilt.edu はGETに実測で最大120秒近くかかるため、2回目=240秒とする。
43+
timeout = 120.0 * (attempt + 1) # 120, 240, 360, 480 秒
2944
try:
3045
headers = {'User-agent': 'Mozilla/5.0'}
31-
# パフォーマンスのため本文は取得せずHEADで確認する。
32-
# ただしallow_redirects=Trueでリダイレクトはrequestsに追わせ、最終的な
33-
# ステータスで判定する (手動でres.urlを辿る再帰をやめ、リトライ回数が
34-
# 途中でリセットされる問題を避ける)。
35-
res = requests.head(url, headers=headers, verify=False, timeout=60.0,
36-
allow_redirects=True)
37-
return res.status_code != 404, str(res.status_code)
46+
# stream=Trueで本文はダウンロードせず、最終的なステータスで判定する。
47+
# allow_redirects=Trueでリダイレクトはrequestsに追わせる (手動でres.urlを
48+
# 辿る再帰をやめ、リトライ回数が途中でリセットされる問題を避ける)。
49+
res = requests.request(method, url, headers=headers, verify=False,
50+
timeout=timeout, allow_redirects=True, stream=True)
51+
status = res.status_code
52+
res.close()
53+
return status != 404, str(status)
3854
except requests.exceptions.TooManyRedirects:
3955
# リダイレクトループ (http↔httpsを行き来する等)。ブラウザではHSTS等で
4056
# 到達できることが多く、リトライしても解消しないため、存在するものとして
@@ -183,6 +199,14 @@ def check(check_inner_link: bool, check_outer_link: bool, url: str) -> bool:
183199
found_error = True
184200

185201
if check_outer_link:
202+
# GitHub-hosted runnerはIPv6アウトバウンド経路を持たない
203+
# (actions/runner-images#668。runner上では ping6 が "Network is unreachable")。
204+
# 接続先がデュアルスタック (A+AAAA) の場合、IPv6アドレスへの接続が
205+
# [Errno 101] Network is unreachable となり、IPv4へのフォールバックが
206+
# 間に合わないと断続的に失敗する (例: www.decadent.org.uk)。
207+
# runnerで実際に到達できるIPv4に固定して、この経路起因の失敗を避ける。
208+
urllib3.util.connection.allowed_gai_family = lambda: socket.AF_INET
209+
186210
if len(url) > 0:
187211
outer_link_dict[url] = ""
188212

0 commit comments

Comments
 (0)