error_passthrough_service.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. package service
  2. import (
  3. "context"
  4. "sort"
  5. "strings"
  6. "sync"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/model"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  10. )
  11. // ErrorPassthroughRepository 定义错误透传规则的数据访问接口
  12. type ErrorPassthroughRepository interface {
  13. // List 获取所有规则
  14. List(ctx context.Context) ([]*model.ErrorPassthroughRule, error)
  15. // GetByID 根据 ID 获取规则
  16. GetByID(ctx context.Context, id int64) (*model.ErrorPassthroughRule, error)
  17. // Create 创建规则
  18. Create(ctx context.Context, rule *model.ErrorPassthroughRule) (*model.ErrorPassthroughRule, error)
  19. // Update 更新规则
  20. Update(ctx context.Context, rule *model.ErrorPassthroughRule) (*model.ErrorPassthroughRule, error)
  21. // Delete 删除规则
  22. Delete(ctx context.Context, id int64) error
  23. }
  24. // ErrorPassthroughCache 定义错误透传规则的缓存接口
  25. type ErrorPassthroughCache interface {
  26. // Get 从缓存获取规则列表
  27. Get(ctx context.Context) ([]*model.ErrorPassthroughRule, bool)
  28. // Set 设置缓存
  29. Set(ctx context.Context, rules []*model.ErrorPassthroughRule) error
  30. // Invalidate 使缓存失效
  31. Invalidate(ctx context.Context) error
  32. // NotifyUpdate 通知其他实例刷新缓存
  33. NotifyUpdate(ctx context.Context) error
  34. // SubscribeUpdates 订阅缓存更新通知
  35. SubscribeUpdates(ctx context.Context, handler func())
  36. }
  37. // ErrorPassthroughService 错误透传规则服务
  38. type ErrorPassthroughService struct {
  39. repo ErrorPassthroughRepository
  40. cache ErrorPassthroughCache
  41. // 本地内存缓存,用于快速匹配
  42. localCache []*cachedPassthroughRule
  43. localCacheMu sync.RWMutex
  44. }
  45. // cachedPassthroughRule 预计算的规则缓存,避免运行时重复 ToLower
  46. type cachedPassthroughRule struct {
  47. *model.ErrorPassthroughRule
  48. lowerKeywords []string // 预计算的小写关键词
  49. lowerPlatforms []string // 预计算的小写平台
  50. errorCodeSet map[int]struct{} // 预计算的 error code set
  51. }
  52. const maxBodyMatchLen = 8 << 10 // 8KB,错误信息不会在 8KB 之后才出现
  53. // NewErrorPassthroughService 创建错误透传规则服务
  54. func NewErrorPassthroughService(
  55. repo ErrorPassthroughRepository,
  56. cache ErrorPassthroughCache,
  57. ) *ErrorPassthroughService {
  58. svc := &ErrorPassthroughService{
  59. repo: repo,
  60. cache: cache,
  61. }
  62. // 启动时加载规则到本地缓存
  63. ctx := context.Background()
  64. if err := svc.reloadRulesFromDB(ctx); err != nil {
  65. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to load rules from DB on startup: %v", err)
  66. if fallbackErr := svc.refreshLocalCache(ctx); fallbackErr != nil {
  67. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to load rules from cache fallback on startup: %v", fallbackErr)
  68. }
  69. }
  70. // 订阅缓存更新通知
  71. if cache != nil {
  72. cache.SubscribeUpdates(ctx, func() {
  73. if err := svc.refreshLocalCache(context.Background()); err != nil {
  74. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to refresh cache on notification: %v", err)
  75. }
  76. })
  77. }
  78. return svc
  79. }
  80. // List 获取所有规则
  81. func (s *ErrorPassthroughService) List(ctx context.Context) ([]*model.ErrorPassthroughRule, error) {
  82. return s.repo.List(ctx)
  83. }
  84. // GetByID 根据 ID 获取规则
  85. func (s *ErrorPassthroughService) GetByID(ctx context.Context, id int64) (*model.ErrorPassthroughRule, error) {
  86. return s.repo.GetByID(ctx, id)
  87. }
  88. // Create 创建规则
  89. func (s *ErrorPassthroughService) Create(ctx context.Context, rule *model.ErrorPassthroughRule) (*model.ErrorPassthroughRule, error) {
  90. if err := rule.Validate(); err != nil {
  91. return nil, err
  92. }
  93. created, err := s.repo.Create(ctx, rule)
  94. if err != nil {
  95. return nil, err
  96. }
  97. // 刷新缓存
  98. refreshCtx, cancel := s.newCacheRefreshContext()
  99. defer cancel()
  100. s.invalidateAndNotify(refreshCtx)
  101. return created, nil
  102. }
  103. // Update 更新规则
  104. func (s *ErrorPassthroughService) Update(ctx context.Context, rule *model.ErrorPassthroughRule) (*model.ErrorPassthroughRule, error) {
  105. if err := rule.Validate(); err != nil {
  106. return nil, err
  107. }
  108. updated, err := s.repo.Update(ctx, rule)
  109. if err != nil {
  110. return nil, err
  111. }
  112. // 刷新缓存
  113. refreshCtx, cancel := s.newCacheRefreshContext()
  114. defer cancel()
  115. s.invalidateAndNotify(refreshCtx)
  116. return updated, nil
  117. }
  118. // Delete 删除规则
  119. func (s *ErrorPassthroughService) Delete(ctx context.Context, id int64) error {
  120. if err := s.repo.Delete(ctx, id); err != nil {
  121. return err
  122. }
  123. // 刷新缓存
  124. refreshCtx, cancel := s.newCacheRefreshContext()
  125. defer cancel()
  126. s.invalidateAndNotify(refreshCtx)
  127. return nil
  128. }
  129. // MatchRule 匹配透传规则
  130. // 返回第一个匹配的规则,如果没有匹配则返回 nil
  131. func (s *ErrorPassthroughService) MatchRule(platform string, statusCode int, body []byte) *model.ErrorPassthroughRule {
  132. rules := s.getCachedRules()
  133. if len(rules) == 0 {
  134. return nil
  135. }
  136. lowerPlatform := strings.ToLower(platform)
  137. var bodyLower string // 延迟初始化,只在需要关键词匹配时计算
  138. var bodyLowerDone bool
  139. for _, rule := range rules {
  140. if !rule.Enabled {
  141. continue
  142. }
  143. if !s.platformMatchesCached(rule, lowerPlatform) {
  144. continue
  145. }
  146. if s.ruleMatchesOptimized(rule, statusCode, body, &bodyLower, &bodyLowerDone) {
  147. return rule.ErrorPassthroughRule
  148. }
  149. }
  150. return nil
  151. }
  152. // getCachedRules 获取缓存的规则列表(按优先级排序)
  153. func (s *ErrorPassthroughService) getCachedRules() []*cachedPassthroughRule {
  154. s.localCacheMu.RLock()
  155. rules := s.localCache
  156. s.localCacheMu.RUnlock()
  157. if rules != nil {
  158. return rules
  159. }
  160. // 如果本地缓存为空,尝试刷新
  161. ctx := context.Background()
  162. if err := s.refreshLocalCache(ctx); err != nil {
  163. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to refresh cache: %v", err)
  164. return nil
  165. }
  166. s.localCacheMu.RLock()
  167. defer s.localCacheMu.RUnlock()
  168. return s.localCache
  169. }
  170. // refreshLocalCache 刷新本地缓存
  171. func (s *ErrorPassthroughService) refreshLocalCache(ctx context.Context) error {
  172. // 先尝试从 Redis 缓存获取
  173. if s.cache != nil {
  174. if rules, ok := s.cache.Get(ctx); ok {
  175. s.setLocalCache(rules)
  176. return nil
  177. }
  178. }
  179. return s.reloadRulesFromDB(ctx)
  180. }
  181. // 从数据库加载(repo.List 已按 priority 排序)
  182. // 注意:该方法会绕过 cache.Get,确保拿到数据库最新值。
  183. func (s *ErrorPassthroughService) reloadRulesFromDB(ctx context.Context) error {
  184. rules, err := s.repo.List(ctx)
  185. if err != nil {
  186. return err
  187. }
  188. // 更新 Redis 缓存
  189. if s.cache != nil {
  190. if err := s.cache.Set(ctx, rules); err != nil {
  191. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to set cache: %v", err)
  192. }
  193. }
  194. // 更新本地缓存(setLocalCache 内部会确保排序)
  195. s.setLocalCache(rules)
  196. return nil
  197. }
  198. // setLocalCache 设置本地缓存,预计算小写值和 set 以避免运行时重复计算
  199. func (s *ErrorPassthroughService) setLocalCache(rules []*model.ErrorPassthroughRule) {
  200. cached := make([]*cachedPassthroughRule, len(rules))
  201. for i, r := range rules {
  202. cr := &cachedPassthroughRule{ErrorPassthroughRule: r}
  203. if len(r.Keywords) > 0 {
  204. cr.lowerKeywords = make([]string, len(r.Keywords))
  205. for j, kw := range r.Keywords {
  206. cr.lowerKeywords[j] = strings.ToLower(kw)
  207. }
  208. }
  209. if len(r.Platforms) > 0 {
  210. cr.lowerPlatforms = make([]string, len(r.Platforms))
  211. for j, p := range r.Platforms {
  212. cr.lowerPlatforms[j] = strings.ToLower(p)
  213. }
  214. }
  215. if len(r.ErrorCodes) > 0 {
  216. cr.errorCodeSet = make(map[int]struct{}, len(r.ErrorCodes))
  217. for _, code := range r.ErrorCodes {
  218. cr.errorCodeSet[code] = struct{}{}
  219. }
  220. }
  221. cached[i] = cr
  222. }
  223. // 按优先级排序
  224. sort.Slice(cached, func(i, j int) bool {
  225. return cached[i].Priority < cached[j].Priority
  226. })
  227. s.localCacheMu.Lock()
  228. s.localCache = cached
  229. s.localCacheMu.Unlock()
  230. }
  231. // clearLocalCache 清空本地缓存,避免刷新失败时继续命中陈旧规则。
  232. func (s *ErrorPassthroughService) clearLocalCache() {
  233. s.localCacheMu.Lock()
  234. s.localCache = nil
  235. s.localCacheMu.Unlock()
  236. }
  237. // newCacheRefreshContext 为写路径缓存同步创建独立上下文,避免受请求取消影响。
  238. func (s *ErrorPassthroughService) newCacheRefreshContext() (context.Context, context.CancelFunc) {
  239. return context.WithTimeout(context.Background(), 3*time.Second)
  240. }
  241. // invalidateAndNotify 使缓存失效并通知其他实例
  242. func (s *ErrorPassthroughService) invalidateAndNotify(ctx context.Context) {
  243. // 先失效缓存,避免后续刷新读到陈旧规则。
  244. if s.cache != nil {
  245. if err := s.cache.Invalidate(ctx); err != nil {
  246. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to invalidate cache: %v", err)
  247. }
  248. }
  249. // 刷新本地缓存
  250. if err := s.reloadRulesFromDB(ctx); err != nil {
  251. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to refresh local cache: %v", err)
  252. // 刷新失败时清空本地缓存,避免继续使用陈旧规则。
  253. s.clearLocalCache()
  254. }
  255. // 通知其他实例
  256. if s.cache != nil {
  257. if err := s.cache.NotifyUpdate(ctx); err != nil {
  258. logger.LegacyPrintf("service.error_passthrough", "[ErrorPassthroughService] Failed to notify cache update: %v", err)
  259. }
  260. }
  261. }
  262. // ensureBodyLower 延迟初始化 body 的小写版本,只做一次转换,限制 8KB
  263. func ensureBodyLower(body []byte, bodyLower *string, done *bool) string {
  264. if *done {
  265. return *bodyLower
  266. }
  267. b := body
  268. if len(b) > maxBodyMatchLen {
  269. b = b[:maxBodyMatchLen]
  270. }
  271. *bodyLower = strings.ToLower(string(b))
  272. *done = true
  273. return *bodyLower
  274. }
  275. // platformMatchesCached 使用预计算的小写平台检查是否匹配
  276. func (s *ErrorPassthroughService) platformMatchesCached(rule *cachedPassthroughRule, lowerPlatform string) bool {
  277. if len(rule.lowerPlatforms) == 0 {
  278. return true
  279. }
  280. for _, p := range rule.lowerPlatforms {
  281. if p == lowerPlatform {
  282. return true
  283. }
  284. }
  285. return false
  286. }
  287. // ruleMatchesOptimized 优化的规则匹配,支持短路和延迟 body 转换
  288. func (s *ErrorPassthroughService) ruleMatchesOptimized(rule *cachedPassthroughRule, statusCode int, body []byte, bodyLower *string, bodyLowerDone *bool) bool {
  289. hasErrorCodes := len(rule.errorCodeSet) > 0
  290. hasKeywords := len(rule.lowerKeywords) > 0
  291. if !hasErrorCodes && !hasKeywords {
  292. return false
  293. }
  294. codeMatch := !hasErrorCodes || s.containsIntSet(rule.errorCodeSet, statusCode)
  295. if rule.MatchMode == model.MatchModeAll {
  296. // "all" 模式:所有配置的条件都必须满足,短路
  297. if hasErrorCodes && !codeMatch {
  298. return false
  299. }
  300. if hasKeywords {
  301. return s.containsAnyKeywordCached(ensureBodyLower(body, bodyLower, bodyLowerDone), rule.lowerKeywords)
  302. }
  303. return codeMatch
  304. }
  305. // "any" 模式:任一条件满足即可,短路
  306. if hasErrorCodes && hasKeywords {
  307. if codeMatch {
  308. return true
  309. }
  310. return s.containsAnyKeywordCached(ensureBodyLower(body, bodyLower, bodyLowerDone), rule.lowerKeywords)
  311. }
  312. // 只配置了一种条件
  313. if hasKeywords {
  314. return s.containsAnyKeywordCached(ensureBodyLower(body, bodyLower, bodyLowerDone), rule.lowerKeywords)
  315. }
  316. return codeMatch
  317. }
  318. // containsIntSet 使用 map 查找替代线性扫描
  319. func (s *ErrorPassthroughService) containsIntSet(set map[int]struct{}, val int) bool {
  320. _, ok := set[val]
  321. return ok
  322. }
  323. // containsAnyKeywordCached 使用预计算的小写关键词检查匹配
  324. func (s *ErrorPassthroughService) containsAnyKeywordCached(bodyLower string, lowerKeywords []string) bool {
  325. for _, kw := range lowerKeywords {
  326. if strings.Contains(bodyLower, kw) {
  327. return true
  328. }
  329. }
  330. return false
  331. }