From 3349c204906080034599dbfee35ad7e08e14baab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 22 Aug 2026 23:43:25 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9B=85=20Sync=202026-08-22=2023:43:25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dae/Makefile | 6 +- daed/Makefile | 6 +- internet-detector-mod-email/Makefile | 4 +- internet-detector-mod-modem-restart/Makefile | 4 +- internet-detector-mod-telegram/Makefile | 4 +- internet-detector/Makefile | 4 +- .../usr/lib/lua/internet-detector/main.lua | 61 +++-- .../modules/mod_public_ip.lua | 2 +- luci-app-internet-detector/Makefile | 4 +- luci-app-mosdns/Makefile | 4 +- .../resources/view/mosdns/basic.js | 24 ++ luci-app-mosdns/po/templates/mosdns.pot | 96 +++++--- luci-app-mosdns/po/zh_Hans/mosdns.po | 96 +++++--- luci-app-mosdns/root/etc/init.d/mosdns | 10 + mosdns/Makefile | 2 +- ...cache-add-proactive-prefetch-support.patch | 227 ++++++++++++++++++ 16 files changed, 450 insertions(+), 104 deletions(-) create mode 100644 mosdns/patches/213-feat-cache-add-proactive-prefetch-support.patch diff --git a/dae/Makefile b/dae/Makefile index 7ee47975d..d5513791c 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae -PKG_VERSION:=2026.08.13 -PKG_RELEASE:=36 +PKG_VERSION:=2026.08.21 +PKG_RELEASE:=37 -PKG_SOURCE:=dae-src-2026.08.13-0d20959710a0.tar.gz +PKG_SOURCE:=dae-src-2026.08.21-8138f66f2c6d.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/daed/Makefile b/daed/Makefile index 106f921c6..2d4757c96 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed -PKG_VERSION:=2026.08.13 -PKG_RELEASE:=47 +PKG_VERSION:=2026.08.21 +PKG_RELEASE:=48 -PKG_SOURCE:=daed-src-2026.08.13-4a38e0f66b3d.tar.gz +PKG_SOURCE:=daed-src-2026.08.21-aeef51da973f.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/internet-detector-mod-email/Makefile b/internet-detector-mod-email/Makefile index 496cc13a1..6f3803fff 100644 --- a/internet-detector-mod-email/Makefile +++ b/internet-detector-mod-email/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector-mod-email -PKG_VERSION:=1.7.3 -PKG_RELEASE:=3 +PKG_VERSION:=1.7.4 +PKG_RELEASE:=4 PKG_MAINTAINER:=gSpot include $(INCLUDE_DIR)/package.mk diff --git a/internet-detector-mod-modem-restart/Makefile b/internet-detector-mod-modem-restart/Makefile index f3cfb719e..c7681dec4 100644 --- a/internet-detector-mod-modem-restart/Makefile +++ b/internet-detector-mod-modem-restart/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector-mod-modem-restart -PKG_VERSION:=1.7.3 -PKG_RELEASE:=3 +PKG_VERSION:=1.7.4 +PKG_RELEASE:=4 PKG_MAINTAINER:=gSpot include $(INCLUDE_DIR)/package.mk diff --git a/internet-detector-mod-telegram/Makefile b/internet-detector-mod-telegram/Makefile index 993cbd11e..9f49f8bc6 100644 --- a/internet-detector-mod-telegram/Makefile +++ b/internet-detector-mod-telegram/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector-mod-telegram -PKG_VERSION:=1.7.3 -PKG_RELEASE:=3 +PKG_VERSION:=1.7.4 +PKG_RELEASE:=4 PKG_MAINTAINER:=gSpot include $(INCLUDE_DIR)/package.mk diff --git a/internet-detector/Makefile b/internet-detector/Makefile index 199eb7234..b3ef1a173 100644 --- a/internet-detector/Makefile +++ b/internet-detector/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector -PKG_VERSION:=1.7.3 -PKG_RELEASE:=5 +PKG_VERSION:=1.7.4 +PKG_RELEASE:=6 PKG_MAINTAINER:=gSpot include $(INCLUDE_DIR)/package.mk diff --git a/internet-detector/files/usr/lib/lua/internet-detector/main.lua b/internet-detector/files/usr/lib/lua/internet-detector/main.lua index d4c90cb19..6ff4c7f94 100644 --- a/internet-detector/files/usr/lib/lua/internet-detector/main.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/main.lua @@ -12,8 +12,8 @@ local uci = require("uci") -- Default settings local InternetDetector = { - appName = "internet-detector", - libDir = "/usr/lib/lua", + appName = "internet-detector", + libDir = "/usr/lib/lua", logLevels = { emerg = { level = syslog.LOG_EMERG, num = 0 }, alert = { level = syslog.LOG_ALERT, num = 1 }, @@ -24,18 +24,32 @@ local InternetDetector = { info = { level = syslog.LOG_INFO, num = 6 }, debug = { level = syslog.LOG_DEBUG, num = 7 }, }, - pingCmd = "/bin/ping", - pingParams = "-c 1", - curlExec = "/usr/bin/curl", - curlParams = '-s -g --no-keepalive --head --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0"', - mode = 0, -- 0: disabled, 1: Service, 2: UI detector - loggingLevel = 6, - hostname = "OpenWrt", - uiRunTime = 30, - noModules = false, - uiAvailModules = { mod_public_ip = true }, - debug = false, - serviceConfig = { + pingCmd = "/bin/ping", + pingParams = "-c 1", + curlExec = "/usr/bin/curl", + curlParams = '-s -g --no-keepalive --head --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0"', + curlHttpOkCodes = { + [200] = true, + [202] = true, + [203] = true, + [204] = true, + [206] = true, + [300] = true, + [301] = true, + [302] = true, + [303] = true, + [304] = true, + [307] = true, + [308] = true, + }, + mode = 0, -- 0: disabled, 1: Service, 2: UI detector + loggingLevel = 6, + hostname = "OpenWrt", + uiRunTime = 30, + noModules = false, + uiAvailModules = { mod_public_ip = true }, + debug = false, + serviceConfig = { hosts = { [1] = "8.8.8.8", [2] = "1.1.1.1", @@ -43,7 +57,7 @@ local InternetDetector = { urls = { [1] = "https://www.google.com", }, - check_type = 0, -- 0: TCP, 1: ICMP + check_type = 0, -- 0: TCP, 1: ICMP, 2: URL test tcp_port = 53, icmp_packet_size = 56, interval_up = 30, @@ -58,13 +72,13 @@ local InternetDetector = { iface = nil, instance = nil, }, - modules = {}, - parsedHosts = {}, - proxyAuthString = "", - proxyString = "", - uiCounter = 0, - pidFile = nil, - statusFile = nil, + modules = {}, + parsedHosts = {}, + proxyAuthString = "", + proxyString = "", + uiCounter = 0, + pidFile = nil, + statusFile = nil, } InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName) InternetDetector.modulesDir = string.format( @@ -280,7 +294,6 @@ function InternetDetector:TCPConnectionToHost(host, port) if not ok then self:debugOutput(string.format( "SOCKET ERROR: %s, %s", errMsg, errNum)) - unistd.close(sock) return retCode end @@ -380,7 +393,7 @@ function InternetDetector:checkURL(url) if retCode == 0 and data then httpCode = self:getHTTPCode(data) end - return (httpCode ~= 200) and 1 or 0 + return (not self.curlHttpOkCodes[httpCode]) and 1 or 0 end function InternetDetector:exit() diff --git a/internet-detector/files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua b/internet-detector/files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua index 1affda952..10e0415c0 100644 --- a/internet-detector/files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua @@ -27,7 +27,7 @@ local Module = { requestAttempts = 2, timeout = 3, curlExec = "/usr/bin/curl", - curlParams = '-s -g --no-keepalive --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0"', + curlParams = '-s -g --no-keepalive --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0"', providers = { opendns1 = { name = "opendns1", type = "dns", host = "myip.opendns.com", diff --git a/luci-app-internet-detector/Makefile b/luci-app-internet-detector/Makefile index 9ff60c7fd..06ec0fd64 100644 --- a/luci-app-internet-detector/Makefile +++ b/luci-app-internet-detector/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-internet-detector -PKG_VERSION:=1.7.3 -PKG_RELEASE:=3 +PKG_VERSION:=1.7.4 +PKG_RELEASE:=4 LUCI_TITLE:=LuCI support for internet-detector LUCI_DEPENDS:=+internet-detector LUCI_PKGARCH:=all diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile index 3cab2d6b8..806de52b9 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns -PKG_VERSION:=1.7.6 -PKG_RELEASE:=13 +PKG_VERSION:=1.7.7 +PKG_RELEASE:=14 LUCI_TITLE:=LuCI Support for mosdns LUCI_PKGARCH:=all diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js index 8d9adc02e..b09eafc2b 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js @@ -320,6 +320,30 @@ return view.extend({ o.default = 86400; o.depends('cache', '1'); + o = s.taboption('advanced', form.Flag, 'prefetch', _('Cache Prefetching'), + _('Proactively refresh hot cache entries in the background before they expire.')); + o.rmempty = false; + o.default = false; + o.depends('cache', '1'); + + o = s.taboption('advanced', form.Value, 'prefetch_before_expire', _('Prefetch Before Expire'), + _('Prefetch when the remaining TTL is less than this value (in seconds).')); + o.datatype = 'and(uinteger,min(1))'; + o.default = 10; + o.depends('prefetch', '1'); + + o = s.taboption('advanced', form.Value, 'prefetch_min_hits', _('Prefetch Min Hits'), + _('Minimum cache hits required since the last refresh to trigger a prefetch.')); + o.datatype = 'and(uinteger,min(1))'; + o.default = 3; + o.depends('prefetch', '1'); + + o = s.taboption('advanced', form.Value, 'prefetch_scan_interval', _('Prefetch Scan Interval'), + _('Interval for the background thread to scan the cache for prefetching (in seconds).')); + o.datatype = 'and(uinteger,min(1))'; + o.default = 5; + o.depends('prefetch', '1'); + o = s.taboption('advanced', form.Flag, 'dump_file', _('Cache Dump'), _('Save the cache locally and reload the cache dump on the next startup')); o.rmempty = false; diff --git a/luci-app-mosdns/po/templates/mosdns.pot b/luci-app-mosdns/po/templates/mosdns.pot index aa79b681d..ca471c206 100644 --- a/luci-app-mosdns/po/templates/mosdns.pot +++ b/luci-app-mosdns/po/templates/mosdns.pot @@ -5,11 +5,11 @@ msgstr "Content-Type: text/plain; charset=UTF-8" msgid "360 Public DNS (DNS over HTTPS)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:356 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:380 msgid "ADblock Source" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:395 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:419 msgid "API Listen port" msgstr "" @@ -82,7 +82,7 @@ msgstr "" msgid "Apple domains optimization" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:330 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:354 msgid "Auto Save Cache Interval" msgstr "" @@ -105,7 +105,7 @@ msgstr "" msgid "Basic Setting" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:348 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:372 msgid "" "Block DNS RR Type 65 records (HTTPS/SVCB, used for HTTP/3, ECH, etc.), force " "using only A/AAAA records." @@ -129,10 +129,14 @@ msgid "" "resolvers you specify as upstreams" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:323 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:347 msgid "Cache Dump" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:323 +msgid "Cache Prefetching" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 msgid "Check And Update" msgstr "" @@ -178,7 +182,7 @@ msgstr "" msgid "CloudFlare Public DNS (1.1.1.1)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:380 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:404 msgid "Cloudflare IP Ranges" msgstr "" @@ -198,7 +202,7 @@ msgstr "" msgid "Config File" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:426 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:450 msgid "Configuration Editor" msgstr "" @@ -216,7 +220,7 @@ msgid "" "supports domain matching rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:375 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:399 msgid "Custom IP" msgstr "" @@ -258,7 +262,7 @@ msgstr "" msgid "Default Config" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:347 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:371 msgid "Disable RR Type 65 (HTTPS/SVCB)" msgstr "" @@ -280,7 +284,7 @@ msgstr "" msgid "Enable Auto Database Update" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:352 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:376 msgid "Enable DNS ADblock" msgstr "" @@ -302,15 +306,15 @@ msgid "Enable this option fallback policy forces forwarding to remote DNS" msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:142 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:368 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:392 msgid "Enabled" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:453 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:477 msgid "Enter the GeoIP.dat category to be exported, Allow add multiple tags" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:448 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:472 msgid "Enter the GeoSite.dat category to be exported, Allow add multiple tags" msgstr "" @@ -350,8 +354,8 @@ msgstr "" msgid "Every Wednesday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:449 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:454 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:473 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:478 msgid "Export directory: /var/mosdns" msgstr "" @@ -363,7 +367,7 @@ msgstr "" msgid "Failed to start update." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:403 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:427 msgid "Flush DNS Cache" msgstr "" @@ -375,7 +379,7 @@ msgstr "" msgid "Flushing DNS Cache Success." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:401 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:425 msgid "" "Flushing DNS Cache will clear any IP addresses or DNS records from MosDNS " "cache." @@ -411,7 +415,7 @@ msgstr "" msgid "GeoData Export" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:452 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:476 msgid "GeoIP Tags" msgstr "" @@ -419,7 +423,7 @@ msgstr "" msgid "GeoIP Type" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:447 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:471 msgid "GeoSite Tags" msgstr "" @@ -458,7 +462,7 @@ msgstr "" msgid "IP Address" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:381 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:405 msgid "" "IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: +Date: Sat, 22 Aug 2026 22:56:30 +0800 +Subject: [PATCH] feat(cache): add proactive prefetch support +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +plugins: + - tag: lazy_cache + type: cache + args: + size: 1024 + # 开启预读功能 (默认为 false) + prefetch: true + # 在 TTL 还剩 10 秒时尝试预读 (默认为 10) + prefetch_before_expire: 10 + # 至少被命中 2 次才触发预读 (默认为 3) + prefetch_min_hits: 2 + # 后台扫描周期,每 3 秒扫描一次 (默认为 5) + prefetch_scan_interval: 3 + +Signed-off-by: sbwml +--- + plugin/executable/cache/cache.go | 113 +++++++++++++++++++++++++++++-- + plugin/executable/cache/utils.go | 2 + + 2 files changed, 110 insertions(+), 5 deletions(-) + +--- a/plugin/executable/cache/cache.go ++++ b/plugin/executable/cache/cache.go +@@ -27,6 +27,7 @@ import ( + "io" + "net/http" + "os" ++ "reflect" + "strconv" + "sync" + "sync/atomic" +@@ -69,15 +70,22 @@ const ( + var _ sequence.RecursiveExecutable = (*Cache)(nil) + + type Args struct { +- Size int `yaml:"size"` +- LazyCacheTTL int `yaml:"lazy_cache_ttl"` +- DumpFile string `yaml:"dump_file"` +- DumpInterval int `yaml:"dump_interval"` ++ Size int `yaml:"size"` ++ LazyCacheTTL int `yaml:"lazy_cache_ttl"` ++ DumpFile string `yaml:"dump_file"` ++ DumpInterval int `yaml:"dump_interval"` ++ Prefetch bool `yaml:"prefetch"` ++ PrefetchBeforeExpire int `yaml:"prefetch_before_expire"` ++ PrefetchMinHits int `yaml:"prefetch_min_hits"` ++ PrefetchScanInterval int `yaml:"prefetch_scan_interval"` + } + + func (a *Args) init() { + utils.SetDefaultUnsignNum(&a.Size, 1024) + utils.SetDefaultUnsignNum(&a.DumpInterval, 600) ++ utils.SetDefaultUnsignNum(&a.PrefetchBeforeExpire, 10) ++ utils.SetDefaultUnsignNum(&a.PrefetchMinHits, 3) ++ utils.SetDefaultUnsignNum(&a.PrefetchScanInterval, 5) + } + + type Cache struct { +@@ -94,6 +102,11 @@ type Cache struct { + hitTotal prometheus.Counter + lazyHitTotal prometheus.Counter + size prometheus.GaugeFunc ++ ++ prefetchTotal prometheus.Counter ++ prefetchFailTotal prometheus.Counter ++ capturedNext *sequence.ChainWalker ++ captureOnce sync.Once + } + + func Init(bp *coremain.BP, args any) (any, error) { +@@ -160,6 +173,16 @@ func NewCache(args *Args, opts Opts) *Ca + Help: "The total number of queries that hit the expired cache", + ConstLabels: lb, + }), ++ prefetchTotal: prometheus.NewCounter(prometheus.CounterOpts{ ++ Name: "prefetch_total", ++ Help: "The total number of successful proactive prefetch operations", ++ ConstLabels: lb, ++ }), ++ prefetchFailTotal: prometheus.NewCounter(prometheus.CounterOpts{ ++ Name: "prefetch_fail_total", ++ Help: "The total number of failed proactive prefetch operations", ++ ConstLabels: lb, ++ }), + size: prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "size_current", + Help: "Current cache size in records", +@@ -173,12 +196,15 @@ func NewCache(args *Args, opts Opts) *Ca + p.logger.Error("failed to load cache dump", zap.Error(err)) + } + p.startDumpLoop() ++ if p.args.Prefetch { ++ p.startPrefetchLoop() ++ } + + return p + } + + func (c *Cache) RegMetricsTo(r prometheus.Registerer) error { +- for _, collector := range [...]prometheus.Collector{c.queryTotal, c.hitTotal, c.lazyHitTotal, c.size} { ++ for _, collector := range [...]prometheus.Collector{c.queryTotal, c.hitTotal, c.lazyHitTotal, c.prefetchTotal, c.prefetchFailTotal, c.size} { + if err := r.Register(collector); err != nil { + return err + } +@@ -187,6 +213,15 @@ func (c *Cache) RegMetricsTo(r prometheu + } + + func (c *Cache) Exec(ctx context.Context, qCtx *query_context.Context, next sequence.ChainWalker) error { ++ c.captureOnce.Do(func() { ++ // This implementation assumes the cache plugin is invoked from a single fixed position in the sequence. ++ nextCopy := next ++ c.capturedNext = &nextCopy ++ }) ++ if c.capturedNext != nil && !reflect.DeepEqual(next, *c.capturedNext) { ++ c.logger.Warn("cache plugin Exec called with a different next chain than first time") ++ } ++ + c.queryTotal.Inc() + q := qCtx.Q() + +@@ -205,6 +240,7 @@ func (c *Cache) Exec(ctx context.Context + cachedResp.Id = q.Id // change msg id + qCtx.SetResponse(cachedResp) + if v, _, ok := c.backend.Get(key(msgKey)); ok && v != nil { ++ v.hitCount.Add(1) + ttl := int(v.expirationTime.Sub(v.storedTime).Seconds()) + remainingTtl := int(v.expirationTime.Sub(time.Now()).Seconds()) + if remainingTtl < 0 { +@@ -255,6 +291,73 @@ func (c *Cache) doLazyUpdate(msgKey stri + c.lazyUpdateSF.DoChan(msgKey, lazyUpdateFunc) // DoChan won't block this goroutine + } + ++func (c *Cache) startPrefetchLoop() { ++ go func() { ++ ticker := time.NewTicker(time.Duration(c.args.PrefetchScanInterval) * time.Second) ++ defer ticker.Stop() ++ for { ++ select { ++ case <-ticker.C: ++ if c.capturedNext == nil { ++ continue ++ } ++ now := time.Now() ++ _ = c.backend.Range(func(k key, v *item, cacheExpirationTime time.Time) error { ++ remainingTTL := int(v.expirationTime.Sub(now).Seconds()) ++ if remainingTTL >= 0 && remainingTTL < c.args.PrefetchBeforeExpire && v.hitCount.Load() >= uint32(c.args.PrefetchMinHits) { ++ c.doPrefetch(string(k), v, remainingTTL) ++ } ++ return nil ++ }) ++ case <-c.closeNotify: ++ return ++ } ++ } ++ }() ++} ++ ++func (c *Cache) doPrefetch(msgKey string, v *item, remainingTTL int) { ++ prefetchFunc := func() (any, error) { ++ defer c.lazyUpdateSF.Forget(msgKey) ++ ++ req := new(dns.Msg) ++ req.SetQuestion(v.resp.Question[0].Name, v.resp.Question[0].Qtype) ++ req.Id = dns.Id() ++ req.RecursionDesired = true ++ newQCtx := query_context.NewContext(req) ++ ++ qname := v.resp.Question[0].Name ++ hitCount := v.hitCount.Load() ++ ++ c.logger.Debug("start proactive prefetch", ++ zap.String("qname", qname), ++ zap.Uint32("hit_count", hitCount), ++ zap.Int("remaining_ttl", remainingTTL), ++ ) ++ ctx, cancel := context.WithTimeout(context.Background(), defaultLazyUpdateTimeout) ++ defer cancel() ++ ++ err := c.capturedNext.ExecNext(ctx, newQCtx) ++ if err != nil { ++ c.logger.Warn("failed to proactive prefetch", zap.String("qname", qname), zap.Error(err)) ++ c.prefetchFailTotal.Inc() ++ return nil, err ++ } ++ ++ r := newQCtx.R() ++ if r != nil { ++ if saveRespToCache(msgKey, r, c.backend, c.args.LazyCacheTTL) { ++ v.hitCount.Store(0) ++ c.prefetchTotal.Inc() ++ c.updatedKey.Add(1) ++ } ++ } ++ c.logger.Debug("proactive prefetch updated", zap.String("qname", qname), newQCtx.InfoField()) ++ return nil, nil ++ } ++ c.lazyUpdateSF.DoChan(msgKey, prefetchFunc) ++} ++ + func (c *Cache) Close() error { + if err := c.dumpCache(); err != nil { + c.logger.Error("failed to dump cache", zap.Error(err)) +--- a/plugin/executable/cache/utils.go ++++ b/plugin/executable/cache/utils.go +@@ -21,6 +21,7 @@ package cache + + import ( + "hash/maphash" ++ "sync/atomic" + "time" + + "github.com/IrineSistiana/mosdns/v5/pkg/cache" +@@ -78,6 +79,7 @@ type item struct { + resp *dns.Msg + storedTime time.Time + expirationTime time.Time ++ hitCount atomic.Uint32 + } + + func copyNoOpt(m *dns.Msg) *dns.Msg {