Compare commits

...

440 Commits

Author SHA1 Message Date
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
Kevin Wan
3343fc2cdb chore: update goctl version to 1.6.4 (#4057) 2024-04-09 22:59:33 +08:00
Kevin Wan
3866b5741a feat: support http stream response (#4055) 2024-04-09 20:46:44 +08:00
Kevin Wan
5fbe8ff5c4 chore: coding style (#4054) 2024-04-09 17:19:47 +08:00
jaron
6f763f71f9 chore(goctl): update readme (#4053) 2024-04-09 08:30:25 +00:00
dependabot[bot]
80377f18e7 chore(deps): bump codecov/codecov-action from 3 to 4 (#4051)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 15:24:24 +08:00
dependabot[bot]
8690859c7d chore(deps): bump golang.org/x/net from 0.23.0 to 0.24.0 (#4048)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 10:03:06 +08:00
dependabot[bot]
d744038198 chore(deps): bump google.golang.org/grpc from 1.62.1 to 1.63.0 (#4045)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 19:43:17 +08:00
dependabot[bot]
58ad8cac8a chore(deps): bump golang.org/x/sys from 0.18.0 to 0.19.0 (#4046)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 19:28:54 +08:00
dependabot[bot]
74886a151e chore(deps): bump google.golang.org/grpc from 1.62.1 to 1.63.0 in /tools/goctl (#4047)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 18:45:49 +08:00
Kevin Wan
c5eda1f155 chore: fix codecov (#4044) 2024-04-05 00:53:13 +08:00
Kevin Wan
b5b7c054ca chore: fix codecov (#4043) 2024-04-05 00:43:38 +08:00
Kevin Wan
6c8073b691 chore: add more tests (#4042) 2024-04-05 00:13:42 +08:00
Kevin Wan
64d430d424 fix: bug on form data with slices (#4040) 2024-04-04 20:28:54 +08:00
Jayson Wang
f138cc792e fix(goctl): multi imports the api cause redeclared error in types.go (#3988)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-04-04 11:39:24 +00:00
dependabot[bot]
b20ec8aedb chore(deps): bump golang.org/x/net from 0.22.0 to 0.23.0 (#4039)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 10:48:52 +08:00
Kevin Wan
a53254fa91 chore: update codecov config (#4038) 2024-04-03 23:58:02 +08:00
Kevin Wan
08563482e5 chore: coding style (#4037) 2024-04-03 22:55:52 +08:00
fearlessfei
968727412d add custom health response information (#4034)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-04-03 14:33:55 +00:00
linden-in-China
6f3d094eba opton to option (#4035) 2024-04-03 14:15:21 +00:00
kesonan
2d3ebb9b62 (goctl) fix #4027 (#4032) 2024-04-01 15:22:29 +00:00
chentong
8c0bb27136 feat: add gen api @doc comment to logic handler routes (#3790)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-03-30 11:09:54 +00:00
ak5w
cf987295df fix the usage datasource url of postgresql (#4029) (#4030) 2024-03-30 05:51:54 +00:00
dependabot[bot]
8c92b3af7d chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.12 to 3.5.13 (#4028)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 13:40:24 +08:00
Kevin Wan
5dd9342703 chore: fix test failure (#4031) 2024-03-30 13:29:58 +08:00
shyandsy
3ef59f6a71 fix(httpx): support array field for request dto (#4026)
Co-authored-by: yshi3 <yshi3@tesla.com>
2024-03-30 12:10:56 +08:00
dependabot[bot]
f12802abc7 chore(deps): bump github.com/go-sql-driver/mysql from 1.8.0 to 1.8.1 (#4022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 18:42:35 +08:00
dependabot[bot]
6f0fe67804 chore(deps): bump github.com/go-sql-driver/mysql from 1.8.0 to 1.8.1 in /tools/goctl (#4023)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 18:07:00 +08:00
dependabot[bot]
f44f0e7e62 chore(deps): bump github.com/pelletier/go-toml/v2 from 2.1.1 to 2.2.0 (#4017)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 23:34:06 +08:00
dependabot[bot]
cdd95296db chore(deps): bump k8s.io/client-go from 0.29.2 to 0.29.3 (#4012)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 15:41:20 +08:00
kesonan
3e794cf991 (goctl)fix code_ql (#4009) 2024-03-17 02:21:36 +00:00
Kevin Wan
bbce95e7e1 fix: didn't count failure in allow method with breaker algorithm (#4008) 2024-03-16 22:19:36 +08:00
dependabot[bot]
0449450c64 chore(deps): bump github.com/jackc/pgx/v5 from 5.5.3 to 5.5.4 in /tools/goctl (#4007)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 12:55:57 +08:00
dependabot[bot]
9f9a12ea57 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.31.1 to 2.32.1 (#4003)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 11:19:44 +08:00
Kevin Wan
cc2a7e97f9 chore: coding style, add code for prometheus (#4002) 2024-03-13 20:00:35 +08:00
dependabot[bot]
09d7af76af chore(deps): bump github.com/go-sql-driver/mysql from 1.7.1 to 1.8.0 in /tools/goctl (#3997)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-13 13:28:27 +08:00
dependabot[bot]
c233a66601 chore(deps): bump github.com/go-sql-driver/mysql from 1.7.1 to 1.8.0 (#3998)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-13 12:56:41 +08:00
dependabot[bot]
94fa12560c chore(deps): bump github.com/jackc/pgx/v5 from 5.5.4 to 5.5.5 (#3999)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 12:28:28 +08:00
MarkJoyMa
7d90f906f5 feat: migrate redis breaker into hook (#3982) 2024-03-12 04:21:33 +00:00
Viktor Patchev
f372b98d96 Add: Optimize the error log to be more specific (#3994) 2024-03-11 13:06:50 +08:00
mongobaba
459d3025c5 optimize: change err == xx to errors.Is(err, xx) (#3991) 2024-03-09 12:49:16 +00:00
Kevin Wan
e9e55125a9 chore: fix warnings (#3990) 2024-03-09 13:48:11 +08:00
Kevin Wan
159ecb7386 chore: fix warnings (#3989) 2024-03-08 22:35:17 +08:00
ansoda
69bb746a1d fix: StopAgent panics when trace agent disabled (#3981)
Co-authored-by: ansoda <ansoda@gmail.com>
2024-03-08 10:28:23 +00:00
Kevin Wan
d184f96b13 chore: coding style (#3987) 2024-03-08 16:11:28 +08:00
MarkJoyMa
c7dacb0146 fix: mysql WithAcceptable bug (#3986) 2024-03-08 04:23:41 +00:00
dependabot[bot]
2207477b60 chore(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 in /tools/goctl (#3978)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-07 11:15:48 +08:00
dependabot[bot]
105ab590ff chore(deps): bump google.golang.org/grpc from 1.62.0 to 1.62.1 in /tools/goctl (#3977)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-07 11:01:14 +08:00
dependabot[bot]
2f4c58ed73 chore(deps): bump google.golang.org/grpc from 1.62.0 to 1.62.1 (#3976)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-07 10:45:19 +08:00
dependabot[bot]
1631aa02ad chore(deps): bump github.com/golang/protobuf from 1.5.3 to 1.5.4 (#3984)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-07 10:24:51 +08:00
dependabot[bot]
4df10eef5d chore(deps): bump golang.org/x/net from 0.21.0 to 0.22.0 (#3975)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 23:31:02 +08:00
dependabot[bot]
3d552ea7a8 chore(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#3974)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 21:33:45 +08:00
Kevin Wan
74b87ac9fd chore: coding style (#3972) 2024-03-05 14:40:10 +08:00
Alex Last
ba1d6e3664 fix: only add log middleware to not found handler when enabled (#3969) 2024-03-05 04:14:54 +00:00
dependabot[bot]
2096cd5749 chore(deps): bump github.com/jackc/pgx/v5 from 5.5.3 to 5.5.4 (#3970)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 12:09:18 +08:00
dependabot[bot]
2eb2fa26f6 chore(deps): bump golang.org/x/sys from 0.17.0 to 0.18.0 (#3971)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 12:03:06 +08:00
Kevin Wan
bc4187ca90 Create SECURITY.md (#3968) 2024-03-04 23:07:54 +08:00
Kevin Wan
b7be25b98b Update readme-cn.md (#3966) 2024-03-03 14:18:27 +08:00
Kevin Wan
dd01695d45 chore: update goctl version to 1.6.3 (#3965) 2024-03-03 13:36:35 +08:00
Kevin Wan
25821bdee6 chore: coding style (#3960) 2024-03-03 00:17:38 +08:00
dependabot[bot]
b624b966f0 chore(deps): bump actions/stale from 8 to 9 (#3963)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-03 00:11:56 +08:00
dependabot[bot]
df96262235 chore(deps): bump codecov/codecov-action from 3 to 4 (#3962)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-03 00:06:41 +08:00
dependabot[bot]
2629636f64 chore(deps): bump actions/setup-go from 4 to 5 (#3964)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-03 00:01:14 +08:00
dependabot[bot]
708ad207d7 chore(deps): bump github/codeql-action from 2 to 3 (#3961)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 23:54:37 +08:00
Rene Leonhardt
b53ba76a99 feat: Improve Docker build (#3682) 2024-03-02 15:40:31 +00:00
POABOB
be7f93924a feature: add a mongo registry option to convert type easier. (#3780)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-03-02 15:13:44 +00:00
Kevin Wan
45be48a4ee chore: coding style (#3959) 2024-03-02 22:45:24 +08:00
kesonan
e08ba2fee8 (goctl)fix parser issues (#3930) 2024-03-02 14:27:39 +00:00
Kevin Wan
a5d2b971a1 chore: add more tests (#3958) 2024-03-02 21:58:13 +08:00
Qiu shao
9763c8b143 feat:add redis mset func (#3820) 2024-03-02 12:00:25 +00:00
Kevin Wan
4e3f1776dc chore: coding style (#3957) 2024-03-02 19:09:14 +08:00
fearlessfei
e38036cea2 feat: retry ignore specified errors (#3808) 2024-03-02 18:53:20 +08:00
Kevin Wan
8e97c5819f chore: add more tests (#3954) 2024-03-02 12:22:55 +08:00
#Suyghur
0ee44c7064 feat(redis): added and impl ZADDNX command (#3944) 2024-03-02 10:15:10 +08:00
Kevin Wan
a1bacd3fc8 feat: a concurrent runner with messages taken in pushing order (#3941) 2024-03-02 10:03:58 +08:00
Kevin Wan
c98d5fdaf4 chore: simplify linux nocgroup logic (#3953) 2024-03-02 07:13:41 +08:00
dependabot[bot]
2ee43b41b8 chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 in /tools/goctl (#3952)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 06:50:33 +08:00
dependabot[bot]
8367af3416 chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#3951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 06:42:25 +08:00
Kevin Wan
03b6e377d7 chore: add lock for batcherror (#3950) 2024-03-02 00:59:15 +08:00
chentong
ec41880476 fix: BatchError.Add() non thread safe (#3946) 2024-03-01 16:32:39 +00:00
Kevin Wan
5263805b3b chore: simplify linux nocgroup logic (#3949) 2024-03-02 00:23:52 +08:00
Alex Last
a7363f0c21 feat: add nocgroup build tag for systems without cgroup (#3948) 2024-03-01 15:52:20 +00:00
mongobaba
52e5d85221 feat: add break metrics for sqlx.statement (#3947) 2024-03-01 14:55:32 +00:00
MarkJoyMa
88aab8f635 fix: mapping FillDefault is optional! bug (#3940) 2024-02-27 16:23:47 +00:00
Kevin Wan
1f63cbe9c6 Update readme-cn.md (#3939) 2024-02-26 17:24:52 +08:00
Kevin Wan
0dfaf135dd feat: support breaker with sql statements (#3936) 2024-02-25 11:24:44 +08:00
Kevin Wan
914bcdcf2b chore: add tests (#3931) 2024-02-23 23:00:35 +08:00
fffreedom
e38cb0118d when the Unmarshaler parsing value by fillSliceFromString, if the val… (#3927)
Co-authored-by: danahan <danahan@tencent.com>
2024-02-23 14:00:58 +00:00
dependabot[bot]
cb8161c799 chore(deps): bump google.golang.org/grpc from 1.61.1 to 1.62.0 in /tools/goctl (#3929) 2024-02-23 10:26:41 +08:00
dependabot[bot]
c4dac2095f chore(deps): bump google.golang.org/grpc from 1.61.1 to 1.62.0 (#3928) 2024-02-23 03:34:23 +08:00
Kevin Wan
25a807afb2 chore: add tests (#3921) 2024-02-20 10:11:43 +08:00
Kevin Wan
6be37ad533 chore: optimize coding style and add unit tests (#3917) 2024-02-17 15:50:07 +08:00
chen quan
28cb2c5804 feat: support sse ignore timeout (#2041)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2024-02-17 07:06:45 +00:00
Kevin Wan
0f1d4c6bca optimize: improve performance on log disabled (#3916) 2024-02-17 14:55:48 +08:00
dependabot[bot]
bfe8335cb2 chore(deps): bump k8s.io/client-go from 0.29.1 to 0.29.2 (#3913)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 10:43:40 +08:00
dependabot[bot]
3c10ce0115 chore(deps): bump k8s.io/apimachinery from 0.29.1 to 0.29.2 (#3912)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 10:33:54 +08:00
Kevin Wan
1303e0fe6f feat: optimize circuit breaker algorithm (#3897) 2024-02-15 20:29:24 +08:00
Kevin Wan
9c17499757 optimize: shedding algorithm performance (#3908) 2024-02-15 20:22:22 +08:00
dependabot[bot]
8ceb2885db chore(deps): bump google.golang.org/grpc from 1.61.0 to 1.61.1 in /tools/goctl (#3911)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 20:09:38 +08:00
dependabot[bot]
00944894b4 chore(deps): bump google.golang.org/grpc from 1.61.0 to 1.61.1 (#3910)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 17:31:10 +08:00
dependabot[bot]
609fb3d59e chore(deps): bump golang.org/x/net from 0.20.0 to 0.21.0 (#3907)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-10 11:58:12 +08:00
dependabot[bot]
01c330abe7 chore(deps): bump golang.org/x/sys from 0.16.0 to 0.17.0 (#3902)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-08 22:37:11 +08:00
Kevin Wan
2ccef5bb4f feat: support ScheduleImmediately in TaskRunner (#3896) 2024-02-06 06:26:22 +00:00
dependabot[bot]
10f1d93e2a chore(deps): bump github.com/jackc/pgx/v5 from 5.5.2 to 5.5.3 (#3895) 2024-02-06 09:42:05 +08:00
Kevin Wan
dd518c8eac chore: update goctl version to 1.6.2 (#3890) 2024-02-03 21:31:11 +08:00
Kevin Wan
97cf2421de chore: add more tests (#3888) 2024-02-03 20:33:20 +08:00
Kevin Wan
786a80131e chore: fix test failure (#3883) 2024-02-01 22:41:05 +08:00
dependabot[bot]
93d257f9f5 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.11 to 3.5.12 (#3887)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 10:34:46 +08:00
dependabot[bot]
f79535057f chore(deps): bump github.com/jhump/protoreflect from 1.15.5 to 1.15.6 (#3886)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 10:37:30 +08:00
dependabot[bot]
a905f4c20c chore(deps): bump github.com/emicklei/proto from 1.13.0 to 1.13.2 in /tools/goctl (#3882)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 15:02:02 +08:00
kesonan
3331954a78 (goctl): fix unresolved type if linked api imported (#3881) 2024-01-29 13:05:08 +00:00
dependabot[bot]
f54c2e384f chore(deps): bump google.golang.org/grpc from 1.60.1 to 1.61.0 (#3879)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-27 19:15:05 +08:00
dependabot[bot]
4b83f2ebd0 chore(deps): bump k8s.io/client-go from 0.29.0 to 0.29.1 (#3861)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 21:42:55 +08:00
dependabot[bot]
1c572ee16b chore(deps): bump github.com/jhump/protoreflect from 1.15.4 to 1.15.5 (#3875)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 13:44:15 +08:00
dependabot[bot]
b3402430e8 chore(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 (#3876)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 13:02:09 +08:00
dependabot[bot]
076f5de7d9 chore(deps): bump google.golang.org/grpc from 1.60.1 to 1.61.0 in /tools/goctl (#3870)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-25 22:57:17 +08:00
dependabot[bot]
303a74559a chore(deps): bump k8s.io/apimachinery from 0.29.0 to 0.29.1 (#3864)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-25 22:14:22 +08:00
Kevin Wan
c08e741d7a fix: cpu stat in cgroup v2 (#3857) 2024-01-17 23:35:42 +08:00
dependabot[bot]
06d2c07fce chore(deps): bump github.com/jackc/pgx/v5 from 5.5.1 to 5.5.2 (#3850)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-17 22:55:53 +08:00
Kevin Wan
b6f00a5789 Update readme-cn.md (#3849) 2024-01-15 18:42:28 +08:00
MarkJoyMa
dace520654 fix: revert sqlx metric namespace (#3847) 2024-01-14 02:38:10 +00:00
Kevin Wan
44d347d48a fix: issue #3840 (#3846) 2024-01-14 09:37:01 +08:00
Kevin Wan
408827d876 fix: issue 3840 (#3845) 2024-01-13 23:48:50 +08:00
Kevin Wan
9e33b557b1 chore: refactor redis (#3844) 2024-01-13 23:02:19 +08:00
Kevin Wan
368caa7608 feat: upgrade go-redis to v9 (#3088)
Co-authored-by: cong <zhangcong1992@gmail.com>
2024-01-13 22:40:58 +08:00
Kevin Wan
7822a4c1cb chore: refactor mapping errors (#3843) 2024-01-13 22:11:19 +08:00
Remember
0441f84606 fix(mapping): call fillSliceValue panic if the value is nil (#3839) 2024-01-13 13:48:43 +00:00
Kevin Wan
81d72b5010 chore: make cpu usage more smooth (#3842) 2024-01-13 19:36:25 +08:00
kesonan
7ba8adfc74 fix(goctl)/new parser (#3834)
Co-authored-by: keson <keson@kesondeMacBook-Pro.local>
2024-01-11 15:50:53 +00:00
dependabot[bot]
ffd2a78623 chore(deps): bump github.com/DATA-DOG/go-sqlmock from 1.5.1 to 1.5.2 (#3830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 11:42:47 +08:00
Kevin Wan
1b9b3cada7 Update readme-cn.md (#3836) 2024-01-10 20:08:48 +08:00
dependabot[bot]
38c8f9cf21 chore(deps): bump golang.org/x/net from 0.19.0 to 0.20.0 (#3831)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 23:37:04 +08:00
dependabot[bot]
54dbb05bb9 chore(deps): bump github.com/DATA-DOG/go-sqlmock from 1.5.1 to 1.5.2 in /tools/goctl (#3832)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 21:45:06 +08:00
kesonan
9a671f6059 fix #3825 (#3828) 2024-01-06 14:45:46 +00:00
dependabot[bot]
80aab0b3f8 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.31.0 to 2.31.1 (#3826)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-06 21:10:12 +08:00
dependabot[bot]
2d0286646f chore(deps): bump golang.org/x/sys from 0.15.0 to 0.16.0 (#3829)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-06 21:01:00 +08:00
dependabot[bot]
d012fe97b1 chore(deps): bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 (#3822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-30 10:25:20 +08:00
dependabot[bot]
7ca13bc25e chore(deps): bump google.golang.org/protobuf from 1.31.1-0.20231027082548-f4a6c1f6e5c1 to 1.32.0 (#3809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 23:12:33 +08:00
dependabot[bot]
9c20f10743 chore(deps): bump google.golang.org/protobuf from 1.31.1-0.20231027082548-f4a6c1f6e5c1 to 1.32.0 in /tools/goctl (#3810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-23 22:57:37 +08:00
Kimjin-gd
6ec38ec056 fix: negative float32 overflow when unmarshalling (#3811)
Co-authored-by: kim1.jin <kim1.jin@bkyo.io>
2023-12-23 14:47:11 +00:00
dependabot[bot]
28c742a1e1 chore(deps): bump k8s.io/client-go from 0.28.4 to 0.29.0 (#3800)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-22 23:46:19 +08:00
dependabot[bot]
b3b6cfe947 chore(deps): bump github.com/pelletier/go-toml/v2 from 2.1.0 to 2.1.1 (#3801)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 19:18:55 +08:00
dependabot[bot]
a8ef7b51eb chore(deps): bump k8s.io/apimachinery from 0.28.4 to 0.29.0 (#3802)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 18:59:55 +08:00
dependabot[bot]
124968114a chore(deps): bump google.golang.org/grpc from 1.60.0 to 1.60.1 (#3806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 18:21:21 +08:00
dependabot[bot]
04ed821b65 chore(deps): bump google.golang.org/grpc from 1.60.0 to 1.60.1 in /tools/goctl (#3807)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 17:47:30 +08:00
dependabot[bot]
15599ac0a0 chore(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#3799) 2023-12-20 00:48:09 +08:00
dependabot[bot]
0cf6971664 chore(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#3803) 2023-12-20 00:35:43 +08:00
dependabot[bot]
47c4f2831c chore(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /tools/goctl (#3804)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 23:44:50 +08:00
Kevin Wan
2b18dd1764 chore: update goctl deps (#3797) 2023-12-17 14:06:32 +08:00
Kevin Wan
27c4908342 chore: coding style (#3796) 2023-12-17 13:44:55 +08:00
李登富
48625fa381 fix endless loop caused by ErrCompacted (#3774)
Co-authored-by: lidengfu <lidengfu@excean.com>
2023-12-17 05:28:19 +00:00
Kevin Wan
83a776a190 chore: upgrade otel, removed ut temporarily because of otel API changes (#3795) 2023-12-17 12:26:48 +08:00
Qiu shao
431f9af43e feat:add redis ExistsMany method (#3769) 2023-12-16 14:49:16 +08:00
gongluck
8c2f4c1899 Fixed #3771 (#3788) 2023-12-16 06:37:35 +00:00
Alex Last
919477ffe4 fix(servicegroup): use logx for shutdown message (#3719) 2023-12-16 06:25:02 +00:00
Summer-lights
400386459c fix(redis): redis ttl -1 and -2 (#3783) 2023-12-16 05:46:53 +00:00
dependabot[bot]
ebe0801d2f chore(deps): bump github.com/emicklei/proto from 1.12.2 to 1.13.0 in /tools/goctl (#3782)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 13:39:59 +08:00
dependabot[bot]
b76d85f204 chore(deps): bump google.golang.org/grpc from 1.59.0 to 1.60.0 in /tools/goctl (#3787)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 13:25:31 +08:00
dependabot[bot]
28ba57afb3 chore(deps): bump github.com/jhump/protoreflect from 1.15.3 to 1.15.4 (#3794)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 13:18:36 +08:00
dependabot[bot]
d6873047ce chore(deps): bump google.golang.org/grpc from 1.59.0 to 1.60.0 (#3791) 2023-12-16 00:44:30 +08:00
dependabot[bot]
54c0f2e5cf chore(deps): bump github.com/DATA-DOG/go-sqlmock from 1.5.0 to 1.5.1 in /tools/goctl (#3781) 2023-12-15 12:13:12 +08:00
dependabot[bot]
7795231cc6 chore(deps): bump github.com/DATA-DOG/go-sqlmock from 1.5.0 to 1.5.1 (#3779)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 00:24:28 +08:00
zzZZzzz888
4835e4fe51 fix: coredump: goctl model mysql ddl --src user_base.sql --dir . area… (#3777)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-12-12 16:09:15 +00:00
dependabot[bot]
daef970091 chore(deps): bump github.com/jackc/pgx/v5 from 5.5.0 to 5.5.1 (#3778)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 23:53:29 +08:00
guangwu
05020a92e8 fix: primary key unique key simultaneously exist cacheIdPrefix duplicate (#3763)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-12-11 23:13:35 +08:00
POABOB
a1bbac3c6c fix: prevent a crash if there is a unique key constraint with a nil field. (#3770) 2023-12-11 13:29:05 +00:00
Kevin Wan
22c98beb24 feat: add dbtest to facility db test (#3768) 2023-12-09 22:52:06 +08:00
dependabot[bot]
8fd710d5e7 chore(deps): bump go.mongodb.org/mongo-driver from 1.13.0 to 1.13.1 (#3767) 2023-12-09 18:47:08 +08:00
dependabot[bot]
91a735ae47 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.10 to 3.5.11 (#3764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-08 17:01:12 +08:00
Kevin Wan
39c662eece Update readme-cn.md (#3755) 2023-12-04 21:02:30 +08:00
kesonan
5e63002cf8 (goctl:) fix circle import in case new parser (#3750)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-11-29 11:13:39 +00:00
dependabot[bot]
c46bcf7e1b chore(deps): bump golang.org/x/net from 0.18.0 to 0.19.0 (#3749)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 19:00:37 +08:00
dependabot[bot]
3c65bdbb66 chore(deps): bump golang.org/x/sys from 0.14.0 to 0.15.0 (#3747)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 15:37:52 +08:00
null
5630bce286 Fix incorrect description in documentation (#3745) 2023-11-28 06:58:08 +00:00
dependabot[bot]
75524da21e chore(deps): bump golang.org/x/time from 0.4.0 to 0.5.0 (#3746)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 14:15:58 +08:00
Kevin Wan
ede7e683fd feat: auto stop profiling after one minute (#3742) 2023-11-24 21:27:05 +08:00
Kevin Wan
eb14d1347e chore: refactor ring (#3739) 2023-11-23 23:57:26 +08:00
POABOB
c220b5d886 fix: prevent ring index overflow (#3738) 2023-11-23 15:44:33 +00:00
dependabot[bot]
5e8e21b257 chore(deps): bump k8s.io/client-go from 0.28.3 to 0.28.4 (#3737)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-23 23:13:16 +08:00
dependabot[bot]
0635a4ac96 chore(deps): bump k8s.io/apimachinery from 0.28.3 to 0.28.4 (#3733)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-22 23:37:24 +08:00
MarkJoyMa
c71b753c78 fix: goctl FindOne error (#3731) 2023-11-21 03:52:51 +00:00
Kevin Wan
2f8cffc699 chore: update mongo driver (#3727) 2023-11-19 16:35:25 +08:00
Kevin Wan
9c1aa6da3d chore: refact dart code generation (#3726) 2023-11-18 22:41:26 +08:00
anstns
da67ea2300 add map type (#3704) 2023-11-18 13:55:13 +00:00
kesonan
72dd2736f5 change command-line arg 'table' from string to slice type (#3707) 2023-11-13 11:46:17 +00:00
dependabot[bot]
24695bba09 chore(deps): bump golang.org/x/net from 0.17.0 to 0.18.0 (#3709)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-10 10:55:38 +08:00
dependabot[bot]
c7c43062c5 chore(deps): bump golang.org/x/text from 0.13.0 to 0.14.0 in /tools/goctl (#3701)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 20:25:33 +08:00
dependabot[bot]
97e1ea0633 chore(deps): bump github.com/spf13/cobra from 1.7.0 to 1.8.0 in /tools/goctl (#3700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 20:11:03 +08:00
dependabot[bot]
04b9737a61 chore(deps): bump github.com/fatih/color from 1.15.0 to 1.16.0 (#3698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 19:31:41 +08:00
dependabot[bot]
b0fb246693 chore(deps): bump golang.org/x/sys from 0.13.0 to 0.14.0 (#3699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 18:03:49 +08:00
dependabot[bot]
41140ac78c chore(deps): bump golang.org/x/time from 0.3.0 to 0.4.0 (#3697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 17:44:25 +08:00
dependabot[bot]
1281904572 chore(deps): bump github.com/jackc/pgx/v5 from 5.4.3 to 5.5.0 (#3696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 11:32:10 +08:00
kesonan
c8a8ff7cad Feat/default new api parser (#3683) 2023-11-04 14:48:44 +00:00
zhaolei
df2799fff1 fix import error if generate multiple proto (#3694) 2023-11-04 12:42:25 +00:00
dependabot[bot]
fd8ee0b851 chore(deps): bump github.com/emicklei/proto from 1.12.1 to 1.12.2 in /tools/goctl (#3690)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 10:24:21 +08:00
Ash V
6ecc5e7b73 Enhanced the CODE_OF_CONDUCT Guidelines (#3680) 2023-10-29 07:07:02 +00:00
Kevin Wan
52963c2ebf chore: update go-zero version to v1.6.0 in goctl (#3679) 2023-10-28 21:42:16 +08:00
Kevin Wan
07e3e14c0e chore: remove go build version in fuzz test (#3678) 2023-10-28 20:53:03 +08:00
Rene Leonhardt
34c5f6616c chore: upgrade go to 1.19 (#3677) 2023-10-28 12:12:04 +00:00
MarkJoyMa
32600f2619 fix: adjust log encode output mode (#3676) 2023-10-28 11:46:52 +00:00
dependabot[bot]
b07df1c344 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.9 to 3.5.10 (#3675)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-28 13:16:25 +08:00
Kevin Wan
a1fca3a1da chore: upgrade go dependencies (#3657) 2023-10-28 00:19:22 +08:00
Kevin Wan
9394e59597 chore: update goctl version to 1.6.0 (#3674) 2023-10-27 21:59:35 +08:00
dependabot[bot]
f8adc71529 chore(deps): bump github.com/google/uuid from 1.3.1 to 1.4.0 (#3673)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 21:34:27 +08:00
MarkJoyMa
c05e03bb5a feat: add metrics (#3624) 2023-10-26 15:51:28 +00:00
Kevin Wan
199e86050e chore: simplify prometheus check (#3672) 2023-10-26 20:32:13 +08:00
#Suyghur
1e2a12b3d6 feat(metric): added Dec() and Sub() in GaugeVec interface (#3666) 2023-10-26 20:13:42 +08:00
Kevin Wan
922efbfc2d chore: refactor zrpc timeout (#3671) 2023-10-26 08:55:26 +08:00
vankillua
842c4d81cc feat: support the specified timeout of rpc methods (#2742)
Co-authored-by: hanzijian <hanzijian@52tt.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-25 13:01:57 +00:00
dependabot[bot]
2a335c7608 chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.8 to 1.8.9 (#3668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 15:15:30 +08:00
Bhargav Shirin Nalamati
35edd6b19d fixed typo: reds to redis (#3664) 2023-10-24 02:54:15 +00:00
Kevin Wan
36bbc6a2e2 chore: add error handling on registering event handlers to k8s (#3663) 2023-10-23 21:57:09 +08:00
唐小鸭
e20ccdd011 Support for resource injection (#3383)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-23 13:22:16 +00:00
Kevin Wan
c2ff00883a chore: update restful/grpc servers shutdown stages (#3662) 2023-10-23 13:03:05 +00:00
MarkJoyMa
00db97fcc1 feat: model add withSession (#3658) 2023-10-23 04:43:05 +00:00
7134g
117c3a9069 fix: multiple files import the same api file (#3642) 2023-10-23 04:04:52 +00:00
Kevin Wan
172ff407f3 chore: refactor mongo logs (#3660) 2023-10-23 11:03:55 +08:00
shenbaise9527
a242fec5e1 feat: support for disable mon logs like sqlx (#3606)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-22 15:00:02 +00:00
Kevin Wan
6286941ebf chore: add go-zero users (#3659) 2023-10-22 22:23:24 +08:00
Kevin Wan
42e0a6f90c chore: refactor errors to use errors.Is (#3654) 2023-10-21 00:00:57 +08:00
cary
81ae7d36b5 Support for adding ignore_columns parameters to the goctl model pg (#3427) 2023-10-20 08:58:18 +00:00
Kevin Wan
944e76edb9 chore: refactor errors (#3651) 2023-10-20 14:58:38 +08:00
MarkJoyMa
151768ef82 feat: optimize logx print error (#3649) 2023-10-19 13:46:52 +00:00
Surav Shrestha
50581c7f5c docs fix typo in core/logx/readme.md (#3650) 2023-10-19 13:33:25 +00:00
dependabot[bot]
54041ef9e4 chore(deps): bump google.golang.org/grpc from 1.58.2 to 1.59.0 in /tools/goctl (#3645) 2023-10-19 12:38:08 +08:00
dependabot[bot]
5a9ae5ef02 chore(deps): bump google.golang.org/grpc from 1.58.2 to 1.59.0 (#3647) 2023-10-19 12:26:36 +08:00
guonaihong
19de13bb04 Upgrade grpc-go,fix 0day problem. (#3623) 2023-10-19 03:45:59 +00:00
Kevin Wan
3ab4e82168 chore: upgrade go to 1.19 (#3648) 2023-10-19 11:30:37 +08:00
Armaan
619e838513 updated CONTRIBUTING.md with emojified , fun, precise and engaging text (#3643) 2023-10-19 03:06:38 +00:00
kesonan
423597a01c feat: export devserver.Config (#3638) 2023-10-17 15:38:21 +00:00
kesonan
d84dfe1b20 fix: goctl unit test (#3636) 2023-10-17 11:15:32 +00:00
Soham Tembhurne
87b7a1120d Update documenation section #background (#3634) 2023-10-16 00:11:17 +00:00
Kevin Wan
528af8a99d chore: update readme for Mac install instructions (#3633) 2023-10-16 08:08:42 +08:00
Soham Tembhurne
17fc68ac5a Update readme.md (#3630) 2023-10-15 23:45:25 +08:00
Kevin Wan
804a56bd14 fix: optimize logx for less GC objects (#3627) 2023-10-15 23:37:45 +08:00
Kevin Wan
88f60d7736 chore: refactor signal sigterm and sigint (#3632) 2023-10-15 23:24:17 +08:00
#Suyghur
95b7a3d3ce feat: add the SIGINT signal in signals.go to subscribe the user input ctrl+c to exit the application operation (#3611) 2023-10-15 22:58:15 +08:00
Kevin Wan
d71c0da7b7 chore: refactor error comparison (#3629) 2023-10-15 13:41:06 +00:00
Kevin Wan
fd070fec91 feat: retry with ctx deadline (#3626) 2023-10-15 13:39:44 +00:00
Kevin Wan
4f22034342 fix: unmarshal from number to string with incorrect error message (#3625) 2023-10-15 02:06:00 +00:00
Ikko Eltociear Ashimine
b731aa38af refactor: update builder.go (#3620) 2023-10-13 07:03:15 +00:00
dependabot[bot]
bf996a1812 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.5 to 2.31.0 (#3616)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-12 11:44:55 +08:00
dependabot[bot]
af7ce65244 chore(deps): bump golang.org/x/net from 0.15.0 to 0.17.0 in /tools/goctl (#3618)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-12 11:37:06 +08:00
dependabot[bot]
952db71835 chore(deps): bump golang.org/x/net from 0.16.0 to 0.17.0 (#3612)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 17:37:14 +08:00
Kevin Wan
abd1fa96a9 fix: UpdateStmt doesn't update the statement correctly in sqlx/bulkinserter.go (#3607) 2023-10-09 21:57:26 +08:00
Kevin Wan
5aedd9c076 chore: simplify parsing numbers with overflow (#3610) 2023-10-09 13:00:09 +00:00
Kevin Wan
ff230c4b1d chore: refactor goctl api (#3605) 2023-10-07 22:58:29 +08:00
kesonan
02c95108b9 optimize: fix experimental api (#3604) 2023-10-07 19:48:41 +08:00
dependabot[bot]
1ff541afe4 chore(deps): bump golang.org/x/net from 0.15.0 to 0.16.0 (#3603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 10:50:49 +08:00
Kevin Wan
11a8cbc1e5 chore: rename noOpBreaker to nopBreaker (#3602) 2023-10-06 23:41:09 +08:00
dependabot[bot]
c063976822 chore(deps): bump golang.org/x/sys from 0.12.0 to 0.13.0 (#3601)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-06 22:04:14 +08:00
dependabot[bot]
cb707034ce chore(deps): bump github.com/jhump/protoreflect from 1.15.2 to 1.15.3 (#3600)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-05 15:26:21 +08:00
dependabot[bot]
f10db27efd chore(deps): bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 (#3594)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-28 11:38:20 +08:00
Kevin Wan
4878f90546 chore: update goctl version to 1.5.6 (#3593) 2023-09-27 22:55:50 +08:00
Kevin Wan
421e6617b1 chore: add more tests (#3592) 2023-09-27 22:33:27 +08:00
Kevin Wan
0ee7a271d3 fix: avoid float overflow in mapping.Unmarshal (#3590) 2023-09-26 13:46:34 +00:00
dependabot[bot]
af022b9655 chore(deps): bump google.golang.org/grpc from 1.58.1 to 1.58.2 in /tools/goctl (#3584) 2023-09-25 16:23:29 +08:00
dependabot[bot]
98d46261d9 chore(deps): bump google.golang.org/grpc from 1.58.1 to 1.58.2 (#3585)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-24 23:27:02 +08:00
Kevin Wan
4222fd97bc chore: add test for logging rotate size (#3587) 2023-09-24 22:28:03 +08:00
dependabot[bot]
814852f0b8 chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.7 to 1.8.8 (#3586)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-24 19:04:34 +08:00
Kevin Wan
ded2888759 fix: avoid integer overflow in mapping.Unmarshal (#3582) 2023-09-21 14:22:33 +00:00
Kevin Wan
18d66a795d chore: add more tests (#3578) 2023-09-20 23:52:10 +08:00
Kevin Wan
4211672bfd chore: add more tests (#3577) 2023-09-20 00:01:26 +08:00
Kevin Wan
68df0c3620 chore: add more tests (#3575) 2023-09-18 11:01:46 +08:00
xt-inking
5e435b6a76 fix: avoid losing logs before closing (#3573) 2023-09-17 11:38:53 +00:00
Kevin Wan
0dcede6457 chore: refactor log limit in rest (#3572) 2023-09-16 22:33:30 +08:00
Awadabang
cc21f5fae2 update: limit logBrief http body size (#3498)
Co-authored-by: 常公征 <changgz@yealink.com>
2023-09-16 11:58:21 +00:00
dependabot[bot]
b22ad50d59 chore(deps): bump google.golang.org/grpc from 1.58.0 to 1.58.1 in /tools/goctl (#3568)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-16 16:52:01 +08:00
Kevin Wan
974252980c chore: upgrade grpc (#3570) 2023-09-15 23:21:22 +08:00
dependabot[bot]
8d83986d27 chore(deps): bump google.golang.org/grpc from 1.57.0 to 1.58.0 in /tools/goctl (#3546) 2023-09-12 20:00:52 +08:00
Kevin Wan
6821b0a7dd chore: upgrade grpc (#3558) 2023-09-12 10:30:26 +08:00
Kevin Wan
1ba1724c65 chore: refactor (#3545) 2023-09-06 22:36:43 +08:00
Xinwei Xiong
ca5a7df5b0 feat: Optimize Encoding Functions and Add Descriptive Comments (#3543) 2023-09-06 14:19:50 +00:00
dependabot[bot]
69a3024853 chore(deps): bump golang.org/x/net from 0.14.0 to 0.15.0 (#3544)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 20:28:03 +08:00
dependabot[bot]
fd3abf3717 chore(deps): bump golang.org/x/text from 0.12.0 to 0.13.0 in /tools/goctl (#3542)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-09-06 20:20:12 +08:00
dependabot[bot]
99b3750d10 chore(deps): bump golang.org/x/sys from 0.11.0 to 0.12.0 (#3541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 00:02:00 +08:00
POABOB
33f6d7ebb8 fix: goctl pg gen will extract all fields when the same table name exists in different schemas (#3496) (#3517) 2023-09-04 20:48:26 +08:00
kesonan
c4ef9ceb68 Add api version (#3536) 2023-09-02 01:45:48 +00:00
dependabot[bot]
e95861f28a chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.9 to 2.1.0 (#3532)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-31 22:52:23 +08:00
Kevin Wan
d3cd7b17c0 Revert "add:func() QueryRowsPartial,QueryRowPartial into cachedsql.go" (#3523) 2023-08-27 21:36:14 +08:00
liumin-go
a50515496c add:func() QueryRowsPartial,QueryRowPartial into cachedsql.go (#3512)
Co-authored-by: 刘敏 <liumin@liumindeMac-mini.local>
2023-08-27 08:05:02 +00:00
Kevin Wan
0423313d9b feat: support json:"-" in mapping (#3521) 2023-08-27 16:04:38 +08:00
dependabot[bot]
7bbe7de05f chore(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1 (#3511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-24 14:59:49 +08:00
dependabot[bot]
83a451f2f4 chore(deps): bump github.com/jhump/protoreflect from 1.15.1 to 1.15.2 (#3518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-24 14:39:50 +08:00
Kevin Wan
d2a874f21d chore: upgrade go-zero, and update goctl version (#3509) 2023-08-21 09:09:51 +08:00
Kevin Wan
fd85b24b25 Update readme-cn.md 2023-08-20 23:38:39 +08:00
Kevin Wan
14fcbd7658 fix #3499 (#3508) 2023-08-19 22:17:24 +08:00
Kevin Wan
cb3ffc76a3 fix: #3478 (#3493) 2023-08-14 14:22:22 +00:00
dependabot[bot]
45fbd7dc35 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.4 to 2.30.5 (#3490)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-10 19:53:17 +08:00
dependabot[bot]
af821cf794 chore(deps): bump golang.org/x/net from 0.13.0 to 0.14.0 (#3484)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:39:32 +08:00
dependabot[bot]
ec69950153 chore(deps): bump github.com/jackc/pgx/v5 from 5.4.2 to 5.4.3 (#3483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:18:27 +08:00
Kevin Wan
ce5e78db53 chore: use jsonTagKey to replace json literals (#3479) 2023-08-06 22:00:24 +08:00
dependabot[bot]
ed75802eaa chore(deps): bump golang.org/x/text from 0.11.0 to 0.12.0 in /tools/goctl (#3477)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-06 09:57:19 +08:00
dependabot[bot]
76c92b571d chore(deps): bump golang.org/x/sys from 0.10.0 to 0.11.0 (#3476)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-06 00:19:00 +08:00
dependabot[bot]
a2e703c53e chore(deps): bump golang.org/x/net from 0.12.0 to 0.13.0 (#3463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-08-04 20:39:17 +08:00
dependabot[bot]
ca698deb2a chore(deps): bump go.mongodb.org/mongo-driver from 1.12.0 to 1.12.1 (#3472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-04 19:13:16 +08:00
guangwu
a9f4aab86b fix: "EXPRIMENTAL" is a misspelling of "EXPERIMENTAL" (#3462) 2023-08-02 23:59:37 +08:00
Kevin Wan
c3f57e9b0a chore: fix potential nil pointer errors (#3454) 2023-07-30 21:37:41 +08:00
Kevin Wan
ad4cce959d chore: add more tests (#3453) 2023-07-29 22:34:16 +08:00
Shyunn
279123f4a7 feat: add prometheus summary metrics (#3440)
Co-authored-by: chen quan <chenquan.dev@gmail.com>
2023-07-29 16:51:43 +08:00
dependabot[bot]
457eb1961b chore(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#3445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 22:18:14 +08:00
dependabot[bot]
63df384a4b chore(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 in /tools/goctl (#3446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 22:17:23 +08:00
MarkJoyMa
42bfa26e2b fix: remove mapping redundant error (#3439) 2023-07-24 00:10:50 +08:00
Kevin Wan
ff04356704 fix: format error should not trigger circuit breaker in sqlx (#3437) 2023-07-23 20:40:03 +08:00
MarkJoyMa
05db706c62 feat: optimize mapping error (#3438) 2023-07-23 12:10:41 +00:00
dependabot[bot]
ef2e0d859d chore(deps): bump go.uber.org/automaxprocs from 1.5.2 to 1.5.3 (#3435)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 00:03:07 +08:00
dependabot[bot]
05ec16ae9d chore(deps): bump github.com/gookit/color from 1.5.3 to 1.5.4 in /tools/goctl (#3433)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 23:27:49 +08:00
dependabot[bot]
13e685e0db chore(deps): bump github.com/emicklei/proto from 1.12.0 to 1.12.1 in /tools/goctl (#3431)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 23:12:06 +08:00
dependabot[bot]
c10f44b74e chore(deps): bump github.com/emicklei/proto from 1.11.2 to 1.12.0 in /tools/goctl (#3429) 2023-07-18 09:41:13 +08:00
Kevin Wan
57644420ed chore: update go-zero for goctl (#3426) 2023-07-14 21:38:42 +08:00
417 changed files with 13909 additions and 6284 deletions

View File

@@ -1,3 +1,7 @@
coverage:
status:
patch: true
project: false # disabled because project coverage is not stable
comment: comment:
layout: "flags, files" layout: "flags, files"
behavior: once behavior: once

View File

@@ -1 +1,7 @@
**/.git **/.git
.dockerignore
Dockerfile
goctl
Makefile
readme.md
readme-cn.md

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

@@ -5,6 +5,14 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "docker" # Update image tags in Dockerfile
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions" # Update GitHub Actions
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod" # See documentation for possible values - package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:

View File

@@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version-file: go.mod
check-latest: true check-latest: true
cache: true cache: true
id: go id: go
@@ -40,20 +40,20 @@ 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@v3 uses: codecov/codecov-action@v4
test-win: test-win:
name: Windows name: Windows
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
# use 1.18 to guarantee Go 1.18 compatibility # make sure Go version compatible with go-zero
go-version: 1.18 go-version-file: go.mod
check-latest: true check-latest: true
cache: true cache: true

View File

@@ -7,7 +7,7 @@ jobs:
close-issues: close-issues:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v6 - uses: actions/stale@v9
with: with:
days-before-issue-stale: 365 days-before-issue-stale: 365
days-before-issue-close: 90 days-before-issue-close: 90

View File

@@ -16,13 +16,13 @@ jobs:
- goarch: "386" - goarch: "386"
goos: darwin goos: darwin
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: zeromicro/go-zero-release-action@master - uses: zeromicro/go-zero-release-action@master
with: with:
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.18.10.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

@@ -5,7 +5,7 @@ jobs:
name: runner / staticcheck name: runner / staticcheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: reviewdog/action-staticcheck@v1 - uses: reviewdog/action-staticcheck@v1
with: with:
github_token: ${{ secrets.github_token }} github_token: ${{ secrets.github_token }}

View File

@@ -1,102 +1,76 @@
# Contributing # 🚀 Contributing to go-zero
Welcome to go-zero! Welcome to the go-zero community! We're thrilled to have you here. Contributing to our project is a fantastic way to be a part of the go-zero journey. Let's make this guide exciting and fun!
- [Before you get started](#before-you-get-started) ## 📜 Before You Dive In
- [Code of Conduct](#code-of-conduct)
- [Community Expectations](#community-expectations)
- [Getting started](#getting-started)
- [Your First Contribution](#your-first-contribution)
- [Find something to work on](#find-something-to-work-on)
- [Find a good first topic](#find-a-good-first-topic)
- [Work on an Issue](#work-on-an-issue)
- [File an Issue](#file-an-issue)
- [Contributor Workflow](#contributor-workflow)
- [Creating Pull Requests](#creating-pull-requests)
- [Code Review](#code-review)
- [Testing](#testing)
# Before you get started ### 🤝 Code of Conduct
## Code of Conduct Let's start on the right foot. Please take a moment to read and embrace our [Code of Conduct](/code-of-conduct.md). We're all about creating a welcoming and respectful environment.
Please make sure to read and observe our [Code of Conduct](/code-of-conduct.md). ### 🌟 Community Expectations
## Community Expectations At go-zero, we're like a close-knit family, and we believe in creating a healthy, friendly, and productive atmosphere. It's all about sharing knowledge and building amazing things together.
go-zero is a community project driven by its community which strives to promote a healthy, friendly and productive environment. ## 🚀 Getting Started
go-zero is a web and rpc framework written in Go. It's born to ensure the stability of the busy sites with resilient design. Builtin goctl greatly improves the development productivity.
# Getting started Get your adventure rolling! Here's how to begin:
- Fork the repository on GitHub. 1. 🍴 **Fork the Repository**: Head over to the GitHub repository and fork it to your own space.
- Make your changes on your fork repository.
- Submit a PR.
2. 🛠️ **Make Your Magic**: Work your magic in your forked repository. Create new features, squash bugs, or improve documentation - it's your world to conquer!
# Your First Contribution 3. 🚀 **Submit a PR (Pull Request)**: When you're ready to unveil your creation, submit a Pull Request. We can't wait to see your awesome work!
We will help you to contribute in different areas like filing issues, developing features, fixing critical bugs and ## 🌟 Your First Contribution
getting your work reviewed and merged.
If you have questions about the development process, We're here to guide you on your quest to become a go-zero contributor. Whether you want to file issues, develop features, or tame some critical bugs, we've got you covered.
feel free to [file an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
## Find something to work on If you have questions or need guidance at any stage, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
We are always in need of help, be it fixing documentation, reporting bugs or writing some code. ## 🔍 Find Something to Work On
Look at places where you feel best coding practices aren't followed, code refactoring is needed or tests are missing.
Here is how you get started.
### Find a good first topic Ready to dive into the action? There are several ways to contribute:
[go-zero](https://github.com/zeromicro/go-zero) has beginner-friendly issues that provide a good first issue. ### 💼 Find a Good First Topic
For example, [go-zero](https://github.com/zeromicro/go-zero) has
[help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and
[good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
labels for issues that should not need deep knowledge of the system.
We can help new contributors who wish to work on such issues.
Another good way to contribute is to find a documentation improvement, such as a missing/broken link. Discover easy-entry issues labeled as [help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These issues are perfect for newcomers and don't require deep knowledge of the system. We're here to assist you with these tasks.
Please see [Contributing](#contributing) below for the workflow.
#### Work on an issue ### 🪄 Work on an Issue
When you are willing to take on an issue, just reply on the issue. The maintainer will assign it to you. Once you've picked an issue that excites you, let us know by commenting on it. Our maintainers will assign it to you, and you can embark on your mission!
### File an Issue ### 📢 File an Issue
While we encourage everyone to contribute code, it is also appreciated when someone reports an issue. Reporting an issue is just as valuable as code contributions. If you discover a problem, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose). Be sure to follow our guidelines when submitting an issue.
Please follow the prompted submission guidelines while opening an issue. ## 🎯 Contributor Workflow
# Contributor Workflow Here's a rough guide to your contributor journey:
Please do not ever hesitate to ask a question or send a pull request. 1. 🌱 Create a New Branch: Start by creating a topic branch, usually based on the 'master' branch. This is where your contribution will grow.
This is a rough outline of what a contributor's workflow looks like: 2. 💡 Make Commits: Commit your work in logical units. Each commit should tell a story.
- Create a topic branch from where to base the contribution. This is usually master. 3. 🚀 Push Changes: Push the changes in your topic branch to your personal fork of the repository.
- Make commits of logical units.
- Push changes in a topic branch to a personal fork of the repository.
- Submit a pull request to [go-zero](https://github.com/zeromicro/go-zero).
## Creating Pull Requests 4. 📦 Submit a Pull Request: When your creation is complete, submit a Pull Request to the [go-zero repository](https://github.com/zeromicro/go-zero).
Pull requests are often called simply "PR". ## 🌠 Creating Pull Requests
go-zero generally follows the standard [github pull request](https://help.github.com/articles/about-pull-requests/) process.
To submit a proposed change, please develop the code/fix and add new test cases.
After that, run these local verifications before submitting pull request to predict the pass or
fail of continuous integration.
* Format the code with `gofmt` Pull Requests (PRs) are your way of making a grand entrance with your contribution. Here's how to do it:
* Run the test with data race enabled `go test -race ./...`
## Code Review - 💼 Format Your Code: Ensure your code is beautifully formatted with `gofmt`.
- 🏃 Run Tests: Verify that your changes pass all the tests, including data race tests. Run `go test -race ./...` for the ultimate validation.
To make it easier for your PR to receive reviews, consider the reviewers will need you to: ## 👁️‍🗨️ Code Review
* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments). Getting your PR reviewed is the final step before your contribution becomes part of go-zero's magical world. To make the process smooth, keep these things in mind:
* write [good commit messages](https://chris.beams.io/posts/git-commit/).
* break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue.
- 🧙‍♀️ Follow Good Coding Practices: Stick to [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
- 📝 Write Awesome Commit Messages: Craft [impressive commit messages](https://chris.beams.io/posts/git-commit/) - they're like spells in the wizard's book!
- 🔍 Break It Down: For larger changes, consider breaking them into a series of smaller, logical patches. Each patch should make an understandable and meaningful improvement.
Congratulations on your contribution journey! We're thrilled to have you as part of our go-zero community. Let's make amazing things together! 🌟
Now, go out there and start your adventure! If you have any more magical ideas to enhance this guide, please share them. 🔥

16
SECURITY.md Normal file
View File

@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
We publish releases monthly.
| Version | Supported |
| ------- | ------------------ |
| >= 1.4.4 | :white_check_mark: |
| < 1.4.4 | :x: |
## Reporting a Vulnerability
https://github.com/zeromicro/go-zero/security/advisories
Accepted vulnerabilities are expected to be fixed within a month.

View File

@@ -1,76 +1,127 @@
# Contributor Covenant Code of Conduct # Contributor Covenant Code of Conduct
## Our Pledge ## Our Pledge
In the interest of fostering an open and welcoming environment, we as We as members, contributors, and leaders pledge to make participation in our
contributors and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body
our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender
size, disability, ethnicity, sex characteristics, gender identity and expression, identity and expression, level of experience, education, socio-economic status,
level of experience, education, socio-economic status, nationality, personal nationality, personal appearance, race, caste, color, religion, or sexual
appearance, race, religion, or sexual identity and orientation. identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards ## Our Standards
Examples of behavior that contributes to creating a positive environment Examples of behavior that contributes to a positive environment for our
include: community include:
* Using welcoming and inclusive language * Demonstrating empathy and kindness toward other people
* Being respectful of differing viewpoints and experiences * Being respectful of differing opinions, viewpoints, and experiences
* Gracefully accepting constructive criticism * Giving and gracefully accepting constructive feedback
* Focusing on what is best for the community * Accepting responsibility and apologizing to those affected by our mistakes,
* Showing empathy towards other community members and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior include:
* The use of sexualized language or imagery and unwelcome sexual attention or * The use of sexualized language or imagery, and sexual attention or advances of
advances any kind
* Trolling, insulting/derogatory comments, and personal or political attacks * Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment * Public or private harassment
* Publishing others' private information, such as a physical or electronic * Publishing others' private information, such as a physical or email address,
address, without explicit permission without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Our Responsibilities ## Enforcement Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable Community leaders are responsible for clarifying and enforcing our standards of
behavior and are expected to take appropriate and fair corrective action in acceptable behavior and will take appropriate and fair corrective action in
response to any instances of unacceptable behavior. response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or Community leaders have the right and responsibility to remove, edit, or reject
reject comments, commits, code, wiki edits, issues, and other contributions comments, commits, code, wiki edits, issues, and other contributions that are
that are not aligned to this Code of Conduct, or to ban temporarily or not aligned to this Code of Conduct, and will communicate reasons for moderation
permanently any contributor for other behaviors that they deem inappropriate, decisions when appropriate.
threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies within all project spaces, and it also applies when This Code of Conduct applies within all community spaces, and also applies when
an individual is representing the project or its community in public spaces. an individual is officially representing the community in public spaces.
Examples of representing a project or community include using an official Examples of representing our community include using an official e-mail address,
project e-mail address, posting via an official social media account, or acting posting via an official social media account, or acting as an appointed
as an appointed representative at an online or offline event. Representation of representative at an online or offline event.
a project may be further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All reported to the community leaders responsible for enforcement at
complaints will be reviewed and investigated and will result in a response that [INSERT CONTACT METHOD].
is deemed necessary and appropriate to the circumstances. The project team is All complaints will be reviewed and investigated promptly and fairly.
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good All community leaders are obligated to respect the privacy and security of the
faith may face temporary or permanent repercussions as determined by other reporter of any incident.
members of the project's leadership.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant][homepage],
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
[homepage]: https://www.contributor-covenant.org Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

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 {
@@ -130,7 +125,7 @@ func (r *redisBitSet) check(ctx context.Context, offsets []uint) (bool, error) {
} }
resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args) resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args)
if err == redis.Nil { if errors.Is(err, redis.Nil) {
return false, nil return false, nil
} else if err != nil { } else if err != nil {
return false, err return false, err
@@ -162,7 +157,7 @@ func (r *redisBitSet) set(ctx context.Context, offsets []uint) error {
} }
_, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args) _, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args)
if err == redis.Nil { if errors.Is(err, redis.Nil) {
return nil return nil
} }

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"
@@ -31,38 +32,53 @@ type (
Name() string Name() string
// Allow checks if the request is allowed. // Allow checks if the request is allowed.
// If allowed, a promise will be returned, the caller needs to call promise.Accept() // If allowed, a promise will be returned,
// on success, or call promise.Reject() on failure. // otherwise ErrServiceUnavailable will be returned as the error.
// If not allow, ErrServiceUnavailable will be returned. // The caller needs to call promise.Accept() on success,
// 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.
// 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.
// acceptable checks if it's a successful call, even if the err 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 func(err error) error) 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.
// 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.
// acceptable checks if it's a successful call, even if the err is not nil. // acceptable checks if it's a successful call, even if the error is not nil.
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, 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 func(err error) error
// Option defines the method to customize a Breaker. // Option defines the method to customize a Breaker.
Option func(breaker *circuitBreaker) Option func(breaker *circuitBreaker)
@@ -86,12 +102,12 @@ type (
internalThrottle interface { internalThrottle interface {
allow() (internalPromise, error) allow() (internalPromise, error)
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error doReq(req func() error, fallback Fallback, acceptable Acceptable) error
} }
throttle interface { throttle interface {
allow() (Promise, error) allow() (Promise, error)
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error doReq(req func() error, fallback Fallback, acceptable Acceptable) error
} }
) )
@@ -114,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) DoWithFallback(req func() error, fallback func(err error) error) error { 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 {
return cb.throttle.doReq(req, fallback, defaultAcceptable) return cb.throttle.doReq(req, fallback, defaultAcceptable)
} }
func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error, 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,
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
} }
@@ -168,7 +232,7 @@ func (lt loggedThrottle) allow() (Promise, error) {
}, lt.logError(err) }, lt.logError(err)
} }
func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { func (lt loggedThrottle) doReq(req func() error, fallback Fallback, acceptable Acceptable) error {
return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool { return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool {
accept := acceptable(err) accept := acceptable(err)
if !accept && err != nil { if !accept && err != nil {
@@ -179,7 +243,7 @@ func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error,
} }
func (lt loggedThrottle) logError(err error) error { func (lt loggedThrottle) logError(err error) error {
if err == ErrServiceUnavailable { if errors.Is(err, ErrServiceUnavailable) {
// if circuit open, not possible to have empty error window // if circuit open, not possible to have empty error window
stat.Report(fmt.Sprintf( stat.Report(fmt.Sprintf(
"proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s", "proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s",
@@ -205,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,21 +31,44 @@ 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 func(err error) error) 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 {
return b.DoWithFallback(req, fallback) return b.DoWithFallback(req, fallback)
}) })
} }
// 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 func(err error) error, func DoWithFallbackAcceptable(name string, req func() error, fallback Fallback,
acceptable Acceptable) error { acceptable Acceptable) error {
return do(name, func(b Breaker) error { return do(name, func(b Breaker) error {
return b.DoWithFallbackAcceptable(req, fallback, acceptable) return b.DoWithFallbackAcceptable(req, fallback, acceptable)
}) })
} }
// 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()
@@ -59,7 +92,7 @@ func GetBreaker(name string) Breaker {
// NoBreakerFor disables the circuit breaker for the given name. // NoBreakerFor disables the circuit breaker for the given name.
func NoBreakerFor(name string) { func NoBreakerFor(name string) {
lock.Lock() lock.Lock()
breakers[name] = newNoOpBreaker() breakers[name] = NopBreaker()
lock.Unlock() lock.Unlock()
} }

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) {
@@ -30,7 +34,7 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
assert.Equal(t, errDummy, GetBreaker("anyone").DoWithAcceptable(func() error { assert.Equal(t, errDummy, GetBreaker("anyone").DoWithAcceptable(func() error {
return errDummy return errDummy
}, func(err error) bool { }, func(err error) bool {
return err == nil || err == errDummy return err == nil || errors.Is(err, errDummy)
})) }))
} }
verify(t, func() bool { verify(t, func() bool {
@@ -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 {
@@ -45,12 +56,12 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
}, func(err error) bool { }, func(err error) bool {
return err == nil return err == nil
}) })
assert.True(t, err == errDummy || err == ErrServiceUnavailable) assert.True(t, errors.Is(err, errDummy) || errors.Is(err, ErrServiceUnavailable))
} }
verify(t, func() bool { verify(t, func() bool {
return ErrServiceUnavailable == Do("another", func() error { return errors.Is(Do("another", func() error {
return nil return nil
}) }), ErrServiceUnavailable)
}) })
} }
@@ -75,18 +86,24 @@ func TestBreakersFallback(t *testing.T) {
}, func(err error) error { }, func(err error) error {
return nil return nil
}) })
assert.True(t, err == nil || err == errDummy) assert.True(t, err == nil || errors.Is(err, errDummy))
} err = DoWithFallbackCtx(context.Background(), "fallback", func() error {
verify(t, func() bool { return errDummy
return ErrServiceUnavailable == Do("fallback", func() error { }, func(err error) error {
return nil return nil
}) })
assert.True(t, err == nil || errors.Is(err, errDummy))
}
verify(t, func() bool {
return errors.Is(Do("fallback", func() error {
return nil
}), ErrServiceUnavailable)
}) })
} }
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 {
@@ -94,12 +111,20 @@ func TestBreakersAcceptableFallback(t *testing.T) {
}, func(err error) bool { }, func(err error) bool {
return err == nil return err == nil
}) })
assert.True(t, err == nil || 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 ErrServiceUnavailable == Do("acceptablefallback", func() error { return errors.Is(Do("acceptablefallback", func() error {
return nil return nil
}) }), ErrServiceUnavailable)
}) })
} }
@@ -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

@@ -1,57 +1,87 @@
package breaker package breaker
import ( import (
"math"
"time" "time"
"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
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) // for better performance, no need to care about the negative ratio
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.markDrop()
return nil, err return nil, err
} }
@@ -60,8 +90,9 @@ func (b *googleBreaker) allow() (internalPromise, error) {
}, nil }, nil
} }
func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, 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.markDrop()
if fallback != nil { if fallback != nil {
return fallback(err) return fallback(err)
} }
@@ -69,38 +100,55 @@ func (b *googleBreaker) doReq(req func() error, fallback func(err error) error,
return err return err
} }
var succ bool
defer func() { defer func() {
if e := recover(); e != nil { // if req() panic, success is false, mark as failure
if succ {
b.markSuccess()
} else {
b.markFailure() b.markFailure()
panic(e)
} }
}() }()
err := req() err := req()
if acceptable(err) { if acceptable(err) {
b.markSuccess() succ = true
} else {
b.markFailure()
} }
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,13 +120,50 @@ 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")
assert.Equal(t, errAcceptable, b.doReq(func() error { assert.Equal(t, errAcceptable, b.doReq(func() error {
return errAcceptable return errAcceptable
}, nil, func(err error) bool { }, nil, func(err error) bool {
return err == errAcceptable return errors.Is(err, errAcceptable)
})) }))
} }
@@ -105,7 +173,7 @@ func TestGoogleBreakerNotAcceptable(t *testing.T) {
assert.Equal(t, errAcceptable, b.doReq(func() error { assert.Equal(t, errAcceptable, b.doReq(func() error {
return errAcceptable return errAcceptable
}, nil, func(err error) bool { }, nil, func(err error) bool {
return err != errAcceptable return !errors.Is(err, errAcceptable)
})) }))
} }
@@ -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)
}) })
} }
@@ -206,7 +271,7 @@ func BenchmarkGoogleBreakerAllow(b *testing.B) {
breaker := getGoogleBreaker() breaker := getGoogleBreaker()
b.ResetTimer() b.ResetTimer()
for i := 0; i <= b.N; i++ { for i := 0; i <= b.N; i++ {
breaker.accept() _ = breaker.accept()
if i%2 == 0 { if i%2 == 0 {
breaker.markSuccess() breaker.markSuccess()
} else { } else {
@@ -215,6 +280,16 @@ func BenchmarkGoogleBreakerAllow(b *testing.B) {
} }
} }
func BenchmarkGoogleBreakerDoReq(b *testing.B) {
breaker := getGoogleBreaker()
b.ResetTimer()
for i := 0; i <= b.N; i++ {
_ = breaker.doReq(func() error {
return nil
}, nil, defaultAcceptable)
}
}
func markSuccess(b *googleBreaker, count int) { func markSuccess(b *googleBreaker, count int) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
p, err := b.allow() p, err := b.allow()

View File

@@ -1,35 +1,58 @@
package breaker package breaker
const noOpBreakerName = "nopBreaker" import "context"
type noOpBreaker struct{} const nopBreakerName = "nopBreaker"
func newNoOpBreaker() Breaker { type nopBreaker struct{}
return noOpBreaker{}
// NopBreaker returns a breaker that never trigger breaker circuit.
func NopBreaker() Breaker {
return nopBreaker{}
} }
func (b noOpBreaker) Name() string { func (b nopBreaker) Name() string {
return noOpBreakerName return nopBreakerName
} }
func (b noOpBreaker) Allow() (Promise, error) { func (b nopBreaker) Allow() (Promise, error) {
return nopPromise{}, nil return nopPromise{}, nil
} }
func (b noOpBreaker) Do(req func() error) error { func (b nopBreaker) AllowCtx(_ context.Context) (Promise, error) {
return nopPromise{}, nil
}
func (b nopBreaker) Do(req func() error) error {
return req() return req()
} }
func (b noOpBreaker) DoWithAcceptable(req func() error, _ Acceptable) error { func (b nopBreaker) DoCtx(_ context.Context, req func() error) error {
return req() return req()
} }
func (b noOpBreaker) DoWithFallback(req func() error, _ func(err error) error) error { func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
return req() return req()
} }
func (b noOpBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error, func (b nopBreaker) DoWithAcceptableCtx(_ context.Context, req func() error, _ Acceptable) error {
_ Acceptable) error { return req()
}
func (b nopBreaker) DoWithFallback(req func() error, _ Fallback) error {
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 {
return req()
}
func (b nopBreaker) DoWithFallbackAcceptableCtx(_ context.Context, req func() error,
_ Fallback, _ Acceptable) error {
return req() return req()
} }

View File

@@ -1,6 +1,7 @@
package breaker package breaker
import ( import (
"context"
"errors" "errors"
"testing" "testing"
@@ -8,10 +9,12 @@ import (
) )
func TestNopBreaker(t *testing.T) { func TestNopBreaker(t *testing.T) {
b := newNoOpBreaker() b := NopBreaker()
assert.Equal(t, noOpBreakerName, 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

@@ -23,7 +23,7 @@ var (
zero = big.NewInt(0) zero = big.NewInt(0)
) )
// DhKey defines the Diffie Hellman key. // DhKey defines the Diffie-Hellman key.
type DhKey struct { type DhKey struct {
PriKey *big.Int PriKey *big.Int
PubKey *big.Int PubKey *big.Int
@@ -46,7 +46,7 @@ func ComputeKey(pubKey, priKey *big.Int) (*big.Int, error) {
return new(big.Int).Exp(pubKey, priKey, p), nil return new(big.Int).Exp(pubKey, priKey, p), nil
} }
// GenerateKey returns a Diffie Hellman key. // GenerateKey returns a Diffie-Hellman key.
func GenerateKey() (*DhKey, error) { func GenerateKey() (*DhKey, error) {
var err error var err error
var x *big.Int var x *big.Int

View File

@@ -128,8 +128,8 @@ func (c *Cache) Take(key string, fetch func() (any, error)) (any, error) {
var fresh bool var fresh bool
val, err := c.barrier.Do(key, func() (any, error) { val, err := c.barrier.Do(key, func() (any, error) {
// because O(1) on map search in memory, and fetch is an IO query // because O(1) on map search in memory, and fetch is an IO query,
// so we do double check, cache might be taken by another call // so we do double-check, cache might be taken by another call
if val, ok := c.doGet(key); ok { if val, ok := c.doGet(key); ok {
return val, nil return val, nil
} }

View File

@@ -25,8 +25,14 @@ func (r *Ring) Add(v any) {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
r.elements[r.index%len(r.elements)] = v rlen := len(r.elements)
r.elements[r.index%rlen] = v
r.index++ r.index++
// prevent ring index overflow
if r.index >= rlen<<1 {
r.index -= rlen
}
} }
// Take takes all items from r. // Take takes all items from r.
@@ -36,16 +42,18 @@ func (r *Ring) Take() []any {
var size int var size int
var start int var start int
if r.index > len(r.elements) { rlen := len(r.elements)
size = len(r.elements)
start = r.index % len(r.elements) if r.index > rlen {
size = rlen
start = r.index % rlen
} else { } else {
size = r.index size = r.index
} }
elements := make([]any, size) elements := make([]any, size)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
elements[i] = r.elements[(start+i)%len(r.elements)] elements[i] = r.elements[(start+i)%rlen]
} }
return elements return elements

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

@@ -29,6 +29,8 @@ func NewSafeMap() *SafeMap {
// Del deletes the value with the given key from m. // Del deletes the value with the given key from m.
func (m *SafeMap) Del(key any) { func (m *SafeMap) Del(key any) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.dirtyOld[key]; ok { if _, ok := m.dirtyOld[key]; ok {
delete(m.dirtyOld, key) delete(m.dirtyOld, key)
m.deletionOld++ m.deletionOld++
@@ -52,7 +54,6 @@ func (m *SafeMap) Del(key any) {
m.dirtyNew = make(map[any]any) m.dirtyNew = make(map[any]any)
m.deletionNew = 0 m.deletionNew = 0
} }
m.lock.Unlock()
} }
// Get gets the value with the given key from m. // Get gets the value with the given key from m.
@@ -89,6 +90,8 @@ func (m *SafeMap) Range(f func(key, val any) bool) {
// Set sets the value into m with the given key. // Set sets the value into m with the given key.
func (m *SafeMap) Set(key, value any) { func (m *SafeMap) Set(key, value any) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock()
if m.deletionOld <= maxDeletion { if m.deletionOld <= maxDeletion {
if _, ok := m.dirtyNew[key]; ok { if _, ok := m.dirtyNew[key]; ok {
delete(m.dirtyNew, key) delete(m.dirtyNew, key)
@@ -102,7 +105,6 @@ func (m *SafeMap) Set(key, value any) {
} }
m.dirtyNew[key] = value m.dirtyNew[key] = value
} }
m.lock.Unlock()
} }
// Size returns the size of m. // Size returns the size of m.

View File

@@ -147,3 +147,65 @@ func TestSafeMap_Range(t *testing.T) {
assert.Equal(t, m.dirtyNew, newMap.dirtyNew) assert.Equal(t, m.dirtyNew, newMap.dirtyNew)
assert.Equal(t, m.dirtyOld, newMap.dirtyOld) assert.Equal(t, m.dirtyOld, newMap.dirtyOld)
} }
func TestSetManyTimes(t *testing.T) {
const iteration = maxDeletion * 2
m := NewSafeMap()
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
var count int
m.Range(func(k, v any) bool {
count++
return count < maxDeletion/2
})
assert.Equal(t, maxDeletion/2, count)
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
count = 0
m.Range(func(k, v any) bool {
count++
return count < maxDeletion
})
assert.Equal(t, maxDeletion, count)
}
func TestSetManyTimesNew(t *testing.T) {
m := NewSafeMap()
for i := 0; i < maxDeletion*3; i++ {
m.Set(i, i)
}
for i := 0; i < maxDeletion*2; i++ {
m.Del(i)
}
for i := 0; i < maxDeletion*3; i++ {
m.Set(i+maxDeletion*3, i+maxDeletion*3)
}
for i := 0; i < maxDeletion*2; i++ {
m.Del(i + maxDeletion*2)
}
for i := 0; i < maxDeletion-copyThreshold+1; i++ {
m.Del(i + maxDeletion*2)
}
assert.Equal(t, 0, len(m.dirtyNew))
}

View File

@@ -133,7 +133,7 @@ func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo, fullName st
return newConflictKeyError(fullName) return newConflictKeyError(fullName)
} }
if err := mergeFields(prev, key, child.children, fullName); err != nil { if err := mergeFields(prev, child.children, fullName); err != nil {
return err return err
} }
} else { } else {
@@ -281,7 +281,7 @@ func getTagName(field reflect.StructField) string {
return field.Name return field.Name
} }
func mergeFields(prev *fieldInfo, key string, children map[string]*fieldInfo, fullName string) error { func mergeFields(prev *fieldInfo, children map[string]*fieldInfo, fullName string) error {
if len(prev.children) == 0 || len(children) == 0 { if len(prev.children) == 0 || len(children) == 0 {
return newConflictKeyError(fullName) return newConflictKeyError(fullName)
} }

View File

@@ -12,7 +12,7 @@ type RestfulConf struct {
MaxConns int `json:",default=10000"` MaxConns int `json:",default=10000"`
MaxBytes int64 `json:",default=1048576"` MaxBytes int64 `json:",default=1048576"`
Timeout time.Duration `json:",default=3s"` Timeout time.Duration `json:",default=3s"`
CpuThreshold int64 `json:",default=900,range=[0:1000]"` CpuThreshold int64 `json:",default=900,range=[0:1000)"`
} }
``` ```

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

@@ -2,6 +2,7 @@ package internal
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"sort" "sort"
@@ -9,12 +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"
clientv3 "go.etcd.io/etcd/client/v3"
) )
var ( var (
@@ -22,12 +25,13 @@ var (
clusters: make(map[string]*cluster), clusters: make(map[string]*cluster),
} }
connManager = syncx.NewResourceManager() connManager = syncx.NewResourceManager()
errClosed = errors.New("etcd monitor chan has been closed")
) )
// 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.
@@ -42,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 {
@@ -52,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
@@ -75,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 {
@@ -105,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] {
@@ -122,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]
@@ -170,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 {
@@ -213,13 +226,18 @@ 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
} }
logx.Error(err) logx.Errorf("%s, key is %s", err.Error(), key)
time.Sleep(coolDownInterval) time.Sleep(coolDownInterval)
} }
@@ -236,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()
@@ -288,40 +307,53 @@ func (c *cluster) reload(cli EtcdClient) {
func (c *cluster) watch(cli EtcdClient, key string, rev int64) { func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
for { for {
if c.watchStream(cli, key, rev) { err := c.watchStream(cli, key, rev)
if err == nil {
return return
} }
if rev != 0 && errors.Is(err, rpctypes.ErrCompacted) {
logx.Errorf("etcd watch stream has been compacted, try to reload, rev %d", rev)
rev = c.load(cli, key)
}
// log the error and retry
logx.Error(err)
} }
} }
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool { 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), clientv3.WithPrefix(), ops []clientv3.OpOption
clientv3.WithRev(rev+1)) watchKey = key
} else { )
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix()) if !c.exactMatch {
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 {
case wresp, ok := <-rch: case wresp, ok := <-rch:
if !ok { if !ok {
logx.Error("etcd monitor chan has been closed") return errClosed
return false
} }
if wresp.Canceled { if wresp.Canceled {
logx.Errorf("etcd monitor chan has been canceled, error: %v", wresp.Err()) return fmt.Errorf("etcd monitor chan has been canceled, error: %w", wresp.Err())
return false
} }
if wresp.Err() != nil { if wresp.Err() != nil {
logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err())) return fmt.Errorf("etcd monitor chan error: %w", wresp.Err())
return false
} }
c.handleWatchEvents(key, wresp.Events) c.handleWatchEvents(key, wresp.Events)
case <-c.done: case <-c.done:
return true return nil
} }
} }
} }

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,18 +1,21 @@
package errorx package errorx
import "bytes" import (
"errors"
type ( "sync"
// A BatchError is an error that can hold multiple errors.
BatchError struct {
errs errorArray
}
errorArray []error
) )
// Add adds errs to be, nil errors are ignored. // BatchError is an error that can hold multiple errors.
type BatchError struct {
errs []error
lock sync.RWMutex
}
// Add adds one or more non-nil errors to the BatchError instance.
func (be *BatchError) Add(errs ...error) { func (be *BatchError) Add(errs ...error) {
be.lock.Lock()
defer be.lock.Unlock()
for _, err := range errs { for _, err := range errs {
if err != nil { if err != nil {
be.errs = append(be.errs, err) be.errs = append(be.errs, err)
@@ -20,33 +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 {
switch len(be.errs) { be.lock.RLock()
case 0: defer be.lock.RUnlock()
return nil
case 1: // If there are no non-nil errors, errors.Join(...) returns nil.
return be.errs[0] return errors.Join(be.errs...)
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.RLock()
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

@@ -3,6 +3,7 @@ package errorx
import ( import (
"errors" "errors"
"fmt" "fmt"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -33,7 +34,7 @@ func TestBatchErrorNilFromFunc(t *testing.T) {
func TestBatchErrorOneError(t *testing.T) { func TestBatchErrorOneError(t *testing.T) {
var batch BatchError var batch BatchError
batch.Add(errors.New(err1)) batch.Add(errors.New(err1))
assert.NotNil(t, batch) assert.NotNil(t, batch.Err())
assert.Equal(t, err1, batch.Err().Error()) assert.Equal(t, err1, batch.Err().Error())
assert.True(t, batch.NotNil()) assert.True(t, batch.NotNil())
} }
@@ -42,7 +43,105 @@ func TestBatchErrorWithErrors(t *testing.T) {
var batch BatchError var batch BatchError
batch.Add(errors.New(err1)) batch.Add(errors.New(err1))
batch.Add(errors.New(err2)) batch.Add(errors.New(err2))
assert.NotNil(t, batch) assert.NotNil(t, batch.Err())
assert.Equal(t, fmt.Sprintf("%s\n%s", err1, err2), batch.Err().Error()) assert.Equal(t, fmt.Sprintf("%s\n%s", err1, err2), batch.Err().Error())
assert.True(t, batch.NotNil()) assert.True(t, batch.NotNil())
} }
func TestBatchErrorConcurrentAdd(t *testing.T) {
const count = 10000
var batch BatchError
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
defer wg.Done()
batch.Add(errors.New(err1))
}()
}
wg.Wait()
assert.NotNil(t, batch.Err())
assert.Equal(t, count, len(batch.errs))
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

@@ -5,7 +5,7 @@ import "gopkg.in/cheggaaa/pb.v1"
type ( type (
// A Scanner is used to read lines. // A Scanner is used to read lines.
Scanner interface { Scanner interface {
// Scan checks if has remaining to read. // Scan checks if it has remaining to read.
Scan() bool Scan() bool
// Text returns next line. // Text returns next line.
Text() string Text() string

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

@@ -10,16 +10,15 @@ import (
const defaultRetryTimes = 3 const defaultRetryTimes = 3
var errTimeout = errors.New("retry timeout")
type ( type (
// RetryOption defines the method to customize DoWithRetry. // RetryOption defines the method to customize DoWithRetry.
RetryOption func(*retryOptions) RetryOption func(*retryOptions)
retryOptions struct { retryOptions struct {
times int times int
interval time.Duration interval time.Duration
timeout time.Duration timeout time.Duration
ignoreErrors []error
} }
) )
@@ -28,7 +27,7 @@ type (
// and performs modification operations, it is best to lock them, // and performs modification operations, it is best to lock them,
// otherwise there may be data race issues // otherwise there may be data race issues
func DoWithRetry(fn func() error, opts ...RetryOption) error { func DoWithRetry(fn func() error, opts ...RetryOption) error {
return retry(func(errChan chan error, retryCount int) { return retry(context.Background(), func(errChan chan error, retryCount int) {
errChan <- fn() errChan <- fn()
}, opts...) }, opts...)
} }
@@ -40,12 +39,12 @@ func DoWithRetry(fn func() error, opts ...RetryOption) error {
// otherwise there may be data race issues // otherwise there may be data race issues
func DoWithRetryCtx(ctx context.Context, fn func(ctx context.Context, retryCount int) error, func DoWithRetryCtx(ctx context.Context, fn func(ctx context.Context, retryCount int) error,
opts ...RetryOption) error { opts ...RetryOption) error {
return retry(func(errChan chan error, retryCount int) { return retry(ctx, func(errChan chan error, retryCount int) {
errChan <- fn(ctx, retryCount) errChan <- fn(ctx, retryCount)
}, opts...) }, opts...)
} }
func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) error { func retry(ctx context.Context, fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
options := newRetryOptions() options := newRetryOptions()
for _, opt := range opts { for _, opt := range opts {
opt(options) opt(options)
@@ -53,7 +52,6 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
var berr errorx.BatchError var berr errorx.BatchError
var cancelFunc context.CancelFunc var cancelFunc context.CancelFunc
ctx := context.Background()
if options.timeout > 0 { if options.timeout > 0 {
ctx, cancelFunc = context.WithTimeout(ctx, options.timeout) ctx, cancelFunc = context.WithTimeout(ctx, options.timeout)
defer cancelFunc() defer cancelFunc()
@@ -66,19 +64,24 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
select { select {
case err := <-errChan: case err := <-errChan:
if err != nil { if err != nil {
for _, ignoreErr := range options.ignoreErrors {
if errors.Is(err, ignoreErr) {
return nil
}
}
berr.Add(err) berr.Add(err)
} else { } else {
return nil return nil
} }
case <-ctx.Done(): case <-ctx.Done():
berr.Add(errTimeout) berr.Add(ctx.Err())
return berr.Err() return berr.Err()
} }
if options.interval > 0 { if options.interval > 0 {
select { select {
case <-ctx.Done(): case <-ctx.Done():
berr.Add(errTimeout) berr.Add(ctx.Err())
return berr.Err() return berr.Err()
case <-time.After(options.interval): case <-time.After(options.interval):
} }
@@ -88,19 +91,28 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
return berr.Err() return berr.Err()
} }
// WithRetry customize a DoWithRetry call with given retry times. // WithIgnoreErrors Ignore the specified errors
func WithRetry(times int) RetryOption { func WithIgnoreErrors(ignoreErrors []error) RetryOption {
return func(options *retryOptions) { return func(options *retryOptions) {
options.times = times options.ignoreErrors = ignoreErrors
} }
} }
// WithInterval customizes a DoWithRetry call with given interval.
func WithInterval(interval time.Duration) RetryOption { func WithInterval(interval time.Duration) RetryOption {
return func(options *retryOptions) { return func(options *retryOptions) {
options.interval = interval options.interval = interval
} }
} }
// WithRetry customizes a DoWithRetry call with given retry times.
func WithRetry(times int) RetryOption {
return func(options *retryOptions) {
options.times = times
}
}
// WithTimeout customizes a DoWithRetry call with given timeout.
func WithTimeout(timeout time.Duration) RetryOption { func WithTimeout(timeout time.Duration) RetryOption {
return func(options *retryOptions) { return func(options *retryOptions) {
options.timeout = timeout options.timeout = timeout

View File

@@ -97,20 +97,70 @@ func TestRetryWithInterval(t *testing.T) {
} }
func TestRetryCtx(t *testing.T) { func TestRetryWithWithIgnoreErrors(t *testing.T) {
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error { ignoreErr1 := errors.New("ignore error1")
if retryCount == 0 { ignoreErr2 := errors.New("ignore error2")
return errors.New("any") ignoreErrs := []error{ignoreErr1, ignoreErr2}
}
time.Sleep(time.Millisecond * 150)
return nil
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error { assert.Nil(t, DoWithRetry(func() error {
if retryCount == 1 { return ignoreErr1
return nil }, WithIgnoreErrors(ignoreErrs)))
}
time.Sleep(time.Millisecond * 150) assert.Nil(t, DoWithRetry(func() error {
return errors.New("any ") return ignoreErr2
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150))) }, WithIgnoreErrors(ignoreErrs)))
assert.NotNil(t, DoWithRetry(func() error {
return errors.New("any")
}))
}
func TestRetryCtx(t *testing.T) {
t.Run("with timeout", func(t *testing.T) {
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
if retryCount == 0 {
return errors.New("any")
}
time.Sleep(time.Millisecond * 150)
return nil
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
if retryCount == 1 {
return nil
}
time.Sleep(time.Millisecond * 150)
return errors.New("any ")
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
})
t.Run("with deadline exceeded", func(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
defer cancel()
var times int
assert.Error(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
times++
time.Sleep(time.Millisecond * 150)
return errors.New("any")
}, WithInterval(time.Millisecond*150)))
assert.Equal(t, 1, times)
})
t.Run("with deadline not exceeded", func(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
defer cancel()
var times int
assert.NoError(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
times++
if times == defaultRetryTimes {
return nil
}
time.Sleep(time.Millisecond * 50)
return errors.New("any")
}))
assert.Equal(t, defaultRetryTimes, times)
})
} }

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.
@@ -352,7 +352,7 @@ func (s Stream) Parallel(fn ParallelFunc, opts ...Option) {
}, opts...).Done() }, opts...).Done()
} }
// Reduce is an utility method to let the caller deal with the underlying channel. // Reduce is a utility method to let the caller deal with the underlying channel.
func (s Stream) Reduce(fn ReduceFunc) (any, error) { func (s Stream) Reduce(fn ReduceFunc) (any, error) {
return fn(s.source) return fn(s.source)
} }

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

@@ -32,6 +32,10 @@ func (bp *BufferPool) Get() *bytes.Buffer {
// Put returns buf into bp. // Put returns buf into bp.
func (bp *BufferPool) Put(buf *bytes.Buffer) { func (bp *BufferPool) Put(buf *bytes.Buffer) {
if buf == nil {
return
}
if buf.Cap() < bp.capability { if buf.Cap() < bp.capability {
bp.pool.Put(buf) bp.pool.Put(buf)
} }

View File

@@ -13,3 +13,26 @@ func TestBufferPool(t *testing.T) {
pool.Put(bytes.NewBuffer(make([]byte, 0, 2*capacity))) pool.Put(bytes.NewBuffer(make([]byte, 0, 2*capacity)))
assert.True(t, pool.Get().Cap() <= capacity) assert.True(t, pool.Get().Cap() <= capacity)
} }
func TestBufferPool_Put(t *testing.T) {
t.Run("with nil buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(nil)
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
t.Run("with less-cap buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(bytes.NewBuffer(make([]byte, 0, 512)))
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
t.Run("with more-cap buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(bytes.NewBuffer(make([]byte, 0, 1024<<1)))
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
}

View File

@@ -0,0 +1,12 @@
package iox
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNopCloser(t *testing.T) {
closer := NopCloser(nil)
assert.NoError(t, closer.Close())
}

View File

@@ -2,7 +2,7 @@ package iox
import "os" import "os"
// RedirectInOut redirects stdin to r, stdout to w, and callers need to call restore afterwards. // RedirectInOut redirects stdin to r, stdout to w, and callers need to call restore afterward.
func RedirectInOut() (restore func(), err error) { func RedirectInOut() (restore func(), err error) {
var r, w *os.File var r, w *os.File
r, w, err = os.Pipe() r, w, err = os.Pipe()

View File

@@ -35,6 +35,16 @@ func KeepSpace() TextReadOption {
} }
} }
// LimitDupReadCloser returns two io.ReadCloser that read from the first will be written to the second.
// But the second io.ReadCloser is limited to up to n bytes.
// The first returned reader needs to be read first, because the content
// read from it will be written to the underlying buffer of the second reader.
func LimitDupReadCloser(reader io.ReadCloser, n int64) (io.ReadCloser, io.ReadCloser) {
var buf bytes.Buffer
tee := LimitTeeReader(reader, &buf, n)
return io.NopCloser(tee), io.NopCloser(&buf)
}
// ReadBytes reads exactly the bytes with the length of len(buf) // ReadBytes reads exactly the bytes with the length of len(buf)
func ReadBytes(reader io.Reader, buf []byte) error { func ReadBytes(reader io.Reader, buf []byte) error {
var got int var got int

View File

@@ -51,6 +51,11 @@ b`,
} }
} }
func TestReadTextError(t *testing.T) {
_, err := ReadText("not-exist")
assert.NotNil(t, err)
}
func TestReadTextLines(t *testing.T) { func TestReadTextLines(t *testing.T) {
text := `1 text := `1
@@ -94,6 +99,11 @@ func TestReadTextLines(t *testing.T) {
} }
} }
func TestReadTextLinesError(t *testing.T) {
_, err := ReadTextLines("not-exist")
assert.NotNil(t, err)
}
func TestDupReadCloser(t *testing.T) { func TestDupReadCloser(t *testing.T) {
input := "hello" input := "hello"
reader := io.NopCloser(bytes.NewBufferString(input)) reader := io.NopCloser(bytes.NewBufferString(input))
@@ -108,6 +118,29 @@ func TestDupReadCloser(t *testing.T) {
verify(r2) verify(r2)
} }
func TestLimitDupReadCloser(t *testing.T) {
input := "hello world"
limitBytes := int64(4)
reader := io.NopCloser(bytes.NewBufferString(input))
r1, r2 := LimitDupReadCloser(reader, limitBytes)
verify := func(r io.Reader) {
output, err := io.ReadAll(r)
assert.Nil(t, err)
assert.Equal(t, input, string(output))
}
verifyLimit := func(r io.Reader, limit int64) {
output, err := io.ReadAll(r)
if limit < int64(len(input)) {
input = input[:limit]
}
assert.Nil(t, err)
assert.Equal(t, input, string(output))
}
verify(r1)
verifyLimit(r2, limitBytes)
}
func TestReadBytes(t *testing.T) { func TestReadBytes(t *testing.T) {
reader := io.NopCloser(bytes.NewBufferString("helloworld")) reader := io.NopCloser(bytes.NewBufferString("helloworld"))
buf := make([]byte, 5) buf := make([]byte, 5)

35
core/iox/tee.go Normal file
View File

@@ -0,0 +1,35 @@
package iox
import "io"
// LimitTeeReader returns a Reader that writes up to n bytes to w what it reads from r.
// First n bytes reads from r performed through it are matched with
// corresponding writes to w. There is no internal buffering -
// the write must complete before the first n bytes read completes.
// Any error encountered while writing is reported as a read error.
func LimitTeeReader(r io.Reader, w io.Writer, n int64) io.Reader {
return &limitTeeReader{r, w, n}
}
type limitTeeReader struct {
r io.Reader
w io.Writer
n int64 // limit bytes remaining
}
func (t *limitTeeReader) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p)
if n > 0 && t.n > 0 {
limit := int64(n)
if limit > t.n {
limit = t.n
}
if n, err := t.w.Write(p[:limit]); err != nil {
return n, err
}
t.n -= limit
}
return
}

40
core/iox/tee_test.go Normal file
View File

@@ -0,0 +1,40 @@
package iox
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLimitTeeReader(t *testing.T) {
limit := int64(4)
src := []byte("hello, world")
dst := make([]byte, len(src))
rb := bytes.NewBuffer(src)
wb := new(bytes.Buffer)
r := LimitTeeReader(rb, wb, limit)
if n, err := io.ReadFull(r, dst); err != nil || n != len(src) {
t.Fatalf("ReadFull(r, dst) = %d, %v; want %d, nil", n, err, len(src))
}
if !bytes.Equal(dst, src) {
t.Errorf("bytes read = %q want %q", dst, src)
}
if !bytes.Equal(wb.Bytes(), src[:limit]) {
t.Errorf("bytes written = %q want %q", wb.Bytes(), src)
}
n, err := r.Read(dst)
assert.Equal(t, 0, n)
assert.Equal(t, io.EOF, err)
rb = bytes.NewBuffer(src)
pr, pw := io.Pipe()
if assert.NoError(t, pr.Close()) {
r = LimitTeeReader(rb, pw, limit)
n, err := io.ReadFull(r, dst)
assert.Equal(t, 0, n)
assert.Equal(t, io.ErrClosedPipe, err)
}
}

View File

@@ -2,13 +2,14 @@ package iox
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"os" "os"
) )
const bufSize = 32 * 1024 const bufSize = 32 * 1024
// CountLines returns the number of lines in file. // CountLines returns the number of lines in the file.
func CountLines(file string) (int, error) { func CountLines(file string) (int, error) {
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
@@ -26,7 +27,7 @@ func CountLines(file string) (int, error) {
count += bytes.Count(buf[:c], lineSep) count += bytes.Count(buf[:c], lineSep)
switch { switch {
case err == io.EOF: case errors.Is(err, io.EOF):
if noEol { if noEol {
count++ count++
} }

View File

@@ -24,3 +24,8 @@ func TestCountLines(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, lines) assert.Equal(t, 4, lines)
} }
func TestCountLinesError(t *testing.T) {
_, err := CountLines("not-exist")
assert.NotNil(t, err)
}

View File

@@ -2,11 +2,12 @@ package iox
import ( import (
"bufio" "bufio"
"errors"
"io" "io"
"strings" "strings"
) )
// A TextLineScanner is a scanner that can scan lines from given reader. // A TextLineScanner is a scanner that can scan lines from the given reader.
type TextLineScanner struct { type TextLineScanner struct {
reader *bufio.Reader reader *bufio.Reader
hasNext bool hasNext bool
@@ -14,7 +15,7 @@ type TextLineScanner struct {
err error err error
} }
// NewTextLineScanner returns a TextLineScanner with given reader. // NewTextLineScanner returns a TextLineScanner with the given reader.
func NewTextLineScanner(reader io.Reader) *TextLineScanner { func NewTextLineScanner(reader io.Reader) *TextLineScanner {
return &TextLineScanner{ return &TextLineScanner{
reader: bufio.NewReader(reader), reader: bufio.NewReader(reader),
@@ -30,7 +31,7 @@ func (scanner *TextLineScanner) Scan() bool {
line, err := scanner.reader.ReadString('\n') line, err := scanner.reader.ReadString('\n')
scanner.line = strings.TrimRight(line, "\n") scanner.line = strings.TrimRight(line, "\n")
if err == io.EOF { if errors.Is(err, io.EOF) {
scanner.hasNext = false scanner.hasNext = false
return true return true
} else if err != nil { } else if err != nil {

View File

@@ -3,6 +3,7 @@ package iox
import ( import (
"strings" "strings"
"testing" "testing"
"testing/iotest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -22,3 +23,10 @@ func TestScanner(t *testing.T) {
} }
assert.EqualValues(t, []string{"1", "2", "3", "4"}, lines) assert.EqualValues(t, []string{"1", "2", "3", "4"}, lines)
} }
func TestBadScanner(t *testing.T) {
scanner := NewTextLineScanner(iotest.ErrReader(iotest.ErrTimeout))
assert.False(t, scanner.Scan())
_, err := scanner.Line()
assert.ErrorIs(t, err, iotest.ErrTimeout)
}

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,
@@ -125,10 +101,10 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
}) })
// redis allowed == false // redis allowed == false
// Lua boolean false -> r Nil bulk reply // Lua boolean false -> r Nil bulk reply
if 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

@@ -9,6 +9,7 @@ import (
"github.com/zeromicro/go-zero/core/collection" "github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"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" "github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/timex" "github.com/zeromicro/go-zero/core/timex"
@@ -21,8 +22,11 @@ const (
defaultCpuThreshold = 900 defaultCpuThreshold = 900
defaultMinRt = float64(time.Second / time.Millisecond) defaultMinRt = float64(time.Second / time.Millisecond)
// moving average hyperparameter beta for calculating requests on the fly // moving average hyperparameter beta for calculating requests on the fly
flyingBeta = 0.9 flyingBeta = 0.9
coolOffDuration = time.Second coolOffDuration = time.Second
cpuMax = 1000 // millicpu
millisecondsPerSecond = 1000
overloadFactorLowerBound = 0.1
) )
var ( var (
@@ -66,14 +70,14 @@ type (
adaptiveShedder struct { adaptiveShedder struct {
cpuThreshold int64 cpuThreshold int64
windows int64 windowScale float64
flying int64 flying int64
avgFlying float64 avgFlying float64
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]]
} }
) )
@@ -103,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,
windows: int64(time.Second / bucketDuration), 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()),
} }
} }
@@ -134,10 +139,10 @@ func (as *adaptiveShedder) Allow() (Promise, error) {
func (as *adaptiveShedder) addFlying(delta int64) { func (as *adaptiveShedder) addFlying(delta int64) {
flying := atomic.AddInt64(&as.flying, delta) flying := atomic.AddInt64(&as.flying, delta)
// update avgFlying when the request is finished. // update avgFlying when the request is finished.
// this strategy makes avgFlying have a little bit lag against flying, and smoother. // this strategy makes avgFlying have a little bit of lag against flying, and smoother.
// when the flying requests increase rapidly, avgFlying increase slower, accept more requests. // when the flying requests increase rapidly, avgFlying increase slower, accept more requests.
// when the flying requests drop rapidly, avgFlying drop slower, accept less requests. // when the flying requests drop rapidly, avgFlying drop slower, accept fewer requests.
// it makes the service to serve as more requests as possible. // it makes the service to serve as many requests as possible.
if delta < 0 { if delta < 0 {
as.avgFlyingLock.Lock() as.avgFlyingLock.Lock()
as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta) as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta)
@@ -149,39 +154,42 @@ func (as *adaptiveShedder) highThru() bool {
as.avgFlyingLock.Lock() as.avgFlyingLock.Lock()
avgFlying := as.avgFlying avgFlying := as.avgFlying
as.avgFlyingLock.Unlock() as.avgFlyingLock.Unlock()
maxFlight := as.maxFlight() maxFlight := as.maxFlight() * as.overloadFactor()
return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight return avgFlying > maxFlight && float64(atomic.LoadInt64(&as.flying)) > maxFlight
} }
func (as *adaptiveShedder) maxFlight() int64 { func (as *adaptiveShedder) maxFlight() float64 {
// windows = buckets per second // windows = buckets per second
// maxQPS = maxPASS * windows // maxQPS = maxPASS * windows
// minRT = min average response time in milliseconds // minRT = min average response time in milliseconds
// maxQPS * minRT / milliseconds_per_second // allowedFlying = maxQPS * minRT / milliseconds_per_second
return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3))) maxFlight := float64(as.maxPass()) * as.minRt() * as.windowScale
return mathx.AtLeast(maxFlight, 1)
} }
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 {
// if no requests in previous windows, return defaultMinRt,
// 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
} }
@@ -190,6 +198,13 @@ func (as *adaptiveShedder) minRt() float64 {
return result return result
} }
func (as *adaptiveShedder) overloadFactor() float64 {
// as.cpuThreshold must be less than cpuMax
factor := (cpuMax - float64(stat.CpuUsage())) / (cpuMax - float64(as.cpuThreshold))
// at least accept 10% of acceptable requests, even cpu is highly overloaded.
return mathx.Between(factor, overloadFactorLowerBound, 1)
}
func (as *adaptiveShedder) shouldDrop() bool { func (as *adaptiveShedder) shouldDrop() bool {
if as.systemOverloaded() || as.stillHot() { if as.systemOverloaded() || as.stillHot() {
if as.highThru() { if as.highThru() {
@@ -236,14 +251,14 @@ func (as *adaptiveShedder) systemOverloaded() bool {
return true return true
} }
// WithBuckets customizes the Shedder with given number of buckets. // WithBuckets customizes the Shedder with the given number of buckets.
func WithBuckets(buckets int) ShedderOption { func WithBuckets(buckets int) ShedderOption {
return func(opts *shedderOptions) { return func(opts *shedderOptions) {
opts.buckets = buckets opts.buckets = buckets
} }
} }
// WithCpuThreshold customizes the Shedder with given cpu threshold. // WithCpuThreshold customizes the Shedder with the given cpu threshold.
func WithCpuThreshold(threshold int64) ShedderOption { func WithCpuThreshold(threshold int64) ShedderOption {
return func(opts *shedderOptions) { return func(opts *shedderOptions) {
opts.cpuThreshold = threshold opts.cpuThreshold = threshold
@@ -269,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

@@ -19,6 +19,7 @@ import (
const ( const (
buckets = 10 buckets = 10
bucketDuration = time.Millisecond * 50 bucketDuration = time.Millisecond * 50
windowFactor = 0.01
) )
func init() { func init() {
@@ -57,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{
@@ -82,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{
@@ -106,18 +107,18 @@ 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{
passCounter: passCounter, passCounter: passCounter,
rtCounter: rtCounter, rtCounter: rtCounter,
windows: buckets, windowScale: windowFactor,
droppedRecently: syncx.NewAtomicBool(), droppedRecently: syncx.NewAtomicBool(),
} }
assert.Equal(t, int64(54), shedder.maxFlight()) assert.Equal(t, float64(54), shedder.maxFlight())
} }
func TestAdaptiveShedderShouldDrop(t *testing.T) { func TestAdaptiveShedderShouldDrop(t *testing.T) {
@@ -128,15 +129,15 @@ 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{
passCounter: passCounter, passCounter: passCounter,
rtCounter: rtCounter, rtCounter: rtCounter,
windows: buckets, windowScale: windowFactor,
overloadTime: syncx.NewAtomicDuration(), overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.NewAtomicBool(), droppedRecently: syncx.NewAtomicBool(),
} }
@@ -149,7 +150,8 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
// cpu >= 800, inflight > maxPass // cpu >= 800, inflight > maxPass
shedder.avgFlying = 80 shedder.avgFlying = 80
shedder.flying = 50 // because of the overloadFactor, so we need to make sure maxFlight is greater than flying
shedder.flying = int64(shedder.maxFlight()*shedder.overloadFactor()) - 5
assert.False(t, shedder.shouldDrop()) assert.False(t, shedder.shouldDrop())
// cpu >= 800, inflight > maxPass // cpu >= 800, inflight > maxPass
@@ -182,15 +184,15 @@ 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{
passCounter: passCounter, passCounter: passCounter,
rtCounter: rtCounter, rtCounter: rtCounter,
windows: buckets, windowScale: windowFactor,
overloadTime: syncx.NewAtomicDuration(), overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.ForAtomicBool(true), droppedRecently: syncx.ForAtomicBool(true),
} }
@@ -239,6 +241,32 @@ func BenchmarkAdaptiveShedder_Allow(b *testing.B) {
b.Run("low load", bench) b.Run("low load", bench)
} }
func newRollingWindow() *collection.RollingWindow { func BenchmarkMaxFlight(b *testing.B) {
return collection.NewRollingWindow(buckets, bucketDuration, collection.IgnoreCurrentBucket()) passCounter := newRollingWindow()
rtCounter := newRollingWindow()
for i := 0; i < 10; i++ {
if i > 0 {
time.Sleep(bucketDuration)
}
passCounter.Add(int64((i + 1) * 100))
for j := i*10 + 1; j <= i*10+10; j++ {
rtCounter.Add(int64(j))
}
}
shedder := &adaptiveShedder{
passCounter: passCounter,
rtCounter: rtCounter,
windowScale: windowFactor,
droppedRecently: syncx.NewAtomicBool(),
}
for i := 0; i < b.N; i++ {
_ = shedder.maxFlight()
}
}
func newRollingWindow() *collection.RollingWindow[int64, *collection.Bucket[int64]] {
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

@@ -6,7 +6,7 @@ import (
"github.com/zeromicro/go-zero/core/syncx" "github.com/zeromicro/go-zero/core/syncx"
) )
// A ShedderGroup is a manager to manage key based shedders. // A ShedderGroup is a manager to manage key-based shedders.
type ShedderGroup struct { type ShedderGroup struct {
options []ShedderOption options []ShedderOption
manager *syncx.ResourceManager manager *syncx.ResourceManager

View File

@@ -42,7 +42,7 @@ func Debugv(ctx context.Context, v interface{}) {
getLogger(ctx).Debugv(v) getLogger(ctx).Debugv(v)
} }
// Debugw writes msg along with fields into access log. // Debugw writes msg along with fields into the access log.
func Debugw(ctx context.Context, msg string, fields ...LogField) { func Debugw(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Debugw(msg, fields...) getLogger(ctx).Debugw(msg, fields...)
} }
@@ -63,7 +63,7 @@ func Errorv(ctx context.Context, v any) {
getLogger(ctx).Errorv(v) getLogger(ctx).Errorv(v)
} }
// Errorw writes msg along with fields into error log. // Errorw writes msg along with fields into the error log.
func Errorw(ctx context.Context, msg string, fields ...LogField) { func Errorw(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Errorw(msg, fields...) getLogger(ctx).Errorw(msg, fields...)
} }
@@ -88,7 +88,7 @@ func Infov(ctx context.Context, v any) {
getLogger(ctx).Infov(v) getLogger(ctx).Infov(v)
} }
// Infow writes msg along with fields into access log. // Infow writes msg along with fields into the access log.
func Infow(ctx context.Context, msg string, fields ...LogField) { func Infow(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Infow(msg, fields...) getLogger(ctx).Infow(msg, fields...)
} }
@@ -108,10 +108,11 @@ func SetLevel(level uint32) {
logx.SetLevel(level) logx.SetLevel(level)
} }
// SetUp sets up the logx. If already set up, just return nil. // SetUp sets up the logx.
// we allow SetUp to be called multiple times, because for example // If already set up, return nil.
// We allow SetUp to be called multiple times, because, for example,
// we need to allow different service frameworks to initialize logx respectively. // we need to allow different service frameworks to initialize logx respectively.
// the same logic for SetUp // The same logic for SetUp
func SetUp(c LogConf) error { func SetUp(c LogConf) error {
return logx.SetUp(c) return logx.SetUp(c)
} }

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

@@ -1,6 +1,6 @@
package logx package logx
// A LessLogger is a logger that control to log once during the given duration. // A LessLogger is a logger that controls to log once during the given duration.
type LessLogger struct { type LessLogger struct {
*limitedExecutor *limitedExecutor
} }

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"
@@ -17,14 +18,13 @@ import (
const callerDepth = 4 const callerDepth = 4
var ( var (
timeFormat = "2006-01-02T15:04:05.000Z07:00" timeFormat = "2006-01-02T15:04:05.000Z07:00"
logLevel uint32
encoding uint32 = jsonEncodingType encoding uint32 = jsonEncodingType
// maxContentLength is used to truncate the log content, 0 for not truncating. // maxContentLength is used to truncate the log content, 0 for not truncating.
maxContentLength uint32 maxContentLength uint32
// use uint32 for atomic operations // use uint32 for atomic operations
disableLog uint32
disableStat uint32 disableStat uint32
logLevel uint32
options logOptions options logOptions
writer = new(atomicWriter) writer = new(atomicWriter)
setupOnce sync.Once setupOnce sync.Once
@@ -52,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)
@@ -87,7 +107,7 @@ func Debugv(v any) {
} }
} }
// Debugw writes msg along with fields into access log. // Debugw writes msg along with fields into the access log.
func Debugw(msg string, fields ...LogField) { func Debugw(msg string, fields ...LogField) {
if shallLog(DebugLevel) { if shallLog(DebugLevel) {
writeDebug(msg, fields...) writeDebug(msg, fields...)
@@ -96,7 +116,7 @@ func Debugw(msg string, fields ...LogField) {
// Disable disables the logging. // Disable disables the logging.
func Disable() { func Disable() {
atomic.StoreUint32(&disableLog, 1) atomic.StoreUint32(&logLevel, disableLevel)
writer.Store(nopWriter{}) writer.Store(nopWriter{})
} }
@@ -143,7 +163,7 @@ func Errorv(v any) {
} }
} }
// Errorw writes msg along with fields into error log. // Errorw writes msg along with fields into the error log.
func Errorw(msg string, fields ...LogField) { func Errorw(msg string, fields ...LogField) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
writeError(msg, fields...) writeError(msg, fields...)
@@ -154,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:
@@ -176,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:
@@ -209,7 +229,7 @@ func Infov(v any) {
} }
} }
// Infow writes msg along with fields into access log. // Infow writes msg along with fields into the access log.
func Infow(msg string, fields ...LogField) { func Infow(msg string, fields ...LogField) {
if shallLog(InfoLevel) { if shallLog(InfoLevel) {
writeInfo(msg, fields...) writeInfo(msg, fields...)
@@ -250,16 +270,17 @@ func SetLevel(level uint32) {
// SetWriter sets the logging writer. It can be used to customize the logging. // SetWriter sets the logging writer. It can be used to customize the logging.
func SetWriter(w Writer) { func SetWriter(w Writer) {
if atomic.LoadUint32(&disableLog) == 0 { if atomic.LoadUint32(&logLevel) != disableLevel {
writer.Store(w) writer.Store(w)
} }
} }
// SetUp sets up the logx. If already set up, just return nil. // SetUp sets up the logx.
// we allow SetUp to be called multiple times, because for example // If already set up, return nil.
// We allow SetUp to be called multiple times, because, for example,
// we need to allow different service frameworks to initialize logx respectively. // we need to allow different service frameworks to initialize logx respectively.
func SetUp(c LogConf) (err error) { func SetUp(c LogConf) (err error) {
// Just ignore the subsequent SetUp calls. // Ignore the later SetUp calls.
// Because multiple services in one process might call SetUp respectively. // Because multiple services in one process might call SetUp respectively.
// Need to wait for the first caller to complete the execution. // Need to wait for the first caller to complete the execution.
setupOnce.Do(func() { setupOnce.Do(func() {
@@ -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 {
@@ -481,7 +532,7 @@ func writeDebug(val any, fields ...LogField) {
getWriter().Debug(val, addCaller(fields...)...) getWriter().Debug(val, addCaller(fields...)...)
} }
// writeError writes v into error log. // writeError writes v into the error log.
// Not checking shallLog here is for performance consideration. // Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. // If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function. // The caller should check shallLog before calling this function.
@@ -521,7 +572,7 @@ func writeStack(msg string) {
getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
} }
// writeStat writes v into stat log. // writeStat writes v into the stat log.
// Not checking shallLog here is for performance consideration. // Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled. // If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function. // The caller should check shallLog before calling this function.

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)
@@ -570,7 +591,7 @@ func TestErrorfWithWrappedError(t *testing.T) {
old := writer.Swap(w) old := writer.Swap(w)
defer writer.Store(old) defer writer.Store(old)
Errorf("hello %w", errors.New(message)) Errorf("hello %s", errors.New(message))
assert.True(t, strings.Contains(w.String(), "hello there")) assert.True(t, strings.Contains(w.String(), "hello there"))
} }
@@ -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)
@@ -666,6 +691,7 @@ func TestDisable(t *testing.T) {
WithMaxSize(1024)(&opt) WithMaxSize(1024)(&opt)
assert.Nil(t, Close()) assert.Nil(t, Close())
assert.Nil(t, Close()) assert.Nil(t, Close())
assert.Equal(t, uint32(disableLevel), atomic.LoadUint32(&logLevel))
} }
func TestDisableStat(t *testing.T) { func TestDisableStat(t *testing.T) {
@@ -679,8 +705,19 @@ 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(&disableLog, 0) atomic.StoreUint32(&logLevel, 0)
Reset() Reset()
SetWriter(nopWriter{}) SetWriter(nopWriter{})
assert.NotNil(t, writer.Load()) assert.NotNil(t, writer.Load())
@@ -813,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,
@@ -858,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

@@ -40,7 +40,7 @@ type LogConf struct {
- `Compress`: whether or not to compress log files, only works with `file` mode. - `Compress`: whether or not to compress log files, only works with `file` mode.
- `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode. - `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode.
- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. Its used to avoid stacktrace flooding. - `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. Its used to avoid stacktrace flooding.
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even thougth `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached. - `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even though `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
- `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`. - `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`.
- `Rotation`: represents the type of log rotation rule. Default is `daily`. - `Rotation`: represents the type of log rotation rule. Default is `daily`.
- `daily` rotate the logs by day. - `daily` rotate the logs by day.

View File

@@ -41,67 +41,99 @@ type richLogger struct {
} }
func (l *richLogger) Debug(v ...any) { func (l *richLogger) Debug(v ...any) {
l.debug(fmt.Sprint(v...)) if shallLog(DebugLevel) {
l.debug(fmt.Sprint(v...))
}
} }
func (l *richLogger) Debugf(format string, v ...any) { func (l *richLogger) Debugf(format string, v ...any) {
l.debug(fmt.Sprintf(format, v...)) if shallLog(DebugLevel) {
l.debug(fmt.Sprintf(format, v...))
}
} }
func (l *richLogger) Debugv(v any) { func (l *richLogger) Debugv(v any) {
l.debug(v) if shallLog(DebugLevel) {
l.debug(v)
}
} }
func (l *richLogger) Debugw(msg string, fields ...LogField) { func (l *richLogger) Debugw(msg string, fields ...LogField) {
l.debug(msg, fields...) if shallLog(DebugLevel) {
l.debug(msg, fields...)
}
} }
func (l *richLogger) Error(v ...any) { func (l *richLogger) Error(v ...any) {
l.err(fmt.Sprint(v...)) if shallLog(ErrorLevel) {
l.err(fmt.Sprint(v...))
}
} }
func (l *richLogger) Errorf(format string, v ...any) { func (l *richLogger) Errorf(format string, v ...any) {
l.err(fmt.Sprintf(format, v...)) if shallLog(ErrorLevel) {
l.err(fmt.Sprintf(format, v...))
}
} }
func (l *richLogger) Errorv(v any) { func (l *richLogger) Errorv(v any) {
l.err(v) if shallLog(ErrorLevel) {
l.err(v)
}
} }
func (l *richLogger) Errorw(msg string, fields ...LogField) { func (l *richLogger) Errorw(msg string, fields ...LogField) {
l.err(msg, fields...) if shallLog(ErrorLevel) {
l.err(msg, fields...)
}
} }
func (l *richLogger) Info(v ...any) { func (l *richLogger) Info(v ...any) {
l.info(fmt.Sprint(v...)) if shallLog(InfoLevel) {
l.info(fmt.Sprint(v...))
}
} }
func (l *richLogger) Infof(format string, v ...any) { func (l *richLogger) Infof(format string, v ...any) {
l.info(fmt.Sprintf(format, v...)) if shallLog(InfoLevel) {
l.info(fmt.Sprintf(format, v...))
}
} }
func (l *richLogger) Infov(v any) { func (l *richLogger) Infov(v any) {
l.info(v) if shallLog(InfoLevel) {
l.info(v)
}
} }
func (l *richLogger) Infow(msg string, fields ...LogField) { func (l *richLogger) Infow(msg string, fields ...LogField) {
l.info(msg, fields...) if shallLog(InfoLevel) {
l.info(msg, fields...)
}
} }
func (l *richLogger) Slow(v ...any) { func (l *richLogger) Slow(v ...any) {
l.slow(fmt.Sprint(v...)) if shallLog(ErrorLevel) {
l.slow(fmt.Sprint(v...))
}
} }
func (l *richLogger) Slowf(format string, v ...any) { func (l *richLogger) Slowf(format string, v ...any) {
l.slow(fmt.Sprintf(format, v...)) if shallLog(ErrorLevel) {
l.slow(fmt.Sprintf(format, v...))
}
} }
func (l *richLogger) Slowv(v any) { func (l *richLogger) Slowv(v any) {
l.slow(v) if shallLog(ErrorLevel) {
l.slow(v)
}
} }
func (l *richLogger) Sloww(msg string, fields ...LogField) { func (l *richLogger) Sloww(msg string, fields ...LogField) {
l.slow(msg, fields...) if shallLog(ErrorLevel) {
l.slow(msg, fields...)
}
} }
func (l *richLogger) WithCallerSkip(skip int) Logger { func (l *richLogger) WithCallerSkip(skip int) Logger {
@@ -109,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.
@@ -298,6 +301,7 @@ func (l *RotateLogger) initialize() error {
if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil { if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
return err return err
} }
l.currentSize = fileInfo.Size() l.currentSize = fileInfo.Size()
} }
@@ -318,7 +322,7 @@ func (l *RotateLogger) maybeCompressFile(file string) {
}() }()
if _, err := os.Stat(file); err != nil { if _, err := os.Stat(file); err != nil {
// file not exists or other error, ignore compression // file doesn't exist or another error, ignore compression
return return
} }
@@ -381,7 +385,15 @@ func (l *RotateLogger) startWorker() {
case event := <-l.channel: case event := <-l.channel:
l.write(event) l.write(event)
case <-l.done: case <-l.done:
return // avoid losing logs before closing.
for {
select {
case event := <-l.channel:
l.write(event)
default:
return
}
}
} }
} }
}() }()

View File

@@ -206,6 +206,27 @@ func TestRotateLoggerClose(t *testing.T) {
_, err := logger.Write([]byte("foo")) _, err := logger.Write([]byte("foo"))
assert.ErrorIs(t, err, ErrLogFileClosed) assert.ErrorIs(t, err, ErrLogFileClosed)
}) })
t.Run("close without losing logs", func(t *testing.T) {
text := "foo"
filename, err := fs.TempFilenameWithText(text)
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
msg := []byte("foo")
n := 100
for i := 0; i < n; i++ {
_, err = logger.Write(msg)
assert.Nil(t, err)
}
assert.Nil(t, logger.Close())
bs, err := os.ReadFile(filename)
assert.Nil(t, err)
assert.Equal(t, len(msg)*n+len(text), len(bs))
})
} }
func TestRotateLoggerGetBackupFilename(t *testing.T) { func TestRotateLoggerGetBackupFilename(t *testing.T) {
@@ -496,6 +517,21 @@ func TestGzipFile(t *testing.T) {
}) })
} }
func TestRotateLogger_WithExistingFile(t *testing.T) {
const body = "foo"
filename, err := fs.TempFilenameWithText(body)
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
rule := NewSizeLimitRotateRule(filename, "-", 1, 100, 3, false)
logger, err := NewLogger(filename, rule, false)
assert.Nil(t, err)
assert.Equal(t, int64(len(body)), logger.currentSize)
assert.Nil(t, logger.Close())
}
func BenchmarkRotateLogger(b *testing.B) { func BenchmarkRotateLogger(b *testing.B) {
filename := "./test.log" filename := "./test.log"
filename2 := "./test2.log" filename2 := "./test2.log"

View File

@@ -15,6 +15,8 @@ const (
ErrorLevel ErrorLevel
// SevereLevel only log severe messages // SevereLevel only log severe messages
SevereLevel SevereLevel
// disableLevel doesn't log any messages
disableLevel = 0xff
) )
const ( const (
@@ -46,6 +48,7 @@ const (
levelDebug = "debug" levelDebug = "debug"
backupFileDelimiter = "-" backupFileDelimiter = "-"
nilAngleString = "<nil>"
flags = 0x0 flags = 0x0
) )

View File

@@ -7,11 +7,13 @@ import (
"io" "io"
"log" "log"
"path" "path"
"runtime/debug"
"sync" "sync"
"sync/atomic" "sync/atomic"
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 (
@@ -32,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
@@ -87,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))
@@ -253,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
@@ -277,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 {
@@ -288,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
@@ -331,12 +408,14 @@ 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.Println(err.Error()) 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))
} else { } else {
writer.Write(append(content, '\n')) if _, err := writer.Write(append(content, '\n')); err != nil {
log.Println(err.Error())
}
} }
} }
@@ -384,7 +463,7 @@ func writePlainValue(writer io.Writer, level string, val any, fields ...string)
buf.WriteString(level) buf.WriteString(level)
buf.WriteByte(plainEncodingSep) buf.WriteByte(plainEncodingSep)
if err := json.NewEncoder(&buf).Encode(val); err != nil { if err := json.NewEncoder(&buf).Encode(val); err != nil {
log.Println(err.Error()) log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
return return
} }

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) {
@@ -126,9 +127,23 @@ func TestWriteJson(t *testing.T) {
log.SetOutput(&buf) log.SetOutput(&buf)
writeJson(nil, "foo") writeJson(nil, "foo")
assert.Contains(t, buf.String(), "foo") assert.Contains(t, buf.String(), "foo")
buf.Reset()
writeJson(hardToWriteWriter{}, "foo")
assert.Contains(t, buf.String(), "write error")
buf.Reset() buf.Reset()
writeJson(nil, make(chan int)) writeJson(nil, make(chan int))
assert.Contains(t, buf.String(), "unsupported type") assert.Contains(t, buf.String(), "unsupported type")
buf.Reset()
type C struct {
RC func()
}
writeJson(nil, C{
RC: func() {},
})
assert.Contains(t, buf.String(), "runtime/debug.Stack")
} }
func TestWritePlainAny(t *testing.T) { func TestWritePlainAny(t *testing.T) {
@@ -165,6 +180,49 @@ func TestWritePlainAny(t *testing.T) {
writePlainAny(hardToWriteWriter{}, levelFatal, "foo") writePlainAny(hardToWriteWriter{}, levelFatal, "foo")
assert.Contains(t, buf.String(), "write error") assert.Contains(t, buf.String(), "write error")
buf.Reset()
type C struct {
RC func()
}
writePlainAny(nil, levelError, C{
RC: func() {},
})
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) {
@@ -197,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"`
@@ -228,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

@@ -12,7 +12,7 @@ const (
) )
// Marshal marshals the given val and returns the map that contains the fields. // Marshal marshals the given val and returns the map that contains the fields.
// optional=another is not implemented, and it's hard to implement and not common used. // optional=another is not implemented, and it's hard to implement and not commonly used.
func Marshal(val any) (map[string]map[string]any, error) { func Marshal(val any) (map[string]map[string]any, error) {
ret := make(map[string]map[string]any) ret := make(map[string]map[string]any)
tp := reflect.TypeOf(val) tp := reflect.TypeOf(val)

View File

@@ -18,8 +18,10 @@ import (
) )
const ( const (
defaultKeyName = "key" defaultKeyName = "key"
delimiter = '.' delimiter = '.'
ignoreKey = "-"
numberTypeString = "number"
) )
var ( var (
@@ -37,7 +39,7 @@ var (
) )
type ( type (
// Unmarshaler is used to unmarshal with given tag key. // Unmarshaler is used to unmarshal with the given tag key.
Unmarshaler struct { Unmarshaler struct {
key string key string
opts unmarshalOptions opts unmarshalOptions
@@ -49,6 +51,7 @@ type (
unmarshalOptions struct { unmarshalOptions struct {
fillDefault bool fillDefault bool
fromString bool fromString bool
opaqueKeys bool
canonicalKey func(key string) string canonicalKey func(key string) string
} }
) )
@@ -66,13 +69,17 @@ func NewUnmarshaler(key string, opts ...UnmarshalOption) *Unmarshaler {
return &unmarshaler return &unmarshaler
} }
// UnmarshalKey unmarshals m into v with tag key. // UnmarshalKey unmarshals m into v with the tag key.
func UnmarshalKey(m map[string]any, v any) error { func UnmarshalKey(m map[string]any, v any) error {
return keyUnmarshaler.Unmarshal(m, v) return keyUnmarshaler.Unmarshal(m, v)
} }
// Unmarshal unmarshals m into v. // Unmarshal unmarshals m into v.
func (u *Unmarshaler) Unmarshal(i any, v any) error { func (u *Unmarshaler) Unmarshal(i, v any) error {
return u.unmarshal(i, v, "")
}
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v) valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr { if valueType.Kind() != reflect.Ptr {
return errValueNotSettable return errValueNotSettable
@@ -85,13 +92,13 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
return errTypeMismatch return errTypeMismatch
} }
return u.UnmarshalValuer(mapValuer(iv), v) return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any: case []any:
if elemType.Kind() != reflect.Slice { if elemType.Kind() != reflect.Slice {
return errTypeMismatch return errTypeMismatch
} }
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv) return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default: default:
return errUnsupportedType return errUnsupportedType
} }
@@ -99,17 +106,22 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
// UnmarshalValuer unmarshals m into v. // UnmarshalValuer unmarshals m into v.
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error { func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, "") return u.unmarshalValuer(simpleValuer{current: m}, v, "")
} }
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any) error { func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
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
} }
fieldKeyType := fieldType.Key() fieldKeyType := fieldType.Key()
fieldElemType := fieldType.Elem() fieldElemType := fieldType.Elem()
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue) targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue, fullName)
if err != nil { if err != nil {
return err return err
} }
@@ -143,14 +155,15 @@ 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) 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
} }
refValue := reflect.ValueOf(mapValue) refValue := reflect.ValueOf(mapValue)
if refValue.Kind() != reflect.Slice { if refValue.Kind() != reflect.Slice {
return errTypeMismatch return newTypeMismatchErrorWithHint(fullName, reflect.Slice.String(), refValue.Type().String())
} }
if refValue.IsNil() { if refValue.IsNil() {
return nil return nil
@@ -173,6 +186,8 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
} }
valid = true valid = true
sliceFullName := fmt.Sprintf("%s[%d]", fullName, i)
switch dereffedBaseKind { switch dereffedBaseKind {
case reflect.Struct: case reflect.Struct:
target := reflect.New(dereffedBaseType) target := reflect.New(dereffedBaseType)
@@ -181,17 +196,17 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
return errTypeMismatch return errTypeMismatch
} }
if err := u.Unmarshal(val, target.Interface()); err != nil { if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
return err return err
} }
SetValue(fieldType.Elem(), conv.Index(i), target.Elem()) SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice: case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue); err != nil { if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err return err
} }
default: default:
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue); err != nil { if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue, sliceFullName); err != nil {
return err return err
} }
} }
@@ -205,16 +220,16 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
} }
func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value, func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value,
mapValue any) error { mapValue any, fullName string) error {
var slice []any var slice []any
switch v := mapValue.(type) { switch v := mapValue.(type) {
case fmt.Stringer: case fmt.Stringer:
if err := jsonx.UnmarshalFromString(v.String(), &slice); err != nil { if err := jsonx.UnmarshalFromString(v.String(), &slice); err != nil {
return err return fmt.Errorf("fullName: `%s`, error: `%w`", fullName, err)
} }
case string: case string:
if err := jsonx.UnmarshalFromString(v, &slice); err != nil { if err := jsonx.UnmarshalFromString(v, &slice); err != nil {
return err return fmt.Errorf("fullName: `%s`, error: `%w`", fullName, err)
} }
default: default:
return errUnsupportedType return errUnsupportedType
@@ -225,7 +240,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice)) conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
for i := 0; i < len(slice); i++ { for i := 0; i < len(slice); i++ {
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i]); err != nil { if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i], fullName); err != nil {
return err return err
} }
} }
@@ -235,7 +250,11 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
} }
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int, func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
baseKind reflect.Kind, value any) error { baseKind reflect.Kind, value any, fullName string) error {
if value == nil {
return errNilSliceElement
}
ithVal := slice.Index(index) ithVal := slice.Index(index)
switch v := value.(type) { switch v := value.(type) {
case fmt.Stringer: case fmt.Stringer:
@@ -243,7 +262,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
case string: case string:
return setValueFromString(baseKind, ithVal, v) return setValueFromString(baseKind, ithVal, v)
case map[string]any: case map[string]any:
return u.fillMap(ithVal.Type(), ithVal, value) return u.fillMap(ithVal.Type(), ithVal, value, fullName)
default: default:
// don't need to consider the difference between int, int8, int16, int32, int64, // don't need to consider the difference between int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number. // uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
@@ -269,7 +288,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
} }
func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value, func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value,
defaultValue string) error { defaultValue, fullName string) error {
baseFieldType := Deref(derefedType.Elem()) baseFieldType := Deref(derefedType.Elem())
baseFieldKind := baseFieldType.Kind() baseFieldKind := baseFieldType.Kind()
defaultCacheLock.Lock() defaultCacheLock.Lock()
@@ -287,10 +306,37 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
defaultCacheLock.Unlock() defaultCacheLock.Unlock()
} }
return u.fillSlice(derefedType, value, slice) return u.fillSlice(derefedType, value, slice, fullName)
} }
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any) (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 {
@@ -309,11 +355,12 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
for _, key := range refValue.MapKeys() { for _, key := range refValue.MapKeys() {
keythValue := refValue.MapIndex(key) keythValue := refValue.MapIndex(key)
keythData := keythValue.Interface() keythData := keythValue.Interface()
mapFullName := fmt.Sprintf("%s[%s]", fullName, key.String())
switch dereffedElemKind { switch dereffedElemKind {
case reflect.Slice: case reflect.Slice:
target := reflect.New(dereffedElemType) target := reflect.New(dereffedElemType)
if err := u.fillSlice(elemType, target.Elem(), keythData); err != nil { if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
return emptyValue, err return emptyValue, err
} }
@@ -325,7 +372,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
} }
target := reflect.New(dereffedElemType) target := reflect.New(dereffedElemType)
if err := u.Unmarshal(keythMap, target.Interface()); err != nil { if err := u.unmarshal(keythMap, target.Interface(), mapFullName); err != nil {
return emptyValue, err return emptyValue, err
} }
@@ -336,7 +383,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
return emptyValue, errTypeMismatch return emptyValue, errTypeMismatch
} }
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap) innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap, mapFullName)
if err != nil { if err != nil {
return emptyValue, err return emptyValue, err
} }
@@ -381,6 +428,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)
@@ -410,6 +466,10 @@ func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Value
} }
} }
if u.opts.fillDefault {
return key, &options.fieldOptionsWithContext, nil
}
optsWithContext, err := options.toOptionsWithContext(key, m, fullName) optsWithContext, err := options.toOptionsWithContext(key, m, fullName)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
@@ -425,6 +485,10 @@ func (u *Unmarshaler) processAnonymousField(field reflect.StructField, value ref
return err return err
} }
if key == ignoreKey {
return nil
}
if options.optional() { if options.optional() {
return u.processAnonymousFieldOptional(field, value, key, m, fullName) return u.processAnonymousFieldOptional(field, value, key, m, fullName)
} }
@@ -483,7 +547,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
return err return err
} }
_, hasValue := getValue(m, fieldKey) _, hasValue := getValue(m, fieldKey, u.opts.opaqueKeys)
if hasValue { if hasValue {
if !filled { if !filled {
filled = true filled = true
@@ -541,15 +605,17 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
parent: vp.parent, parent: vp.parent,
}, fullName) }, fullName)
case typeKind == reflect.Slice && valueKind == reflect.Slice: case typeKind == reflect.Slice && valueKind == reflect.Slice:
return u.fillSlice(fieldType, value, mapValue) return u.fillSlice(fieldType, value, mapValue, fullName)
case valueKind == reflect.Map && typeKind == reflect.Map: case valueKind == reflect.Map && typeKind == reflect.Map:
return u.fillMap(fieldType, value, mapValue) return u.fillMap(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && typeKind == reflect.Map: case valueKind == reflect.String && typeKind == reflect.Map:
return u.fillMapFromString(value, mapValue) return u.fillMapFromString(value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Slice: case valueKind == reflect.String && typeKind == reflect.Slice:
return u.fillSliceFromString(fieldType, value, mapValue) 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)
} }
@@ -592,25 +658,28 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
target := reflect.New(Deref(fieldType)).Elem() target := reflect.New(Deref(fieldType)).Elem()
switch typeKind { switch typeKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
iValue, err := v.Int64() reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if err := setValueFromString(typeKind, target, v.String()); err != nil {
return err
}
case reflect.Float32:
fValue, err := v.Float64()
if err != nil { if err != nil {
return err return err
} }
target.SetInt(iValue) // if the value is a pointer, we need to check overflow with the pointer's value.
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: derefedValue := value
iValue, err := v.Int64() for derefedValue.Type().Kind() == reflect.Ptr {
if err != nil { derefedValue = derefedValue.Elem()
return err }
if derefedValue.CanFloat() && derefedValue.OverflowFloat(fValue) {
return fmt.Errorf("parsing %q as float32: value out of range", v.String())
} }
if iValue < 0 { target.SetFloat(fValue)
return fmt.Errorf("unmarshal %q with bad value %q", fullName, v.String()) case reflect.Float64:
}
target.SetUint(uint64(iValue))
case reflect.Float32, reflect.Float64:
fValue, err := v.Float64() fValue, err := v.Float64()
if err != nil { if err != nil {
return err return err
@@ -618,7 +687,7 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
target.SetFloat(fValue) target.SetFloat(fValue)
default: default:
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), value.Type().String()) return newTypeMismatchErrorWithHint(fullName, typeKind.String(), numberTypeString)
} }
SetValue(fieldType, value, target) SetValue(fieldType, value, target)
@@ -712,6 +781,10 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
return err return err
} }
if key == ignoreKey {
return nil
}
fullName = join(fullName, key) fullName = join(fullName, key)
if opts != nil && len(opts.EnvVar) > 0 { if opts != nil && len(opts.EnvVar) > 0 {
envVal := proc.Env(opts.EnvVar) envVal := proc.Env(opts.EnvVar)
@@ -726,7 +799,7 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
} }
valuer := createValuer(m, opts) valuer := createValuer(m, opts)
mapValue, hasValue := getValue(valuer, canonicalKey) mapValue, hasValue := getValue(valuer, canonicalKey, u.opts.opaqueKeys)
// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault. // When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
if u.opts.fillDefault { if u.opts.fillDefault {
@@ -819,7 +892,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
switch fieldKind { switch fieldKind {
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
return u.fillSliceWithDefault(derefedType, value, defaultValue) return u.fillSliceWithDefault(derefedType, value, defaultValue, fullName)
default: default:
return setValueFromString(fieldKind, value, defaultValue) return setValueFromString(fieldKind, value, defaultValue)
} }
@@ -867,7 +940,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error { func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if err := ValidatePtr(&rv); err != nil { if err := ValidatePtr(rv); err != nil {
return err return err
} }
@@ -889,11 +962,6 @@ func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName
typeField := baseType.Field(i) typeField := baseType.Field(i)
valueField := valElem.Field(i) valueField := valElem.Field(i)
if err := u.processField(typeField, valueField, m, fullName); err != nil { if err := u.processField(typeField, valueField, m, fullName); err != nil {
if len(fullName) > 0 {
err = fmt.Errorf("%w, fullName: %s, field: %s, type: %s",
err, fullName, typeField.Name, valueField.Type().Name())
}
return err return err
} }
} }
@@ -922,6 +990,14 @@ func WithDefault() UnmarshalOption {
} }
} }
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.opaqueKeys = true
}
}
func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent { func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
if opts.inherit() { if opts.inherit() {
return recursiveValuer{ return recursiveValuer{
@@ -999,8 +1075,8 @@ func fillWithSameType(fieldType reflect.Type, value reflect.Value, mapValue any,
} }
// getValue gets the value for the specific key, the key can be in the format of parentKey.childKey // getValue gets the value for the specific key, the key can be in the format of parentKey.childKey
func getValue(m valuerWithParent, key string) (any, bool) { func getValue(m valuerWithParent, key string, opaque bool) (any, bool) {
keys := readKeys(key) keys := readKeys(key, opaque)
return getValueWithChainedKeys(m, keys) return getValueWithChainedKeys(m, keys)
} }
@@ -1059,7 +1135,11 @@ func newTypeMismatchErrorWithHint(name, expectType, actualType string) error {
name, expectType, actualType) name, expectType, actualType)
} }
func readKeys(key string) []string { func readKeys(key string, opaque bool) []string {
if opaque {
return []string{key}
}
cacheKeysLock.Lock() cacheKeysLock.Lock()
keys, ok := cacheKeys[key] keys, ok := cacheKeys[key]
cacheKeysLock.Unlock() cacheKeysLock.Unlock()

File diff suppressed because it is too large Load Diff

View File

@@ -30,11 +30,13 @@ const (
leftSquareBracket = '[' leftSquareBracket = '['
rightSquareBracket = ']' rightSquareBracket = ']'
segmentSeparator = ',' segmentSeparator = ','
intSize = 32 << (^uint(0) >> 63) // 32 or 64
) )
var ( var (
errUnsupportedType = errors.New("unsupported type on setting field value") errUnsupportedType = errors.New("unsupported type on setting field value")
errNumberRange = errors.New("wrong number range setting") errNumberRange = errors.New("wrong number range setting")
errNilSliceElement = errors.New("null element for slice")
optionsCache = make(map[string]optionsCacheValue) optionsCache = make(map[string]optionsCacheValue)
cacheLock sync.RWMutex cacheLock sync.RWMutex
structRequiredCache = make(map[reflect.Type]requiredCacheValue) structRequiredCache = make(map[reflect.Type]requiredCacheValue)
@@ -79,7 +81,7 @@ func SetMapIndexValue(tp reflect.Type, value, key, target reflect.Value) {
} }
// ValidatePtr validates v if it's a valid pointer. // ValidatePtr validates v if it's a valid pointer.
func ValidatePtr(v *reflect.Value) error { func ValidatePtr(v reflect.Value) error {
// sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr, // sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr,
// panic otherwise // panic otherwise
if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() { if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() {
@@ -100,27 +102,30 @@ func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
default: default:
return false, errTypeMismatch return false, errTypeMismatch
} }
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int:
intValue, err := strconv.ParseInt(str, 10, 64) return strconv.ParseInt(str, 10, intSize)
if err != nil { case reflect.Int8:
return 0, fmt.Errorf("the value %q cannot parsed as int", str) return strconv.ParseInt(str, 10, 8)
} case reflect.Int16:
return strconv.ParseInt(str, 10, 16)
return intValue, nil case reflect.Int32:
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.ParseInt(str, 10, 32)
uintValue, err := strconv.ParseUint(str, 10, 64) case reflect.Int64:
if err != nil { return strconv.ParseInt(str, 10, 64)
return 0, fmt.Errorf("the value %q cannot parsed as uint", str) case reflect.Uint:
} return strconv.ParseUint(str, 10, intSize)
case reflect.Uint8:
return uintValue, nil return strconv.ParseUint(str, 10, 8)
case reflect.Float32, reflect.Float64: case reflect.Uint16:
floatValue, err := strconv.ParseFloat(str, 64) return strconv.ParseUint(str, 10, 16)
if err != nil { case reflect.Uint32:
return 0, fmt.Errorf("the value %q cannot parsed as float", str) return strconv.ParseUint(str, 10, 32)
} case reflect.Uint64:
return strconv.ParseUint(str, 10, 64)
return floatValue, nil case reflect.Float32:
return strconv.ParseFloat(str, 32)
case reflect.Float64:
return strconv.ParseFloat(str, 64)
case reflect.String: case reflect.String:
return str, nil return str, nil
default: default:
@@ -411,7 +416,7 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
} }
// parseOptions parses the given options in tag. // parseOptions parses the given options in tag.
// for example: `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"` // for example, `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"`
func parseOptions(val string) []string { func parseOptions(val string) []string {
if len(val) == 0 { if len(val) == 0 {
return nil return nil
@@ -486,19 +491,22 @@ func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) err
switch kind { switch kind {
case reflect.Bool: case reflect.Bool:
value.SetBool(v.(bool)) value.SetBool(v.(bool))
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
value.SetInt(v.(int64)) value.SetInt(v.(int64))
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
value.SetUint(v.(uint64)) value.SetUint(v.(uint64))
return nil
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
value.SetFloat(v.(float64)) value.SetFloat(v.(float64))
return nil
case reflect.String: case reflect.String:
value.SetString(v.(string)) value.SetString(v.(string))
return nil
default: default:
return errUnsupportedType return errUnsupportedType
} }
return nil
} }
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error { func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error {
@@ -575,7 +583,8 @@ func usingDifferentKeys(key string, field reflect.StructField) bool {
return false return false
} }
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string, opts *fieldOptionsWithContext) error { func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string,
opts *fieldOptionsWithContext) error {
if !value.CanSet() { if !value.CanSet() {
return errValueNotSettable return errValueNotSettable
} }

View File

@@ -218,25 +218,25 @@ func TestParseSegments(t *testing.T) {
func TestValidatePtrWithNonPtr(t *testing.T) { func TestValidatePtrWithNonPtr(t *testing.T) {
var foo string var foo string
rve := reflect.ValueOf(foo) rve := reflect.ValueOf(foo)
assert.NotNil(t, ValidatePtr(&rve)) assert.NotNil(t, ValidatePtr(rve))
} }
func TestValidatePtrWithPtr(t *testing.T) { func TestValidatePtrWithPtr(t *testing.T) {
var foo string var foo string
rve := reflect.ValueOf(&foo) rve := reflect.ValueOf(&foo)
assert.Nil(t, ValidatePtr(&rve)) assert.Nil(t, ValidatePtr(rve))
} }
func TestValidatePtrWithNilPtr(t *testing.T) { func TestValidatePtrWithNilPtr(t *testing.T) {
var foo *string var foo *string
rve := reflect.ValueOf(foo) rve := reflect.ValueOf(foo)
assert.NotNil(t, ValidatePtr(&rve)) assert.NotNil(t, ValidatePtr(rve))
} }
func TestValidatePtrWithZeroValue(t *testing.T) { func TestValidatePtrWithZeroValue(t *testing.T) {
var s string var s string
e := reflect.Zero(reflect.TypeOf(s)) e := reflect.Zero(reflect.TypeOf(s))
assert.NotNil(t, ValidatePtr(&e)) assert.NotNil(t, ValidatePtr(e))
} }
func TestSetValueNotSettable(t *testing.T) { func TestSetValueNotSettable(t *testing.T) {

View File

@@ -26,9 +26,9 @@ type (
parent valuerWithParent parent valuerWithParent
} }
// mapValuer is a type for map to meet the Valuer interface. // mapValuer is a type for the map to meet the Valuer interface.
mapValuer map[string]any mapValuer map[string]any
// simpleValuer is a type to get value from current node. // simpleValuer is a type to get value from the current node.
simpleValuer node simpleValuer node
// recursiveValuer is a type to get the value recursively from current and parent nodes. // recursiveValuer is a type to get the value recursively from current and parent nodes.
recursiveValuer node recursiveValuer node

View File

@@ -1011,6 +1011,15 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
assert.Equal(t, rune(3), v.Machine["node3"]) assert.Equal(t, rune(3), v.Machine["node3"])
} }
func TestUnmarshalYamlStringOfInt(t *testing.T) {
text := `password: 123456`
var v struct {
Password string `json:"password"`
}
reader := strings.NewReader(text)
assert.Error(t, UnmarshalYamlReader(reader, &v))
}
func TestUnmarshalYamlBadInput(t *testing.T) { func TestUnmarshalYamlBadInput(t *testing.T) {
var v struct { var v struct {
Any string Any string

View File

@@ -15,3 +15,17 @@ func TestCalcEntropy(t *testing.T) {
} }
assert.True(t, CalcEntropy(m) > .99) assert.True(t, CalcEntropy(m) > .99)
} }
func TestCalcEmptyEntropy(t *testing.T) {
m := make(map[any]int)
assert.Equal(t, float64(1), CalcEntropy(m))
}
func TestCalcDiffEntropy(t *testing.T) {
const total = 1000
m := make(map[any]int, total)
for i := 0; i < total; i++ {
m[i] = i
}
assert.True(t, CalcEntropy(m) < .99)
}

34
core/mathx/range.go Normal file
View File

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

513
core/mathx/range_test.go Normal file
View File

@@ -0,0 +1,513 @@
package mathx
import "testing"
func TestAtLeast(t *testing.T) {
t.Run("test int", func(t *testing.T) {
if got := AtLeast(10, 5); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(3, 5); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(5, 5); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test int8", func(t *testing.T) {
if got := AtLeast(int8(10), int8(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(int8(3), int8(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(int8(5), int8(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test int16", func(t *testing.T) {
if got := AtLeast(int16(10), int16(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(int16(3), int16(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(int16(5), int16(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test int32", func(t *testing.T) {
if got := AtLeast(int32(10), int32(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(int32(3), int32(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(int32(5), int32(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test int64", func(t *testing.T) {
if got := AtLeast(int64(10), int64(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(int64(3), int64(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(int64(5), int64(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test uint", func(t *testing.T) {
if got := AtLeast(uint(10), uint(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(uint(3), uint(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(uint(5), uint(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test uint8", func(t *testing.T) {
if got := AtLeast(uint8(10), uint8(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(uint8(3), uint8(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(uint8(5), uint8(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test uint16", func(t *testing.T) {
if got := AtLeast(uint16(10), uint16(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(uint16(3), uint16(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(uint16(5), uint16(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test uint32", func(t *testing.T) {
if got := AtLeast(uint32(10), uint32(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(uint32(3), uint32(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(uint32(5), uint32(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test uint64", func(t *testing.T) {
if got := AtLeast(uint64(10), uint64(5)); got != 10 {
t.Errorf("AtLeast() = %v, want 10", got)
}
if got := AtLeast(uint64(3), uint64(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
if got := AtLeast(uint64(5), uint64(5)); got != 5 {
t.Errorf("AtLeast() = %v, want 5", got)
}
})
t.Run("test float32", func(t *testing.T) {
if got := AtLeast(float32(10.0), float32(5.0)); got != 10.0 {
t.Errorf("AtLeast() = %v, want 10.0", got)
}
if got := AtLeast(float32(3.0), float32(5.0)); got != 5.0 {
t.Errorf("AtLeast() = %v, want 5.0", got)
}
if got := AtLeast(float32(5.0), float32(5.0)); got != 5.0 {
t.Errorf("AtLeast() = %v, want 5.0", got)
}
})
t.Run("test float64", func(t *testing.T) {
if got := AtLeast(10.0, 5.0); got != 10.0 {
t.Errorf("AtLeast() = %v, want 10.0", got)
}
if got := AtLeast(3.0, 5.0); got != 5.0 {
t.Errorf("AtLeast() = %v, want 5.0", got)
}
if got := AtLeast(5.0, 5.0); got != 5.0 {
t.Errorf("AtLeast() = %v, want 5.0", got)
}
})
}
func TestAtMost(t *testing.T) {
t.Run("test int", func(t *testing.T) {
if got := AtMost(10, 5); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(3, 5); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(5, 5); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test int8", func(t *testing.T) {
if got := AtMost(int8(10), int8(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(int8(3), int8(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(int8(5), int8(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test int16", func(t *testing.T) {
if got := AtMost(int16(10), int16(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(int16(3), int16(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(int16(5), int16(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test int32", func(t *testing.T) {
if got := AtMost(int32(10), int32(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(int32(3), int32(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(int32(5), int32(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test int64", func(t *testing.T) {
if got := AtMost(int64(10), int64(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(int64(3), int64(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(int64(5), int64(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test uint", func(t *testing.T) {
if got := AtMost(uint(10), uint(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(uint(3), uint(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(uint(5), uint(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test uint8", func(t *testing.T) {
if got := AtMost(uint8(10), uint8(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(uint8(3), uint8(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(uint8(5), uint8(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test uint16", func(t *testing.T) {
if got := AtMost(uint16(10), uint16(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(uint16(3), uint16(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(uint16(5), uint16(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test uint32", func(t *testing.T) {
if got := AtMost(uint32(10), uint32(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(uint32(3), uint32(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(uint32(5), uint32(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test uint64", func(t *testing.T) {
if got := AtMost(uint64(10), uint64(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
if got := AtMost(uint64(3), uint64(5)); got != 3 {
t.Errorf("AtMost() = %v, want 3", got)
}
if got := AtMost(uint64(5), uint64(5)); got != 5 {
t.Errorf("AtMost() = %v, want 5", got)
}
})
t.Run("test float32", func(t *testing.T) {
if got := AtMost(float32(10.0), float32(5.0)); got != 5.0 {
t.Errorf("AtMost() = %v, want 5.0", got)
}
if got := AtMost(float32(3.0), float32(5.0)); got != 3.0 {
t.Errorf("AtMost() = %v, want 3.0", got)
}
if got := AtMost(float32(5.0), float32(5.0)); got != 5.0 {
t.Errorf("AtMost() = %v, want 5.0", got)
}
})
t.Run("test float64", func(t *testing.T) {
if got := AtMost(10.0, 5.0); got != 5.0 {
t.Errorf("AtMost() = %v, want 5.0", got)
}
if got := AtMost(3.0, 5.0); got != 3.0 {
t.Errorf("AtMost() = %v, want 3.0", got)
}
if got := AtMost(5.0, 5.0); got != 5.0 {
t.Errorf("AtMost() = %v, want 5.0", got)
}
})
}
func TestBetween(t *testing.T) {
t.Run("test int", func(t *testing.T) {
if got := Between(10, 5, 15); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(3, 5, 15); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(20, 5, 15); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(5, 5, 15); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(15, 5, 15); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test int8", func(t *testing.T) {
if got := Between(int8(10), int8(5), int8(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(int8(3), int8(5), int8(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int8(20), int8(5), int8(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(int8(5), int8(5), int8(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int8(15), int8(5), int8(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test int16", func(t *testing.T) {
if got := Between(int16(10), int16(5), int16(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(int16(3), int16(5), int16(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int16(20), int16(5), int16(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(int16(5), int16(5), int16(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int16(15), int16(5), int16(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test int32", func(t *testing.T) {
if got := Between(int32(10), int32(5), int32(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(int32(3), int32(5), int32(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int32(20), int32(5), int32(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(int32(5), int32(5), int32(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int32(15), int32(5), int32(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test int64", func(t *testing.T) {
if got := Between(int64(10), int64(5), int64(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(int64(3), int64(5), int64(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int64(20), int64(5), int64(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(int64(5), int64(5), int64(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(int64(15), int64(5), int64(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test uint", func(t *testing.T) {
if got := Between(uint(10), uint(5), uint(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(uint(3), uint(5), uint(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint(20), uint(5), uint(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(uint(5), uint(5), uint(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint(15), uint(5), uint(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test uint8", func(t *testing.T) {
if got := Between(uint8(10), uint8(5), uint8(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(uint8(3), uint8(5), uint8(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint8(20), uint8(5), uint8(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(uint8(5), uint8(5), uint8(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint8(15), uint8(5), uint8(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test uint16", func(t *testing.T) {
if got := Between(uint16(10), uint16(5), uint16(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(uint16(3), uint16(5), uint16(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint16(20), uint16(5), uint16(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(uint16(5), uint16(5), uint16(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint16(15), uint16(5), uint16(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test uint32", func(t *testing.T) {
if got := Between(uint32(10), uint32(5), uint32(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(uint32(3), uint32(5), uint32(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint32(20), uint32(5), uint32(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(uint32(5), uint32(5), uint32(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint32(15), uint32(5), uint32(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test uint64", func(t *testing.T) {
if got := Between(uint64(10), uint64(5), uint64(15)); got != 10 {
t.Errorf("Between() = %v, want 10", got)
}
if got := Between(uint64(3), uint64(5), uint64(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint64(20), uint64(5), uint64(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
if got := Between(uint64(5), uint64(5), uint64(15)); got != 5 {
t.Errorf("Between() = %v, want 5", got)
}
if got := Between(uint64(15), uint64(5), uint64(15)); got != 15 {
t.Errorf("Between() = %v, want 15", got)
}
})
t.Run("test float32", func(t *testing.T) {
if got := Between(float32(10.0), float32(5.0), float32(15.0)); got != 10.0 {
t.Errorf("Between() = %v, want 10.0", got)
}
if got := Between(float32(3.0), float32(5.0), float32(15.0)); got != 5.0 {
t.Errorf("Between() = %v, want 5.0", got)
}
if got := Between(float32(20.0), float32(5.0), float32(15.0)); got != 15.0 {
t.Errorf("Between() = %v, want 15.0", got)
}
if got := Between(float32(5.0), float32(5.0), float32(15.0)); got != 5.0 {
t.Errorf("Between() = %v, want 5.0", got)
}
if got := Between(float32(15.0), float32(5.0), float32(15.0)); got != 15.0 {
t.Errorf("Between() = %v, want 15.0", got)
}
})
t.Run("test float64", func(t *testing.T) {
if got := Between(10.0, 5.0, 15.0); got != 10.0 {
t.Errorf("Between() = %v, want 10.0", got)
}
if got := Between(3.0, 5.0, 15.0); got != 5.0 {
t.Errorf("Between() = %v, want 5.0", got)
}
if got := Between(20.0, 5.0, 15.0); got != 15.0 {
t.Errorf("Between() = %v, want 15.0", got)
}
if got := Between(5.0, 5.0, 15.0); got != 5.0 {
t.Errorf("Between() = %v, want 5.0", got)
}
if got := Between(15.0, 5.0, 15.0); got != 15.0 {
t.Errorf("Between() = %v, want 15.0", got)
}
})
}

View File

@@ -3,7 +3,6 @@ package metric
import ( import (
prom "github.com/prometheus/client_golang/prometheus" prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
) )
type ( type (
@@ -47,20 +46,16 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
return cv return cv
} }
func (cv *promCounterVec) Inc(labels ...string) { func (cv *promCounterVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() { update(func() {
return cv.counter.WithLabelValues(labels...).Add(v)
} })
cv.counter.WithLabelValues(labels...).Inc()
} }
func (cv *promCounterVec) Add(v float64, labels ...string) { func (cv *promCounterVec) Inc(labels ...string) {
if !prometheus.Enabled() { update(func() {
return cv.counter.WithLabelValues(labels...).Inc()
} })
cv.counter.WithLabelValues(labels...).Add(v)
} }
func (cv *promCounterVec) close() bool { func (cv *promCounterVec) close() bool {

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