Compare commits

...

186 Commits

Author SHA1 Message Date
刘祥超
822e967874 优化文件句柄缓存容量判断 2023-10-17 09:59:04 +08:00
刘祥超
2acf890b8e 限制内存缓存最大容量为系统内存的三分之一 2023-10-16 14:28:07 +08:00
刘祥超
3909695b44 优化代码 2023-10-16 11:48:38 +08:00
刘祥超
d0f420a945 将版本号修改为1.2.10 2023-10-15 13:33:18 +08:00
刘祥超
7618338f38 WAF记录IP动作中IP名单如果为空时,默认为全局黑名单 2023-10-15 09:34:50 +08:00
刘祥超
9b2a704e7f 如果设置的缓存容量比当前磁盘总容量大的时候,自动调整为95%磁盘总容量 2023-10-14 22:05:38 +08:00
刘祥超
630c1ec63b 优化缓存自动清理逻辑 2023-10-13 08:36:11 +08:00
刘祥超
5b93b28690 优化缓存命中率统计采样时长和数量 2023-10-13 08:28:13 +08:00
刘祥超
3aa68b5ffc 对WAF正则缓存增加命中率检查 2023-10-12 20:10:30 +08:00
刘祥超
adb0069c59 优化ttlcache回收速度 2023-10-12 16:03:52 +08:00
刘祥超
211da66226 去除遗留的调试信息 2023-10-12 14:59:12 +08:00
刘祥超
81911c4073 更新依赖库 2023-10-12 08:00:26 +08:00
刘祥超
6ff3230bab 限制文件句柄缓存内存使用 2023-10-11 21:51:05 +08:00
刘祥超
f01eae3590 优化代码 2023-10-11 14:07:13 +08:00
刘祥超
47e8761209 优化WAF正则表达式缓存时间 2023-10-11 12:21:10 +08:00
刘祥超
8449fe7c0b 优化代码 2023-10-11 07:24:02 +08:00
刘祥超
7f3e6ddc65 优化批量删除缓存Key代码,防止列表删除了文件还在 2023-10-11 06:31:35 +08:00
刘祥超
6ca8b6837c 删除过期缓存时使用批量删除 2023-10-10 22:08:42 +08:00
刘祥超
a9969430a3 修复内存缓存无法缓存的问题 2023-10-10 15:23:23 +08:00
刘祥超
7c5c06191d 在缓存写入内存之前检查磁盘是否超出容量 2023-10-10 14:45:14 +08:00
刘祥超
afc533c3e4 清理LFU缓存时日志打印消耗时间/删除缓存分区信息文件前判断文件是否存在 2023-10-10 14:02:45 +08:00
刘祥超
71c891ae14 调整vm.dirty_相关系统参数 2023-10-10 11:30:49 +08:00
刘祥超
e8570bfd09 优化内存缓存碎片GC程序 2023-10-09 18:08:30 +08:00
刘祥超
534d64673f 优化内存缓存相关代码 2023-10-09 12:48:30 +08:00
刘祥超
6bff5c978b 优化一处测试用例 2023-10-09 08:51:03 +08:00
刘祥超
38a214878a 缩短内存缓存索引缓存保留时间 2023-10-09 07:49:21 +08:00
刘祥超
149e04d0e4 优化反向代理相关错误提示 2023-10-08 19:05:09 +08:00
刘祥超
5733d466ca 自动调节系统参数时调整vm.dirty_background_ratio和vm.dirty_ratio 2023-10-08 15:09:00 +08:00
刘祥超
a95e2e3259 将本地数据库同步模式改回OFF 2023-10-08 12:29:54 +08:00
刘祥超
a80a89edf5 优化脆片内存逻辑 2023-10-07 14:56:35 +08:00
刘祥超
b8f7d4110f 删除文件缓存时增加文件系统写计数 2023-10-07 12:37:51 +08:00
刘祥超
00e76a6a09 提升内存缓存的碎片内存复用效率 2023-10-07 11:56:34 +08:00
刘祥超
79fa9d88a1 写文件增加负载保护 2023-10-07 09:39:37 +08:00
刘祥超
c460421279 优化本地数据库相关代码 2023-10-06 20:56:27 +08:00
刘祥超
69e4dd6cfe 本地数据库同步模式从关闭改为NORMAL,以降低损坏概率 2023-10-06 00:49:37 +08:00
刘祥超
b73f0ae2c9 加在文件Hash时加入防无限循环机制 2023-10-05 23:08:40 +08:00
刘祥超
2af577380e 修复查询缓存Hash列表SQL参数占位符错误 2023-10-05 21:23:47 +08:00
刘祥超
89bfdc478f 优化缓存Hash查询速度 2023-10-05 17:40:27 +08:00
刘祥超
b6e8221ac1 优化计数器相关代码 2023-10-05 16:29:02 +08:00
刘祥超
ea6d3d7107 增加计数器容量上限 2023-10-05 13:36:55 +08:00
刘祥超
7d8bdfcd45 优化计数器内存使用(内存用量减少40%) 2023-10-05 13:19:32 +08:00
刘祥超
70fe1b5d2b 合并多个计数器,便于统一的内存控制 2023-10-05 09:45:46 +08:00
刘祥超
a7caf0260a 测试用内存统计增加回调函数 2023-10-05 09:15:17 +08:00
刘祥超
d547793eee 优化代码 2023-10-05 08:41:07 +08:00
刘祥超
d92f27c44b ttlcache支持泛型 2023-10-05 08:28:16 +08:00
刘祥超
8561ff3e2d 文件缓存自动加载热门数据时检查是否有足够的内存空间 2023-10-04 18:13:48 +08:00
刘祥超
0736b20d33 优化计数器内存使用 2023-10-04 16:53:39 +08:00
刘祥超
263acb775b 优化测试用例 2023-10-04 14:56:26 +08:00
刘祥超
7a1fff8504 增加测试用例 2023-10-03 21:41:03 +08:00
刘祥超
5c5adf690f 优化代码 2023-10-03 21:38:45 +08:00
刘祥超
1eca5099df 优化缓存相关代码 2023-10-03 19:02:07 +08:00
刘祥超
5f2ad8b096 优化缓存相关代码 2023-10-03 11:39:28 +08:00
刘祥超
4405cfd405 优化缓存相关代码 2023-10-02 19:48:11 +08:00
刘祥超
2f6aa0c14f 优化缓存列表数据库加载速度 2023-10-02 16:05:42 +08:00
刘祥超
bb50ecd682 增加内存缓存队列长度,确保不会产生不在队列里的缓存对象 2023-10-02 15:20:19 +08:00
刘祥超
ae1454f9bb 优化热门缓存算法 2023-10-02 10:40:20 +08:00
刘祥超
3781b1920a 优化代码 2023-10-02 08:18:43 +08:00
刘祥超
125ad9c606 优化文件列表缓存时间 2023-10-01 20:30:07 +08:00
刘祥超
e516300dc7 修复一处测试用例 2023-10-01 19:48:35 +08:00
刘祥超
6c1d24c3e5 预留最大内存总是设置为系统内存的20% 2023-10-01 16:09:49 +08:00
刘祥超
6f230b30e0 调整WAF和其他配置的优先级顺序 2023-10-01 15:16:39 +08:00
刘祥超
8a0318b4f3 优化内存写入速度 2023-10-01 15:06:58 +08:00
刘祥超
d9fddcb001 优化内存缓存限制 2023-10-01 14:11:48 +08:00
刘祥超
9113c4c1b3 修复一处测试用例 2023-09-29 19:37:46 +08:00
刘祥超
f0762fe1b9 清理缓存时智能判断是否需要完整LFU 2023-09-29 14:52:08 +08:00
刘祥超
9054b8ec05 执行edge-node cache.badge命令时打印进度 2023-09-28 15:02:06 +08:00
刘祥超
5ad25e34c6 提升快速硬盘清理过期缓存速度 2023-09-28 10:56:33 +08:00
刘祥超
d4cca10301 修复一处无法编译的问题 2023-09-21 16:20:29 +08:00
刘祥超
8597884ec5 修复URL域名跳转设置可能不生效的问题 2023-09-20 08:21:22 +08:00
刘祥超
9f469f5b77 访客IP设置中支持多个请求报头/X-Real-IP也可以从变量中读取 2023-09-17 19:15:10 +08:00
刘祥超
5bc4f5b359 优化Ln请求 2023-09-17 18:17:34 +08:00
刘祥超
494ff5b5bb 智能调节清理缓存阈值 2023-09-17 12:05:06 +08:00
刘祥超
bac0060a74 edge-node cache.garbage命令执行时检查Key列表是否已加载完毕 2023-09-17 11:43:46 +08:00
刘祥超
062ca1cfcd 缓存内容报头时忽略X-Cache 2023-09-17 10:54:14 +08:00
刘祥超
8a4373e984 修复节点缓存磁盘容量设置不生效的问题 2023-09-16 09:36:04 +08:00
刘祥超
99670e46a5 增加edge-node cache.garbage命令用于清理垃圾缓存 2023-09-15 18:14:58 +08:00
刘祥超
64642c6680 优化单次清理LFU缓存数量逻辑 2023-09-15 14:46:31 +08:00
刘祥超
f7a7b50eda 优化缓存自动清理 2023-09-14 20:17:48 +08:00
刘祥超
f5265f1832 优化缓存LFU逻辑 2023-09-14 18:30:11 +08:00
刘祥超
2d6f7522fc 优化删除IP名单时操作 2023-09-13 17:17:05 +08:00
刘祥超
cfb15e764d 将版本号修改为1.2.9 2023-09-12 17:17:56 +08:00
刘祥超
406d8de999 本地数据库启动时在必要的情况下尝试恢复数据 2023-09-12 16:53:41 +08:00
刘祥超
2ca79953b9 优化一处测试用例 2023-09-12 16:51:15 +08:00
刘祥超
d067cd8437 IP名单同步时增加调试信息 2023-09-12 16:05:18 +08:00
刘祥超
e4937685bc 系统在进行健康检查时不进行指标统计 2023-09-12 10:45:44 +08:00
刘祥超
6ac2343aa7 更新依赖 2023-09-11 17:15:10 +08:00
刘祥超
1d80bc640d WebP转换增加宽度和高度限制 2023-09-11 16:05:59 +08:00
刘祥超
b19c431b89 优化代码 2023-09-07 15:10:05 +08:00
刘祥超
a1e1b5ef98 修复集群启用“允许使用节点IP访问”时无法访问IPv6的问题 2023-09-07 14:48:27 +08:00
刘祥超
e93275ac5c 重新实现套餐 2023-09-06 16:34:11 +08:00
刘祥超
a6059ab070 修复状未绑定域名中${status}和${statusMessage}变量值 2023-08-27 21:38:35 +08:00
刘祥超
5b0f94f317 优化过时缓存时长(从600秒改为1200秒) 2023-08-27 14:49:28 +08:00
刘祥超
0b4ac55aa6 源站返回50X时,也可以尝试使用过时缓存 2023-08-26 15:46:45 +08:00
刘祥超
9080c78b13 优化内置的消息提示:增加连接信息方便诊断 2023-08-25 15:43:12 +08:00
刘祥超
aa7d67e387 网站设置增加是否支持${serverAddr}选项/增强${serverAddr}安全性 2023-08-25 15:30:59 +08:00
刘祥超
405b3615fe 增加${serverAddr}变量 2023-08-25 15:04:18 +08:00
刘祥超
7534af09ed 优化编译脚本 2023-08-23 18:21:32 +08:00
刘祥超
cd3bf0cad4 优化编译脚本 2023-08-23 17:50:33 +08:00
刘祥超
41ebdfb7d2 优化响应报头 2023-08-23 16:33:19 +08:00
刘祥超
fa7d4963cb 反向代理增加是否重试50X选项,默认为启用 2023-08-20 15:50:31 +08:00
刘祥超
627d9721b3 优化反向代理错误处理代码 2023-08-20 11:45:39 +08:00
刘祥超
8c3cd53dc3 检查硬盘是否已满时同时检测缓存策略中定义的容量 2023-08-20 11:02:09 +08:00
刘祥超
5995be8489 修复访问日志无法记录自定义跳转状态码的问题 2023-08-20 10:10:23 +08:00
刘祥超
eabaa84252 修复磁盘空间可能为0的情形 2023-08-18 15:56:45 +08:00
刘祥超
40e137e69e 优化指标统计性能 2023-08-15 20:12:09 +08:00
刘祥超
6f51fe52f8 优化代码 2023-08-15 15:49:23 +08:00
刘祥超
dd4071e7dc 优化api_node.yaml生成 2023-08-15 10:33:01 +08:00
刘祥超
7e5b3254eb 修改版本号为1.2.8 2023-08-14 12:24:03 +08:00
刘祥超
ad1947379d 自动生成新的配置文件(api_node.yaml) 2023-08-14 12:23:55 +08:00
刘祥超
2ff2afca36 节点启动时删除缓存目录下遗留的*.trash文件 2023-08-13 18:18:55 +08:00
刘祥超
9f36678d75 修复缓存磁盘总体统计时同分区重复统计的问题 2023-08-13 17:29:54 +08:00
刘祥超
288074c8b3 优化错误日志处理 2023-08-13 14:25:59 +08:00
刘祥超
0a290251cd WAF增加通配符匹配/不匹配操作符 2023-08-13 10:37:58 +08:00
刘祥超
edf98f1889 将cluster.yaml修改为api_cluster.yaml 2023-08-12 18:53:32 +08:00
刘祥超
02c3d24038 优化API配置格式化 2023-08-12 18:20:30 +08:00
刘祥超
2a2f4ff7b1 修正api_node.template.yaml中的rpc.endpoints 2023-08-12 15:14:56 +08:00
刘祥超
d416a2186c 将api.yaml修改为api_node.yaml 2023-08-12 15:08:22 +08:00
刘祥超
94b9c23323 使用edge-node start启动时先检查API配置 2023-08-11 17:47:17 +08:00
刘祥超
70d8507c4b 优化错误处理相关代码 2023-08-11 14:51:23 +08:00
刘祥超
2eee314ec8 优化错误处理相关代码 2023-08-11 14:38:00 +08:00
刘祥超
93521963b1 重新支持低版本HTTP 2023-08-10 14:02:42 +08:00
刘祥超
f456b6bc6b 静态分发增加例外URL、限制URL、排除隐藏文件等选项 2023-08-10 11:27:24 +08:00
刘祥超
d30e2b2cc8 WAF策略可以自定义默认的区域/省份封禁提示 2023-08-10 10:31:38 +08:00
刘祥超
25bf4ab55a 地区/省份封禁提示中支持变量 2023-08-10 09:50:02 +08:00
刘祥超
9536c49a8f 优化WAF文件上传处理 2023-08-09 17:55:48 +08:00
刘祥超
3f19d88246 将版本号修改为1.2.7 2023-08-09 14:24:38 +08:00
刘祥超
9931d057a9 优化代码 2023-08-09 11:17:13 +08:00
刘祥超
c61ae6c8ba Update .golangci.yaml 2023-08-09 08:12:05 +08:00
刘祥超
249dad3e97 优化代码 2023-08-08 18:19:30 +08:00
刘祥超
f9fbd23a77 增加golangci-lint配置 2023-08-08 18:14:48 +08:00
刘祥超
22eb143dee 调整空闲时间清理缓存算法 2023-08-08 16:10:14 +08:00
刘祥超
075c11a3cf 优化代码 2023-08-08 15:39:00 +08:00
刘祥超
f4258ed00e 优化代码 2023-08-08 12:02:21 +08:00
刘祥超
8ac115f865 优化代码 2023-08-08 11:36:54 +08:00
刘祥超
53f109861c 优化代码 2023-08-08 11:23:04 +08:00
刘祥超
f034a1cfb3 优化代码 2023-08-08 11:10:51 +08:00
刘祥超
ae74114fca 优化代码 2023-08-08 10:07:24 +08:00
刘祥超
cdfc37ac14 缓存策略增加“缓存磁盘最小空余空间”选项 2023-08-06 18:08:28 +08:00
刘祥超
362a4d9ef4 缓存策略增加预热超时时间设置(默认从10分钟调整为20分钟) 2023-08-06 17:08:29 +08:00
刘祥超
7653c16586 修复HTTP协议下ProxyProtocol只能传递第一个IP的问题;修复每个小时都自动清除所有源站连接池的问题 2023-08-06 09:46:32 +08:00
刘祥超
f1a4a7ebc6 删除不必要的文件 2023-08-06 08:50:49 +08:00
刘祥超
0f0436c7a8 优化高速硬盘下的缓存 2023-08-04 16:32:15 +08:00
刘祥超
cc881c070c WAF策略增加“最多检查内容尺寸“选项 2023-08-02 17:00:16 +08:00
刘祥超
10db4e3ccd 修复WAF的一处测试用例无法工作的问题 2023-08-02 15:09:58 +08:00
刘祥超
a2a33a65e8 对硬盘写入速度格式化 2023-08-02 12:21:47 +08:00
刘祥超
5a4162987c 上报硬盘写入速度 2023-08-02 12:00:19 +08:00
刘祥超
b46744cb13 优化磁盘速度检查 2023-08-02 11:34:14 +08:00
刘祥超
8b7845fd15 对缓存文件关闭事件也增加写入统计 2023-08-02 08:51:31 +08:00
刘祥超
a129178abc 优化服务相关代码 2023-08-01 16:10:05 +08:00
刘祥超
02875374aa 自定义页面消息默认Content-Type设置为text/html; charset=utf-8 2023-08-01 15:44:36 +08:00
刘祥超
8859625ce7 增加卸载命令edge-node uninstall 2023-08-01 15:36:04 +08:00
刘祥超
6dccd0ad46 启动时自动创建相关软链接 2023-08-01 10:46:11 +08:00
刘祥超
18b76013b9 缓存条件增加“强制Range回源选项” 2023-07-31 17:32:09 +08:00
刘祥超
fa04c041df 优化代码 2023-07-31 16:05:08 +08:00
刘祥超
c8142741ff 缓存条件增加是否允许异步读取源站选项 2023-07-31 15:49:04 +08:00
刘祥超
547874aa43 修复集群自定义页面设置无法生效的问题 2023-07-31 09:55:13 +08:00
刘祥超
45c6b2ddac 优化数据统计 2023-07-30 14:49:16 +08:00
刘祥超
eb145393ab 优化代码 2023-07-30 09:22:13 +08:00
刘祥超
d767ca177a 在写入缓存数据时自动分多次写入“大”的文件内容 2023-07-30 09:00:51 +08:00
刘祥超
c8fc2815b9 优化代码 2023-07-30 08:49:31 +08:00
刘祥超
3b2ba1aad6 写缓存元数据也加入写并发数 2023-07-29 09:46:14 +08:00
刘祥超
33bb06fbc3 使用新的方法控制缓存并发写入速度 2023-07-29 09:29:36 +08:00
刘祥超
3793d684fa 优化代码 2023-07-28 11:10:57 +08:00
刘祥超
2390a3ef61 版本号更改为1.2.6 2023-07-28 09:28:05 +08:00
刘祥超
ffda81715f 可以修改访问未绑定域名时的状态码 2023-07-27 11:21:35 +08:00
刘祥超
f991700031 未绑定域名页面提示、访问节点IP显示自定义内容支持变量 2023-07-27 10:50:18 +08:00
刘祥超
e1ba6a90ff 增加重载一组网站事件 2023-07-27 10:37:16 +08:00
刘祥超
354161037b 优化升级程序 2023-07-27 10:03:29 +08:00
刘祥超
00d28df3ee 优化本地数据库关闭时提示 2023-07-27 10:03:18 +08:00
刘祥超
23abed0949 改进缓存相关错误提示 2023-07-26 19:12:02 +08:00
刘祥超
2acf01dcb7 刷新/预热缓存任务可以并行处理 2023-07-26 18:45:17 +08:00
刘祥超
470c6a8b0e 版本号更改为1.2.5 2023-07-26 15:29:49 +08:00
刘祥超
efc2810d1d 修复分区内容长度判断错误的问题 2023-07-26 14:48:07 +08:00
刘祥超
de2374577f 增加硬盘速度检测命令:edge-node disk speed 2023-07-26 11:22:15 +08:00
刘祥超
2a1f949c13 版本号修改为1.2.3 2023-07-25 13:18:06 +08:00
刘祥超
959e274063 调整关闭连接后的Linger值 2023-07-25 09:36:45 +08:00
刘祥超
b6a2bd37b1 去除连接中的Linger设置,防止有些反向代理在数据未发送前关闭连接 2023-07-24 19:22:22 +08:00
刘祥超
3e60c9913a 优化TOA 2023-07-24 10:01:38 +08:00
刘祥超
fd7f3f4029 取消HTTP源站传递TOA(因为此时源站连接是可以重用的) 2023-07-23 18:57:16 +08:00
刘祥超
2705a5d444 修复HTTP传输时可能无法传递TOA的问题 2023-07-23 18:49:50 +08:00
刘祥超
556055cfcb 优化代码 2023-07-22 14:51:17 +08:00
刘祥超
67a0d06944 缓存条件一些无法匹配的情况在X-Cache中也增加详情 2023-07-20 16:42:54 +08:00
刘祥超
a16d8f1afa 版本号更改为1.2.2 2023-07-18 14:31:47 +08:00
刘祥超
1bab7bfcba 优化对未知长度内容的缓存长度限制 2023-07-18 12:45:25 +08:00
刘祥超
5928875623 优化代码 2023-07-18 11:21:39 +08:00
刘祥超
734cf81ff0 增加测试用例 2023-07-17 20:31:50 +08:00
239 changed files with 5564 additions and 2576 deletions

74
.golangci.yaml Normal file
View File

@@ -0,0 +1,74 @@
# https://golangci-lint.run/usage/configuration/
linters:
enable-all: true
disable:
- ifshort
- exhaustivestruct
- golint
- nosnakecase
- scopelint
- varcheck
- structcheck
- interfacer
- maligned
- deadcode
- dogsled
- wrapcheck
- wastedassign
- varnamelen
- testpackage
- thelper
- nilerr
- sqlclosecheck
- paralleltest
- nonamedreturns
- nlreturn
- nakedret
- ireturn
- interfacebloat
- gosmopolitan
- gomnd
- goerr113
- gochecknoglobals
- exhaustruct
- errorlint
- depguard
- exhaustive
- containedctx
- wsl
- cyclop
- dupword
- errchkjson
- contextcheck
- tagalign
- dupl
- forbidigo
- funlen
- goconst
- godox
- gosec
- lll
- nestif
- revive
- unparam
- stylecheck
- gocritic
- gofumpt
- gomoddirectives
- godot
- gofmt
- gocognit
- mirror
- gocyclo
- gochecknoinits
- gci
- maintidx
- prealloc
- goimports
- errname
- musttag
- forcetypeassert
- whitespace
- noctx
- rowserrcheck

View File

