Compare commits

...

161 Commits

Author SHA1 Message Date
Robin
cad243905f fix ts request cli (#4461)
Co-authored-by: robinzhang <azhangrongbing@163.com>
2024-11-21 21:40:08 +08:00
dependabot[bot]
7c8f41d577 chore(deps): bump codecov/codecov-action from 4 to 5 (#4459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 21:25:53 +08:00
dependabot[bot]
cbd118d55f chore(deps): bump google.golang.org/protobuf from 1.35.1 to 1.35.2 (#4457) 2024-11-15 17:00:10 +08:00
dependabot[bot]
9d2a1b8b0a chore(deps): bump golang.org/x/time from 0.7.0 to 0.8.0 (#4454)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 00:24:06 +08:00
dependabot[bot]
f6ada979aa chore(deps): bump golang.org/x/net from 0.30.0 to 0.31.0 (#4452)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 00:13:28 +08:00
Kevin Wan
53a74759a5 feat: support json array in request body (#4444) 2024-11-05 14:09:30 +00:00
dependabot[bot]
1940f7bd58 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#4447) 2024-11-05 19:37:32 +08:00
Kevin Wan
18cb3141ba feat: support query array in httpx.Parse (#4440) 2024-11-02 13:55:37 +00:00
dependabot[bot]
f822c9a94f chore(deps): bump github.com/fatih/color from 1.17.0 to 1.18.0 (#4434)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 14:49:56 +08:00
Kevin Wan
1a3dc75874 chore: upgrade goctl version (#4429) 2024-10-20 10:38:04 +08:00
yangjinheng
796dd5b6e2 fix the source code directory after the soft link (#4425) 2024-10-19 15:37:44 +00:00
dependabot[bot]
94e476ade7 chore(deps): bump github.com/redis/go-redis/v9 from 9.6.1 to 9.7.0 (#4426) 2024-10-19 08:10:32 +08:00
dependabot[bot]
2a74996e1b chore(deps): bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 (#4424) 2024-10-18 11:38:41 +08:00
fishJack01
f52af1ebf9 feat:New redis method TxPipeline (#4417)
Co-authored-by: fish <fish@fishdeMac-mini.local>
2024-10-13 05:51:55 +00:00
dependabot[bot]
24450f18bb chore(deps): bump google.golang.org/protobuf from 1.34.2 to 1.35.1 (#4414)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-12 21:13:32 +08:00
dependabot[bot]
f1a45d8a23 chore(deps): bump golang.org/x/time from 0.6.0 to 0.7.0 (#4409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:58:06 +08:00
MarkJoyMa
9aebba1566 fix/conf_multi_layer_map (#4407)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2024-10-05 04:46:32 +00:00
dependabot[bot]
4998479f9a chore(deps): bump golang.org/x/net from 0.29.0 to 0.30.0 (#4410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:27:34 +08:00
dependabot[bot]
873d1351ee chore(deps): bump golang.org/x/sys from 0.25.0 to 0.26.0 (#4408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:04:39 +08:00
dependabot[bot]
afcbca8f24 chore(deps): bump go.mongodb.org/mongo-driver from 1.17.0 to 1.17.1 (#4402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-03 11:16:00 +08:00
dependabot[bot]
5b8126c2cf chore(deps): bump go.uber.org/automaxprocs from 1.5.3 to 1.6.0 (#4389) 2024-09-24 09:16:20 +08:00
Kevin Wan
4dfaf35151 fix: goctl k8s autoscaling version upgrade (#4387) 2024-09-20 23:16:38 +08:00
dependabot[bot]
00cd77c92b chore(deps): bump github.com/prometheus/client_golang from 1.20.3 to 1.20.4 (#4380)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 10:26:52 +08:00
dependabot[bot]
2145a7a93c chore(deps): bump go.mongodb.org/mongo-driver from 1.16.1 to 1.17.0 (#4379)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 22:10:29 +08:00
dependabot[bot]
d5302f2dbe chore(deps): bump golang.org/x/net from 0.28.0 to 0.29.0 (#4360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-07 21:13:42 +08:00
dependabot[bot]
6181594bc8 chore(deps): bump github.com/jhump/protoreflect from 1.16.0 to 1.17.0 (#4359)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 20:13:29 +08:00
dependabot[bot]
11c10e51ff chore(deps): bump github.com/prometheus/client_golang from 1.20.2 to 1.20.3 (#4361)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 18:52:59 +08:00
Kevin Wan
3f03126d27 chore: fix goctl Dockerfile warnings (#4358) 2024-09-05 22:13:01 +08:00
dependabot[bot]
d43adc2823 chore(deps): bump golang.org/x/sys from 0.24.0 to 0.25.0 (#4356)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-05 10:17:27 +08:00
Kevin Wan
656222b572 chore: update goctl version (#4353) 2024-09-03 09:22:04 +08:00
Kevin Wan
077b6072fa chore: coding style (#4352) 2024-09-03 09:06:50 +08:00
jursonmo
0cafb1164b should check if devServer Enabled, then call once.Do() (#4351)
Co-authored-by: william <myname@example.com>
2024-09-03 00:54:43 +00:00
MarkJoyMa
90afa08367 Revert "fix: etcd scheme on grpc resolver" (#4349) 2024-09-03 00:48:08 +00:00
Kevin Wan
c92f788292 chore: update go version in test (#4347) 2024-09-01 16:19:31 +08:00
Kevin Wan
e94be9b302 chore: update go version for building goctl releases (#4346) 2024-09-01 16:02:49 +08:00
Kevin Wan
e713d9013d chore: update goctl deps (#4345) 2024-09-01 15:42:45 +08:00
Kevin Wan
24d6150073 chore: refactor config center (#4339)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-08-28 12:02:48 +00:00
MarkJoyMa
44cddec5c3 feat: added configuration center function (#3035)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2024-08-28 14:47:52 +08:00
Kevin Wan
47d13e5ef8 fix: etcd scheme on grpc resolver (#4121) 2024-08-27 23:13:37 +08:00
Kevin Wan
896e1a2abb chore: refactor logx file time format (#4335) 2024-08-27 22:01:01 +08:00
kui
075817a8dd Add custom log file name date format (#4333) 2024-08-27 20:43:25 +08:00
dependabot[bot]
29400f6814 chore(deps): bump github.com/prometheus/client_golang from 1.20.1 to 1.20.2 (#4334)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-27 19:51:54 +08:00
dependabot[bot]
34f536264f chore(deps): bump github.com/prometheus/client_golang from 1.20.0 to 1.20.1 (#4324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-21 17:38:54 +08:00
Kevin Wan
9d9c7e0fe0 feat: support build tag to reduce binary size w/o k8s (#4323) 2024-08-20 19:53:20 +08:00
dependabot[bot]
e220d3a4cb chore(deps): bump github.com/prometheus/client_golang from 1.19.1 to 1.20.0 (#4317)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:22:06 +08:00
dependabot[bot]
193dcf90bc chore(deps): bump golang.org/x/sys from 0.23.0 to 0.24.0 (#4307)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-10 21:15:08 +08:00
featherlight
03756c9166 refactor zrpc server interceptor builder (#4300) 2024-08-08 14:37:19 +00:00
kesonan
c1f12c5784 (goctl): fix map conversion (#4306) 2024-08-08 14:19:14 +00:00
dependabot[bot]
2883111af5 chore(deps): bump go.mongodb.org/mongo-driver from 1.16.0 to 1.16.1 (#4305)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 22:07:18 +08:00
dependabot[bot]
2758c4e842 chore(deps): bump golang.org/x/net from 0.27.0 to 0.28.0 (#4301)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-07 20:19:47 +08:00
Kevin Wan
4196ddb3e3 Update readme-cn.md (#4294) 2024-08-06 17:54:38 +08:00
dependabot[bot]
e24d797226 chore(deps): bump golang.org/x/time from 0.5.0 to 0.6.0 (#4299)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 17:54:01 +08:00
dependabot[bot]
d4349fa958 chore(deps): bump golang.org/x/sys from 0.22.0 to 0.23.0 (#4298)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 16:39:57 +08:00
kesonan
da2c14d45f (goctl): fix quickstart error while reading go module info (#4297) 2024-08-05 15:29:06 +00:00
kesonan
64e3aeda55 Add goctl version to code header (#4293) 2024-08-03 14:22:51 +00:00
Kevin Wan
dedba17219 refactor: simplify BatchError (#4292) 2024-08-03 13:57:41 +08:00
kesonan
c6348b9855 refactor goctl-compare (#4290) 2024-08-03 04:26:24 +00:00
chentong
8689a6247e refactor(core/errorx): use errors.Join simplify error handle (#4289) 2024-08-03 03:00:59 +00:00
Rui Chen
ff6ee25d23 ci: update workflows to use go.mod instead of specifying go version (#4286)
Signed-off-by: Rui Chen <rui@chenrui.dev>
2024-08-03 02:12:28 +00:00
Kevin Wan
5213243bbb Update readme.md (#4287) 2024-08-02 18:26:54 +08:00
Kevin Wan
2588a36555 feat: support rest.WithCorsHeaders to customize cors headers (#4284) 2024-07-30 17:29:44 +08:00
Krystian Kulas
c2421beb25 fix: readme remove duplicate text (#4280) 2024-07-29 01:06:29 +00:00
kesonan
dfe8a81c76 Upgrade goctl version to 1.7.1 (#4282) 2024-07-29 00:54:21 +00:00
kesonan
ee643a945e (goctl): fix nested struct generation (#4281) 2024-07-28 15:40:25 +00:00
Kevin Wan
eeda6efae7 chore: upgrade go-zero version (#4277) 2024-07-27 17:31:32 +08:00
Kevin Wan
caf0e64beb chore: optimize lock in discov.etcd (#4275) 2024-07-27 16:27:05 +08:00
dependabot[bot]
0e61303cb0 chore(deps): bump github.com/redis/go-redis/v9 from 9.6.0 to 9.6.1 (#4274)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-26 23:17:39 +08:00
dependabot[bot]
f651d7cf6c chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.14 to 3.5.15 (#4267)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-25 19:24:07 +08:00
tsinghuacoder
05da2c560b chore: fix some comments (#4270)
Signed-off-by: tsinghuacoder <tsinghuacoder@icloud.com>
2024-07-25 19:11:56 +08:00
Kevin Wan
8ae0f287d6 chore: optimize lock in discov.etcd (#4272) 2024-07-25 17:24:05 +08:00
Kevin Wan
8f7aff558f chore: refactor BuildTypes in tsgen. (#4266) 2024-07-22 21:17:59 +08:00
jaron
6e08d478fe feat(tsgen): tsgen export buildTypes function (#4197) 2024-07-22 12:11:06 +00:00
dependabot[bot]
944ac383d2 chore(deps): bump github.com/redis/go-redis/v9 from 9.5.4 to 9.6.0 (#4262)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-20 23:18:19 +08:00
Kevin Wan
0eec33f14b chore: optimize file reading (#4264) 2024-07-20 22:44:13 +08:00
JiChen
9de04ee035 fix: handle with read the empty file (#4258) 2024-07-20 12:01:13 +00:00
kesonan
cf5b080fbe (goctl): Use .goctl as home if not exists (#4260) 2024-07-19 05:54:24 +00:00
Kevin Wan
4a14164be1 feat: handle using root as the path of file server (#4255) 2024-07-18 15:15:03 +00:00
Kevin Wan
5dd6f2a43a feat: support embed file system to serve files in rest (#4253) 2024-07-17 16:21:08 +08:00
Kevin Wan
a00c956776 chore: upgrade go version (#4248)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-07-16 11:43:25 +08:00
yonwoo9
c02fb3acab chore: initialize some slice type variables (#4249) 2024-07-15 15:50:42 +00:00
Kevin Wan
9f8455ddb3 chore: fix typo (#4246) 2024-07-14 10:52:47 +08:00
guonaihong
775b105ab2 added code comments (#4219) 2024-07-13 12:09:58 +00:00
Kevin Wan
ec86f22cd6 feat: support file server in rest (#4244) 2024-07-13 19:58:35 +08:00
dependabot[bot]
e776b5d8ab chore(deps): bump github.com/redis/go-redis/v9 from 9.5.3 to 9.5.4 (#4245)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-13 16:31:57 +08:00
Kevin Wan
2026d4410b fix: should not trigger breaker on duplicate key with mongodb (#4238) 2024-07-08 23:41:02 +08:00
MarkJoyMa
f8437e6364 feat/sqlc_partial (#4237) 2024-07-07 15:54:18 +00:00
Kevin Wan
bd2033eb35 feat: support adding more writer, easy to write to console additionally (#4234)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-07-07 23:31:27 +08:00
MarkJoyMa
fed835bc25 feat/redis_hook (#4233) 2024-07-07 04:50:30 +00:00
dependabot[bot]
c9cbd74bf3 chore(deps): bump golang.org/x/net from 0.26.0 to 0.27.0 (#4231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-07 10:54:37 +08:00
dependabot[bot]
27ea106293 chore(deps): bump golang.org/x/sys from 0.21.0 to 0.22.0 (#4229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 19:20:54 +08:00
Kevin Wan
657923b9d5 Update readme-cn.md (#4228) 2024-07-04 11:05:23 +08:00
dependabot[bot]
8dbec6a800 chore(deps): bump google.golang.org/grpc from 1.64.0 to 1.65.0 (#4227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 21:03:23 +08:00
Kevin Wan
490559434a chore: remove unnecessary return (#4226) 2024-07-02 23:45:18 +08:00
kesonan
4a62d084a9 fix: disable array request body (#4220) 2024-07-02 03:55:01 +00:00
kesonan
2f9b6cf8ec disable nested struct for array and map type (#4222) 2024-06-29 05:44:46 +00:00
Kevin Wan
01bbc78bac Update FUNDING.yml (#4216) 2024-06-27 11:15:42 +08:00
kesonan
a012a9138f (goctl): support nested struct (#4211) 2024-06-25 15:18:15 +00:00
Kevin Wan
4ec9cac82b chore: update readme for goctl installation (#4206) 2024-06-23 11:47:17 +08:00
苏蓝
8d9746e794 Update readme-cn.md (#4205) 2024-06-23 03:32:57 +00:00
Kevin Wan
8f83705199 Update version.go (#4204) 2024-06-21 20:09:36 +08:00
Kevin Wan
f1ed7bd75d Update readme-cn.md (#4195) 2024-06-17 22:28:24 +08:00
Kevin Wan
7a20608756 chore: add trending badge (#4194) 2024-06-17 22:26:08 +08:00
dependabot[bot]
5cfff95e95 chore(deps): bump google.golang.org/protobuf from 1.34.1 to 1.34.2 (#4185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-12 14:26:26 +08:00
dependabot[bot]
1e1cc1a0d9 chore(deps): bump github.com/redis/go-redis/v9 from 9.5.2 to 9.5.3 (#4183)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-08 09:28:45 +08:00
dependabot[bot]
0a1440a839 chore(deps): bump golang.org/x/net from 0.25.0 to 0.26.0 (#4180) 2024-06-05 08:01:46 +08:00
kesonan
23980d29c3 fix no such dir if not create goctl home (#4177) 2024-06-04 10:55:56 +00:00
jiz4oh
424119d796 chore: fix the confused log level in comment (#4175) 2024-06-04 10:43:26 +00:00
kesonan
97c7835d9e fix #4161 (#4176) 2024-06-04 10:26:34 +00:00
kesonan
7954ad3759 fix: fix readme (#4174) 2024-06-02 15:22:33 +00:00
dependabot[bot]
e8c9b0ddf8 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.13 to 3.5.14 (#4169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-02 11:17:55 +08:00
dependabot[bot]
70112e59cb chore(deps): bump github.com/redis/go-redis/v9 from 9.4.0 to 9.5.2 (#4172)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-01 13:12:33 +08:00
dependabot[bot]
7ba5ced2d9 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.32.1 to 2.33.0 (#4168)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-30 11:15:59 +08:00
Kevin Wan
962b36d745 fix: log concurrency problems after calling WithXXX methods (#4164) 2024-05-26 12:52:05 +08:00
dependabot[bot]
57060cc6d7 chore(deps): bump google.golang.org/grpc from 1.63.2 to 1.64.0 (#4155) 2024-05-16 07:47:19 +08:00
Kevin Wan
e0c16059d9 optimize: simplify breaker algorithm (#4151) 2024-05-14 17:02:21 +08:00
dependabot[bot]
a0d954dfab chore(deps): bump github.com/fatih/color from 1.16.0 to 1.17.0 (#4150)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 10:37:25 +08:00
Alex Last
a5ece25c07 feat: add secure option for sending traces via otlphttp (#3973) 2024-05-12 17:00:54 +00:00
Kevin Wan
0cac41a38b chore: refactor mapping unmarshaler (#4145) 2024-05-12 14:37:36 +08:00
Kevin Wan
f10084a3f5 chore: refactor and coding style (#4144) 2024-05-11 23:06:59 +08:00
Leo
040fee5669 feat: httpx.Parse supports parsing structures that implement the Unmarshaler interface (#4143) 2024-05-11 22:25:10 +08:00
Kevin Wan
42b3bae65a optimize: improve breaker algorithm on recovery time (#4141) 2024-05-11 21:44:26 +08:00
guangwu
7c730b97d8 fix: make: command: Command not found (#4132)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-05-10 13:33:03 +00:00
Kevin Wan
057bae92ab fix: log panic on Error() or String() panics (#4136) 2024-05-10 12:49:34 +08:00
Kevin Wan
74331a45c9 fix: log panic when use nil error or stringer with Field method (#4130) 2024-05-10 00:31:36 +08:00
chen quan
9d551d507f chore(api/maxconnshandler): add tracing information to the log (#4126) 2024-05-08 05:25:35 +00:00
Kevin Wan
02dd81c05c Update FUNDING.yml (#4128) 2024-05-08 12:52:42 +08:00
dependabot[bot]
3095ba2b1f chore(deps): bump golang.org/x/net from 0.24.0 to 0.25.0 (#4124)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 12:52:13 +08:00
dependabot[bot]
2afa60132c chore(deps): bump google.golang.org/protobuf from 1.34.0 to 1.34.1 (#4123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 12:45:10 +08:00
Kevin Wan
e71ed7294b Update FUNDING.yml (#4127) 2024-05-08 12:26:41 +08:00
dependabot[bot]
95822281bf chore(deps): bump golang.org/x/sys from 0.19.0 to 0.20.0 (#4122)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 12:22:06 +08:00
Kevin Wan
588e10daef chore: refactor and coding style (#4120) 2024-05-06 18:16:56 +08:00
soasurs
62ba01120e fix: zrpc kube resolver builder (#4119)
Signed-off-by: soasurs <soasurs@gmail.com>
2024-05-06 14:50:35 +08:00
dependabot[bot]
527de1c50e chore(deps): bump google.golang.org/protobuf from 1.33.1-0.20240408130810-98873a205002 to 1.34.0 (#4115)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-04 23:59:47 +08:00
dependabot[bot]
abfe62a2d7 chore(deps): bump github.com/pelletier/go-toml/v2 from 2.2.1 to 2.2.2 (#4116)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-04 23:39:25 +08:00
Kevin Wan
36f4cf97ff Update FUNDING.yml (#4114) 2024-04-30 22:58:51 +08:00
Kevin Wan
b3cd8a32ed feat: trigger breaker on underlying service timeout (#4112) 2024-04-30 19:01:20 +08:00
kesonan
a9d27cda8a (goctl): fix prefix syntax (#4113) 2024-04-30 09:11:08 +00:00
kesonan
04116f647d chore(goctl): change goctl version to 1.6.5 (#4111) 2024-04-30 04:25:47 +00:00
Kevin Wan
a8ccda0c06 feat: add fx.ParallelErr (#4107) 2024-04-29 00:18:30 +08:00
Kevin Wan
bfddb9dae4 feat: add errorx.In to facility error checking (#4105) 2024-04-27 20:43:45 +08:00
Kevin Wan
b337ae36e5 Update readme-cn.md 2024-04-20 10:00:10 +08:00
Kevin Wan
5e5123caa3 chore: add more tests (#4094) 2024-04-19 11:13:23 +08:00
Kevin Wan
d371ab5479 feat: use breaker with ctx to prevent deadline exceeded (#4091)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-04-18 23:18:49 +08:00
jaron
1b9b61f505 fix(goctl): GOPROXY env should set by ourself (#4087) 2024-04-18 22:50:30 +08:00
suyhuai
e1f15efb3b add customized.tpl for model template (#4086)
Co-authored-by: sudaoxyz <sudaoxyz@gmail.com>
2024-04-18 14:40:54 +00:00
Kevin Wan
1540bdc4c9 optimize: improve breaker algorithm on recovery time (#4077) 2024-04-18 22:33:25 +08:00
Kevin Wan
95b32b5779 chore: add code coverage (#4090) 2024-04-18 20:58:36 +08:00
Kevin Wan
815a4f7eed feat: support context in breaker methods (#4088) 2024-04-18 18:00:17 +08:00
dependabot[bot]
4b0bacc9c6 chore(deps): bump k8s.io/apimachinery from 0.29.3 to 0.29.4 (#4084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 11:22:49 +08:00
Kevin Wan
e9dc96af17 chore: coding style (#4082) 2024-04-17 23:37:35 +08:00
fearlessfei
62c88a84d1 feat: migrate lua script to lua file (#4069) 2024-04-17 15:20:10 +00:00
Kevin Wan
36088ea0d4 fix: avoid duplicate in logx plain mode (#4080) 2024-04-17 17:43:22 +08:00
dependabot[bot]
164f5aa86c chore(deps): bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1 (#4073)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-13 12:32:56 +08:00
dependabot[bot]
07d07cdd23 chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.9 to 1.9.1 (#4065)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 13:02:52 +08:00
dependabot[bot]
0efe99af66 chore(deps): bump github.com/jhump/protoreflect from 1.15.6 to 1.16.0 (#4064)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 12:39:00 +08:00
Kevin Wan
927f8bc821 fix: fix ignored context.DeadlineExceeded (#4066) 2024-04-11 11:14:20 +08:00
kesonan
2a7ada993b (goctl)feature/model config (#4062)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-04-10 15:01:59 +00:00
Kevin Wan
682460c1c8 fix: fix ignored scanner.Err() (#4063) 2024-04-10 17:28:52 +08:00
Kevin Wan
a66ae0d4c4 fix: timeout on query should return context.DeadlineExceeded (#4060) 2024-04-10 04:17:39 +00:00
dependabot[bot]
d1f24ab70f chore(deps): bump google.golang.org/grpc from 1.63.0 to 1.63.2 (#4058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 10:23:33 +08:00
dependabot[bot]
d0983948b5 chore(deps): bump google.golang.org/grpc from 1.63.0 to 1.63.2 in /tools/goctl (#4059)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 10:14:15 +08:00
205 changed files with 5883 additions and 1999 deletions

12
.github/FUNDING.yml vendored
View File

@@ -1,13 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: [zeromicro]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # https://gitee.com/kevwan/static/raw/master/images/sponsor.jpg
ethereum: # 0x5052b7f6B937B02563996D23feb69b38D06Ca150 | kevwan

View File

@@ -17,7 +17,7 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.19' go-version-file: go.mod
check-latest: true check-latest: true
cache: true cache: true
id: go id: go
@@ -40,7 +40,7 @@ jobs:
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Codecov - name: Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
test-win: test-win:
name: Windows name: Windows
@@ -52,8 +52,8 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
# use 1.19 to guarantee Go 1.19 compatibility # make sure Go version compatible with go-zero
go-version: '1.19' go-version-file: go.mod
check-latest: true check-latest: true
cache: true cache: true

View File

@@ -22,7 +22,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }} goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }} goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.19.13.linux-amd64.tar.gz" goversion: "https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz"
project_path: "tools/goctl" project_path: "tools/goctl"
binary_name: "goctl" binary_name: "goctl"
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md

View File

@@ -2,6 +2,7 @@ package bloom
import ( import (
"context" "context"
_ "embed"
"errors" "errors"
"strconv" "strconv"
@@ -17,19 +18,13 @@ var (
// ErrTooLargeOffset indicates the offset is too large in bitset. // ErrTooLargeOffset indicates the offset is too large in bitset.
ErrTooLargeOffset = errors.New("too large offset") ErrTooLargeOffset = errors.New("too large offset")
setScript = redis.NewScript(` //go:embed setscript.lua
for _, offset in ipairs(ARGV) do setLuaScript string
redis.call("setbit", KEYS[1], offset, 1) setScript = redis.NewScript(setLuaScript)
end
`) //go:embed testscript.lua
testScript = redis.NewScript(` testLuaScript string
for _, offset in ipairs(ARGV) do testScript = redis.NewScript(testLuaScript)
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`)
) )
type ( type (
@@ -110,7 +105,7 @@ func newRedisBitSet(store *redis.Redis, key string, bits uint) *redisBitSet {
} }
func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) { func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) {
var args []string args := make([]string, 0, len(offsets))
for _, offset := range offsets { for _, offset := range offsets {
if offset >= r.bits { if offset >= r.bits {

3
core/bloom/setscript.lua Normal file
View File

@@ -0,0 +1,3 @@
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end

View File

@@ -0,0 +1,6 @@
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true

View File

@@ -1,6 +1,7 @@
package breaker package breaker
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -36,12 +37,16 @@ type (
// The caller needs to call promise.Accept() on success, // The caller needs to call promise.Accept() on success,
// or call promise.Reject() on failure. // or call promise.Reject() on failure.
Allow() (Promise, error) Allow() (Promise, error)
// AllowCtx checks if the request is allowed when ctx isn't done.
AllowCtx(ctx context.Context) (Promise, error)
// Do runs the given request if the Breaker accepts it. // Do runs the given request if the Breaker accepts it.
// Do returns an error instantly if the Breaker rejects the request. // Do returns an error instantly if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error // If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again. // and causes the same panic again.
Do(req func() error) error Do(req func() error) error
// DoCtx runs the given request if the Breaker accepts it when ctx isn't done.
DoCtx(ctx context.Context, req func() error) error
// DoWithAcceptable runs the given request if the Breaker accepts it. // DoWithAcceptable runs the given request if the Breaker accepts it.
// DoWithAcceptable returns an error instantly if the Breaker rejects the request. // DoWithAcceptable returns an error instantly if the Breaker rejects the request.
@@ -49,12 +54,16 @@ type (
// and causes the same panic again. // and causes the same panic again.
// acceptable checks if it's a successful call, even if the error is not nil. // acceptable checks if it's a successful call, even if the error is not nil.
DoWithAcceptable(req func() error, acceptable Acceptable) error DoWithAcceptable(req func() error, acceptable Acceptable) error
// DoWithAcceptableCtx runs the given request if the Breaker accepts it when ctx isn't done.
DoWithAcceptableCtx(ctx context.Context, req func() error, acceptable Acceptable) error
// DoWithFallback runs the given request if the Breaker accepts it. // DoWithFallback runs the given request if the Breaker accepts it.
// DoWithFallback runs the fallback if the Breaker rejects the request. // DoWithFallback runs the fallback if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error // If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again. // and causes the same panic again.
DoWithFallback(req func() error, fallback Fallback) error DoWithFallback(req func() error, fallback Fallback) error
// DoWithFallbackCtx runs the given request if the Breaker accepts it when ctx isn't done.
DoWithFallbackCtx(ctx context.Context, req func() error, fallback Fallback) error
// DoWithFallbackAcceptable runs the given request if the Breaker accepts it. // DoWithFallbackAcceptable runs the given request if the Breaker accepts it.
// DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request. // DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request.
@@ -62,6 +71,9 @@ type (
// and causes the same panic again. // and causes the same panic again.
// acceptable checks if it's a successful call, even if the error is not nil. // acceptable checks if it's a successful call, even if the error is not nil.
DoWithFallbackAcceptable(req func() error, fallback Fallback, acceptable Acceptable) error DoWithFallbackAcceptable(req func() error, fallback Fallback, acceptable Acceptable) error
// DoWithFallbackAcceptableCtx runs the given request if the Breaker accepts it when ctx isn't done.
DoWithFallbackAcceptableCtx(ctx context.Context, req func() error, fallback Fallback,
acceptable Acceptable) error
} }
// Fallback is the func to be called if the request is rejected. // Fallback is the func to be called if the request is rejected.
@@ -118,23 +130,71 @@ func (cb *circuitBreaker) Allow() (Promise, error) {
return cb.throttle.allow() return cb.throttle.allow()
} }
func (cb *circuitBreaker) AllowCtx(ctx context.Context) (Promise, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return cb.Allow()
}
}
func (cb *circuitBreaker) Do(req func() error) error { func (cb *circuitBreaker) Do(req func() error) error {
return cb.throttle.doReq(req, nil, defaultAcceptable) return cb.throttle.doReq(req, nil, defaultAcceptable)
} }
func (cb *circuitBreaker) DoCtx(ctx context.Context, req func() error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return cb.Do(req)
}
}
func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error { func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error {
return cb.throttle.doReq(req, nil, acceptable) return cb.throttle.doReq(req, nil, acceptable)
} }
func (cb *circuitBreaker) DoWithAcceptableCtx(ctx context.Context, req func() error,
acceptable Acceptable) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return cb.DoWithAcceptable(req, acceptable)
}
}
func (cb *circuitBreaker) DoWithFallback(req func() error, fallback Fallback) error { func (cb *circuitBreaker) DoWithFallback(req func() error, fallback Fallback) error {
return cb.throttle.doReq(req, fallback, defaultAcceptable) return cb.throttle.doReq(req, fallback, defaultAcceptable)
} }
func (cb *circuitBreaker) DoWithFallbackCtx(ctx context.Context, req func() error,
fallback Fallback) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return cb.DoWithFallback(req, fallback)
}
}
func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback Fallback, func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback Fallback,
acceptable Acceptable) error { acceptable Acceptable) error {
return cb.throttle.doReq(req, fallback, acceptable) return cb.throttle.doReq(req, fallback, acceptable)
} }
func (cb *circuitBreaker) DoWithFallbackAcceptableCtx(ctx context.Context, req func() error,
fallback Fallback, acceptable Acceptable) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return cb.DoWithFallbackAcceptable(req, fallback, acceptable)
}
}
func (cb *circuitBreaker) Name() string { func (cb *circuitBreaker) Name() string {
return cb.name return cb.name
} }
@@ -209,7 +269,7 @@ func (ew *errorWindow) add(reason string) {
} }
func (ew *errorWindow) String() string { func (ew *errorWindow) String() string {
var reasons []string reasons := make([]string, 0, ew.count)
ew.lock.Lock() ew.lock.Lock()
// reverse order // reverse order

View File

@@ -1,11 +1,13 @@
package breaker package breaker
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stat" "github.com/zeromicro/go-zero/core/stat"
@@ -16,10 +18,274 @@ func init() {
} }
func TestCircuitBreaker_Allow(t *testing.T) { func TestCircuitBreaker_Allow(t *testing.T) {
b := NewBreaker() t.Run("allow", func(t *testing.T) {
assert.True(t, len(b.Name()) > 0) b := NewBreaker()
_, err := b.Allow() assert.True(t, len(b.Name()) > 0)
assert.Nil(t, err) _, err := b.Allow()
assert.Nil(t, err)
})
t.Run("allow with ctx", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
_, err := b.AllowCtx(context.Background())
assert.Nil(t, err)
})
t.Run("allow with ctx timeout", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
defer cancel()
time.Sleep(time.Millisecond)
_, err := b.AllowCtx(ctx)
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("allow with ctx cancel", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
for i := 0; i < 100; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
_, err := b.AllowCtx(ctx)
assert.ErrorIs(t, err, context.Canceled)
}
_, err := b.AllowCtx(context.Background())
assert.NoError(t, err)
})
}
func TestCircuitBreaker_Do(t *testing.T) {
t.Run("do", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.Do(func() error {
return nil
})
assert.Nil(t, err)
})
t.Run("do with ctx", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoCtx(context.Background(), func() error {
return nil
})
assert.Nil(t, err)
})
t.Run("do with ctx timeout", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
defer cancel()
time.Sleep(time.Millisecond)
err := b.DoCtx(ctx, func() error {
return nil
})
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("do with ctx cancel", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
for i := 0; i < 100; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
err := b.DoCtx(ctx, func() error {
return nil
})
assert.ErrorIs(t, err, context.Canceled)
}
assert.NoError(t, b.DoCtx(context.Background(), func() error {
return nil
}))
})
}
func TestCircuitBreaker_DoWithAcceptable(t *testing.T) {
t.Run("doWithAcceptable", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithAcceptable(func() error {
return nil
}, func(err error) bool {
return true
})
assert.Nil(t, err)
})
t.Run("doWithAcceptable with ctx", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithAcceptableCtx(context.Background(), func() error {
return nil
}, func(err error) bool {
return true
})
assert.Nil(t, err)
})
t.Run("doWithAcceptable with ctx timeout", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
defer cancel()
time.Sleep(time.Millisecond)
err := b.DoWithAcceptableCtx(ctx, func() error {
return nil
}, func(err error) bool {
return true
})
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("doWithAcceptable with ctx cancel", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
for i := 0; i < 100; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
err := b.DoWithAcceptableCtx(ctx, func() error {
return nil
}, func(err error) bool {
return true
})
assert.ErrorIs(t, err, context.Canceled)
}
assert.NoError(t, b.DoWithAcceptableCtx(context.Background(), func() error {
return nil
}, func(err error) bool {
return true
}))
})
}
func TestCircuitBreaker_DoWithFallback(t *testing.T) {
t.Run("doWithFallback", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithFallback(func() error {
return nil
}, func(err error) error {
return err
})
assert.Nil(t, err)
})
t.Run("doWithFallback with ctx", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithFallbackCtx(context.Background(), func() error {
return nil
}, func(err error) error {
return err
})
assert.Nil(t, err)
})
t.Run("doWithFallback with ctx timeout", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
defer cancel()
time.Sleep(time.Millisecond)
err := b.DoWithFallbackCtx(ctx, func() error {
return nil
}, func(err error) error {
return err
})
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("doWithFallback with ctx cancel", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
for i := 0; i < 100; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
err := b.DoWithFallbackCtx(ctx, func() error {
return nil
}, func(err error) error {
return err
})
assert.ErrorIs(t, err, context.Canceled)
}
assert.NoError(t, b.DoWithFallbackCtx(context.Background(), func() error {
return nil
}, func(err error) error {
return err
}))
})
}
func TestCircuitBreaker_DoWithFallbackAcceptable(t *testing.T) {
t.Run("doWithFallbackAcceptable", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithFallbackAcceptable(func() error {
return nil
}, func(err error) error {
return err
}, func(err error) bool {
return true
})
assert.Nil(t, err)
})
t.Run("doWithFallbackAcceptable with ctx", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
err := b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
return nil
}, func(err error) error {
return err
}, func(err error) bool {
return true
})
assert.Nil(t, err)
})
t.Run("doWithFallbackAcceptable with ctx timeout", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
defer cancel()
time.Sleep(time.Millisecond)
err := b.DoWithFallbackAcceptableCtx(ctx, func() error {
return nil
}, func(err error) error {
return err
}, func(err error) bool {
return true
})
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("doWithFallbackAcceptable with ctx cancel", func(t *testing.T) {
b := NewBreaker()
assert.True(t, len(b.Name()) > 0)
for i := 0; i < 100; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
err := b.DoWithFallbackAcceptableCtx(ctx, func() error {
return nil
}, func(err error) error {
return err
}, func(err error) bool {
return true
})
assert.ErrorIs(t, err, context.Canceled)
}
assert.NoError(t, b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
return nil
}, func(err error) error {
return err
}, func(err error) bool {
return true
}))
})
} }
func TestLogReason(t *testing.T) { func TestLogReason(t *testing.T) {

View File

@@ -1,6 +1,9 @@
package breaker package breaker
import "sync" import (
"context"
"sync"
)
var ( var (
lock sync.RWMutex lock sync.RWMutex
@@ -14,6 +17,13 @@ func Do(name string, req func() error) error {
}) })
} }
// DoCtx calls Breaker.DoCtx on the Breaker with given name.
func DoCtx(ctx context.Context, name string, req func() error) error {
return do(name, func(b Breaker) error {
return b.DoCtx(ctx, req)
})
}
// DoWithAcceptable calls Breaker.DoWithAcceptable on the Breaker with given name. // DoWithAcceptable calls Breaker.DoWithAcceptable on the Breaker with given name.
func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error { func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error {
return do(name, func(b Breaker) error { return do(name, func(b Breaker) error {
@@ -21,6 +31,14 @@ func DoWithAcceptable(name string, req func() error, acceptable Acceptable) erro
}) })
} }
// DoWithAcceptableCtx calls Breaker.DoWithAcceptableCtx on the Breaker with given name.
func DoWithAcceptableCtx(ctx context.Context, name string, req func() error,
acceptable Acceptable) error {
return do(name, func(b Breaker) error {
return b.DoWithAcceptableCtx(ctx, req, acceptable)
})
}
// DoWithFallback calls Breaker.DoWithFallback on the Breaker with given name. // DoWithFallback calls Breaker.DoWithFallback on the Breaker with given name.
func DoWithFallback(name string, req func() error, fallback Fallback) error { func DoWithFallback(name string, req func() error, fallback Fallback) error {
return do(name, func(b Breaker) error { return do(name, func(b Breaker) error {
@@ -28,6 +46,13 @@ func DoWithFallback(name string, req func() error, fallback Fallback) error {
}) })
} }
// DoWithFallbackCtx calls Breaker.DoWithFallbackCtx on the Breaker with given name.
func DoWithFallbackCtx(ctx context.Context, name string, req func() error, fallback Fallback) error {
return do(name, func(b Breaker) error {
return b.DoWithFallbackCtx(ctx, req, fallback)
})
}
// DoWithFallbackAcceptable calls Breaker.DoWithFallbackAcceptable on the Breaker with given name. // DoWithFallbackAcceptable calls Breaker.DoWithFallbackAcceptable on the Breaker with given name.
func DoWithFallbackAcceptable(name string, req func() error, fallback Fallback, func DoWithFallbackAcceptable(name string, req func() error, fallback Fallback,
acceptable Acceptable) error { acceptable Acceptable) error {
@@ -36,6 +61,14 @@ func DoWithFallbackAcceptable(name string, req func() error, fallback Fallback,
}) })
} }
// DoWithFallbackAcceptableCtx calls Breaker.DoWithFallbackAcceptableCtx on the Breaker with given name.
func DoWithFallbackAcceptableCtx(ctx context.Context, name string, req func() error,
fallback Fallback, acceptable Acceptable) error {
return do(name, func(b Breaker) error {
return b.DoWithFallbackAcceptableCtx(ctx, req, fallback, acceptable)
})
}
// GetBreaker returns the Breaker with the given name. // GetBreaker returns the Breaker with the given name.
func GetBreaker(name string) Breaker { func GetBreaker(name string) Breaker {
lock.RLock() lock.RLock()

View File

@@ -1,6 +1,7 @@
package breaker package breaker
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"testing" "testing"
@@ -22,6 +23,9 @@ func TestBreakersDo(t *testing.T) {
assert.Equal(t, errDummy, Do("any", func() error { assert.Equal(t, errDummy, Do("any", func() error {
return errDummy return errDummy
})) }))
assert.Equal(t, errDummy, DoCtx(context.Background(), "any", func() error {
return errDummy
}))
} }
func TestBreakersDoWithAcceptable(t *testing.T) { func TestBreakersDoWithAcceptable(t *testing.T) {
@@ -38,6 +42,13 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
return nil return nil
}) == nil }) == nil
}) })
verify(t, func() bool {
return DoWithAcceptableCtx(context.Background(), "anyone", func() error {
return nil
}, func(err error) bool {
return true
}) == nil
})
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
err := DoWithAcceptable("another", func() error { err := DoWithAcceptable("another", func() error {
@@ -76,6 +87,12 @@ func TestBreakersFallback(t *testing.T) {
return nil return nil
}) })
assert.True(t, err == nil || errors.Is(err, errDummy)) assert.True(t, err == nil || errors.Is(err, errDummy))
err = DoWithFallbackCtx(context.Background(), "fallback", func() error {
return errDummy
}, func(err error) error {
return nil
})
assert.True(t, err == nil || errors.Is(err, errDummy))
} }
verify(t, func() bool { verify(t, func() bool {
return errors.Is(Do("fallback", func() error { return errors.Is(Do("fallback", func() error {
@@ -86,7 +103,7 @@ func TestBreakersFallback(t *testing.T) {
func TestBreakersAcceptableFallback(t *testing.T) { func TestBreakersAcceptableFallback(t *testing.T) {
errDummy := errors.New("any") errDummy := errors.New("any")
for i := 0; i < 10000; i++ { for i := 0; i < 5000; i++ {
err := DoWithFallbackAcceptable("acceptablefallback", func() error { err := DoWithFallbackAcceptable("acceptablefallback", func() error {
return errDummy return errDummy
}, func(err error) error { }, func(err error) error {
@@ -95,6 +112,14 @@ func TestBreakersAcceptableFallback(t *testing.T) {
return err == nil return err == nil
}) })
assert.True(t, err == nil || errors.Is(err, errDummy)) assert.True(t, err == nil || errors.Is(err, errDummy))
err = DoWithFallbackAcceptableCtx(context.Background(), "acceptablefallback", func() error {
return errDummy
}, func(err error) error {
return nil
}, func(err error) bool {
return err == nil
})
assert.True(t, err == nil || errors.Is(err, errDummy))
} }
verify(t, func() bool { verify(t, func() bool {
return errors.Is(Do("acceptablefallback", func() error { return errors.Is(Do("acceptablefallback", func() error {
@@ -110,5 +135,5 @@ func verify(t *testing.T, fn func() bool) {
count++ count++
} }
} }
assert.True(t, count >= 80, fmt.Sprintf("should be greater than 80, actual %d", count)) assert.True(t, count >= 75, fmt.Sprintf("should be greater than 75, actual %d", count))
} }

48
core/breaker/bucket.go Normal file
View File

@@ -0,0 +1,48 @@
package breaker
const (
success = iota
fail
drop
)
// bucket defines the bucket that holds sum and num of additions.
type bucket struct {
Sum int64
Success int64
Failure int64
Drop int64
}
func (b *bucket) Add(v int64) {
switch v {
case fail:
b.fail()
case drop:
b.drop()
default:
b.succeed()
}
}
func (b *bucket) Reset() {
b.Sum = 0
b.Success = 0
b.Failure = 0
b.Drop = 0
}
func (b *bucket) drop() {
b.Sum++
b.Drop++
}
func (b *bucket) fail() {
b.Sum++
b.Failure++
}
func (b *bucket) succeed() {
b.Sum++
b.Success++
}

View File

@@ -0,0 +1,43 @@
package breaker
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBucketAdd(t *testing.T) {
b := &bucket{}
// Test succeed
b.Add(0) // Using 0 for success
assert.Equal(t, int64(1), b.Sum, "Sum should be incremented")
assert.Equal(t, int64(1), b.Success, "Success should be incremented")
assert.Equal(t, int64(0), b.Failure, "Failure should not be incremented")
assert.Equal(t, int64(0), b.Drop, "Drop should not be incremented")
// Test failure
b.Add(fail)
assert.Equal(t, int64(2), b.Sum, "Sum should be incremented")
assert.Equal(t, int64(1), b.Failure, "Failure should be incremented")
assert.Equal(t, int64(0), b.Drop, "Drop should not be incremented")
// Test drop
b.Add(drop)
assert.Equal(t, int64(3), b.Sum, "Sum should be incremented")
assert.Equal(t, int64(1), b.Drop, "Drop should be incremented")
}
func TestBucketReset(t *testing.T) {
b := &bucket{
Sum: 3,
Success: 1,
Failure: 1,
Drop: 1,
}
b.Reset()
assert.Equal(t, int64(0), b.Sum, "Sum should be reset to 0")
assert.Equal(t, int64(0), b.Success, "Success should be reset to 0")
assert.Equal(t, int64(0), b.Failure, "Failure should be reset to 0")
assert.Equal(t, int64(0), b.Drop, "Drop should be reset to 0")
}

View File

@@ -5,54 +5,83 @@ import (
"github.com/zeromicro/go-zero/core/collection" "github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/core/mathx" "github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/timex"
) )
const ( const (
// 250ms for bucket duration // 250ms for bucket duration
window = time.Second * 10 window = time.Second * 10
buckets = 40 buckets = 40
k = 1.5 forcePassDuration = time.Second
protection = 5 k = 1.5
minK = 1.1
protection = 5
) )
// googleBreaker is a netflixBreaker pattern from google. // googleBreaker is a netflixBreaker pattern from google.
// see Client-Side Throttling section in https://landing.google.com/sre/sre-book/chapters/handling-overload/ // see Client-Side Throttling section in https://landing.google.com/sre/sre-book/chapters/handling-overload/
type googleBreaker struct { type (
k float64 googleBreaker struct {
stat *collection.RollingWindow k float64
proba *mathx.Proba stat *collection.RollingWindow[int64, *bucket]
} proba *mathx.Proba
lastPass *syncx.AtomicDuration
}
windowResult struct {
accepts int64
total int64
failingBuckets int64
workingBuckets int64
}
)
func newGoogleBreaker() *googleBreaker { func newGoogleBreaker() *googleBreaker {
bucketDuration := time.Duration(int64(window) / int64(buckets)) bucketDuration := time.Duration(int64(window) / int64(buckets))
st := collection.NewRollingWindow(buckets, bucketDuration) st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
return new(bucket)
}, buckets, bucketDuration)
return &googleBreaker{ return &googleBreaker{
stat: st, stat: st,
k: k, k: k,
proba: mathx.NewProba(), proba: mathx.NewProba(),
lastPass: syncx.NewAtomicDuration(),
} }
} }
func (b *googleBreaker) accept() error { func (b *googleBreaker) accept() error {
accepts, total := b.history() var w float64
weightedAccepts := b.k * float64(accepts) history := b.history()
w = b.k - (b.k-minK)*float64(history.failingBuckets)/buckets
weightedAccepts := mathx.AtLeast(w, minK) * float64(history.accepts)
// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101 // https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
// for better performance, no need to care about negative ratio // for better performance, no need to care about the negative ratio
dropRatio := (float64(total-protection) - weightedAccepts) / float64(total+1) dropRatio := (float64(history.total-protection) - weightedAccepts) / float64(history.total+1)
if dropRatio <= 0 { if dropRatio <= 0 {
return nil return nil
} }
lastPass := b.lastPass.Load()
if lastPass > 0 && timex.Since(lastPass) > forcePassDuration {
b.lastPass.Set(timex.Now())
return nil
}
dropRatio *= float64(buckets-history.workingBuckets) / buckets
if b.proba.TrueOnProba(dropRatio) { if b.proba.TrueOnProba(dropRatio) {
return ErrServiceUnavailable return ErrServiceUnavailable
} }
b.lastPass.Set(timex.Now())
return nil return nil
} }
func (b *googleBreaker) allow() (internalPromise, error) { func (b *googleBreaker) allow() (internalPromise, error) {
if err := b.accept(); err != nil { if err := b.accept(); err != nil {
b.markFailure() b.markDrop()
return nil, err return nil, err
} }
@@ -63,7 +92,7 @@ func (b *googleBreaker) allow() (internalPromise, error) {
func (b *googleBreaker) doReq(req func() error, fallback Fallback, acceptable Acceptable) error { func (b *googleBreaker) doReq(req func() error, fallback Fallback, acceptable Acceptable) error {
if err := b.accept(); err != nil { if err := b.accept(); err != nil {
b.markFailure() b.markDrop()
if fallback != nil { if fallback != nil {
return fallback(err) return fallback(err)
} }
@@ -71,10 +100,10 @@ func (b *googleBreaker) doReq(req func() error, fallback Fallback, acceptable Ac
return err return err
} }
var success bool var succ bool
defer func() { defer func() {
// if req() panic, success is false, mark as failure // if req() panic, success is false, mark as failure
if success { if succ {
b.markSuccess() b.markSuccess()
} else { } else {
b.markFailure() b.markFailure()
@@ -83,27 +112,43 @@ func (b *googleBreaker) doReq(req func() error, fallback Fallback, acceptable Ac
err := req() err := req()
if acceptable(err) { if acceptable(err) {
success = true succ = true
} }
return err return err
} }
func (b *googleBreaker) markSuccess() { func (b *googleBreaker) markDrop() {
b.stat.Add(1) b.stat.Add(drop)
} }
func (b *googleBreaker) markFailure() { func (b *googleBreaker) markFailure() {
b.stat.Add(0) b.stat.Add(fail)
} }
func (b *googleBreaker) history() (accepts, total int64) { func (b *googleBreaker) markSuccess() {
b.stat.Reduce(func(b *collection.Bucket) { b.stat.Add(success)
accepts += int64(b.Sum) }
total += b.Count
func (b *googleBreaker) history() windowResult {
var result windowResult
b.stat.Reduce(func(b *bucket) {
result.accepts += b.Success
result.total += b.Sum
if b.Failure > 0 {
result.workingBuckets = 0
} else if b.Success > 0 {
result.workingBuckets++
}
if b.Success > 0 {
result.failingBuckets = 0
} else if b.Failure > 0 {
result.failingBuckets++
}
}) })
return return result
} }
type googlePromise struct { type googlePromise struct {

View File

@@ -10,6 +10,7 @@ import (
"github.com/zeromicro/go-zero/core/collection" "github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/core/mathx" "github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/stat" "github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/syncx"
) )
const ( const (
@@ -22,11 +23,14 @@ func init() {
} }
func getGoogleBreaker() *googleBreaker { func getGoogleBreaker() *googleBreaker {
st := collection.NewRollingWindow(testBuckets, testInterval) st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
return new(bucket)
}, testBuckets, testInterval)
return &googleBreaker{ return &googleBreaker{
stat: st, stat: st,
k: 5, k: 5,
proba: mathx.NewProba(), proba: mathx.NewProba(),
lastPass: syncx.NewAtomicDuration(),
} }
} }
@@ -63,6 +67,33 @@ func TestGoogleBreakerOpen(t *testing.T) {
}) })
} }
func TestGoogleBreakerRecover(t *testing.T) {
st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
return new(bucket)
}, testBuckets*2, testInterval)
b := &googleBreaker{
stat: st,
k: k,
proba: mathx.NewProba(),
lastPass: syncx.NewAtomicDuration(),
}
for i := 0; i < testBuckets; i++ {
for j := 0; j < 100; j++ {
b.stat.Add(1)
}
time.Sleep(testInterval)
}
for i := 0; i < testBuckets; i++ {
for j := 0; j < 100; j++ {
b.stat.Add(0)
}
time.Sleep(testInterval)
}
verify(t, func() bool {
return b.accept() == nil
})
}
func TestGoogleBreakerFallback(t *testing.T) { func TestGoogleBreakerFallback(t *testing.T) {
b := getGoogleBreaker() b := getGoogleBreaker()
markSuccess(b, 1) markSuccess(b, 1)
@@ -89,6 +120,43 @@ func TestGoogleBreakerReject(t *testing.T) {
}, nil, defaultAcceptable)) }, nil, defaultAcceptable))
} }
func TestGoogleBreakerMoreFallingBuckets(t *testing.T) {
t.Parallel()
t.Run("more falling buckets", func(t *testing.T) {
b := getGoogleBreaker()
func() {
stopChan := time.After(testInterval * 6)
for {
time.Sleep(time.Millisecond)
select {
case <-stopChan:
return
default:
assert.Error(t, b.doReq(func() error {
return errors.New("foo")
}, func(err error) error {
return err
}, func(err error) bool {
return err == nil
}))
}
}
}()
var count int
for i := 0; i < 100; i++ {
if errors.Is(b.doReq(func() error {
return ErrServiceUnavailable
}, nil, defaultAcceptable), ErrServiceUnavailable) {
count++
}
}
assert.True(t, count > 90)
})
}
func TestGoogleBreakerAcceptable(t *testing.T) { func TestGoogleBreakerAcceptable(t *testing.T) {
b := getGoogleBreaker() b := getGoogleBreaker()
errAcceptable := errors.New("any") errAcceptable := errors.New("any")
@@ -164,41 +232,38 @@ func TestGoogleBreakerSelfProtection(t *testing.T) {
} }
func TestGoogleBreakerHistory(t *testing.T) { func TestGoogleBreakerHistory(t *testing.T) {
var b *googleBreaker
var accepts, total int64
sleep := testInterval sleep := testInterval
t.Run("accepts == total", func(t *testing.T) { t.Run("accepts == total", func(t *testing.T) {
b = getGoogleBreaker() b := getGoogleBreaker()
markSuccessWithDuration(b, 10, sleep/2) markSuccessWithDuration(b, 10, sleep/2)
accepts, total = b.history() result := b.history()
assert.Equal(t, int64(10), accepts) assert.Equal(t, int64(10), result.accepts)
assert.Equal(t, int64(10), total) assert.Equal(t, int64(10), result.total)
}) })
t.Run("fail == total", func(t *testing.T) { t.Run("fail == total", func(t *testing.T) {
b = getGoogleBreaker() b := getGoogleBreaker()
markFailedWithDuration(b, 10, sleep/2) markFailedWithDuration(b, 10, sleep/2)
accepts, total = b.history() result := b.history()
assert.Equal(t, int64(0), accepts) assert.Equal(t, int64(0), result.accepts)
assert.Equal(t, int64(10), total) assert.Equal(t, int64(10), result.total)
}) })
t.Run("accepts = 1/2 * total, fail = 1/2 * total", func(t *testing.T) { t.Run("accepts = 1/2 * total, fail = 1/2 * total", func(t *testing.T) {
b = getGoogleBreaker() b := getGoogleBreaker()
markFailedWithDuration(b, 5, sleep/2) markFailedWithDuration(b, 5, sleep/2)
markSuccessWithDuration(b, 5, sleep/2) markSuccessWithDuration(b, 5, sleep/2)
accepts, total = b.history() result := b.history()
assert.Equal(t, int64(5), accepts) assert.Equal(t, int64(5), result.accepts)
assert.Equal(t, int64(10), total) assert.Equal(t, int64(10), result.total)
}) })
t.Run("auto reset rolling counter", func(t *testing.T) { t.Run("auto reset rolling counter", func(t *testing.T) {
b = getGoogleBreaker() b := getGoogleBreaker()
time.Sleep(testInterval * testBuckets) time.Sleep(testInterval * testBuckets)
accepts, total = b.history() result := b.history()
assert.Equal(t, int64(0), accepts) assert.Equal(t, int64(0), result.accepts)
assert.Equal(t, int64(0), total) assert.Equal(t, int64(0), result.total)
}) })
} }

View File

@@ -1,5 +1,7 @@
package breaker package breaker
import "context"
const nopBreakerName = "nopBreaker" const nopBreakerName = "nopBreaker"
type nopBreaker struct{} type nopBreaker struct{}
@@ -17,22 +19,43 @@ func (b nopBreaker) Allow() (Promise, error) {
return nopPromise{}, nil return nopPromise{}, nil
} }
func (b nopBreaker) AllowCtx(_ context.Context) (Promise, error) {
return nopPromise{}, nil
}
func (b nopBreaker) Do(req func() error) error { func (b nopBreaker) Do(req func() error) error {
return req() return req()
} }
func (b nopBreaker) DoCtx(_ context.Context, req func() error) error {
return req()
}
func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error { func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
return req() return req()
} }
func (b nopBreaker) DoWithAcceptableCtx(_ context.Context, req func() error, _ Acceptable) error {
return req()
}
func (b nopBreaker) DoWithFallback(req func() error, _ Fallback) error { func (b nopBreaker) DoWithFallback(req func() error, _ Fallback) error {
return req() return req()
} }
func (b nopBreaker) DoWithFallbackCtx(_ context.Context, req func() error, _ Fallback) error {
return req()
}
func (b nopBreaker) DoWithFallbackAcceptable(req func() error, _ Fallback, _ Acceptable) error { func (b nopBreaker) DoWithFallbackAcceptable(req func() error, _ Fallback, _ Acceptable) error {
return req() return req()
} }
func (b nopBreaker) DoWithFallbackAcceptableCtx(_ context.Context, req func() error,
_ Fallback, _ Acceptable) error {
return req()
}
type nopPromise struct{} type nopPromise struct{}
func (p nopPromise) Accept() { func (p nopPromise) Accept() {

View File

@@ -1,6 +1,7 @@
package breaker package breaker
import ( import (
"context"
"errors" "errors"
"testing" "testing"
@@ -12,6 +13,8 @@ func TestNopBreaker(t *testing.T) {
assert.Equal(t, nopBreakerName, b.Name()) assert.Equal(t, nopBreakerName, b.Name())
p, err := b.Allow() p, err := b.Allow()
assert.Nil(t, err) assert.Nil(t, err)
p, err = b.AllowCtx(context.Background())
assert.Nil(t, err)
p.Accept() p.Accept()
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
p, err := b.Allow() p, err := b.Allow()
@@ -21,18 +24,34 @@ func TestNopBreaker(t *testing.T) {
assert.Nil(t, b.Do(func() error { assert.Nil(t, b.Do(func() error {
return nil return nil
})) }))
assert.Nil(t, b.DoCtx(context.Background(), func() error {
return nil
}))
assert.Nil(t, b.DoWithAcceptable(func() error { assert.Nil(t, b.DoWithAcceptable(func() error {
return nil return nil
}, defaultAcceptable)) }, defaultAcceptable))
assert.Nil(t, b.DoWithAcceptableCtx(context.Background(), func() error {
return nil
}, defaultAcceptable))
errDummy := errors.New("any") errDummy := errors.New("any")
assert.Equal(t, errDummy, b.DoWithFallback(func() error { assert.Equal(t, errDummy, b.DoWithFallback(func() error {
return errDummy return errDummy
}, func(err error) error { }, func(err error) error {
return nil return nil
})) }))
assert.Equal(t, errDummy, b.DoWithFallbackCtx(context.Background(), func() error {
return errDummy
}, func(err error) error {
return nil
}))
assert.Equal(t, errDummy, b.DoWithFallbackAcceptable(func() error { assert.Equal(t, errDummy, b.DoWithFallbackAcceptable(func() error {
return errDummy return errDummy
}, func(err error) error { }, func(err error) error {
return nil return nil
}, defaultAcceptable)) }, defaultAcceptable))
assert.Equal(t, errDummy, b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
return errDummy
}, func(err error) error {
return nil
}, defaultAcceptable))
} }

View File

@@ -4,18 +4,28 @@ import (
"sync" "sync"
"time" "time"
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/timex" "github.com/zeromicro/go-zero/core/timex"
) )
type ( type (
// RollingWindowOption let callers customize the RollingWindow. // BucketInterface is the interface that defines the buckets.
RollingWindowOption func(rollingWindow *RollingWindow) BucketInterface[T Numerical] interface {
Add(v T)
Reset()
}
// RollingWindow defines a rolling window to calculate the events in buckets with time interval. // Numerical is the interface that restricts the numerical type.
RollingWindow struct { Numerical = mathx.Numerical
// RollingWindowOption let callers customize the RollingWindow.
RollingWindowOption[T Numerical, B BucketInterface[T]] func(rollingWindow *RollingWindow[T, B])
// RollingWindow defines a rolling window to calculate the events in buckets with the time interval.
RollingWindow[T Numerical, B BucketInterface[T]] struct {
lock sync.RWMutex lock sync.RWMutex
size int size int
win *window win *window[T, B]
interval time.Duration interval time.Duration
offset int offset int
ignoreCurrent bool ignoreCurrent bool
@@ -25,14 +35,15 @@ type (
// NewRollingWindow returns a RollingWindow that with size buckets and time interval, // NewRollingWindow returns a RollingWindow that with size buckets and time interval,
// use opts to customize the RollingWindow. // use opts to customize the RollingWindow.
func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOption) *RollingWindow { func NewRollingWindow[T Numerical, B BucketInterface[T]](newBucket func() B, size int,
interval time.Duration, opts ...RollingWindowOption[T, B]) *RollingWindow[T, B] {
if size < 1 { if size < 1 {
panic("size must be greater than 0") panic("size must be greater than 0")
} }
w := &RollingWindow{ w := &RollingWindow[T, B]{
size: size, size: size,
win: newWindow(size), win: newWindow[T, B](newBucket, size),
interval: interval, interval: interval,
lastTime: timex.Now(), lastTime: timex.Now(),
} }
@@ -43,7 +54,7 @@ func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOpt
} }
// Add adds value to current bucket. // Add adds value to current bucket.
func (rw *RollingWindow) Add(v float64) { func (rw *RollingWindow[T, B]) Add(v T) {
rw.lock.Lock() rw.lock.Lock()
defer rw.lock.Unlock() defer rw.lock.Unlock()
rw.updateOffset() rw.updateOffset()
@@ -51,13 +62,13 @@ func (rw *RollingWindow) Add(v float64) {
} }
// Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set. // Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set.
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) { func (rw *RollingWindow[T, B]) Reduce(fn func(b B)) {
rw.lock.RLock() rw.lock.RLock()
defer rw.lock.RUnlock() defer rw.lock.RUnlock()
var diff int var diff int
span := rw.span() span := rw.span()
// ignore current bucket, because of partial data // ignore the current bucket, because of partial data
if span == 0 && rw.ignoreCurrent { if span == 0 && rw.ignoreCurrent {
diff = rw.size - 1 diff = rw.size - 1
} else { } else {
@@ -69,7 +80,7 @@ func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
} }
} }
func (rw *RollingWindow) span() int { func (rw *RollingWindow[T, B]) span() int {
offset := int(timex.Since(rw.lastTime) / rw.interval) offset := int(timex.Since(rw.lastTime) / rw.interval)
if 0 <= offset && offset < rw.size { if 0 <= offset && offset < rw.size {
return offset return offset
@@ -78,7 +89,7 @@ func (rw *RollingWindow) span() int {
return rw.size return rw.size
} }
func (rw *RollingWindow) updateOffset() { func (rw *RollingWindow[T, B]) updateOffset() {
span := rw.span() span := rw.span()
if span <= 0 { if span <= 0 {
return return
@@ -97,54 +108,54 @@ func (rw *RollingWindow) updateOffset() {
} }
// Bucket defines the bucket that holds sum and num of additions. // Bucket defines the bucket that holds sum and num of additions.
type Bucket struct { type Bucket[T Numerical] struct {
Sum float64 Sum T
Count int64 Count int64
} }
func (b *Bucket) add(v float64) { func (b *Bucket[T]) Add(v T) {
b.Sum += v b.Sum += v
b.Count++ b.Count++
} }
func (b *Bucket) reset() { func (b *Bucket[T]) Reset() {
b.Sum = 0 b.Sum = 0
b.Count = 0 b.Count = 0
} }
type window struct { type window[T Numerical, B BucketInterface[T]] struct {
buckets []*Bucket buckets []B
size int size int
} }
func newWindow(size int) *window { func newWindow[T Numerical, B BucketInterface[T]](newBucket func() B, size int) *window[T, B] {
buckets := make([]*Bucket, size) buckets := make([]B, size)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
buckets[i] = new(Bucket) buckets[i] = newBucket()
} }
return &window{ return &window[T, B]{
buckets: buckets, buckets: buckets,
size: size, size: size,
} }
} }
func (w *window) add(offset int, v float64) { func (w *window[T, B]) add(offset int, v T) {
w.buckets[offset%w.size].add(v) w.buckets[offset%w.size].Add(v)
} }
func (w *window) reduce(start, count int, fn func(b *Bucket)) { func (w *window[T, B]) reduce(start, count int, fn func(b B)) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fn(w.buckets[(start+i)%w.size]) fn(w.buckets[(start+i)%w.size])
} }
} }
func (w *window) resetBucket(offset int) { func (w *window[T, B]) resetBucket(offset int) {
w.buckets[offset%w.size].reset() w.buckets[offset%w.size].Reset()
} }
// IgnoreCurrentBucket lets the Reduce call ignore current bucket. // IgnoreCurrentBucket lets the Reduce call ignore current bucket.
func IgnoreCurrentBucket() RollingWindowOption { func IgnoreCurrentBucket[T Numerical, B BucketInterface[T]]() RollingWindowOption[T, B] {
return func(w *RollingWindow) { return func(w *RollingWindow[T, B]) {
w.ignoreCurrent = true w.ignoreCurrent = true
} }
} }

View File

@@ -12,18 +12,24 @@ import (
const duration = time.Millisecond * 50 const duration = time.Millisecond * 50
func TestNewRollingWindow(t *testing.T) { func TestNewRollingWindow(t *testing.T) {
assert.NotNil(t, NewRollingWindow(10, time.Second)) assert.NotNil(t, NewRollingWindow[int64, *Bucket[int64]](func() *Bucket[int64] {
return new(Bucket[int64])
}, 10, time.Second))
assert.Panics(t, func() { assert.Panics(t, func() {
NewRollingWindow(0, time.Second) NewRollingWindow[int64, *Bucket[int64]](func() *Bucket[int64] {
return new(Bucket[int64])
}, 0, time.Second)
}) })
} }
func TestRollingWindowAdd(t *testing.T) { func TestRollingWindowAdd(t *testing.T) {
const size = 3 const size = 3
r := NewRollingWindow(size, duration) r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, duration)
listBuckets := func() []float64 { listBuckets := func() []float64 {
var buckets []float64 var buckets []float64
r.Reduce(func(b *Bucket) { r.Reduce(func(b *Bucket[float64]) {
buckets = append(buckets, b.Sum) buckets = append(buckets, b.Sum)
}) })
return buckets return buckets
@@ -47,10 +53,12 @@ func TestRollingWindowAdd(t *testing.T) {
func TestRollingWindowReset(t *testing.T) { func TestRollingWindowReset(t *testing.T) {
const size = 3 const size = 3
r := NewRollingWindow(size, duration, IgnoreCurrentBucket()) r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, duration, IgnoreCurrentBucket[float64, *Bucket[float64]]())
listBuckets := func() []float64 { listBuckets := func() []float64 {
var buckets []float64 var buckets []float64
r.Reduce(func(b *Bucket) { r.Reduce(func(b *Bucket[float64]) {
buckets = append(buckets, b.Sum) buckets = append(buckets, b.Sum)
}) })
return buckets return buckets
@@ -72,15 +80,19 @@ func TestRollingWindowReset(t *testing.T) {
func TestRollingWindowReduce(t *testing.T) { func TestRollingWindowReduce(t *testing.T) {
const size = 4 const size = 4
tests := []struct { tests := []struct {
win *RollingWindow win *RollingWindow[float64, *Bucket[float64]]
expect float64 expect float64
}{ }{
{ {
win: NewRollingWindow(size, duration), win: NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, duration),
expect: 10, expect: 10,
}, },
{ {
win: NewRollingWindow(size, duration, IgnoreCurrentBucket()), win: NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, duration, IgnoreCurrentBucket[float64, *Bucket[float64]]()),
expect: 4, expect: 4,
}, },
} }
@@ -97,7 +109,7 @@ func TestRollingWindowReduce(t *testing.T) {
} }
} }
var result float64 var result float64
r.Reduce(func(b *Bucket) { r.Reduce(func(b *Bucket[float64]) {
result += b.Sum result += b.Sum
}) })
assert.Equal(t, test.expect, result) assert.Equal(t, test.expect, result)
@@ -108,10 +120,12 @@ func TestRollingWindowReduce(t *testing.T) {
func TestRollingWindowBucketTimeBoundary(t *testing.T) { func TestRollingWindowBucketTimeBoundary(t *testing.T) {
const size = 3 const size = 3
interval := time.Millisecond * 30 interval := time.Millisecond * 30
r := NewRollingWindow(size, interval) r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, interval)
listBuckets := func() []float64 { listBuckets := func() []float64 {
var buckets []float64 var buckets []float64
r.Reduce(func(b *Bucket) { r.Reduce(func(b *Bucket[float64]) {
buckets = append(buckets, b.Sum) buckets = append(buckets, b.Sum)
}) })
return buckets return buckets
@@ -138,7 +152,9 @@ func TestRollingWindowBucketTimeBoundary(t *testing.T) {
func TestRollingWindowDataRace(t *testing.T) { func TestRollingWindowDataRace(t *testing.T) {
const size = 3 const size = 3
r := NewRollingWindow(size, duration) r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
return new(Bucket[float64])
}, size, duration)
stop := make(chan bool) stop := make(chan bool)
go func() { go func() {
for { for {
@@ -157,7 +173,7 @@ func TestRollingWindowDataRace(t *testing.T) {
case <-stop: case <-stop:
return return
default: default:
r.Reduce(func(b *Bucket) {}) r.Reduce(func(b *Bucket[float64]) {})
} }
} }
}() }()

View File

@@ -189,7 +189,7 @@ func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) {
switch tp.Kind() { switch tp.Kind() {
case reflect.Struct: case reflect.Struct:
return buildStructFieldsInfo(tp, fullName) return buildStructFieldsInfo(tp, fullName)
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice, reflect.Map:
return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName) return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName)
case reflect.Chan, reflect.Func: case reflect.Chan, reflect.Func:
return nil, fmt.Errorf("unsupported type: %s", tp.Kind()) return nil, fmt.Errorf("unsupported type: %s", tp.Kind())
@@ -332,6 +332,8 @@ func toLowerCaseKeyMap(m map[string]any, info *fieldInfo) map[string]any {
res[lk] = toLowerCaseInterface(v, ti) res[lk] = toLowerCaseInterface(v, ti)
} else if info.mapField != nil { } else if info.mapField != nil {
res[k] = toLowerCaseInterface(v, info.mapField) res[k] = toLowerCaseInterface(v, info.mapField)
} else if vv, ok := v.(map[string]any); ok {
res[k] = toLowerCaseKeyMap(vv, info)
} else { } else {
res[k] = v res[k] = v
} }

View File

@@ -1192,6 +1192,29 @@ Email = "bar"`)
assert.Len(t, c.Value, 2) assert.Len(t, c.Value, 2)
} }
}) })
t.Run("multi layer map", func(t *testing.T) {
type Value struct {
User struct {
Name string
}
}
type Config struct {
Value map[string]map[string]Value
}
var input = []byte(`
[Value.first.User1.User]
Name = "foo"
[Value.second.User2.User]
Name = "bar"
`)
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.Value, 2)
}
})
} }
func Test_getFullName(t *testing.T) { func Test_getFullName(t *testing.T) {

View File

@@ -0,0 +1,200 @@
package configurator
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"sync/atomic"
"github.com/zeromicro/go-zero/core/configcenter/subscriber"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mapping"
"github.com/zeromicro/go-zero/core/threading"
)
var (
errEmptyConfig = errors.New("empty config value")
errMissingUnmarshalerType = errors.New("missing unmarshaler type")
)
// Configurator is the interface for configuration center.
type Configurator[T any] interface {
// GetConfig returns the subscription value.
GetConfig() (T, error)
// AddListener adds a listener to the subscriber.
AddListener(listener func())
}
type (
// Config is the configuration for Configurator.
Config struct {
// Type is the value type, yaml, json or toml.
Type string `json:",default=yaml,options=[yaml,json,toml]"`
// Log is the flag to control logging.
Log bool `json:",default=true"`
}
configCenter[T any] struct {
conf Config
unmarshaler LoaderFn
subscriber subscriber.Subscriber
listeners []func()
lock sync.Mutex
snapshot atomic.Value
}
value[T any] struct {
data string
marshalData T
err error
}
)
// Configurator is the interface for configuration center.
var _ Configurator[any] = (*configCenter[any])(nil)
// MustNewConfigCenter returns a Configurator, exits on errors.
func MustNewConfigCenter[T any](c Config, subscriber subscriber.Subscriber) Configurator[T] {
cc, err := NewConfigCenter[T](c, subscriber)
logx.Must(err)
return cc
}
// NewConfigCenter returns a Configurator.
func NewConfigCenter[T any](c Config, subscriber subscriber.Subscriber) (Configurator[T], error) {
unmarshaler, ok := Unmarshaler(strings.ToLower(c.Type))
if !ok {
return nil, fmt.Errorf("unknown format: %s", c.Type)
}
cc := &configCenter[T]{
conf: c,
unmarshaler: unmarshaler,
subscriber: subscriber,
}
if err := cc.loadConfig(); err != nil {
return nil, err
}
if err := cc.subscriber.AddListener(cc.onChange); err != nil {
return nil, err
}
if _, err := cc.GetConfig(); err != nil {
return nil, err
}
return cc, nil
}
// AddListener adds listener to s.
func (c *configCenter[T]) AddListener(listener func()) {
c.lock.Lock()
defer c.lock.Unlock()
c.listeners = append(c.listeners, listener)
}
// GetConfig return structured config.
func (c *configCenter[T]) GetConfig() (T, error) {
v := c.value()
if v == nil || len(v.data) == 0 {
var empty T
return empty, errEmptyConfig
}
return v.marshalData, v.err
}
// Value returns the subscription value.
func (c *configCenter[T]) Value() string {
v := c.value()
if v == nil {
return ""
}
return v.data
}
func (c *configCenter[T]) loadConfig() error {
v, err := c.subscriber.Value()
if err != nil {
if c.conf.Log {
logx.Errorf("ConfigCenter loads changed configuration, error: %v", err)
}
return err
}
if c.conf.Log {
logx.Infof("ConfigCenter loads changed configuration, content [%s]", v)
}
c.snapshot.Store(c.genValue(v))
return nil
}
func (c *configCenter[T]) onChange() {
if err := c.loadConfig(); err != nil {
return
}
c.lock.Lock()
listeners := make([]func(), len(c.listeners))
copy(listeners, c.listeners)
c.lock.Unlock()
for _, l := range listeners {
threading.GoSafe(l)
}
}
func (c *configCenter[T]) value() *value[T] {
content := c.snapshot.Load()
if content == nil {
return nil
}
return content.(*value[T])
}
func (c *configCenter[T]) genValue(data string) *value[T] {
v := &value[T]{
data: data,
}
if len(data) == 0 {
return v
}
t := reflect.TypeOf(v.marshalData)
// if the type is nil, it means that the user has not set the type of the configuration.
if t == nil {
v.err = errMissingUnmarshalerType
return v
}
t = mapping.Deref(t)
switch t.Kind() {
case reflect.Struct, reflect.Array, reflect.Slice:
if err := c.unmarshaler([]byte(data), &v.marshalData); err != nil {
v.err = err
if c.conf.Log {
logx.Errorf("ConfigCenter unmarshal configuration failed, err: %+v, content [%s]",
err.Error(), data)
}
}
case reflect.String:
if str, ok := any(data).(T); ok {
v.marshalData = str
} else {
v.err = errMissingUnmarshalerType
}
default:
if c.conf.Log {
logx.Errorf("ConfigCenter unmarshal configuration missing unmarshaler for type: %s, content [%s]",
t.Kind(), data)
}
v.err = errMissingUnmarshalerType
}
return v
}

View File

@@ -0,0 +1,233 @@
package configurator
import (
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewConfigCenter(t *testing.T) {
_, err := NewConfigCenter[any](Config{
Log: true,
}, &mockSubscriber{})
assert.Error(t, err)
_, err = NewConfigCenter[any](Config{
Type: "json",
Log: true,
}, &mockSubscriber{})
assert.Error(t, err)
}
func TestConfigCenter_GetConfig(t *testing.T) {
mock := &mockSubscriber{}
type Data struct {
Name string `json:"name"`
}
mock.v = `{"name": "go-zero"}`
c1, err := NewConfigCenter[Data](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
data, err := c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero", data.Name)
mock.v = `{"name": "111"}`
c2, err := NewConfigCenter[Data](Config{Type: "json"}, mock)
assert.NoError(t, err)
mock.v = `{}`
c3, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
_, err = c3.GetConfig()
assert.NoError(t, err)
data, err = c2.GetConfig()
assert.NoError(t, err)
mock.lisErr = errors.New("mock error")
_, err = NewConfigCenter[Data](Config{
Type: "json",
Log: true,
}, mock)
assert.Error(t, err)
}
func TestConfigCenter_onChange(t *testing.T) {
mock := &mockSubscriber{}
type Data struct {
Name string `json:"name"`
}
mock.v = `{"name": "go-zero"}`
c1, err := NewConfigCenter[Data](Config{Type: "json", Log: true}, mock)
assert.NoError(t, err)
data, err := c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero", data.Name)
mock.v = `{"name": "go-zero2"}`
mock.change()
data, err = c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero2", data.Name)
mock.valErr = errors.New("mock error")
_, err = NewConfigCenter[Data](Config{Type: "json", Log: false}, mock)
assert.Error(t, err)
}
func TestConfigCenter_Value(t *testing.T) {
mock := &mockSubscriber{}
mock.v = "1234"
c, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
cc := c.(*configCenter[string])
assert.Equal(t, cc.Value(), "1234")
mock.valErr = errors.New("mock error")
_, err = NewConfigCenter[any](Config{
Type: "json",
Log: true,
}, mock)
assert.Error(t, err)
}
func TestConfigCenter_AddListener(t *testing.T) {
mock := &mockSubscriber{}
mock.v = "1234"
c, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
cc := c.(*configCenter[string])
var a, b int
var mutex sync.Mutex
cc.AddListener(func() {
mutex.Lock()
a = 1
mutex.Unlock()
})
cc.AddListener(func() {
mutex.Lock()
b = 2
mutex.Unlock()
})
assert.Equal(t, 2, len(cc.listeners))
mock.change()
time.Sleep(time.Millisecond * 100)
mutex.Lock()
assert.Equal(t, 1, a)
assert.Equal(t, 2, b)
mutex.Unlock()
}
func TestConfigCenter_genValue(t *testing.T) {
t.Run("data is empty", func(t *testing.T) {
c := &configCenter[string]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("")
assert.Equal(t, "", v.data)
})
t.Run("invalid template type", func(t *testing.T) {
c := &configCenter[any]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("xxxx")
assert.Equal(t, errMissingUnmarshalerType, v.err)
})
t.Run("unsupported template type", func(t *testing.T) {
c := &configCenter[int]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("1")
assert.Equal(t, errMissingUnmarshalerType, v.err)
})
t.Run("supported template string type", func(t *testing.T) {
c := &configCenter[string]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("12345")
assert.NoError(t, v.err)
assert.Equal(t, "12345", v.data)
})
t.Run("unmarshal fail", func(t *testing.T) {
c := &configCenter[struct {
Name string `json:"name"`
}]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue(`{"name":"new name}`)
assert.Equal(t, `{"name":"new name}`, v.data)
assert.Error(t, v.err)
})
t.Run("success", func(t *testing.T) {
c := &configCenter[struct {
Name string `json:"name"`
}]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue(`{"name":"new name"}`)
assert.Equal(t, `{"name":"new name"}`, v.data)
assert.Equal(t, "new name", v.marshalData.Name)
assert.NoError(t, v.err)
})
}
type mockSubscriber struct {
v string
lisErr, valErr error
listener func()
}
func (m *mockSubscriber) AddListener(listener func()) error {
m.listener = listener
return m.lisErr
}
func (m *mockSubscriber) Value() (string, error) {
return m.v, m.valErr
}
func (m *mockSubscriber) change() {
if m.listener != nil {
m.listener()
}
}

View File

@@ -0,0 +1,67 @@
package subscriber
import (
"github.com/zeromicro/go-zero/core/discov"
"github.com/zeromicro/go-zero/core/logx"
)
type (
// etcdSubscriber is a subscriber that subscribes to etcd.
etcdSubscriber struct {
*discov.Subscriber
}
// EtcdConf is the configuration for etcd.
EtcdConf = discov.EtcdConf
)
// MustNewEtcdSubscriber returns an etcd Subscriber, exits on errors.
func MustNewEtcdSubscriber(conf EtcdConf) Subscriber {
s, err := NewEtcdSubscriber(conf)
logx.Must(err)
return s
}
// NewEtcdSubscriber returns an etcd Subscriber.
func NewEtcdSubscriber(conf EtcdConf) (Subscriber, error) {
opts := buildSubOptions(conf)
s, err := discov.NewSubscriber(conf.Hosts, conf.Key, opts...)
if err != nil {
return nil, err
}
return &etcdSubscriber{Subscriber: s}, nil
}
// buildSubOptions constructs the options for creating a new etcd subscriber.
func buildSubOptions(conf EtcdConf) []discov.SubOption {
opts := []discov.SubOption{
discov.WithExactMatch(),
}
if len(conf.User) > 0 {
opts = append(opts, discov.WithSubEtcdAccount(conf.User, conf.Pass))
}
if len(conf.CertFile) > 0 || len(conf.CertKeyFile) > 0 || len(conf.CACertFile) > 0 {
opts = append(opts, discov.WithSubEtcdTLS(conf.CertFile, conf.CertKeyFile,
conf.CACertFile, conf.InsecureSkipVerify))
}
return opts
}
// AddListener adds a listener to the subscriber.
func (s *etcdSubscriber) AddListener(listener func()) error {
s.Subscriber.AddListener(listener)
return nil
}
// Value returns the value of the subscriber.
func (s *etcdSubscriber) Value() (string, error) {
vs := s.Subscriber.Values()
if len(vs) > 0 {
return vs[len(vs)-1], nil
}
return "", nil
}

View File

@@ -0,0 +1,9 @@
package subscriber
// Subscriber is the interface for configcenter subscribers.
type Subscriber interface {
// AddListener adds a listener to the subscriber.
AddListener(listener func()) error
// Value returns the value of the subscriber.
Value() (string, error)
}

View File

@@ -0,0 +1,41 @@
package configurator
import (
"sync"
"github.com/zeromicro/go-zero/core/conf"
)
var registry = &unmarshalerRegistry{
unmarshalers: map[string]LoaderFn{
"json": conf.LoadFromJsonBytes,
"toml": conf.LoadFromTomlBytes,
"yaml": conf.LoadFromYamlBytes,
},
}
type (
// LoaderFn is the function type for loading configuration.
LoaderFn func([]byte, any) error
// unmarshalerRegistry is the registry for unmarshalers.
unmarshalerRegistry struct {
unmarshalers map[string]LoaderFn
mu sync.RWMutex
}
)
// RegisterUnmarshaler registers an unmarshaler.
func RegisterUnmarshaler(name string, fn LoaderFn) {
registry.mu.Lock()
defer registry.mu.Unlock()
registry.unmarshalers[name] = fn
}
// Unmarshaler returns the unmarshaler by name.
func Unmarshaler(name string) (LoaderFn, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
fn, ok := registry.unmarshalers[name]
return fn, ok
}

View File

@@ -0,0 +1,28 @@
package configurator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRegisterUnmarshaler(t *testing.T) {
RegisterUnmarshaler("test", func(data []byte, v interface{}) error {
return nil
})
_, ok := Unmarshaler("test")
assert.True(t, ok)
_, ok = Unmarshaler("test2")
assert.False(t, ok)
_, ok = Unmarshaler("json")
assert.True(t, ok)
_, ok = Unmarshaler("toml")
assert.True(t, ok)
_, ok = Unmarshaler("yaml")
assert.True(t, ok)
}

View File

@@ -10,13 +10,14 @@ import (
"sync" "sync"
"time" "time"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/zeromicro/go-zero/core/contextx" "github.com/zeromicro/go-zero/core/contextx"
"github.com/zeromicro/go-zero/core/lang" "github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/syncx" "github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/threading" "github.com/zeromicro/go-zero/core/threading"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
clientv3 "go.etcd.io/etcd/client/v3"
) )
var ( var (
@@ -30,7 +31,7 @@ var (
// A Registry is a registry that manages the etcd client connections. // A Registry is a registry that manages the etcd client connections.
type Registry struct { type Registry struct {
clusters map[string]*cluster clusters map[string]*cluster
lock sync.Mutex lock sync.RWMutex
} }
// GetRegistry returns a global Registry. // GetRegistry returns a global Registry.
@@ -45,7 +46,7 @@ func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
} }
// Monitor monitors the key on given etcd endpoints, notify with the given UpdateListener. // Monitor monitors the key on given etcd endpoints, notify with the given UpdateListener.
func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener) error { func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener, exactMatch bool) error {
c, exists := r.getCluster(endpoints) c, exists := r.getCluster(endpoints)
// if exists, the existing values should be updated to the listener. // if exists, the existing values should be updated to the listener.
if exists { if exists {
@@ -55,17 +56,24 @@ func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener) err
} }
} }
return c.monitor(key, l) return c.monitor(key, l, exactMatch)
} }
func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) { func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) {
clusterKey := getClusterKey(endpoints) clusterKey := getClusterKey(endpoints)
r.lock.Lock() r.lock.RLock()
defer r.lock.Unlock()
c, exists = r.clusters[clusterKey] c, exists = r.clusters[clusterKey]
r.lock.RUnlock()
if !exists { if !exists {
c = newCluster(endpoints) r.lock.Lock()
r.clusters[clusterKey] = c defer r.lock.Unlock()
// double-check locking
c, exists = r.clusters[clusterKey]
if !exists {
c = newCluster(endpoints)
r.clusters[clusterKey] = c
}
} }
return return
@@ -78,7 +86,8 @@ type cluster struct {
listeners map[string][]UpdateListener listeners map[string][]UpdateListener
watchGroup *threading.RoutineGroup watchGroup *threading.RoutineGroup
done chan lang.PlaceholderType done chan lang.PlaceholderType
lock sync.Mutex lock sync.RWMutex
exactMatch bool
} }
func newCluster(endpoints []string) *cluster { func newCluster(endpoints []string) *cluster {
@@ -108,8 +117,8 @@ func (c *cluster) getClient() (EtcdClient, error) {
} }
func (c *cluster) getCurrent(key string) []KV { func (c *cluster) getCurrent(key string) []KV {
c.lock.Lock() c.lock.RLock()
defer c.lock.Unlock() defer c.lock.RUnlock()
var kvs []KV var kvs []KV
for k, v := range c.values[key] { for k, v := range c.values[key] {
@@ -125,6 +134,7 @@ func (c *cluster) getCurrent(key string) []KV {
func (c *cluster) handleChanges(key string, kvs []KV) { func (c *cluster) handleChanges(key string, kvs []KV) {
var add []KV var add []KV
var remove []KV var remove []KV
c.lock.Lock() c.lock.Lock()
listeners := append([]UpdateListener(nil), c.listeners[key]...) listeners := append([]UpdateListener(nil), c.listeners[key]...)
vals, ok := c.values[key] vals, ok := c.values[key]
@@ -173,9 +183,9 @@ func (c *cluster) handleChanges(key string, kvs []KV) {
} }
func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) { func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
c.lock.Lock() c.lock.RLock()
listeners := append([]UpdateListener(nil), c.listeners[key]...) listeners := append([]UpdateListener(nil), c.listeners[key]...)
c.lock.Unlock() c.lock.RUnlock()
for _, ev := range events { for _, ev := range events {
switch ev.Type { switch ev.Type {
@@ -216,7 +226,12 @@ func (c *cluster) load(cli EtcdClient, key string) int64 {
for { for {
var err error var err error
ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout) ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout)
resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix()) if c.exactMatch {
resp, err = cli.Get(ctx, key)
} else {
resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix())
}
cancel() cancel()
if err == nil { if err == nil {
break break
@@ -239,9 +254,10 @@ func (c *cluster) load(cli EtcdClient, key string) int64 {
return resp.Header.Revision return resp.Header.Revision
} }
func (c *cluster) monitor(key string, l UpdateListener) error { func (c *cluster) monitor(key string, l UpdateListener, exactMatch bool) error {
c.lock.Lock() c.lock.Lock()
c.listeners[key] = append(c.listeners[key], l) c.listeners[key] = append(c.listeners[key], l)
c.exactMatch = exactMatch
c.lock.Unlock() c.lock.Unlock()
cli, err := c.getClient() cli, err := c.getClient()
@@ -307,14 +323,20 @@ func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
} }
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error { func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error {
var rch clientv3.WatchChan var (
if rev != 0 { rch clientv3.WatchChan
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), ops []clientv3.OpOption
clientv3.WithPrefix(), clientv3.WithRev(rev+1)) watchKey = key
} else { )
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), if !c.exactMatch {
clientv3.WithPrefix()) watchKey = makeKeyPrefix(key)
ops = append(ops, clientv3.WithPrefix())
} }
if rev != 0 {
ops = append(ops, clientv3.WithRev(rev+1))
}
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), watchKey, ops...)
for { for {
select { select {

View File

@@ -289,7 +289,7 @@ func TestRegistry_Monitor(t *testing.T) {
}, },
} }
GetRegistry().lock.Unlock() GetRegistry().lock.Unlock()
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", new(mockListener))) assert.Error(t, GetRegistry().Monitor(endpoints, "foo", new(mockListener), false))
} }
type mockListener struct { type mockListener struct {

View File

@@ -15,9 +15,10 @@ type (
// A Subscriber is used to subscribe the given key on an etcd cluster. // A Subscriber is used to subscribe the given key on an etcd cluster.
Subscriber struct { Subscriber struct {
endpoints []string endpoints []string
exclusive bool exclusive bool
items *container exactMatch bool
items *container
} }
) )
@@ -34,7 +35,7 @@ func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscrib
} }
sub.items = newContainer(sub.exclusive) sub.items = newContainer(sub.exclusive)
if err := internal.GetRegistry().Monitor(endpoints, key, sub.items); err != nil { if err := internal.GetRegistry().Monitor(endpoints, key, sub.items, sub.exactMatch); err != nil {
return nil, err return nil, err
} }
@@ -59,6 +60,13 @@ func Exclusive() SubOption {
} }
} }
// WithExactMatch turn off querying using key prefixes.
func WithExactMatch() SubOption {
return func(sub *Subscriber) {
sub.exactMatch = true
}
}
// WithSubEtcdAccount provides the etcd username/password. // WithSubEtcdAccount provides the etcd username/password.
func WithSubEtcdAccount(user, pass string) SubOption { func WithSubEtcdAccount(user, pass string) SubOption {
return func(sub *Subscriber) { return func(sub *Subscriber) {

View File

@@ -1,21 +1,17 @@
package errorx package errorx
import ( import (
"bytes" "errors"
"sync" "sync"
) )
type ( // BatchError is an error that can hold multiple errors.
// A BatchError is an error that can hold multiple errors. type BatchError struct {
BatchError struct { errs []error
errs errorArray lock sync.RWMutex
lock sync.Mutex }
}
errorArray []error // Add adds one or more non-nil errors to the BatchError instance.
)
// Add adds errs to be, nil errors are ignored.
func (be *BatchError) Add(errs ...error) { func (be *BatchError) Add(errs ...error) {
be.lock.Lock() be.lock.Lock()
defer be.lock.Unlock() defer be.lock.Unlock()
@@ -27,39 +23,20 @@ func (be *BatchError) Add(errs ...error) {
} }
} }
// Err returns an error that represents all errors. // Err returns an error that represents all accumulated errors.
// It returns nil if there are no errors.
func (be *BatchError) Err() error { func (be *BatchError) Err() error {
be.lock.Lock() be.lock.RLock()
defer be.lock.Unlock() defer be.lock.RUnlock()
switch len(be.errs) { // If there are no non-nil errors, errors.Join(...) returns nil.
case 0: return errors.Join(be.errs...)
return nil
case 1:
return be.errs[0]
default:
return be.errs
}
} }
// NotNil checks if any error inside. // NotNil checks if there is at least one error inside the BatchError.
func (be *BatchError) NotNil() bool { func (be *BatchError) NotNil() bool {
be.lock.Lock() be.lock.RLock()
defer be.lock.Unlock() defer be.lock.RUnlock()
return len(be.errs) > 0 return len(be.errs) > 0
} }
// Error returns a string that represents inside errors.
func (ea errorArray) Error() string {
var buf bytes.Buffer
for i := range ea {
if i > 0 {
buf.WriteByte('\n')
}
buf.WriteString(ea[i].Error())
}
return buf.String()
}

View File

@@ -66,3 +66,82 @@ func TestBatchErrorConcurrentAdd(t *testing.T) {
assert.Equal(t, count, len(batch.errs)) assert.Equal(t, count, len(batch.errs))
assert.True(t, batch.NotNil()) assert.True(t, batch.NotNil())
} }
func TestBatchError_Unwrap(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var be BatchError
assert.Nil(t, be.Err())
assert.True(t, errors.Is(be.Err(), nil))
})
t.Run("one error", func(t *testing.T) {
var errFoo = errors.New("foo")
var errBar = errors.New("bar")
var be BatchError
be.Add(errFoo)
assert.True(t, errors.Is(be.Err(), errFoo))
assert.False(t, errors.Is(be.Err(), errBar))
})
t.Run("two errors", func(t *testing.T) {
var errFoo = errors.New("foo")
var errBar = errors.New("bar")
var errBaz = errors.New("baz")
var be BatchError
be.Add(errFoo)
be.Add(errBar)
assert.True(t, errors.Is(be.Err(), errFoo))
assert.True(t, errors.Is(be.Err(), errBar))
assert.False(t, errors.Is(be.Err(), errBaz))
})
}
func TestBatchError_Add(t *testing.T) {
var be BatchError
// Test adding nil errors
be.Add(nil, nil)
assert.False(t, be.NotNil(), "Expected BatchError to be empty after adding nil errors")
// Test adding non-nil errors
err1 := errors.New("error 1")
err2 := errors.New("error 2")
be.Add(err1, err2)
assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding errors")
// Test adding a mix of nil and non-nil errors
err3 := errors.New("error 3")
be.Add(nil, err3, nil)
assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding a mix of nil and non-nil errors")
}
func TestBatchError_Err(t *testing.T) {
var be BatchError
// Test Err() on empty BatchError
assert.Nil(t, be.Err(), "Expected nil error for empty BatchError")
// Test Err() with multiple errors
err1 := errors.New("error 1")
err2 := errors.New("error 2")
be.Add(err1, err2)
combinedErr := be.Err()
assert.NotNil(t, combinedErr, "Expected nil error for BatchError with multiple errors")
// Check if the combined error contains both error messages
errString := combinedErr.Error()
assert.Truef(t, errors.Is(combinedErr, err1), "Combined error doesn't contain first error: %s", errString)
assert.Truef(t, errors.Is(combinedErr, err2), "Combined error doesn't contain second error: %s", errString)
}
func TestBatchError_NotNil(t *testing.T) {
var be BatchError
// Test NotNil() on empty BatchError
assert.Nil(t, be.Err(), "Expected nil error for empty BatchError")
// Test NotNil() after adding an error
be.Add(errors.New("test error"))
assert.NotNil(t, be.Err(), "Expected non-nil error after adding an error")
}

14
core/errorx/check.go Normal file
View File

@@ -0,0 +1,14 @@
package errorx
import "errors"
// In checks if the given err is one of errs.
func In(err error, errs ...error) bool {
for _, each := range errs {
if errors.Is(err, each) {
return true
}
}
return false
}

70
core/errorx/check_test.go Normal file
View File

@@ -0,0 +1,70 @@
package errorx
import (
"errors"
"testing"
)
func TestIn(t *testing.T) {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
err3 := errors.New("error 3")
tests := []struct {
name string
err error
errs []error
want bool
}{
{
name: "Error matches one of the errors in the list",
err: err1,
errs: []error{err1, err2},
want: true,
},
{
name: "Error does not match any errors in the list",
err: err3,
errs: []error{err1, err2},
want: false,
},
{
name: "Empty error list",
err: err1,
errs: []error{},
want: false,
},
{
name: "Nil error with non-nil list",
err: nil,
errs: []error{err1, err2},
want: false,
},
{
name: "Non-nil error with nil in list",
err: err1,
errs: []error{nil, err2},
want: false,
},
{
name: "Error matches nil error in the list",
err: nil,
errs: []error{nil, err2},
want: true,
},
{
name: "Nil error with empty list",
err: nil,
errs: []error{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := In(tt.err, tt.errs...); got != tt.want {
t.Errorf("In() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -35,6 +35,7 @@ func firstLine(file *os.File) (string, error) {
for { for {
buf := make([]byte, bufSize) buf := make([]byte, bufSize)
n, err := file.ReadAt(buf, offset) n, err := file.ReadAt(buf, offset)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return "", err return "", err
} }
@@ -45,6 +46,10 @@ func firstLine(file *os.File) (string, error) {
} }
} }
if err == io.EOF {
return string(append(first, buf[:n]...)), nil
}
first = append(first, buf[:n]...) first = append(first, buf[:n]...)
offset += bufSize offset += bufSize
} }
@@ -57,30 +62,42 @@ func lastLine(filename string, file *os.File) (string, error) {
} }
var last []byte var last []byte
bufLen := int64(bufSize)
offset := info.Size() offset := info.Size()
for {
offset -= bufSize for offset > 0 {
if offset < 0 { if offset < bufLen {
bufLen = offset
offset = 0 offset = 0
} else {
offset -= bufLen
} }
buf := make([]byte, bufSize)
buf := make([]byte, bufLen)
n, err := file.ReadAt(buf, offset) n, err := file.ReadAt(buf, offset)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return "", err return "", err
} }
if n == 0 {
break
}
if buf[n-1] == '\n' { if buf[n-1] == '\n' {
buf = buf[:n-1] buf = buf[:n-1]
n-- n--
} else { } else {
buf = buf[:n] buf = buf[:n]
} }
for n--; n >= 0; n-- {
if buf[n] == '\n' { for i := n - 1; i >= 0; i-- {
return string(append(buf[n+1:], last...)), nil if buf[i] == '\n' {
return string(append(buf[i+1:], last...)), nil
} }
} }
last = append(buf, last...) last = append(buf, last...)
} }
return string(last), nil
} }

View File

@@ -52,6 +52,7 @@ last line`
second line second line
last line last line
` `
emptyContent = ``
) )
func TestFirstLine(t *testing.T) { func TestFirstLine(t *testing.T) {
@@ -79,6 +80,26 @@ func TestFirstLineError(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestFirstLineEmptyFile(t *testing.T) {
filename, err := fs.TempFilenameWithText(emptyContent)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := FirstLine(filename)
assert.Nil(t, err)
assert.Equal(t, "", val)
}
func TestFirstLineWithoutNewline(t *testing.T) {
filename, err := fs.TempFilenameWithText(longLine)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := FirstLine(filename)
assert.Nil(t, err)
assert.Equal(t, longLine, val)
}
func TestLastLine(t *testing.T) { func TestLastLine(t *testing.T) {
filename, err := fs.TempFilenameWithText(text) filename, err := fs.TempFilenameWithText(text)
assert.Nil(t, err) assert.Nil(t, err)
@@ -99,6 +120,16 @@ func TestLastLineWithLastNewline(t *testing.T) {
assert.Equal(t, longLine, val) assert.Equal(t, longLine, val)
} }
func TestLastLineWithoutLastNewline(t *testing.T) {
filename, err := fs.TempFilenameWithText(longLine)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := LastLine(filename)
assert.Nil(t, err)
assert.Equal(t, longLine, val)
}
func TestLastLineShort(t *testing.T) { func TestLastLineShort(t *testing.T) {
filename, err := fs.TempFilenameWithText(shortText) filename, err := fs.TempFilenameWithText(shortText)
assert.Nil(t, err) assert.Nil(t, err)
@@ -123,3 +154,67 @@ func TestLastLineError(t *testing.T) {
_, err := LastLine("/tmp/does-not-exist") _, err := LastLine("/tmp/does-not-exist")
assert.Error(t, err) assert.Error(t, err)
} }
func TestLastLineEmptyFile(t *testing.T) {
filename, err := fs.TempFilenameWithText(emptyContent)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := LastLine(filename)
assert.Nil(t, err)
assert.Equal(t, "", val)
}
func TestFirstLineExactlyBufSize(t *testing.T) {
content := make([]byte, bufSize)
for i := range content {
content[i] = 'a'
}
content[bufSize-1] = '\n' // Ensure there is a newline at the edge
filename, err := fs.TempFilenameWithText(string(content))
assert.Nil(t, err)
defer os.Remove(filename)
val, err := FirstLine(filename)
assert.Nil(t, err)
assert.Equal(t, string(content[:bufSize-1]), val)
}
func TestLastLineExactlyBufSize(t *testing.T) {
content := make([]byte, bufSize)
for i := range content {
content[i] = 'a'
}
content[bufSize-1] = '\n' // Ensure there is a newline at the edge
filename, err := fs.TempFilenameWithText(string(content))
assert.Nil(t, err)
defer os.Remove(filename)
val, err := LastLine(filename)
assert.Nil(t, err)
assert.Equal(t, string(content[:bufSize-1]), val)
}
func TestFirstLineLargeFile(t *testing.T) {
content := text + text + text + "\n" + "extra"
filename, err := fs.TempFilenameWithText(content)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := FirstLine(filename)
assert.Nil(t, err)
assert.Equal(t, "first line", val)
}
func TestLastLineLargeFile(t *testing.T) {
content := text + text + text + "\n" + "extra"
filename, err := fs.TempFilenameWithText(content)
assert.Nil(t, err)
defer os.Remove(filename)
val, err := LastLine(filename)
assert.Nil(t, err)
assert.Equal(t, "extra", val)
}

View File

@@ -1,6 +1,9 @@
package fx package fx
import "github.com/zeromicro/go-zero/core/threading" import (
"github.com/zeromicro/go-zero/core/errorx"
"github.com/zeromicro/go-zero/core/threading"
)
// Parallel runs fns parallelly and waits for done. // Parallel runs fns parallelly and waits for done.
func Parallel(fns ...func()) { func Parallel(fns ...func()) {
@@ -10,3 +13,20 @@ func Parallel(fns ...func()) {
} }
group.Wait() group.Wait()
} }
func ParallelErr(fns ...func() error) error {
var be errorx.BatchError
group := threading.NewRoutineGroup()
for _, fn := range fns {
f := fn
group.RunSafe(func() {
if err := f(); err != nil {
be.Add(err)
}
})
}
group.Wait()
return be.Err()
}

View File

@@ -1,6 +1,7 @@
package fx package fx
import ( import (
"errors"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@@ -22,3 +23,54 @@ func TestParallel(t *testing.T) {
}) })
assert.Equal(t, int32(6), count) assert.Equal(t, int32(6), count)
} }
func TestParallelErr(t *testing.T) {
var count int32
err := ParallelErr(
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 1)
return errors.New("failed to exec #1")
},
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 2)
return errors.New("failed to exec #2")
},
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 3)
return nil
},
)
assert.Equal(t, int32(6), count)
assert.Error(t, err)
assert.ErrorContains(t, err, "failed to exec #1", "failed to exec #2")
}
func TestParallelErrErrorNil(t *testing.T) {
var count int32
err := ParallelErr(
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 1)
return nil
},
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 2)
return nil
},
func() error {
time.Sleep(time.Millisecond * 100)
atomic.AddInt32(&count, 3)
return nil
},
)
assert.Equal(t, int32(6), count)
assert.NoError(t, err)
}

View File

@@ -84,10 +84,10 @@ func Range(source <-chan any) Stream {
} }
} }
// AllMach returns whether all elements of this stream match the provided predicate. // AllMatch returns whether all elements of this stream match the provided predicate.
// May not evaluate the predicate on all elements if not necessary for determining the result. // May not evaluate the predicate on all elements if not necessary for determining the result.
// If the stream is empty then true is returned and the predicate is not evaluated. // If the stream is empty then true is returned and the predicate is not evaluated.
func (s Stream) AllMach(predicate func(item any) bool) bool { func (s Stream) AllMatch(predicate func(item any) bool) bool {
for item := range s.source { for item := range s.source {
if !predicate(item) { if !predicate(item) {
// make sure the former goroutine not block, and current func returns fast. // make sure the former goroutine not block, and current func returns fast.
@@ -99,10 +99,10 @@ func (s Stream) AllMach(predicate func(item any) bool) bool {
return true return true
} }
// AnyMach returns whether any elements of this stream match the provided predicate. // AnyMatch returns whether any elements of this stream match the provided predicate.
// May not evaluate the predicate on all elements if not necessary for determining the result. // May not evaluate the predicate on all elements if not necessary for determining the result.
// If the stream is empty then false is returned and the predicate is not evaluated. // If the stream is empty then false is returned and the predicate is not evaluated.
func (s Stream) AnyMach(predicate func(item any) bool) bool { func (s Stream) AnyMatch(predicate func(item any) bool) bool {
for item := range s.source { for item := range s.source {
if predicate(item) { if predicate(item) {
// make sure the former goroutine not block, and current func returns fast. // make sure the former goroutine not block, and current func returns fast.

View File

@@ -398,16 +398,16 @@ func TestWalk(t *testing.T) {
func TestStream_AnyMach(t *testing.T) { func TestStream_AnyMach(t *testing.T) {
runCheckedTest(t, func(t *testing.T) { runCheckedTest(t, func(t *testing.T) {
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item any) bool { assetEqual(t, false, Just(1, 2, 3).AnyMatch(func(item any) bool {
return item.(int) == 4 return item.(int) == 4
})) }))
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item any) bool { assetEqual(t, false, Just(1, 2, 3).AnyMatch(func(item any) bool {
return item.(int) == 0 return item.(int) == 0
})) }))
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item any) bool { assetEqual(t, true, Just(1, 2, 3).AnyMatch(func(item any) bool {
return item.(int) == 2 return item.(int) == 2
})) }))
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item any) bool { assetEqual(t, true, Just(1, 2, 3).AnyMatch(func(item any) bool {
return item.(int) == 2 return item.(int) == 2
})) }))
}) })
@@ -416,17 +416,17 @@ func TestStream_AnyMach(t *testing.T) {
func TestStream_AllMach(t *testing.T) { func TestStream_AllMach(t *testing.T) {
runCheckedTest(t, func(t *testing.T) { runCheckedTest(t, func(t *testing.T) {
assetEqual( assetEqual(
t, true, Just(1, 2, 3).AllMach(func(item any) bool { t, true, Just(1, 2, 3).AllMatch(func(item any) bool {
return true return true
}), }),
) )
assetEqual( assetEqual(
t, false, Just(1, 2, 3).AllMach(func(item any) bool { t, false, Just(1, 2, 3).AllMatch(func(item any) bool {
return false return false
}), }),
) )
assetEqual( assetEqual(
t, false, Just(1, 2, 3).AllMach(func(item any) bool { t, false, Just(1, 2, 3).AllMatch(func(item any) bool {
return item.(int) == 1 return item.(int) == 1
}), }),
) )

View File

@@ -2,6 +2,7 @@ package limit
import ( import (
"context" "context"
_ "embed"
"errors" "errors"
"strconv" "strconv"
"time" "time"
@@ -28,20 +29,9 @@ var (
// ErrUnknownCode is an error that represents unknown status code. // ErrUnknownCode is an error that represents unknown status code.
ErrUnknownCode = errors.New("unknown status code") ErrUnknownCode = errors.New("unknown status code")
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key //go:embed periodscript.lua
periodScript = redis.NewScript(`local limit = tonumber(ARGV[1]) periodLuaScript string
local window = tonumber(ARGV[2]) periodScript = redis.NewScript(periodLuaScript)
local current = redis.call("INCRBY", KEYS[1], 1)
if current == 1 then
redis.call("expire", KEYS[1], window)
end
if current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end`)
) )
type ( type (

View File

@@ -0,0 +1,14 @@
-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("INCRBY", KEYS[1], 1)
if current == 1 then
redis.call("expire", KEYS[1], window)
end
if current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end

View File

@@ -2,6 +2,7 @@ package limit
import ( import (
"context" "context"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
@@ -9,6 +10,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/zeromicro/go-zero/core/errorx"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/redis"
xrate "golang.org/x/time/rate" xrate "golang.org/x/time/rate"
@@ -20,37 +22,11 @@ const (
pingInterval = time.Millisecond * 100 pingInterval = time.Millisecond * 100
) )
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key var (
// KEYS[1] as tokens_key //go:embed tokenscript.lua
// KEYS[2] as timestamp_key tokenLuaScript string
var script = redis.NewScript(`local rate = tonumber(ARGV[1]) tokenScript = redis.NewScript(tokenLuaScript)
local capacity = tonumber(ARGV[2]) )
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return allowed`)
// A TokenLimiter controls how frequently events are allowed to happen with in one second. // A TokenLimiter controls how frequently events are allowed to happen with in one second.
type TokenLimiter struct { type TokenLimiter struct {
@@ -112,7 +88,7 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
} }
resp, err := lim.store.ScriptRunCtx(ctx, resp, err := lim.store.ScriptRunCtx(ctx,
script, tokenScript,
[]string{ []string{
lim.tokenKey, lim.tokenKey,
lim.timestampKey, lim.timestampKey,
@@ -128,7 +104,7 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
if errors.Is(err, redis.Nil) { if errors.Is(err, redis.Nil) {
return false return false
} }
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { if errorx.In(err, context.DeadlineExceeded, context.Canceled) {
logx.Errorf("fail to use rate limiter: %s", err) logx.Errorf("fail to use rate limiter: %s", err)
return false return false
} }

View File

@@ -0,0 +1,31 @@
-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
-- KEYS[1] as tokens_key
-- KEYS[2] as timestamp_key
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return allowed

View File

@@ -76,8 +76,8 @@ type (
avgFlyingLock syncx.SpinLock avgFlyingLock syncx.SpinLock
overloadTime *syncx.AtomicDuration overloadTime *syncx.AtomicDuration
droppedRecently *syncx.AtomicBool droppedRecently *syncx.AtomicBool
passCounter *collection.RollingWindow passCounter *collection.RollingWindow[int64, *collection.Bucket[int64]]
rtCounter *collection.RollingWindow rtCounter *collection.RollingWindow[int64, *collection.Bucket[int64]]
} }
) )
@@ -107,15 +107,16 @@ func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
opt(&options) opt(&options)
} }
bucketDuration := options.window / time.Duration(options.buckets) bucketDuration := options.window / time.Duration(options.buckets)
newBucket := func() *collection.Bucket[int64] {
return new(collection.Bucket[int64])
}
return &adaptiveShedder{ return &adaptiveShedder{
cpuThreshold: options.cpuThreshold, cpuThreshold: options.cpuThreshold,
windowScale: float64(time.Second) / float64(bucketDuration) / millisecondsPerSecond, windowScale: float64(time.Second) / float64(bucketDuration) / millisecondsPerSecond,
overloadTime: syncx.NewAtomicDuration(), overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.NewAtomicBool(), droppedRecently: syncx.NewAtomicBool(),
passCounter: collection.NewRollingWindow(options.buckets, bucketDuration, passCounter: collection.NewRollingWindow[int64, *collection.Bucket[int64]](newBucket, options.buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]()),
collection.IgnoreCurrentBucket()), rtCounter: collection.NewRollingWindow[int64, *collection.Bucket[int64]](newBucket, options.buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]()),
rtCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
collection.IgnoreCurrentBucket()),
} }
} }
@@ -167,15 +168,15 @@ func (as *adaptiveShedder) maxFlight() float64 {
} }
func (as *adaptiveShedder) maxPass() int64 { func (as *adaptiveShedder) maxPass() int64 {
var result float64 = 1 var result int64 = 1
as.passCounter.Reduce(func(b *collection.Bucket) { as.passCounter.Reduce(func(b *collection.Bucket[int64]) {
if b.Sum > result { if b.Sum > result {
result = b.Sum result = b.Sum
} }
}) })
return int64(result) return result
} }
func (as *adaptiveShedder) minRt() float64 { func (as *adaptiveShedder) minRt() float64 {
@@ -183,12 +184,12 @@ func (as *adaptiveShedder) minRt() float64 {
// its a reasonable large value to avoid dropping requests. // its a reasonable large value to avoid dropping requests.
result := defaultMinRt result := defaultMinRt
as.rtCounter.Reduce(func(b *collection.Bucket) { as.rtCounter.Reduce(func(b *collection.Bucket[int64]) {
if b.Count <= 0 { if b.Count <= 0 {
return return
} }
avg := math.Round(b.Sum / float64(b.Count)) avg := math.Round(float64(b.Sum) / float64(b.Count))
if avg < result { if avg < result {
result = avg result = avg
} }
@@ -283,6 +284,6 @@ func (p *promise) Fail() {
func (p *promise) Pass() { func (p *promise) Pass() {
rt := float64(timex.Since(p.start)) / float64(time.Millisecond) rt := float64(timex.Since(p.start)) / float64(time.Millisecond)
p.shedder.addFlying(-1) p.shedder.addFlying(-1)
p.shedder.rtCounter.Add(math.Ceil(rt)) p.shedder.rtCounter.Add(int64(math.Ceil(rt)))
p.shedder.passCounter.Add(1) p.shedder.passCounter.Add(1)
} }

View File

@@ -58,7 +58,7 @@ func TestAdaptiveShedder(t *testing.T) {
func TestAdaptiveShedderMaxPass(t *testing.T) { func TestAdaptiveShedderMaxPass(t *testing.T) {
passCounter := newRollingWindow() passCounter := newRollingWindow()
for i := 1; i <= 10; i++ { for i := 1; i <= 10; i++ {
passCounter.Add(float64(i * 100)) passCounter.Add(int64(i * 100))
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -83,7 +83,7 @@ func TestAdaptiveShedderMinRt(t *testing.T) {
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
for j := i*10 + 1; j <= i*10+10; j++ { for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j)) rtCounter.Add(int64(j))
} }
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -107,9 +107,9 @@ func TestAdaptiveShedderMaxFlight(t *testing.T) {
if i > 0 { if i > 0 {
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
passCounter.Add(float64((i + 1) * 100)) passCounter.Add(int64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ { for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j)) rtCounter.Add(int64(j))
} }
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -129,9 +129,9 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
if i > 0 { if i > 0 {
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
passCounter.Add(float64((i + 1) * 100)) passCounter.Add(int64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ { for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j)) rtCounter.Add(int64(j))
} }
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -184,9 +184,9 @@ func TestAdaptiveShedderStillHot(t *testing.T) {
if i > 0 { if i > 0 {
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
passCounter.Add(float64((i + 1) * 100)) passCounter.Add(int64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ { for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j)) rtCounter.Add(int64(j))
} }
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -248,9 +248,9 @@ func BenchmarkMaxFlight(b *testing.B) {
if i > 0 { if i > 0 {
time.Sleep(bucketDuration) time.Sleep(bucketDuration)
} }
passCounter.Add(float64((i + 1) * 100)) passCounter.Add(int64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ { for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(float64(j)) rtCounter.Add(int64(j))
} }
} }
shedder := &adaptiveShedder{ shedder := &adaptiveShedder{
@@ -265,6 +265,8 @@ func BenchmarkMaxFlight(b *testing.B) {
} }
} }
func newRollingWindow() *collection.RollingWindow { func newRollingWindow() *collection.RollingWindow[int64, *collection.Bucket[int64]] {
return collection.NewRollingWindow(buckets, bucketDuration, collection.IgnoreCurrentBucket()) return collection.NewRollingWindow[int64, *collection.Bucket[int64]](func() *collection.Bucket[int64] {
return new(collection.Bucket[int64])
}, buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]())
} }

View File

@@ -42,4 +42,6 @@ type LogConf struct {
// daily: daily rotation. // daily: daily rotation.
// size: size limited rotation. // size: size limited rotation.
Rotation string `json:",default=daily,options=[daily,size]"` Rotation string `json:",default=daily,options=[daily,size]"`
// FileTimeFormat represents the time format for file name, default is `2006-01-02T15:04:05.000Z07:00`.
FileTimeFormat string `json:",optional"`
} }

View File

@@ -7,13 +7,13 @@ import (
// A Logger represents a logger. // A Logger represents a logger.
type Logger interface { type Logger interface {
// Debug logs a message at info level. // Debug logs a message at debug level.
Debug(...any) Debug(...any)
// Debugf logs a message at info level. // Debugf logs a message at debug level.
Debugf(string, ...any) Debugf(string, ...any)
// Debugv logs a message at info level. // Debugv logs a message at debug level.
Debugv(any) Debugv(any)
// Debugw logs a message at info level. // Debugw logs a message at debug level.
Debugw(string, ...LogField) Debugw(string, ...LogField)
// Error logs a message at error level. // Error logs a message at error level.
Error(...any) Error(...any)

View File

@@ -6,6 +6,7 @@ import (
"log" "log"
"os" "os"
"path" "path"
"reflect"
"runtime/debug" "runtime/debug"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -51,6 +52,26 @@ type (
} }
) )
// AddWriter adds a new writer.
// If there is already a writer, the new writer will be added to the writer chain.
// For example, to write logs to both file and console, if there is already a file writer,
// ```go
// logx.AddWriter(logx.NewWriter(os.Stdout))
// ```
func AddWriter(w Writer) {
ow := Reset()
if ow == nil {
SetWriter(w)
} else {
// no need to check if the existing writer is a comboWriter,
// because it is not common to add more than one writer.
// even more than one writer, the behavior is the same.
SetWriter(comboWriter{
writers: []Writer{ow, w},
})
}
}
// Alert alerts v in alert level, and the message is written to error log. // Alert alerts v in alert level, and the message is written to error log.
func Alert(v string) { func Alert(v string) {
getWriter().Alert(v) getWriter().Alert(v)
@@ -153,11 +174,11 @@ func Errorw(msg string, fields ...LogField) {
func Field(key string, value any) LogField { func Field(key string, value any) LogField {
switch val := value.(type) { switch val := value.(type) {
case error: case error:
return LogField{Key: key, Value: val.Error()} return LogField{Key: key, Value: encodeError(val)}
case []error: case []error:
var errs []string var errs []string
for _, err := range val { for _, err := range val {
errs = append(errs, err.Error()) errs = append(errs, encodeError(err))
} }
return LogField{Key: key, Value: errs} return LogField{Key: key, Value: errs}
case time.Duration: case time.Duration:
@@ -175,11 +196,11 @@ func Field(key string, value any) LogField {
} }
return LogField{Key: key, Value: times} return LogField{Key: key, Value: times}
case fmt.Stringer: case fmt.Stringer:
return LogField{Key: key, Value: val.String()} return LogField{Key: key, Value: encodeStringer(val)}
case []fmt.Stringer: case []fmt.Stringer:
var strs []string var strs []string
for _, str := range val { for _, str := range val {
strs = append(strs, str.String()) strs = append(strs, encodeStringer(str))
} }
return LogField{Key: key, Value: strs} return LogField{Key: key, Value: strs}
default: default:
@@ -273,6 +294,10 @@ func SetUp(c LogConf) (err error) {
timeFormat = c.TimeFormat timeFormat = c.TimeFormat
} }
if len(c.FileTimeFormat) > 0 {
fileTimeFormat = c.FileTimeFormat
}
atomic.StoreUint32(&maxContentLength, c.MaxContentLength) atomic.StoreUint32(&maxContentLength, c.MaxContentLength)
switch c.Encoding { switch c.Encoding {
@@ -414,6 +439,32 @@ func createOutput(path string) (io.WriteCloser, error) {
return NewLogger(path, rule, options.gzipEnabled) return NewLogger(path, rule, options.gzipEnabled)
} }
func encodeError(err error) (ret string) {
return encodeWithRecover(err, func() string {
return err.Error()
})
}
func encodeStringer(v fmt.Stringer) (ret string) {
return encodeWithRecover(v, func() string {
return v.String()
})
}
func encodeWithRecover(arg any, fn func() string) (ret string) {
defer func() {
if err := recover(); err != nil {
if v := reflect.ValueOf(arg); v.Kind() == reflect.Ptr && v.IsNil() {
ret = nilAngleString
} else {
ret = fmt.Sprintf("panic: %v", err)
}
}
}()
return fn()
}
func getWriter() Writer { func getWriter() Writer {
w := writer.Load() w := writer.Load()
if w == nil { if w == nil {

View File

@@ -348,6 +348,27 @@ func TestStructedLogInfow(t *testing.T) {
}) })
} }
func TestStructedLogFieldNil(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
assert.NotPanics(t, func() {
var s *string
Infow("test", Field("bb", s))
var d *nilStringer
Infow("test", Field("bb", d))
var e *nilError
Errorw("test", Field("bb", e))
})
assert.NotPanics(t, func() {
var p panicStringer
Infow("test", Field("bb", p))
var ps innerPanicStringer
Infow("test", Field("bb", ps))
})
}
func TestStructedLogInfoConsoleAny(t *testing.T) { func TestStructedLogInfoConsoleAny(t *testing.T) {
w := new(mockWriter) w := new(mockWriter)
old := writer.Swap(w) old := writer.Swap(w)
@@ -658,6 +679,10 @@ func TestSetup(t *testing.T) {
func TestDisable(t *testing.T) { func TestDisable(t *testing.T) {
Disable() Disable()
defer func() {
SetLevel(InfoLevel)
atomic.StoreUint32(&encoding, jsonEncodingType)
}()
var opt logOptions var opt logOptions
WithKeepDays(1)(&opt) WithKeepDays(1)(&opt)
@@ -680,6 +705,17 @@ func TestDisableStat(t *testing.T) {
assert.Equal(t, 0, w.builder.Len()) assert.Equal(t, 0, w.builder.Len())
} }
func TestAddWriter(t *testing.T) {
const message = "hello there"
w := new(mockWriter)
AddWriter(w)
w1 := new(mockWriter)
AddWriter(w1)
Error(message)
assert.Contains(t, w.String(), message)
assert.Contains(t, w1.String(), message)
}
func TestSetWriter(t *testing.T) { func TestSetWriter(t *testing.T) {
atomic.StoreUint32(&logLevel, 0) atomic.StoreUint32(&logLevel, 0)
Reset() Reset()
@@ -814,12 +850,13 @@ func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...any)) {
func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) { func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) {
writer.Store(nil) writer.Store(nil)
SetUp(LogConf{ SetUp(LogConf{
Mode: mode, Mode: mode,
Level: "debug", Level: "debug",
Path: "/dev/null", Path: "/dev/null",
Encoding: plainEncoding, Encoding: plainEncoding,
Stat: false, Stat: false,
TimeFormat: time.RFC3339, TimeFormat: time.RFC3339,
FileTimeFormat: time.DateTime,
}) })
SetUp(LogConf{ SetUp(LogConf{
Mode: mode, Mode: mode,
@@ -859,3 +896,36 @@ func validateFields(t *testing.T, content string, fields map[string]any) {
} }
} }
} }
type nilError struct {
Name string
}
func (e *nilError) Error() string {
return e.Name
}
type nilStringer struct {
Name string
}
func (s *nilStringer) String() string {
return s.Name
}
type innerPanicStringer struct {
Inner *struct {
Name string
}
}
func (s innerPanicStringer) String() string {
return s.Inner.Name
}
type panicStringer struct {
}
func (s panicStringer) String() string {
panic("panic")
}

View File

@@ -141,23 +141,43 @@ func (l *richLogger) WithCallerSkip(skip int) Logger {
return l return l
} }
l.callerSkip = skip return &richLogger{
return l ctx: l.ctx,
callerSkip: skip,
fields: l.fields,
}
} }
func (l *richLogger) WithContext(ctx context.Context) Logger { func (l *richLogger) WithContext(ctx context.Context) Logger {
l.ctx = ctx return &richLogger{
return l ctx: ctx,
callerSkip: l.callerSkip,
fields: l.fields,
}
} }
func (l *richLogger) WithDuration(duration time.Duration) Logger { func (l *richLogger) WithDuration(duration time.Duration) Logger {
l.fields = append(l.fields, Field(durationKey, timex.ReprOfDuration(duration))) fields := append(l.fields, Field(durationKey, timex.ReprOfDuration(duration)))
return l
return &richLogger{
ctx: l.ctx,
callerSkip: l.callerSkip,
fields: fields,
}
} }
func (l *richLogger) WithFields(fields ...LogField) Logger { func (l *richLogger) WithFields(fields ...LogField) Logger {
l.fields = append(l.fields, fields...) if len(fields) == 0 {
return l return l
}
f := append(l.fields, fields...)
return &richLogger{
ctx: l.ctx,
callerSkip: l.callerSkip,
fields: f,
}
} }
func (l *richLogger) buildFields(fields ...LogField) []LogField { func (l *richLogger) buildFields(fields ...LogField) []LogField {

View File

@@ -287,6 +287,54 @@ func TestLogWithCallerSkip(t *testing.T) {
assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
} }
func TestLogWithCallerSkipCopy(t *testing.T) {
log1 := WithCallerSkip(2)
log2 := log1.WithCallerSkip(3)
log3 := log2.WithCallerSkip(-1)
assert.Equal(t, 2, log1.(*richLogger).callerSkip)
assert.Equal(t, 3, log2.(*richLogger).callerSkip)
assert.Equal(t, 3, log3.(*richLogger).callerSkip)
}
func TestLogWithContextCopy(t *testing.T) {
c1 := context.Background()
c2 := context.WithValue(context.Background(), "foo", "bar")
log1 := WithContext(c1)
log2 := log1.WithContext(c2)
assert.Equal(t, c1, log1.(*richLogger).ctx)
assert.Equal(t, c2, log2.(*richLogger).ctx)
}
func TestLogWithDurationCopy(t *testing.T) {
log1 := WithContext(context.Background())
log2 := log1.WithDuration(time.Second)
assert.Empty(t, log1.(*richLogger).fields)
assert.Equal(t, 1, len(log2.(*richLogger).fields))
var w mockWriter
old := writer.Swap(&w)
defer writer.Store(old)
log2.Info("hello")
assert.Contains(t, w.String(), `"duration":"1000.0ms"`)
}
func TestLogWithFieldsCopy(t *testing.T) {
log1 := WithContext(context.Background())
log2 := log1.WithFields(Field("foo", "bar"))
log3 := log1.WithFields()
assert.Empty(t, log1.(*richLogger).fields)
assert.Equal(t, 1, len(log2.(*richLogger).fields))
assert.Equal(t, log1, log3)
assert.Empty(t, log3.(*richLogger).fields)
var w mockWriter
old := writer.Swap(&w)
defer writer.Store(old)
log2.Info("hello")
assert.Contains(t, w.String(), `"foo":"bar"`)
}
func TestLoggerWithFields(t *testing.T) { func TestLoggerWithFields(t *testing.T) {
w := new(mockWriter) w := new(mockWriter)
old := writer.Swap(w) old := writer.Swap(w)

View File

@@ -19,7 +19,6 @@ import (
const ( const (
dateFormat = "2006-01-02" dateFormat = "2006-01-02"
fileTimeFormat = time.RFC3339
hoursPerDay = 24 hoursPerDay = 24
bufferSize = 100 bufferSize = 100
defaultDirMode = 0o755 defaultDirMode = 0o755
@@ -28,8 +27,12 @@ const (
megaBytes = 1 << 20 megaBytes = 1 << 20
) )
// ErrLogFileClosed is an error that indicates the log file is already closed. var (
var ErrLogFileClosed = errors.New("error: log file closed") // ErrLogFileClosed is an error that indicates the log file is already closed.
ErrLogFileClosed = errors.New("error: log file closed")
fileTimeFormat = time.RFC3339
)
type ( type (
// A RotateRule interface is used to define the log rotating rules. // A RotateRule interface is used to define the log rotating rules.

View File

@@ -48,6 +48,7 @@ const (
levelDebug = "debug" levelDebug = "debug"
backupFileDelimiter = "-" backupFileDelimiter = "-"
nilAngleString = "<nil>"
flags = 0x0 flags = 0x0
) )

View File

@@ -13,6 +13,7 @@ import (
fatihcolor "github.com/fatih/color" fatihcolor "github.com/fatih/color"
"github.com/zeromicro/go-zero/core/color" "github.com/zeromicro/go-zero/core/color"
"github.com/zeromicro/go-zero/core/errorx"
) )
type ( type (
@@ -33,6 +34,10 @@ type (
lock sync.RWMutex lock sync.RWMutex
} }
comboWriter struct {
writers []Writer
}
concreteWriter struct { concreteWriter struct {
infoLog io.WriteCloser infoLog io.WriteCloser
errorLog io.WriteCloser errorLog io.WriteCloser
@@ -88,6 +93,62 @@ func (w *atomicWriter) Swap(v Writer) Writer {
return old return old
} }
func (c comboWriter) Alert(v any) {
for _, w := range c.writers {
w.Alert(v)
}
}
func (c comboWriter) Close() error {
var be errorx.BatchError
for _, w := range c.writers {
be.Add(w.Close())
}
return be.Err()
}
func (c comboWriter) Debug(v any, fields ...LogField) {
for _, w := range c.writers {
w.Debug(v, fields...)
}
}
func (c comboWriter) Error(v any, fields ...LogField) {
for _, w := range c.writers {
w.Error(v, fields...)
}
}
func (c comboWriter) Info(v any, fields ...LogField) {
for _, w := range c.writers {
w.Info(v, fields...)
}
}
func (c comboWriter) Severe(v any) {
for _, w := range c.writers {
w.Severe(v)
}
}
func (c comboWriter) Slow(v any, fields ...LogField) {
for _, w := range c.writers {
w.Slow(v, fields...)
}
}
func (c comboWriter) Stack(v any) {
for _, w := range c.writers {
w.Stack(v)
}
}
func (c comboWriter) Stat(v any, fields ...LogField) {
for _, w := range c.writers {
w.Stat(v, fields...)
}
}
func newConsoleWriter() Writer { func newConsoleWriter() Writer {
outLog := newLogWriter(log.New(fatihcolor.Output, "", flags)) outLog := newLogWriter(log.New(fatihcolor.Output, "", flags))
errLog := newLogWriter(log.New(fatihcolor.Error, "", flags)) errLog := newLogWriter(log.New(fatihcolor.Error, "", flags))
@@ -254,11 +315,10 @@ func (n nopWriter) Stack(_ any) {
func (n nopWriter) Stat(_ any, _ ...LogField) { func (n nopWriter) Stat(_ any, _ ...LogField) {
} }
func buildPlainFields(fields ...LogField) []string { func buildPlainFields(fields logEntry) []string {
var items []string items := make([]string, 0, len(fields))
for k, v := range fields {
for _, field := range fields { items = append(items, fmt.Sprintf("%s=%+v", k, v))
items = append(items, fmt.Sprintf("%s=%+v", field.Key, field.Value))
} }
return items return items
@@ -278,6 +338,20 @@ func combineGlobalFields(fields []LogField) []LogField {
return ret return ret
} }
func marshalJson(t interface{}) ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
err := encoder.Encode(t)
// go 1.5+ will append a newline to the end of the json string
// https://github.com/golang/go/issues/13520
if l := buf.Len(); l > 0 && buf.Bytes()[l-1] == '\n' {
buf.Truncate(l - 1)
}
return buf.Bytes(), err
}
func output(writer io.Writer, level string, val any, fields ...LogField) { func output(writer io.Writer, level string, val any, fields ...LogField) {
// only truncate string content, don't know how to truncate the values of other types. // only truncate string content, don't know how to truncate the values of other types.
if v, ok := val.(string); ok { if v, ok := val.(string); ok {
@@ -289,15 +363,17 @@ func output(writer io.Writer, level string, val any, fields ...LogField) {
} }
fields = combineGlobalFields(fields) fields = combineGlobalFields(fields)
// +3 for timestamp, level and content
entry := make(logEntry, len(fields)+3)
for _, field := range fields {
entry[field.Key] = field.Value
}
switch atomic.LoadUint32(&encoding) { switch atomic.LoadUint32(&encoding) {
case plainEncodingType: case plainEncodingType:
writePlainAny(writer, level, val, buildPlainFields(fields...)...) plainFields := buildPlainFields(entry)
writePlainAny(writer, level, val, plainFields...)
default: default:
entry := make(logEntry)
for _, field := range fields {
entry[field.Key] = field.Value
}
entry[timestampKey] = getTimestamp() entry[timestampKey] = getTimestamp()
entry[levelKey] = level entry[levelKey] = level
entry[contentKey] = val entry[contentKey] = val
@@ -332,7 +408,7 @@ func wrapLevelWithColor(level string) string {
} }
func writeJson(writer io.Writer, info any) { func writeJson(writer io.Writer, info any) {
if content, err := json.Marshal(info); err != nil { if content, err := marshalJson(info); err != nil {
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack()) log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
} else if writer == nil { } else if writer == nil {
log.Println(string(content)) log.Println(string(content))

View File

@@ -9,6 +9,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
) )
func TestNewWriter(t *testing.T) { func TestNewWriter(t *testing.T) {
@@ -189,6 +190,41 @@ func TestWritePlainAny(t *testing.T) {
assert.Contains(t, buf.String(), "runtime/debug.Stack") assert.Contains(t, buf.String(), "runtime/debug.Stack")
} }
func TestWritePlainDuplicate(t *testing.T) {
old := atomic.SwapUint32(&encoding, plainEncodingType)
t.Cleanup(func() {
atomic.StoreUint32(&encoding, old)
})
var buf bytes.Buffer
output(&buf, levelInfo, "foo", LogField{
Key: "first",
Value: "a",
}, LogField{
Key: "first",
Value: "b",
})
assert.Contains(t, buf.String(), "foo")
assert.NotContains(t, buf.String(), "first=a")
assert.Contains(t, buf.String(), "first=b")
buf.Reset()
output(&buf, levelInfo, "foo", LogField{
Key: "first",
Value: "a",
}, LogField{
Key: "first",
Value: "b",
}, LogField{
Key: "second",
Value: "c",
})
assert.Contains(t, buf.String(), "foo")
assert.NotContains(t, buf.String(), "first=a")
assert.Contains(t, buf.String(), "first=b")
assert.Contains(t, buf.String(), "second=c")
}
func TestLogWithLimitContentLength(t *testing.T) { func TestLogWithLimitContentLength(t *testing.T) {
maxLen := atomic.LoadUint32(&maxContentLength) maxLen := atomic.LoadUint32(&maxContentLength)
atomic.StoreUint32(&maxContentLength, 10) atomic.StoreUint32(&maxContentLength, 10)
@@ -219,6 +255,117 @@ func TestLogWithLimitContentLength(t *testing.T) {
}) })
} }
func TestComboWriter(t *testing.T) {
var mockWriters []Writer
for i := 0; i < 3; i++ {
mockWriters = append(mockWriters, new(tracedWriter))
}
cw := comboWriter{
writers: mockWriters,
}
t.Run("Alert", func(t *testing.T) {
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Alert", "test alert").Once()
}
cw.Alert("test alert")
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Alert", "test alert")
}
})
t.Run("Close", func(t *testing.T) {
for i := range cw.writers {
if i == 1 {
cw.writers[i].(*tracedWriter).On("Close").Return(errors.New("error")).Once()
} else {
cw.writers[i].(*tracedWriter).On("Close").Return(nil).Once()
}
}
err := cw.Close()
assert.Error(t, err)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Close")
}
})
t.Run("Debug", func(t *testing.T) {
fields := []LogField{{Key: "key", Value: "value"}}
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Debug", "test debug", fields).Once()
}
cw.Debug("test debug", fields...)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Debug", "test debug", fields)
}
})
t.Run("Error", func(t *testing.T) {
fields := []LogField{{Key: "key", Value: "value"}}
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Error", "test error", fields).Once()
}
cw.Error("test error", fields...)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Error", "test error", fields)
}
})
t.Run("Info", func(t *testing.T) {
fields := []LogField{{Key: "key", Value: "value"}}
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Info", "test info", fields).Once()
}
cw.Info("test info", fields...)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Info", "test info", fields)
}
})
t.Run("Severe", func(t *testing.T) {
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Severe", "test severe").Once()
}
cw.Severe("test severe")
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Severe", "test severe")
}
})
t.Run("Slow", func(t *testing.T) {
fields := []LogField{{Key: "key", Value: "value"}}
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Slow", "test slow", fields).Once()
}
cw.Slow("test slow", fields...)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Slow", "test slow", fields)
}
})
t.Run("Stack", func(t *testing.T) {
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Stack", "test stack").Once()
}
cw.Stack("test stack")
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Stack", "test stack")
}
})
t.Run("Stat", func(t *testing.T) {
fields := []LogField{{Key: "key", Value: "value"}}
for _, mw := range cw.writers {
mw.(*tracedWriter).On("Stat", "test stat", fields).Once()
}
cw.Stat("test stat", fields...)
for _, mw := range cw.writers {
mw.(*tracedWriter).AssertCalled(t, "Stat", "test stat", fields)
}
})
}
type mockedEntry struct { type mockedEntry struct {
Level string `json:"level"` Level string `json:"level"`
Content string `json:"content"` Content string `json:"content"`
@@ -250,3 +397,44 @@ type hardToWriteWriter struct{}
func (h hardToWriteWriter) Write(_ []byte) (_ int, _ error) { func (h hardToWriteWriter) Write(_ []byte) (_ int, _ error) {
return 0, errors.New("write error") return 0, errors.New("write error")
} }
type tracedWriter struct {
mock.Mock
}
func (w *tracedWriter) Alert(v any) {
w.Called(v)
}
func (w *tracedWriter) Close() error {
args := w.Called()
return args.Error(0)
}
func (w *tracedWriter) Debug(v any, fields ...LogField) {
w.Called(v, fields)
}
func (w *tracedWriter) Error(v any, fields ...LogField) {
w.Called(v, fields)
}
func (w *tracedWriter) Info(v any, fields ...LogField) {
w.Called(v, fields)
}
func (w *tracedWriter) Severe(v any) {
w.Called(v)
}
func (w *tracedWriter) Slow(v any, fields ...LogField) {
w.Called(v, fields)
}
func (w *tracedWriter) Stack(v any) {
w.Called(v)
}
func (w *tracedWriter) Stat(v any, fields ...LogField) {
w.Called(v, fields)
}

View File

@@ -50,6 +50,7 @@ type (
unmarshalOptions struct { unmarshalOptions struct {
fillDefault bool fillDefault bool
fromArray bool
fromString bool fromString bool
opaqueKeys bool opaqueKeys bool
canonicalKey func(key string) string canonicalKey func(key string) string
@@ -113,7 +114,8 @@ func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName) return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
} }
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error { func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value,
mapValue any, fullName string) error {
if !value.CanSet() { if !value.CanSet() {
return errValueNotSettable return errValueNotSettable
} }
@@ -154,7 +156,8 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
return nil return nil
} }
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error { func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
mapValue any, fullName string) error {
if !value.CanSet() { if !value.CanSet() {
return errValueNotSettable return errValueNotSettable
} }
@@ -307,7 +310,34 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
return u.fillSlice(derefedType, value, slice, fullName) return u.fillSlice(derefedType, value, slice, fullName)
} }
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any, fullName string) (reflect.Value, error) { func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
value reflect.Value, targetValue string) error {
if !value.CanSet() {
return errValueNotSettable
}
baseType := Deref(fieldType)
target := reflect.New(baseType)
switch u.key {
case jsonTagKey:
unmarshaler, ok := target.Interface().(json.Unmarshaler)
if !ok {
return errUnsupportedType
}
if err := unmarshaler.UnmarshalJSON([]byte(targetValue)); err != nil {
return err
}
default:
return errUnsupportedType
}
value.Set(target)
return nil
}
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any,
fullName string) (reflect.Value, error) {
mapType := reflect.MapOf(keyType, elemType) mapType := reflect.MapOf(keyType, elemType)
valueType := reflect.TypeOf(mapValue) valueType := reflect.TypeOf(mapValue)
if mapType == valueType { if mapType == valueType {
@@ -399,6 +429,15 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any,
return targetValue, nil return targetValue, nil
} }
func (u *Unmarshaler) implementsUnmarshaler(t reflect.Type) bool {
switch u.key {
case jsonTagKey:
return t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem())
default:
return false
}
}
func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Valuer, fullName string) ( func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Valuer, fullName string) (
string, *fieldOptionsWithContext, error) { string, *fieldOptionsWithContext, error) {
key, options, err := parseKeyAndOptions(u.key, field) key, options, err := parseKeyAndOptions(u.key, field)
@@ -576,6 +615,8 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
return u.fillSliceFromString(fieldType, value, mapValue, fullName) return u.fillSliceFromString(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && derefedFieldType == durationType: case valueKind == reflect.String && derefedFieldType == durationType:
return fillDurationValue(fieldType, value, mapValue.(string)) return fillDurationValue(fieldType, value, mapValue.(string))
case valueKind == reflect.String && typeKind == reflect.Struct && u.implementsUnmarshaler(fieldType):
return u.fillUnmarshalerStruct(fieldType, value, mapValue.(string))
default: default:
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName) return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
} }
@@ -771,6 +812,19 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName) return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
} }
if u.opts.fromArray {
fieldKind := field.Type.Kind()
if fieldKind != reflect.Slice && fieldKind != reflect.Array {
valueKind := reflect.TypeOf(mapValue).Kind()
if valueKind == reflect.Slice || valueKind == reflect.Array {
val := reflect.ValueOf(mapValue)
if val.Len() > 0 {
mapValue = val.Index(0).Interface()
}
}
}
}
return u.processNamedFieldWithValue(field.Type, value, valueWithParent{ return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
value: mapValue, value: mapValue,
parent: valuer, parent: valuer,
@@ -950,6 +1004,16 @@ func WithDefault() UnmarshalOption {
} }
} }
// WithFromArray customizes an Unmarshaler with converting array values to non-array types.
// For example, if the field type is []string, and the value is [hello],
// the field type can be `string`, instead of `[]string`.
// Typically, this option is used for unmarshaling from form values.
func WithFromArray() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.fromArray = true
}
}
// WithOpaqueKeys customizes an Unmarshaler with opaque keys. // WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler. // Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption { func WithOpaqueKeys() UnmarshalOption {

View File

@@ -2,6 +2,7 @@ package mapping
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
@@ -260,6 +261,7 @@ func TestUnmarshalInt(t *testing.T) {
Int64FromStr int64 `key:"int64str,string"` Int64FromStr int64 `key:"int64str,string"`
DefaultInt int64 `key:"defaultint,default=11"` DefaultInt int64 `key:"defaultint,default=11"`
Optional int `key:"optional,optional"` Optional int `key:"optional,optional"`
IntOptDef int `key:"intopt,optional,default=6"`
} }
m := map[string]any{ m := map[string]any{
"int": 1, "int": 1,
@@ -288,6 +290,7 @@ func TestUnmarshalInt(t *testing.T) {
ast.Equal(int64(9), in.Int64) ast.Equal(int64(9), in.Int64)
ast.Equal(int64(10), in.Int64FromStr) ast.Equal(int64(10), in.Int64FromStr)
ast.Equal(int64(11), in.DefaultInt) ast.Equal(int64(11), in.DefaultInt)
ast.Equal(6, in.IntOptDef)
} }
} }
@@ -5636,6 +5639,62 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
}, &v)) }, &v))
} }
func TestUnmarshalWithFromArray(t *testing.T) {
t.Run("array", func(t *testing.T) {
var v struct {
Value []string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo", "bar"},
}, &v)) {
assert.ElementsMatch(t, []string{"foo", "bar"}, v.Value)
}
})
t.Run("not array", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo"},
}, &v)) {
assert.Equal(t, "foo", v.Value)
}
})
t.Run("not array and empty", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{""},
}, &v)) {
assert.Empty(t, v.Value)
}
})
t.Run("not array and no value", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
assert.Error(t, unmarshaler.Unmarshal(map[string]any{}, &v))
})
t.Run("not array and no value and optional", func(t *testing.T) {
var v struct {
Value string `key:"value,optional"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{}, &v)) {
assert.Empty(t, v.Value)
}
})
}
func TestUnmarshalWithOpaqueKeys(t *testing.T) { func TestUnmarshalWithOpaqueKeys(t *testing.T) {
var v struct { var v struct {
Opaque string `key:"opaque.key"` Opaque string `key:"opaque.key"`
@@ -5760,6 +5819,49 @@ func TestUnmarshalWithIgnoreFields(t *testing.T) {
} }
} }
func TestUnmarshal_Unmarshaler(t *testing.T) {
t.Run("success", func(t *testing.T) {
v := struct {
Foo *mockUnmarshaler `json:"name"`
}{}
body := `{"name": "hello"}`
assert.NoError(t, UnmarshalJsonBytes([]byte(body), &v))
assert.Equal(t, "hello", v.Foo.Name)
})
t.Run("failure", func(t *testing.T) {
v := struct {
Foo *mockUnmarshalerWithError `json:"name"`
}{}
body := `{"name": "hello"}`
assert.Error(t, UnmarshalJsonBytes([]byte(body), &v))
})
t.Run("not json unmarshaler", func(t *testing.T) {
v := struct {
Foo *struct {
Name string
} `key:"name"`
}{}
u := NewUnmarshaler(defaultKeyName)
assert.Error(t, u.Unmarshal(map[string]any{
"name": "hello",
}, &v))
})
t.Run("not with json key", func(t *testing.T) {
v := struct {
Foo *mockUnmarshaler `json:"name"`
}{}
u := NewUnmarshaler(defaultKeyName)
// with different key, ignore
assert.NoError(t, u.Unmarshal(map[string]any{
"name": "hello",
}, &v))
assert.Nil(t, v.Foo)
})
}
func BenchmarkDefaultValue(b *testing.B) { func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var a struct { var a struct {
@@ -5873,3 +5975,20 @@ func (m mockValuerWithParent) Value(_ string) (any, bool) {
func (m mockValuerWithParent) Parent() valuerWithParent { func (m mockValuerWithParent) Parent() valuerWithParent {
return m.parent return m.parent
} }
type mockUnmarshaler struct {
Name string
}
func (m *mockUnmarshaler) UnmarshalJSON(b []byte) error {
m.Name = string(b)
return nil
}
type mockUnmarshalerWithError struct {
Name string
}
func (m *mockUnmarshalerWithError) UnmarshalJSON(b []byte) error {
return errors.New("foo")
}

View File

@@ -1,13 +1,13 @@
package mathx package mathx
type numerical interface { type Numerical interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 ~float32 | ~float64
} }
// AtLeast returns the greater of x or lower. // AtLeast returns the greater of x or lower.
func AtLeast[T numerical](x, lower T) T { func AtLeast[T Numerical](x, lower T) T {
if x < lower { if x < lower {
return lower return lower
} }
@@ -15,7 +15,7 @@ func AtLeast[T numerical](x, lower T) T {
} }
// AtMost returns the smaller of x or upper. // AtMost returns the smaller of x or upper.
func AtMost[T numerical](x, upper T) T { func AtMost[T Numerical](x, upper T) T {
if x > upper { if x > upper {
return upper return upper
} }
@@ -23,7 +23,7 @@ func AtMost[T numerical](x, upper T) T {
} }
// Between returns the value of x clamped to the range [lower, upper]. // Between returns the value of x clamped to the range [lower, upper].
func Between[T numerical](x, lower, upper T) T { func Between[T Numerical](x, lower, upper T) T {
if x < lower { if x < lower {
return lower return lower
} }

View File

@@ -363,9 +363,7 @@ func newGuardedWriter[T any](ctx context.Context, channel chan<- T, done <-chan
func (gw guardedWriter[T]) Write(v T) { func (gw guardedWriter[T]) Write(v T) {
select { select {
case <-gw.ctx.Done(): case <-gw.ctx.Done():
return
case <-gw.done: case <-gw.done:
return
default: default:
gw.channel <- v gw.channel <- v
} }

View File

@@ -7,6 +7,10 @@ import (
"go.mongodb.org/mongo-driver/mongo/integration/mtest" "go.mongodb.org/mongo-driver/mongo/integration/mtest"
) )
func init() {
_ = mtest.Setup()
}
func TestClientManger_getClient(t *testing.T) { func TestClientManger_getClient(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("test", func(mt *mtest.T) { mt.Run("test", func(mt *mtest.T) {

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/zeromicro/go-zero/core/breaker" "github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/errorx"
"github.com/zeromicro/go-zero/core/timex" "github.com/zeromicro/go-zero/core/timex"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
mopt "go.mongodb.org/mongo-driver/mongo/options" mopt "go.mongodb.org/mongo-driver/mongo/options"
@@ -15,7 +16,8 @@ import (
const ( const (
defaultSlowThreshold = time.Millisecond * 500 defaultSlowThreshold = time.Millisecond * 500
// spanName is the span name of the mongo calls. // spanName is the span name of the mongo calls.
spanName = "mongo" spanName = "mongo"
duplicateKeyCode = 11000
// mongodb method names // mongodb method names
aggregate = "Aggregate" aggregate = "Aggregate"
@@ -141,7 +143,7 @@ func (c *decoratedCollection) Aggregate(ctx context.Context, pipeline any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
starTime := timex.Now() starTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, aggregate, starTime, err) c.logDurationSimple(ctx, aggregate, starTime, err)
@@ -161,7 +163,7 @@ func (c *decoratedCollection) BulkWrite(ctx context.Context, models []mongo.Writ
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, bulkWrite, startTime, err) c.logDurationSimple(ctx, bulkWrite, startTime, err)
@@ -181,7 +183,7 @@ func (c *decoratedCollection) CountDocuments(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, countDocuments, startTime, err) c.logDurationSimple(ctx, countDocuments, startTime, err)
@@ -201,7 +203,7 @@ func (c *decoratedCollection) DeleteMany(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, deleteMany, startTime, err) c.logDurationSimple(ctx, deleteMany, startTime, err)
@@ -221,7 +223,7 @@ func (c *decoratedCollection) DeleteOne(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, deleteOne, startTime, err, filter) c.logDuration(ctx, deleteOne, startTime, err, filter)
@@ -241,7 +243,7 @@ func (c *decoratedCollection) Distinct(ctx context.Context, fieldName string, fi
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, distinct, startTime, err) c.logDurationSimple(ctx, distinct, startTime, err)
@@ -261,7 +263,7 @@ func (c *decoratedCollection) EstimatedDocumentCount(ctx context.Context,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, estimatedDocumentCount, startTime, err) c.logDurationSimple(ctx, estimatedDocumentCount, startTime, err)
@@ -281,7 +283,7 @@ func (c *decoratedCollection) Find(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, find, startTime, err, filter) c.logDuration(ctx, find, startTime, err, filter)
@@ -301,7 +303,7 @@ func (c *decoratedCollection) FindOne(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, findOne, startTime, err, filter) c.logDuration(ctx, findOne, startTime, err, filter)
@@ -322,7 +324,7 @@ func (c *decoratedCollection) FindOneAndDelete(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, findOneAndDelete, startTime, err, filter) c.logDuration(ctx, findOneAndDelete, startTime, err, filter)
@@ -344,7 +346,7 @@ func (c *decoratedCollection) FindOneAndReplace(ctx context.Context, filter any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, findOneAndReplace, startTime, err, filter, replacement) c.logDuration(ctx, findOneAndReplace, startTime, err, filter, replacement)
@@ -365,7 +367,7 @@ func (c *decoratedCollection) FindOneAndUpdate(ctx context.Context, filter, upda
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, findOneAndUpdate, startTime, err, filter, update) c.logDuration(ctx, findOneAndUpdate, startTime, err, filter, update)
@@ -386,7 +388,7 @@ func (c *decoratedCollection) InsertMany(ctx context.Context, documents []any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, insertMany, startTime, err) c.logDurationSimple(ctx, insertMany, startTime, err)
@@ -406,7 +408,7 @@ func (c *decoratedCollection) InsertOne(ctx context.Context, document any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, insertOne, startTime, err, document) c.logDuration(ctx, insertOne, startTime, err, document)
@@ -426,7 +428,7 @@ func (c *decoratedCollection) ReplaceOne(ctx context.Context, filter, replacemen
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, replaceOne, startTime, err, filter, replacement) c.logDuration(ctx, replaceOne, startTime, err, filter, replacement)
@@ -446,7 +448,7 @@ func (c *decoratedCollection) UpdateByID(ctx context.Context, id, update any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, updateByID, startTime, err, id, update) c.logDuration(ctx, updateByID, startTime, err, id, update)
@@ -466,7 +468,7 @@ func (c *decoratedCollection) UpdateMany(ctx context.Context, filter, update any
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDurationSimple(ctx, updateMany, startTime, err) c.logDurationSimple(ctx, updateMany, startTime, err)
@@ -486,7 +488,7 @@ func (c *decoratedCollection) UpdateOne(ctx context.Context, filter, update any,
endSpan(span, err) endSpan(span, err)
}() }()
err = c.brk.DoWithAcceptable(func() error { err = c.brk.DoWithAcceptableCtx(ctx, func() error {
startTime := timex.Now() startTime := timex.Now()
defer func() { defer func() {
c.logDuration(ctx, updateOne, startTime, err, filter, update) c.logDuration(ctx, updateOne, startTime, err, filter, update)
@@ -527,19 +529,20 @@ func (p keepablePromise) keep(err error) error {
} }
func acceptable(err error) bool { func acceptable(err error) bool {
return err == nil || return err == nil || isDupKeyError(err) ||
errors.Is(err, mongo.ErrNoDocuments) || errorx.In(err, mongo.ErrNoDocuments, mongo.ErrNilValue,
errors.Is(err, mongo.ErrNilValue) || mongo.ErrNilDocument, mongo.ErrNilCursor, mongo.ErrEmptySlice,
errors.Is(err, mongo.ErrNilDocument) || // session errors
errors.Is(err, mongo.ErrNilCursor) || session.ErrSessionEnded, session.ErrNoTransactStarted, session.ErrTransactInProgress,
errors.Is(err, mongo.ErrEmptySlice) || session.ErrAbortAfterCommit, session.ErrAbortTwice, session.ErrCommitAfterAbort,
// session errors session.ErrUnackWCUnsupported, session.ErrSnapshotTransaction)
errors.Is(err, session.ErrSessionEnded) || }
errors.Is(err, session.ErrNoTransactStarted) ||
errors.Is(err, session.ErrTransactInProgress) || func isDupKeyError(err error) bool {
errors.Is(err, session.ErrAbortAfterCommit) || var e mongo.WriteException
errors.Is(err, session.ErrAbortTwice) || if !errors.As(err, &e) {
errors.Is(err, session.ErrCommitAfterAbort) || return false
errors.Is(err, session.ErrUnackWCUnsupported) || }
errors.Is(err, session.ErrSnapshotTransaction)
return e.HasErrorCode(duplicateKeyCode)
} }

View File

@@ -15,6 +15,7 @@ import (
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/integration/mtest" "go.mongodb.org/mongo-driver/mongo/integration/mtest"
mopt "go.mongodb.org/mongo-driver/mongo/options" mopt "go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
) )
var errDummy = errors.New("dummy") var errDummy = errors.New("dummy")
@@ -572,6 +573,56 @@ func TestDecoratedCollection_LogDuration(t *testing.T) {
assert.Contains(t, buf.String(), "slowcall") assert.Contains(t, buf.String(), "slowcall")
} }
func TestAcceptable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"NilError", nil, true},
{"NoDocuments", mongo.ErrNoDocuments, true},
{"NilValue", mongo.ErrNilValue, true},
{"NilDocument", mongo.ErrNilDocument, true},
{"NilCursor", mongo.ErrNilCursor, true},
{"EmptySlice", mongo.ErrEmptySlice, true},
{"SessionEnded", session.ErrSessionEnded, true},
{"NoTransactStarted", session.ErrNoTransactStarted, true},
{"TransactInProgress", session.ErrTransactInProgress, true},
{"AbortAfterCommit", session.ErrAbortAfterCommit, true},
{"AbortTwice", session.ErrAbortTwice, true},
{"CommitAfterAbort", session.ErrCommitAfterAbort, true},
{"UnackWCUnsupported", session.ErrUnackWCUnsupported, true},
{"SnapshotTransaction", session.ErrSnapshotTransaction, true},
{"DuplicateKeyError", mongo.WriteException{WriteErrors: []mongo.WriteError{{Code: duplicateKeyCode}}}, true},
{"OtherError", errors.New("other error"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, acceptable(tt.err))
})
}
}
func TestIsDupKeyError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"NilError", nil, false},
{"NonDupKeyError", errors.New("some other error"), false},
{"DupKeyError", mongo.WriteException{WriteErrors: []mongo.WriteError{{Code: duplicateKeyCode}}}, true},
{"OtherMongoError", mongo.WriteException{WriteErrors: []mongo.WriteError{{Code: 12345}}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isDupKeyError(tt.err))
})
}
}
type mockPromise struct { type mockPromise struct {
accepted bool accepted bool
reason string reason string
@@ -595,19 +646,40 @@ func (d *dropBreaker) Allow() (breaker.Promise, error) {
return nil, errDummy return nil, errDummy
} }
func (d *dropBreaker) AllowCtx(_ context.Context) (breaker.Promise, error) {
return nil, errDummy
}
func (d *dropBreaker) Do(_ func() error) error { func (d *dropBreaker) Do(_ func() error) error {
return nil return nil
} }
func (d *dropBreaker) DoCtx(_ context.Context, _ func() error) error {
return nil
}
func (d *dropBreaker) DoWithAcceptable(_ func() error, _ breaker.Acceptable) error { func (d *dropBreaker) DoWithAcceptable(_ func() error, _ breaker.Acceptable) error {
return errDummy return errDummy
} }
func (d *dropBreaker) DoWithAcceptableCtx(_ context.Context, _ func() error, _ breaker.Acceptable) error {
return errDummy
}
func (d *dropBreaker) DoWithFallback(_ func() error, _ breaker.Fallback) error { func (d *dropBreaker) DoWithFallback(_ func() error, _ breaker.Fallback) error {
return nil return nil
} }
func (d *dropBreaker) DoWithFallbackCtx(_ context.Context, _ func() error, _ breaker.Fallback) error {
return nil
}
func (d *dropBreaker) DoWithFallbackAcceptable(_ func() error, _ breaker.Fallback, func (d *dropBreaker) DoWithFallbackAcceptable(_ func() error, _ breaker.Fallback,
_ breaker.Acceptable) error { _ breaker.Acceptable) error {
return nil return nil
} }
func (d *dropBreaker) DoWithFallbackAcceptableCtx(_ context.Context, _ func() error,
_ breaker.Fallback, _ breaker.Acceptable) error {
return nil
}

View File

@@ -69,27 +69,21 @@ func newModel(name string, cli *mongo.Client, coll Collection, brk breaker.Break
// StartSession starts a new session. // StartSession starts a new session.
func (m *Model) StartSession(opts ...*mopt.SessionOptions) (sess mongo.Session, err error) { func (m *Model) StartSession(opts ...*mopt.SessionOptions) (sess mongo.Session, err error) {
err = m.brk.DoWithAcceptable(func() error { starTime := timex.Now()
starTime := timex.Now() defer func() {
defer func() { logDuration(context.Background(), m.name, startSession, starTime, err)
logDuration(context.Background(), m.name, startSession, starTime, err) }()
}()
session, sessionErr := m.cli.StartSession(opts...) session, sessionErr := m.cli.StartSession(opts...)
if sessionErr != nil { if sessionErr != nil {
return sessionErr return nil, sessionErr
} }
sess = &wrappedSession{ return &wrappedSession{
Session: session, Session: session,
name: m.name, name: m.name,
brk: m.brk, brk: m.brk,
} }, nil
return nil
}, acceptable)
return
} }
// Aggregate executes an aggregation pipeline. // Aggregate executes an aggregation pipeline.
@@ -184,7 +178,7 @@ func (w *wrappedSession) AbortTransaction(ctx context.Context) (err error) {
endSpan(span, err) endSpan(span, err)
}() }()
return w.brk.DoWithAcceptable(func() error { return w.brk.DoWithAcceptableCtx(ctx, func() error {
starTime := timex.Now() starTime := timex.Now()
defer func() { defer func() {
logDuration(ctx, w.name, abortTransaction, starTime, err) logDuration(ctx, w.name, abortTransaction, starTime, err)
@@ -201,7 +195,7 @@ func (w *wrappedSession) CommitTransaction(ctx context.Context) (err error) {
endSpan(span, err) endSpan(span, err)
}() }()
return w.brk.DoWithAcceptable(func() error { return w.brk.DoWithAcceptableCtx(ctx, func() error {
starTime := timex.Now() starTime := timex.Now()
defer func() { defer func() {
logDuration(ctx, w.name, commitTransaction, starTime, err) logDuration(ctx, w.name, commitTransaction, starTime, err)
@@ -222,7 +216,7 @@ func (w *wrappedSession) WithTransaction(
endSpan(span, err) endSpan(span, err)
}() }()
err = w.brk.DoWithAcceptable(func() error { err = w.brk.DoWithAcceptableCtx(ctx, func() error {
starTime := timex.Now() starTime := timex.Now()
defer func() { defer func() {
logDuration(ctx, w.name, withTransaction, starTime, err) logDuration(ctx, w.name, withTransaction, starTime, err)
@@ -243,7 +237,7 @@ func (w *wrappedSession) EndSession(ctx context.Context) {
endSpan(span, err) endSpan(span, err)
}() }()
err = w.brk.DoWithAcceptable(func() error { err = w.brk.DoWithAcceptableCtx(ctx, func() error {
starTime := timex.Now() starTime := timex.Now()
defer func() { defer func() {
logDuration(ctx, w.name, endSession, starTime, err) logDuration(ctx, w.name, endSession, starTime, err)

View File

@@ -2,8 +2,8 @@ package mon
import ( import (
"context" "context"
"errors"
"github.com/zeromicro/go-zero/core/errorx"
"github.com/zeromicro/go-zero/core/trace" "github.com/zeromicro/go-zero/core/trace"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
@@ -24,8 +24,7 @@ func startSpan(ctx context.Context, cmd string) (context.Context, oteltrace.Span
func endSpan(span oteltrace.Span, err error) { func endSpan(span oteltrace.Span, err error) {
defer span.End() defer span.End()
if err == nil || errors.Is(err, mongo.ErrNoDocuments) || if err == nil || errorx.In(err, mongo.ErrNoDocuments, mongo.ErrNilValue, mongo.ErrNilDocument) {
errors.Is(err, mongo.ErrNilValue) || errors.Is(err, mongo.ErrNilDocument) {
span.SetStatus(codes.Ok, "") span.SetStatus(codes.Ok, "")
return return
} }

View File

@@ -26,7 +26,7 @@ func (h breakerHook) ProcessHook(next red.ProcessHook) red.ProcessHook {
return next(ctx, cmd) return next(ctx, cmd)
} }
return h.brk.DoWithAcceptable(func() error { return h.brk.DoWithAcceptableCtx(ctx, func() error {
return next(ctx, cmd) return next(ctx, cmd)
}, acceptable) }, acceptable)
} }
@@ -34,7 +34,7 @@ func (h breakerHook) ProcessHook(next red.ProcessHook) red.ProcessHook {
func (h breakerHook) ProcessPipelineHook(next red.ProcessPipelineHook) red.ProcessPipelineHook { func (h breakerHook) ProcessPipelineHook(next red.ProcessPipelineHook) red.ProcessPipelineHook {
return func(ctx context.Context, cmds []red.Cmder) error { return func(ctx context.Context, cmds []red.Cmder) error {
return h.brk.DoWithAcceptable(func() error { return h.brk.DoWithAcceptableCtx(ctx, func() error {
return next(ctx, cmds) return next(ctx, cmds)
}, acceptable) }, acceptable)
} }

View File

@@ -0,0 +1,5 @@
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

View File

@@ -0,0 +1,6 @@
if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end

View File

@@ -90,6 +90,18 @@ type (
StringCmd = red.StringCmd StringCmd = red.StringCmd
// Script is an alias of redis.Script. // Script is an alias of redis.Script.
Script = red.Script Script = red.Script
// Hook is an alias of redis.Hook.
Hook = red.Hook
// DialHook is an alias of redis.DialHook.
DialHook = red.DialHook
// ProcessHook is an alias of redis.ProcessHook.
ProcessHook = red.ProcessHook
// ProcessPipelineHook is an alias of redis.ProcessPipelineHook.
ProcessPipelineHook = red.ProcessPipelineHook
// Cmder is an alias of redis.Cmder.
Cmder = red.Cmder
) )
// MustNewRedis returns a Redis with given options. // MustNewRedis returns a Redis with given options.
@@ -1185,6 +1197,18 @@ func (s *Redis) PipelinedCtx(ctx context.Context, fn func(Pipeliner) error) erro
return err return err
} }
func (s *Redis) Publish(channel string, message interface{}) (int64, error) {
return s.PublishCtx(context.Background(), channel, message)
}
func (s *Redis) PublishCtx(ctx context.Context, channel string, message interface{}) (int64, error) {
conn, err := getRedis(s)
if err != nil {
return 0, err
}
return conn.Publish(ctx, channel, message).Result()
}
// Rpop is the implementation of redis rpop command. // Rpop is the implementation of redis rpop command.
func (s *Redis) Rpop(key string) (string, error) { func (s *Redis) Rpop(key string) (string, error) {
return s.RpopCtx(context.Background(), key) return s.RpopCtx(context.Background(), key)
@@ -1235,6 +1259,18 @@ func (s *Redis) RpushCtx(ctx context.Context, key string, values ...any) (int, e
return int(v), nil return int(v), nil
} }
func (s *Redis) RPopLPush(source string, destination string) (string, error) {
return s.RPopLPushCtx(context.Background(), source, destination)
}
func (s *Redis) RPopLPushCtx(ctx context.Context, source string, destination string) (string, error) {
conn, err := getRedis(s)
if err != nil {
return "", err
}
return conn.RPopLPush(ctx, source, destination).Result()
}
// Sadd is the implementation of redis sadd command. // Sadd is the implementation of redis sadd command.
func (s *Redis) Sadd(key string, values ...any) (int, error) { func (s *Redis) Sadd(key string, values ...any) (int, error) {
return s.SaddCtx(context.Background(), key, values...) return s.SaddCtx(context.Background(), key, values...)
@@ -1633,6 +1669,26 @@ func (s *Redis) TtlCtx(ctx context.Context, key string) (int, error) {
return int(duration), nil return int(duration), nil
} }
func (s *Redis) TxPipeline() (pipe Pipeliner, err error) {
conn, err := getRedis(s)
if err != nil {
return nil, err
}
return conn.TxPipeline(), nil
}
func (s *Redis) Unlink(keys ...string) (int64, error) {
return s.UnlinkCtx(context.Background(), keys...)
}
func (s *Redis) UnlinkCtx(ctx context.Context, keys ...string) (int64, error) {
conn, err := getRedis(s)
if err != nil {
return 0, err
}
return conn.Unlink(ctx, keys...).Result()
}
// Zadd is the implementation of redis zadd command. // Zadd is the implementation of redis zadd command.
func (s *Redis) Zadd(key string, score int64, value string) (bool, error) { func (s *Redis) Zadd(key string, score int64, value string) (bool, error) {
return s.ZaddCtx(context.Background(), key, score, value) return s.ZaddCtx(context.Background(), key, score, value)
@@ -2363,16 +2419,16 @@ func WithTLS() Option {
} }
} }
// withHook customizes the given Redis with given durationHook, only for private use now, // WithHook customizes the given Redis with given durationHook, only for private use now,
// maybe expose later. // maybe expose later.
func withHook(hook red.Hook) Option { func WithHook(hook Hook) Option {
return func(r *Redis) { return func(r *Redis) {
r.hooks = append(r.hooks, hook) r.hooks = append(r.hooks, hook)
} }
} }
func acceptable(err error) bool { func acceptable(err error) bool {
return err == nil || errors.Is(err, red.Nil) || errors.Is(err, context.Canceled) return err == nil || errorx.In(err, red.Nil, context.Canceled)
} }
func getRedis(r *Redis) (RedisNode, error) { func getRedis(r *Redis) (RedisNode, error) {

View File

@@ -160,7 +160,7 @@ func TestRedis_NonBlock(t *testing.T) {
Host: s.Addr(), Host: s.Addr(),
NonBlock: true, NonBlock: true,
Type: NodeType, Type: NodeType,
}, withHook(myHook{includePing: true})) }, WithHook(myHook{includePing: true}))
assert.NoError(t, err) assert.NoError(t, err)
}) })
@@ -170,7 +170,7 @@ func TestRedis_NonBlock(t *testing.T) {
Host: s.Addr(), Host: s.Addr(),
NonBlock: false, NonBlock: false,
Type: NodeType, Type: NodeType,
}, withHook(myHook{includePing: true})) }, WithHook(myHook{includePing: true}))
assert.ErrorContains(t, err, "redis connect error") assert.ErrorContains(t, err, "redis connect error")
}) })
} }
@@ -2080,3 +2080,70 @@ func (n mockedNode) BLPop(_ context.Context, _ time.Duration, _ ...string) *red.
return cmd return cmd
} }
func TestRedisPublish(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).Publish("Test", "message")
assert.NotNil(t, err)
_, err = client.Publish("Test", "message")
assert.Nil(t, err)
})
}
func TestRedisRPopLPush(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).RPopLPush("Source", "Destination")
assert.NotNil(t, err)
_, err = client.Rpush("Source", "Destination")
assert.Nil(t, err)
_, err = client.RPopLPush("Source", "Destination")
assert.Nil(t, err)
})
}
func TestRedisUnlink(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).Unlink("Key1", "Key2")
assert.NotNil(t, err)
err = client.Set("Key1", "Key2")
assert.Nil(t, err)
get, err := client.Get("Key1")
assert.Nil(t, err)
assert.Equal(t, "Key2", get)
res, err := client.Unlink("Key1")
assert.Nil(t, err)
assert.Equal(t, int64(1), res)
})
}
func TestRedisTxPipeline(t *testing.T) {
runOnRedis(t, func(client *Redis) {
ctx := context.Background()
pipe, err := newRedis(client.Addr, badType()).TxPipeline()
assert.NotNil(t, err)
pipe, err = client.TxPipeline()
assert.Nil(t, err)
key := "key"
hashKey := "field"
hashValue := "value"
// setting value
pipe.HSet(ctx, key, hashKey, hashValue)
existsCmd := pipe.Exists(ctx, key)
getCmd := pipe.HGet(ctx, key, hashKey)
// execution
_, err = pipe.Exec(ctx)
assert.Nil(t, err)
// verification results
exists, err := existsCmd.Result()
assert.Nil(t, err)
assert.Equal(t, int64(1), exists)
value, err := getCmd.Result()
assert.Nil(t, err)
assert.Equal(t, hashValue, value)
})
}

View File

@@ -2,6 +2,7 @@ package redis
import ( import (
"context" "context"
_ "embed"
"errors" "errors"
"math/rand" "math/rand"
"strconv" "strconv"
@@ -20,17 +21,13 @@ const (
) )
var ( var (
lockScript = NewScript(`if redis.call("GET", KEYS[1]) == ARGV[1] then //go:embed lockscript.lua
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) lockLuaScript string
return "OK" lockScript = NewScript(lockLuaScript)
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) //go:embed delscript.lua
end`) delLuaScript string
delScript = NewScript(`if redis.call("GET", KEYS[1]) == ARGV[1] then delScript = NewScript(delLuaScript)
return redis.call("DEL", KEYS[1])
else
return 0
end`)
) )
// A RedisLock is a redis lock. // A RedisLock is a redis lock.

View File

@@ -190,6 +190,17 @@ func (cc CachedConn) QueryRowNoCacheCtx(ctx context.Context, v any, q string,
return cc.db.QueryRowCtx(ctx, v, q, args...) return cc.db.QueryRowCtx(ctx, v, q, args...)
} }
// QueryRowPartialNoCache unmarshals into v with given statement.
func (cc CachedConn) QueryRowPartialNoCache(v any, q string, args ...any) error {
return cc.QueryRowPartialNoCacheCtx(context.Background(), v, q, args...)
}
// QueryRowPartialNoCacheCtx unmarshals into v with given statement.
func (cc CachedConn) QueryRowPartialNoCacheCtx(ctx context.Context, v any, q string,
args ...any) error {
return cc.db.QueryRowPartialCtx(ctx, v, q, args...)
}
// QueryRowsNoCache unmarshals into v with given statement. // QueryRowsNoCache unmarshals into v with given statement.
// It doesn't use cache, because it might cause consistency problem. // It doesn't use cache, because it might cause consistency problem.
func (cc CachedConn) QueryRowsNoCache(v any, q string, args ...any) error { func (cc CachedConn) QueryRowsNoCache(v any, q string, args ...any) error {
@@ -203,6 +214,19 @@ func (cc CachedConn) QueryRowsNoCacheCtx(ctx context.Context, v any, q string,
return cc.db.QueryRowsCtx(ctx, v, q, args...) return cc.db.QueryRowsCtx(ctx, v, q, args...)
} }
// QueryRowsPartialNoCache unmarshals into v with given statement.
// It doesn't use cache, because it might cause consistency problem.
func (cc CachedConn) QueryRowsPartialNoCache(v any, q string, args ...any) error {
return cc.QueryRowsPartialNoCacheCtx(context.Background(), v, q, args...)
}
// QueryRowsPartialNoCacheCtx unmarshals into v with given statement.
// It doesn't use cache, because it might cause consistency problem.
func (cc CachedConn) QueryRowsPartialNoCacheCtx(ctx context.Context, v any, q string,
args ...any) error {
return cc.db.QueryRowsPartialCtx(ctx, v, q, args...)
}
// SetCache sets v into cache with given key. // SetCache sets v into cache with given key.
func (cc CachedConn) SetCache(key string, val any) error { func (cc CachedConn) SetCache(key string, val any) error {
return cc.SetCacheCtx(context.Background(), key, val) return cc.SetCacheCtx(context.Background(), key, val)

View File

@@ -579,6 +579,48 @@ func TestQueryRowNoCache(t *testing.T) {
assert.True(t, ran) assert.True(t, ran)
} }
func TestQueryRowPartialNoCache(t *testing.T) {
r := redistest.CreateRedis(t)
const (
key = "user"
value = "any"
)
var user string
var ran bool
conn := dummySqlConn{queryRow: func(v any, q string, args ...any) error {
user = value
ran = true
return nil
}}
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
err := c.QueryRowPartialNoCache(&user, key)
assert.Nil(t, err)
assert.Equal(t, value, user)
assert.True(t, ran)
}
func TestQueryRowsPartialNoCache(t *testing.T) {
r := redistest.CreateRedis(t)
var (
key = "user"
values = []string{"any", "any"}
)
var users []string
var ran bool
conn := dummySqlConn{queryRows: func(v any, q string, args ...any) error {
users = values
ran = true
return nil
}}
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*30))
err := c.QueryRowsPartialNoCache(&users, key)
assert.Nil(t, err)
assert.Equal(t, values, users)
assert.True(t, ran)
}
func TestNewConnWithCache(t *testing.T) { func TestNewConnWithCache(t *testing.T) {
r := redistest.CreateRedis(t) r := redistest.CreateRedis(t)
@@ -716,7 +758,8 @@ func resetStats() {
} }
type dummySqlConn struct { type dummySqlConn struct {
queryRow func(any, string, ...any) error queryRow func(any, string, ...any) error
queryRows func(any, string, ...any) error
} }
func (d dummySqlConn) ExecCtx(_ context.Context, _ string, _ ...any) (sql.Result, error) { func (d dummySqlConn) ExecCtx(_ context.Context, _ string, _ ...any) (sql.Result, error) {
@@ -727,7 +770,11 @@ func (d dummySqlConn) PrepareCtx(_ context.Context, _ string) (sqlx.StmtSession,
return nil, nil return nil, nil
} }
func (d dummySqlConn) QueryRowPartialCtx(_ context.Context, _ any, _ string, _ ...any) error { func (d dummySqlConn) QueryRowPartialCtx(_ context.Context, v any, query string, args ...any) error {
if d.queryRow != nil {
return d.queryRow(v, query, args...)
}
return nil return nil
} }
@@ -735,7 +782,11 @@ func (d dummySqlConn) QueryRowsCtx(_ context.Context, _ any, _ string, _ ...any)
return nil return nil
} }
func (d dummySqlConn) QueryRowsPartialCtx(_ context.Context, _ any, _ string, _ ...any) error { func (d dummySqlConn) QueryRowsPartialCtx(_ context.Context, v any, query string, args ...any) error {
if d.queryRows != nil {
return d.queryRows(v, query, args...)
}
return nil return nil
} }

View File

@@ -1,6 +1,7 @@
package sqlx package sqlx
import ( import (
"context"
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
@@ -87,6 +88,10 @@ func getValueInterface(value reflect.Value) (any, error) {
} }
} }
func isScanFailed(err error) bool {
return err != nil && !errors.Is(err, context.DeadlineExceeded)
}
func mapStructFieldsIntoSlice(v reflect.Value, columns []string, strict bool) ([]any, error) { func mapStructFieldsIntoSlice(v reflect.Value, columns []string, strict bool) ([]any, error) {
fields := unwrapFields(v) fields := unwrapFields(v)
if strict && len(columns) < len(fields) { if strict && len(columns) < len(fields) {
@@ -248,7 +253,7 @@ func unmarshalRows(v any, scanner rowsScanner, strict bool) error {
return ErrUnsupportedValueType return ErrUnsupportedValueType
} }
return nil return scanner.Err()
default: default:
return ErrUnsupportedValueType return ErrUnsupportedValueType
} }

View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"github.com/zeromicro/go-zero/core/breaker" "github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/errorx"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -83,7 +84,7 @@ func NewSqlConn(driverName, datasource string, opts ...SqlOption) SqlConn {
} }
// NewSqlConnFromDB returns a SqlConn with the given sql.DB. // NewSqlConnFromDB returns a SqlConn with the given sql.DB.
// Use it with caution, it's provided for other ORM to interact with. // Use it with caution; it's provided for other ORM to interact with.
func NewSqlConnFromDB(db *sql.DB, opts ...SqlOption) SqlConn { func NewSqlConnFromDB(db *sql.DB, opts ...SqlOption) SqlConn {
conn := &commonSqlConn{ conn := &commonSqlConn{
connProv: func() (*sql.DB, error) { connProv: func() (*sql.DB, error) {
@@ -120,7 +121,7 @@ func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...any) (
endSpan(span, err) endSpan(span, err)
}() }()
err = db.brk.DoWithAcceptable(func() error { err = db.brk.DoWithAcceptableCtx(ctx, func() error {
var conn *sql.DB var conn *sql.DB
conn, err = db.connProv() conn, err = db.connProv()
if err != nil { if err != nil {
@@ -148,7 +149,7 @@ func (db *commonSqlConn) PrepareCtx(ctx context.Context, query string) (stmt Stm
endSpan(span, err) endSpan(span, err)
}() }()
err = db.brk.DoWithAcceptable(func() error { err = db.brk.DoWithAcceptableCtx(ctx, func() error {
var conn *sql.DB var conn *sql.DB
conn, err = db.connProv() conn, err = db.connProv()
if err != nil { if err != nil {
@@ -256,7 +257,7 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
endSpan(span, err) endSpan(span, err)
}() }()
err = db.brk.DoWithAcceptable(func() error { err = db.brk.DoWithAcceptableCtx(ctx, func() error {
return transact(ctx, db, db.beginTx, fn) return transact(ctx, db, db.beginTx, fn)
}, db.acceptable) }, db.acceptable)
if errors.Is(err, breaker.ErrServiceUnavailable) { if errors.Is(err, breaker.ErrServiceUnavailable) {
@@ -267,8 +268,7 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
} }
func (db *commonSqlConn) acceptable(err error) bool { func (db *commonSqlConn) acceptable(err error) bool {
if err == nil || errors.Is(err, sql.ErrNoRows) || errors.Is(err, sql.ErrTxDone) || if err == nil || errorx.In(err, sql.ErrNoRows, sql.ErrTxDone, context.Canceled) {
errors.Is(err, context.Canceled) {
return true return true
} }
@@ -287,7 +287,7 @@ func (db *commonSqlConn) acceptable(err error) bool {
func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows) error, func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows) error,
q string, args ...any) (err error) { q string, args ...any) (err error) {
var scanFailed bool var scanFailed bool
err = db.brk.DoWithAcceptable(func() error { err = db.brk.DoWithAcceptableCtx(ctx, func() error {
conn, err := db.connProv() conn, err := db.connProv()
if err != nil { if err != nil {
db.onError(ctx, err) db.onError(ctx, err)
@@ -296,7 +296,7 @@ func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows)
return query(ctx, conn, func(rows *sql.Rows) error { return query(ctx, conn, func(rows *sql.Rows) error {
e := scanner(rows) e := scanner(rows)
if e != nil { if isScanFailed(e) {
scanFailed = true scanFailed = true
} }
return e return e

View File

@@ -65,7 +65,7 @@ func (s statement) ExecCtx(ctx context.Context, args ...any) (result sql.Result,
endSpan(span, err) endSpan(span, err)
}() }()
err = s.brk.DoWithAcceptable(func() error { err = s.brk.DoWithAcceptableCtx(ctx, func() error {
result, err = execStmt(ctx, s.stmt, s.query, args...) result, err = execStmt(ctx, s.stmt, s.query, args...)
return err return err
}, func(err error) bool { }, func(err error) bool {
@@ -141,10 +141,10 @@ func (s statement) QueryRowsPartialCtx(ctx context.Context, v any, args ...any)
func (s statement) queryRows(ctx context.Context, scanFn func(any, rowsScanner) error, func (s statement) queryRows(ctx context.Context, scanFn func(any, rowsScanner) error,
v any, args ...any) error { v any, args ...any) error {
var scanFailed bool var scanFailed bool
err := s.brk.DoWithAcceptable(func() error { err := s.brk.DoWithAcceptableCtx(ctx, func() error {
return queryStmt(ctx, s.stmt, func(rows *sql.Rows) error { return queryStmt(ctx, s.stmt, func(rows *sql.Rows) error {
err := scanFn(v, rows) err := scanFn(v, rows)
if err != nil { if isScanFailed(err) {
scanFailed = true scanFailed = true
} }
return err return err

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"strconv"
"testing" "testing"
"time" "time"
@@ -290,6 +291,24 @@ func TestStmtBreaker(t *testing.T) {
}) })
} }
func TestQueryRowsScanTimeout(t *testing.T) {
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{"foo"})
for i := 0; i < 10000; i++ {
rows = rows.AddRow("bar" + strconv.Itoa(i))
}
mock.ExpectQuery("any").WillReturnRows(rows)
var val []struct {
Foo string
}
conn := NewSqlConnFromDB(db)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*2)
err := conn.QueryRowsCtx(ctx, &val, "any")
assert.ErrorIs(t, err, context.DeadlineExceeded)
cancel()
})
}
type mockedSessionConn struct { type mockedSessionConn struct {
lastInsertId int64 lastInsertId int64
rowsAffected int64 rowsAffected int64

View File

@@ -97,9 +97,12 @@ func createExporter(c Config) (sdktrace.SpanExporter, error) {
case kindOtlpHttp: case kindOtlpHttp:
// Not support flexible configuration now. // Not support flexible configuration now.
opts := []otlptracehttp.Option{ opts := []otlptracehttp.Option{
otlptracehttp.WithInsecure(),
otlptracehttp.WithEndpoint(c.Endpoint), otlptracehttp.WithEndpoint(c.Endpoint),
} }
if !c.OtlpHttpSecure {
opts = append(opts, otlptracehttp.WithInsecure())
}
if len(c.OtlpHeaders) > 0 { if len(c.OtlpHeaders) > 0 {
opts = append(opts, otlptracehttp.WithHeaders(c.OtlpHeaders)) opts = append(opts, otlptracehttp.WithHeaders(c.OtlpHeaders))
} }

View File

@@ -17,6 +17,8 @@ type Config struct {
// For example // For example
// /v1/traces // /v1/traces
OtlpHttpPath string `json:",optional"` OtlpHttpPath string `json:",optional"`
// OtlpHttpSecure represents the scheme to use for OTLP HTTP transport.
OtlpHttpSecure bool `json:",optional"`
// Disabled indicates whether StartAgent starts the agent. // Disabled indicates whether StartAgent starts the agent.
Disabled bool `json:",optional"` Disabled bool `json:",optional"`
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@@ -13,8 +14,9 @@ import (
) )
func TestMessageType_Event(t *testing.T) { func TestMessageType_Event(t *testing.T) {
var span mockSpan ctx, s := otel.Tracer(TraceName).Start(context.Background(), "test")
ctx := trace.ContextWithSpan(context.Background(), &span) span := mockSpan{Span: s}
ctx = trace.ContextWithSpan(ctx, &span)
MessageReceived.Event(ctx, 1, "foo") MessageReceived.Event(ctx, 1, "foo")
assert.Equal(t, messageEvent, span.name) assert.Equal(t, messageEvent, span.name)
assert.NotEmpty(t, span.options) assert.NotEmpty(t, span.options)
@@ -30,6 +32,7 @@ func TestMessageType_EventProtoMessage(t *testing.T) {
} }
type mockSpan struct { type mockSpan struct {
trace.Span
name string name string
options []trace.EventOption options []trace.EventOption
} }

114
go.mod
View File

@@ -1,66 +1,69 @@
module github.com/zeromicro/go-zero module github.com/zeromicro/go-zero
go 1.19 go 1.20
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alicebob/miniredis/v2 v2.32.1 github.com/alicebob/miniredis/v2 v2.33.0
github.com/fatih/color v1.16.0 github.com/fatih/color v1.18.0
github.com/fullstorydev/grpcurl v1.8.9 github.com/fullstorydev/grpcurl v1.9.1
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.4 github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.5.5 github.com/jackc/pgx/v5 v5.6.0
github.com/jhump/protoreflect v1.15.6 github.com/jhump/protoreflect v1.17.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml/v2 v2.2.0 github.com/pelletier/go-toml/v2 v2.2.2
github.com/prometheus/client_golang v1.18.0 github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.4.0 github.com/redis/go-redis/v9 v9.7.0
github.com/spaolacci/murmur3 v1.1.0 github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
go.etcd.io/etcd/api/v3 v3.5.13 go.etcd.io/etcd/api/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.13 go.etcd.io/etcd/client/v3 v3.5.15
go.mongodb.org/mongo-driver v1.13.1 go.mongodb.org/mongo-driver v1.17.1
go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0
go.opentelemetry.io/otel/exporters/zipkin v1.19.0 go.opentelemetry.io/otel/exporters/zipkin v1.24.0
go.opentelemetry.io/otel/sdk v1.19.0 go.opentelemetry.io/otel/sdk v1.24.0
go.opentelemetry.io/otel/trace v1.19.0 go.opentelemetry.io/otel/trace v1.24.0
go.uber.org/automaxprocs v1.5.3 go.uber.org/automaxprocs v1.6.0
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.3.0
golang.org/x/net v0.24.0 golang.org/x/net v0.31.0
golang.org/x/sys v0.19.0 golang.org/x/sys v0.27.0
golang.org/x/time v0.5.0 golang.org/x/time v0.8.0
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
google.golang.org/grpc v1.63.0 google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.35.2
gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.29.3 k8s.io/api v0.29.3
k8s.io/apimachinery v0.29.3 k8s.io/apimachinery v0.29.4
k8s.io/client-go v0.29.3 k8s.io/client-go v0.29.3
k8s.io/utils v0.0.0-20230726121419-3b25d923346b k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.8.0 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/envoyproxy/go-control-plane v0.12.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -70,49 +73,48 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.22.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.9.0 // indirect
golang.org/x/term v0.19.0 // indirect golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.20.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.110.1 // indirect k8s.io/klog/v2 v2.110.1 // indirect

244
go.sum
View File

@@ -2,25 +2,23 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bufbuild/protocompile v0.8.0 h1:9Kp1q6OkS9L4nM3FYbr8vlJnEwtbpDPQlQOVXfR+78s= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.8.0/go.mod h1:+Etjg4guZoAqzVk2czwEQP12yaxLJ8DxuqCJ9qHdH94= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -33,13 +31,18 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
github.com/fullstorydev/grpcurl v1.8.9 h1:JMvZXK8lHDGyLmTQ0ZdGDnVVGuwjbpaumf8p42z0d+c= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/fullstorydev/grpcurl v1.8.9/go.mod h1:PNNKevV5VNAV2loscyLISrEnWQI61eqR0F8l3bVadAA= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fullstorydev/grpcurl v1.9.1 h1:YxX1aCcCc4SDBQfj9uoWcTLe8t4NWrZe1y+mk83BQgo=
github.com/fullstorydev/grpcurl v1.9.1/go.mod h1:i8gKLIC6s93WdU3LSmkE5vtsCxyRmihUj5FK1cNW5EM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@@ -55,22 +58,16 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 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/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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -80,20 +77,20 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.15.6 h1:WMYJbw2Wo+KOWwZFvgY0jMoVHM6i4XIvRs2RcBj5VmI= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.15.6/go.mod h1:jCHoyYQIJnaabEYnbGwyo9hUqfyUMTbJw/tAut5t97E= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -101,15 +98,16 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -120,15 +118,13 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
@@ -137,24 +133,24 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -180,63 +176,60 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4= go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c= go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg= go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8= go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js= go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI= go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -247,73 +240,62 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -331,16 +313,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=

View File

@@ -77,10 +77,12 @@ func (s *Server) StartAsync(c Config) {
// StartAgent start inner http server by config. // StartAgent start inner http server by config.
func StartAgent(c Config) { func StartAgent(c Config) {
if !c.Enabled {
return
}
once.Do(func() { once.Do(func() {
if c.Enabled { s := NewServer(c)
s := NewServer(c) s.StartAsync(c)
s.StartAsync(c)
}
}) })
} }

View File

@@ -8,13 +8,14 @@
[![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/zeromicro/go-zero/actions) [![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/zeromicro/go-zero/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/zeromicro/go-zero)](https://goreportcard.com/report/github.com/zeromicro/go-zero) [![Go Report Card](https://goreportcard.com/badge/github.com/zeromicro/go-zero)](https://goreportcard.com/report/github.com/zeromicro/go-zero)
[![goproxy](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg)](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg) [![goproxy](https://goproxy.cn/stats/github.com/zeromicro/go-zero/badges/download-count.svg)](https://goproxy.cn/stats/github.com/zeromicro/go-zero/badges/download-count.svg)
[![codecov](https://codecov.io/gh/zeromicro/go-zero/branch/master/graph/badge.svg)](https://codecov.io/gh/zeromicro/go-zero) [![codecov](https://codecov.io/gh/zeromicro/go-zero/branch/master/graph/badge.svg)](https://codecov.io/gh/zeromicro/go-zero)
[![Release](https://img.shields.io/github/v/release/zeromicro/go-zero.svg?style=flat-square)](https://github.com/zeromicro/go-zero) [![Release](https://img.shields.io/github/v/release/zeromicro/go-zero.svg?style=flat-square)](https://github.com/zeromicro/go-zero)
[![Go Reference](https://pkg.go.dev/badge/github.com/zeromicro/go-zero.svg)](https://pkg.go.dev/github.com/zeromicro/go-zero) [![Go Reference](https://pkg.go.dev/badge/github.com/zeromicro/go-zero.svg)](https://pkg.go.dev/github.com/zeromicro/go-zero)
[![Awesome Go](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go) [![Awesome Go](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<a href="https://trendshift.io/repositories/3263" target="_blank"><img src="https://trendshift.io/api/badge/repositories/3263" alt="zeromicro%2Fgo-zero | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go&#0045;zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go&#0045;zero - A&#0032;web&#0032;&#0038;&#0032;rpc&#0032;framework&#0032;written&#0032;in&#0032;Go&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go&#0045;zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go&#0045;zero - A&#0032;web&#0032;&#0038;&#0032;rpc&#0032;framework&#0032;written&#0032;in&#0032;Go&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 0. go-zero 介绍 ## 0. go-zero 介绍
@@ -117,25 +118,19 @@ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro
# For Mac # For Mac
brew install goctl brew install goctl
# docker for amd64 architecture # docker for all platforms
docker pull kevinwan/goctl docker pull kevinwan/goctl
# run goctl like # run goctl
docker run --rm -it -v `pwd`:/app kevinwan/goctl --help docker run --rm -it -v `pwd`:/app kevinwan/goctl --help
# docker for arm64(Mac) architecture
docker pull kevinwan/goctl:latest-arm64
# run goctl like
docker run --rm -it -v `pwd`:/app kevinwan/goctl:latest-arm64 --help
``` ```
确保 goctl 可执行 确保 goctl 可执行,并且在 $PATH 环境变量里。
2. 快速生成 api 服务 2. 快速生成 api 服务
```shell ```shell
goctl api new greet goctl api new greet
cd greet cd greet
go mod init
go mod tidy go mod tidy
go run greet.go -f etc/greet-api.yaml go run greet.go -f etc/greet-api.yaml
``` ```
@@ -302,6 +297,8 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
>98. 上海同犀智能科技有限公司 >98. 上海同犀智能科技有限公司
>99. 新华三技术有限公司 >99. 新华三技术有限公司
>100. 上海邑脉科技有限公司 >100. 上海邑脉科技有限公司
>101. 上海巨瓴科技有限公司
>102. 深圳市兴海物联科技有限公司
如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。 如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
@@ -332,8 +329,8 @@ go-zero 收录在 [CNCF Cloud Native 云原生技术全景图](https://landscape
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/wechat.jpg" alt="wechat" width="300" /> <img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/wechat.jpg" alt="wechat" width="300" />
## 13. 赞助一下👍 ## 13. 知识星球
如果觉得项目有帮助,可以请作者喝杯咖啡 🍹 官方团队运营的知识星球
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/sponsor.png" alt="wechat" width="300" /> <img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/zsxq.jpg" alt="知识星球" width="300" />

View File

@@ -22,6 +22,7 @@ go-zero is a web and rpc framework with lots of builtin engineering practices. I
## 🤷‍ What is go-zero? ## 🤷‍ What is go-zero?
English | [简体中文](readme-cn.md) English | [简体中文](readme-cn.md)
<a href="https://trendshift.io/repositories/3263" target="_blank"><img src="https://trendshift.io/api/badge/repositories/3263" alt="zeromicro%2Fgo-zero | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go&#0045;zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go&#0045;zero - A&#0032;web&#0032;&#0038;&#0032;rpc&#0032;framework&#0032;written&#0032;in&#0032;Go&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go&#0045;zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go&#0045;zero - A&#0032;web&#0032;&#0038;&#0032;rpc&#0032;framework&#0032;written&#0032;in&#0032;Go&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@@ -42,7 +43,6 @@ go-zero contains simple API description syntax and code generation tool called `
## Backgrounds of go-zero ## Backgrounds of go-zero
At the beginning of 2018, we decided to re-design our system, from monolithic architecture with Java+MongoDB to microservice architecture. After research and comparison, we chose to:
In early 2018, we embarked on a transformative journey to redesign our system, transitioning from a monolithic architecture built with Java and MongoDB to a microservices architecture. After careful research and comparison, we made a deliberate choice to: In early 2018, we embarked on a transformative journey to redesign our system, transitioning from a monolithic architecture built with Java and MongoDB to a microservices architecture. After careful research and comparison, we made a deliberate choice to:
* Go Beyond with Golang * Go Beyond with Golang
@@ -124,18 +124,13 @@ go get -u github.com/zeromicro/go-zero
# For Mac # For Mac
brew install goctl brew install goctl
# docker for amd64 architecture # docker for all platforms
docker pull kevinwan/goctl docker pull kevinwan/goctl
# run goctl like # run goctl
docker run --rm -it -v `pwd`:/app kevinwan/goctl --help docker run --rm -it -v `pwd`:/app kevinwan/goctl --help
# docker for arm64(Mac) architecture
docker pull kevinwan/goctl:latest-arm64
# run goctl like
docker run --rm -it -v `pwd`:/app kevinwan/goctl:latest-arm64 --help
``` ```
make sure goctl is executable. make sure goctl is executable and in your $PATH.
3. Create the API file, like greet.api, you can install the plugin of goctl in vs code, api syntax is supported. 3. Create the API file, like greet.api, you can install the plugin of goctl in vs code, api syntax is supported.
@@ -194,7 +189,6 @@ go get -u github.com/zeromicro/go-zero
```shell ```shell
cd greet cd greet
go mod init
go mod tidy go mod tidy
go run greet.go -f etc/greet-api.yaml go run greet.go -f etc/greet-api.yaml
``` ```
@@ -256,7 +250,7 @@ go-zero enlisted in the [CNCF Cloud Native Landscape](https://landscape.cncf.io/
## Give a Star! ⭐ ## Give a Star! ⭐
If you like or are using this project to learn or start your solution, please give it a star. Thanks! If you like this project or are using it to learn or start your own solution, give it a star to get updates on new releases. Your support matters!
## Buy me a coffee ## Buy me a coffee

View File

@@ -23,7 +23,7 @@ func MaxConnsHandler(n int) func(http.Handler) http.Handler {
if latch.TryBorrow() { if latch.TryBorrow() {
defer func() { defer func() {
if err := latch.Return(); err != nil { if err := latch.Return(); err != nil {
logx.Error(err) logx.WithContext(r.Context()).Error(err)
} }
}() }()

View File

@@ -143,7 +143,7 @@ func fillPath(u *nurl.URL, val map[string]any) error {
delete(val, key) delete(val, key)
} }
var unused []string unused := make([]string, 0, len(val))
for key := range val { for key := range val {
unused = append(unused, key) unused = append(unused, key)
} }

View File

@@ -63,7 +63,7 @@ func (s namedService) do(r *http.Request) (resp *http.Response, err error) {
} }
brk := breaker.GetBreaker(s.name) brk := breaker.GetBreaker(s.name)
err = brk.DoWithAcceptable(func() error { err = brk.DoWithAcceptableCtx(r.Context(), func() error {
resp, err = s.cli.Do(r) resp, err = s.cli.Do(r)
return err return err
}, func(err error) bool { }, func(err error) bool {

View File

@@ -3,6 +3,7 @@ package httpx
import ( import (
"io" "io"
"net/http" "net/http"
"reflect"
"strings" "strings"
"sync/atomic" "sync/atomic"
@@ -23,9 +24,16 @@ const (
) )
var ( var (
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithOpaqueKeys()) formUnmarshaler = mapping.NewUnmarshaler(
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithOpaqueKeys()) formKey,
validator atomic.Value mapping.WithStringValues(),
mapping.WithOpaqueKeys(),
mapping.WithFromArray())
pathUnmarshaler = mapping.NewUnmarshaler(
pathKey,
mapping.WithStringValues(),
mapping.WithOpaqueKeys())
validator atomic.Value
) )
// Validator defines the interface for validating the request. // Validator defines the interface for validating the request.
@@ -36,16 +44,19 @@ type Validator interface {
// Parse parses the request. // Parse parses the request.
func Parse(r *http.Request, v any) error { func Parse(r *http.Request, v any) error {
if err := ParsePath(r, v); err != nil { kind := mapping.Deref(reflect.TypeOf(v)).Kind()
return err if kind != reflect.Array && kind != reflect.Slice {
} if err := ParsePath(r, v); err != nil {
return err
}
if err := ParseForm(r, v); err != nil { if err := ParseForm(r, v); err != nil {
return err return err
} }
if err := ParseHeaders(r, v); err != nil { if err := ParseHeaders(r, v); err != nil {
return err return err
}
} }
if err := ParseJsonBody(r, v); err != nil { if err := ParseJsonBody(r, v); err != nil {

View File

@@ -1,6 +1,7 @@
package httpx package httpx
import ( import (
"bytes"
"errors" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -48,6 +49,61 @@ func TestParseForm(t *testing.T) {
}) })
} }
func TestParseFormArray(t *testing.T) {
t.Run("slice", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&name=world&age=18&age=19&percent=3.4&percent=4.5",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello", "world"}, v.Name)
assert.ElementsMatch(t, []int{18, 19}, v.Age)
assert.ElementsMatch(t, []float64{3.4, 4.5}, v.Percent)
}
})
t.Run("slice with single value", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&age=18&percent=3.4",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello"}, v.Name)
assert.ElementsMatch(t, []int{18}, v.Age)
assert.ElementsMatch(t, []float64{3.4}, v.Percent)
}
})
t.Run("slice with empty and non-empty", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?name=&name=1",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1"}, v.Name)
}
})
}
func TestParseForm_Error(t *testing.T) { func TestParseForm_Error(t *testing.T) {
var v struct { var v struct {
Name string `form:"name"` Name string `form:"name"`
@@ -269,11 +325,31 @@ func TestParseJsonBody(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
r.Header.Set(ContentType, header.JsonContentType) r.Header.Set(ContentType, header.JsonContentType)
assert.NoError(t, ParseJsonBody(r, &v)) assert.NoError(t, Parse(r, &v))
assert.Equal(t, 1, len(v)) assert.Equal(t, 1, len(v))
assert.Equal(t, "kevin", v[0].Name) assert.Equal(t, "kevin", v[0].Name)
assert.Equal(t, 18, v[0].Age) assert.Equal(t, 18, v[0].Age)
}) })
t.Run("form and array body", func(t *testing.T) {
var v []struct {
// we can only ignore the form tag,
// because if the value is a slice, it should be in the body,
// but it's hard to detect when we treat it as a json body.
Product string `form:"product"`
Name string `json:"name"`
Age int `json:"age"`
}
body := `[{"name":"apple", "age": 18}]`
r := httptest.NewRequest(http.MethodPost, "/a?product=tree", strings.NewReader(body))
r.Header.Set(ContentType, header.JsonContentType)
assert.NoError(t, Parse(r, &v))
assert.Equal(t, 1, len(v))
assert.Equal(t, "apple", v[0].Name)
assert.Equal(t, 18, v[0].Age)
})
} }
func TestParseRequired(t *testing.T) { func TestParseRequired(t *testing.T) {
@@ -441,6 +517,17 @@ func TestParseWithEscapedParams(t *testing.T) {
}) })
} }
func TestCustomUnmarshalerStructRequest(t *testing.T) {
reqBody := `{"name": "hello"}`
r := httptest.NewRequest(http.MethodPost, "/a", bytes.NewReader([]byte(reqBody)))
r.Header.Set(ContentType, JsonContentType)
v := struct {
Foo *mockUnmarshaler `json:"name"`
}{}
assert.Nil(t, Parse(r, &v))
assert.Equal(t, "hello", v.Foo.Name)
}
func BenchmarkParseRaw(b *testing.B) { func BenchmarkParseRaw(b *testing.B) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody) r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody)
if err != nil { if err != nil {
@@ -515,3 +602,12 @@ func (m mockRequest) Validate() error {
return nil return nil
} }
type mockUnmarshaler struct {
Name string
}
func (m *mockUnmarshaler) UnmarshalJSON(b []byte) error {
m.Name = string(b)
return nil
}

View File

@@ -20,10 +20,16 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
} }
params := make(map[string]any, len(r.Form)) params := make(map[string]any, len(r.Form))
for name := range r.Form { for name, values := range r.Form {
formValue := r.Form.Get(name) filtered := make([]string, 0, len(values))
if len(formValue) > 0 { for _, v := range values {
params[name] = formValue if len(v) > 0 {
filtered = append(filtered, v)
}
}
if len(filtered) > 0 {
params[name] = filtered
} }
} }

View File

@@ -26,6 +26,11 @@ const (
originHeader = "Origin" originHeader = "Origin"
) )
// AddAllowHeaders sets the allowed headers.
func AddAllowHeaders(header http.Header, headers ...string) {
header.Add(allowHeaders, strings.Join(headers, ", "))
}
// NotAllowedHandler handles cross domain not allowed requests. // NotAllowedHandler handles cross domain not allowed requests.
// At most one origin can be specified, other origins are ignored if given, default to be *. // At most one origin can be specified, other origins are ignored if given, default to be *.
func NotAllowedHandler(fn func(w http.ResponseWriter), origins ...string) http.Handler { func NotAllowedHandler(fn func(w http.ResponseWriter), origins ...string) http.Handler {

View File

@@ -3,11 +3,80 @@ package cors
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAddAllowHeaders(t *testing.T) {
tests := []struct {
name string
initial string
headers []string
expected string
}{
{
name: "single header",
initial: "",
headers: []string{"Content-Type"},
expected: "Content-Type",
},
{
name: "multiple headers",
initial: "",
headers: []string{"Content-Type", "Authorization", "X-Requested-With"},
expected: "Content-Type, Authorization, X-Requested-With",
},
{
name: "add to existing headers",
initial: "Origin, Accept",
headers: []string{"Content-Type"},
expected: "Origin, Accept, Content-Type",
},
{
name: "no headers",
initial: "",
headers: []string{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header := http.Header{}
headers := make(map[string]struct{})
if tt.initial != "" {
header.Set(allowHeaders, tt.initial)
vals := strings.Split(tt.initial, ", ")
for _, v := range vals {
headers[v] = struct{}{}
}
}
for _, h := range tt.headers {
headers[h] = struct{}{}
}
AddAllowHeaders(header, tt.headers...)
var actual []string
vals := header.Values(allowHeaders)
for _, v := range vals {
bunch := strings.Split(v, ", ")
for _, b := range bunch {
if len(b) > 0 {
actual = append(actual, b)
}
}
}
var expect []string
for k := range headers {
expect = append(expect, k)
}
assert.ElementsMatch(t, expect, actual)
})
}
}
func TestCorsHandlerWithOrigins(t *testing.T) { func TestCorsHandlerWithOrigins(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -0,0 +1,79 @@
package fileserver
import (
"net/http"
"strings"
"sync"
)
// Middleware returns a middleware that serves files from the given file system.
func Middleware(path string, fs http.FileSystem) func(http.HandlerFunc) http.HandlerFunc {
fileServer := http.FileServer(fs)
pathWithoutTrailSlash := ensureNoTrailingSlash(path)
canServe := createServeChecker(path, fs)
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if canServe(r) {
r.URL.Path = r.URL.Path[len(pathWithoutTrailSlash):]
fileServer.ServeHTTP(w, r)
} else {
next(w, r)
}
}
}
}
func createFileChecker(fs http.FileSystem) func(string) bool {
var lock sync.RWMutex
fileChecker := make(map[string]bool)
return func(path string) bool {
lock.RLock()
exist, ok := fileChecker[path]
lock.RUnlock()
if ok {
return exist
}
lock.Lock()
defer lock.Unlock()
file, err := fs.Open(path)
exist = err == nil
fileChecker[path] = exist
if err != nil {
return false
}
_ = file.Close()
return true
}
}
func createServeChecker(path string, fs http.FileSystem) func(r *http.Request) bool {
pathWithTrailSlash := ensureTrailingSlash(path)
fileChecker := createFileChecker(fs)
return func(r *http.Request) bool {
return r.Method == http.MethodGet &&
strings.HasPrefix(r.URL.Path, pathWithTrailSlash) &&
fileChecker(r.URL.Path[len(pathWithTrailSlash):])
}
}
func ensureTrailingSlash(path string) string {
if strings.HasSuffix(path, "/") {
return path
}
return path + "/"
}
func ensureNoTrailingSlash(path string) string {
if strings.HasSuffix(path, "/") {
return path[:len(path)-1]
}
return path
}

View File

@@ -0,0 +1,122 @@
package fileserver
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMiddleware(t *testing.T) {
tests := []struct {
name string
path string
dir string
requestPath string
expectedStatus int
expectedContent string
}{
{
name: "Serve static file",
path: "/static/",
dir: "./testdata",
requestPath: "/static/example.txt",
expectedStatus: http.StatusOK,
expectedContent: "1",
},
{
name: "Pass through non-matching path",
path: "/static/",
dir: "./testdata",
requestPath: "/other/path",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "Directory with trailing slash",
path: "/assets",
dir: "testdata",
requestPath: "/assets/sample.txt",
expectedStatus: http.StatusOK,
expectedContent: "2",
},
{
name: "Not exist file",
path: "/assets",
dir: "testdata",
requestPath: "/assets/not-exist.txt",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "Not exist file in root",
path: "/",
dir: "testdata",
requestPath: "/not-exist.txt",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "websocket request",
path: "/",
dir: "testdata",
requestPath: "/ws",
expectedStatus: http.StatusAlreadyReported,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
middleware := Middleware(tt.path, http.Dir(tt.dir))
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAlreadyReported)
})
handlerToTest := middleware(nextHandler)
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
rr := httptest.NewRecorder()
handlerToTest.ServeHTTP(rr, req)
assert.Equal(t, tt.expectedStatus, rr.Code)
if len(tt.expectedContent) > 0 {
assert.Equal(t, tt.expectedContent, rr.Body.String())
}
}
})
}
}
func TestEnsureTrailingSlash(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"path", "path/"},
{"path/", "path/"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := ensureTrailingSlash(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestEnsureNoTrailingSlash(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"path", "path"},
{"path/", "path"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := ensureNoTrailingSlash(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1 @@
2

View File

@@ -137,7 +137,7 @@ func TestPatRouter(t *testing.T) {
} }
func TestParseSlice(t *testing.T) { func TestParseSlice(t *testing.T) {
body := `names=%5B%22first%22%2C%22second%22%5D` body := `names=first&names=second`
reader := strings.NewReader(body) reader := strings.NewReader(body)
r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader) r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
assert.Nil(t, err) assert.Nil(t, err)
@@ -388,7 +388,7 @@ func TestParseQueryRequired(t *testing.T) {
} }
func TestParseOptional(t *testing.T) { func TestParseOptional(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil) r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever", nil)
assert.Nil(t, err) assert.Nil(t, err)
router := NewRouter() router := NewRouter()

View File

@@ -13,6 +13,7 @@ import (
"github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/rest/internal" "github.com/zeromicro/go-zero/rest/internal"
"github.com/zeromicro/go-zero/rest/internal/cors" "github.com/zeromicro/go-zero/rest/internal/cors"
"github.com/zeromicro/go-zero/rest/internal/fileserver"
"github.com/zeromicro/go-zero/rest/router" "github.com/zeromicro/go-zero/rest/router"
) )
@@ -85,7 +86,7 @@ func (s *Server) PrintRoutes() {
// Routes returns the HTTP routers that registered in the server. // Routes returns the HTTP routers that registered in the server.
func (s *Server) Routes() []Route { func (s *Server) Routes() []Route {
var routes []Route routes := make([]Route, 0, len(s.ngin.routes))
for _, r := range s.ngin.routes { for _, r := range s.ngin.routes {
routes = append(routes, r.routes...) routes = append(routes, r.routes...)
@@ -160,6 +161,18 @@ func WithCors(origin ...string) RunOption {
} }
} }
// WithCorsHeaders returns a RunOption to enable CORS with given headers.
func WithCorsHeaders(headers ...string) RunOption {
const allDomains = "*"
return func(server *Server) {
server.router.SetNotAllowedHandler(cors.NotAllowedHandler(nil, allDomains))
server.router = newCorsRouter(server.router, func(header http.Header) {
cors.AddAllowHeaders(header, headers...)
}, allDomains)
}
}
// WithCustomCors returns a func to enable CORS for given origin, or default to all origins (*), // WithCustomCors returns a func to enable CORS for given origin, or default to all origins (*),
// fn lets caller customizing the response. // fn lets caller customizing the response.
func WithCustomCors(middlewareFn func(header http.Header), notAllowedFn func(http.ResponseWriter), func WithCustomCors(middlewareFn func(header http.Header), notAllowedFn func(http.ResponseWriter),
@@ -170,6 +183,13 @@ func WithCustomCors(middlewareFn func(header http.Header), notAllowedFn func(htt
} }
} }
// WithFileServer returns a RunOption to serve files from given dir with given path.
func WithFileServer(path string, fs http.FileSystem) RunOption {
return func(server *Server) {
server.router = newFileServingRouter(server.router, path, fs)
}
}
// WithJwt returns a func to enable jwt authentication in given route. // WithJwt returns a func to enable jwt authentication in given route.
func WithJwt(secret string) RouteOption { func WithJwt(secret string) RouteOption {
return func(r *featuredRoutes) { return func(r *featuredRoutes) {
@@ -241,7 +261,7 @@ func WithNotAllowedHandler(handler http.Handler) RunOption {
// WithPrefix adds group as a prefix to the route paths. // WithPrefix adds group as a prefix to the route paths.
func WithPrefix(group string) RouteOption { func WithPrefix(group string) RouteOption {
return func(r *featuredRoutes) { return func(r *featuredRoutes) {
var routes []Route routes := make([]Route, 0, len(r.routes))
for _, rt := range r.routes { for _, rt := range r.routes {
p := path.Join(group, rt.Path) p := path.Join(group, rt.Path)
routes = append(routes, Route{ routes = append(routes, Route{
@@ -337,3 +357,19 @@ func newCorsRouter(router httpx.Router, headerFn func(http.Header), origins ...s
func (c *corsRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (c *corsRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.middleware(c.Router.ServeHTTP)(w, r) c.middleware(c.Router.ServeHTTP)(w, r)
} }
type fileServingRouter struct {
httpx.Router
middleware Middleware
}
func newFileServingRouter(router httpx.Router, path string, fs http.FileSystem) httpx.Router {
return &fileServingRouter{
Router: router,
middleware: fileserver.Middleware(path, fs),
}
}
func (f *fileServingRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.middleware(f.Router.ServeHTTP)(w, r)
}

View File

@@ -2,8 +2,10 @@ package rest
import ( import (
"crypto/tls" "crypto/tls"
"embed"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@@ -21,6 +23,11 @@ import (
"github.com/zeromicro/go-zero/rest/router" "github.com/zeromicro/go-zero/rest/router"
) )
const (
exampleContent = "example content"
sampleContent = "sample content"
)
func TestNewServer(t *testing.T) { func TestNewServer(t *testing.T) {
logtest.Discard(t) logtest.Discard(t)
@@ -184,6 +191,56 @@ func TestWithMiddleware(t *testing.T) {
}, m) }, m)
} }
func TestWithFileServerMiddleware(t *testing.T) {
tests := []struct {
name string
path string
dir string
requestPath string
expectedStatus int
expectedContent string
}{
{
name: "Serve static file",
path: "/assets/",
dir: "./testdata",
requestPath: "/assets/example.txt",
expectedStatus: http.StatusOK,
expectedContent: exampleContent,
},
{
name: "Pass through non-matching path",
path: "/static/",
dir: "./testdata",
requestPath: "/other/path",
expectedStatus: http.StatusNotFound,
},
{
name: "Directory with trailing slash",
path: "/static",
dir: "testdata",
requestPath: "/static/sample.txt",
expectedStatus: http.StatusOK,
expectedContent: sampleContent,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := MustNewServer(RestConf{}, WithFileServer(tt.path, http.Dir(tt.dir)))
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
assert.Equal(t, tt.expectedStatus, rr.Code)
if len(tt.expectedContent) > 0 {
assert.Equal(t, tt.expectedContent, rr.Body.String())
}
})
}
}
func TestMultiMiddlewares(t *testing.T) { func TestMultiMiddlewares(t *testing.T) {
m := make(map[string]string) m := make(map[string]string)
rt := router.NewRouter() rt := router.NewRouter()
@@ -363,6 +420,64 @@ Port: 54321
opt(svr) opt(svr)
} }
func TestWithCorsHeaders(t *testing.T) {
tests := []struct {
name string
headers []string
}{
{
name: "single header",
headers: []string{"UserHeader"},
},
{
name: "multiple headers",
headers: []string{"UserHeader", "X-Requested-With"},
},
{
name: "no headers",
headers: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
const configYaml = `
Name: foo
Port: 54321
`
var cnf RestConf
assert.Nil(t, conf.LoadFromYamlBytes([]byte(configYaml), &cnf))
rt := router.NewRouter()
svr, err := NewServer(cnf, WithRouter(rt))
assert.Nil(t, err)
defer svr.Stop()
option := WithCorsHeaders(tt.headers...)
option(svr)
// Assuming newCorsRouter sets headers correctly,
// we would need to verify the behavior here. Since we don't have
// direct access to headers, we'll mock newCorsRouter to capture it.
w := httptest.NewRecorder()
svr.ServeHTTP(w, httptest.NewRequest(http.MethodOptions, "/", nil))
vals := w.Header().Values("Access-Control-Allow-Headers")
respHeaders := make(map[string]struct{})
for _, header := range vals {
headers := strings.Split(header, ", ")
for _, h := range headers {
if len(h) > 0 {
respHeaders[h] = struct{}{}
}
}
}
for _, h := range tt.headers {
_, ok := respHeaders[h]
assert.Truef(t, ok, "expected header %s not found", h)
}
})
}
}
func TestServer_PrintRoutes(t *testing.T) { func TestServer_PrintRoutes(t *testing.T) {
const ( const (
configYaml = ` configYaml = `
@@ -638,3 +753,18 @@ Port: 54321
}) })
} }
} }
//go:embed testdata
var content embed.FS
func TestServerEmbedFileSystem(t *testing.T) {
filesys, err := fs.Sub(content, "testdata")
assert.NoError(t, err)
server := MustNewServer(RestConf{}, WithFileServer("/assets", http.FS(filesys)))
req, err := http.NewRequest(http.MethodGet, "/assets/sample.txt", http.NoBody)
assert.Nil(t, err)
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
assert.Equal(t, sampleContent, rr.Body.String())
}

1
rest/testdata/example.txt vendored Normal file
View File

@@ -0,0 +1 @@
example content

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