@@ -6,8 +6,10 @@ function build() {
VERSION=$(lookup-version "$ROOT"/../internal/const/const.go)
DIST=$ROOT/"../dist/${NAME}"
MUSL_DIR="/usr/local/opt/musl-cross/bin"
GCC_X86_64_DIR="/usr/local/Cellar/x86_64-unknown-linux-gnu/10.3.0/bin"
GCC_ARM64_DIR="/usr/local/Cellar/aarch64-unknown-linux-gnu/10.3.0/bin"
# for macOS users: precompiled gcc can be downloaded from https://github.com/messense/homebrew-macos-cross-toolchains
GCC_X86_64_DIR="/usr/local/gcc/x86_64-unknown-linux-gnu/bin"
GCC_ARM64_DIR="//usr/local/gcc/aarch64-unknown-linux-gnu/bin"
OS=${1}
ARCH=${2}
@@ -49,15 +51,19 @@ function build() {
fi
fi
cp "$ROOT"/configs/api.template.yaml "$DIST"/configs
cp "$ROOT"/configs/api_node.template.yaml "$DIST"/configs
cp "$ROOT"/configs/cluster.template.yaml "$DIST"/configs
cp -R "$ROOT"/www "$DIST"/
cp -R "$ROOT"/pages "$DIST"/
# we support TOA on linux/amd64 only
if [ "$OS" == "linux" ] && [ "$ARCH" == "amd64" ]
# we support TOA on linux only
if [ "$OS" == "linux" ] && [ -f "${ROOT}/edge-toa/edge-toa-${ARCH}" ]
then
cp -R "$ROOT"/edge-toa "$DIST"
if [ ! -d "$DIST/edge-toa" ]
then
mkdir "$DIST/edge-toa"
fi
cp "${ROOT}/edge-toa/edge-toa-${ARCH}" "$DIST/edge-toa/edge-toa"
fi
echo "building ..."
@@ -120,6 +126,11 @@ function build() {
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 go build -trimpath -tags $BUILD_TAG -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-node/main.go
fi
if [ ! -f "${DIST}/bin/${NAME}" ]; then
echo "build failed!"
exit
fi
# delete hidden files
find "$DIST" -name ".DS_Store" -delete
find "$DIST" -name ".gitignore" -delete

View File

@@ -1,4 +1,6 @@
node.json
api.yaml
api_node.yaml
cluster.yaml
api_cluster.yaml
*.cache

View File

@@ -1,2 +1,2 @@
* `api.template.yaml` - API相关配置模板
* `api_node.template.yaml` - API相关配置模板
* `cluster.template.yaml` - 通过集群自动接入节点模板

View File

@@ -1,4 +0,0 @@
rpc:
endpoints: [ "" ]
nodeId: ""
secret: ""

View File

@@ -0,0 +1,3 @@
rpc.endpoints: [ "" ]
nodeId: ""
secret: ""

Binary file not shown.

View File

@@ -5,8 +5,12 @@ import (
"flag"
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/apps"
"github.com/TeaOSLab/EdgeNode/internal/configs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/nodes"
"github.com/TeaOSLab/EdgeNode/internal/utils"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
@@ -16,17 +20,90 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
func main() {
var app = apps.NewAppCmd().
Version(teaconst.Version).
Product(teaconst.ProductName).
Usage(teaconst.ProcessName + " [-v|start|stop|restart|status|quit|test|reload|service|daemon|pprof|accesslog]").
Usage(teaconst.ProcessName + " [trackers|goman|conns|gc]").
Usage(teaconst.ProcessName + " [-v|start|stop|restart|status|quit|test|reload|service|daemon|pprof|accesslog|uninstall]").
Usage(teaconst.ProcessName + " [trackers|goman|conns|gc|bandwidth|disk|cache.garbage]").
Usage(teaconst.ProcessName + " [ip.drop|ip.reject|ip.remove|ip.close] IP")
app.On("start:before", func() {
// validate config
_, err := configs.LoadAPIConfig()
if err != nil {
// validate cluster config
_, clusterErr := configs.LoadClusterConfig()
if clusterErr != nil { // fail again
fmt.Println("[ERROR]start failed: load api config from '" + Tea.ConfigFile(configs.ConfigFileName) + "' failed: " + err.Error())
os.Exit(0)
}
}
})
app.On("uninstall", func() {
// service
fmt.Println("Uninstall service ...")
var manager = utils.NewServiceManager(teaconst.ProcessName, teaconst.ProductName)
go func() {
_ = manager.Uninstall()
}()
// stop
fmt.Println("Stopping ...")
_, _ = gosock.NewTmpSock(teaconst.ProcessName).SendTimeout(&gosock.Command{Code: "stop"}, 1*time.Second)
// delete files
var exe, _ = os.Executable()
if len(exe) == 0 {
return
}
var dir = filepath.Dir(filepath.Dir(exe)) // ROOT / bin / exe
// verify dir
{
fmt.Println("Checking '" + dir + "' ...")
for _, subDir := range []string{"bin/" + filepath.Base(exe), "configs", "logs"} {
_, err := os.Stat(dir + "/" + subDir)
if err != nil {
fmt.Println("[ERROR]program directory structure has been broken, please remove it manually.")
return
}
}
fmt.Println("Removing '" + dir + "' ...")
err := os.RemoveAll(dir)
if err != nil {
fmt.Println("[ERROR]remove failed: " + err.Error())
}
}
// delete symbolic links
fmt.Println("Removing symbolic links ...")
_ = os.Remove("/usr/bin/" + teaconst.ProcessName)
_ = os.Remove("/var/log/" + teaconst.ProcessName)
// delete configs
// nothing to delete for EdgeNode
// delete sock
fmt.Println("Removing temporary files ...")
var tempDir = os.TempDir()
_ = os.Remove(tempDir + "/" + teaconst.ProcessName + ".sock")
_ = os.Remove(tempDir + "/" + teaconst.AccessLogSockName)
// cache ...
fmt.Println("Please delete cache directories by yourself.")
// done
fmt.Println("[DONE]")
})
app.On("test", func() {
err := nodes.NewNode().Test()
if err != nil {
@@ -365,6 +442,81 @@ func main() {
}
fmt.Println(string(statsJSON))
})
app.On("disk", func() {
var args = os.Args[2:]
if len(args) > 0 {
switch args[0] {
case "speed":
speedMB, isFast, err := fsutils.CheckDiskIsFast()
if err != nil {
fmt.Println("[ERROR]" + err.Error())
} else {
fmt.Printf("Speed: %.0fMB/s\n", speedMB)
if isFast {
fmt.Println("IsFast: true")
} else {
fmt.Println("IsFast: false")
}
}
default:
fmt.Println("Usage: edge-node disk [speed]")
}
} else {
fmt.Println("Usage: edge-node disk [speed]")
}
})
app.On("cache.garbage", func() {
fmt.Println("scanning ...")
var shouldDelete bool
for _, arg := range os.Args {
if strings.TrimLeft(arg, "-") == "delete" {
shouldDelete = true
}
}
var progressSock = gosock.NewTmpSock(teaconst.CacheGarbageSockName)
progressSock.OnCommand(func(cmd *gosock.Command) {
var params = maps.NewMap(cmd.Params)
if cmd.Code == "progress" {
fmt.Printf("%.2f%% %d\n", params.GetFloat64("progress")*100, params.GetInt("count"))
_ = cmd.ReplyOk()
}
})
go func() {
_ = progressSock.Listen()
}()
time.Sleep(1 * time.Second)
var sock = gosock.NewTmpSock(teaconst.ProcessName)
reply, err := sock.Send(&gosock.Command{
Code: "cache.garbage",
Params: map[string]any{"delete": shouldDelete},
})
if err != nil {
fmt.Println("[ERROR]" + err.Error())
return
}
var params = maps.NewMap(reply.Params)
if params.GetBool("isOk") {
var count = params.GetInt("count")
fmt.Println("found", count, "bad caches")
if count > 0 {
fmt.Println("======")
var sampleFiles = params.GetSlice("sampleFiles")
for _, file := range sampleFiles {
fmt.Println(types.String(file))
}
if count > len(sampleFiles) {
fmt.Println("... more files")
}
}
} else {
fmt.Println("[ERROR]" + params.GetString("error"))
}
})
app.Run(func() {
var node = nodes.NewNode()
node.Start()

33
go.mod
View File

@@ -13,32 +13,34 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
github.com/andybalholm/brotli v1.0.5
github.com/aws/aws-sdk-go v1.44.279
github.com/baidubce/bce-sdk-go v0.9.153
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670
github.com/cespare/xxhash v1.1.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/fsnotify/fsnotify v1.6.0
github.com/go-redis/redis/v8 v8.11.5
github.com/golang/protobuf v1.5.3
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.1.0
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.4+incompatible
github.com/iwind/TeaGo v0.0.0-20230630104525-161f0b32996d
github.com/iwind/gofcgi v0.0.0-20210528023741-a92711d45f11
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
github.com/iwind/gowebp v0.0.0-20230615040911-5013dbb9d508
github.com/iwind/gowebp v0.0.0-20230927084601-21954d2e229f
github.com/klauspost/compress v1.16.5
github.com/mattn/go-sqlite3 v1.14.9
github.com/mattn/go-sqlite3 v1.14.17
github.com/mdlayher/netlink v1.7.1
github.com/miekg/dns v1.1.43
github.com/mssola/useragent v1.0.0
github.com/pires/go-proxyproto v0.6.1
github.com/qiniu/go-sdk/v7 v7.16.0
github.com/quic-go/quic-go v0.36.1
github.com/quic-go/quic-go v0.39.0
github.com/shirou/gopsutil/v3 v3.22.2
github.com/tencentyun/cos-go-sdk-v5 v0.7.41
golang.org/x/image v0.7.0
golang.org/x/net v0.12.0
golang.org/x/sys v0.10.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0
google.golang.org/grpc v1.55.0
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -49,10 +51,10 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
@@ -60,23 +62,22 @@ require (
github.com/mdlayher/socket v0.4.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/onsi/ginkgo/v2 v2.12.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/tdewolff/minify/v2 v2.12.7 // indirect
github.com/tdewolff/parse/v2 v2.6.6 // indirect
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
go.uber.org/mock v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.11.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

76
go.sum
View File

@@ -7,6 +7,8 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aws/aws-sdk-go v1.44.279 h1:g23dxnYjIiPlQo0gIKNR0zVPsSvo1bj5frWln+5sfhk=
github.com/aws/aws-sdk-go v1.44.279/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/baidubce/bce-sdk-go v0.9.153 h1:h5l2EXehe4C4/bdlAPBaULrbnEDgIu5HOYgniN7bjGM=
github.com/baidubce/bce-sdk-go v0.9.153/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670 h1:FQPKKjDhzG0T4ew6dm6MGrXb4PRAi8ZmTuYuxcF62BM=
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670/go.mod h1:iRWAFbKXMMkVQyxZ1PfGlkBr1TjATx1zy2MRprV7A3Q=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -41,8 +43,6 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -53,8 +53,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA=
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 h1:gpptm606MZYGaMHMsB4Srmb6EbW/IVHnt04rcMXnkBQ=
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.4+incompatible h1:XRAk4HBDLCYEdPLWtKf5iZhOi7lfx17aY0oSO9+mcg8=
@@ -67,8 +69,8 @@ github.com/iwind/gofcgi v0.0.0-20210528023741-a92711d45f11 h1:DaQjoWZhLNxjhIXedV
github.com/iwind/gofcgi v0.0.0-20210528023741-a92711d45f11/go.mod h1:JtbX20untAjUVjZs1ZBtq80f5rJWvwtQNRL6EnuYRnY=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
github.com/iwind/gowebp v0.0.0-20230615040911-5013dbb9d508 h1:fjKiHAyPQmdwuw1DQ2BI1JTbhUWAtI3Kr9wIZQBdRgQ=
github.com/iwind/gowebp v0.0.0-20230615040911-5013dbb9d508/go.mod h1:Re7TEhwL+ygnxFg52fC0PWy01ULAIZp2QR0q5WwEOQA=
github.com/iwind/gowebp v0.0.0-20230927084601-21954d2e229f h1:DCUsOhpZbuKiROTZGc9V9z1uEfm+EbU5nhze+Tv5xo0=
github.com/iwind/gowebp v0.0.0-20230927084601-21954d2e229f/go.mod h1:Re7TEhwL+ygnxFg52fC0PWy01ULAIZp2QR0q5WwEOQA=
github.com/iwind/nftables v0.0.0-20230419014751-9f023a644ad4 h1:RPAH9Sj9l/20zH5zU5/iJGszfwPq6eLjoiC/n/asulA=
github.com/iwind/nftables v0.0.0-20230419014751-9f023a644ad4/go.mod h1:7OLL+86wZKfBnAJxNxmdcZ0ebbgdp/A28fcagx9oJqA=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -93,8 +95,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
@@ -109,9 +111,9 @@ github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5
github.com/mssola/useragent v1.0.0/go.mod h1:hz9Cqz4RXusgg1EdI4Al0INR62kP7aPSRNHnpU+b85Y=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA=
github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw=
github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -126,12 +128,10 @@ github.com/qiniu/go-sdk/v7 v7.16.0/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYX
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.36.1 h1:WsG73nVtnDy1TiACxFxhQ3TqaW+DipmqzLEtNlAwZyY=
github.com/quic-go/quic-go v0.36.1/go.mod h1:zPetvwDlILVxt15n3hr3Gf/I3mDf7LpLKPhR4Ez0AZQ=
github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So=
github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -161,22 +161,26 @@ github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
@@ -184,12 +188,13 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -202,8 +207,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -214,8 +217,10 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -228,21 +233,20 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=

View File

@@ -1,6 +1,7 @@
package apps
import (
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
executils "github.com/TeaOSLab/EdgeNode/internal/utils/exec"
@@ -10,6 +11,7 @@ import (
"github.com/iwind/gosock/pkg/gosock"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
@@ -87,8 +89,8 @@ func (this *AppCmd) Print() {
fmt.Println("")
fmt.Println("Options:")
spaces := 20
max := 40
var spaces = 20
var max = 40
for _, option := range this.options {
l := len(option.Code)
if l < max && l > spaces {
@@ -126,9 +128,12 @@ func (this *AppCmd) On(arg string, callback func()) {
// Run 运行
func (this *AppCmd) Run(main func()) {
// 获取参数
args := os.Args[1:]
var args = os.Args[1:]
if len(args) > 0 {
switch args[0] {
var mainArg = args[0]
this.callDirective(mainArg + ":before")
switch mainArg {
case "-v", "version", "-version", "--version":
this.runVersion()
return
@@ -151,19 +156,19 @@ func (this *AppCmd) Run(main func()) {
// 查找指令
for _, directive := range this.directives {
if directive.Arg == args[0] {
if directive.Arg == mainArg {
directive.Callback()
return
}
}
fmt.Println("unknown command '" + args[0] + "'")
fmt.Println("unknown command '" + mainArg + "'")
return
}
// 日志
writer := new(LogWriter)
var writer = new(LogWriter)
writer.Init()
logs.SetWriter(writer)
@@ -191,7 +196,7 @@ func (this *AppCmd) runStart() {
_ = os.Setenv("EdgeBackground", "on")
cmd := exec.Command(os.Args[0])
var cmd = exec.Command(this.exe())
cmd.SysProcAttr = &syscall.SysProcAttr{
Foreground: false,
Setsid: true,
@@ -203,6 +208,9 @@ func (this *AppCmd) runStart() {
return
}
// create symbolic links
_ = this.createSymLinks()
fmt.Println(this.product+" started ok, pid:", cmd.Process.Pid)
}
@@ -277,3 +285,69 @@ func (this *AppCmd) ParseOptions(args []string) map[string][]string {
}
return result
}
func (this *AppCmd) exe() string {
var exe, _ = os.Executable()
if len(exe) == 0 {
exe = os.Args[0]
}
return exe
}
// 创建软链接
func (this *AppCmd) createSymLinks() error {
if runtime.GOOS != "linux" {
return nil
}
var exe, _ = os.Executable()
if len(exe) == 0 {
return nil
}
var errorList = []string{}
// bin
{
var target = "/usr/bin/" + teaconst.ProcessName
old, _ := filepath.EvalSymlinks(target)
if old != exe {
_ = os.Remove(target)
err := os.Symlink(exe, target)
if err != nil {
errorList = append(errorList, err.Error())
}
}
}
// log
{
var realPath = filepath.Dir(filepath.Dir(exe)) + "/logs/run.log"
var target = "/var/log/" + teaconst.ProcessName + ".log"
old, _ := filepath.EvalSymlinks(target)
if old != realPath {
_ = os.Remove(target)
err := os.Symlink(realPath, target)
if err != nil {
errorList = append(errorList, err.Error())
}
}
}
if len(errorList) > 0 {
return errors.New(strings.Join(errorList, "\n"))
}
return nil
}
func (this *AppCmd) callDirective(code string) {
for _, directive := range this.directives {
if directive.Arg == code {
if directive.Callback != nil {
directive.Callback()
}
return
}
}
}

View File

@@ -7,12 +7,12 @@ import "errors"
// 常用的几个错误
var (
ErrNotFound = errors.New("cache not found")
ErrFileIsWriting = errors.New("the file is writing")
ErrFileIsWriting = errors.New("the cache file is updating")
ErrInvalidRange = errors.New("invalid range")
ErrEntityTooLarge = errors.New("entity too large")
ErrWritingUnavailable = errors.New("writing unavailable")
ErrWritingQueueFull = errors.New("writing queue full")
ErrTooManyOpenFiles = errors.New("too many open files")
ErrServerIsBusy = errors.New("server is busy")
)
// CapacityError 容量错误
@@ -38,7 +38,7 @@ func CanIgnoreErr(err error) bool {
err == ErrEntityTooLarge ||
err == ErrWritingUnavailable ||
err == ErrWritingQueueFull ||
err == ErrTooManyOpenFiles {
err == ErrServerIsBusy {
return true
}
_, ok := err.(*CapacityError)

View File

@@ -3,7 +3,6 @@ package caches
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"strings"
"time"
)
type ItemType = int
@@ -16,7 +15,7 @@ const (
// 计算当前周
// 不要用YW因为需要计算两周是否临近
func currentWeek() int32 {
return int32(time.Now().Unix() / 86400)
return int32(fasttime.Now().Unix() / 86400)
}
type Item struct {
@@ -29,10 +28,7 @@ type Item struct {
MetaSize int64 `json:"metaSize"`
Host string `json:"host"` // 主机名
ServerId int64 `json:"serverId"` // 服务ID
Week1Hits int64 `json:"week1Hits"`
Week2Hits int64 `json:"week2Hits"`
Week int32 `json:"week"`
Week int32 `json:"week"`
}
func (this *Item) IsExpired() bool {
@@ -47,20 +43,6 @@ func (this *Item) Size() int64 {
return this.HeaderSize + this.BodySize
}
func (this *Item) IncreaseHit(week int32) {
if this.Week == week {
this.Week2Hits++
} else {
if week-this.Week == 1 {
this.Week1Hits = this.Week2Hits
} else {
this.Week1Hits = 0
}
this.Week2Hits = 1
this.Week = week
}
}
func (this *Item) RequestURI() string {
var schemeIndex = strings.Index(this.Key, "://")
if schemeIndex <= 0 {

View File

@@ -1,8 +1,9 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package caches
package caches_test
import (
"github.com/TeaOSLab/EdgeNode/internal/caches"
"github.com/TeaOSLab/EdgeNode/internal/zero"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
@@ -11,27 +12,14 @@ import (
"time"
)
func TestItem_IncreaseHit(t *testing.T) {
var week = currentWeek()
var item = &Item{}
//item.Week = 2704
item.Week2Hits = 100
item.IncreaseHit(week)
t.Log("week:", item.Week, "week1:", item.Week1Hits, "week2:", item.Week2Hits)
item.IncreaseHit(week)
t.Log("week:", item.Week, "week1:", item.Week1Hits, "week2:", item.Week2Hits)
}
func TestItems_Memory(t *testing.T) {
var stat = &runtime.MemStats{}
runtime.ReadMemStats(stat)
var memory1 = stat.HeapInuse
var items = []*Item{}
var items = []*caches.Item{}
for i := 0; i < 10_000_000; i++ {
items = append(items, &Item{
items = append(items, &caches.Item{
Key: types.String(i),
})
}
@@ -41,18 +29,11 @@ func TestItems_Memory(t *testing.T) {
t.Log(memory1, memory2, (memory2-memory1)/1024/1024, "M")
var weekItems = make(map[string]*Item, 10_000_000)
for _, item := range items {
weekItems[item.Key] = item
}
runtime.ReadMemStats(stat)
var memory3 = stat.HeapInuse
t.Log(memory2, memory3, (memory3-memory2)/1024/1024, "M")
time.Sleep(1 * time.Second)
t.Log(len(items), len(weekItems))
}
func TestItems_Memory2(t *testing.T) {
@@ -88,7 +69,7 @@ func TestItem_RequestURI(t *testing.T) {
"https://goedge.cn:8080/hello/world",
"https://goedge.cn/hello/world?v=1&t=123",
} {
var item = &Item{Key: u}
var item = &caches.Item{Key: u}
t.Log(u, "=>", item.RequestURI())
}
}

View File

@@ -4,13 +4,18 @@ package caches
import (
"database/sql"
"errors"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/ttlcache"
"github.com/TeaOSLab/EdgeNode/internal/utils/dbs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/fnv"
"github.com/TeaOSLab/EdgeNode/internal/zero"
"github.com/iwind/TeaGo/types"
"os"
"strings"
"sync"
"time"
)
@@ -24,7 +29,7 @@ type FileList struct {
onAdd func(item *Item)
onRemove func(item *Item)
memoryCache *ttlcache.Cache
memoryCache *ttlcache.Cache[zero.Zero]
// 老数据库地址
oldDir string
@@ -33,7 +38,7 @@ type FileList struct {
func NewFileList(dir string) ListInterface {
return &FileList{
dir: dir,
memoryCache: ttlcache.NewCache(),
memoryCache: ttlcache.NewCache[zero.Zero](),
}
}
@@ -59,19 +64,37 @@ func (this *FileList) Init() error {
}
remotelogs.Println("CACHE", "loading database from '"+dir+"' ...")
var wg = &sync.WaitGroup{}
var locker = sync.Mutex{}
var lastErr error
for i := 0; i < CountFileDB; i++ {
var db = NewFileListDB()
err = db.Open(dir + "/db-" + types.String(i) + ".db")
if err != nil {
return err
}
wg.Add(1)
go func(i int) {
defer wg.Done()
err = db.Init()
if err != nil {
return err
}
var db = NewFileListDB()
dbErr := db.Open(dir + "/db-" + types.String(i) + ".db")
if dbErr != nil {
lastErr = dbErr
return
}
this.dbList[i] = db
dbErr = db.Init()
if dbErr != nil {
lastErr = dbErr
return
}
locker.Lock()
this.dbList[i] = db
locker.Unlock()
}(i)
}
wg.Wait()
if lastErr != nil {
return lastErr
}
// 升级老版本数据库
@@ -99,9 +122,7 @@ func (this *FileList) Add(hash string, item *Item) error {
return err
}
// 这里不增加点击量,以减少对数据库的操作次数
this.memoryCache.Write(hash, 1, item.ExpiredAt)
this.memoryCache.Write(hash, zero.Zero{}, this.maxExpiresAtForMemoryCache(item.ExpiredAt))
if this.onAdd != nil {
this.onAdd(item)
@@ -134,15 +155,27 @@ func (this *FileList) Exist(hash string) (bool, error) {
var expiredAt int64
err := row.Scan(&expiredAt)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return false, err
}
this.memoryCache.Write(hash, 1, expiredAt)
this.memoryCache.Write(hash, zero.Zero{}, this.maxExpiresAtForMemoryCache(expiredAt))
return true, nil
}
func (this *FileList) ExistQuick(hash string) (isReady bool, found bool) {
var db = this.GetDB(hash)
if !db.IsReady() || !db.HashMapIsLoaded() {
return
}
isReady = true
found = db.hashMap.Exist(hash)
return
}
// CleanPrefix 清理某个前缀的缓存数据
func (this *FileList) CleanPrefix(prefix string) error {
if len(prefix) == 0 {
@@ -204,7 +237,7 @@ func (this *FileList) CleanMatchPrefix(prefix string) error {
}
func (this *FileList) Remove(hash string) error {
_, err := this.remove(hash)
_, err := this.remove(hash, false)
return err
}
@@ -223,11 +256,16 @@ func (this *FileList) Purge(count int, callback func(hash string) error) (int, e
if err != nil {
return 0, nil
}
if len(hashStrings) == 0 {
continue
}
countFound += len(hashStrings)
// 不在 rows.Next() 循环中操作是为了避免死锁
for _, hash := range hashStrings {
err = this.Remove(hash)
_, err = this.remove(hash, true)
if err != nil {
return 0, err
}
@@ -237,6 +275,11 @@ func (this *FileList) Purge(count int, callback func(hash string) error) (int, e
return 0, err
}
}
_, err = db.writeDB.Exec(`DELETE FROM "cacheItems" WHERE "hash" IN ('` + strings.Join(hashStrings, "', '") + `')`)
if err != nil {
return 0, err
}
}
return countFound, nil
@@ -254,24 +297,27 @@ func (this *FileList) PurgeLFU(count int, callback func(hash string) error) erro
return err
}
if len(hashStrings) == 0 {
continue
}
// 不在 rows.Next() 循环中操作是为了避免死锁
for _, hash := range hashStrings {
notFound, err := this.remove(hash)
_, err = this.remove(hash, true)
if err != nil {
return err
}
if notFound {
err = db.DeleteHitAsync(hash)
if err != nil {
return db.WrapError(err)
}
}
err = callback(hash)
if err != nil {
return err
}
}
_, err = db.writeDB.Exec(`DELETE FROM "cacheItems" WHERE "hash" IN ('` + strings.Join(hashStrings, "', '") + `')`)
if err != nil {
return err
}
}
return nil
}
@@ -372,7 +418,16 @@ func (this *FileList) GetDB(hash string) *FileListDB {
return this.dbList[fnv.HashString(hash)%CountFileDB]
}
func (this *FileList) remove(hash string) (notFound bool, err error) {
func (this *FileList) HashMapIsLoaded() bool {
for _, db := range this.dbList {
if !db.HashMapIsLoaded() {
return false
}
}
return true
}
func (this *FileList) remove(hash string, isDeleted bool) (notFound bool, err error) {
var db = this.GetDB(hash)
if !db.IsReady() {
@@ -388,14 +443,11 @@ func (this *FileList) remove(hash string) (notFound bool, err error) {
// 从缓存中删除
this.memoryCache.Delete(hash)
err = db.DeleteSync(hash)
if err != nil {
return false, db.WrapError(err)
}
err = db.DeleteHitAsync(hash)
if err != nil {
return false, db.WrapError(err)
if !isDeleted {
err = db.DeleteSync(hash)
if err != nil {
return false, db.WrapError(err)
}
}
if this.onRemove != nil {
@@ -428,7 +480,7 @@ func (this *FileList) UpgradeV3(oldDir string, brokenOnError bool) error {
remotelogs.Println("CACHE", "upgrading local database finished")
}()
db, err := dbs.OpenWriter("file:" + indexDBPath + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=OFF&_locking_mode=EXCLUSIVE")
db, err := dbs.OpenWriter("file:" + indexDBPath + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=" + dbs.SyncMode + "&_locking_mode=EXCLUSIVE")
if err != nil {
return err
}
@@ -493,9 +545,6 @@ func (this *FileList) UpgradeV3(oldDir string, brokenOnError bool) error {
MetaSize: metaSize,
Host: host,
ServerId: serverId,
Week1Hits: 0,
Week2Hits: 0,
Week: 0,
})
if err != nil {
if brokenOnError {
@@ -515,3 +564,11 @@ func (this *FileList) UpgradeV3(oldDir string, brokenOnError bool) error {
return nil
}
func (this *FileList) maxExpiresAtForMemoryCache(expiresAt int64) int64 {
var maxTimestamp = fasttime.Now().Unix() + 7200
if expiresAt > maxTimestamp {
return maxTimestamp
}
return expiresAt
}

View File

@@ -4,8 +4,8 @@ package caches
import (
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/dbs"
@@ -16,7 +16,6 @@ import (
"net"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
@@ -28,15 +27,13 @@ type FileListDB struct {
readDB *dbs.DB
writeDB *dbs.DB
writeBatch *dbs.Batch
hashMap *FileListHashMap
itemsTableName string
hitsTableName string
isClosed bool
isReady bool
isClosed bool // 是否已关闭
isReady bool // 是否已完成初始化
hashMapIsLoaded bool // Hash是否已加载
// cacheItems
existsByHashStmt *dbs.Stmt // 根据hash检查是否存在
@@ -51,16 +48,11 @@ type FileListDB struct {
deleteByHashStmt *dbs.Stmt // 根据hash删除数据
deleteByHashSQL string
statStmt *dbs.Stmt // 统计
purgeStmt *dbs.Stmt // 清理
deleteAllStmt *dbs.Stmt // 删除所有数据
listOlderItemsStmt *dbs.Stmt // 读取较早存储的缓存
updateAccessWeekSQL string // 修改访问日期
// hits
insertHitSQL string // 写入数据
increaseHitSQL string // 增加点击量
deleteHitByHashSQL string // 根据hash删除数据
statStmt *dbs.Stmt // 统计
purgeStmt *dbs.Stmt // 清理
deleteAllStmt *dbs.Stmt // 删除所有数据
listOlderItemsStmt *dbs.Stmt // 读取较早存储的缓存
updateAccessWeekStmt *dbs.Stmt // 修改访问日期
}
func NewFileListDB() *FileListDB {
@@ -81,9 +73,9 @@ func (this *FileListDB) Open(dbPath string) error {
// write db
// 这里不能加 EXCLUSIVE 锁,不然异步事务可能会失败
writeDB, err := dbs.OpenWriter("file:" + dbPath + "?cache=private&mode=rwc&_journal_mode=WAL&_sync=OFF&_cache_size=" + types.String(cacheSize) + "&_secure_delete=FAST")
writeDB, err := dbs.OpenWriter("file:" + dbPath + "?cache=private&mode=rwc&_journal_mode=WAL&_sync=" + dbs.SyncMode + "&_cache_size=" + types.String(cacheSize) + "&_secure_delete=FAST")
if err != nil {
return errors.New("open write database failed: " + err.Error())
return fmt.Errorf("open write database failed: %w", err)
}
writeDB.SetMaxOpenConns(1)
@@ -108,24 +100,14 @@ func (this *FileListDB) Open(dbPath string) error {
}
}
this.writeBatch = dbs.NewBatch(writeDB, 4)
this.writeBatch.OnFail(func(err error) {
remotelogs.Warn("LIST_FILE_DB", "run batch failed: "+err.Error()+" ("+filepath.Base(this.dbPath)+")")
})
goman.New(func() {
this.writeBatch.Exec()
})
if teaconst.EnableDBStat {
this.writeBatch.EnableStat(true)
this.writeDB.EnableStat(true)
}
// read db
readDB, err := dbs.OpenReader("file:" + dbPath + "?cache=private&mode=ro&_journal_mode=WAL&_sync=OFF&_cache_size=" + types.String(cacheSize))
if err != nil {
return errors.New("open read database failed: " + err.Error())
return fmt.Errorf("open read database failed: %w", err)
}
readDB.SetMaxOpenConns(runtime.NumCPU())
@@ -141,12 +123,11 @@ func (this *FileListDB) Open(dbPath string) error {
func (this *FileListDB) Init() error {
this.itemsTableName = "cacheItems"
this.hitsTableName = "hits"
// 创建
var err = this.initTables(1)
if err != nil {
return errors.New("init tables failed: " + err.Error())
return fmt.Errorf("init tables failed: %w", err)
}
// 常用语句
@@ -166,7 +147,7 @@ func (this *FileListDB) Init() error {
return err
}
this.selectHashListStmt, err = this.readDB.Prepare(`SELECT "id", "hash" FROM "` + this.itemsTableName + `" WHERE id>:id ORDER BY id ASC LIMIT 2000`)
this.selectHashListStmt, err = this.readDB.Prepare(`SELECT "id", "hash" FROM "` + this.itemsTableName + `" WHERE id>? ORDER BY id ASC LIMIT 2000`)
if err != nil {
return err
}
@@ -192,42 +173,20 @@ func (this *FileListDB) Init() error {
return err
}
this.updateAccessWeekStmt, err = this.writeDB.Prepare(`UPDATE "` + this.itemsTableName + `" SET "accessWeek"=? WHERE "hash"=?`)
if err != nil {
return err
}
this.listOlderItemsStmt, err = this.readDB.Prepare(`SELECT "hash" FROM "` + this.itemsTableName + `" ORDER BY "accessWeek" ASC, "id" ASC LIMIT ?`)
if err != nil {
return err
}
this.updateAccessWeekSQL = `UPDATE "` + this.itemsTableName + `" SET "accessWeek"=? WHERE "hash"=?`
this.insertHitSQL = `INSERT INTO "` + this.hitsTableName + `" ("hash", "week2Hits", "week") VALUES (?, 1, ?)`
this.increaseHitSQL = `INSERT INTO "` + this.hitsTableName + `" ("hash", "week2Hits", "week") VALUES (?, 1, ?) ON CONFLICT("hash") DO UPDATE SET "week1Hits"=IIF("week"=?, "week1Hits", "week2Hits"), "week2Hits"=IIF("week"=?, "week2Hits"+1, 1), "week"=?`
this.deleteHitByHashSQL = `DELETE FROM "` + this.hitsTableName + `" WHERE "hash"=?`
this.isReady = true
// 加载HashMap
go func() {
err := this.hashMap.Load(this)
if err != nil {
remotelogs.Error("LIST_FILE_DB", "load hash map failed: "+err.Error()+"(file: "+this.dbPath+")")
// 自动修复错误
// TODO 将来希望能尽可能恢复以往数据库中的内容
if strings.Contains(err.Error(), "database is closed") || strings.Contains(err.Error(), "database disk image is malformed") {
_ = this.Close()
this.deleteDB()
remotelogs.Println("LIST_FILE_DB", "recreating the database (file:"+this.dbPath+") ...")
err = this.Open(this.dbPath)
if err != nil {
remotelogs.Error("LIST_FILE_DB", "recreate the database failed: "+err.Error()+" (file:"+this.dbPath+")")
} else {
_ = this.Init()
}
}
}
}()
go this.loadHashMap()
return nil
}
@@ -350,15 +309,8 @@ func (this *FileListDB) ListHashes(lastId int64) (hashList []string, maxId int64
}
func (this *FileListDB) IncreaseHitAsync(hash string) error {
var week = timeutil.Format("YW")
this.writeBatch.Add(this.increaseHitSQL, hash, week, week, week, week)
this.writeBatch.Add(this.updateAccessWeekSQL, week, hash)
return nil
}
func (this *FileListDB) DeleteHitAsync(hash string) error {
this.writeBatch.Add(this.deleteHitByHashSQL, hash)
return nil
_, err := this.updateAccessWeekStmt.Exec(timeutil.Format("YW"), hash)
return err
}
func (this *FileListDB) CleanPrefix(prefix string) error {
@@ -366,10 +318,9 @@ func (this *FileListDB) CleanPrefix(prefix string) error {
return nil
}
var count = int64(10000)
var staleLife = 600 // TODO 需要可以设置
var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的
for {
result, err := this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET expiredAt=0,staleAt=? WHERE id IN (SELECT id FROM "`+this.itemsTableName+`" WHERE expiredAt>0 AND createdAt<=? AND INSTR("key", ?)=1 LIMIT `+types.String(count)+`)`, unixTime+int64(staleLife), unixTime, prefix)
result, err := this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET expiredAt=0,staleAt=? WHERE id IN (SELECT id FROM "`+this.itemsTableName+`" WHERE expiredAt>0 AND createdAt<=? AND INSTR("key", ?)=1 LIMIT `+types.String(count)+`)`, unixTime+DefaultStaleCacheSeconds, unixTime, prefix)
if err != nil {
return this.WrapError(err)
}
@@ -413,15 +364,14 @@ func (this *FileListDB) CleanMatchKey(key string) error {
queryKey = strings.Replace(queryKey, "*", "%", 1)
// TODO 检查大批量数据下的操作性能
var staleLife = 600 // TODO 需要可以设置
var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryKey)
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryKey)
if err != nil {
return err
}
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryKey+SuffixAll+"%")
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryKey+SuffixAll+"%")
if err != nil {
return err
}
@@ -455,10 +405,9 @@ func (this *FileListDB) CleanMatchPrefix(prefix string) error {
queryPrefix += "%"
// TODO 检查大批量数据下的操作性能
var staleLife = 600 // TODO 需要可以设置
var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryPrefix)
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryPrefix)
return err
}
@@ -509,6 +458,9 @@ func (this *FileListDB) Close() error {
if this.deleteAllStmt != nil {
_ = this.deleteAllStmt.Close()
}
if this.updateAccessWeekStmt != nil {
_ = this.updateAccessWeekStmt.Close()
}
if this.listOlderItemsStmt != nil {
_ = this.listOlderItemsStmt.Close()
}
@@ -539,7 +491,11 @@ func (this *FileListDB) WrapError(err error) error {
if err == nil {
return nil
}
return errors.New(err.Error() + "(file: " + this.dbPath + ")")
return fmt.Errorf("%w (file: %s)", err, this.dbPath)
}
func (this *FileListDB) HashMapIsLoaded() bool {
return this.hashMapIsLoaded
}
// 初始化
@@ -602,32 +558,9 @@ ALTER TABLE "cacheItems" ADD "accessWeek" varchar(6);
}
}
// 删除hits表
{
_, err := this.writeDB.Exec(`CREATE TABLE IF NOT EXISTS "` + this.hitsTableName + `" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"hash" varchar(32),
"week1Hits" integer DEFAULT 0,
"week2Hits" integer DEFAULT 0,
"week" varchar(6)
);
CREATE UNIQUE INDEX IF NOT EXISTS "hits_hash"
ON "` + this.hitsTableName + `" (
"hash" ASC
);
`)
if err != nil {
// 尝试删除重建
if times < 3 {
_, dropErr := this.writeDB.Exec(`DROP TABLE "` + this.hitsTableName + `"`)
if dropErr == nil {
return this.initTables(times + 1)
}
return this.WrapError(err)
}
return this.WrapError(err)
}
_, _ = this.writeDB.Exec(`DROP TABLE "hits"`)
}
return nil
@@ -661,12 +594,11 @@ func (this *FileListDB) shouldRecover() bool {
}
var errString = ""
var shouldRecover = false
for result.Next() {
err = result.Scan(&errString)
if result.Next() {
_ = result.Scan(&errString)
if strings.TrimSpace(errString) != "ok" {
shouldRecover = true
}
break
}
_ = result.Close()
return shouldRecover
@@ -678,3 +610,30 @@ func (this *FileListDB) deleteDB() {
_ = os.Remove(this.dbPath + "-shm")
_ = os.Remove(this.dbPath + "-wal")
}
// 加载Hash列表
func (this *FileListDB) loadHashMap() {
this.hashMapIsLoaded = false
err := this.hashMap.Load(this)
if err != nil {
remotelogs.Error("LIST_FILE_DB", "load hash map failed: "+err.Error()+"(file: "+this.dbPath+")")
// 自动修复错误
// TODO 将来希望能尽可能恢复以往数据库中的内容
if strings.Contains(err.Error(), "database is closed") || strings.Contains(err.Error(), "database disk image is malformed") {
_ = this.Close()
this.deleteDB()
remotelogs.Println("LIST_FILE_DB", "recreating the database (file:"+this.dbPath+") ...")
err = this.Open(this.dbPath)
if err != nil {
remotelogs.Error("LIST_FILE_DB", "recreate the database failed: "+err.Error()+" (file:"+this.dbPath+")")
} else {
_ = this.Init()
}
}
return
}
this.hashMapIsLoaded = true
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
"time"
)
func TestFileListDB_ListLFUItems(t *testing.T) {
@@ -34,28 +33,6 @@ func TestFileListDB_ListLFUItems(t *testing.T) {
t.Log("[", len(hashList), "]", hashList)
}
func TestFileListDB_IncreaseHitAsync(t *testing.T) {
var db = caches.NewFileListDB()
defer func() {
_ = db.Close()
}()
err := db.Open(Tea.Root + "/data/cache-db-large.db")
if err != nil {
t.Fatal(err)
}
err = db.Init()
err = db.IncreaseHitAsync("4598e5231ba47d6ec7aa9ea640ff2eaf")
if err != nil {
t.Fatal(err)
}
// wait transaction
time.Sleep(1 * time.Second)
}
func TestFileListDB_CleanMatchKey(t *testing.T) {
var db = caches.NewFileListDB()
@@ -69,6 +46,9 @@ func TestFileListDB_CleanMatchKey(t *testing.T) {
}
err = db.Init()
if err != nil {
t.Fatal(err)
}
err = db.CleanMatchKey("https://*.goedge.cn/large-text")
if err != nil {
@@ -94,6 +74,9 @@ func TestFileListDB_CleanMatchPrefix(t *testing.T) {
}
err = db.Init()
if err != nil {
t.Fatal(err)
}
err = db.CleanMatchPrefix("https://*.goedge.cn/large-text")
if err != nil {

View File

@@ -9,18 +9,36 @@ import (
"sync"
)
const HashMapSharding = 31
var bigIntPool = sync.Pool{
New: func() any {
return big.NewInt(0)
},
}
// FileListHashMap 文件Hash列表
type FileListHashMap struct {
m map[uint64]zero.Zero
m []map[uint64]zero.Zero
lockers []*sync.RWMutex
locker sync.RWMutex
isAvailable bool
isReady bool
}
func NewFileListHashMap() *FileListHashMap {
var m = make([]map[uint64]zero.Zero, HashMapSharding)
var lockers = make([]*sync.RWMutex, HashMapSharding)
for i := 0; i < HashMapSharding; i++ {
m[i] = map[uint64]zero.Zero{}
lockers[i] = &sync.RWMutex{}
}
return &FileListHashMap{
m: map[uint64]zero.Zero{},
m: m,
lockers: lockers,
isAvailable: false,
isReady: false,
}
@@ -35,6 +53,7 @@ func (this *FileListHashMap) Load(db *FileListDB) error {
this.isAvailable = true
var lastId int64
var maxLoops = 50_000
for {
hashList, maxId, err := db.ListHashes(lastId)
if err != nil {
@@ -45,6 +64,11 @@ func (this *FileListHashMap) Load(db *FileListDB) error {
}
this.AddHashes(hashList)
lastId = maxId
maxLoops--
if maxLoops <= 0 {
break
}
}
this.isReady = true
@@ -56,9 +80,11 @@ func (this *FileListHashMap) Add(hash string) {
return
}
this.locker.Lock()
this.m[this.bigInt(hash)] = zero.New()
this.locker.Unlock()
hashInt, index := this.bigInt(hash)
this.lockers[index].Lock()
this.m[index][hashInt] = zero.New()
this.lockers[index].Unlock()
}
func (this *FileListHashMap) AddHashes(hashes []string) {
@@ -66,11 +92,12 @@ func (this *FileListHashMap) AddHashes(hashes []string) {
return
}
this.locker.Lock()
for _, hash := range hashes {
this.m[this.bigInt(hash)] = zero.New()
hashInt, index := this.bigInt(hash)
this.lockers[index].Lock()
this.m[index][hashInt] = zero.New()
this.lockers[index].Unlock()
}
this.locker.Unlock()
}
func (this *FileListHashMap) Delete(hash string) {
@@ -78,9 +105,10 @@ func (this *FileListHashMap) Delete(hash string) {
return
}
this.locker.Lock()
delete(this.m, this.bigInt(hash))
this.locker.Unlock()
hashInt, index := this.bigInt(hash)
this.lockers[index].Lock()
delete(this.m[index], hashInt)
this.lockers[index].Unlock()
}
func (this *FileListHashMap) Exist(hash string) bool {
@@ -91,16 +119,25 @@ func (this *FileListHashMap) Exist(hash string) bool {
// 只有完全Ready时才能判断是否为false
return true
}
this.locker.RLock()
_, ok := this.m[this.bigInt(hash)]
this.locker.RUnlock()
hashInt, index := this.bigInt(hash)
this.lockers[index].RLock()
_, ok := this.m[index][hashInt]
this.lockers[index].RUnlock()
return ok
}
func (this *FileListHashMap) Clean() {
this.locker.Lock()
this.m = map[uint64]zero.Zero{}
this.locker.Unlock()
for i := 0; i < HashMapSharding; i++ {
this.lockers[i].Lock()
}
this.m = make([]map[uint64]zero.Zero, HashMapSharding)
for i := HashMapSharding - 1; i >= 0; i-- {
this.lockers[i].Unlock()
}
}
func (this *FileListHashMap) IsReady() bool {
@@ -108,13 +145,36 @@ func (this *FileListHashMap) IsReady() bool {
}
func (this *FileListHashMap) Len() int {
this.locker.Lock()
defer this.locker.Unlock()
return len(this.m)
for i := 0; i < HashMapSharding; i++ {
this.lockers[i].Lock()
}
var count = 0
for _, shard := range this.m {
count += len(shard)
}
for i := HashMapSharding - 1; i >= 0; i-- {
this.lockers[i].Unlock()
}
return count
}
func (this *FileListHashMap) bigInt(hash string) uint64 {
var bigInt = big.NewInt(0)
bigInt.SetString(hash, 16)
return bigInt.Uint64()
func (this *FileListHashMap) SetIsAvailable(isAvailable bool) {
this.isAvailable = isAvailable
}
func (this *FileListHashMap) SetIsReady(isReady bool) {
this.isReady = isReady
}
func (this *FileListHashMap) bigInt(hash string) (hashInt uint64, index int) {
var bigInt = bigIntPool.Get().(*big.Int)
bigInt.SetString(hash, 16)
hashInt = bigInt.Uint64()
bigIntPool.Put(bigInt)
index = int(hashInt % HashMapSharding)
return
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeNode/internal/caches"
"github.com/TeaOSLab/EdgeNode/internal/zero"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"math/big"
@@ -20,15 +21,19 @@ func TestFileListHashMap_Memory(t *testing.T) {
runtime.ReadMemStats(stat1)
var m = caches.NewFileListHashMap()
m.SetIsAvailable(true)
for i := 0; i < 1_000_000; i++ {
m.Add(stringutil.Md5(types.String(i)))
}
t.Log("added:", m.Len(), "hashes")
var stat2 = &runtime.MemStats{}
runtime.ReadMemStats(stat2)
t.Log("ready", (stat2.Alloc-stat1.Alloc)/1024/1024, "M")
t.Log("remains:", m.Len(), "hashes")
}
func TestFileListHashMap_Memory2(t *testing.T) {
@@ -48,12 +53,34 @@ func TestFileListHashMap_Memory2(t *testing.T) {
}
func TestFileListHashMap_BigInt(t *testing.T) {
var bigInt = big.NewInt(0)
for _, s := range []string{"1", "2", "3", "123", "123456"} {
var hash = stringutil.Md5(s)
var bigInt = big.NewInt(0)
var bigInt1 = big.NewInt(0)
bigInt1.SetString(hash, 16)
bigInt.SetString(hash, 16)
t.Log(s, "=>", bigInt.Uint64(), "hash:", hash, "format:", strconv.FormatUint(bigInt.Uint64(), 16))
t.Log(s, "=>", bigInt1.Uint64(), "hash:", hash, "format:", strconv.FormatUint(bigInt1.Uint64(), 16), strconv.FormatUint(bigInt.Uint64(), 16))
if strconv.FormatUint(bigInt1.Uint64(), 16) != strconv.FormatUint(bigInt.Uint64(), 16) {
t.Fatal("not equal")
}
}
for i := 0; i < 1_000_000; i++ {
var hash = stringutil.Md5(types.String(i))
var bigInt1 = big.NewInt(0)
bigInt1.SetString(hash, 16)
bigInt.SetString(hash, 16)
if bigInt1.Uint64() != bigInt.Uint64() {
t.Fatal(i, "not equal")
}
}
}
@@ -95,3 +122,23 @@ func Benchmark_BigInt(b *testing.B) {
_ = bigInt.Uint64()
}
}
func BenchmarkFileListHashMap_Exist(b *testing.B) {
var m = caches.NewFileListHashMap()
m.SetIsAvailable(true)
m.SetIsReady(true)
for i := 0; i < 1_000_000; i++ {
m.Add(types.String(i))
}
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Add(types.String(rands.Int64()))
_ = m.Exist(types.String(rands.Int64()))
}
})
}

View File

@@ -281,11 +281,6 @@ func TestFileList_PurgeLFU(t *testing.T) {
t.Fatal(err)
}
err = list.IncreaseHit(stringutil.Md5("123456"))
if err != nil {
t.Fatal(err)
}
var count = 0
err = list.PurgeLFU(caches.CountFileDB*2, func(hash string) error {
t.Log(hash)
@@ -356,41 +351,6 @@ func TestFileList_CleanAll(t *testing.T) {
t.Log(list.Count())
}
func TestFileList_IncreaseHit(t *testing.T) {
var list = caches.NewFileList(Tea.Root + "/data/cache-index/p1")
defer func() {
_ = list.Close()
}()
err := list.Init()
if err != nil {
t.Fatal(err)
}
defer func() {
_ = list.Close()
}()
var before = time.Now()
defer func() {
t.Log(time.Since(before).Seconds()*1000, "ms")
}()
var count = 1_000_000
if !testutils.IsSingleTesting() {
count = 10
}
for i := 0; i < count; i++ {
err = list.IncreaseHit(stringutil.Md5("abc" + types.String(i)))
}
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestFileList_UpgradeV3(t *testing.T) {
var list = caches.NewFileList(Tea.Root + "/data/cache-index/p43").(*caches.FileList)

View File

@@ -2,7 +2,6 @@ package caches
import (
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeNode/internal/zero"
"github.com/iwind/TeaGo/logs"
"net"
"net/url"
@@ -19,9 +18,6 @@ type MemoryList struct {
itemMaps map[string]map[string]*Item // prefix => { hash => item }
weekItemMaps map[int32]map[string]zero.Zero // week => { hash => Zero }
minWeek int32
prefixes []string
locker sync.RWMutex
onAdd func(item *Item)
@@ -32,9 +28,7 @@ type MemoryList struct {
func NewMemoryList() ListInterface {
return &MemoryList{
itemMaps: map[string]map[string]*Item{},
weekItemMaps: map[int32]map[string]zero.Zero{},
minWeek: currentWeek(),
itemMaps: map[string]map[string]*Item{},
}
}
@@ -56,7 +50,6 @@ func (this *MemoryList) Reset() error {
for key := range this.itemMaps {
this.itemMaps[key] = map[string]*Item{}
}
this.weekItemMaps = map[int32]map[string]zero.Zero{}
this.locker.Unlock()
atomic.StoreInt64(&this.count, 0)
@@ -65,10 +58,6 @@ func (this *MemoryList) Reset() error {
}
func (this *MemoryList) Add(hash string, item *Item) error {
if item.Week == 0 {
item.Week = currentWeek()
}
this.locker.Lock()
prefix := this.prefix(hash)
@@ -81,14 +70,6 @@ func (this *MemoryList) Add(hash string, item *Item) error {
// 先删除,为了可以正确触发统计
oldItem, ok := itemMap[hash]
if ok {
// 从week map中删除
if oldItem.Week > 0 {
wm, ok := this.weekItemMaps[oldItem.Week]
if ok {
delete(wm, hash)
}
}
// 回调
if this.onRemove != nil {
this.onRemove(oldItem)
@@ -104,14 +85,6 @@ func (this *MemoryList) Add(hash string, item *Item) error {
itemMap[hash] = item
// week map
wm, ok := this.weekItemMaps[item.Week]
if ok {
wm[hash] = zero.New()
} else {
this.weekItemMaps[item.Week] = map[string]zero.Zero{hash: zero.New()}
}
this.locker.Unlock()
return nil
}
@@ -242,14 +215,6 @@ func (this *MemoryList) Remove(hash string) error {
atomic.AddInt64(&this.count, -1)
delete(itemMap, hash)
// week map
if item.Week > 0 {
wm, ok := this.weekItemMaps[item.Week]
if ok {
delete(wm, hash)
}
}
}
this.locker.Unlock()
@@ -261,12 +226,12 @@ func (this *MemoryList) Remove(hash string) error {
// callback 每次发现过期key的调用
func (this *MemoryList) Purge(count int, callback func(hash string) error) (int, error) {
this.locker.Lock()
deletedHashList := []string{}
var deletedHashList = []string{}
if this.purgeIndex >= len(this.prefixes) {
this.purgeIndex = 0
}
prefix := this.prefixes[this.purgeIndex]
var prefix = this.prefixes[this.purgeIndex]
this.purgeIndex++
@@ -290,14 +255,6 @@ func (this *MemoryList) Purge(count int, callback func(hash string) error) (int,
delete(itemMap, hash)
deletedHashList = append(deletedHashList, hash)
// week map
if item.Week > 0 {
wm, ok := this.weekItemMaps[item.Week]
if ok {
delete(wm, hash)
}
}
countFound++
}
@@ -322,63 +279,48 @@ func (this *MemoryList) PurgeLFU(count int, callback func(hash string) error) er
return nil
}
var week = currentWeek()
if this.minWeek > week {
this.minWeek = week
}
var deletedHashList = []string{}
var week = currentWeek()
var round = 0
this.locker.Lock()
Loop:
for w := this.minWeek; w <= week; w++ {
this.minWeek = w
for {
var found = false
round++
for _, itemMap := range this.itemMaps {
for hash, item := range itemMap {
found = true
this.locker.Lock()
wm, ok := this.weekItemMaps[w]
if ok {
var wc = len(wm)
if wc == 0 {
delete(this.weekItemMaps, w)
} else {
if wc <= count {
delete(this.weekItemMaps, w)
if week-item.Week <= 1 /** 最近有在使用 **/ && round <= 3 /** 查找轮数过多还不满足数量要求的就不再限制 **/ {
continue
}
// TODO 未来支持按照点击量排序
for hash := range wm {
count--
if count < 0 {
this.locker.Unlock()
break Loop
}
delete(wm, hash)
itemMap, ok := this.itemMaps[this.prefix(hash)]
if !ok {
continue
}
item, ok := itemMap[hash]
if !ok {
continue
}
if this.onRemove != nil {
this.onRemove(item)
}
atomic.AddInt64(&this.count, -1)
delete(itemMap, hash)
deletedHashList = append(deletedHashList, hash)
if this.onRemove != nil {
this.onRemove(item)
}
atomic.AddInt64(&this.count, -1)
delete(itemMap, hash)
deletedHashList = append(deletedHashList, hash)
count--
if count <= 0 {
break Loop
}
break
}
} else {
delete(this.weekItemMaps, w)
}
this.locker.Unlock()
if !found {
break
}
}
this.locker.Unlock()
// 执行外部操作
for _, hash := range deletedHashList {
if callback != nil {
@@ -451,23 +393,7 @@ func (this *MemoryList) IncreaseHit(hash string) error {
item, ok := itemMap[hash]
if ok {
var week = currentWeek()
// 交换位置
if item.Week > 0 && item.Week != week {
wm, ok := this.weekItemMaps[item.Week]
if ok {
delete(wm, hash)
}
wm, ok = this.weekItemMaps[week]
if ok {
wm[hash] = zero.New()
} else {
this.weekItemMaps[week] = map[string]zero.Zero{hash: zero.New()}
}
}
item.IncreaseHit(week)
item.Week = currentWeek()
}
this.locker.Unlock()

View File

@@ -6,7 +6,9 @@ import (
"github.com/cespare/xxhash"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
"math/rand"
"sort"
"strconv"
"testing"
"time"
@@ -32,7 +34,6 @@ func TestMemoryList_Add(t *testing.T) {
})
t.Log(list.prefixes)
logs.PrintAsJSON(list.itemMaps, t)
logs.PrintAsJSON(list.weekItemMaps, t)
t.Log(list.Count())
}
@@ -51,7 +52,6 @@ func TestMemoryList_Remove(t *testing.T) {
})
_ = list.Remove("b")
list.print(t)
logs.PrintAsJSON(list.weekItemMaps, t)
t.Log(list.Count())
}
@@ -83,7 +83,6 @@ func TestMemoryList_Purge(t *testing.T) {
return nil
})
list.print(t)
logs.PrintAsJSON(list.weekItemMaps, t)
for i := 0; i < 1000; i++ {
_, _ = list.Purge(100, func(hash string) error {
@@ -172,27 +171,64 @@ func TestMemoryList_CleanPrefix(t *testing.T) {
t.Log(time.Since(before).Seconds()*1000, "ms")
}
func TestMapRandomDelete(t *testing.T) {
var countMap = map[int]int{} // k => count
for j := 0; j < 1_000_000; j++ {
var m = map[int]bool{}
for i := 0; i < 100; i++ {
m[i] = true
}
var count = 0
for k := range m {
delete(m, k)
count++
if count >= 10 {
break
}
}
for k := range m {
countMap[k]++
}
}
var counts = []int{}
for _, count := range countMap {
counts = append(counts, count)
}
sort.Ints(counts)
t.Log("["+types.String(len(counts))+"]", counts)
}
func TestMemoryList_PurgeLFU(t *testing.T) {
var list = NewMemoryList().(*MemoryList)
list.minWeek = 2704
var before = time.Now()
defer func() {
t.Log(time.Since(before).Seconds()*1000, "ms")
}()
t.Log("current week:", currentWeek())
_ = list.Add("1", &Item{})
_ = list.Add("2", &Item{})
_ = list.Add("3", &Item{})
_ = list.Add("4", &Item{})
_ = list.Add("5", &Item{})
_ = list.Add("6", &Item{Week: 2704})
_ = list.Add("7", &Item{Week: 2704})
_ = list.Add("8", &Item{Week: 2705})
err := list.PurgeLFU(2, func(hash string) error {
//_ = list.IncreaseHit("1")
//_ = list.IncreaseHit("2")
//_ = list.IncreaseHit("3")
//_ = list.IncreaseHit("4")
//_ = list.IncreaseHit("5")
count, err := list.Count()
if err != nil {
t.Fatal(err)
}
t.Log("count items before purge:", count)
err = list.PurgeLFU(5, func(hash string) error {
t.Log("purge lfu:", hash)
return nil
})
@@ -201,40 +237,18 @@ func TestMemoryList_PurgeLFU(t *testing.T) {
}
t.Log("ok")
logs.PrintAsJSON(list.weekItemMaps, t)
t.Log(list.Count())
}
func TestMemoryList_IncreaseHit(t *testing.T) {
var list = NewMemoryList().(*MemoryList)
var item = &Item{}
item.Week = 2705
item.Week2Hits = 100
_ = list.Add("a", &Item{})
_ = list.Add("a", item)
t.Log("hits1:", item.Week1Hits, "hits2:", item.Week2Hits, "week:", item.Week)
logs.PrintAsJSON(list.weekItemMaps, t)
_ = list.IncreaseHit("a")
t.Log("hits1:", item.Week1Hits, "hits2:", item.Week2Hits, "week:", item.Week)
logs.PrintAsJSON(list.weekItemMaps, t)
_ = list.IncreaseHit("a")
t.Log("hits1:", item.Week1Hits, "hits2:", item.Week2Hits, "week:", item.Week)
logs.PrintAsJSON(list.weekItemMaps, t)
count, err = list.Count()
if err != nil {
t.Fatal(err)
}
t.Log("count items left:", count)
}
func TestMemoryList_CleanAll(t *testing.T) {
var list = NewMemoryList().(*MemoryList)
var item = &Item{}
item.Week = 2705
item.Week2Hits = 100
_ = list.Add("a", &Item{})
_ = list.CleanAll()
logs.PrintAsJSON(list.itemMaps, t)
logs.PrintAsJSON(list.weekItemMaps, t)
t.Log(list.Count())
}

View File

@@ -1,6 +1,7 @@
package caches
import (
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
@@ -8,6 +9,7 @@ import (
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/types"
"golang.org/x/sys/unix"
"strconv"
"sync"
)
@@ -148,8 +150,7 @@ func (this *Manager) FindPolicy(policyId int64) *serverconfigs.HTTPCachePolicy {
this.locker.RLock()
defer this.locker.RUnlock()
p, _ := this.policyMap[policyId]
return p
return this.policyMap[policyId]
}
// FindStorageWithPolicy 根据策略ID查找存储
@@ -157,8 +158,7 @@ func (this *Manager) FindStorageWithPolicy(policyId int64) StorageInterface {
this.locker.RLock()
defer this.locker.RUnlock()
storage, _ := this.storageMap[policyId]
return storage
return this.storageMap[policyId]
}
// NewStorageWithPolicy 根据策略获取存储对象
@@ -178,8 +178,30 @@ func (this *Manager) TotalDiskSize() int64 {
defer this.locker.RUnlock()
var total = int64(0)
var sidMap = map[string]bool{} // partition sid => bool
for _, storage := range this.storageMap {
total += storage.TotalDiskSize()
// 这里不能直接用 storage.TotalDiskSize() 相加,因为多个缓存策略缓存目录可能处在同一个分区目录下
fileStorage, ok := storage.(*FileStorage)
if ok {
var options = fileStorage.options // copy
if options != nil {
var dir = options.Dir // copy
if len(dir) == 0 {
continue
}
var stat = &unix.Statfs_t{}
err := unix.Statfs(dir, stat)
if err != nil {
continue
}
var sid = fmt.Sprintf("%d_%d", stat.Fsid.Val[0], stat.Fsid.Val[1])
if sidMap[sid] {
continue
}
sidMap[sid] = true
total += int64(stat.Blocks-stat.Bfree) * int64(stat.Bsize) // we add extra int64() for darwin
}
}
}
if total < 0 {
@@ -234,3 +256,19 @@ func (this *Manager) FindAllStorages() []StorageInterface {
}
return storages
}
// ScanGarbageCaches 清理目录中“失联”的缓存文件
func (this *Manager) ScanGarbageCaches(callback func(path string) error) error {
var storages = this.FindAllStorages()
for _, storage := range storages {
fileStorage, ok := storage.(*FileStorage)
if !ok {
continue
}
err := fileStorage.ScanGarbageCaches(callback)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,52 +0,0 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package caches
import (
"github.com/TeaOSLab/EdgeNode/internal/goman"
"sync/atomic"
"time"
)
const (
modeSlow int32 = 1
modeFast int32 = 2
)
// MaxOpenFiles max open files manager
type MaxOpenFiles struct {
ticker *time.Ticker
mode int32
}
func NewMaxOpenFiles() *MaxOpenFiles {
var f = &MaxOpenFiles{}
f.ticker = time.NewTicker(1 * time.Second)
f.init()
return f
}
func (this *MaxOpenFiles) init() {
goman.New(func() {
for range this.ticker.C {
// reset mode
atomic.StoreInt32(&this.mode, modeFast)
}
})
}
func (this *MaxOpenFiles) Fast() {
atomic.AddInt32(&this.mode, modeFast)
}
func (this *MaxOpenFiles) FinishAll() {
this.Fast()
}
func (this *MaxOpenFiles) Slow() {
atomic.StoreInt32(&this.mode, modeSlow)
}
func (this *MaxOpenFiles) Next() bool {
return atomic.LoadInt32(&this.mode) != modeSlow
}

View File

@@ -1,35 +0,0 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package caches_test
import (
"github.com/TeaOSLab/EdgeNode/internal/caches"
"testing"
"time"
)
func TestNewMaxOpenFiles(t *testing.T) {
var maxOpenFiles = caches.NewMaxOpenFiles()
maxOpenFiles.Fast()
t.Log("fast:", maxOpenFiles.Next())
maxOpenFiles.Slow()
t.Log("slow:", maxOpenFiles.Next())
time.Sleep(1*time.Second + 1*time.Millisecond)
t.Log("slow 1 second:", maxOpenFiles.Next())
maxOpenFiles.Slow()
t.Log("slow:", maxOpenFiles.Next())
maxOpenFiles.Slow()
t.Log("slow:", maxOpenFiles.Next())
time.Sleep(1 * time.Second)
t.Log("slow 1 second:", maxOpenFiles.Next())
maxOpenFiles.Slow()
t.Log("slow:", maxOpenFiles.Next())
maxOpenFiles.Fast()
t.Log("fast:", maxOpenFiles.Next())
}

View File

@@ -0,0 +1,374 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package caches
import (
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/iwind/TeaGo/logs"
"os"
"sync"
"sync/atomic"
"time"
)
const (
minMemoryFragmentPoolItemSize = 8 << 10
maxMemoryFragmentPoolItemSize = 128 << 20
maxItemsInMemoryFragmentPoolBucket = 1024
memoryFragmentPoolBucketSegmentSize = 512 << 10
maxMemoryFragmentPoolItemAgeSeconds = 60
)
var SharedFragmentMemoryPool *MemoryFragmentPool
func init() {
if !teaconst.IsMain {
return
}
SharedFragmentMemoryPool = NewMemoryFragmentPool()
goman.New(func() {
var ticker = time.NewTicker(200 * time.Millisecond)
for range ticker.C {
for i := 0; i < 10; i++ { // skip N empty buckets
var isEmpty = SharedFragmentMemoryPool.GCNextBucket()
if !isEmpty {
break
}
}
}
})
}
type MemoryFragmentPoolItem struct {
Bytes []byte
size int64
createdAt int64
Refs int32
}
func (this *MemoryFragmentPoolItem) IsExpired() bool {
return this.createdAt < fasttime.Now().Unix()-maxMemoryFragmentPoolItemAgeSeconds
}
func (this *MemoryFragmentPoolItem) Reset() {
this.Bytes = nil
}
func (this *MemoryFragmentPoolItem) IsAvailable() bool {
return atomic.AddInt32(&this.Refs, 1) == 1
}
// MemoryFragmentPool memory fragments management
type MemoryFragmentPool struct {
bucketMaps []map[uint64]*MemoryFragmentPoolItem // [ { id => Zero }, ... ]
countBuckets int
gcBucketIndex int
mu sync.RWMutex
id uint64
totalMemory int64
isOk bool
capacity int64
debugMode bool
countGet uint64
countNew uint64
}
// NewMemoryFragmentPool create new fragment memory pool
func NewMemoryFragmentPool() *MemoryFragmentPool {
var pool = &MemoryFragmentPool{}
pool.init()
return pool
}
func (this *MemoryFragmentPool) init() {
var capacity = int64(utils.SystemMemoryGB()) << 30 / 16
if capacity > 256<<20 {
this.isOk = true
this.capacity = capacity
this.bucketMaps = []map[uint64]*MemoryFragmentPoolItem{}
for i := 0; i < maxMemoryFragmentPoolItemSize/memoryFragmentPoolBucketSegmentSize+1; i++ {
this.bucketMaps = append(this.bucketMaps, map[uint64]*MemoryFragmentPoolItem{})
}
this.countBuckets = len(this.bucketMaps)
}
// print statistics for debug
if len(os.Getenv("GOEDGE_DEBUG_MEMORY_FRAGMENT_POOL")) > 0 {
this.debugMode = true
go func() {
var maxRounds = 10_000
var ticker = time.NewTicker(10 * time.Second)
for range ticker.C {
logs.Println("reused:", this.countGet, "created:", this.countNew, "fragments:", this.Len(), "memory:", this.totalMemory>>20, "MB")
maxRounds--
if maxRounds <= 0 {
break
}
}
}()
}
}
// Get try to get a bytes object
func (this *MemoryFragmentPool) Get(expectSize int64) (resultBytes []byte, ok bool) {
if !this.isOk {
return
}
if expectSize <= 0 {
return
}
// DO NOT check min segment size
this.mu.RLock()
var bucketIndex = this.bucketIndexForSize(expectSize)
var resultItemId uint64
const maxSearchingBuckets = 20
for i := bucketIndex; i <= bucketIndex+maxSearchingBuckets; i++ {
resultBytes, resultItemId, ok = this.findItemInMap(this.bucketMaps[i], expectSize)
if ok {
this.mu.RUnlock()
// remove from bucket
this.mu.Lock()
delete(this.bucketMaps[i], resultItemId)
this.mu.Unlock()
return
}
if i >= this.countBuckets {
break
}
}
this.mu.RUnlock()
return
}
// Put a bytes object to specified bucket
func (this *MemoryFragmentPool) Put(data []byte) (ok bool) {
if !this.isOk {
return
}
var l = int64(cap(data)) // MUST be 'cap' instead of 'len'
if l < minMemoryFragmentPoolItemSize || l > maxMemoryFragmentPoolItemSize {
return
}
if atomic.LoadInt64(&this.totalMemory) >= this.capacity {
return
}
var itemId = atomic.AddUint64(&this.id, 1)
this.mu.Lock()
defer this.mu.Unlock()
var bucketMap = this.bucketMaps[this.bucketIndexForSize(l)]
if len(bucketMap) >= maxItemsInMemoryFragmentPoolBucket {
return
}
atomic.AddInt64(&this.totalMemory, l)
bucketMap[itemId] = &MemoryFragmentPoolItem{
Bytes: data,
size: l,
createdAt: fasttime.Now().Unix(),
}
return true
}
// GC fully GC
func (this *MemoryFragmentPool) GC() {
if !this.isOk {
return
}
var totalMemory = atomic.LoadInt64(&this.totalMemory)
if totalMemory < this.capacity {
return
}
this.mu.Lock()
defer this.mu.Unlock()
var garbageSize = totalMemory * 1 / 10 // 10%
// remove expired
for _, bucketMap := range this.bucketMaps {
for itemId, item := range bucketMap {
if item.IsExpired() {
delete(bucketMap, itemId)
item.Reset()
atomic.AddInt64(&this.totalMemory, -item.size)
garbageSize -= item.size
}
}
}
// remove others
if garbageSize > 0 {
for _, bucketMap := range this.bucketMaps {
for itemId, item := range bucketMap {
delete(bucketMap, itemId)
item.Reset()
atomic.AddInt64(&this.totalMemory, -item.size)
garbageSize -= item.size
if garbageSize <= 0 {
break
}
}
}
}
}
// GCNextBucket gc one bucket
func (this *MemoryFragmentPool) GCNextBucket() (isEmpty bool) {
if !this.isOk {
return
}
var itemIds = []uint64{}
// find
this.mu.RLock()
var bucketIndex = this.gcBucketIndex
var bucketMap = this.bucketMaps[bucketIndex]
isEmpty = len(bucketMap) == 0
if isEmpty {
this.mu.RUnlock()
// move to next bucket index
bucketIndex++
if bucketIndex >= this.countBuckets {
bucketIndex = 0
}
this.gcBucketIndex = bucketIndex
return
}
for itemId, item := range bucketMap {
if item.IsExpired() {
itemIds = append(itemIds, itemId)
}
}
this.mu.RUnlock()
// remove
if len(itemIds) > 0 {
this.mu.Lock()
for _, itemId := range itemIds {
item, ok := bucketMap[itemId]
if !ok {
continue
}
if !item.IsAvailable() {
continue
}
delete(bucketMap, itemId)
item.Reset()
atomic.AddInt64(&this.totalMemory, -item.size)
}
this.mu.Unlock()
}
// move to next bucket index
bucketIndex++
if bucketIndex >= this.countBuckets {
bucketIndex = 0
}
this.gcBucketIndex = bucketIndex
return
}
func (this *MemoryFragmentPool) SetCapacity(capacity int64) {
this.capacity = capacity
}
func (this *MemoryFragmentPool) TotalSize() int64 {
return atomic.LoadInt64(&this.totalMemory)
}
func (this *MemoryFragmentPool) Len() int {
this.mu.Lock()
defer this.mu.Unlock()
var count = 0
for _, bucketMap := range this.bucketMaps {
count += len(bucketMap)
}
return count
}
func (this *MemoryFragmentPool) IncreaseNew() {
if this.isOk && this.debugMode {
atomic.AddUint64(&this.countNew, 1)
}
}
func (this *MemoryFragmentPool) bucketIndexForSize(size int64) int {
return int(size / memoryFragmentPoolBucketSegmentSize)
}
func (this *MemoryFragmentPool) findItemInMap(bucketMap map[uint64]*MemoryFragmentPoolItem, expectSize int64) (resultBytes []byte, resultItemId uint64, ok bool) {
if len(bucketMap) == 0 {
return
}
for itemId, item := range bucketMap {
if item.size >= expectSize {
// check if is referred
if !item.IsAvailable() {
continue
}
// return result
if item.size != expectSize {
resultBytes = item.Bytes[:expectSize]
} else {
resultBytes = item.Bytes
}
// reset old item
item.Reset()
atomic.AddInt64(&this.totalMemory, -item.size)
resultItemId = itemId
if this.debugMode {
atomic.AddUint64(&this.countGet, 1)
}
ok = true
return
}
}
return
}

View File

@@ -0,0 +1,313 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package caches_test
import (
"bytes"
"github.com/TeaOSLab/EdgeNode/internal/caches"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/rands"
timeutil "github.com/iwind/TeaGo/utils/time"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestNewMemoryFragmentPool(t *testing.T) {
var a = assert.NewAssertion(t)
var pool = caches.NewMemoryFragmentPool()
for i := 0; i < 3000; i++ {
ok := pool.Put(make([]byte, 2<<20))
if !ok {
t.Log("finished at", i)
break
}
}
t.Log(pool.TotalSize()>>20, "MB", pool.Len(), "items")
{
r, ok := pool.Get(1 << 20)
a.IsTrue(ok)
a.IsTrue(len(r) == 1<<20)
}
{
r, ok := pool.Get(2 << 20)
a.IsTrue(ok)
a.IsTrue(len(r) == 2<<20)
}
{
r, ok := pool.Get(4 << 20)
a.IsFalse(ok)
a.IsTrue(len(r) == 0)
}
t.Log(pool.TotalSize()>>20, "MB", pool.Len(), "items")
}
func TestNewMemoryFragmentPool_LargeBucket(t *testing.T) {
var a = assert.NewAssertion(t)
var pool = caches.NewMemoryFragmentPool()
{
pool.Put(make([]byte, 128<<20+1))
a.IsTrue(pool.Len() == 0)
}
{
pool.Put(make([]byte, 128<<20))
a.IsTrue(pool.Len() == 1)
pool.Get(118 << 20)
a.IsTrue(pool.Len() == 0)
}
{
pool.Put(make([]byte, 128<<20))
a.IsTrue(pool.Len() == 1)
pool.Get(110 << 20)
a.IsTrue(pool.Len() == 1)
}
}
func TestMemoryFragmentPool_Get_Exactly(t *testing.T) {
var a = assert.NewAssertion(t)
var pool = caches.NewMemoryFragmentPool()
{
pool.Put(make([]byte, 129<<20))
a.IsTrue(pool.Len() == 0)
}
{
pool.Put(make([]byte, 4<<20))
a.IsTrue(pool.Len() == 1)
}
{
pool.Get(4 << 20)
a.IsTrue(pool.Len() == 0)
}
}
func TestMemoryFragmentPool_Get_Round(t *testing.T) {
var a = assert.NewAssertion(t)
var pool = caches.NewMemoryFragmentPool()
{
pool.Put(make([]byte, 8<<20))
pool.Put(make([]byte, 8<<20))
pool.Put(make([]byte, 8<<20))
a.IsTrue(pool.Len() == 3)
}
{
resultBytes, ok := pool.Get(3 << 20)
a.IsTrue(pool.Len() == 2)
if ok {
pool.Put(resultBytes)
}
}
{
pool.Get(2 << 20)
a.IsTrue(pool.Len() == 2)
}
{
pool.Get(1 << 20)
a.IsTrue(pool.Len() == 1)
}
}
func TestMemoryFragmentPool_GC(t *testing.T) {
var pool = caches.NewMemoryFragmentPool()
pool.SetCapacity(32 << 20)
for i := 0; i < 16; i++ {
pool.Put(make([]byte, 4<<20))
}
var before = time.Now()
pool.GC()
t.Log(time.Since(before).Seconds()*1000, "ms")
t.Log(pool.Len())
}
func TestMemoryFragmentPool_Memory(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var pool = caches.NewMemoryFragmentPool()
testutils.StartMemoryStats(t, func() {
t.Log(pool.Len(), "items")
})
var sampleData = bytes.Repeat([]byte{'A'}, 16<<20)
var countNew = 0
for i := 0; i < 1000; i++ {
cacheData, ok := pool.Get(16 << 20)
if ok {
copy(cacheData, sampleData)
pool.Put(cacheData)
} else {
countNew++
var data = make([]byte, 16<<20)
copy(data, sampleData)
pool.Put(data)
}
}
t.Log("count new:", countNew)
t.Log("count remains:", pool.Len())
time.Sleep(10 * time.Minute)
}
func TestMemoryFragmentPool_GCNextBucket(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var pool = caches.NewMemoryFragmentPool()
for i := 0; i < 1000; i++ {
pool.Put(make([]byte, rands.Int(0, 100)<<20))
}
var lastLen int
for {
pool.GCNextBucket()
var currentLen = pool.Len()
if lastLen == currentLen {
continue
}
lastLen = currentLen
t.Log(currentLen, "items", pool.TotalSize(), "bytes", timeutil.Format("H:i:s"))
time.Sleep(100 * time.Millisecond)
if currentLen == 0 {
break
}
}
}
func TestMemoryFragmentPoolItem(t *testing.T) {
var a = assert.NewAssertion(t)
var m = map[int]*caches.MemoryFragmentPoolItem{}
m[1] = &caches.MemoryFragmentPoolItem{
Refs: 0,
}
var item = m[1]
a.IsTrue(item.Refs == 0)
a.IsTrue(atomic.AddInt32(&item.Refs, 1) == 1)
for _, item2 := range m {
t.Log(item2)
a.IsTrue(atomic.AddInt32(&item2.Refs, 1) == 2)
}
t.Log(m)
}
func BenchmarkMemoryFragmentPool_Get_HIT(b *testing.B) {
runtime.GOMAXPROCS(4)
var pool = caches.NewMemoryFragmentPool()
for i := 0; i < 3000; i++ {
ok := pool.Put(make([]byte, 2<<20))
if !ok {
break
}
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, ok := pool.Get(2 << 20)
if ok {
pool.Put(data)
}
}
})
}
func BenchmarkMemoryFragmentPool_Get_TOTALLY_MISSING(b *testing.B) {
runtime.GOMAXPROCS(4)
var pool = caches.NewMemoryFragmentPool()
for i := 0; i < 3000; i++ {
ok := pool.Put(make([]byte, 2<<20+100))
if !ok {
break
}
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, ok := pool.Get(2<<20 + 200)
if ok {
pool.Put(data)
}
}
})
}
func BenchmarkMemoryPool_Get_HIT_MISSING(b *testing.B) {
runtime.GOMAXPROCS(4)
var pool = caches.NewMemoryFragmentPool()
for i := 0; i < 3000; i++ {
ok := pool.Put(make([]byte, rands.Int(2, 32)<<20))
if !ok {
break
}
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, ok := pool.Get(4 << 20)
if ok {
pool.Put(data)
}
}
})
}
func BenchmarkMemoryFragmentPool_GC(b *testing.B) {
runtime.GOMAXPROCS(4)
var pool = caches.NewMemoryFragmentPool()
pool.SetCapacity(1 << 30)
for i := 0; i < 2_000; i++ {
pool.Put(make([]byte, 1<<20))
}
var mu = sync.Mutex{}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
for i := 0; i < 100; i++ {
pool.GCNextBucket()
}
mu.Unlock()
}
})
}

View File

@@ -12,14 +12,16 @@ type OpenFile struct {
meta []byte
header []byte
version int64
size int64
}
func NewOpenFile(fp *os.File, meta []byte, header []byte, version int64) *OpenFile {
func NewOpenFile(fp *os.File, meta []byte, header []byte, version int64, size int64) *OpenFile {
return &OpenFile{
fp: fp,
meta: meta,
header: header,
version: version,
size: size,
}
}

View File

@@ -3,7 +3,9 @@
package caches
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/linkedlist"
"github.com/fsnotify/fsnotify"
"github.com/iwind/TeaGo/logs"
@@ -14,26 +16,34 @@ import (
"time"
)
const (
maxOpenFileSize = 256 << 20
)
type OpenFileCache struct {
poolMap map[string]*OpenFilePool // file path => Pool
poolList *linkedlist.List
poolList *linkedlist.List[*OpenFilePool]
watcher *fsnotify.Watcher
locker sync.RWMutex
maxSize int
count int
maxCount int
capacitySize int64
count int
usedSize int64
}
func NewOpenFileCache(maxSize int) (*OpenFileCache, error) {
if maxSize <= 0 {
maxSize = 16384
func NewOpenFileCache(maxCount int) (*OpenFileCache, error) {
if maxCount <= 0 {
maxCount = 16384
}
var cache = &OpenFileCache{
maxSize: maxSize,
poolMap: map[string]*OpenFilePool{},
poolList: linkedlist.NewList(),
maxCount: maxCount,
poolMap: map[string]*OpenFilePool{},
poolList: linkedlist.NewList[*OpenFilePool](),
capacitySize: (int64(utils.SystemMemoryGB()) << 30) / 16,
}
watcher, err := fsnotify.NewWatcher()
@@ -58,24 +68,36 @@ func (this *OpenFileCache) Get(filename string) *OpenFile {
pool, ok := this.poolMap[filename]
this.locker.RUnlock()
if ok {
file, consumed := pool.Get()
file, consumed, consumedSize := pool.Get()
if consumed {
this.locker.Lock()
this.count--
this.usedSize -= consumedSize
// pool如果为空也不需要从列表中删除避免put时需要重新创建
this.locker.Unlock()
}
return file
}
return nil
}
func (this *OpenFileCache) Put(filename string, file *OpenFile) {
if file.size > maxOpenFileSize {
return
}
this.locker.Lock()
defer this.locker.Unlock()
// 如果超过当前容量,则关闭最早的
if this.count >= this.maxCount || this.usedSize+file.size >= this.capacitySize {
this.consumeHead()
return
}
pool, ok := this.poolMap[filename]
var success bool
if ok {
@@ -92,35 +114,7 @@ func (this *OpenFileCache) Put(filename string, file *OpenFile) {
// 检查长度
if success {
this.count++
// 如果超过当前容量,则关闭最早的
if this.count > this.maxSize {
var delta = this.maxSize / 100 // 清理1%
if delta == 0 {
delta = 1
}
for i := 0; i < delta; i++ {
var head = this.poolList.Head()
if head == nil {
break
}
var headPool = head.Value.(*OpenFilePool)
headFile, consumed := headPool.Get()
if consumed {
this.count--
if headFile != nil {
_ = headFile.Close()
}
}
if headPool.Len() == 0 {
delete(this.poolMap, headPool.filename)
this.poolList.Remove(head)
_ = this.watcher.Remove(headPool.filename)
}
}
}
this.usedSize += file.size
}
}
@@ -136,6 +130,7 @@ func (this *OpenFileCache) Close(filename string) {
this.poolList.Remove(pool.linkItem)
_ = this.watcher.Remove(filename)
this.count -= pool.Len()
this.usedSize -= pool.usedSize
}
this.locker.Unlock()
@@ -155,18 +150,55 @@ func (this *OpenFileCache) CloseAll() {
this.poolList.Reset()
_ = this.watcher.Close()
this.count = 0
this.usedSize = 0
this.locker.Unlock()
}
func (this *OpenFileCache) SetCapacity(capacityBytes int64) {
this.capacitySize = capacityBytes
}
func (this *OpenFileCache) Debug() {
var ticker = time.NewTicker(5 * time.Second)
goman.New(func() {
for range ticker.C {
logs.Println("==== " + types.String(this.count) + " ====")
this.poolList.Range(func(item *linkedlist.Item) (goNext bool) {
logs.Println(filepath.Base(item.Value.(*OpenFilePool).Filename()), item.Value.(*OpenFilePool).Len())
logs.Println("==== " + types.String(this.count) + ", " + fmt.Sprintf("%.4fMB", float64(this.usedSize)/(1<<20)) + " ====")
this.poolList.Range(func(item *linkedlist.Item[*OpenFilePool]) (goNext bool) {
logs.Println(filepath.Base(item.Value.Filename()), item.Value.Len())
return true
})
}
})
}
func (this *OpenFileCache) consumeHead() {
var delta = 1
if this.count > 100 {
delta = 2
}
for i := 0; i < delta; i++ {
var head = this.poolList.Head()
if head == nil {
break
}
var headPool = head.Value
headFile, consumed, consumedSize := headPool.Get()
if consumed {
this.count--
this.usedSize -= consumedSize
if headFile != nil {
_ = headFile.Close()
}
}
if headPool.Len() == 0 {
delete(this.poolMap, headPool.filename)
this.poolList.Remove(head)
_ = this.watcher.Remove(headPool.filename)
}
}
}

View File

@@ -5,6 +5,7 @@ package caches_test
import (
"github.com/TeaOSLab/EdgeNode/internal/caches"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/types"
"testing"
"time"
)
@@ -15,13 +16,14 @@ func TestNewOpenFileCache_Close(t *testing.T) {
t.Fatal(err)
}
cache.Debug()
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0, 1<<20))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0, 1<<20))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0, 1<<20))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0, 1<<20))
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0, 1<<20))
cache.Get("b.txt")
cache.Get("d.txt")
cache.Get("d.txt") // not exist
cache.Close("a.txt")
if testutils.IsSingleTesting() {
@@ -29,18 +31,39 @@ func TestNewOpenFileCache_Close(t *testing.T) {
}
}
func TestNewOpenFileCache_OverSize(t *testing.T) {
cache, err := caches.NewOpenFileCache(1024)
if err != nil {
t.Fatal(err)
}
cache.SetCapacity(1 << 30)
cache.Debug()
for i := 0; i < 100; i++ {
cache.Put("a"+types.String(i)+".txt", caches.NewOpenFile(nil, nil, nil, 0, 128<<20))
}
if testutils.IsSingleTesting() {
time.Sleep(100 * time.Second)
}
}
func TestNewOpenFileCache_CloseAll(t *testing.T) {
cache, err := caches.NewOpenFileCache(1024)
if err != nil {
t.Fatal(err)
}
cache.Debug()
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0))
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0, 1))
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0, 1))
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0, 1))
cache.Get("b.txt")
cache.Get("d.txt")
cache.CloseAll()
time.Sleep(6 * time.Second)
if testutils.IsSingleTesting() {
time.Sleep(6 * time.Second)
}
}

View File

@@ -9,10 +9,11 @@ import (
type OpenFilePool struct {
c chan *OpenFile
linkItem *linkedlist.Item
linkItem *linkedlist.Item[*OpenFilePool]
filename string
version int64
isClosed bool
usedSize int64
}
func NewOpenFilePool(filename string) *OpenFilePool {
@@ -21,7 +22,7 @@ func NewOpenFilePool(filename string) *OpenFilePool {
c: make(chan *OpenFile, 1024),
version: fasttime.Now().UnixMilli(),
}
pool.linkItem = linkedlist.NewItem(pool)
pool.linkItem = linkedlist.NewItem[*OpenFilePool](pool)
return pool
}
@@ -29,27 +30,29 @@ func (this *OpenFilePool) Filename() string {
return this.filename
}
func (this *OpenFilePool) Get() (*OpenFile, bool) {
func (this *OpenFilePool) Get() (resultFile *OpenFile, consumed bool, consumedSize int64) {
// 如果已经关闭,直接返回
if this.isClosed {
return nil, false
return nil, false, 0
}
select {
case file := <-this.c:
if file != nil {
this.usedSize -= file.size
err := file.SeekStart()
if err != nil {
_ = file.Close()
return nil, true
return nil, true, file.size
}
file.version = this.version
return file, true
return file, true, file.size
}
return nil, false
return nil, false, 0
default:
return nil, false
return nil, false, 0
}
}
@@ -69,6 +72,7 @@ func (this *OpenFilePool) Put(file *OpenFile) bool {
// 加入Pool
select {
case this.c <- file:
this.usedSize += file.size
return true
default:
// 多余的直接关闭
@@ -81,6 +85,10 @@ func (this *OpenFilePool) Len() int {
return len(this.c)
}
func (this *OpenFilePool) TotalSize() int64 {
return this.usedSize
}
func (this *OpenFilePool) SetClosing() {
this.isClosed = true
}

View File

@@ -13,15 +13,15 @@ func TestOpenFilePool_Get(t *testing.T) {
var pool = caches.NewOpenFilePool("a")
t.Log(pool.Filename())
t.Log(pool.Get())
t.Log(pool.Put(caches.NewOpenFile(nil, nil, nil, 0)))
t.Log(pool.Put(caches.NewOpenFile(nil, nil, nil, 0, 1)))
t.Log(pool.Get())
t.Log(pool.Get())
}
func TestOpenFilePool_Close(t *testing.T) {
var pool = caches.NewOpenFilePool("a")
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
pool.Put(caches.NewOpenFile(nil, nil, nil, 0, 1))
pool.Put(caches.NewOpenFile(nil, nil, nil, 0, 1))
pool.Close()
}
@@ -35,7 +35,7 @@ func TestOpenFilePool_Concurrent(t *testing.T) {
defer wg.Done()
if rands.Int(0, 1) == 1 {
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
pool.Put(caches.NewOpenFile(nil, nil, nil, 0, 1))
}
if rands.Int(0, 1) == 0 {
pool.Get()

View File

@@ -366,7 +366,7 @@ func (this *FileReader) Close() error {
} else {
var cacheMeta = make([]byte, len(this.meta))
copy(cacheMeta, this.meta)
this.openFileCache.Put(this.fp.Name(), NewOpenFile(this.fp, cacheMeta, this.header, this.LastModified()))
this.openFileCache.Put(this.fp.Name(), NewOpenFile(this.fp, cacheMeta, this.header, this.LastModified(), this.bodySize))
}
return nil
}

View File

@@ -3,6 +3,7 @@ package caches
import (
"encoding/binary"
"errors"
"fmt"
rangeutils "github.com/TeaOSLab/EdgeNode/internal/utils/ranges"
"github.com/iwind/TeaGo/types"
"io"
@@ -19,7 +20,7 @@ type PartialFileReader struct {
func NewPartialFileReader(fp *os.File) *PartialFileReader {
return &PartialFileReader{
FileReader: NewFileReader(fp),
rangePath: partialRangesFilePath(fp.Name()),
rangePath: PartialRangesFilePath(fp.Name()),
}
}
@@ -46,7 +47,7 @@ func (this *PartialFileReader) InitAutoDiscard(autoDiscard bool) error {
// 读取Range
ranges, err := NewPartialRangesFromFile(this.rangePath)
if err != nil {
return errors.New("read ranges failed: " + err.Error())
return fmt.Errorf("read ranges failed: %w", err)
}
this.ranges = ranges

View File

@@ -14,6 +14,7 @@ import (
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/trackers"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
setutils "github.com/TeaOSLab/EdgeNode/internal/utils/sets"
"github.com/TeaOSLab/EdgeNode/internal/utils/sizes"
@@ -22,6 +23,9 @@ import (
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time"
"github.com/iwind/gosock/pkg/gosock"
"github.com/shirou/gopsutil/v3/load"
"math"
"os"
"path/filepath"
@@ -55,17 +59,14 @@ const (
HotItemLifeSeconds int64 = 3600 // 热点数据生命周期
FileToMemoryMaxSize = 32 * sizes.M // 可以从文件写入到内存的最大文件尺寸
FileTmpSuffix = ".tmp"
MinDiskSpace uint64 = 5 << 30 // 当前磁盘最小剩余空间
DefaultMinDiskFreeSpace uint64 = 5 << 30 // 当前磁盘最小剩余空间
DefaultStaleCacheSeconds = 1200 // 过时缓存留存时间
HashKeyLength = 32
)
var sharedWritingFileKeyMap = map[string]zero.Zero{} // key => bool
var sharedWritingFileKeyLocker = sync.Mutex{}
var maxOpenFiles = NewMaxOpenFiles()
const maxOpenFilesSlowCost = 1000 * time.Microsecond // us
const protectingLoadWhenDump = false
// FileStorage 文件缓存
//
// 文件结构:
@@ -88,7 +89,8 @@ type FileStorage struct {
openFileCache *OpenFileCache
mainDiskIsFull bool
mainDiskIsFull bool
mainDiskTotalSize uint64
subDirs []*FileDir
}
@@ -162,6 +164,7 @@ func (this *FileStorage) UpdatePolicy(newPolicy *serverconfigs.HTTPCachePolicy)
IsFull: false,
})
}
this.subDirs = subDirs
this.checkDiskSpace()
err = newOptions.Init()
@@ -198,6 +201,9 @@ func (this *FileStorage) UpdatePolicy(newPolicy *serverconfigs.HTTPCachePolicy)
if newPolicy.PersistenceAutoPurgeInterval != this.policy.PersistenceAutoPurgeInterval {
this.initPurgeTicker()
}
// reset ignored keys
this.ignoreKeys.Reset()
}
// Init 初始化
@@ -259,7 +265,7 @@ func (this *FileStorage) Init() error {
} else {
err = os.MkdirAll(dir, 0777)
if err != nil {
return errors.New("[CACHE]can not create dir:" + err.Error())
return fmt.Errorf("[CACHE]can not create dir: %w", err)
}
}
}
@@ -299,6 +305,9 @@ func (this *FileStorage) Init() error {
// 检查磁盘空间
this.checkDiskSpace()
// clean *.trash directories
this.cleanAllDeletedDirs()
return nil
}
@@ -410,25 +419,32 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
}
// 是否已忽略
if this.ignoreKeys.Has(key) {
if maxSize > 0 && this.ignoreKeys.Has(types.String(maxSize)+"$"+key) {
return nil, ErrEntityTooLarge
}
// 检查磁盘是否超出容量
// 需要在内存缓存之前执行,避免成功写进到了内存缓存,但无法刷到磁盘
var capacityBytes = this.diskCapacityBytes()
if capacityBytes > 0 && capacityBytes <= this.TotalDiskSize()+(32<<20 /** 余量 **/) {
return nil, NewCapacityError("write file cache failed: over disk size, current: " + types.String(this.TotalDiskSize()) + ", capacity: " + types.String(capacityBytes))
}
// 先尝试内存缓存
// 我们限定仅小文件优先存在内存中
var maxMemorySize = FileToMemoryMaxSize
if maxSize > maxMemorySize {
if maxSize > 0 && maxSize < maxMemorySize {
maxMemorySize = maxSize
}
var memoryStorage = this.memoryStorage
if !isFlushing && !isPartial && memoryStorage != nil && ((bodySize > 0 && bodySize < maxMemorySize) || bodySize < 0) {
if !fsutils.DiskIsExtremelyFast() && !isFlushing && !isPartial && memoryStorage != nil && ((bodySize > 0 && bodySize < maxMemorySize) || bodySize < 0) {
writer, err := memoryStorage.OpenWriter(key, expiredAt, status, headerSize, bodySize, maxMemorySize, false)
if err == nil {
return writer, nil
}
// 如果队列满了,则等待
if err == ErrWritingQueueFull {
if errors.Is(err, ErrWritingQueueFull) {
return nil, err
}
}
@@ -442,9 +458,9 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
return nil, ErrFileIsWriting
}
if !isFlushing && !maxOpenFiles.Next() {
if !isFlushing && !fsutils.WriteReady() {
sharedWritingFileKeyLocker.Unlock()
return nil, ErrTooManyOpenFiles
return nil, ErrServerIsBusy
}
sharedWritingFileKeyMap[key] = zero.New()
@@ -453,19 +469,10 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
if !isOk {
sharedWritingFileKeyLocker.Lock()
delete(sharedWritingFileKeyMap, key)
if len(sharedWritingFileKeyMap) == 0 {
maxOpenFiles.FinishAll()
}
sharedWritingFileKeyLocker.Unlock()
}
}()
// 检查是否超出容量
var capacityBytes = this.diskCapacityBytes()
if capacityBytes > 0 && capacityBytes <= this.TotalDiskSize() {
return nil, NewCapacityError("write file cache failed: over disk size, current total size: " + types.String(this.TotalDiskSize()) + " bytes, capacity: " + types.String(capacityBytes))
}
var hash = stringutil.Md5(key)
dir, diskIsFull := this.subDir(hash)
@@ -487,7 +494,7 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
stat, err := os.Stat(cachePath)
// 检查两次写入缓存的时间是否过于相近,分片内容不受此限制
if err == nil && !isPartial && time.Now().Sub(stat.ModTime()) <= 1*time.Second {
if err == nil && !isPartial && time.Since(stat.ModTime()) <= 1*time.Second {
// 防止并发连续写入
return nil, ErrFileIsWriting
}
@@ -555,8 +562,9 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
if isNewCreated && existsFile {
flags |= os.O_TRUNC
}
var before = time.Now()
fsutils.WriteBegin()
writer, err := os.OpenFile(tmpPath, flags, 0666)
fsutils.WriteEnd()
if err != nil {
// TODO 检查在各个系统中的稳定性
if os.IsNotExist(err) {
@@ -569,13 +577,6 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
return nil, err
}
}
if !isFlushing {
if time.Since(before) >= maxOpenFilesSlowCost {
maxOpenFiles.Slow()
} else {
maxOpenFiles.Fast()
}
}
var removeOnFailure = true
defer func() {
@@ -625,7 +626,9 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
metaBodySize = bodySize
}
fsutils.WriteBegin()
_, err = writer.Write(metaBytes)
fsutils.WriteEnd()
if err != nil {
return nil, err
}
@@ -636,18 +639,12 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, hea
return NewPartialFileWriter(writer, key, expiredAt, metaHeaderSize, metaBodySize, isNewCreated, isPartial, partialBodyOffset, partialRanges, func() {
sharedWritingFileKeyLocker.Lock()
delete(sharedWritingFileKeyMap, key)
if len(sharedWritingFileKeyMap) == 0 {
maxOpenFiles.FinishAll()
}
sharedWritingFileKeyLocker.Unlock()
}), nil
} else {
return NewFileWriter(this, writer, key, expiredAt, metaHeaderSize, metaBodySize, -1, func() {
return NewFileWriter(this, writer, key, expiredAt, metaHeaderSize, metaBodySize, maxSize, func() {
sharedWritingFileKeyLocker.Lock()
delete(sharedWritingFileKeyMap, key)
if len(sharedWritingFileKeyMap) == 0 {
maxOpenFiles.FinishAll()
}
sharedWritingFileKeyLocker.Unlock()
}), nil
}
@@ -761,7 +758,7 @@ func (this *FileStorage) CleanAll() error {
return err
}
for _, info := range subDirs {
subDir := info.Name()
var subDir = info.Name()
// 检查目录名
if !dirNameReg.MatchString(subDir) {
@@ -769,7 +766,7 @@ func (this *FileStorage) CleanAll() error {
}
// 修改目录名
tmpDir := dir + "/" + subDir + "-deleted"
var tmpDir = dir + "/" + subDir + "." + timeutil.Format("YmdHis") + ".trash"
err = os.Rename(dir+"/"+subDir, tmpDir)
if err != nil {
return err
@@ -780,7 +777,11 @@ func (this *FileStorage) CleanAll() error {
goman.New(func() {
err = this.cleanDeletedDirs(dir)
if err != nil {
remotelogs.Warn("CACHE", "delete '*-deleted' dirs failed: "+err.Error())
remotelogs.Warn("CACHE", "delete '*.trash' dirs failed: "+err.Error())
} else {
// try to clean again, to delete writing files when deleting
time.Sleep(10 * time.Minute)
_ = this.cleanDeletedDirs(dir)
}
})
@@ -897,7 +898,7 @@ func (this *FileStorage) Stop() {
// TotalDiskSize 消耗的磁盘尺寸
func (this *FileStorage) TotalDiskSize() int64 {
stat, err := fsutils.StatCache(this.options.Dir)
stat, err := fsutils.StatDeviceCache(this.options.Dir)
if err == nil {
return int64(stat.UsedSize())
}
@@ -914,8 +915,8 @@ func (this *FileStorage) TotalMemorySize() int64 {
}
// IgnoreKey 忽略某个Key即不缓存某个Key
func (this *FileStorage) IgnoreKey(key string) {
this.ignoreKeys.Push(key)
func (this *FileStorage) IgnoreKey(key string, maxSize int64) {
this.ignoreKeys.Push(types.String(maxSize) + "$" + key)
}
// CanSendfile 是否支持Sendfile
@@ -937,7 +938,7 @@ func (this *FileStorage) keyPath(key string) (hash string, path string, diskIsFu
// 获取Hash对应的文件路径
func (this *FileStorage) hashPath(hash string) (path string, diskIsFull bool) {
if len(hash) != 32 {
if len(hash) != HashKeyLength {
return "", false
}
var dir string
@@ -1001,28 +1002,31 @@ func (this *FileStorage) initList() error {
// 清理任务
// TODO purge每个分区
func (this *FileStorage) purgeLoop() {
// 检查磁盘剩余空间
this.checkDiskSpace()
// load
systemLoad, _ := load.Avg()
// TODO 计算平均最近每日新增用量
// 计算是否应该开启LFU清理
var capacityBytes = this.diskCapacityBytes()
var startLFU = false
var requireFullLFU = false // 是否需要完整执行LFU
var lfuFreePercent = this.policy.PersistenceLFUFreePercent
if lfuFreePercent <= 0 {
lfuFreePercent = 5
}
var hasFullDisk = this.mainDiskIsFull
if !hasFullDisk {
var subDirs = this.subDirs // copy slice
for _, subDir := range subDirs {
if subDir.IsFull {
hasFullDisk = true
break
if systemLoad == nil || systemLoad.Load5 > 10 {
// 2TB级别以上
if capacityBytes>>30 > 2000 {
lfuFreePercent = 100 /** GB **/ / float32(capacityBytes>>30) * 100 /** % **/
if lfuFreePercent > 3 {
lfuFreePercent = 3
}
}
}
}
var hasFullDisk = this.hasFullDisk()
if hasFullDisk {
startLFU = true
} else {
@@ -1041,23 +1045,45 @@ func (this *FileStorage) purgeLoop() {
var times = 1
// 空闲时间多清理
if utils.SharedFreeHoursManager.IsFreeHour() {
times = 5
if systemLoad != nil {
if systemLoad.Load5 < 3 {
times = 5
} else if systemLoad.Load5 < 5 {
times = 3
} else if systemLoad.Load5 < 10 {
times = 2
}
}
// 高速硬盘多清理
if fsutils.DiskIsExtremelyFast() {
times *= 8
} else if fsutils.DiskIsFast() {
times *= 4
}
// 处于LFU阈值时多清理
if startLFU {
times = 5
times *= 5
}
var purgeCount = this.policy.PersistenceAutoPurgeCount
if purgeCount <= 0 {
purgeCount = 1000
if fsutils.DiskIsExtremelyFast() {
purgeCount = 4000
} else if fsutils.DiskIsFast() {
purgeCount = 2000
}
}
for i := 0; i < times; i++ {
countFound, err := this.list.Purge(purgeCount, func(hash string) error {
path, _ := this.hashPath(hash)
fsutils.WriteBegin()
err := this.removeCacheFile(path)
fsutils.WriteEnd()
if err != nil && !os.IsNotExist(err) {
remotelogs.Error("CACHE", "purge '"+path+"' error: "+err.Error())
}
@@ -1070,6 +1096,10 @@ func (this *FileStorage) purgeLoop() {
}
if countFound < purgeCount {
if i == 0 && startLFU {
requireFullLFU = true
}
break
}
@@ -1079,28 +1109,61 @@ func (this *FileStorage) purgeLoop() {
// 磁盘空间不足时,清除老旧的缓存
if startLFU {
var maxCount = 1000
var maxLoops = 5
if fsutils.DiskIsExtremelyFast() {
maxCount = 4000
} else if fsutils.DiskIsFast() {
maxCount = 2000
}
var total, _ = this.list.Count()
if total > 0 {
var count = types.Int(math.Ceil(float64(total) * float64(lfuFreePercent*2) / 100))
if count > 0 {
// 限制单次清理的条数,防止占用太多系统资源
if count > 2000 {
count = 2000
for {
maxLoops--
if maxLoops <= 0 {
break
}
remotelogs.Println("CACHE", "LFU purge policy '"+this.policy.Name+"' id: "+types.String(this.policy.Id)+", count: "+types.String(count))
// 开始清理
var count = types.Int(math.Ceil(float64(total) * float64(lfuFreePercent*2) / 100))
if count <= 0 {
break
}
// 限制单次清理的条数,防止占用太多系统资源
if count > maxCount {
count = maxCount
}
var before = time.Now()
err := this.list.PurgeLFU(count, func(hash string) error {
path, _ := this.hashPath(hash)
fsutils.WriteBegin()
err := this.removeCacheFile(path)
fsutils.WriteEnd()
if err != nil && !os.IsNotExist(err) {
remotelogs.Error("CACHE", "purge '"+path+"' error: "+err.Error())
}
return nil
})
var prefix = ""
if requireFullLFU {
prefix = "fully "
}
remotelogs.Println("CACHE", prefix+"LFU purge policy '"+this.policy.Name+"' id: "+types.String(this.policy.Id)+", count: "+types.String(count)+", cost: "+fmt.Sprintf("%.2fms", time.Since(before).Seconds()*1000))
if err != nil {
remotelogs.Warn("CACHE", "purge file storage in LFU failed: "+err.Error())
}
// 检查硬盘空间状态
if !requireFullLFU && !this.hasFullDisk() {
break
}
}
}
}
@@ -1108,11 +1171,16 @@ func (this *FileStorage) purgeLoop() {
// 热点数据任务
func (this *FileStorage) hotLoop() {
var memoryStorage = this.memoryStorage
var memoryStorage = this.memoryStorage // copy
if memoryStorage == nil {
return
}
// check memory space size
if !memoryStorage.HasFreeSpaceForHotItems() {
return
}
this.hotMapLocker.Lock()
if len(this.hotMap) == 0 {
this.hotMapLocker.Unlock()
@@ -1124,6 +1192,9 @@ func (this *FileStorage) hotLoop() {
var result = []*HotItem{} // [ {key: ..., hits: ...}, ... ]
for _, v := range this.hotMap {
if v.Hits <= 1 {
continue
}
result = append(result, v)
}
@@ -1227,16 +1298,43 @@ func (this *FileStorage) hotLoop() {
func (this *FileStorage) diskCapacityBytes() int64 {
var c1 = this.policy.CapacityBytes()
if SharedManager.MaxDiskCapacity != nil {
var c2 = SharedManager.MaxDiskCapacity.Bytes()
var nodeCapacity = SharedManager.MaxDiskCapacity // copy
if nodeCapacity != nil {
var c2 = nodeCapacity.Bytes()
if c2 > 0 {
if this.mainDiskTotalSize > 0 && c2 >= int64(this.mainDiskTotalSize) {
c2 = int64(this.mainDiskTotalSize) * 95 / 100 // keep 5% free
}
return c2
}
}
if c1 <= 0 || (this.mainDiskTotalSize > 0 && c1 >= int64(this.mainDiskTotalSize)) {
c1 = int64(this.mainDiskTotalSize) * 95 / 100 // keep 5% free
}
return c1
}
// 清理 *-deleted 目录
// remove all *.trash directories under policy directory
func (this *FileStorage) cleanAllDeletedDirs() {
var rootDirs = []string{this.options.Dir}
var subDirs = this.subDirs // copy slice
if len(subDirs) > 0 {
for _, subDir := range subDirs {
rootDirs = append(rootDirs, subDir.Path)
}
}
for _, rootDir := range rootDirs {
var dir = rootDir + "/p" + types.String(this.policy.Id)
goman.New(func() {
_ = this.cleanDeletedDirs(dir)
})
}
}
// 清理 *.trash 目录
// 由于在很多硬盘上耗时非常久,所以应该放在后台运行
func (this *FileStorage) cleanDeletedDirs(dir string) error {
fp, err := os.Open(dir)
@@ -1251,8 +1349,8 @@ func (this *FileStorage) cleanDeletedDirs(dir string) error {
return err
}
for _, info := range subDirs {
subDir := info.Name()
if !strings.HasSuffix(subDir, "-deleted") {
var subDir = info.Name()
if !strings.HasSuffix(subDir, ".trash") {
continue
}
@@ -1273,32 +1371,15 @@ func (this *FileStorage) increaseHit(key string, hash string, reader Reader) {
if rate <= 0 {
rate = 1000
}
if this.lastHotSize == 0 {
// 自动降低采样率来增加热点数据的缓存几率
rate = rate / 10
}
if rands.Int(0, rate) == 0 {
var memoryStorage = this.memoryStorage
var hitErr = this.list.IncreaseHit(hash)
if hitErr != nil {
// 此错误可以忽略
remotelogs.Error("CACHE", "increase hit failed: "+hitErr.Error())
}
// 增加到热点
// 这里不收录缓存尺寸过大的文件
if memoryStorage != nil && reader.BodySize() > 0 && reader.BodySize() < 128*sizes.M {
this.hotMapLocker.Lock()
hotItem, ok := this.hotMap[key]
// 限制热点数据存活时间
var unixTime = time.Now().Unix()
var expiresAt = reader.ExpiresAt()
if expiresAt <= 0 || expiresAt > unixTime+HotItemLifeSeconds {
expiresAt = unixTime + HotItemLifeSeconds
}
if ok {
hotItem.Hits++
} else if len(this.hotMap) < HotItemSize { // 控制数量
@@ -1308,6 +1389,15 @@ func (this *FileStorage) increaseHit(key string, hash string, reader Reader) {
}
}
this.hotMapLocker.Unlock()
// 只有重复点击的才增加点击量
if ok {
var hitErr = this.list.IncreaseHit(hash)
if hitErr != nil {
// 此错误可以忽略
remotelogs.Error("CACHE", "increase hit failed: "+hitErr.Error())
}
}
}
}
}
@@ -1324,11 +1414,15 @@ func (this *FileStorage) removeCacheFile(path string) error {
err = nil
// 删除Partial相关
var partialPath = partialRangesFilePath(path)
var partialPath = PartialRangesFilePath(path)
if openFileCache != nil {
openFileCache.Close(partialPath)
}
_ = os.Remove(partialPath)
_, statErr := os.Stat(partialPath)
if statErr == nil {
_ = os.Remove(partialPath)
}
}
return err
}
@@ -1407,7 +1501,7 @@ func (this *FileStorage) initOpenFileCache() {
}
func (this *FileStorage) runMemoryStorageSafety(f func(memoryStorage *MemoryStorage)) {
var memoryStorage = this.memoryStorage
var memoryStorage = this.memoryStorage // copy
if memoryStorage != nil {
f(memoryStorage)
}
@@ -1415,21 +1509,64 @@ func (this *FileStorage) runMemoryStorageSafety(f func(memoryStorage *MemoryStor
// 检查磁盘剩余空间
func (this *FileStorage) checkDiskSpace() {
if this.options != nil && len(this.options.Dir) > 0 {
stat, err := fsutils.Stat(this.options.Dir)
var minFreeSize = DefaultMinDiskFreeSpace
var options = this.options // copy
if options != nil && options.MinFreeSize != nil && options.MinFreeSize.Bytes() > 0 {
minFreeSize = uint64(options.MinFreeSize.Bytes())
}
if options != nil && len(options.Dir) > 0 {
stat, err := fsutils.StatDevice(options.Dir)
if err == nil {
this.mainDiskIsFull = stat.FreeSize() < MinDiskSpace
this.mainDiskIsFull = stat.FreeSize() < minFreeSize
this.mainDiskTotalSize = stat.TotalSize()
// check capacity (only on main directory) when node capacity had not been set
if !this.mainDiskIsFull {
var capacityBytes int64
var maxDiskCapacity = SharedManager.MaxDiskCapacity // copy
if maxDiskCapacity != nil && maxDiskCapacity.Bytes() > 0 {
capacityBytes = SharedManager.MaxDiskCapacity.Bytes()
} else {
var policy = this.policy // copy
if policy != nil {
capacityBytes = policy.CapacityBytes() // copy
}
}
if capacityBytes > 0 && stat.UsedSize() >= uint64(capacityBytes) {
this.mainDiskIsFull = true
}
}
}
}
var subDirs = this.subDirs // copy slice
for _, subDir := range subDirs {
stat, err := fsutils.Stat(subDir.Path)
stat, err := fsutils.StatDevice(subDir.Path)
if err == nil {
subDir.IsFull = stat.FreeSize() < MinDiskSpace
subDir.IsFull = stat.FreeSize() < minFreeSize
}
}
}
// 检查是否有已满的磁盘分区
func (this *FileStorage) hasFullDisk() bool {
this.checkDiskSpace()
var hasFullDisk = this.mainDiskIsFull
if !hasFullDisk {
var subDirs = this.subDirs // copy slice
for _, subDir := range subDirs {
if subDir.IsFull {
hasFullDisk = true
break
}
}
}
return hasFullDisk
}
// 获取目录
func (this *FileStorage) subDir(hash string) (dirPath string, dirIsFull bool) {
var suffix = "/p" + types.String(this.policy.Id) + "/" + hash[:2] + "/" + hash[2:4]
@@ -1459,6 +1596,121 @@ func (this *FileStorage) subDir(hash string) (dirPath string, dirIsFull bool) {
return subDir.Path + suffix, subDir.IsFull
}
// ScanGarbageCaches 清理目录中“失联”的缓存文件
// “失联”为不在HashMap中的文件
func (this *FileStorage) ScanGarbageCaches(fileCallback func(path string) error) error {
if !this.list.(*FileList).HashMapIsLoaded() {
return errors.New("cache list is loading")
}
var mainDir = this.options.Dir
var allDirs = []string{mainDir}
var subDirs = this.subDirs // copy
for _, subDir := range subDirs {
allDirs = append(allDirs, subDir.Path)
}
var countDirs = 0
// process progress
var progressSock = gosock.NewTmpSock(teaconst.CacheGarbageSockName)
_, sockErr := progressSock.SendTimeout(&gosock.Command{Code: "progress", Params: map[string]any{"progress": 0}}, 1*time.Second)
var canReportProgress = sockErr == nil
var lastProgress float64
var countFound = 0
for _, subDir := range allDirs {
var dir0 = subDir + "/p" + types.String(this.policy.Id)
dir1Matches, err := filepath.Glob(dir0 + "/*")
if err != nil {
// ignore error
continue
}
for _, dir1 := range dir1Matches {
if len(filepath.Base(dir1)) != 2 {
continue
}
dir2Matches, err := filepath.Glob(dir1 + "/*")
if err != nil {
// ignore error
continue
}
for _, dir2 := range dir2Matches {
if len(filepath.Base(dir2)) != 2 {
continue
}
countDirs++
// report progress
if canReportProgress {
var progress = float64(countDirs) / 65536
if fmt.Sprintf("%.2f", lastProgress) != fmt.Sprintf("%.2f", progress) {
lastProgress = progress
_, _ = progressSock.SendTimeout(&gosock.Command{Code: "progress", Params: map[string]any{
"progress": progress,
"count": countFound,
}}, 100*time.Millisecond)
}
}
fileMatches, err := filepath.Glob(dir2 + "/*.cache")
if err != nil {
// ignore error
continue
}
for _, file := range fileMatches {
var filename = filepath.Base(file)
var hash = strings.TrimSuffix(filename, ".cache")
if len(hash) != HashKeyLength {
continue
}
isReady, found := this.list.(*FileList).ExistQuick(hash)
if !isReady {
continue
}
if found {
continue
}
// 检查文件正在被写入
stat, err := os.Stat(file)
if err != nil {
continue
}
if fasttime.Now().Unix()-stat.ModTime().Unix() < 300 /** 5 minutes **/ {
continue
}
if fileCallback != nil {
countFound++
err = fileCallback(file)
if err != nil {
return err
}
}
}
}
}
}
// 100% progress
if canReportProgress && lastProgress != 1 {
_, _ = progressSock.SendTimeout(&gosock.Command{Code: "progress", Params: map[string]any{
"progress": 1,
"count": countFound,
}}, 100*time.Millisecond)
}
return nil
}
// 计算字节数字代号
func (this *FileStorage) charCode(r byte) uint8 {
if r >= '0' && r <= '9' {
return r - '0'

View File

@@ -5,6 +5,7 @@ import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/logs"
@@ -42,7 +43,9 @@ func TestFileStorage_Init(t *testing.T) {
time.Sleep(2 * time.Second)
storage.purgeLoop()
t.Log(storage.list.(*FileList).total, "entries left")
t.Log(storage.list.(*FileList).Stat(func(hash string) bool {
return true
}))
}
func TestFileStorage_OpenWriter(t *testing.T) {
@@ -157,7 +160,7 @@ func TestFileStorage_OpenWriter_HTTP(t *testing.T) {
t.Log(writer)
resp := &http.Response{
StatusCode: 200,
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"text/html; charset=utf-8"},
"Last-Modified": []string{"Wed, 06 Jan 2021 10:03:29 GMT"},
@@ -575,6 +578,29 @@ func TestFileStorage_RemoveCacheFile(t *testing.T) {
t.Log(storage.removeCacheFile("/Users/WorkSpace/EdgeProject/EdgeCache/p43/15/7e/157eba0dfc6dfb6fbbf20b1f9e584674.cache"))
}
func TestFileStorage_ScanGarbageCaches(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var storage = NewFileStorage(&serverconfigs.HTTPCachePolicy{
Id: 43,
Options: map[string]any{"dir": "/Users/WorkSpace/EdgeProject/EdgeCache"},
})
err := storage.Init()
if err != nil {
t.Fatal(err)
}
err = storage.ScanGarbageCaches(func(path string) error {
t.Log(path, PartialRangesFilePath(path))
return nil
})
if err != nil {
t.Fatal(err)
}
}
func BenchmarkFileStorage_Read(b *testing.B) {
runtime.GOMAXPROCS(1)

View File

@@ -54,7 +54,7 @@ type StorageInterface interface {
AddToList(item *Item)
// IgnoreKey 忽略某个Key即不缓存某个Key
IgnoreKey(key string)
IgnoreKey(key string, maxSize int64)
// CanSendfile 是否支持Sendfile
CanSendfile() bool

View File

@@ -7,13 +7,11 @@ import (
"github.com/TeaOSLab/EdgeNode/internal/trackers"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
setutils "github.com/TeaOSLab/EdgeNode/internal/utils/sets"
"github.com/TeaOSLab/EdgeNode/internal/utils/sizes"
"github.com/TeaOSLab/EdgeNode/internal/zero"
"github.com/cespare/xxhash"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
"github.com/shirou/gopsutil/v3/load"
"math"
"runtime"
"strconv"
@@ -30,6 +28,11 @@ type MemoryItem struct {
Status int
IsDone bool
ModifiedAt int64
IsPrepared bool
WriteOffset int64
isReferring bool // if it is referring by other objects
}
func (this *MemoryItem) IsExpired() bool {
@@ -50,7 +53,7 @@ type MemoryStorage struct {
purgeTicker *utils.Ticker
totalSize int64
usedSize int64
writingKeyMap map[string]zero.Zero // key => bool
ignoreKeys *setutils.FixedSet
@@ -62,7 +65,7 @@ func NewMemoryStorage(policy *serverconfigs.HTTPCachePolicy, parentStorage Stora
if parentStorage != nil {
if queueSize <= 0 {
queueSize = 2048 + int(policy.CapacityBytes()/sizes.G)*2048
queueSize = utils.SystemMemoryGB() * 100_000
}
dirtyChan = make(chan string, queueSize)
@@ -85,10 +88,10 @@ func (this *MemoryStorage) Init() error {
_ = this.list.Init()
this.list.OnAdd(func(item *Item) {
atomic.AddInt64(&this.totalSize, item.TotalSize())
atomic.AddInt64(&this.usedSize, item.TotalSize())
})
this.list.OnRemove(func(item *Item) {
atomic.AddInt64(&this.totalSize, -item.TotalSize())
atomic.AddInt64(&this.usedSize, -item.TotalSize())
})
this.initPurgeTicker()
@@ -121,7 +124,12 @@ func (this *MemoryStorage) OpenReader(key string, useStale bool, isPartial bool)
// read from valuesMap
this.locker.RLock()
item := this.valuesMap[hash]
var item = this.valuesMap[hash]
if item != nil {
item.isReferring = true
}
if item == nil || !item.IsDone {
this.locker.RUnlock()
return nil, ErrNotFound
@@ -136,17 +144,6 @@ func (this *MemoryStorage) OpenReader(key string, useStale bool, isPartial bool)
}
this.locker.RUnlock()
// 增加点击量
// 1/1000采样
// TODO 考虑是否在缓存策略里设置
if rands.Int(0, 1000) == 0 {
var hitErr = this.list.IncreaseHit(types.String(hash))
if hitErr != nil {
// 此错误可以忽略
remotelogs.Error("CACHE", "increase hit failed: "+hitErr.Error())
}
}
return reader, nil
}
this.locker.RUnlock()
@@ -158,7 +155,7 @@ func (this *MemoryStorage) OpenReader(key string, useStale bool, isPartial bool)
// OpenWriter 打开缓存写入器等待写入
func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, headerSize int, bodySize int64, maxSize int64, isPartial bool) (Writer, error) {
if this.ignoreKeys.Has(key) {
if maxSize > 0 && this.ignoreKeys.Has(types.String(maxSize)+"$"+key) {
return nil, ErrEntityTooLarge
}
@@ -179,7 +176,7 @@ func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, h
if isDirty &&
this.parentStorage != nil &&
this.dirtyQueueSize > 0 &&
len(this.dirtyChan) == this.dirtyQueueSize { // 缓存时间过长
len(this.dirtyChan) >= this.dirtyQueueSize-int(fsutils.DiskMaxWrites) /** delta **/ { // 缓存时间过长
return nil, ErrWritingQueueFull
}
@@ -215,13 +212,13 @@ func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, h
}
}
// 检查是否超出最大值
capacityBytes := this.memoryCapacityBytes()
// 检查是否超出容量最大值
var capacityBytes = this.memoryCapacityBytes()
if bodySize < 0 {
bodySize = 0
}
if capacityBytes > 0 && capacityBytes <= this.totalSize+bodySize {
return nil, NewCapacityError("write memory cache failed: over memory size: " + strconv.FormatInt(capacityBytes, 10) + ", current size: " + strconv.FormatInt(this.totalSize, 10) + " bytes")
if capacityBytes > 0 && capacityBytes <= atomic.LoadInt64(&this.usedSize)+bodySize {
return nil, NewCapacityError("write memory cache failed: over memory size: " + strconv.FormatInt(capacityBytes, 10) + ", current size: " + strconv.FormatInt(this.usedSize, 10) + " bytes")
}
// 先删除
@@ -231,7 +228,7 @@ func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, h
}
isWriting = true
return NewMemoryWriter(this, key, expiresAt, status, isDirty, maxSize, func() {
return NewMemoryWriter(this, key, expiresAt, status, isDirty, bodySize, maxSize, func(valueItem *MemoryItem) {
this.locker.Lock()
delete(this.writingKeyMap, key)
this.locker.Unlock()
@@ -263,7 +260,7 @@ func (this *MemoryStorage) CleanAll() error {
this.locker.Lock()
this.valuesMap = map[uint64]*MemoryItem{}
_ = this.list.Reset()
atomic.StoreInt64(&this.totalSize, 0)
atomic.StoreInt64(&this.usedSize, 0)
this.locker.Unlock()
return nil
}
@@ -362,6 +359,9 @@ func (this *MemoryStorage) UpdatePolicy(newPolicy *serverconfigs.HTTPCachePolicy
if newPolicy.CapacityBytes() == 0 {
_ = this.CleanAll()
}
// reset ignored keys
this.ignoreKeys.Reset()
}
// CanUpdatePolicy 检查策略是否可以更新
@@ -371,6 +371,11 @@ func (this *MemoryStorage) CanUpdatePolicy(newPolicy *serverconfigs.HTTPCachePol
// AddToList 将缓存添加到列表
func (this *MemoryStorage) AddToList(item *Item) {
// skip added item
if item.MetaSize > 0 {
return
}
item.MetaSize = int64(len(item.Key)) + 128 /** 128是我们评估的数据结构的长度 **/
var hash = types.String(this.hash(item.Key))
@@ -388,12 +393,12 @@ func (this *MemoryStorage) TotalDiskSize() int64 {
// TotalMemorySize 内存尺寸
func (this *MemoryStorage) TotalMemorySize() int64 {
return atomic.LoadInt64(&this.totalSize)
return atomic.LoadInt64(&this.usedSize)
}
// IgnoreKey 忽略某个Key即不缓存某个Key
func (this *MemoryStorage) IgnoreKey(key string) {
this.ignoreKeys.Push(key)
func (this *MemoryStorage) IgnoreKey(key string, maxSize int64) {
this.ignoreKeys.Push(types.String(maxSize) + "$" + key)
}
// CanSendfile 是否支持Sendfile
@@ -401,6 +406,11 @@ func (this *MemoryStorage) CanSendfile() bool {
return false
}
// HasFreeSpaceForHotItems 是否有足够的空间提供给热门内容
func (this *MemoryStorage) HasFreeSpaceForHotItems() bool {
return atomic.LoadInt64(&this.usedSize) < this.memoryCapacityBytes()*3/4
}
// 计算Key Hash
func (this *MemoryStorage) hash(key string) uint64 {
return xxhash.Sum64String(key)
@@ -408,22 +418,6 @@ func (this *MemoryStorage) hash(key string) uint64 {
// 清理任务
func (this *MemoryStorage) purgeLoop() {
// 计算是否应该开启LFU清理
var capacityBytes = this.policy.CapacityBytes()
var startLFU = false
var usedPercent = float32(this.TotalMemorySize()*100) / float32(capacityBytes)
var lfuFreePercent = this.policy.MemoryLFUFreePercent
if lfuFreePercent <= 0 {
lfuFreePercent = 5
}
if capacityBytes > 0 {
if lfuFreePercent < 100 {
if usedPercent >= 100-lfuFreePercent {
startLFU = true
}
}
}
// 清理过期
var purgeCount = this.policy.MemoryAutoPurgeCount
if purgeCount <= 0 {
@@ -440,6 +434,23 @@ func (this *MemoryStorage) purgeLoop() {
})
// LFU
// 计算是否应该开启LFU清理
var capacityBytes = this.policy.CapacityBytes()
var startLFU = false
var usedPercent = float32(this.TotalMemorySize()*100) / float32(capacityBytes)
var lfuFreePercent = this.policy.MemoryLFUFreePercent
if lfuFreePercent <= 0 {
lfuFreePercent = 5
}
if capacityBytes > 0 {
if lfuFreePercent < 100 {
if usedPercent >= 100-lfuFreePercent {
startLFU = true
}
}
}
if startLFU {
var total, _ = this.list.Count()
if total > 0 {
@@ -472,34 +483,20 @@ func (this *MemoryStorage) purgeLoop() {
// 开始Flush任务
func (this *MemoryStorage) startFlush() {
var statCount = 0
var writeDelayMS float64 = 0
for key := range this.dirtyChan {
statCount++
if statCount == 100 {
statCount = 0
if protectingLoadWhenDump {
loadStat, err := load.Avg()
if err == nil && loadStat != nil {
if loadStat.Load1 > 10 {
writeDelayMS = 100
} else if loadStat.Load1 > 3 {
writeDelayMS = 50
} else if loadStat.Load1 > 2 {
writeDelayMS = 10
} else {
writeDelayMS = 0
}
}
}
}
this.flushItem(key)
if writeDelayMS > 0 {
time.Sleep(time.Duration(writeDelayMS) * time.Millisecond)
if fsutils.IsInExtremelyHighLoad {
time.Sleep(1 * time.Second)
} else if fsutils.IsInHighLoad {
time.Sleep(100 * time.Millisecond)
}
}
}
@@ -515,9 +512,20 @@ func (this *MemoryStorage) flushItem(key string) {
item, ok := this.valuesMap[hash]
this.locker.RUnlock()
// 从内存中移除,并确保无论如何都会执行
defer func() {
_ = this.Delete(key)
// 重用内存,前提是确保内存不再被引用
if ok && item.IsDone && !item.isReferring && len(item.BodyValue) > 0 {
SharedFragmentMemoryPool.Put(item.BodyValue)
}
}()
if !ok {
return
}
if !item.IsDone {
remotelogs.Error("CACHE", "flush items failed: open writer failed: item has not been done")
return
@@ -526,6 +534,16 @@ func (this *MemoryStorage) flushItem(key string) {
return
}
// 检查是否在列表中防止未加入列表时就开始flush
isInList, err := this.list.Exist(types.String(hash))
if err != nil {
remotelogs.Error("CACHE", "flush items failed: "+err.Error())
return
}
if !isInList {
time.Sleep(1 * time.Second)
}
writer, err := this.parentStorage.OpenFlushWriter(key, item.ExpiresAt, item.Status, len(item.HeaderValue), int64(len(item.BodyValue)))
if err != nil {
if !CanIgnoreErr(err) {
@@ -563,26 +581,39 @@ func (this *MemoryStorage) flushItem(key string) {
HeaderSize: writer.HeaderSize(),
BodySize: writer.BodySize(),
})
// 从内存中移除
_ = this.Delete(key)
}
func (this *MemoryStorage) memoryCapacityBytes() int64 {
var maxSystemBytes = int64(utils.SystemMemoryBytes()) / 3 // 1/3 of the system memory
if this.policy == nil {
return 0
}
c1 := int64(0)
if this.policy.Capacity != nil {
c1 = this.policy.Capacity.Bytes()
return maxSystemBytes
}
if SharedManager.MaxMemoryCapacity != nil {
c2 := SharedManager.MaxMemoryCapacity.Bytes()
if c2 > 0 {
return c2
var capacityBytes = SharedManager.MaxMemoryCapacity.Bytes()
if capacityBytes > 0 {
if capacityBytes > maxSystemBytes {
return maxSystemBytes
}
return capacityBytes
}
}
return c1
var capacity = this.policy.Capacity // copy
if capacity != nil {
var capacityBytes = capacity.Bytes()
if capacityBytes > 0 {
if capacityBytes > maxSystemBytes {
return maxSystemBytes
}
return capacityBytes
}
}
// 1/4 of the system memory
return maxSystemBytes
}
func (this *MemoryStorage) deleteWithoutLocker(key string) error {

View File

@@ -4,11 +4,11 @@ package caches
import "strings"
// 获取 ranges 文件路径
func partialRangesFilePath(path string) string {
// PartialRangesFilePath 获取 ranges 文件路径
func PartialRangesFilePath(path string) string {
// ranges路径
var dotIndex = strings.LastIndex(path, ".")
var rangePath = ""
var rangePath string
if dotIndex < 0 {
rangePath = path + "@ranges.cache"
} else {

View File

@@ -29,7 +29,7 @@ func TestParseHost(t *testing.T) {
func TestUintString(t *testing.T) {
t.Log(strconv.FormatUint(xxhash.Sum64String("https://goedge.cn/"), 10))
t.Log(strconv.FormatUint(123456789, 10))
t.Log(fmt.Sprintf("%d", 1234567890123))
t.Logf("%d", 1234567890123)
}
func BenchmarkUint_String(b *testing.B) {

View File

@@ -3,6 +3,7 @@ package caches
import (
"encoding/binary"
"errors"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
"github.com/iwind/TeaGo/types"
"io"
"os"
@@ -42,7 +43,9 @@ func NewFileWriter(storage StorageInterface, rawWriter *os.File, key string, exp
// WriteHeader 写入数据
func (this *FileWriter) WriteHeader(data []byte) (n int, err error) {
fsutils.WriteBegin()
n, err = this.rawWriter.Write(data)
fsutils.WriteEnd()
this.headerSize += int64(n)
if err != nil {
_ = this.Discard()
@@ -72,21 +75,30 @@ func (this *FileWriter) WriteHeaderLength(headerLength int) error {
// Write 写入数据
func (this *FileWriter) Write(data []byte) (n int, err error) {
n, err = this.rawWriter.Write(data)
this.bodySize += int64(n)
if this.maxSize > 0 && this.bodySize > this.maxSize {
err = ErrEntityTooLarge
if this.storage != nil {
this.storage.IgnoreKey(this.key)
// split LARGE data
var l = len(data)
if l > (2 << 20) {
var offset = 0
const bufferSize = 256 << 10
for {
var end = offset + bufferSize
if end > l {
end = l
}
n1, err1 := this.write(data[offset:end])
n += n1
if err1 != nil {
return n, err1
}
if end >= l {
return n, nil
}
offset = end
}
}
if err != nil {
_ = this.Discard()
}
return
// write NORMAL size data
return this.write(data)
}
// WriteAt 在指定位置写入数据
@@ -126,18 +138,24 @@ func (this *FileWriter) Close() error {
err := this.WriteHeaderLength(types.Int(this.headerSize))
if err != nil {
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
_ = os.Remove(path)
return err
}
err = this.WriteBodyLength(this.bodySize)
if err != nil {
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
_ = os.Remove(path)
return err
}
fsutils.WriteBegin()
err = this.rawWriter.Close()
fsutils.WriteEnd()
if err != nil {
_ = os.Remove(path)
} else if strings.HasSuffix(path, FileTmpSuffix) {
@@ -156,7 +174,9 @@ func (this *FileWriter) Discard() error {
this.endFunc()
})
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
err := os.Remove(this.rawWriter.Name())
return err
@@ -182,3 +202,23 @@ func (this *FileWriter) Key() string {
func (this *FileWriter) ItemType() ItemType {
return ItemTypeFile
}
func (this *FileWriter) write(data []byte) (n int, err error) {
fsutils.WriteBegin()
n, err = this.rawWriter.Write(data)
fsutils.WriteEnd()
this.bodySize += int64(n)
if this.maxSize > 0 && this.bodySize > this.maxSize {
err = ErrEntityTooLarge
if this.storage != nil {
this.storage.IgnoreKey(this.key, this.maxSize)
}
}
if err != nil {
_ = this.Discard()
}
return
}

View File

@@ -2,9 +2,9 @@ package caches
import (
"errors"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/cespare/xxhash"
"sync"
"time"
)
type MemoryWriter struct {
@@ -16,29 +16,49 @@ type MemoryWriter struct {
bodySize int64
status int
isDirty bool
maxSize int64
expectedBodySize int64
maxSize int64
hash uint64
item *MemoryItem
endFunc func()
endFunc func(valueItem *MemoryItem)
once sync.Once
}
func NewMemoryWriter(memoryStorage *MemoryStorage, key string, expiredAt int64, status int, isDirty bool, maxSize int64, endFunc func()) *MemoryWriter {
w := &MemoryWriter{
storage: memoryStorage,
key: key,
expiredAt: expiredAt,
item: &MemoryItem{
ExpiresAt: expiredAt,
ModifiedAt: time.Now().Unix(),
Status: status,
},
status: status,
isDirty: isDirty,
maxSize: maxSize,
endFunc: endFunc,
func NewMemoryWriter(memoryStorage *MemoryStorage, key string, expiredAt int64, status int, isDirty bool, expectedBodySize int64, maxSize int64, endFunc func(valueItem *MemoryItem)) *MemoryWriter {
var valueItem = &MemoryItem{
ExpiresAt: expiredAt,
ModifiedAt: fasttime.Now().Unix(),
Status: status,
}
if expectedBodySize > 0 && expectedBodySize <= maxMemoryFragmentPoolItemSize {
bodyBytes, ok := SharedFragmentMemoryPool.Get(expectedBodySize) // try to reuse memory
if ok {
valueItem.BodyValue = bodyBytes
valueItem.IsPrepared = true
} else {
if expectedBodySize <= (16 << 20) {
var allocSize = (expectedBodySize/16384 + 1) * 16384
valueItem.BodyValue = make([]byte, allocSize)[:expectedBodySize]
valueItem.IsPrepared = true
SharedFragmentMemoryPool.IncreaseNew()
}
}
}
var w = &MemoryWriter{
storage: memoryStorage,
key: key,
expiredAt: expiredAt,
item: valueItem,
status: status,
isDirty: isDirty,
expectedBodySize: expectedBodySize,
maxSize: maxSize,
endFunc: endFunc,
}
w.hash = w.calculateHash(key)
return w
@@ -53,17 +73,32 @@ func (this *MemoryWriter) WriteHeader(data []byte) (n int, err error) {
// Write 写入数据
func (this *MemoryWriter) Write(data []byte) (n int, err error) {
this.bodySize += int64(len(data))
this.item.BodyValue = append(this.item.BodyValue, data...)
var l = len(data)
if l == 0 {
return
}
if this.item.IsPrepared {
if this.item.WriteOffset+int64(l) > this.expectedBodySize {
err = ErrWritingUnavailable
return
}
copy(this.item.BodyValue[this.item.WriteOffset:], data)
this.item.WriteOffset += int64(l)
} else {
this.item.BodyValue = append(this.item.BodyValue, data...)
}
this.bodySize += int64(l)
// 检查尺寸
if this.maxSize > 0 && this.bodySize > this.maxSize {
err = ErrEntityTooLarge
this.storage.IgnoreKey(this.key)
return len(data), err
this.storage.IgnoreKey(this.key, this.maxSize)
return l, err
}
return len(data), nil
return l, nil
}
// WriteAt 在指定位置写入数据
@@ -87,7 +122,8 @@ func (this *MemoryWriter) BodySize() int64 {
func (this *MemoryWriter) Close() error {
// 需要在Locker之外
defer this.once.Do(func() {
this.endFunc()
this.endFunc(this.item)
this.item = nil // free memory
})
if this.item == nil {
@@ -96,30 +132,48 @@ func (this *MemoryWriter) Close() error {
this.storage.locker.Lock()
this.item.IsDone = true
this.storage.valuesMap[this.hash] = this.item
var err error
if this.isDirty {
if this.storage.parentStorage != nil {
this.storage.valuesMap[this.hash] = this.item
select {
case this.storage.dirtyChan <- this.key:
default:
// remove from values map
delete(this.storage.valuesMap, this.hash)
err = ErrWritingQueueFull
}
} else {
this.storage.valuesMap[this.hash] = this.item
}
} else {
this.storage.valuesMap[this.hash] = this.item
}
this.storage.locker.Unlock()
return nil
return err
}
// Discard 丢弃
func (this *MemoryWriter) Discard() error {
// 需要在Locker之外
defer this.once.Do(func() {
this.endFunc()
this.endFunc(this.item)
this.item = nil // free memory
})
this.storage.locker.Lock()
delete(this.storage.valuesMap, this.hash)
if this.item != nil &&
!this.item.isReferring &&
cap(this.item.BodyValue) >= minMemoryFragmentPoolItemSize {
SharedFragmentMemoryPool.Put(this.item.BodyValue)
}
this.storage.locker.Unlock()
return nil
}

View File

@@ -4,6 +4,7 @@ package caches
import (
"encoding/binary"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
"github.com/iwind/TeaGo/types"
"io"
"os"
@@ -42,7 +43,7 @@ func NewPartialFileWriter(rawWriter *os.File, key string, expiredAt int64, metaH
isPartial: isPartial,
bodyOffset: bodyOffset,
ranges: ranges,
rangePath: partialRangesFilePath(rawWriter.Name()),
rangePath: PartialRangesFilePath(rawWriter.Name()),
metaHeaderSize: metaHeaderSize,
metaBodySize: metaBodySize,
}
@@ -53,7 +54,9 @@ func (this *PartialFileWriter) WriteHeader(data []byte) (n int, err error) {
if !this.isNew {
return
}
fsutils.WriteBegin()
n, err = this.rawWriter.Write(data)
fsutils.WriteEnd()
this.headerSize += int64(n)
if err != nil {
_ = this.Discard()
@@ -62,7 +65,9 @@ func (this *PartialFileWriter) WriteHeader(data []byte) (n int, err error) {
}
func (this *PartialFileWriter) AppendHeader(data []byte) error {
fsutils.WriteBegin()
_, err := this.rawWriter.Write(data)
fsutils.WriteEnd()
if err != nil {
_ = this.Discard()
} else {
@@ -99,7 +104,9 @@ func (this *PartialFileWriter) WriteHeaderLength(headerLength int) error {
// Write 写入数据
func (this *PartialFileWriter) Write(data []byte) (n int, err error) {
fsutils.WriteBegin()
n, err = this.rawWriter.Write(data)
fsutils.WriteEnd()
this.bodySize += int64(n)
if err != nil {
_ = this.Discard()
@@ -128,7 +135,9 @@ func (this *PartialFileWriter) WriteAt(offset int64, data []byte) error {
this.bodyOffset = SizeMeta + int64(keyLength) + this.headerSize
}
fsutils.WriteBegin()
_, err := this.rawWriter.WriteAt(data, this.bodyOffset+offset)
fsutils.WriteEnd()
if err != nil {
return err
}
@@ -172,7 +181,9 @@ func (this *PartialFileWriter) Close() error {
this.ranges.BodySize = this.bodySize
err := this.ranges.WriteToFile(this.rangePath)
if err != nil {
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
this.remove()
return err
}
@@ -181,19 +192,25 @@ func (this *PartialFileWriter) Close() error {
if this.isNew {
err = this.WriteHeaderLength(types.Int(this.headerSize))
if err != nil {
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
this.remove()
return err
}
err = this.WriteBodyLength(this.bodySize)
if err != nil {
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
this.remove()
return err
}
}
fsutils.WriteBegin()
err = this.rawWriter.Close()
fsutils.WriteEnd()
if err != nil {
this.remove()
}
@@ -207,7 +224,9 @@ func (this *PartialFileWriter) Discard() error {
this.endFunc()
})
fsutils.WriteBegin()
_ = this.rawWriter.Close()
fsutils.WriteEnd()
_ = os.Remove(this.rangePath)

View File

@@ -1,38 +1,81 @@
package configs
import (
"errors"
"github.com/iwind/TeaGo/Tea"
"gopkg.in/yaml.v3"
"os"
)
// APIConfig 节点API配置
const ConfigFileName = "api_node.yaml"
const oldConfigFileName = "api.yaml"
type APIConfig struct {
RPC struct {
OldRPC struct {
Endpoints []string `yaml:"endpoints" json:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate" json:"disableUpdate"`
} `yaml:"rpc" json:"rpc"`
NodeId string `yaml:"nodeId" json:"nodeId"`
Secret string `yaml:"secret" json:"secret"`
} `yaml:"rpc,omitempty" json:"rpc"`
RPCEndpoints []string `yaml:"rpc.endpoints,flow" json:"rpc.endpoints"`
RPCDisableUpdate bool `yaml:"rpc.disableUpdate" json:"rpc.disableUpdate"`
NodeId string `yaml:"nodeId" json:"nodeId"`
Secret string `yaml:"secret" json:"secret"`
}
func NewAPIConfig() *APIConfig {
return &APIConfig{}
}
func (this *APIConfig) Init() error {
// compatible with old
if len(this.RPCEndpoints) == 0 && len(this.OldRPC.Endpoints) > 0 {
this.RPCEndpoints = this.OldRPC.Endpoints
this.RPCDisableUpdate = this.OldRPC.DisableUpdate
}
if len(this.RPCEndpoints) == 0 {
return errors.New("no valid 'rpc.endpoints'")
}
if len(this.NodeId) == 0 {
return errors.New("'nodeId' required")
}
if len(this.Secret) == 0 {
return errors.New("'secret' required")
}
return nil
}
func LoadAPIConfig() (*APIConfig, error) {
data, err := os.ReadFile(Tea.ConfigFile("api.yaml"))
if err != nil {
return nil, err
}
for _, filename := range []string{ConfigFileName, oldConfigFileName} {
data, err := os.ReadFile(Tea.ConfigFile(filename))
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
config := &APIConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return nil, err
}
var config = &APIConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return nil, err
}
return config, nil
err = config.Init()
if err != nil {
return nil, errors.New("init error: " + err.Error())
}
// 自动生成新的配置文件
if filename == oldConfigFileName {
config.OldRPC.Endpoints = nil
_ = config.WriteFile(Tea.ConfigFile(ConfigFileName))
}
return config, nil
}
return nil, errors.New("no config file '" + ConfigFileName + "' found")
}
// WriteFile 保存到文件

View File

@@ -3,6 +3,7 @@ package configs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/configs"
_ "github.com/iwind/TeaGo/bootstrap"
"gopkg.in/yaml.v3"
"testing"
)
@@ -12,4 +13,10 @@ func TestLoadAPIConfig(t *testing.T) {
t.Fatal(err)
}
t.Logf("%+v", config)
configData, err := yaml.Marshal(config)
if err != nil {
t.Fatal(err)
}
t.Log(string(configData))
}

View File

@@ -1,11 +1,57 @@
package configs
import (
"github.com/iwind/TeaGo/Tea"
"gopkg.in/yaml.v3"
"os"
)
// ClusterConfig 集群配置
type ClusterConfig struct {
RPC struct {
OldRPC struct {
Endpoints []string `yaml:"endpoints" json:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate" json:"disableUpdate"`
} `yaml:"rpc" json:"rpc"`
} `yaml:"rpc,omitempty" json:"rpc"`
RPCEndpoints []string `yaml:"rpc.endpoints,flow" json:"rpc.endpoints"`
RPCDisableUpdate bool `yaml:"rpc.disableUpdate" json:"rpc.disableUpdate"`
ClusterId string `yaml:"clusterId" json:"clusterId"`
Secret string `yaml:"secret" json:"secret"`
}
func (this *ClusterConfig) Init() error {
// compatible with old
if len(this.RPCEndpoints) == 0 && len(this.OldRPC.Endpoints) > 0 {
this.RPCEndpoints = this.OldRPC.Endpoints
this.RPCDisableUpdate = this.OldRPC.DisableUpdate
}
return nil
}
func LoadClusterConfig() (*ClusterConfig, error) {
for _, filename := range []string{"api_cluster.yaml", "cluster.yaml"} {
data, err := os.ReadFile(Tea.ConfigFile(filename))
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
var config = &ClusterConfig{}
err = yaml.Unmarshal(data, config)
if err != nil {
return config, err
}
err = config.Init()
if err != nil {
return nil, err
}
return config, nil
}
return nil, os.ErrNotExist
}

View File

@@ -0,0 +1,23 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package configs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/configs"
"gopkg.in/yaml.v3"
"testing"
)
func TestLoadClusterConfig(t *testing.T) {
config, err := configs.LoadClusterConfig()
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", config)
configData, err := yaml.Marshal(config)
if err != nil {
t.Fatal(err)
}
t.Log(string(configData))
}

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.2.1"
Version = "1.2.10"
ProductName = "Edge Node"
ProcessName = "edge-node"
@@ -14,5 +14,6 @@ const (
// SystemdServiceName systemd
SystemdServiceName = "edge-node"
AccessLogSockName = "edge-node.accesslog.sock"
AccessLogSockName = "edge-node.accesslog"
CacheGarbageSockName = "edge-node.cache.garbage"
)

View File

@@ -22,8 +22,6 @@ var (
IsQuiting = false // 是否正在退出
EnableDBStat = false // 是否开启本地数据库统计
DiskIsFast = false // 是否为高速硬盘
)
// 检查是否为主程序

View File

@@ -19,7 +19,6 @@ func TestAES128CFBMethod_Encrypt(t *testing.T) {
dst = dst[:len(src)]
t.Log("dst:", string(dst))
src = make([]byte, len(src))
src, err = method.Decrypt(dst)
if err != nil {
t.Fatal(err)
@@ -54,7 +53,6 @@ func TestAES128CFBMethod_Encrypt2(t *testing.T) {
}
{
a := []byte{1}
_, err = method.Decrypt(a)
if err != nil {
@@ -64,7 +62,6 @@ func TestAES128CFBMethod_Encrypt2(t *testing.T) {
for _, dst := range sources {
dst2 := append([]byte{}, dst...)
src2 := make([]byte, len(dst2))
src2, err := method.Decrypt(dst2)
if err != nil {
t.Fatal(err)

View File

@@ -3,10 +3,11 @@ package events
type Event = string
const (
EventStart Event = "start" // start loading
EventLoaded Event = "loaded" // first load
EventQuit Event = "quit" // quit node gracefully
EventReload Event = "reload" // reload config
EventTerminated Event = "terminated" // process terminated
EventNFTablesReady Event = "nftablesReady" // nftables ready
EventStart Event = "start" // start loading
EventLoaded Event = "loaded" // first load
EventQuit Event = "quit" // quit node gracefully
EventReload Event = "reload" // reload config
EventTerminated Event = "terminated" // process terminated
EventNFTablesReady Event = "nftablesReady" // nftables ready
EventReloadSomeServers Event = "reloadSomeServers" // reload some servers
)

View File

@@ -24,6 +24,12 @@ func On(event Event, callback func()) {
OnKey(event, nil, callback)
}
func OnEvents(events []Event, callback func()) {
for _, event := range events {
On(event, callback)
}
}
func OnClose(callback func()) {
On(EventQuit, callback)
On(EventTerminated, callback)

View File

@@ -9,8 +9,8 @@ func TestOn(t *testing.T) {
type User struct {
name string
}
var u = &User{}
var u2 = &User{}
var u = &User{name: "lily"}
var u2 = &User{name: "lucy"}
events.On("hello", func() {
t.Log("world")

View File

@@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ddosconfigs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
@@ -92,7 +93,7 @@ func (this *DDoSProtectionManager) Apply(config *ddosconfigs.ProtectionConfig) e
// 对比配置
configJSON, err := json.Marshal(config)
if err != nil {
return errors.New("encode config to json failed: " + err.Error())
return fmt.Errorf("encode config to json failed: %w", err)
}
if !allowIPListChanged && bytes.Equal(this.lastConfig, configJSON) {
return nil
@@ -188,7 +189,7 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
for _, filter := range nftablesFilters {
chain, oldRules, err := this.getRules(filter)
if err != nil {
return errors.New("get old rules failed: " + err.Error())
return fmt.Errorf("get old rules failed: %w", err)
}
var protocol = filter.protocol()
@@ -273,7 +274,7 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
// 先清空所有相关规则
err = this.removeOldTCPRules(chain, oldRules)
if err != nil {
return errors.New("delete old rules failed: " + err.Error())
return fmt.Errorf("delete old rules failed: %w", err)
}
// 添加新规则
@@ -281,9 +282,9 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
if maxConnections > 0 {
var cmd = executils.NewTimeoutCmd(10*time.Second, nftExe, "add", "rule", protocol, filter.Name, nftablesChainName, "tcp", "dport", types.String(port), "ct", "count", "over", types.String(maxConnections), "counter", "drop", "comment", this.encodeUserData([]string{"tcp", types.String(port), "maxConnections", types.String(maxConnections)}))
cmd.WithStderr()
err := cmd.Run()
err = cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
}
@@ -293,7 +294,7 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
}
@@ -305,14 +306,14 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
} else {
var cmd = executils.NewTimeoutCmd(10*time.Second, nftExe, "add", "rule", protocol, filter.Name, nftablesChainName, "tcp", "dport", types.String(port), "ct", "state", "new", "meter", "meter-"+protocol+"-"+types.String(port)+"-new-connections-rate", "{ "+protocol+" saddr limit rate over "+types.String(newConnectionsMinutelyRate)+"/minute burst "+types.String(newConnectionsMinutelyRate+3)+" packets }" /**"add", "@deny_set", "{"+protocol+" saddr}",**/, "counter", "drop", "comment", this.encodeUserData([]string{"tcp", types.String(port), "newConnectionsRate", "0"}))
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
}
}
@@ -325,14 +326,14 @@ func (this *DDoSProtectionManager) addTCPRules(tcpConfig *ddosconfigs.TCPConfig)
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
} else {
var cmd = executils.NewTimeoutCmd(10*time.Second, nftExe, "add", "rule", protocol, filter.Name, nftablesChainName, "tcp", "dport", types.String(port), "ct", "state", "new", "meter", "meter-"+protocol+"-"+types.String(port)+"-new-connections-secondly-rate", "{ "+protocol+" saddr limit rate over "+types.String(newConnectionsSecondlyRate)+"/second burst "+types.String(newConnectionsSecondlyRate+3)+" packets }" /**"add", "@deny_set", "{"+protocol+" saddr}",**/, "counter", "drop", "comment", this.encodeUserData([]string{"tcp", types.String(port), "newConnectionsSecondlyRate", "0"}))
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("add nftables rule '" + cmd.String() + "' failed: " + err.Error() + " (" + cmd.Stderr() + ")")
return fmt.Errorf("add nftables rule '%s' failed: %w (%s)", cmd.String(), err, cmd.Stderr())
}
}
}
@@ -498,11 +499,11 @@ func (this *DDoSProtectionManager) getTable(filter *nftablesTableDefinition) (*n
func (this *DDoSProtectionManager) getRules(filter *nftablesTableDefinition) (*nftables.Chain, []*nftables.Rule, error) {
table, err := this.getTable(filter)
if err != nil {
return nil, nil, errors.New("get table failed: " + err.Error())
return nil, nil, fmt.Errorf("get table failed: %w", err)
}
chain, err := table.GetChain(nftablesChainName)
if err != nil {
return nil, nil, errors.New("get chain failed: " + err.Error())
return nil, nil, fmt.Errorf("get chain failed: %w", err)
}
rules, err := chain.GetRules()
return chain, rules, err
@@ -538,7 +539,7 @@ func (this *DDoSProtectionManager) updateAllowIPList(allIPList []string) error {
// 不存在则删除
err = set.DeleteIPElement(ip)
if err != nil {
return errors.New("delete ip element '" + ip + "' failed: " + err.Error())
return fmt.Errorf("delete ip element '%s' failed: %w", ip, err)
}
}
}
@@ -556,7 +557,7 @@ func (this *DDoSProtectionManager) updateAllowIPList(allIPList []string) error {
// 不存在则添加
err = set.AddIPElement(ip, nil, false)
if err != nil {
return errors.New("add ip '" + ip + "' failed: " + err.Error())
return fmt.Errorf("add ip '%s' failed: %w", ip, err)
}
}
}

View File

@@ -3,7 +3,7 @@
package firewalls
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/conns"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
@@ -194,7 +194,7 @@ func (this *Firewalld) DropSourceIP(ip string, timeoutSeconds int, async bool) e
err := cmd.Run()
if err != nil {
return errors.New("run command failed '" + cmd.String() + "': " + err.Error())
return fmt.Errorf("run command failed '%s': %w", cmd.String(), err)
}
return nil
}

View File

@@ -5,6 +5,7 @@ package firewalls
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeNode/internal/conns"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
@@ -149,10 +150,10 @@ func (this *NFTablesFirewall) init() error {
table, err = this.conn.AddIPv6Table(tableDef.Name)
}
if err != nil {
return errors.New("create table '" + tableDef.Name + "' failed: " + err.Error())
return fmt.Errorf("create table '%s' failed: %w", tableDef.Name, err)
}
} else {
return errors.New("get table '" + tableDef.Name + "' failed: " + err.Error())
return fmt.Errorf("get table '%s' failed: %w", tableDef.Name, err)
}
}
if table == nil {
@@ -166,10 +167,10 @@ func (this *NFTablesFirewall) init() error {
if nftables.IsNotFound(err) {
chain, err = table.AddAcceptChain(chainName)
if err != nil {
return errors.New("create chain '" + chainName + "' failed: " + err.Error())
return fmt.Errorf("create chain '%s' failed: %w", chainName, err)
}
} else {
return errors.New("get chain '" + chainName + "' failed: " + err.Error())
return fmt.Errorf("get chain '%s' failed: %w", chainName, err)
}
}
if chain == nil {
@@ -184,7 +185,7 @@ func (this *NFTablesFirewall) init() error {
_, err = chain.AddAcceptInterfaceRule("lo", loRuleName)
}
if err != nil {
return errors.New("add 'lo' rule failed: " + err.Error())
return fmt.Errorf("add 'lo' rule failed: %w", err)
}
}
@@ -207,10 +208,10 @@ func (this *NFTablesFirewall) init() error {
HasTimeout: true,
})
if err != nil {
return errors.New("create set '" + setName + "' failed: " + err.Error())
return fmt.Errorf("create set '%s' failed: %w", setName, err)
}
} else {
return errors.New("get set '" + setName + "' failed: " + err.Error())
return fmt.Errorf("get set '%s' failed: %w", setName, err)
}
}
if set == nil {
@@ -259,10 +260,10 @@ func (this *NFTablesFirewall) init() error {
}
}
if err != nil {
return errors.New("add rule failed: " + err.Error())
return fmt.Errorf("add rule failed: %w", err)
}
} else {
return errors.New("get rule failed: " + err.Error())
return fmt.Errorf("get rule failed: %w", err)
}
}
if rule == nil {

View File

@@ -4,7 +4,7 @@
package nftables
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/events"
@@ -85,7 +85,7 @@ func (this *Installer) Install() error {
}
// 检查是否已经存在
if len(NftExePath()) > 0 {
if len(NftExePath()) > 0 {
return nil
}
@@ -121,7 +121,7 @@ func (this *Installer) Install() error {
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New(err.Error() + ": " + cmd.Stderr())
return fmt.Errorf("%w: %s", err, cmd.Stderr())
}
remotelogs.Println("NFTABLES", "installed nftables with command '"+cmd.String()+"' successfully")

View File

@@ -1,27 +1,28 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package goman
package goman_test
import (
"github.com/TeaOSLab/EdgeNode/internal/goman"
"testing"
"time"
)
func TestNew(t *testing.T) {
New(func() {
goman.New(func() {
t.Log("Hello")
t.Log(List())
t.Log(goman.List())
})
time.Sleep(1 * time.Second)
t.Log(List())
t.Log(goman.List())
time.Sleep(1 * time.Second)
}
func TestNewWithArgs(t *testing.T) {
NewWithArgs(func(args ...interface{}) {
goman.NewWithArgs(func(args ...interface{}) {
t.Log(args[0], args[1])
}, 1, 2)
time.Sleep(1 * time.Second)

View File

@@ -0,0 +1,52 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package goman
import (
"github.com/TeaOSLab/EdgeNode/internal/zero"
"runtime"
"sync"
)
type TaskGroup struct {
semi chan zero.Zero
wg *sync.WaitGroup
locker *sync.RWMutex
}
func NewTaskGroup() *TaskGroup {
var concurrent = runtime.NumCPU()
if concurrent <= 1 {
concurrent = 2
}
return &TaskGroup{
semi: make(chan zero.Zero, concurrent),
wg: &sync.WaitGroup{},
locker: &sync.RWMutex{},
}
}
func (this *TaskGroup) Run(f func()) {
this.wg.Add(1)
go func() {
defer this.wg.Done()
this.semi <- zero.Zero{}
f()
<-this.semi
}()
}
func (this *TaskGroup) Wait() {
this.wg.Wait()
}
func (this *TaskGroup) Lock() {
this.locker.Lock()
}
func (this *TaskGroup) Unlock() {
this.locker.Unlock()
}

View File

@@ -0,0 +1,30 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package goman_test
import (
"github.com/TeaOSLab/EdgeNode/internal/goman"
"runtime"
"testing"
)
func TestNewTaskGroup(t *testing.T) {
var group = goman.NewTaskGroup()
var m = map[int]bool{}
for i := 0; i < runtime.NumCPU()*2; i++ {
var index = i
group.Run(func() {
t.Log("task", index)
group.Lock()
_, ok := m[index]
if ok {
t.Error("duplicated:", index)
}
m[index] = true
group.Unlock()
})
}
group.Wait()
}

View File

@@ -147,7 +147,7 @@ func (this *FirewalldAction) runActionSingleIP(action string, listType IPListTyp
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New(err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("%w, output: %s", err, cmd.Stderr())
}
return nil
}

View File

@@ -2,6 +2,7 @@ package iplibrary
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
executils "github.com/TeaOSLab/EdgeNode/internal/utils/exec"
@@ -70,7 +71,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
if err != nil {
var output = cmd.Stderr()
if !strings.Contains(output, "already exists") {
return errors.New("create ipset '" + listName + "': " + err.Error() + ", output: " + output)
return fmt.Errorf("create ipset '%s': %w, output: %s", listName, err, output)
} else {
err = nil
}
@@ -88,7 +89,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
if err != nil {
var output = cmd.Stderr()
if !strings.Contains(output, "already exists") {
return errors.New("create ipset '" + listName + "': " + err.Error() + ", output: " + output)
return fmt.Errorf("create ipset '%s': %w, output: %s", listName, err, output)
} else {
err = nil
}
@@ -116,7 +117,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
if strings.Contains(output, "NAME_CONFLICT") {
err = nil
} else {
return errors.New("firewall-cmd add ipset '" + listName + "': " + err.Error() + ", output: " + output)
return fmt.Errorf("firewall-cmd add ipset '%s': %w, output: %s", listName, err, output)
}
}
}
@@ -134,7 +135,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
if strings.Contains(output, "NAME_CONFLICT") {
err = nil
} else {
return errors.New("firewall-cmd add ipset '" + listName + "': " + err.Error() + ", output: " + output)
return fmt.Errorf("firewall-cmd add ipset '%s': %w, output: %s", listName, err, output)
}
}
}
@@ -148,7 +149,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("firewall-cmd add rich rule '" + listName + "': " + err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("firewall-cmd add rich rule '%s': %w, output: %s", listName, err, cmd.Stderr())
}
}
@@ -161,7 +162,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("firewall-cmd add rich rule '" + listName + "': " + err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("firewall-cmd add rich rule '%s': %w, output: %s", listName, err, cmd.Stderr())
}
}
@@ -171,7 +172,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("firewall-cmd reload: " + err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("firewall-cmd reload: %w, output: %s", err, cmd.Stderr())
}
}
}
@@ -200,7 +201,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("iptables add rule: " + err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("iptables add rule: %w, output: %s", err, cmd.Stderr())
}
}
}
@@ -221,7 +222,7 @@ func (this *IPSetAction) Init(config *firewallconfigs.FirewallActionConfig) erro
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New("iptables add rule: " + err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("iptables add rule: %w, output: %s", err, cmd.Stderr())
}
}
}
@@ -283,7 +284,7 @@ func (this *IPSetAction) runActionSingleIP(action string, listType IPListType, i
return nil
}
var listName = ""
var listName string
var isIPv6 = strings.Contains(item.IpFrom, ":")
switch listType {

View File

@@ -1,7 +1,7 @@
package iplibrary
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
@@ -128,7 +128,7 @@ func (this *IPTablesAction) runActionSingleIP(action string, listType IPListType
if strings.Contains(output, "No chain/target/match") {
err = nil
} else {
return errors.New(err.Error() + ", output: " + output)
return fmt.Errorf("%w, output: %s", err, output)
}
}
return nil

View File

@@ -102,7 +102,7 @@ func (this *ActionManager) UpdateActions(actions []*firewallconfigs.FirewallActi
continue
}
instances, _ := this.eventMap[action.EventLevel]
var instances = this.eventMap[action.EventLevel]
instances = append(instances, instance)
this.eventMap[action.EventLevel] = instances
}

View File

@@ -1,7 +1,6 @@
package iplibrary
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
@@ -62,7 +61,7 @@ func (this *ScriptAction) runAction(action string, listType IPListType, item *pb
cmd.WithStderr()
err := cmd.Run()
if err != nil {
return errors.New(err.Error() + ", output: " + cmd.Stderr())
return fmt.Errorf("%w, output: %s", err, cmd.Stderr())
}
return nil
}

View File

@@ -14,6 +14,8 @@ var GlobalWhiteIPList = NewIPList()
// IPList IP名单
// TODO IP名单可以分片关闭这样让每一片的数据量减少查询更快
type IPList struct {
isDeleted bool
itemsMap map[uint64]*IPItem // id => item
sortedItems []*IPItem
allItemsMap map[uint64]*IPItem // id => item
@@ -38,11 +40,19 @@ func NewIPList() *IPList {
}
func (this *IPList) Add(item *IPItem) {
if this.isDeleted {
return
}
this.addItem(item, true)
}
// AddDelay 延迟添加需要手工调用Sort()函数
func (this *IPList) AddDelay(item *IPItem) {
if this.isDeleted {
return
}
this.addItem(item, false)
}
@@ -60,6 +70,10 @@ func (this *IPList) Delete(itemId uint64) {
// Contains 判断是否包含某个IP
func (this *IPList) Contains(ip uint64) bool {
if this.isDeleted {
return false
}
this.locker.RLock()
if len(this.allItemsMap) > 0 {
this.locker.RUnlock()
@@ -75,6 +89,10 @@ func (this *IPList) Contains(ip uint64) bool {
// ContainsExpires 判断是否包含某个IP
func (this *IPList) ContainsExpires(ip uint64) (expiresAt int64, ok bool) {
if this.isDeleted {
return
}
this.locker.RLock()
if len(this.allItemsMap) > 0 {
this.locker.RUnlock()
@@ -94,6 +112,10 @@ func (this *IPList) ContainsExpires(ip uint64) (expiresAt int64, ok bool) {
// ContainsIPStrings 是否包含一组IP中的任意一个并返回匹配的第一个Item
func (this *IPList) ContainsIPStrings(ipStrings []string) (item *IPItem, found bool) {
if this.isDeleted {
return
}
if len(ipStrings) == 0 {
return
}
@@ -125,6 +147,10 @@ func (this *IPList) ContainsIPStrings(ipStrings []string) (item *IPItem, found b
return
}
func (this *IPList) SetDeleted() {
this.isDeleted = true
}
func (this *IPList) addItem(item *IPItem, sortable bool) {
if item == nil {
return

View File

@@ -60,7 +60,7 @@ func (this *IPListDB) init() error {
var path = this.dir + "/ip_list.db"
db, err := dbs.OpenWriter("file:" + path + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=OFF&_locking_mode=EXCLUSIVE")
db, err := dbs.OpenWriter("file:" + path + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=" + dbs.SyncMode + "&_locking_mode=EXCLUSIVE")
if err != nil {
return err
}

View File

@@ -22,10 +22,10 @@ func TestIPListDB_AddItem(t *testing.T) {
IpFrom: "192.168.1.101",
IpTo: "",
Version: 1024,
ExpiredAt: time.Now().Unix(),
ExpiredAt: time.Now().Unix() + 3600,
Reason: "",
ListId: 2,
IsDeleted: true,
IsDeleted: false,
Type: "ipv4",
EventLevel: "error",
ListType: "black",

View File

@@ -187,7 +187,7 @@ func (this *IPListManager) fetch() (hasNext bool, err error) {
})
if err != nil {
if rpc.IsConnError(err) {
remotelogs.Warn("IP_LIST_MANAGER", "rpc connection error: "+err.Error())
remotelogs.Debug("IP_LIST_MANAGER", "rpc connection error: "+err.Error())
return false, nil
}
return false, err
@@ -214,7 +214,7 @@ func (this *IPListManager) fetch() (hasNext bool, err error) {
func (this *IPListManager) FindList(listId int64) *IPList {
this.locker.Lock()
list, _ := this.listMap[listId]
var list = this.listMap[listId]
this.locker.Unlock()
return list
}
@@ -229,6 +229,11 @@ func (this *IPListManager) DeleteExpiredItems() {
func (this *IPListManager) processItems(items []*pb.IPItem, fromRemote bool) {
var changedLists = map[*IPList]zero.Zero{}
for _, item := range items {
// 调试
if Tea.IsTesting() {
this.debugItem(item)
}
var list *IPList
// TODO 实现节点专有List
if item.ServerId > 0 { // 服务专有List
@@ -300,3 +305,12 @@ func (this *IPListManager) processItems(items []*pb.IPItem, fromRemote bool) {
}
}
}
// 调试IP信息
func (this *IPListManager) debugItem(item *pb.IPItem) {
if item.IsDeleted {
remotelogs.Debug("IP_ITEM_DEBUG", "delete '"+item.IpFrom+"'")
} else {
remotelogs.Debug("IP_ITEM_DEBUG", "add '"+item.IpFrom+"'")
}
}

View File

@@ -26,9 +26,9 @@ func init() {
type Manager struct {
isQuiting bool
tasks map[int64]*Task // itemId => *Task
categoryTasks map[string][]*Task // category => []*Task
locker sync.RWMutex
taskMap map[int64]*Task // itemId => *Task
categoryTaskMap map[string][]*Task // category => []*Task
locker sync.RWMutex
hasHTTPMetrics bool
hasTCPMetrics bool
@@ -37,8 +37,8 @@ type Manager struct {
func NewManager() *Manager {
return &Manager{
tasks: map[int64]*Task{},
categoryTasks: map[string][]*Task{},
taskMap: map[int64]*Task{},
categoryTaskMap: map[string][]*Task{},
}
}
@@ -56,7 +56,7 @@ func (this *Manager) Update(items []*serverconfigs.MetricItemConfig) {
}
// 停用以前的 或 修改现在的
for itemId, task := range this.tasks {
for itemId, task := range this.taskMap {
newItem, ok := newMap[itemId]
if !ok || !newItem.IsOn { // 停用以前的
remotelogs.Println("METRIC_MANAGER", "stop task '"+strconv.FormatInt(itemId, 10)+"'")
@@ -64,7 +64,7 @@ func (this *Manager) Update(items []*serverconfigs.MetricItemConfig) {
if err != nil {
remotelogs.Error("METRIC_MANAGER", "stop task '"+strconv.FormatInt(itemId, 10)+"' failed: "+err.Error())
}
delete(this.tasks, itemId)
delete(this.taskMap, itemId)
} else { // 更新已存在的
if newItem.Version != task.item.Version {
remotelogs.Println("METRIC_MANAGER", "update task '"+strconv.FormatInt(itemId, 10)+"'")
@@ -78,7 +78,7 @@ func (this *Manager) Update(items []*serverconfigs.MetricItemConfig) {
if !newItem.IsOn {
continue
}
_, ok := this.tasks[newItem.Id]
_, ok := this.taskMap[newItem.Id]
if !ok {
remotelogs.Println("METRIC_MANAGER", "start task '"+strconv.FormatInt(newItem.Id, 10)+"'")
task := NewTask(newItem)
@@ -92,7 +92,7 @@ func (this *Manager) Update(items []*serverconfigs.MetricItemConfig) {
remotelogs.Error("METRIC_MANAGER", "start task failed: "+err.Error())
continue
}
this.tasks[newItem.Id] = task
this.taskMap[newItem.Id] = task
}
}
@@ -100,11 +100,11 @@ func (this *Manager) Update(items []*serverconfigs.MetricItemConfig) {
this.hasHTTPMetrics = false
this.hasTCPMetrics = false
this.hasUDPMetrics = false
this.categoryTasks = map[string][]*Task{}
for _, task := range this.tasks {
tasks := this.categoryTasks[task.item.Category]
this.categoryTaskMap = map[string][]*Task{}
for _, task := range this.taskMap {
var tasks = this.categoryTaskMap[task.item.Category]
tasks = append(tasks, task)
this.categoryTasks[task.item.Category] = tasks
this.categoryTaskMap[task.item.Category] = tasks
switch task.item.Category {
case serverconfigs.MetricItemCategoryHTTP:
@@ -124,10 +124,12 @@ func (this *Manager) Add(obj MetricInterface) {
}
this.locker.RLock()
for _, task := range this.categoryTasks[obj.MetricCategory()] {
var tasks = this.categoryTaskMap[obj.MetricCategory()]
this.locker.RUnlock()
for _, task := range tasks {
task.Add(obj)
}
this.locker.RUnlock()
}
func (this *Manager) HasHTTPMetrics() bool {
@@ -149,9 +151,9 @@ func (this *Manager) Quit() {
remotelogs.Println("METRIC_MANAGER", "quit")
this.locker.Lock()
for _, task := range this.tasks {
for _, task := range this.taskMap {
_ = task.Stop()
}
this.tasks = map[int64]*Task{}
this.taskMap = map[int64]*Task{}
this.locker.Unlock()
}

View File

@@ -11,7 +11,7 @@ func TestNewManager(t *testing.T) {
var manager = NewManager()
{
manager.Update([]*serverconfigs.MetricItemConfig{})
for _, task := range manager.tasks {
for _, task := range manager.taskMap {
t.Log(task.item.Id)
}
}
@@ -28,7 +28,7 @@ func TestNewManager(t *testing.T) {
Id: 3,
},
})
for _, task := range manager.tasks {
for _, task := range manager.taskMap {
t.Log("task:", task.item.Id)
}
}
@@ -43,7 +43,7 @@ func TestNewManager(t *testing.T) {
Id: 2,
},
})
for _, task := range manager.tasks {
for _, task := range manager.taskMap {
t.Log("task:", task.item.Id)
}
}
@@ -56,7 +56,7 @@ func TestNewManager(t *testing.T) {
Version: 1,
},
})
for _, task := range manager.tasks {
for _, task := range manager.taskMap {
t.Log("task:", task.item.Id)
}
}

View File

@@ -17,6 +17,6 @@ type Stat struct {
}
func SumStat(serverId int64, keys []string, time string, version int32, itemId int64) string {
keysData := strings.Join(keys, "$EDGE$")
var keysData = strings.Join(keys, "$EDGE$")
return strconv.FormatUint(fnv.HashString(strconv.FormatInt(serverId, 10)+"@"+keysData+"@"+time+"@"+strconv.Itoa(int(version))+"@"+strconv.FormatInt(itemId, 10)), 10)
}

View File

@@ -0,0 +1,20 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package metrics_test
import (
"github.com/TeaOSLab/EdgeNode/internal/metrics"
timeutil "github.com/iwind/TeaGo/utils/time"
"runtime"
"testing"
)
func BenchmarkSumStat(b *testing.B) {
runtime.GOMAXPROCS(2)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
metrics.SumStat(1, []string{"1.2.3.4"}, timeutil.Format("Ymd"), 1, 1)
}
})
}

View File

@@ -19,6 +19,7 @@ import (
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
@@ -59,7 +60,7 @@ type Task struct {
serverIdMapLocker sync.Mutex
statsMap map[string]*Stat // 待写入队列hash => *Stat
statsLocker sync.Mutex
statsLocker sync.RWMutex
statsTicker *utils.Ticker
}
@@ -90,7 +91,7 @@ func (this *Task) Init() error {
var path = dir + "/metric." + types.String(this.item.Id) + ".db"
db, err := dbs.OpenWriter("file:" + path + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=OFF&_locking_mode=EXCLUSIVE")
db, err := dbs.OpenWriter("file:" + path + "?cache=shared&mode=rwc&_journal_mode=WAL&_sync=" + dbs.SyncMode + "&_locking_mode=EXCLUSIVE")
if err != nil {
return err
}
@@ -237,7 +238,7 @@ func (this *Task) Add(obj MetricInterface) {
var keys = []string{}
for _, key := range this.item.Keys {
k := obj.MetricKey(key)
var k = obj.MetricKey(key)
// 忽略499状态
if key == "${status}" && k == "499" {
@@ -253,14 +254,19 @@ func (this *Task) Add(obj MetricInterface) {
}
var hash = SumStat(obj.MetricServerId(), keys, this.item.CurrentTime(), this.item.Version, this.item.Id)
this.statsLocker.Lock()
var countItems int
this.statsLocker.RLock()
oldStat, ok := this.statsMap[hash]
if !ok {
countItems = len(this.statsMap)
}
this.statsLocker.RUnlock()
if ok {
oldStat.Value += v
oldStat.Hash = hash
atomic.AddInt64(&oldStat.Value, 1)
} else {
// 防止过载
if len(this.statsMap) < MaxQueueSize {
if countItems < MaxQueueSize {
this.statsLocker.Lock()
this.statsMap[hash] = &Stat{
ServerId: obj.MetricServerId(),
Keys: keys,
@@ -268,9 +274,9 @@ func (this *Task) Add(obj MetricInterface) {
Time: this.item.CurrentTime(),
Hash: hash,
}
this.statsLocker.Unlock()
}
}
this.statsLocker.Unlock()
}
// Stop 停止任务

View File

@@ -4,11 +4,15 @@ package metrics_test
import (
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeNode/internal/metrics"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/rands"
"log"
"runtime"
"sync"
"testing"
"time"
)
@@ -213,3 +217,65 @@ func TestTask_Upload(t *testing.T) {
}
t.Log("ok")
}
var testingTask *metrics.Task
var testingTaskInitOnce = &sync.Once{}
func initTestingTask() {
testingTask = metrics.NewTask(&serverconfigs.MetricItemConfig{
Id: 1,
IsOn: false,
Category: "tcp",
Period: 1,
PeriodUnit: serverconfigs.MetricItemPeriodUnitDay,
Keys: []string{"${remoteAddr}"},
Value: "${countRequest}",
})
err := testingTask.Init()
if err != nil {
log.Fatal(err)
}
err = testingTask.Start()
if err != nil {
log.Fatal(err)
}
}
func BenchmarkTask_Add(b *testing.B) {
runtime.GOMAXPROCS(1)
testingTaskInitOnce.Do(func() {
initTestingTask()
})
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
testingTask.Add(&taskRequest{})
}
})
}
type taskRequest struct {
}
func (this *taskRequest) MetricKey(key string) string {
return configutils.ParseVariables(key, func(varName string) (value string) {
return "1.2.3.4"
})
}
func (this *taskRequest) MetricValue(value string) (result int64, ok bool) {
return 1, true
}
func (this *taskRequest) MetricServerId() int64 {
return 1
}
func (this *taskRequest) MetricCategory() string {
return "http"
}

View File

@@ -50,7 +50,11 @@ func (this *APIStream) Start() {
}
err := this.loop()
if err != nil {
remotelogs.Warn("API_STREAM", err.Error())
if rpc.IsConnError(err) {
remotelogs.Debug("API_STREAM", err.Error())
} else {
remotelogs.Warn("API_STREAM", err.Error())
}
time.Sleep(10 * time.Second)
continue
}
@@ -76,7 +80,7 @@ func (this *APIStream) loop() error {
if this.isQuiting {
return nil
}
return errors.Wrap(err)
return err
}
this.stream = nodeStream
@@ -92,7 +96,7 @@ func (this *APIStream) loop() error {
remotelogs.Println("API_STREAM", "quit")
return nil
}
return errors.Wrap(err)
return err
}
// 处理消息
@@ -438,10 +442,10 @@ func (this *APIStream) handleChangeAPINode(message *pb.NodeStreamMessage) error
return nil
}
config.RPC.Endpoints = []string{messageData.Addr}
config.RPCEndpoints = []string{messageData.Addr}
// 保存到文件
err = config.WriteFile(Tea.ConfigFile("api.yaml"))
err = config.WriteFile(Tea.ConfigFile(configs.ConfigFileName))
if err != nil {
this.replyFail(message.RequestId, "save config file failed: "+err.Error())
return nil

View File

@@ -3,7 +3,7 @@
package nodes
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeNode/internal/conns"
@@ -24,8 +24,6 @@ import (
"time"
)
var synFloodCounter = counters.NewCounter().WithGC()
// ClientConn 客户端连接
type ClientConn struct {
BaseClientConn
@@ -98,7 +96,7 @@ func (this *ClientConn) Read(b []byte) (n int, err error) {
defer func() {
if err != nil {
this.lastErr = errors.New("read error: " + err.Error())
this.lastErr = fmt.Errorf("read error: %w", err)
} else {
this.lastErr = nil
}
@@ -129,9 +127,8 @@ func (this *ClientConn) Read(b []byte) (n int, err error) {
// 检测是否为超时错误
var isTimeout = err != nil && os.IsTimeout(err)
var isHandshakeError = isTimeout && !this.hasRead
if isTimeout {
_ = this.SetLinger(0)
} else {
if err != nil {
_ = this.SetLinger(nodeconfigs.DefaultTCPLinger)
}
@@ -164,7 +161,7 @@ func (this *ClientConn) Write(b []byte) (n int, err error) {
defer func() {
if err != nil {
this.lastErr = errors.New("write error: " + err.Error())
this.lastErr = fmt.Errorf("write error: %w", err)
} else {
this.lastErr = nil
}
@@ -189,6 +186,8 @@ func (this *ClientConn) Write(b []byte) (n int, err error) {
var before = time.Now()
n, err = this.rawConn.Write(b)
if n > 0 {
atomic.AddInt64(&this.totalSentBytes, int64(n))
// 统计当前服务带宽
if this.serverId > 0 {
// TODO 需要加入在serverId绑定之前的带宽
@@ -197,9 +196,9 @@ func (this *ClientConn) Write(b []byte) (n int, err error) {
var cost = time.Since(before).Seconds()
if cost > 1 {
stats.SharedBandwidthStatManager.AddBandwidth(this.userId, this.serverId, int64(float64(n)/cost), int64(n))
stats.SharedBandwidthStatManager.AddBandwidth(this.userId, this.userPlanId, this.serverId, int64(float64(n)/cost), int64(n))
} else {
stats.SharedBandwidthStatManager.AddBandwidth(this.userId, this.serverId, int64(n), int64(n))
stats.SharedBandwidthStatManager.AddBandwidth(this.userId, this.userPlanId, this.serverId, int64(n), int64(n))
}
}
}
@@ -291,13 +290,13 @@ func (this *ClientConn) LastErr() error {
}
func (this *ClientConn) resetSYNFlood() {
synFloodCounter.ResetKey("SYN_FLOOD:" + this.RawIP())
counters.SharedCounter.ResetKey("SYN_FLOOD:" + this.RawIP())
}
func (this *ClientConn) increaseSYNFlood(synFloodConfig *firewallconfigs.SYNFloodConfig) {
var ip = this.RawIP()
if len(ip) > 0 && !iplibrary.IsInWhiteList(ip) && (!synFloodConfig.IgnoreLocal || !utils.IsLocalIP(ip)) {
var result = synFloodCounter.IncreaseKey("SYN_FLOOD:"+ip, 60)
var result = counters.SharedCounter.IncreaseKey("SYN_FLOOD:"+ip, 60)
var minAttempts = synFloodConfig.MinAttempts
if minAttempts < 5 {
minAttempts = 5

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeNode/internal/firewalls"
"github.com/TeaOSLab/EdgeNode/internal/iplibrary"
"net"
"sync/atomic"
"time"
)
@@ -15,6 +16,7 @@ type BaseClientConn struct {
isBound bool
userId int64
userPlanId int64
serverId int64
remoteAddr string
hasLimit bool
@@ -25,6 +27,8 @@ type BaseClientConn struct {
isClosed bool
rawIP string
totalSentBytes int64
}
func (this *BaseClientConn) IsClosed() bool {
@@ -103,11 +107,31 @@ func (this *BaseClientConn) SetUserId(userId int64) {
}
}
func (this *BaseClientConn) SetUserPlanId(userPlanId int64) {
this.userPlanId = userPlanId
// 设置包装前连接
switch conn := this.rawConn.(type) {
case *tls.Conn:
nativeConn, ok := conn.NetConn().(ClientConnInterface)
if ok {
nativeConn.SetUserPlanId(userPlanId)
}
case *ClientConn:
conn.SetUserPlanId(userPlanId)
}
}
// UserId 获取当前连接所属服务的用户ID
func (this *BaseClientConn) UserId() int64 {
return this.userId
}
// UserPlanId 用户套餐ID
func (this *BaseClientConn) UserPlanId() int64 {
return this.userPlanId
}
// RawIP 原本IP
func (this *BaseClientConn) RawIP() string {
if len(this.rawIP) > 0 {
@@ -160,3 +184,10 @@ func (this *BaseClientConn) SetFingerprint(fingerprint []byte) {
func (this *BaseClientConn) Fingerprint() []byte {
return this.fingerprint
}
// LastRequestBytes 读取上一次请求发送的字节数
func (this *BaseClientConn) LastRequestBytes() int64 {
var result = atomic.LoadInt64(&this.totalSentBytes)
atomic.StoreInt64(&this.totalSentBytes, 0)
return result
}

View File

@@ -18,9 +18,12 @@ type ClientConnInterface interface {
// SetServerId 设置服务ID
SetServerId(serverId int64) (goNext bool)
// SetUserId 设置所属服务的用户ID
// SetUserId 设置所属网站的用户ID
SetUserId(userId int64)
// SetUserPlanId 设置
SetUserPlanId(userPlanId int64)
// UserId 获取当前连接所属服务的用户ID
UserId() int64
@@ -32,4 +35,7 @@ type ClientConnInterface interface {
// Fingerprint 读取指纹信息
Fingerprint() []byte
// LastRequestBytes 读取上一次请求发送的字节数
LastRequestBytes() int64
}

View File

@@ -48,7 +48,7 @@ func (this *ClientListener) Accept() (net.Conn, error) {
firewalls.DropTemporaryTo(ip, expiresAt)
} else {
if !waf.SharedIPWhiteList.Contains(waf.IPTypeAll, firewallconfigs.FirewallScopeGlobal, 0, ip) {
var ok = false
var ok bool
expiresAt, ok = waf.SharedIPBlackList.ContainsExpires(waf.IPTypeAll, firewallconfigs.FirewallScopeGlobal, 0, ip)
if ok {
canGoNext = false

View File

@@ -82,3 +82,18 @@ func (this *ClientTLSConn) Fingerprint() []byte {
}
return nil
}
// LastRequestBytes 读取上一次请求发送的字节数
func (this *ClientTLSConn) LastRequestBytes() int64 {
tlsConn, ok := this.rawConn.(*tls.Conn)
if ok {
var rawConn = tlsConn.NetConn()
if rawConn != nil {
clientConn, ok := rawConn.(*ClientConn)
if ok {
return clientConn.LastRequestBytes()
}
}
}
return 0
}

View File

@@ -122,6 +122,7 @@ func TestHTTPAccessLogQueue_Memory(t *testing.T) {
}
runtime.GC()
_ = accessLogs
// will not release automatically
func() {
@@ -131,6 +132,7 @@ func TestHTTPAccessLogQueue_Memory(t *testing.T) {
RequestPath: "https://goedge.cn/hello/world",
})
}
_ = accessLogs1
}()
time.Sleep(5 * time.Second)

View File

@@ -6,7 +6,9 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeNode/internal/caches"
"github.com/TeaOSLab/EdgeNode/internal/compressions"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
@@ -19,8 +21,10 @@ import (
"io"
"net"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
)
@@ -41,9 +45,11 @@ var SharedHTTPCacheTaskManager = NewHTTPCacheTaskManager()
// HTTPCacheTaskManager 缓存任务管理
type HTTPCacheTaskManager struct {
ticker *time.Ticker
httpClient *http.Client
protocolReg *regexp.Regexp
timeoutClientMap map[time.Duration]*http.Client // timeout seconds=> *http.Client
locker sync.Mutex
taskQueue chan *pb.PurgeServerCacheRequest
}
@@ -52,36 +58,12 @@ func NewHTTPCacheTaskManager() *HTTPCacheTaskManager {
if Tea.IsTesting() {
duration = 10 * time.Second
}
return &HTTPCacheTaskManager{
ticker: time.NewTicker(duration),
httpClient: &http.Client{
Timeout: 10 * time.Minute, // TODO 可以设置请求超时时间
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
_, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
conn, err := net.Dial(network, "127.0.0.1:"+port)
if err != nil {
return nil, err
}
return connutils.NewNoStat(conn), nil
},
MaxIdleConns: 128,
MaxIdleConnsPerHost: 32,
MaxConnsPerHost: 32,
IdleConnTimeout: 2 * time.Minute,
ExpectContinueTimeout: 1 * time.Second,
TLSHandshakeTimeout: 0,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
protocolReg: regexp.MustCompile(`^(?i)(http|https)://`),
taskQueue: make(chan *pb.PurgeServerCacheRequest, 1024),
return &HTTPCacheTaskManager{
ticker: time.NewTicker(duration),
protocolReg: regexp.MustCompile(`^(?i)(http|https)://`),
taskQueue: make(chan *pb.PurgeServerCacheRequest, 1024),
timeoutClientMap: make(map[time.Duration]*http.Client),
}
}
@@ -131,21 +113,29 @@ func (this *HTTPCacheTaskManager) Loop() error {
var pbResults = []*pb.UpdateHTTPCacheTaskKeysStatusRequest_KeyResult{}
var taskGroup = goman.NewTaskGroup()
for _, key := range keys {
err = this.processKey(key)
var taskKey = key
taskGroup.Run(func() {
processErr := this.processKey(taskKey)
var pbResult = &pb.UpdateHTTPCacheTaskKeysStatusRequest_KeyResult{
Id: taskKey.Id,
NodeClusterId: taskKey.NodeClusterId,
Error: "",
}
var pbResult = &pb.UpdateHTTPCacheTaskKeysStatusRequest_KeyResult{
Id: key.Id,
NodeClusterId: key.NodeClusterId,
Error: "",
}
if processErr != nil {
pbResult.Error = processErr.Error()
}
if err != nil {
pbResult.Error = err.Error()
}
pbResults = append(pbResults, pbResult)
taskGroup.Lock()
pbResults = append(pbResults, pbResult)
taskGroup.Unlock()
})
}
taskGroup.Wait()
_, err = rpcClient.HTTPCacheTaskKeyRPC.UpdateHTTPCacheTaskKeysStatus(rpcClient.Context(), &pb.UpdateHTTPCacheTaskKeysStatusRequest{KeyResults: pbResults})
if err != nil {
return err
@@ -224,7 +214,6 @@ func (this *HTTPCacheTaskManager) processKey(key *pb.HTTPCacheTaskKey) error {
}
// TODO 增加失败重试
// TODO 使用并发操作
func (this *HTTPCacheTaskManager) fetchKey(key *pb.HTTPCacheTaskKey) error {
var fullKey = key.Key
if !this.protocolReg.MatchString(fullKey) {
@@ -233,29 +222,103 @@ func (this *HTTPCacheTaskManager) fetchKey(key *pb.HTTPCacheTaskKey) error {
req, err := http.NewRequest(http.MethodGet, fullKey, nil)
if err != nil {
return errors.New("invalid url: " + fullKey + ": " + err.Error())
return fmt.Errorf("invalid url: '%s': %w", fullKey, err)
}
// TODO 可以在管理界面自定义Header
req.Header.Set("X-Edge-Cache-Action", "fetch")
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36") // TODO 可以定义
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
resp, err := this.httpClient.Do(req)
resp, err := this.httpClient().Do(req)
if err != nil {
return errors.New("request failed: " + fullKey + ": " + err.Error())
err = this.simplifyErr(err)
return fmt.Errorf("request failed: '%s': %w", fullKey, err)
}
defer func() {
_ = resp.Body.Close()
}()
// 读取内容,以便于生成缓存
_, _ = io.Copy(io.Discard, resp.Body)
// 处理502
if resp.StatusCode == http.StatusBadGateway {
return errors.New("read origin site timeout")
}
// 读取内容,以便于生成缓存
_, err = io.Copy(io.Discard, resp.Body)
if err != nil {
if err != io.EOF {
err = this.simplifyErr(err)
return fmt.Errorf("request failed: '%s': %w", fullKey, err)
} else {
err = nil
}
}
return nil
}
func (this *HTTPCacheTaskManager) simplifyErr(err error) error {
if err == nil {
return nil
}
if os.IsTimeout(err) {
return errors.New("timeout to read origin site")
}
return err
}
func (this *HTTPCacheTaskManager) httpClient() *http.Client {
var timeout = serverconfigs.DefaultHTTPCachePolicyFetchTimeout
var nodeConfig = sharedNodeConfig // copy
if nodeConfig != nil {
var cachePolicies = nodeConfig.HTTPCachePolicies // copy
if len(cachePolicies) > 0 && cachePolicies[0].FetchTimeout != nil && cachePolicies[0].FetchTimeout.Count > 0 {
var fetchTimeout = cachePolicies[0].FetchTimeout.Duration()
if fetchTimeout > 0 {
timeout = fetchTimeout
}
}
}
this.locker.Lock()
defer this.locker.Unlock()
client, ok := this.timeoutClientMap[timeout]
if ok {
return client
}
client = &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
_, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
conn, err := net.Dial(network, "127.0.0.1:"+port)
if err != nil {
return nil, err
}
return connutils.NewNoStat(conn), nil
},
MaxIdleConns: 128,
MaxIdleConnsPerHost: 32,
MaxConnsPerHost: 32,
IdleConnTimeout: 2 * time.Minute,
ExpectContinueTimeout: 1 * time.Second,
TLSHandshakeTimeout: 0,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
this.timeoutClientMap[timeout] = client
return client
}

View File

@@ -6,7 +6,7 @@ import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeNode/internal/goman"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/pires/go-proxyproto"
"golang.org/x/net/http2"
"net"
@@ -21,6 +21,8 @@ import (
// SharedHTTPClientPool HTTP客户端池单例
var SharedHTTPClientPool = NewHTTPClientPool()
const httpClientProxyProtocolTag = "@ProxyProtocol@"
// HTTPClientPool 客户端池
type HTTPClientPool struct {
clientsMap map[string]*HTTPClient // backend key => client
@@ -55,6 +57,14 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
}
var key = origin.UniqueKey() + "@" + originAddr
// if we are under available ProxyProtocol, we add client ip to key to make every client unique
var isProxyProtocol = false
if proxyProtocol != nil && proxyProtocol.IsOn {
key += httpClientProxyProtocolTag + req.requestRemoteAddr(true)
isProxyProtocol = true
}
var isLnRequest = origin.Id == 0
this.locker.RLock()
@@ -103,8 +113,9 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
idleConns = numberCPU * 16
}
// 可以判断为Ln节点请求
if isLnRequest {
if isProxyProtocol { // ProxyProtocol无需保持太多空闲连接
idleConns = 3
} else if isLnRequest { // 可以判断为Ln节点请求
maxConnections *= 8
idleConns *= 8
idleTimeout *= 4
@@ -132,14 +143,8 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
var transport = &HTTPClientTransport{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 支持TOA的连接
conn, err := this.handleTOA(req, ctx, network, originAddr, connectionTimeout)
if conn != nil || err != nil {
return conn, err
}
// 普通的连接
conn, err = (&net.Dialer{
conn, err := (&net.Dialer{
Timeout: connectionTimeout,
KeepAlive: 1 * time.Minute,
}).DialContext(ctx, network, originAddr)
@@ -153,7 +158,7 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
return nil, err
}
return conn, nil
return NewOriginConn(conn), nil
},
MaxIdleConns: 0,
MaxIdleConnsPerHost: idleConns,
@@ -202,54 +207,45 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
// 清理不使用的Client
func (this *HTTPClientPool) cleanClients() {
for range this.cleanTicker.C {
var nowTime = time.Now().Unix()
var nowTime = fasttime.Now().Unix()
this.locker.Lock()
var expiredKeys = []string{}
var expiredClients = []*HTTPClient{}
// lookup expired clients
this.locker.RLock()
for k, client := range this.clientsMap {
if client.AccessTime() < nowTime+86400 { // 超过 N 秒没有调用就关闭
if client.AccessTime() < nowTime-86400 ||
(strings.Contains(k, httpClientProxyProtocolTag) && client.AccessTime() < nowTime-3600) { // 超过 N 秒没有调用就关闭
expiredKeys = append(expiredKeys, k)
expiredClients = append(expiredClients, client)
}
}
this.locker.RUnlock()
// remove expired keys
if len(expiredKeys) > 0 {
this.locker.Lock()
for _, k := range expiredKeys {
delete(this.clientsMap, k)
}
this.locker.Unlock()
}
// close expired clients
if len(expiredClients) > 0 {
for _, client := range expiredClients {
client.Close()
}
}
this.locker.Unlock()
}
}
// 支持TOA
func (this *HTTPClientPool) handleTOA(req *HTTPRequest, ctx context.Context, network string, originAddr string, connectionTimeout time.Duration) (net.Conn, error) {
// TODO 每个服务读取自身所属集群的TOA设置
var toaConfig = sharedTOAManager.Config()
if toaConfig != nil && toaConfig.IsOn {
var retries = 3
for i := 1; i <= retries; i++ {
var port = int(toaConfig.RandLocalPort())
// TODO 思考是否支持X-Real-IP/X-Forwarded-IP
err := sharedTOAManager.SendMsg("add:" + strconv.Itoa(port) + ":" + req.requestRemoteAddr(true))
if err != nil {
remotelogs.Error("TOA", "add failed: "+err.Error())
} else {
dialer := net.Dialer{
Timeout: connectionTimeout,
KeepAlive: 1 * time.Minute,
LocalAddr: &net.TCPAddr{
Port: port,
},
}
conn, err := dialer.DialContext(ctx, network, originAddr)
// TODO 需要在合适的时机删除TOA记录
if err == nil || i == retries {
return conn, err
}
}
}
}
return nil, nil
}
// 支持PROXY Protocol
func (this *HTTPClientPool) handlePROXYProtocol(conn net.Conn, req *HTTPRequest, proxyProtocol *serverconfigs.ProxyProtocolConfig) error {
if proxyProtocol != nil && proxyProtocol.IsOn && (proxyProtocol.Version == serverconfigs.ProxyProtocolVersion1 || proxyProtocol.Version == serverconfigs.ProxyProtocolVersion2) {
if proxyProtocol != nil &&
proxyProtocol.IsOn &&
(proxyProtocol.Version == serverconfigs.ProxyProtocolVersion1 || proxyProtocol.Version == serverconfigs.ProxyProtocolVersion2) {
var remoteAddr = req.requestRemoteAddr(true)
var transportProtocol = proxyproto.TCPv4
if strings.Contains(remoteAddr, ":") {

View File

@@ -1,6 +1,7 @@
package nodes
import (
"context"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"runtime"
"testing"
@@ -16,7 +17,7 @@ func TestHTTPClientPool_Client(t *testing.T) {
Version: 2,
Addr: &serverconfigs.NetworkAddressConfig{Host: "127.0.0.1", PortRange: "1234"},
}
err := origin.Init(nil)
err := origin.Init(context.Background())
if err != nil {
t.Fatal(err)
}
@@ -43,7 +44,7 @@ func TestHTTPClientPool_cleanClients(t *testing.T) {
Version: 2,
Addr: &serverconfigs.NetworkAddressConfig{Host: "127.0.0.1", PortRange: "1234"},
}
err := origin.Init(nil)
err := origin.Init(context.Background())
if err != nil {
t.Fatal(err)
}
@@ -65,7 +66,7 @@ func BenchmarkHTTPClientPool_Client(b *testing.B) {
Version: 2,
Addr: &serverconfigs.NetworkAddressConfig{Host: "127.0.0.1", PortRange: "1234"},
}
err := origin.Init(nil)
err := origin.Init(context.Background())
if err != nil {
b.Fatal(err)
}

View File

@@ -38,15 +38,16 @@ type HTTPRequest struct {
requestId string
// 外部参数
RawReq *http.Request
RawWriter http.ResponseWriter
ReqServer *serverconfigs.ServerConfig
ReqHost string // 请求的Host
ServerName string // 实际匹配到的Host
ServerAddr string // 实际启动的服务器监听地址
IsHTTP bool
IsHTTPS bool
IsHTTP3 bool
RawReq *http.Request
RawWriter http.ResponseWriter
ReqServer *serverconfigs.ServerConfig
ReqHost string // 请求的Host
ServerName string // 实际匹配到的Host
ServerAddr string // 实际启动的服务器监听地址
IsHTTP bool
IsHTTPS bool
IsHTTP3 bool
isHealthCheck bool
// 共享参数
nodeConfig *nodeconfigs.NodeConfig
@@ -168,10 +169,9 @@ func (this *HTTPRequest) Do() {
}
// 处理健康检查
var isHealthCheck = false
var healthCheckKey = this.RawReq.Header.Get(serverconfigs.HealthCheckHeaderName)
if len(healthCheckKey) > 0 {
if this.doHealthCheck(healthCheckKey, &isHealthCheck) {
if this.doHealthCheck(healthCheckKey, &this.isHealthCheck) {
this.doEnd()
return
}
@@ -198,14 +198,22 @@ func (this *HTTPRequest) Do() {
}
// 流量限制
if this.ReqServer.TrafficLimit != nil && this.ReqServer.TrafficLimit.IsOn && !this.ReqServer.TrafficLimit.IsEmpty() && this.ReqServer.TrafficLimitStatus != nil && this.ReqServer.TrafficLimitStatus.IsValid() {
if this.ReqServer.TrafficLimitStatus != nil && this.ReqServer.TrafficLimitStatus.IsValid() {
this.doTrafficLimit()
this.doEnd()
return
}
// WAF
if this.web.FirewallRef != nil && this.web.FirewallRef.IsOn {
if this.doWAFRequest() {
this.doEnd()
return
}
}
// UAM
if !isHealthCheck {
if !this.isHealthCheck {
if this.web.UAM != nil {
if this.web.UAM.IsOn {
if this.doUAM() {
@@ -223,7 +231,7 @@ func (this *HTTPRequest) Do() {
}
// CC
if !isHealthCheck {
if !this.isHealthCheck {
if this.web.CC != nil {
if this.web.CC.IsOn {
if this.doCC() {
@@ -234,14 +242,6 @@ func (this *HTTPRequest) Do() {
}
}
// WAF
if this.web.FirewallRef != nil && this.web.FirewallRef.IsOn {
if this.doWAFRequest() {
this.doEnd()
return
}
}
// 防盗链
if !this.isSubRequest && this.web.Referers != nil && this.web.Referers.IsOn {
if this.doCheckReferers() {
@@ -388,7 +388,22 @@ func (this *HTTPRequest) doEnd() {
// 流量统计
// TODO 增加是否开启开关
if this.ReqServer != nil && this.ReqServer.Id > 0 {
if this.ReqServer != nil && this.ReqServer.Id > 0 && !this.isHealthCheck /** 健康检查时不统计 **/ {
var totalBytes int64 = 0
var requestConn = this.RawReq.Context().Value(HTTPConnContextKey)
if requestConn != nil {
requestClientConn, ok := requestConn.(ClientConnInterface)
if ok {
// 这里读取的其实是上一个请求消耗的流量,不是当前请求消耗的流量,只不过单个请求的流量统计不需要特别精确,整体趋于一致即可
totalBytes = requestClientConn.LastRequestBytes()
}
}
if totalBytes == 0 {
totalBytes = this.writer.SentBodyBytes() + this.writer.SentHeaderBytes()
}
var countCached int64 = 0
var cachedBytes int64 = 0
@@ -397,14 +412,17 @@ func (this *HTTPRequest) doEnd() {
if this.isCached {
countCached = 1
cachedBytes = this.writer.SentBodyBytes() + this.writer.SentHeaderBytes()
cachedBytes = totalBytes
}
if this.isAttack {
countAttacks = 1
attackBytes = this.CalculateSize()
if attackBytes < totalBytes {
attackBytes = totalBytes
}
}
stats.SharedTrafficStatManager.Add(this.ReqServer.UserId, this.ReqServer.Id, this.ReqHost, this.writer.SentBodyBytes()+this.writer.SentHeaderBytes(), cachedBytes, 1, countCached, countAttacks, attackBytes, this.ReqServer.ShouldCheckTrafficLimit(), this.ReqServer.PlanId())
stats.SharedTrafficStatManager.Add(this.ReqServer.UserId, this.ReqServer.Id, this.ReqHost, totalBytes, cachedBytes, 1, countCached, countAttacks, attackBytes, this.ReqServer.ShouldCheckTrafficLimit(), this.ReqServer.PlanId())
// 指标
if metrics.SharedManager.HasHTTPMetrics() {
@@ -836,6 +854,24 @@ func (this *HTTPRequest) Format(source string) string {
return this.requestHeadersString()
case "serverName":
return this.ServerName
case "serverAddr":
var nodeConfig = this.nodeConfig
if nodeConfig != nil && nodeConfig.GlobalServerConfig != nil && nodeConfig.GlobalServerConfig.HTTPAll.EnableServerAddrVariable {
if len(this.requestRemoteAddrs()) > 1 {
return "" // hidden for security
}
var requestConn = this.RawReq.Context().Value(HTTPConnContextKey)
if requestConn != nil {
conn, ok := requestConn.(net.Conn)
if ok {
host, _, _ := net.SplitHostPort(conn.LocalAddr().String())
if len(host) > 0 {
return host
}
}
}
}
return ""
case "serverPort":
return strconv.Itoa(this.requestServerPort())
case "hostname":
@@ -1135,10 +1171,32 @@ func (this *HTTPRequest) requestRemoteAddr(supportVar bool) string {
this.web.RemoteAddr != nil &&
this.web.RemoteAddr.IsOn &&
!this.web.RemoteAddr.IsEmpty() {
var remoteAddr = this.Format(this.web.RemoteAddr.Value)
if net.ParseIP(remoteAddr) != nil {
this.remoteAddr = remoteAddr
return remoteAddr
if this.web.RemoteAddr.HasValues() { // multiple values
for _, value := range this.web.RemoteAddr.Values() {
var remoteAddr = this.Format(value)
if len(remoteAddr) > 0 && net.ParseIP(remoteAddr) != nil {
this.remoteAddr = remoteAddr
return remoteAddr
}
}
} else { // single value
var remoteAddr = this.Format(this.web.RemoteAddr.Value)
if len(remoteAddr) > 0 && net.ParseIP(remoteAddr) != nil {
this.remoteAddr = remoteAddr
return remoteAddr
}
}
// 如果是从Header中读取则直接返回原始IP
if this.web.RemoteAddr.Type == serverconfigs.HTTPRemoteAddrTypeRequestHeader {
var remoteAddr = this.RawReq.RemoteAddr
host, _, err := net.SplitHostPort(remoteAddr)
if err == nil {
this.remoteAddr = host
return host
} else {
return remoteAddr
}
}
}
@@ -1204,7 +1262,7 @@ func (this *HTTPRequest) requestRemoteAddrs() (result []string) {
var forwardedFor = this.RawReq.Header.Get("X-Forwarded-For")
if len(forwardedFor) > 0 {
commaIndex := strings.Index(forwardedFor, ",")
if commaIndex > 0 {
if commaIndex > 0 && !lists.ContainsString(result, forwardedFor[:commaIndex]) {
result = append(result, forwardedFor[:commaIndex])
}
}
@@ -1212,7 +1270,7 @@ func (this *HTTPRequest) requestRemoteAddrs() (result []string) {
// Real-IP
{
realIP, ok := this.RawReq.Header["X-Real-IP"]
if ok && len(realIP) > 0 {
if ok && len(realIP) > 0 && !lists.ContainsString(result, realIP[0]) {
result = append(result, realIP[0])
}
}
@@ -1220,7 +1278,7 @@ func (this *HTTPRequest) requestRemoteAddrs() (result []string) {
// Real-Ip
{
realIP, ok := this.RawReq.Header["X-Real-Ip"]
if ok && len(realIP) > 0 {
if ok && len(realIP) > 0 && !lists.ContainsString(result, realIP[0]) {
result = append(result, realIP[0])
}
}
@@ -1230,7 +1288,9 @@ func (this *HTTPRequest) requestRemoteAddrs() (result []string) {
var remoteAddr = this.RawReq.RemoteAddr
host, _, err := net.SplitHostPort(remoteAddr)
if err == nil {
result = append(result, host)
if !lists.ContainsString(result, host) {
result = append(result, host)
}
} else {
result = append(result, remoteAddr)
}
@@ -1547,10 +1607,10 @@ func (this *HTTPRequest) setForwardHeaders(header http.Header) {
this.RawReq.Header.Set("Connection", "keep-alive")
}
remoteAddr := this.RawReq.RemoteAddr
host, _, err := net.SplitHostPort(remoteAddr)
var rawRemoteAddr = this.RawReq.RemoteAddr
host, _, err := net.SplitHostPort(rawRemoteAddr)
if err == nil {
remoteAddr = host
rawRemoteAddr = host
}
// x-real-ip
@@ -1558,24 +1618,24 @@ func (this *HTTPRequest) setForwardHeaders(header http.Header) {
_, ok1 := header["X-Real-IP"]
_, ok2 := header["X-Real-Ip"]
if !ok1 && !ok2 {
header["X-Real-IP"] = []string{remoteAddr}
header["X-Real-IP"] = []string{this.requestRemoteAddr(true)}
}
}
// X-Forwarded-For
if this.reverseProxy != nil && this.reverseProxy.ShouldAddXForwardedForHeader() {
forwardedFor, ok := header["X-Forwarded-For"]
if ok {
if ok && len(forwardedFor) > 0 { // already exists
_, hasForwardHeader := this.RawReq.Header["X-Forwarded-For"]
if hasForwardHeader {
header["X-Forwarded-For"] = []string{strings.Join(forwardedFor, ", ") + ", " + remoteAddr}
header["X-Forwarded-For"] = []string{strings.Join(forwardedFor, ", ") + ", " + rawRemoteAddr}
}
} else {
var clientRemoteAddr = this.requestRemoteAddr(true)
if len(clientRemoteAddr) > 0 && clientRemoteAddr != remoteAddr {
header["X-Forwarded-For"] = []string{clientRemoteAddr + ", " + remoteAddr}
if len(clientRemoteAddr) > 0 && clientRemoteAddr != rawRemoteAddr {
header["X-Forwarded-For"] = []string{clientRemoteAddr + ", " + rawRemoteAddr}
} else {
header["X-Forwarded-For"] = []string{remoteAddr}
header["X-Forwarded-For"] = []string{rawRemoteAddr}
}
}
}

View File

@@ -38,8 +38,13 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
// 添加 X-Cache Header
var addStatusHeader = this.web.Cache.AddStatusHeader
var cacheBypassDescription = ""
if addStatusHeader {
defer func() {
if len(cacheBypassDescription) > 0 {
this.writer.Header().Set("X-Cache", cacheBypassDescription)
return
}
var cacheStatus = this.varMapping["cache.status"]
if cacheStatus != "HIT" {
this.writer.Header().Set("X-Cache", cacheStatus)
@@ -85,6 +90,13 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
return
}
// 是否强制Range回源
if this.cacheRef.AlwaysForwardRangeRequest && len(this.RawReq.Header.Get("Range")) > 0 {
this.cacheRef = nil
cacheBypassDescription = "BYPASS, forward range"
return
}
// 是否正在Purge
var isPurging = this.web.Cache.PurgeIsOn && strings.ToUpper(this.RawReq.Method) == "PURGE" && this.RawReq.Header.Get("X-Edge-Purge-Key") == this.web.Cache.PurgeKey
if isPurging {
@@ -94,6 +106,7 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
// 校验请求
if !this.cacheRef.MatchRequest(this.RawReq) {
this.cacheRef = nil
cacheBypassDescription = "BYPASS, not match"
return
}
@@ -106,6 +119,7 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
if this.cacheRef.EnableRequestCachePragma {
if this.RawReq.Header.Get("Cache-Control") == "no-cache" || this.RawReq.Header.Get("Pragma") == "no-cache" {
this.cacheRef = nil
cacheBypassDescription = "BYPASS, Cache-Control or Pragma"
return
}
}
@@ -119,6 +133,7 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
var key = this.Format(this.cacheRef.Key)
if len(key) == 0 {
this.cacheRef = nil
cacheBypassDescription = "BYPASS, empty key"
return
}
var method = this.Method()
@@ -134,6 +149,7 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
var storage = caches.SharedManager.FindStorageWithPolicy(cachePolicy.Id)
if storage == nil {
this.cacheRef = nil
cacheBypassDescription = "BYPASS, no policy found"
return
}
this.writer.cacheStorage = storage

View File

@@ -9,16 +9,20 @@ import (
)
const httpStatusPageTemplate = `<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>${status} ${statusMessage}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
address { line-height: 1.8; }
</style>
</head>
<body>
<h1>${status} ${statusMessage}</h1>
<p>${message}</p>
<address>Connection: ${remoteAddr} (Client) -&gt; ${serverAddr} (Server)</address>
<address>Request ID: ${requestId}.</address>
</body>
@@ -39,8 +43,6 @@ func (this *HTTPRequest) writeCode(statusCode int, enMessage string, zhMessage s
return types.String(statusCode)
case "statusMessage":
return http.StatusText(statusCode)
case "requestId":
return this.requestId
case "message":
var acceptLanguages = this.RawReq.Header.Get("Accept-Language")
if len(acceptLanguages) > 0 {
@@ -54,7 +56,7 @@ func (this *HTTPRequest) writeCode(statusCode int, enMessage string, zhMessage s
}
return enMessage
}
return "${" + varName + "}"
return this.Format("${" + varName + "}")
})
this.ProcessResponseHeaders(this.writer.Header(), statusCode)
@@ -107,7 +109,7 @@ func (this *HTTPRequest) write50x(err error, statusCode int, enMessage string, z
}
return "The site is unavailable now, cause: " + enMessage + "."
}
return "${" + varName + "}"
return this.Format("${" + varName + "}")
})
this.ProcessResponseHeaders(this.writer.Header(), statusCode)

View File

@@ -111,7 +111,7 @@ func (this *HTTPRequest) doFastcgi() (shouldStop bool) {
// 处理SCRIPT_FILENAME
scriptPath := env.GetString("SCRIPT_FILENAME")
if len(scriptPath) > 0 && (strings.Index(scriptPath, "/") < 0 && strings.Index(scriptPath, "\\") < 0) {
if len(scriptPath) > 0 && !strings.Contains(scriptPath, "/") && !strings.Contains(scriptPath, "\\") {
env["SCRIPT_FILENAME"] = env.GetString("DOCUMENT_ROOT") + Tea.DS + scriptPath
}
scriptFilename := filepath.Base(this.RawReq.URL.Path)

View File

@@ -34,7 +34,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
}
var fullURL = ""
var fullURL string
if u.BeforeHasQuery() {
fullURL = this.URL()
} else {
@@ -55,7 +55,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
this.ProcessResponseHeaders(this.writer.Header(), status)
http.Redirect(this.RawWriter, this.RawReq, afterURL, status)
httpRedirect(this.writer, this.RawReq, afterURL, status)
return true
}
} else if u.MatchRegexp { // 正则匹配
@@ -97,7 +97,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
this.ProcessResponseHeaders(this.writer.Header(), status)
http.Redirect(this.RawWriter, this.RawReq, afterURL, status)
httpRedirect(this.writer, this.RawReq, afterURL, status)
return true
} else { // 精准匹配
if fullURL == u.RealBeforeURL() {
@@ -120,7 +120,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
this.ProcessResponseHeaders(this.writer.Header(), status)
http.Redirect(this.RawWriter, this.RawReq, afterURL, status)
httpRedirect(this.writer, this.RawReq, afterURL, status)
return true
}
}
@@ -139,11 +139,6 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
}
// 如果跳转前后域名一致,则终止
if u.DomainAfter == reqHost {
return false
}
var scheme = u.DomainAfterScheme
if len(scheme) == 0 {
scheme = this.requestScheme()
@@ -155,6 +150,11 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
return false
}
// 如果跳转前后域名一致,则终止
if u.DomainAfter == reqHost {
return false
}
this.ProcessResponseHeaders(this.writer.Header(), status)
// 参数
@@ -163,7 +163,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
afterURL += this.uri[qIndex:]
}
http.Redirect(this.RawWriter, this.RawReq, afterURL, status)
httpRedirect(this.writer, this.RawReq, afterURL, status)
return true
}
} else if u.Type == serverconfigs.HTTPHostRedirectTypePort {
@@ -194,7 +194,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
return false
}
var containsPort = false
var containsPort bool
if u.PortsAll {
containsPort = true
} else {
@@ -212,7 +212,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
}
this.ProcessResponseHeaders(this.writer.Header(), status)
http.Redirect(this.RawWriter, this.RawReq, afterURL, status)
httpRedirect(this.writer, this.RawReq, afterURL, status)
return true
}
}

View File

@@ -20,6 +20,6 @@ func (this *HTTPRequest) checkLnRequest() bool {
return false
}
func (this *HTTPRequest) getLnOrigin(excludingNodeIds []int64) (originConfig *serverconfigs.OriginConfig, lnNodeId int64, hasMultipleNodes bool) {
func (this *HTTPRequest) getLnOrigin(excludingNodeIds []int64, urlHash uint64) (originConfig *serverconfigs.OriginConfig, lnNodeId int64, hasMultipleNodes bool) {
return nil, 0, false
}

View File

@@ -7,8 +7,9 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeNode/internal/ttlcache"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/waf"
"net"
"github.com/iwind/TeaGo/types"
"net/http"
"time"
)
@@ -35,9 +36,25 @@ func (this *HTTPRequest) doMismatch() {
// 根据配置进行相应的处理
var globalServerConfig = sharedNodeConfig.GlobalServerConfig
if globalServerConfig != nil && globalServerConfig.HTTPAll.MatchDomainStrictly {
var statusCode = 404
var httpAllConfig = globalServerConfig.HTTPAll
var mismatchAction = httpAllConfig.DomainMismatchAction
if mismatchAction != nil && mismatchAction.Options != nil {
var mismatchStatusCode = mismatchAction.Options.GetInt("statusCode")
if mismatchStatusCode > 0 && mismatchStatusCode >= 100 && mismatchStatusCode < 1000 {
statusCode = mismatchStatusCode
}
}
// 是否正在访问IP
if globalServerConfig.HTTPAll.NodeIPShowPage && net.ParseIP(this.ReqHost) != nil {
_, _ = this.writer.WriteString(globalServerConfig.HTTPAll.NodeIPPageHTML)
if globalServerConfig.HTTPAll.NodeIPShowPage && utils.IsWildIP(this.ReqHost) {
this.writer.statusCode = statusCode
var contentHTML = this.Format(globalServerConfig.HTTPAll.NodeIPPageHTML)
this.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
this.writer.Header().Set("Content-Length", types.String(len(contentHTML)))
this.writer.WriteHeader(statusCode)
_, _ = this.writer.WriteString(contentHTML)
return
}
@@ -46,7 +63,7 @@ func (this *HTTPRequest) doMismatch() {
// 要考虑到服务在切换集群时,域名未生效状态时,用户访问的仍然是老集群中的节点,就会产生找不到域名的情况
if len(remoteIP) > 0 {
const maxAttempts = 100
if ttlcache.SharedCache.IncreaseInt64("MISMATCH_DOMAIN:"+remoteIP, int64(1), time.Now().Unix()+60, false) > maxAttempts {
if ttlcache.SharedInt64Cache.IncreaseInt64("MISMATCH_DOMAIN:"+remoteIP, int64(1), time.Now().Unix()+60, false) > maxAttempts {
// 在加入之前再次检查黑名单
if !waf.SharedIPBlackList.Contains(waf.IPTypeAll, firewallconfigs.FirewallScopeGlobal, 0, remoteIP) {
waf.SharedIPBlackList.Add(waf.IPTypeAll, firewallconfigs.FirewallScopeGlobal, 0, remoteIP, time.Now().Unix()+3600)
@@ -55,13 +72,14 @@ func (this *HTTPRequest) doMismatch() {
}
// 处理当前连接
var httpAllConfig = globalServerConfig.HTTPAll
var mismatchAction = httpAllConfig.DomainMismatchAction
if mismatchAction != nil && mismatchAction.Code == "page" {
if mismatchAction != nil && mismatchAction.Code == serverconfigs.DomainMismatchActionPage {
if mismatchAction.Options != nil {
this.writer.statusCode = statusCode
var contentHTML = this.Format(mismatchAction.Options.GetString("contentHTML"))
this.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
this.writer.WriteHeader(mismatchAction.Options.GetInt("statusCode"))
_, _ = this.writer.Write([]byte(mismatchAction.Options.GetString("contentHTML")))
this.writer.Header().Set("Content-Length", types.String(len(contentHTML)))
this.writer.WriteHeader(statusCode)
_, _ = this.writer.Write([]byte(contentHTML))
} else {
http.Error(this.writer, "404 page not found: '"+this.URL()+"'", http.StatusNotFound)
}

View File

@@ -12,6 +12,8 @@ import (
"strings"
)
const defaultPageContentType = "text/html; charset=utf-8"
// 请求特殊页面
func (this *HTTPRequest) doPage(status int) (shouldStop bool) {
if len(this.web.Pages) == 0 {
@@ -58,6 +60,7 @@ func (this *HTTPRequest) doPageLookup(pages []*serverconfigs.HTTPPageConfig, sta
var realpath = path.Clean(page.URL)
if !strings.HasPrefix(realpath, "/pages/") && !strings.HasPrefix(realpath, "pages/") { // only files under "/pages/" can be used
var msg = "404 page not found: '" + page.URL + "'"
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.writer.WriteHeader(http.StatusNotFound)
_, _ = this.writer.Write([]byte(msg))
return true
@@ -66,6 +69,7 @@ func (this *HTTPRequest) doPageLookup(pages []*serverconfigs.HTTPPageConfig, sta
fp, err := os.Open(file)
if err != nil {
var msg = "404 page not found: '" + page.URL + "'"
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.writer.WriteHeader(http.StatusNotFound)
_, _ = this.writer.Write([]byte(msg))
return true
@@ -77,6 +81,7 @@ func (this *HTTPRequest) doPageLookup(pages []*serverconfigs.HTTPPageConfig, sta
stat, err := fp.Stat()
if err != nil {
var msg = "404 could not read page content: '" + page.URL + "'"
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.writer.WriteHeader(http.StatusNotFound)
_, _ = this.writer.Write([]byte(msg))
return true
@@ -85,10 +90,12 @@ func (this *HTTPRequest) doPageLookup(pages []*serverconfigs.HTTPPageConfig, sta
// 修改状态码
if page.NewStatus > 0 {
// 自定义响应Headers
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.ProcessResponseHeaders(this.writer.Header(), page.NewStatus)
this.writer.Prepare(nil, stat.Size(), page.NewStatus, true)
this.writer.WriteHeader(page.NewStatus)
} else {
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.ProcessResponseHeaders(this.writer.Header(), status)
this.writer.Prepare(nil, stat.Size(), status, true)
this.writer.WriteHeader(status)
@@ -120,10 +127,12 @@ func (this *HTTPRequest) doPageLookup(pages []*serverconfigs.HTTPPageConfig, sta
// 修改状态码
if page.NewStatus > 0 {
// 自定义响应Headers
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.ProcessResponseHeaders(this.writer.Header(), page.NewStatus)
this.writer.Prepare(nil, int64(len(content)), page.NewStatus, true)
this.writer.WriteHeader(page.NewStatus)
} else {
this.writer.Header().Set("Content-Type", defaultPageContentType)
this.ProcessResponseHeaders(this.writer.Header(), status)
this.writer.Prepare(nil, int64(len(content)), status, true)
this.writer.WriteHeader(status)

View File

@@ -43,7 +43,7 @@ func (this *HTTPRequest) doRedirectToHTTPS(redirectToHTTPSConfig *serverconfigs.
var newURL = "https://" + host + this.RawReq.RequestURI
this.ProcessResponseHeaders(this.writer.Header(), statusCode)
http.Redirect(this.writer, this.RawReq, newURL, statusCode)
httpRedirect(this.writer, this.RawReq, newURL, statusCode)
return true
}

View File

@@ -7,6 +7,8 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/fnv"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
@@ -21,15 +23,13 @@ func (this *HTTPRequest) doReverseProxy() {
return
}
var isLowVersionHTTP = this.RawReq.ProtoMajor < 1 /** 0.x **/ || (this.RawReq.ProtoMajor == 1 && this.RawReq.ProtoMinor == 0 /** 1.0 **/)
var retries = 3
var failedOriginIds []int64
var failedLnNodeIds []int64
for i := 0; i < retries; i++ {
originId, lnNodeId, shouldRetry := this.doOriginRequest(failedOriginIds, failedLnNodeIds, i == 0, i == retries-1, isLowVersionHTTP)
originId, lnNodeId, shouldRetry := this.doOriginRequest(failedOriginIds, failedLnNodeIds, i == 0, i == retries-1)
if !shouldRetry {
break
}
@@ -43,7 +43,7 @@ func (this *HTTPRequest) doReverseProxy() {
}
// 请求源站
func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeIds []int64, isFirstTry bool, isLastRetry bool, isLowVersionHTTP bool) (originId int64, lnNodeId int64, shouldRetry bool) {
func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeIds []int64, isFirstTry bool, isLastRetry bool) (originId int64, lnNodeId int64, shouldRetry bool) {
// 对URL的处理
var stripPrefix = this.reverseProxy.StripPrefix
var requestURI = this.reverseProxy.RequestURI
@@ -67,7 +67,7 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
// 二级节点
var hasMultipleLnNodes = false
if this.cacheRef != nil || (this.nodeConfig != nil && this.nodeConfig.GlobalServerConfig != nil && this.nodeConfig.GlobalServerConfig.HTTPAll.ForceLnRequest) {
origin, lnNodeId, hasMultipleLnNodes = this.getLnOrigin(failedLnNodeIds)
origin, lnNodeId, hasMultipleLnNodes = this.getLnOrigin(failedLnNodeIds, fnv.HashString(this.URL()))
if origin != nil {
// 强制变更原来访问的域名
requestHost = this.ReqHost
@@ -258,6 +258,7 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
}
var resp *http.Response
var respBodyIsClosed bool
var requestErr error
var requestErrCode string
if isHTTPOrigin { // 普通HTTP(S)源站
@@ -296,9 +297,19 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
this.writeCode(http.StatusBadGateway, "The type of origin site has not been supported", "设置的源站类型尚未支持")
return
}
if resp != nil && resp.Body != nil {
defer func() {
if !respBodyIsClosed {
_ = resp.Body.Close()
}
}()
}
if requestErr != nil {
// 客户端取消请求,则不提示
httpErr, ok := requestErr.(*url.Error)
var httpErr *url.Error
ok := errors.As(requestErr, &httpErr)
if !ok {
if isHTTPOrigin {
SharedOriginStateManager.Fail(origin, requestHost, this.reverseProxy, func() {
@@ -312,7 +323,7 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
this.write50x(requestErr, http.StatusBadGateway, "Failed to read origin site", "源站读取失败", true)
}
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.RawReq.URL.String()+": Request origin server failed: "+requestErr.Error())
} else if httpErr.Err != context.Canceled {
} else if !errors.Is(httpErr, context.Canceled) {
if isHTTPOrigin {
SharedOriginStateManager.Fail(origin, requestHost, this.reverseProxy, func() {
this.reverseProxy.ResetScheduling()
@@ -324,11 +335,7 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
shouldRetry = true
this.uri = oldURI // 恢复备份
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if httpErr.Err != io.EOF {
if httpErr.Err != io.EOF && !errors.Is(httpErr.Err, http.ErrBodyReadAfterClose) {
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.URL()+": Request origin server failed: "+requestErr.Error())
}
@@ -342,14 +349,14 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
} else {
this.write50x(requestErr, http.StatusBadGateway, "Failed to read origin site", "源站读取失败", true)
}
if httpErr.Err != io.EOF {
if httpErr.Err != io.EOF && !errors.Is(httpErr.Err, http.ErrBodyReadAfterClose) {
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.URL()+": Request origin server failed: "+requestErr.Error())
}
} else {
// 是否为客户端方面的错误
var isClientError = false
if ok {
if httpErr.Err == context.Canceled {
if errors.Is(httpErr, context.Canceled) {
// 如果是服务器端主动关闭,则无需提示
if this.isConnClosed() {
this.disableLog = true
@@ -366,22 +373,38 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
this.write50x(requestErr, http.StatusBadGateway, "Failed to read origin site", "源站读取失败", true)
}
}
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
return
}
// 是否为1.1以下
if isLowVersionHTTP && resp.ContentLength < 0 {
this.writer.WriteHeader(http.StatusBadRequest)
_, _ = this.writer.WriteString("The content does not support " + this.RawReq.Proto + " request.")
// 50x
if resp != nil &&
resp.StatusCode >= 500 &&
resp.StatusCode < 510 &&
this.reverseProxy.Retry50X &&
(originId > 0 || (lnNodeId > 0 && hasMultipleLnNodes)) &&
!isLastRetry {
if resp.Body != nil {
_ = resp.Body.Close()
}
shouldRetry = true
return
}
// 尝试从缓存中恢复
if resp != nil &&
resp.StatusCode >= 500 && // support 50X only
resp.StatusCode < 510 &&
this.cacheCanTryStale &&
this.web.Cache.Stale != nil &&
this.web.Cache.Stale.IsOn &&
(len(this.web.Cache.Stale.Status) == 0 || lists.ContainsInt(this.web.Cache.Stale.Status, resp.StatusCode)) {
var ok = this.doCacheRead(true)
if ok {
return
}
}
// 记录相关数据
this.originStatus = int32(resp.StatusCode)
@@ -395,20 +418,12 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
// WAF对出站进行检查
if this.web.FirewallRef != nil && this.web.FirewallRef.IsOn {
if this.doWAFResponse(resp) {
err := resp.Body.Close()
if err != nil {
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.URL()+": Closing Error (WAF): "+err.Error())
}
return
}
}
// 特殊页面
if len(this.web.Pages) > 0 && this.doPage(resp.StatusCode) {
err := resp.Body.Close()
if err != nil {
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.URL()+": Closing error (Page): "+err.Error())
}
if this.doPage(resp.StatusCode) {
return
}
@@ -499,6 +514,7 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
_, _ = io.CopyBuffer(this.writer, resp.Body, buf)
utils.BytePool4k.Put(buf)
_ = resp.Body.Close()
respBodyIsClosed = true
this.writer.SetOk()
return
@@ -524,11 +540,33 @@ func (this *HTTPRequest) doOriginRequest(failedOriginIds []int64, failedLnNodeId
}
}
} else {
_, err = io.CopyBuffer(this.writer, resp.Body, buf)
if this.cacheRef != nil &&
this.cacheRef.EnableReadingOriginAsync &&
resp.ContentLength > 0 &&
resp.ContentLength < (128<<20) { // TODO configure max content-length in cache policy OR CacheRef
var requestIsCanceled = false
for {
n, readErr := resp.Body.Read(buf)
if n > 0 && !requestIsCanceled {
_, err = this.writer.Write(buf[:n])
if err != nil {
requestIsCanceled = true
}
}
if readErr != nil {
err = readErr
break
}
}
} else {
_, err = io.CopyBuffer(this.writer, resp.Body, buf)
}
}
pool.Put(buf)
var closeErr = resp.Body.Close()
respBodyIsClosed = true
if closeErr != nil {
if !this.canIgnore(closeErr) {
remotelogs.WarnServer("HTTP_REQUEST_REVERSE_PROXY", this.URL()+": Closing error: "+closeErr.Error())

View File

@@ -31,10 +31,10 @@ func (this *HTTPRequest) doRewrite() (shouldShop bool) {
if this.rewriteRule.Mode == serverconfigs.HTTPRewriteModeRedirect {
if this.rewriteRule.RedirectStatus > 0 {
this.ProcessResponseHeaders(this.writer.Header(), this.rewriteRule.RedirectStatus)
http.Redirect(this.writer, this.RawReq, this.rewriteReplace, this.rewriteRule.RedirectStatus)
httpRedirect(this.writer, this.RawReq, this.rewriteReplace, this.rewriteRule.RedirectStatus)
} else {
this.ProcessResponseHeaders(this.writer.Header(), http.StatusTemporaryRedirect)
http.Redirect(this.writer, this.RawReq, this.rewriteReplace, http.StatusTemporaryRedirect)
httpRedirect(this.writer, this.RawReq, this.rewriteReplace, http.StatusTemporaryRedirect)
}
return true
}

View File

@@ -66,6 +66,19 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
requestPath = this.uri[:questionMarkIndex]
}
// except hidden files
if this.web.Root.ExceptHiddenFiles &&
(strings.Contains(requestPath, "/.") || strings.Contains(requestPath, "\\.")) {
this.write404()
return true
}
// except and only files
if !this.web.Root.MatchURL(this.URL()) {
this.write404()
return true
}
// 去掉其中的奇怪的路径
requestPath = strings.Replace(requestPath, "..\\", "", -1)
@@ -95,7 +108,7 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
}
var filename = strings.Replace(requestPath, "/", Tea.DS, -1)
var filePath = ""
var filePath string
if len(filename) > 0 && filename[0:1] == Tea.DS {
filePath = rootDir + filename
} else {

View File

@@ -8,15 +8,17 @@ import (
// 流量限制
func (this *HTTPRequest) doTrafficLimit() {
var config = this.ReqServer.TrafficLimit
this.tags = append(this.tags, "bandwidth")
this.tags = append(this.tags, "trafficLimit")
var statusCode = 509
this.writer.statusCode = statusCode
this.ProcessResponseHeaders(this.writer.Header(), statusCode)
this.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
this.writer.WriteHeader(statusCode)
if len(config.NoticePageBody) != 0 {
var config = this.ReqServer.TrafficLimit
if config != nil && len(config.NoticePageBody) != 0 {
_, _ = this.writer.WriteString(this.Format(config.NoticePageBody))
} else {
_, _ = this.writer.WriteString(this.Format(serverconfigs.DefaultTrafficLimitNoticePageBody))

View File

@@ -7,7 +7,7 @@ import (
)
func (this *HTTPRequest) doCheckUserAgent() (shouldStop bool) {
if this.web.UserAgent == nil {
if this.web.UserAgent == nil || !this.web.UserAgent.IsOn {
return
}

View File

@@ -174,11 +174,19 @@ func (this *HTTPRequest) checkWAFRequest(firewallPolicy *firewallconfigs.HTTPFir
if !regionConfig.IsAllowedCountry(result.CountryId(), result.ProvinceId()) {
this.firewallPolicyId = firewallPolicy.Id
var promptHTML string
if len(regionConfig.CountryHTML) > 0 {
promptHTML = regionConfig.CountryHTML
} else if this.ReqServer != nil && this.ReqServer.HTTPFirewallPolicy != nil && len(this.ReqServer.HTTPFirewallPolicy.DenyCountryHTML) > 0 {
promptHTML = this.ReqServer.HTTPFirewallPolicy.DenyCountryHTML
}
if len(promptHTML) > 0 {
var formattedHTML = this.Format(promptHTML)
this.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
this.writer.Header().Set("Content-Length", types.String(len(regionConfig.CountryHTML)))
this.writer.Header().Set("Content-Length", types.String(len(formattedHTML)))
this.writer.WriteHeader(http.StatusForbidden)
_, _ = this.writer.Write([]byte(regionConfig.CountryHTML))
_, _ = this.writer.Write([]byte(formattedHTML))
} else {
this.writeCode(http.StatusForbidden, "The region has been denied.", "当前区域禁止访问")
}
@@ -202,11 +210,19 @@ func (this *HTTPRequest) checkWAFRequest(firewallPolicy *firewallconfigs.HTTPFir
if !regionConfig.IsAllowedProvince(result.CountryId(), result.ProvinceId()) {
this.firewallPolicyId = firewallPolicy.Id
var promptHTML string
if len(regionConfig.ProvinceHTML) > 0 {
promptHTML = regionConfig.ProvinceHTML
} else if this.ReqServer != nil && this.ReqServer.HTTPFirewallPolicy != nil && len(this.ReqServer.HTTPFirewallPolicy.DenyProvinceHTML) > 0 {
promptHTML = this.ReqServer.HTTPFirewallPolicy.DenyProvinceHTML
}
if len(promptHTML) > 0 {
var formattedHTML = this.Format(promptHTML)
this.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
this.writer.Header().Set("Content-Length", types.String(len(regionConfig.ProvinceHTML)))
this.writer.Header().Set("Content-Length", types.String(len(formattedHTML)))
this.writer.WriteHeader(http.StatusForbidden)
_, _ = this.writer.Write([]byte(regionConfig.ProvinceHTML))
_, _ = this.writer.Write([]byte(formattedHTML))
} else {
this.writeCode(http.StatusForbidden, "The region has been denied.", "当前区域禁止访问")
}
@@ -441,6 +457,14 @@ func (this *HTTPRequest) WAFFingerprint() []byte {
return nil
}
func (this *HTTPRequest) WAFMaxRequestSize() int64 {
var maxRequestSize = firewallconfigs.DefaultMaxRequestBodySize
if this.ReqServer.HTTPFirewallPolicy != nil && this.ReqServer.HTTPFirewallPolicy.MaxRequestBodySize > 0 {
maxRequestSize = this.ReqServer.HTTPFirewallPolicy.MaxRequestBodySize
}
return maxRequestSize
}
// DisableAccessLog 在当前请求中不使用访问日志
func (this *HTTPRequest) DisableAccessLog() {
this.disableLog = true

Some files were not shown because too many files have changed in this diff Show More