mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-27 00:25:29 +08:00
Compare commits
571 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eff777b62 | ||
|
|
cafbf535f7 | ||
|
|
6edfce63e3 | ||
|
|
cdb0098b18 | ||
|
|
620c7f9693 | ||
|
|
dba444a382 | ||
|
|
b24fb3ebf7 | ||
|
|
967f0926eb | ||
|
|
e68c683df9 | ||
|
|
247985a065 | ||
|
|
80573af0d8 | ||
|
|
c0394b631a | ||
|
|
68d1aba377 | ||
|
|
3315e60272 | ||
|
|
327ef73700 | ||
|
|
eb11521655 | ||
|
|
4c37545e55 | ||
|
|
2f47c1fba4 | ||
|
|
16d54d0ace | ||
|
|
9925bcbf99 | ||
|
|
38a5ecb796 | ||
|
|
af78fc7c5f | ||
|
|
790302b486 | ||
|
|
6a0672b801 | ||
|
|
560c61612c | ||
|
|
6a988dc4a9 | ||
|
|
15842c3c7a | ||
|
|
f2914a74df | ||
|
|
f113d512e8 | ||
|
|
7a4818da59 | ||
|
|
48d0709ca6 | ||
|
|
f747585518 | ||
|
|
507ff96546 | ||
|
|
651eabb4c6 | ||
|
|
e6b4372056 | ||
|
|
24073969a1 | ||
|
|
ca797ed22c | ||
|
|
e347d3f8f8 | ||
|
|
396393b336 | ||
|
|
1f0531b254 | ||
|
|
77fb271a06 | ||
|
|
af7cf79963 | ||
|
|
7926d396d7 | ||
|
|
080cd3df84 | ||
|
|
c4e1a6a2d8 | ||
|
|
4e71e95e44 | ||
|
|
84db9bcd15 | ||
|
|
b28f79ac11 | ||
|
|
e134e77b2b | ||
|
|
f669d84ce8 | ||
|
|
9213b8ac27 | ||
|
|
ae09d0e56d | ||
|
|
0bc4206d08 | ||
|
|
39ce17bfd2 | ||
|
|
d415ba39e2 | ||
|
|
c71829c8de | ||
|
|
a32f6d7642 | ||
|
|
64e8c94198 | ||
|
|
7d05a4bc93 | ||
|
|
44504e8df7 | ||
|
|
114311e51b | ||
|
|
4307ce45fc | ||
|
|
37b54d1fc7 | ||
|
|
00e0db5def | ||
|
|
cbcacf31c1 | ||
|
|
238c92aaa9 | ||
|
|
520d2a2075 | ||
|
|
1023800b02 | ||
|
|
030c859171 | ||
|
|
e6d1b47a43 | ||
|
|
6138f85470 | ||
|
|
bf883101d7 | ||
|
|
33011c7ed1 | ||
|
|
17d98f69e0 | ||
|
|
b650c8c425 | ||
|
|
3d931d7030 | ||
|
|
68da9ed51a | ||
|
|
b25c45b352 | ||
|
|
f05234a967 | ||
|
|
12071d17b4 | ||
|
|
11c47d23df | ||
|
|
024f285f86 | ||
|
|
fa4674611a | ||
|
|
730c3c5246 | ||
|
|
2c9310ac3a | ||
|
|
74ba0bcd50 | ||
|
|
5f4190b6c6 | ||
|
|
e1787b4ccb | ||
|
|
4ac8b492ef | ||
|
|
cdd068575c | ||
|
|
e89e2d8a75 | ||
|
|
acd2b94bd9 | ||
|
|
6a0c8047f4 | ||
|
|
cfe03ea9e1 | ||
|
|
48d21ef8ad | ||
|
|
28a001c5f9 | ||
|
|
22a41cacc7 | ||
|
|
fcc246933c | ||
|
|
5c3679ffe7 | ||
|
|
eaa01ccb9f | ||
|
|
b8206fb46a | ||
|
|
1c3876810e | ||
|
|
1d9159ea39 | ||
|
|
2159d112c3 | ||
|
|
f57874a51f | ||
|
|
8625864d43 | ||
|
|
8f9ba3ec11 | ||
|
|
a1d9bc08f0 | ||
|
|
b9d7f1cc77 | ||
|
|
6700910f64 | ||
|
|
9c4ed394a7 | ||
|
|
fd07a9c6e4 | ||
|
|
b8c239630c | ||
|
|
672ea55736 | ||
|
|
f7097866bf | ||
|
|
796b2bd1b0 | ||
|
|
e1e5fb2071 | ||
|
|
89ecb50005 | ||
|
|
dbed1ea042 | ||
|
|
ad291daf78 | ||
|
|
13746a3706 | ||
|
|
f03b13f632 | ||
|
|
f6f64b1286 | ||
|
|
300a415f5d | ||
|
|
c5de546f8a | ||
|
|
cad243905f | ||
|
|
7c8f41d577 | ||
|
|
cbd118d55f | ||
|
|
9d2a1b8b0a | ||
|
|
f6ada979aa | ||
|
|
53a74759a5 | ||
|
|
1940f7bd58 | ||
|
|
18cb3141ba | ||
|
|
f822c9a94f | ||
|
|
1a3dc75874 | ||
|
|
796dd5b6e2 | ||
|
|
94e476ade7 | ||
|
|
2a74996e1b | ||
|
|
f52af1ebf9 | ||
|
|
24450f18bb | ||
|
|
f1a45d8a23 | ||
|
|
9aebba1566 | ||
|
|
4998479f9a | ||
|
|
873d1351ee | ||
|
|
afcbca8f24 | ||
|
|
5b8126c2cf | ||
|
|
4dfaf35151 | ||
|
|
00cd77c92b | ||
|
|
2145a7a93c | ||
|
|
d5302f2dbe | ||
|
|
6181594bc8 | ||
|
|
11c10e51ff | ||
|
|
3f03126d27 | ||
|
|
d43adc2823 | ||
|
|
656222b572 | ||
|
|
077b6072fa | ||
|
|
0cafb1164b | ||
|
|
90afa08367 | ||
|
|
c92f788292 | ||
|
|
e94be9b302 | ||
|
|
e713d9013d | ||
|
|
24d6150073 | ||
|
|
44cddec5c3 | ||
|
|
47d13e5ef8 | ||
|
|
896e1a2abb | ||
|
|
075817a8dd | ||
|
|
29400f6814 | ||
|
|
34f536264f | ||
|
|
9d9c7e0fe0 | ||
|
|
e220d3a4cb | ||
|
|
193dcf90bc | ||
|
|
03756c9166 | ||
|
|
c1f12c5784 | ||
|
|
2883111af5 | ||
|
|
2758c4e842 | ||
|
|
4196ddb3e3 | ||
|
|
e24d797226 | ||
|
|
d4349fa958 | ||
|
|
da2c14d45f | ||
|
|
64e3aeda55 | ||
|
|
dedba17219 | ||
|
|
c6348b9855 | ||
|
|
8689a6247e | ||
|
|
ff6ee25d23 | ||
|
|
5213243bbb | ||
|
|
2588a36555 | ||
|
|
c2421beb25 | ||
|
|
dfe8a81c76 | ||
|
|
ee643a945e | ||
|
|
eeda6efae7 | ||
|
|
caf0e64beb | ||
|
|
0e61303cb0 | ||
|
|
f651d7cf6c | ||
|
|
05da2c560b | ||
|
|
8ae0f287d6 | ||
|
|
8f7aff558f | ||
|
|
6e08d478fe | ||
|
|
944ac383d2 | ||
|
|
0eec33f14b | ||
|
|
9de04ee035 | ||
|
|
cf5b080fbe | ||
|
|
4a14164be1 | ||
|
|
5dd6f2a43a | ||
|
|
a00c956776 | ||
|
|
c02fb3acab | ||
|
|
9f8455ddb3 | ||
|
|
775b105ab2 | ||
|
|
ec86f22cd6 | ||
|
|
e776b5d8ab | ||
|
|
2026d4410b | ||
|
|
f8437e6364 | ||
|
|
bd2033eb35 | ||
|
|
fed835bc25 | ||
|
|
c9cbd74bf3 | ||
|
|
27ea106293 | ||
|
|
657923b9d5 | ||
|
|
8dbec6a800 | ||
|
|
490559434a | ||
|
|
4a62d084a9 | ||
|
|
2f9b6cf8ec | ||
|
|
01bbc78bac | ||
|
|
a012a9138f | ||
|
|
4ec9cac82b | ||
|
|
8d9746e794 | ||
|
|
8f83705199 | ||
|
|
f1ed7bd75d | ||
|
|
7a20608756 | ||
|
|
5cfff95e95 | ||
|
|
1e1cc1a0d9 | ||
|
|
0a1440a839 | ||
|
|
23980d29c3 | ||
|
|
424119d796 | ||
|
|
97c7835d9e | ||
|
|
7954ad3759 | ||
|
|
e8c9b0ddf8 | ||
|
|
70112e59cb | ||
|
|
7ba5ced2d9 | ||
|
|
962b36d745 | ||
|
|
57060cc6d7 | ||
|
|
e0c16059d9 | ||
|
|
a0d954dfab | ||
|
|
a5ece25c07 | ||
|
|
0cac41a38b | ||
|
|
f10084a3f5 | ||
|
|
040fee5669 | ||
|
|
42b3bae65a | ||
|
|
7c730b97d8 | ||
|
|
057bae92ab | ||
|
|
74331a45c9 | ||
|
|
9d551d507f | ||
|
|
02dd81c05c | ||
|
|
3095ba2b1f | ||
|
|
2afa60132c | ||
|
|
e71ed7294b | ||
|
|
95822281bf | ||
|
|
588e10daef | ||
|
|
62ba01120e | ||
|
|
527de1c50e | ||
|
|
abfe62a2d7 | ||
|
|
36f4cf97ff | ||
|
|
b3cd8a32ed | ||
|
|
a9d27cda8a | ||
|
|
04116f647d | ||
|
|
a8ccda0c06 | ||
|
|
bfddb9dae4 | ||
|
|
b337ae36e5 | ||
|
|
5e5123caa3 | ||
|
|
d371ab5479 | ||
|
|
1b9b61f505 | ||
|
|
e1f15efb3b | ||
|
|
1540bdc4c9 | ||
|
|
95b32b5779 | ||
|
|
815a4f7eed | ||
|
|
4b0bacc9c6 | ||
|
|
e9dc96af17 | ||
|
|
62c88a84d1 | ||
|
|
36088ea0d4 | ||
|
|
164f5aa86c | ||
|
|
07d07cdd23 | ||
|
|
0efe99af66 | ||
|
|
927f8bc821 | ||
|
|
2a7ada993b | ||
|
|
682460c1c8 | ||
|
|
a66ae0d4c4 | ||
|
|
d1f24ab70f | ||
|
|
d0983948b5 | ||
|
|
3343fc2cdb | ||
|
|
3866b5741a | ||
|
|
5fbe8ff5c4 | ||
|
|
6f763f71f9 | ||
|
|
80377f18e7 | ||
|
|
8690859c7d | ||
|
|
d744038198 | ||
|
|
58ad8cac8a | ||
|
|
74886a151e | ||
|
|
c5eda1f155 | ||
|
|
b5b7c054ca | ||
|
|
6c8073b691 | ||
|
|
64d430d424 | ||
|
|
f138cc792e | ||
|
|
b20ec8aedb | ||
|
|
a53254fa91 | ||
|
|
08563482e5 | ||
|
|
968727412d | ||
|
|
6f3d094eba | ||
|
|
2d3ebb9b62 | ||
|
|
8c0bb27136 | ||
|
|
cf987295df | ||
|
|
8c92b3af7d | ||
|
|
5dd9342703 | ||
|
|
3ef59f6a71 | ||
|
|
f12802abc7 | ||
|
|
6f0fe67804 | ||
|
|
f44f0e7e62 | ||
|
|
cdd95296db | ||
|
|
3e794cf991 | ||
|
|
bbce95e7e1 | ||
|
|
0449450c64 | ||
|
|
9f9a12ea57 | ||
|
|
cc2a7e97f9 | ||
|
|
09d7af76af | ||
|
|
c233a66601 | ||
|
|
94fa12560c | ||
|
|
7d90f906f5 | ||
|
|
f372b98d96 | ||
|
|
459d3025c5 | ||
|
|
e9e55125a9 | ||
|
|
159ecb7386 | ||
|
|
69bb746a1d | ||
|
|
d184f96b13 | ||
|
|
c7dacb0146 | ||
|
|
2207477b60 | ||
|
|
105ab590ff | ||
|
|
2f4c58ed73 | ||
|
|
1631aa02ad | ||
|
|
4df10eef5d | ||
|
|
3d552ea7a8 | ||
|
|
74b87ac9fd | ||
|
|
ba1d6e3664 | ||
|
|
2096cd5749 | ||
|
|
2eb2fa26f6 | ||
|
|
bc4187ca90 | ||
|
|
b7be25b98b | ||
|
|
dd01695d45 | ||
|
|
25821bdee6 | ||
|
|
b624b966f0 | ||
|
|
df96262235 | ||
|
|
2629636f64 | ||
|
|
708ad207d7 | ||
|
|
b53ba76a99 | ||
|
|
be7f93924a | ||
|
|
45be48a4ee | ||
|
|
e08ba2fee8 | ||
|
|
a5d2b971a1 | ||
|
|
9763c8b143 | ||
|
|
4e3f1776dc | ||
|
|
e38036cea2 | ||
|
|
8e97c5819f | ||
|
|
0ee44c7064 | ||
|
|
a1bacd3fc8 | ||
|
|
c98d5fdaf4 | ||
|
|
2ee43b41b8 | ||
|
|
8367af3416 | ||
|
|
03b6e377d7 | ||
|
|
ec41880476 | ||
|
|
5263805b3b | ||
|
|
a7363f0c21 | ||
|
|
52e5d85221 | ||
|
|
88aab8f635 | ||
|
|
1f63cbe9c6 | ||
|
|
0dfaf135dd | ||
|
|
914bcdcf2b | ||
|
|
e38cb0118d | ||
|
|
cb8161c799 | ||
|
|
c4dac2095f | ||
|
|
25a807afb2 | ||
|
|
6be37ad533 | ||
|
|
28cb2c5804 | ||
|
|
0f1d4c6bca | ||
|
|
bfe8335cb2 | ||
|
|
3c10ce0115 | ||
|
|
1303e0fe6f | ||
|
|
9c17499757 | ||
|
|
8ceb2885db | ||
|
|
00944894b4 | ||
|
|
609fb3d59e | ||
|
|
01c330abe7 | ||
|
|
2ccef5bb4f | ||
|
|
10f1d93e2a | ||
|
|
dd518c8eac | ||
|
|
97cf2421de | ||
|
|
786a80131e | ||
|
|
93d257f9f5 | ||
|
|
f79535057f | ||
|
|
a905f4c20c | ||
|
|
3331954a78 | ||
|
|
f54c2e384f | ||
|
|
4b83f2ebd0 | ||
|
|
1c572ee16b | ||
|
|
b3402430e8 | ||
|
|
076f5de7d9 | ||
|
|
303a74559a | ||
|
|
c08e741d7a | ||
|
|
06d2c07fce | ||
|
|
b6f00a5789 | ||
|
|
dace520654 | ||
|
|
44d347d48a | ||
|
|
408827d876 | ||
|
|
9e33b557b1 | ||
|
|
368caa7608 | ||
|
|
7822a4c1cb | ||
|
|
0441f84606 | ||
|
|
81d72b5010 | ||
|
|
7ba8adfc74 | ||
|
|
ffd2a78623 | ||
|
|
1b9b3cada7 | ||
|
|
38c8f9cf21 | ||
|
|
54dbb05bb9 | ||
|
|
9a671f6059 | ||
|
|
80aab0b3f8 | ||
|
|
2d0286646f | ||
|
|
d012fe97b1 | ||
|
|
7ca13bc25e | ||
|
|
9c20f10743 | ||
|
|
6ec38ec056 | ||
|
|
28c742a1e1 | ||
|
|
b3b6cfe947 | ||
|
|
a8ef7b51eb | ||
|
|
124968114a | ||
|
|
04ed821b65 | ||
|
|
15599ac0a0 | ||
|
|
0cf6971664 | ||
|
|
47c4f2831c | ||
|
|
2b18dd1764 | ||
|
|
27c4908342 | ||
|
|
48625fa381 | ||
|
|
83a776a190 | ||
|
|
431f9af43e | ||
|
|
8c2f4c1899 | ||
|
|
919477ffe4 | ||
|
|
400386459c | ||
|
|
ebe0801d2f | ||
|
|
b76d85f204 | ||
|
|
28ba57afb3 | ||
|
|
d6873047ce | ||
|
|
54c0f2e5cf | ||
|
|
7795231cc6 | ||
|
|
4835e4fe51 | ||
|
|
daef970091 | ||
|
|
05020a92e8 | ||
|
|
a1bbac3c6c | ||
|
|
22c98beb24 | ||
|
|
8fd710d5e7 | ||
|
|
91a735ae47 | ||
|
|
39c662eece | ||
|
|
5e63002cf8 | ||
|
|
c46bcf7e1b | ||
|
|
3c65bdbb66 | ||
|
|
5630bce286 | ||
|
|
75524da21e | ||
|
|
ede7e683fd | ||
|
|
eb14d1347e | ||
|
|
c220b5d886 | ||
|
|
5e8e21b257 | ||
|
|
0635a4ac96 | ||
|
|
c71b753c78 | ||
|
|
2f8cffc699 | ||
|
|
9c1aa6da3d | ||
|
|
da67ea2300 | ||
|
|
72dd2736f5 | ||
|
|
24695bba09 | ||
|
|
c7c43062c5 | ||
|
|
97e1ea0633 | ||
|
|
04b9737a61 | ||
|
|
b0fb246693 | ||
|
|
41140ac78c | ||
|
|
1281904572 | ||
|
|
c8a8ff7cad | ||
|
|
df2799fff1 | ||
|
|
fd8ee0b851 | ||
|
|
6ecc5e7b73 | ||
|
|
52963c2ebf | ||
|
|
07e3e14c0e | ||
|
|
34c5f6616c | ||
|
|
32600f2619 | ||
|
|
b07df1c344 | ||
|
|
a1fca3a1da | ||
|
|
9394e59597 | ||
|
|
f8adc71529 | ||
|
|
c05e03bb5a | ||
|
|
199e86050e | ||
|
|
1e2a12b3d6 | ||
|
|
922efbfc2d | ||
|
|
842c4d81cc | ||
|
|
2a335c7608 | ||
|
|
35edd6b19d | ||
|
|
36bbc6a2e2 | ||
|
|
e20ccdd011 | ||
|
|
c2ff00883a | ||
|
|
00db97fcc1 | ||
|
|
117c3a9069 | ||
|
|
172ff407f3 | ||
|
|
a242fec5e1 | ||
|
|
6286941ebf | ||
|
|
42e0a6f90c | ||
|
|
81ae7d36b5 | ||
|
|
944e76edb9 | ||
|
|
151768ef82 | ||
|
|
50581c7f5c | ||
|
|
54041ef9e4 | ||
|
|
5a9ae5ef02 | ||
|
|
19de13bb04 | ||
|
|
3ab4e82168 | ||
|
|
619e838513 | ||
|
|
423597a01c | ||
|
|
d84dfe1b20 | ||
|
|
87b7a1120d | ||
|
|
528af8a99d | ||
|
|
17fc68ac5a | ||
|
|
804a56bd14 | ||
|
|
88f60d7736 | ||
|
|
95b7a3d3ce | ||
|
|
d71c0da7b7 | ||
|
|
fd070fec91 | ||
|
|
4f22034342 | ||
|
|
b731aa38af | ||
|
|
bf996a1812 | ||
|
|
af7ce65244 | ||
|
|
952db71835 | ||
|
|
abd1fa96a9 | ||
|
|
5aedd9c076 | ||
|
|
ff230c4b1d | ||
|
|
02c95108b9 | ||
|
|
1ff541afe4 | ||
|
|
11a8cbc1e5 | ||
|
|
c063976822 | ||
|
|
cb707034ce | ||
|
|
f10db27efd | ||
|
|
4878f90546 | ||
|
|
421e6617b1 | ||
|
|
0ee7a271d3 | ||
|
|
af022b9655 | ||
|
|
98d46261d9 | ||
|
|
4222fd97bc | ||
|
|
814852f0b8 | ||
|
|
ded2888759 | ||
|
|
18d66a795d | ||
|
|
4211672bfd | ||
|
|
68df0c3620 | ||
|
|
5e435b6a76 | ||
|
|
0dcede6457 | ||
|
|
cc21f5fae2 | ||
|
|
b22ad50d59 | ||
|
|
974252980c | ||
|
|
8d83986d27 | ||
|
|
6821b0a7dd | ||
|
|
1ba1724c65 | ||
|
|
ca5a7df5b0 | ||
|
|
69a3024853 | ||
|
|
fd3abf3717 | ||
|
|
99b3750d10 | ||
|
|
33f6d7ebb8 | ||
|
|
c4ef9ceb68 | ||
|
|
e95861f28a | ||
|
|
d3cd7b17c0 | ||
|
|
a50515496c | ||
|
|
0423313d9b | ||
|
|
7bbe7de05f | ||
|
|
83a451f2f4 | ||
|
|
d2a874f21d | ||
|
|
fd85b24b25 |
@@ -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
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
**/.git
|
**/.git
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
goctl
|
||||||
|
Makefile
|
||||||
|
readme.md
|
||||||
|
readme-cn.md
|
||||||
|
|||||||
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -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
|
|
||||||
|
|||||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -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:
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||||
|
|||||||
16
.github/workflows/go.yml
vendored
16
.github/workflows/go.yml
vendored
@@ -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@v5
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -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.21.13.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
|
||||||
|
|||||||
4
.github/workflows/reviewdog.yml
vendored
4
.github/workflows/reviewdog.yml
vendored
@@ -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 }}
|
||||||
@@ -14,6 +14,6 @@ jobs:
|
|||||||
# Report all results.
|
# Report all results.
|
||||||
filter_mode: nofilter
|
filter_mode: nofilter
|
||||||
# Exit with 1 when it find at least one finding.
|
# Exit with 1 when it find at least one finding.
|
||||||
fail_on_error: true
|
fail_level: any
|
||||||
# Set staticcheck flags
|
# Set staticcheck flags
|
||||||
staticcheck_flags: -checks=inherit,-SA1019,-SA1029,-SA5008
|
staticcheck_flags: -checks=inherit,-SA1019,-SA1029,-SA5008
|
||||||
|
|||||||
42
.github/workflows/version-check.yml
vendored
Normal file
42
.github/workflows/version-check.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Release Version Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'tools/goctl/v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Extract tag version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
# Extract version from tools/goctl/v* format
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/tools/goctl/v}"
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "Extracted version: $VERSION"
|
||||||
|
|
||||||
|
- name: Check version in goctl source code
|
||||||
|
run: |
|
||||||
|
# Change to goctl directory
|
||||||
|
cd tools/goctl
|
||||||
|
|
||||||
|
# Check version in BuildVersion constant
|
||||||
|
VERSION_IN_CODE=$(grep -r "const BuildVersion =" . | grep -o '".*"' | tr -d '"')
|
||||||
|
echo "Version in code: $VERSION_IN_CODE"
|
||||||
|
echo "Expected version: $VERSION"
|
||||||
|
|
||||||
|
if [ "$VERSION_IN_CODE" != "$VERSION" ]; then
|
||||||
|
echo "Version mismatch: Version in code ($VERSION_IN_CODE) doesn't match tag version ($VERSION)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Version check passed!"
|
||||||
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -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
16
SECURITY.md
Normal 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.
|
||||||
@@ -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].
|
||||||
|
|||||||
@@ -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
3
core/bloom/setscript.lua
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
for _, offset in ipairs(ARGV) do
|
||||||
|
redis.call("setbit", KEYS[1], offset, 1)
|
||||||
|
end
|
||||||
6
core/bloom/testscript.lua
Normal file
6
core/bloom/testscript.lua
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
48
core/breaker/bucket.go
Normal 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++
|
||||||
|
}
|
||||||
43
core/breaker/bucket_test.go
Normal file
43
core/breaker/bucket_test.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package breaker
|
package breaker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -8,9 +9,11 @@ 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()
|
_, err := b.Allow()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
p, err := b.AllowCtx(context.Background())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
p.Accept()
|
p.Accept()
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]) {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ func Load(file string, v any, opts ...Option) error {
|
|||||||
return loader([]byte(os.ExpandEnv(string(content))), v)
|
return loader([]byte(os.ExpandEnv(string(content))), v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return loader(content, v)
|
if err = loader(content, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
|
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||||
@@ -85,7 +89,12 @@ func LoadFromJsonBytes(content []byte, v any) error {
|
|||||||
|
|
||||||
lowerCaseKeyMap := toLowerCaseKeyMap(m, info)
|
lowerCaseKeyMap := toLowerCaseKeyMap(m, info)
|
||||||
|
|
||||||
return mapping.UnmarshalJsonMap(lowerCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toLowerCase))
|
if err = mapping.UnmarshalJsonMap(lowerCaseKeyMap, v,
|
||||||
|
mapping.WithCanonicalKeyFunc(toLowerCase)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigFromJsonBytes loads config into v from content json bytes.
|
// LoadConfigFromJsonBytes loads config into v from content json bytes.
|
||||||
@@ -133,7 +142,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 {
|
||||||
@@ -189,10 +198,10 @@ func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) {
|
|||||||
switch tp.Kind() {
|
switch tp.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return buildStructFieldsInfo(tp, fullName)
|
return buildStructFieldsInfo(tp, fullName)
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice, reflect.Map:
|
||||||
return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName)
|
return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName)
|
||||||
case reflect.Chan, reflect.Func:
|
case reflect.Chan, reflect.Func:
|
||||||
return nil, fmt.Errorf("unsupported type: %s", tp.Kind())
|
return nil, fmt.Errorf("unsupported type: %s, fullName: %s", tp.Kind(), fullName)
|
||||||
default:
|
default:
|
||||||
return &fieldInfo{
|
return &fieldInfo{
|
||||||
children: make(map[string]*fieldInfo),
|
children: make(map[string]*fieldInfo),
|
||||||
@@ -281,7 +290,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)
|
||||||
}
|
}
|
||||||
@@ -332,6 +341,8 @@ func toLowerCaseKeyMap(m map[string]any, info *fieldInfo) map[string]any {
|
|||||||
res[lk] = toLowerCaseInterface(v, ti)
|
res[lk] = toLowerCaseInterface(v, ti)
|
||||||
} else if info.mapField != nil {
|
} else if info.mapField != nil {
|
||||||
res[k] = toLowerCaseInterface(v, info.mapField)
|
res[k] = toLowerCaseInterface(v, info.mapField)
|
||||||
|
} else if vv, ok := v.(map[string]any); ok {
|
||||||
|
res[k] = toLowerCaseKeyMap(vv, info)
|
||||||
} else {
|
} else {
|
||||||
res[k] = v
|
res[k] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -40,9 +41,8 @@ func TestConfigJson(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
tmpfile, err := createTempFile(test, text)
|
tmpfile, err := createTempFile(t, test, text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -82,9 +82,8 @@ c = "${FOO}"
|
|||||||
d = "abcd!@#$112"
|
d = "abcd!@#$112"
|
||||||
`
|
`
|
||||||
t.Setenv("FOO", "2")
|
t.Setenv("FOO", "2")
|
||||||
tmpfile, err := createTempFile(".toml", text)
|
tmpfile, err := createTempFile(t, ".toml", text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -105,9 +104,8 @@ b = 1
|
|||||||
c = "FOO"
|
c = "FOO"
|
||||||
d = "abcd"
|
d = "abcd"
|
||||||
`
|
`
|
||||||
tmpfile, err := createTempFile(".toml", text)
|
tmpfile, err := createTempFile(t, ".toml", text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -127,9 +125,8 @@ func TestConfigWithLower(t *testing.T) {
|
|||||||
text := `a = "foo"
|
text := `a = "foo"
|
||||||
b = 1
|
b = 1
|
||||||
`
|
`
|
||||||
tmpfile, err := createTempFile(".toml", text)
|
tmpfile, err := createTempFile(t, ".toml", text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -207,9 +204,8 @@ c = "${FOO}"
|
|||||||
d = "abcd!@#112"
|
d = "abcd!@#112"
|
||||||
`
|
`
|
||||||
t.Setenv("FOO", "2")
|
t.Setenv("FOO", "2")
|
||||||
tmpfile, err := createTempFile(".toml", text)
|
tmpfile, err := createTempFile(t, ".toml", text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -241,9 +237,8 @@ func TestConfigJsonEnv(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
tmpfile, err := createTempFile(test, text)
|
tmpfile, err := createTempFile(t, test, text)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(tmpfile)
|
|
||||||
|
|
||||||
var val struct {
|
var val struct {
|
||||||
A string `json:"a"`
|
A string `json:"a"`
|
||||||
@@ -1192,6 +1187,42 @@ Email = "bar"`)
|
|||||||
assert.Len(t, c.Value, 2)
|
assert.Len(t, c.Value, 2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("multi layer map", func(t *testing.T) {
|
||||||
|
type Value struct {
|
||||||
|
User struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Value map[string]map[string]Value
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = []byte(`
|
||||||
|
[Value.first.User1.User]
|
||||||
|
Name = "foo"
|
||||||
|
[Value.second.User2.User]
|
||||||
|
Name = "bar"
|
||||||
|
`)
|
||||||
|
var c Config
|
||||||
|
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
|
||||||
|
assert.Len(t, c.Value, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_LoadBadConfig(t *testing.T) {
|
||||||
|
type Config struct {
|
||||||
|
Name string `json:"name,options=foo|bar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := createTempFile(t, ".json", `{"name": "baz"}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
err = Load(file, &c)
|
||||||
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getFullName(t *testing.T) {
|
func Test_getFullName(t *testing.T) {
|
||||||
@@ -1199,6 +1230,26 @@ func Test_getFullName(t *testing.T) {
|
|||||||
assert.Equal(t, "a", getFullName("", "a"))
|
assert.Equal(t, "a", getFullName("", "a"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
t.Run("normal config", func(t *testing.T) {
|
||||||
|
var c mockConfig
|
||||||
|
err := LoadFromJsonBytes([]byte(`{"val": "hello", "number": 8}`), &c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error no int", func(t *testing.T) {
|
||||||
|
var c mockConfig
|
||||||
|
err := LoadFromJsonBytes([]byte(`{"val": "hello"}`), &c)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error no string", func(t *testing.T) {
|
||||||
|
var c mockConfig
|
||||||
|
err := LoadFromJsonBytes([]byte(`{"number": 8}`), &c)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_buildFieldsInfo(t *testing.T) {
|
func Test_buildFieldsInfo(t *testing.T) {
|
||||||
type ParentSt struct {
|
type ParentSt struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -1288,13 +1339,13 @@ func Test_buildFieldsInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTempFile(ext, text string) (string, error) {
|
func createTempFile(t *testing.T, ext, text string) (string, error) {
|
||||||
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
|
if err = os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,5 +1354,26 @@ func createTempFile(ext, text string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Remove(filename)
|
||||||
|
})
|
||||||
|
|
||||||
return filename, nil
|
return filename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockConfig struct {
|
||||||
|
Val string
|
||||||
|
Number int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockConfig) Validate() error {
|
||||||
|
if len(m.Val) == 0 {
|
||||||
|
return errors.New("val is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Number == 0 {
|
||||||
|
return errors.New("number is zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)"`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
12
core/conf/validate.go
Normal file
12
core/conf/validate.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import "github.com/zeromicro/go-zero/core/validation"
|
||||||
|
|
||||||
|
// validate validates the value if it implements the Validator interface.
|
||||||
|
func validate(v any) error {
|
||||||
|
if val, ok := v.(validation.Validator); ok {
|
||||||
|
return val.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
81
core/conf/validate_test.go
Normal file
81
core/conf/validate_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockType int
|
||||||
|
|
||||||
|
func (m mockType) Validate() error {
|
||||||
|
if m < 10 {
|
||||||
|
return errors.New("invalid value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type anotherMockType int
|
||||||
|
|
||||||
|
func Test_validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
v any
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
v: mockType(5),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
v: mockType(10),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not validator",
|
||||||
|
v: anotherMockType(5),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validate(tt.v)
|
||||||
|
assert.Equal(t, tt.wantErr, err != nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockVal struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockVal) Validate() error {
|
||||||
|
return errors.New("invalid value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validateValPtr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
v any
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
v: mockVal{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value",
|
||||||
|
v: &mockVal{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Error(t, validate(tt.v))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
200
core/configcenter/configurator.go
Normal file
200
core/configcenter/configurator.go
Normal 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
|
||||||
|
}
|
||||||
233
core/configcenter/configurator_test.go
Normal file
233
core/configcenter/configurator_test.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
67
core/configcenter/subscriber/etcd.go
Normal file
67
core/configcenter/subscriber/etcd.go
Normal 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
|
||||||
|
}
|
||||||
9
core/configcenter/subscriber/subscriber.go
Normal file
9
core/configcenter/subscriber/subscriber.go
Normal 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)
|
||||||
|
}
|
||||||
41
core/configcenter/unmarshaler.go
Normal file
41
core/configcenter/unmarshaler.go
Normal 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
|
||||||
|
}
|
||||||
28
core/configcenter/unmarshaler_test.go
Normal file
28
core/configcenter/unmarshaler_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -9,25 +10,30 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/logc"
|
||||||
|
"github.com/zeromicro/go-zero/core/mathx"
|
||||||
"github.com/zeromicro/go-zero/core/syncx"
|
"github.com/zeromicro/go-zero/core/syncx"
|
||||||
"github.com/zeromicro/go-zero/core/threading"
|
"github.com/zeromicro/go-zero/core/threading"
|
||||||
|
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
||||||
clientv3 "go.etcd.io/etcd/client/v3"
|
clientv3 "go.etcd.io/etcd/client/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const coolDownDeviation = 0.05
|
||||||
|
|
||||||
var (
|
var (
|
||||||
registry = Registry{
|
registry = Registry{
|
||||||
clusters: make(map[string]*cluster),
|
clusters: make(map[string]*cluster),
|
||||||
}
|
}
|
||||||
connManager = syncx.NewResourceManager()
|
connManager = syncx.NewResourceManager()
|
||||||
|
coolDownUnstable = mathx.NewUnstable(coolDownDeviation)
|
||||||
|
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.
|
||||||
@@ -37,60 +43,148 @@ func GetRegistry() *Registry {
|
|||||||
|
|
||||||
// GetConn returns an etcd client connection associated with given endpoints.
|
// GetConn returns an etcd client connection associated with given endpoints.
|
||||||
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
|
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
|
||||||
c, _ := r.getCluster(endpoints)
|
c, _ := r.getOrCreateCluster(endpoints)
|
||||||
return c.getClient()
|
return c.getClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, exactMatch bool, l UpdateListener) error {
|
||||||
c, exists := r.getCluster(endpoints)
|
wkey := watchKey{
|
||||||
|
key: key,
|
||||||
|
exactMatch: exactMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, exists := r.getOrCreateCluster(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 {
|
||||||
kvs := c.getCurrent(key)
|
c.lock.Lock()
|
||||||
for _, kv := range kvs {
|
watcher, ok := c.watchers[wkey]
|
||||||
l.OnAdd(kv)
|
if ok {
|
||||||
|
watcher.listeners = append(watcher.listeners, l)
|
||||||
|
}
|
||||||
|
c.lock.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
kvs := c.getCurrent(wkey)
|
||||||
|
for _, kv := range kvs {
|
||||||
|
l.OnAdd(kv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.monitor(key, l)
|
return c.monitor(wkey, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) {
|
func (r *Registry) Unmonitor(endpoints []string, key string, exactMatch bool, l UpdateListener) {
|
||||||
clusterKey := getClusterKey(endpoints)
|
c, exists := r.getCluster(endpoints)
|
||||||
r.lock.Lock()
|
|
||||||
defer r.lock.Unlock()
|
|
||||||
c, exists = r.clusters[clusterKey]
|
|
||||||
if !exists {
|
if !exists {
|
||||||
c = newCluster(endpoints)
|
return
|
||||||
r.clusters[clusterKey] = c
|
}
|
||||||
|
|
||||||
|
wkey := watchKey{
|
||||||
|
key: key,
|
||||||
|
exactMatch: exactMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
watcher, ok := c.watchers[wkey]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, listener := range watcher.listeners {
|
||||||
|
if listener == l {
|
||||||
|
watcher.listeners = append(watcher.listeners[:i], watcher.listeners[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(watcher.listeners) == 0 {
|
||||||
|
if watcher.cancel != nil {
|
||||||
|
watcher.cancel()
|
||||||
|
}
|
||||||
|
delete(c.watchers, wkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) getCluster(endpoints []string) (*cluster, bool) {
|
||||||
|
clusterKey := getClusterKey(endpoints)
|
||||||
|
|
||||||
|
r.lock.RLock()
|
||||||
|
c, ok := r.clusters[clusterKey]
|
||||||
|
r.lock.RUnlock()
|
||||||
|
|
||||||
|
return c, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) getOrCreateCluster(endpoints []string) (c *cluster, exists bool) {
|
||||||
|
c, exists = r.getCluster(endpoints)
|
||||||
|
if !exists {
|
||||||
|
clusterKey := getClusterKey(endpoints)
|
||||||
|
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
// double-check locking
|
||||||
|
c, exists = r.clusters[clusterKey]
|
||||||
|
if !exists {
|
||||||
|
c = newCluster(endpoints)
|
||||||
|
r.clusters[clusterKey] = c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type cluster struct {
|
type (
|
||||||
endpoints []string
|
watchKey struct {
|
||||||
key string
|
key string
|
||||||
values map[string]map[string]string
|
exactMatch bool
|
||||||
listeners map[string][]UpdateListener
|
}
|
||||||
watchGroup *threading.RoutineGroup
|
|
||||||
done chan lang.PlaceholderType
|
watchValue struct {
|
||||||
lock sync.Mutex
|
listeners []UpdateListener
|
||||||
}
|
values map[string]string
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster struct {
|
||||||
|
endpoints []string
|
||||||
|
key string
|
||||||
|
watchers map[watchKey]*watchValue
|
||||||
|
watchGroup *threading.RoutineGroup
|
||||||
|
done chan lang.PlaceholderType
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func newCluster(endpoints []string) *cluster {
|
func newCluster(endpoints []string) *cluster {
|
||||||
return &cluster{
|
return &cluster{
|
||||||
endpoints: endpoints,
|
endpoints: endpoints,
|
||||||
key: getClusterKey(endpoints),
|
key: getClusterKey(endpoints),
|
||||||
values: make(map[string]map[string]string),
|
watchers: make(map[watchKey]*watchValue),
|
||||||
listeners: make(map[string][]UpdateListener),
|
|
||||||
watchGroup: threading.NewRoutineGroup(),
|
watchGroup: threading.NewRoutineGroup(),
|
||||||
done: make(chan lang.PlaceholderType),
|
done: make(chan lang.PlaceholderType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) context(cli EtcdClient) context.Context {
|
func (c *cluster) addListener(key watchKey, l UpdateListener) {
|
||||||
return contextx.ValueOnlyFrom(cli.Ctx())
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
watcher, ok := c.watchers[key]
|
||||||
|
if ok {
|
||||||
|
watcher.listeners = append(watcher.listeners, l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val := newWatchValue()
|
||||||
|
val.listeners = []UpdateListener{l}
|
||||||
|
c.watchers[key] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) getClient() (EtcdClient, error) {
|
func (c *cluster) getClient() (EtcdClient, error) {
|
||||||
@@ -104,12 +198,17 @@ func (c *cluster) getClient() (EtcdClient, error) {
|
|||||||
return val.(EtcdClient), nil
|
return val.(EtcdClient), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) getCurrent(key string) []KV {
|
func (c *cluster) getCurrent(key watchKey) []KV {
|
||||||
c.lock.Lock()
|
c.lock.RLock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.RUnlock()
|
||||||
|
|
||||||
|
watcher, ok := c.watchers[key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var kvs []KV
|
var kvs []KV
|
||||||
for k, v := range c.values[key] {
|
for k, v := range watcher.values {
|
||||||
kvs = append(kvs, KV{
|
kvs = append(kvs, KV{
|
||||||
Key: k,
|
Key: k,
|
||||||
Val: v,
|
Val: v,
|
||||||
@@ -119,42 +218,23 @@ func (c *cluster) getCurrent(key string) []KV {
|
|||||||
return kvs
|
return kvs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) handleChanges(key string, kvs []KV) {
|
func (c *cluster) handleChanges(key watchKey, kvs []KV) {
|
||||||
var add []KV
|
|
||||||
var remove []KV
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
listeners := append([]UpdateListener(nil), c.listeners[key]...)
|
watcher, ok := c.watchers[key]
|
||||||
vals, ok := c.values[key]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
add = kvs
|
c.lock.Unlock()
|
||||||
vals = make(map[string]string)
|
return
|
||||||
for _, kv := range kvs {
|
|
||||||
vals[kv.Key] = kv.Val
|
|
||||||
}
|
|
||||||
c.values[key] = vals
|
|
||||||
} else {
|
|
||||||
m := make(map[string]string)
|
|
||||||
for _, kv := range kvs {
|
|
||||||
m[kv.Key] = kv.Val
|
|
||||||
}
|
|
||||||
for k, v := range vals {
|
|
||||||
if val, ok := m[k]; !ok || v != val {
|
|
||||||
remove = append(remove, KV{
|
|
||||||
Key: k,
|
|
||||||
Val: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k, v := range m {
|
|
||||||
if val, ok := vals[k]; !ok || v != val {
|
|
||||||
add = append(add, KV{
|
|
||||||
Key: k,
|
|
||||||
Val: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.values[key] = m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listeners := append([]UpdateListener(nil), watcher.listeners...)
|
||||||
|
// watcher.values cannot be nil
|
||||||
|
vals := watcher.values
|
||||||
|
newVals := make(map[string]string, len(kvs)+len(vals))
|
||||||
|
for _, kv := range kvs {
|
||||||
|
newVals[kv.Key] = kv.Val
|
||||||
|
}
|
||||||
|
add, remove := calculateChanges(vals, newVals)
|
||||||
|
watcher.values = newVals
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
|
||||||
for _, kv := range add {
|
for _, kv := range add {
|
||||||
@@ -169,20 +249,22 @@ func (c *cluster) handleChanges(key string, kvs []KV) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
|
func (c *cluster) handleWatchEvents(ctx context.Context, key watchKey, events []*clientv3.Event) {
|
||||||
c.lock.Lock()
|
c.lock.RLock()
|
||||||
listeners := append([]UpdateListener(nil), c.listeners[key]...)
|
watcher, ok := c.watchers[key]
|
||||||
c.lock.Unlock()
|
if !ok {
|
||||||
|
c.lock.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := append([]UpdateListener(nil), watcher.listeners...)
|
||||||
|
c.lock.RUnlock()
|
||||||
|
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case clientv3.EventTypePut:
|
case clientv3.EventTypePut:
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
if vals, ok := c.values[key]; ok {
|
watcher.values[string(ev.Kv.Key)] = string(ev.Kv.Value)
|
||||||
vals[string(ev.Kv.Key)] = string(ev.Kv.Value)
|
|
||||||
} else {
|
|
||||||
c.values[key] = map[string]string{string(ev.Kv.Key): string(ev.Kv.Value)}
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
for _, l := range listeners {
|
for _, l := range listeners {
|
||||||
l.OnAdd(KV{
|
l.OnAdd(KV{
|
||||||
@@ -192,9 +274,7 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
|
|||||||
}
|
}
|
||||||
case clientv3.EventTypeDelete:
|
case clientv3.EventTypeDelete:
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
if vals, ok := c.values[key]; ok {
|
delete(watcher.values, string(ev.Kv.Key))
|
||||||
delete(vals, string(ev.Kv.Key))
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
for _, l := range listeners {
|
for _, l := range listeners {
|
||||||
l.OnDelete(KV{
|
l.OnDelete(KV{
|
||||||
@@ -203,24 +283,29 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
logx.Errorf("Unknown event type: %v", ev.Type)
|
logc.Errorf(ctx, "Unknown event type: %v", ev.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) load(cli EtcdClient, key string) int64 {
|
func (c *cluster) load(cli EtcdClient, key watchKey) int64 {
|
||||||
var resp *clientv3.GetResponse
|
var resp *clientv3.GetResponse
|
||||||
for {
|
for {
|
||||||
var err error
|
var err error
|
||||||
ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout)
|
ctx, cancel := context.WithTimeout(cli.Ctx(), RequestTimeout)
|
||||||
resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix())
|
if key.exactMatch {
|
||||||
|
resp, err = cli.Get(ctx, key.key)
|
||||||
|
} else {
|
||||||
|
resp, err = cli.Get(ctx, makeKeyPrefix(key.key), clientv3.WithPrefix())
|
||||||
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Error(err)
|
logc.Errorf(cli.Ctx(), "%s, key: %s, exactMatch: %t", err.Error(), key.key, key.exactMatch)
|
||||||
time.Sleep(coolDownInterval)
|
time.Sleep(coolDownUnstable.AroundDuration(coolDownInterval))
|
||||||
}
|
}
|
||||||
|
|
||||||
var kvs []KV
|
var kvs []KV
|
||||||
@@ -236,16 +321,13 @@ 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 watchKey, l UpdateListener) error {
|
||||||
c.lock.Lock()
|
|
||||||
c.listeners[key] = append(c.listeners[key], l)
|
|
||||||
c.lock.Unlock()
|
|
||||||
|
|
||||||
cli, err := c.getClient()
|
cli, err := c.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.addListener(key, l)
|
||||||
rev := c.load(cli, key)
|
rev := c.load(cli, key)
|
||||||
c.watchGroup.Run(func() {
|
c.watchGroup.Run(func() {
|
||||||
c.watch(cli, key, rev)
|
c.watch(cli, key, rev)
|
||||||
@@ -267,16 +349,22 @@ func (c *cluster) newClient() (EtcdClient, error) {
|
|||||||
|
|
||||||
func (c *cluster) reload(cli EtcdClient) {
|
func (c *cluster) reload(cli EtcdClient) {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
|
// cancel the previous watches
|
||||||
close(c.done)
|
close(c.done)
|
||||||
c.watchGroup.Wait()
|
c.watchGroup.Wait()
|
||||||
|
var keys []watchKey
|
||||||
|
for wk, wval := range c.watchers {
|
||||||
|
keys = append(keys, wk)
|
||||||
|
if wval.cancel != nil {
|
||||||
|
wval.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.done = make(chan lang.PlaceholderType)
|
c.done = make(chan lang.PlaceholderType)
|
||||||
c.watchGroup = threading.NewRoutineGroup()
|
c.watchGroup = threading.NewRoutineGroup()
|
||||||
var keys []string
|
|
||||||
for k := range c.listeners {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
|
||||||
|
// start new watches
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
k := key
|
k := key
|
||||||
c.watchGroup.Run(func() {
|
c.watchGroup.Run(func() {
|
||||||
@@ -286,46 +374,80 @@ func (c *cluster) reload(cli EtcdClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
|
func (c *cluster) watch(cli EtcdClient, key watchKey, 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) {
|
||||||
|
logc.Errorf(cli.Ctx(), "etcd watch stream has been compacted, try to reload, rev %d", rev)
|
||||||
|
rev = c.load(cli, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log the error and retry
|
||||||
|
logc.Error(cli.Ctx(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool {
|
func (c *cluster) watchStream(cli EtcdClient, key watchKey, rev int64) error {
|
||||||
var rch clientv3.WatchChan
|
ctx, rch := c.setupWatch(cli, key, rev)
|
||||||
if rev != 0 {
|
|
||||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix(),
|
|
||||||
clientv3.WithRev(rev+1))
|
|
||||||
} else {
|
|
||||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
|
|
||||||
}
|
|
||||||
|
|
||||||
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(ctx, key, wresp.Events)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cluster) setupWatch(cli EtcdClient, key watchKey, rev int64) (context.Context, clientv3.WatchChan) {
|
||||||
|
var (
|
||||||
|
rch clientv3.WatchChan
|
||||||
|
ops []clientv3.OpOption
|
||||||
|
wkey = key.key
|
||||||
|
)
|
||||||
|
|
||||||
|
if !key.exactMatch {
|
||||||
|
wkey = makeKeyPrefix(key.key)
|
||||||
|
ops = append(ops, clientv3.WithPrefix())
|
||||||
|
}
|
||||||
|
if rev != 0 {
|
||||||
|
ops = append(ops, clientv3.WithRev(rev+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cli.Ctx())
|
||||||
|
if watcher, ok := c.watchers[key]; ok {
|
||||||
|
watcher.cancel = cancel
|
||||||
|
} else {
|
||||||
|
val := newWatchValue()
|
||||||
|
val.cancel = cancel
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
c.watchers[key] = val
|
||||||
|
c.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
rch = cli.Watch(clientv3.WithRequireLeader(ctx), wkey, ops...)
|
||||||
|
|
||||||
|
return ctx, rch
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cluster) watchConnState(cli EtcdClient) {
|
func (c *cluster) watchConnState(cli EtcdClient) {
|
||||||
watcher := newStateWatcher()
|
watcher := newStateWatcher()
|
||||||
watcher.addListener(func() {
|
watcher.addListener(func() {
|
||||||
@@ -354,6 +476,28 @@ func DialClient(endpoints []string) (EtcdClient, error) {
|
|||||||
return clientv3.New(cfg)
|
return clientv3.New(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateChanges(oldVals, newVals map[string]string) (add, remove []KV) {
|
||||||
|
for k, v := range newVals {
|
||||||
|
if val, ok := oldVals[k]; !ok || v != val {
|
||||||
|
add = append(add, KV{
|
||||||
|
Key: k,
|
||||||
|
Val: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range oldVals {
|
||||||
|
if val, ok := newVals[k]; !ok || v != val {
|
||||||
|
remove = append(remove, KV{
|
||||||
|
Key: k,
|
||||||
|
Val: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return add, remove
|
||||||
|
}
|
||||||
|
|
||||||
func getClusterKey(endpoints []string) string {
|
func getClusterKey(endpoints []string) string {
|
||||||
sort.Strings(endpoints)
|
sort.Strings(endpoints)
|
||||||
return strings.Join(endpoints, endpointsSeparator)
|
return strings.Join(endpoints, endpointsSeparator)
|
||||||
@@ -362,3 +506,10 @@ func getClusterKey(endpoints []string) string {
|
|||||||
func makeKeyPrefix(key string) string {
|
func makeKeyPrefix(key string) string {
|
||||||
return fmt.Sprintf("%s%c", key, Delimiter)
|
return fmt.Sprintf("%s%c", key, Delimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewClient returns a watchValue that make sure values are not nil.
|
||||||
|
func newWatchValue() *watchValue {
|
||||||
|
return &watchValue{
|
||||||
|
values: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"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/stringx"
|
"github.com/zeromicro/go-zero/core/stringx"
|
||||||
|
"github.com/zeromicro/go-zero/core/threading"
|
||||||
"go.etcd.io/etcd/api/v3/etcdserverpb"
|
"go.etcd.io/etcd/api/v3/etcdserverpb"
|
||||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||||
clientv3 "go.etcd.io/etcd/client/v3"
|
clientv3 "go.etcd.io/etcd/client/v3"
|
||||||
@@ -38,9 +39,9 @@ func setMockClient(cli EtcdClient) func() {
|
|||||||
|
|
||||||
func TestGetCluster(t *testing.T) {
|
func TestGetCluster(t *testing.T) {
|
||||||
AddAccount([]string{"first"}, "foo", "bar")
|
AddAccount([]string{"first"}, "foo", "bar")
|
||||||
c1, _ := GetRegistry().getCluster([]string{"first"})
|
c1, _ := GetRegistry().getOrCreateCluster([]string{"first"})
|
||||||
c2, _ := GetRegistry().getCluster([]string{"second"})
|
c2, _ := GetRegistry().getOrCreateCluster([]string{"second"})
|
||||||
c3, _ := GetRegistry().getCluster([]string{"first"})
|
c3, _ := GetRegistry().getOrCreateCluster([]string{"first"})
|
||||||
assert.Equal(t, c1, c3)
|
assert.Equal(t, c1, c3)
|
||||||
assert.NotEqual(t, c1, c2)
|
assert.NotEqual(t, c1, c2)
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,36 @@ func TestGetClusterKey(t *testing.T) {
|
|||||||
getClusterKey([]string{"remotehost:5678", "localhost:1234"}))
|
getClusterKey([]string{"remotehost:5678", "localhost:1234"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmonitor(t *testing.T) {
|
||||||
|
t.Run("no listener", func(t *testing.T) {
|
||||||
|
reg := &Registry{
|
||||||
|
clusters: map[string]*cluster{},
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
reg.Unmonitor([]string{"any"}, "any", false, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no value", func(t *testing.T) {
|
||||||
|
reg := &Registry{
|
||||||
|
clusters: map[string]*cluster{
|
||||||
|
"any": {
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
{
|
||||||
|
key: "any",
|
||||||
|
}: {
|
||||||
|
values: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
reg.Unmonitor([]string{"any"}, "another", false, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCluster_HandleChanges(t *testing.T) {
|
func TestCluster_HandleChanges(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
l := NewMockUpdateListener(ctrl)
|
l := NewMockUpdateListener(ctrl)
|
||||||
@@ -78,8 +109,14 @@ func TestCluster_HandleChanges(t *testing.T) {
|
|||||||
Val: "4",
|
Val: "4",
|
||||||
})
|
})
|
||||||
c := newCluster([]string{"any"})
|
c := newCluster([]string{"any"})
|
||||||
c.listeners["any"] = []UpdateListener{l}
|
key := watchKey{
|
||||||
c.handleChanges("any", []KV{
|
key: "any",
|
||||||
|
exactMatch: false,
|
||||||
|
}
|
||||||
|
c.watchers[key] = &watchValue{
|
||||||
|
listeners: []UpdateListener{l},
|
||||||
|
}
|
||||||
|
c.handleChanges(key, []KV{
|
||||||
{
|
{
|
||||||
Key: "first",
|
Key: "first",
|
||||||
Val: "1",
|
Val: "1",
|
||||||
@@ -92,8 +129,8 @@ func TestCluster_HandleChanges(t *testing.T) {
|
|||||||
assert.EqualValues(t, map[string]string{
|
assert.EqualValues(t, map[string]string{
|
||||||
"first": "1",
|
"first": "1",
|
||||||
"second": "2",
|
"second": "2",
|
||||||
}, c.values["any"])
|
}, c.watchers[key].values)
|
||||||
c.handleChanges("any", []KV{
|
c.handleChanges(key, []KV{
|
||||||
{
|
{
|
||||||
Key: "third",
|
Key: "third",
|
||||||
Val: "3",
|
Val: "3",
|
||||||
@@ -106,7 +143,7 @@ func TestCluster_HandleChanges(t *testing.T) {
|
|||||||
assert.EqualValues(t, map[string]string{
|
assert.EqualValues(t, map[string]string{
|
||||||
"third": "3",
|
"third": "3",
|
||||||
"fourth": "4",
|
"fourth": "4",
|
||||||
}, c.values["any"])
|
}, c.watchers[key].values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCluster_Load(t *testing.T) {
|
func TestCluster_Load(t *testing.T) {
|
||||||
@@ -126,9 +163,11 @@ func TestCluster_Load(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
cli.EXPECT().Ctx().Return(context.Background())
|
cli.EXPECT().Ctx().Return(context.Background())
|
||||||
c := &cluster{
|
c := &cluster{
|
||||||
values: make(map[string]map[string]string),
|
watchers: make(map[watchKey]*watchValue),
|
||||||
}
|
}
|
||||||
c.load(cli, "any")
|
c.load(cli, watchKey{
|
||||||
|
key: "any",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCluster_Watch(t *testing.T) {
|
func TestCluster_Watch(t *testing.T) {
|
||||||
@@ -160,11 +199,16 @@ func TestCluster_Watch(t *testing.T) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
c := &cluster{
|
c := &cluster{
|
||||||
listeners: make(map[string][]UpdateListener),
|
watchers: make(map[watchKey]*watchValue),
|
||||||
values: make(map[string]map[string]string),
|
}
|
||||||
|
key := watchKey{
|
||||||
|
key: "any",
|
||||||
}
|
}
|
||||||
listener := NewMockUpdateListener(ctrl)
|
listener := NewMockUpdateListener(ctrl)
|
||||||
c.listeners["any"] = []UpdateListener{listener}
|
c.watchers[key] = &watchValue{
|
||||||
|
listeners: []UpdateListener{listener},
|
||||||
|
values: make(map[string]string),
|
||||||
|
}
|
||||||
listener.EXPECT().OnAdd(gomock.Any()).Do(func(kv KV) {
|
listener.EXPECT().OnAdd(gomock.Any()).Do(func(kv KV) {
|
||||||
assert.Equal(t, "hello", kv.Key)
|
assert.Equal(t, "hello", kv.Key)
|
||||||
assert.Equal(t, "world", kv.Val)
|
assert.Equal(t, "world", kv.Val)
|
||||||
@@ -173,7 +217,7 @@ func TestCluster_Watch(t *testing.T) {
|
|||||||
listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ any) {
|
listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ any) {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}).MaxTimes(1)
|
}).MaxTimes(1)
|
||||||
go c.watch(cli, "any", 0)
|
go c.watch(cli, key, 0)
|
||||||
ch <- clientv3.WatchResponse{
|
ch <- clientv3.WatchResponse{
|
||||||
Events: []*clientv3.Event{
|
Events: []*clientv3.Event{
|
||||||
{
|
{
|
||||||
@@ -211,17 +255,111 @@ func TestClusterWatch_RespFailures(t *testing.T) {
|
|||||||
ch := make(chan clientv3.WatchResponse)
|
ch := make(chan clientv3.WatchResponse)
|
||||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
||||||
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
||||||
c := new(cluster)
|
c := &cluster{
|
||||||
|
watchers: make(map[watchKey]*watchValue),
|
||||||
|
}
|
||||||
c.done = make(chan lang.PlaceholderType)
|
c.done = make(chan lang.PlaceholderType)
|
||||||
go func() {
|
go func() {
|
||||||
ch <- resp
|
ch <- resp
|
||||||
close(c.done)
|
close(c.done)
|
||||||
}()
|
}()
|
||||||
c.watch(cli, "any", 0)
|
key := watchKey{
|
||||||
|
key: "any",
|
||||||
|
}
|
||||||
|
c.watch(cli, key, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCluster_getCurrent(t *testing.T) {
|
||||||
|
t.Run("no value", func(t *testing.T) {
|
||||||
|
c := &cluster{
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
{
|
||||||
|
key: "any",
|
||||||
|
}: {
|
||||||
|
values: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(t, c.getCurrent(watchKey{
|
||||||
|
key: "another",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCluster_handleWatchEvents(t *testing.T) {
|
||||||
|
t.Run("no value", func(t *testing.T) {
|
||||||
|
c := &cluster{
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
{
|
||||||
|
key: "any",
|
||||||
|
}: {
|
||||||
|
values: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
c.handleWatchEvents(context.Background(), watchKey{
|
||||||
|
key: "another",
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCluster_addListener(t *testing.T) {
|
||||||
|
t.Run("has listener", func(t *testing.T) {
|
||||||
|
c := &cluster{
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
{
|
||||||
|
key: "any",
|
||||||
|
}: {
|
||||||
|
listeners: make([]UpdateListener, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
c.addListener(watchKey{
|
||||||
|
key: "any",
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no listener", func(t *testing.T) {
|
||||||
|
c := &cluster{
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
{
|
||||||
|
key: "any",
|
||||||
|
}: {
|
||||||
|
listeners: make([]UpdateListener, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
c.addListener(watchKey{
|
||||||
|
key: "another",
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCluster_reload(t *testing.T) {
|
||||||
|
c := &cluster{
|
||||||
|
watchers: map[watchKey]*watchValue{},
|
||||||
|
watchGroup: threading.NewRoutineGroup(),
|
||||||
|
done: make(chan lang.PlaceholderType),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
cli := NewMockEtcdClient(ctrl)
|
||||||
|
restore := setMockClient(cli)
|
||||||
|
defer restore()
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
c.reload(cli)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestClusterWatch_CloseChan(t *testing.T) {
|
func TestClusterWatch_CloseChan(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
@@ -231,13 +369,17 @@ func TestClusterWatch_CloseChan(t *testing.T) {
|
|||||||
ch := make(chan clientv3.WatchResponse)
|
ch := make(chan clientv3.WatchResponse)
|
||||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
||||||
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
||||||
c := new(cluster)
|
c := &cluster{
|
||||||
|
watchers: make(map[watchKey]*watchValue),
|
||||||
|
}
|
||||||
c.done = make(chan lang.PlaceholderType)
|
c.done = make(chan lang.PlaceholderType)
|
||||||
go func() {
|
go func() {
|
||||||
close(ch)
|
close(ch)
|
||||||
close(c.done)
|
close(c.done)
|
||||||
}()
|
}()
|
||||||
c.watch(cli, "any", 0)
|
c.watch(cli, watchKey{
|
||||||
|
key: "any",
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValueOnlyContext(t *testing.T) {
|
func TestValueOnlyContext(t *testing.T) {
|
||||||
@@ -280,16 +422,59 @@ func TestRegistry_Monitor(t *testing.T) {
|
|||||||
GetRegistry().lock.Lock()
|
GetRegistry().lock.Lock()
|
||||||
GetRegistry().clusters = map[string]*cluster{
|
GetRegistry().clusters = map[string]*cluster{
|
||||||
getClusterKey(endpoints): {
|
getClusterKey(endpoints): {
|
||||||
listeners: map[string][]UpdateListener{},
|
watchers: map[watchKey]*watchValue{
|
||||||
values: map[string]map[string]string{
|
watchKey{
|
||||||
"foo": {
|
key: "foo",
|
||||||
"bar": "baz",
|
exactMatch: true,
|
||||||
|
}: {
|
||||||
|
values: map[string]string{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
GetRegistry().lock.Unlock()
|
GetRegistry().lock.Unlock()
|
||||||
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", new(mockListener)))
|
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", false, new(mockListener)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistry_Unmonitor(t *testing.T) {
|
||||||
|
svr, err := mockserver.StartMockServers(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
svr.StartAt(0)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
endpoints := []string{svr.Servers[0].Address}
|
||||||
|
GetRegistry().lock.Lock()
|
||||||
|
GetRegistry().clusters = map[string]*cluster{
|
||||||
|
getClusterKey(endpoints): {
|
||||||
|
watchers: map[watchKey]*watchValue{
|
||||||
|
watchKey{
|
||||||
|
key: "foo",
|
||||||
|
exactMatch: true,
|
||||||
|
}: {
|
||||||
|
values: map[string]string{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
cancel: cancel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
GetRegistry().lock.Unlock()
|
||||||
|
l := new(mockListener)
|
||||||
|
assert.NoError(t, GetRegistry().Monitor(endpoints, "foo", true, l))
|
||||||
|
watchVals := GetRegistry().clusters[getClusterKey(endpoints)].watchers[watchKey{
|
||||||
|
key: "foo",
|
||||||
|
exactMatch: true,
|
||||||
|
}]
|
||||||
|
assert.Equal(t, 1, len(watchVals.listeners))
|
||||||
|
GetRegistry().Unmonitor(endpoints, "foo", true, l)
|
||||||
|
watchVals = GetRegistry().clusters[getClusterKey(endpoints)].watchers[watchKey{
|
||||||
|
key: "foo",
|
||||||
|
exactMatch: true,
|
||||||
|
}]
|
||||||
|
assert.Nil(t, watchVals)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockListener struct {
|
type mockListener struct {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateListener wraps the OnAdd and OnDelete methods.
|
// UpdateListener wraps the OnAdd and OnDelete methods.
|
||||||
|
// The implementation should be thread-safe and idempotent.
|
||||||
UpdateListener interface {
|
UpdateListener interface {
|
||||||
OnAdd(kv KV)
|
OnAdd(kv KV)
|
||||||
OnDelete(kv KV)
|
OnDelete(kv KV)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/discov/internal"
|
"github.com/zeromicro/go-zero/core/discov/internal"
|
||||||
"github.com/zeromicro/go-zero/core/lang"
|
"github.com/zeromicro/go-zero/core/lang"
|
||||||
|
"github.com/zeromicro/go-zero/core/logc"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
"github.com/zeromicro/go-zero/core/proc"
|
"github.com/zeromicro/go-zero/core/proc"
|
||||||
"github.com/zeromicro/go-zero/core/syncx"
|
"github.com/zeromicro/go-zero/core/syncx"
|
||||||
@@ -91,12 +92,12 @@ func (p *Publisher) doKeepAlive() error {
|
|||||||
default:
|
default:
|
||||||
cli, err := p.doRegister()
|
cli, err := p.doRegister()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("etcd publisher doRegister: %s", err.Error())
|
logc.Errorf(cli.Ctx(), "etcd publisher doRegister: %s", err.Error())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.keepAliveAsync(cli); err != nil {
|
if err := p.keepAliveAsync(cli); err != nil {
|
||||||
logx.Errorf("etcd publisher keepAliveAsync: %s", err.Error())
|
logc.Errorf(cli.Ctx(), "etcd publisher keepAliveAsync: %s", err.Error())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +131,17 @@ func (p *Publisher) keepAliveAsync(cli internal.EtcdClient) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
p.revoke(cli)
|
p.revoke(cli)
|
||||||
if err := p.doKeepAlive(); err != nil {
|
if err := p.doKeepAlive(); err != nil {
|
||||||
logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
|
logc.Errorf(cli.Ctx(), "etcd publisher KeepAlive: %s", err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-p.pauseChan:
|
case <-p.pauseChan:
|
||||||
logx.Infof("paused etcd renew, key: %s, value: %s", p.key, p.value)
|
logc.Infof(cli.Ctx(), "paused etcd renew, key: %s, value: %s", p.key, p.value)
|
||||||
p.revoke(cli)
|
p.revoke(cli)
|
||||||
select {
|
select {
|
||||||
case <-p.resumeChan:
|
case <-p.resumeChan:
|
||||||
if err := p.doKeepAlive(); err != nil {
|
if err := p.doKeepAlive(); err != nil {
|
||||||
logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
|
logc.Errorf(cli.Ctx(), "etcd publisher KeepAlive: %s", err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case <-p.quit.Done():
|
case <-p.quit.Done():
|
||||||
@@ -175,7 +176,7 @@ func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, erro
|
|||||||
|
|
||||||
func (p *Publisher) revoke(cli internal.EtcdClient) {
|
func (p *Publisher) revoke(cli internal.EtcdClient) {
|
||||||
if _, err := cli.Revoke(cli.Ctx(), p.lease); err != nil {
|
if _, err := cli.Revoke(cli.Ctx(), p.lease); err != nil {
|
||||||
logx.Errorf("etcd publisher revoke: %s", err.Error())
|
logc.Errorf(cli.Ctx(), "etcd publisher revoke: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ 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
|
key string
|
||||||
|
exactMatch bool
|
||||||
|
items *container
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,13 +30,14 @@ type (
|
|||||||
func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscriber, error) {
|
func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscriber, error) {
|
||||||
sub := &Subscriber{
|
sub := &Subscriber{
|
||||||
endpoints: endpoints,
|
endpoints: endpoints,
|
||||||
|
key: key,
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(sub)
|
opt(sub)
|
||||||
}
|
}
|
||||||
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.exactMatch, sub.items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +49,11 @@ func (s *Subscriber) AddListener(listener func()) {
|
|||||||
s.items.addListener(listener)
|
s.items.addListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the subscriber.
|
||||||
|
func (s *Subscriber) Close() {
|
||||||
|
internal.GetRegistry().Unmonitor(s.endpoints, s.key, s.exactMatch, s.items)
|
||||||
|
}
|
||||||
|
|
||||||
// Values returns all the subscription values.
|
// Values returns all the subscription values.
|
||||||
func (s *Subscriber) Values() []string {
|
func (s *Subscriber) Values() []string {
|
||||||
return s.items.getValues()
|
return s.items.getValues()
|
||||||
@@ -59,6 +67,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) {
|
||||||
|
|||||||
@@ -225,3 +225,28 @@ func TestWithSubEtcdAccount(t *testing.T) {
|
|||||||
assert.Equal(t, user, account.User)
|
assert.Equal(t, user, account.User)
|
||||||
assert.Equal(t, "bar", account.Pass)
|
assert.Equal(t, "bar", account.Pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWithExactMatch(t *testing.T) {
|
||||||
|
sub := new(Subscriber)
|
||||||
|
WithExactMatch()(sub)
|
||||||
|
sub.items = newContainer(sub.exclusive)
|
||||||
|
var count int32
|
||||||
|
sub.AddListener(func() {
|
||||||
|
atomic.AddInt32(&count, 1)
|
||||||
|
})
|
||||||
|
sub.items.notifyChange()
|
||||||
|
assert.Empty(t, sub.Values())
|
||||||
|
assert.Equal(t, int32(1), atomic.LoadInt32(&count))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscriberClose(t *testing.T) {
|
||||||
|
l := newContainer(false)
|
||||||
|
sub := &Subscriber{
|
||||||
|
endpoints: []string{"localhost:12379"},
|
||||||
|
key: "foo",
|
||||||
|
items: l,
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
sub.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
14
core/errorx/check.go
Normal 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
70
core/errorx/check_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux || darwin
|
//go:build linux || darwin || freebsd
|
||||||
|
|
||||||
package fs
|
package fs
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
12
core/iox/nopcloser_test.go
Normal file
12
core/iox/nopcloser_test.go
Normal 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())
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
35
core/iox/tee.go
Normal 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
40
core/iox/tee_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
14
core/limit/periodscript.lua
Normal file
14
core/limit/periodscript.lua
Normal 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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
31
core/limit/tokenscript.lua
Normal file
31
core/limit/tokenscript.lua
Normal 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
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]]())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -37,12 +37,19 @@ func Debugf(ctx context.Context, format string, v ...interface{}) {
|
|||||||
getLogger(ctx).Debugf(format, v...)
|
getLogger(ctx).Debugf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debugfn writes fn result into access log.
|
||||||
|
// This is useful when the function is expensive to compute,
|
||||||
|
// and we want to log it only when necessary.
|
||||||
|
func Debugfn(ctx context.Context, fn func() any) {
|
||||||
|
getLogger(ctx).Debugfn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Debugv writes v into access log with json content.
|
// Debugv writes v into access log with json content.
|
||||||
func Debugv(ctx context.Context, v interface{}) {
|
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...)
|
||||||
}
|
}
|
||||||
@@ -57,13 +64,20 @@ func Errorf(ctx context.Context, format string, v ...any) {
|
|||||||
getLogger(ctx).Errorf(fmt.Errorf(format, v...).Error())
|
getLogger(ctx).Errorf(fmt.Errorf(format, v...).Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Errorfn writes fn result into error log.
|
||||||
|
// This is useful when the function is expensive to compute,
|
||||||
|
// and we want to log it only when necessary.
|
||||||
|
func Errorfn(ctx context.Context, fn func() any) {
|
||||||
|
getLogger(ctx).Errorfn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Errorv writes v into error log with json content.
|
// Errorv writes v into error log with json content.
|
||||||
// No call stack attached, because not elegant to pack the messages.
|
// No call stack attached, because not elegant to pack the messages.
|
||||||
func Errorv(ctx context.Context, v any) {
|
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...)
|
||||||
}
|
}
|
||||||
@@ -83,12 +97,19 @@ func Infof(ctx context.Context, format string, v ...any) {
|
|||||||
getLogger(ctx).Infof(format, v...)
|
getLogger(ctx).Infof(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Infofn writes fn result into access log.
|
||||||
|
// This is useful when the function is expensive to compute,
|
||||||
|
// and we want to log it only when necessary.
|
||||||
|
func Infofn(ctx context.Context, fn func() any) {
|
||||||
|
getLogger(ctx).Infofn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Infov writes v into access log with json content.
|
// Infov writes v into access log with json content.
|
||||||
func Infov(ctx context.Context, v any) {
|
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 +129,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)
|
||||||
}
|
}
|
||||||
@@ -126,6 +148,13 @@ func Slowf(ctx context.Context, format string, v ...any) {
|
|||||||
getLogger(ctx).Slowf(format, v...)
|
getLogger(ctx).Slowf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slowfn writes fn result into slow log.
|
||||||
|
// This is useful when the function is expensive to compute,
|
||||||
|
// and we want to log it only when necessary.
|
||||||
|
func Slowfn(ctx context.Context, fn func() any) {
|
||||||
|
getLogger(ctx).Slowfn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Slowv writes v into slow log with json content.
|
// Slowv writes v into slow log with json content.
|
||||||
func Slowv(ctx context.Context, v any) {
|
func Slowv(ctx context.Context, v any) {
|
||||||
getLogger(ctx).Slowv(v)
|
getLogger(ctx).Slowv(v)
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ func TestErrorf(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErrorfn(t *testing.T) {
|
||||||
|
buf := logtest.NewCollector(t)
|
||||||
|
file, line := getFileLine()
|
||||||
|
Errorfn(context.Background(), func() any {
|
||||||
|
return fmt.Sprintf("foo %s", "bar")
|
||||||
|
})
|
||||||
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrorv(t *testing.T) {
|
func TestErrorv(t *testing.T) {
|
||||||
buf := logtest.NewCollector(t)
|
buf := logtest.NewCollector(t)
|
||||||
file, line := getFileLine()
|
file, line := getFileLine()
|
||||||
@@ -77,6 +86,15 @@ func TestInfof(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInfofn(t *testing.T) {
|
||||||
|
buf := logtest.NewCollector(t)
|
||||||
|
file, line := getFileLine()
|
||||||
|
Infofn(context.Background(), func() any {
|
||||||
|
return fmt.Sprintf("foo %s", "bar")
|
||||||
|
})
|
||||||
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestInfov(t *testing.T) {
|
func TestInfov(t *testing.T) {
|
||||||
buf := logtest.NewCollector(t)
|
buf := logtest.NewCollector(t)
|
||||||
file, line := getFileLine()
|
file, line := getFileLine()
|
||||||
@@ -105,6 +123,15 @@ func TestDebugf(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugfn(t *testing.T) {
|
||||||
|
buf := logtest.NewCollector(t)
|
||||||
|
file, line := getFileLine()
|
||||||
|
Debugfn(context.Background(), func() any {
|
||||||
|
return fmt.Sprintf("foo %s", "bar")
|
||||||
|
})
|
||||||
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestDebugv(t *testing.T) {
|
func TestDebugv(t *testing.T) {
|
||||||
buf := logtest.NewCollector(t)
|
buf := logtest.NewCollector(t)
|
||||||
file, line := getFileLine()
|
file, line := getFileLine()
|
||||||
@@ -148,6 +175,15 @@ func TestSlowf(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlowfn(t *testing.T) {
|
||||||
|
buf := logtest.NewCollector(t)
|
||||||
|
file, line := getFileLine()
|
||||||
|
Slowfn(context.Background(), func() any {
|
||||||
|
return fmt.Sprintf("foo %s", "bar")
|
||||||
|
})
|
||||||
|
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestSlowv(t *testing.T) {
|
func TestSlowv(t *testing.T) {
|
||||||
buf := logtest.NewCollector(t)
|
buf := logtest.NewCollector(t)
|
||||||
file, line := getFileLine()
|
file, line := getFileLine()
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ 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.
|
// Debugfn logs a message at debug level.
|
||||||
|
Debugfn(func() any)
|
||||||
|
// 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)
|
||||||
// Errorf logs a message at error level.
|
// Errorf logs a message at error level.
|
||||||
Errorf(string, ...any)
|
Errorf(string, ...any)
|
||||||
|
// Errorfn logs a message at error level.
|
||||||
|
Errorfn(func() any)
|
||||||
// Errorv logs a message at error level.
|
// Errorv logs a message at error level.
|
||||||
Errorv(any)
|
Errorv(any)
|
||||||
// Errorw logs a message at error level.
|
// Errorw logs a message at error level.
|
||||||
@@ -27,6 +31,8 @@ type Logger interface {
|
|||||||
Info(...any)
|
Info(...any)
|
||||||
// Infof logs a message at info level.
|
// Infof logs a message at info level.
|
||||||
Infof(string, ...any)
|
Infof(string, ...any)
|
||||||
|
// Infofn logs a message at info level.
|
||||||
|
Infofn(func() any)
|
||||||
// Infov logs a message at info level.
|
// Infov logs a message at info level.
|
||||||
Infov(any)
|
Infov(any)
|
||||||
// Infow logs a message at info level.
|
// Infow logs a message at info level.
|
||||||
@@ -35,6 +41,8 @@ type Logger interface {
|
|||||||
Slow(...any)
|
Slow(...any)
|
||||||
// Slowf logs a message at slow level.
|
// Slowf logs a message at slow level.
|
||||||
Slowf(string, ...any)
|
Slowf(string, ...any)
|
||||||
|
// Slowfn logs a message at slow level.
|
||||||
|
Slowfn(func() any)
|
||||||
// Slowv logs a message at slow level.
|
// Slowv logs a message at slow level.
|
||||||
Slowv(any)
|
Slowv(any)
|
||||||
// Sloww logs a message at slow level.
|
// Sloww logs a message at slow level.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -80,6 +100,14 @@ func Debugf(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debugfn writes function result into access log if debug level enabled.
|
||||||
|
// This is useful when the function is expensive to call and debug level disabled.
|
||||||
|
func Debugfn(fn func() any) {
|
||||||
|
if shallLog(DebugLevel) {
|
||||||
|
writeDebug(fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debugv writes v into access log with json content.
|
// Debugv writes v into access log with json content.
|
||||||
func Debugv(v any) {
|
func Debugv(v any) {
|
||||||
if shallLog(DebugLevel) {
|
if shallLog(DebugLevel) {
|
||||||
@@ -87,7 +115,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 +124,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{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +147,13 @@ func Errorf(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Errorfn writes function result into error log.
|
||||||
|
func Errorfn(fn func() any) {
|
||||||
|
if shallLog(ErrorLevel) {
|
||||||
|
writeError(fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ErrorStack writes v along with call stack into error log.
|
// ErrorStack writes v along with call stack into error log.
|
||||||
func ErrorStack(v ...any) {
|
func ErrorStack(v ...any) {
|
||||||
if shallLog(ErrorLevel) {
|
if shallLog(ErrorLevel) {
|
||||||
@@ -143,7 +178,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 +189,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 +211,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:
|
||||||
@@ -202,6 +237,14 @@ func Infof(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Infofn writes function result into access log.
|
||||||
|
// This is useful when the function is expensive to call and info level disabled.
|
||||||
|
func Infofn(fn func() any) {
|
||||||
|
if shallLog(InfoLevel) {
|
||||||
|
writeInfo(fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Infov writes v into access log with json content.
|
// Infov writes v into access log with json content.
|
||||||
func Infov(v any) {
|
func Infov(v any) {
|
||||||
if shallLog(InfoLevel) {
|
if shallLog(InfoLevel) {
|
||||||
@@ -209,7 +252,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 +293,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 +317,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 {
|
||||||
@@ -323,6 +371,14 @@ func Slowf(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slowfn writes function result into slow log.
|
||||||
|
// This is useful when the function is expensive to call and slow level disabled.
|
||||||
|
func Slowfn(fn func() any) {
|
||||||
|
if shallLog(ErrorLevel) {
|
||||||
|
writeSlow(fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Slowv writes v into slow log with json content.
|
// Slowv writes v into slow log with json content.
|
||||||
func Slowv(v any) {
|
func Slowv(v any) {
|
||||||
if shallLog(ErrorLevel) {
|
if shallLog(ErrorLevel) {
|
||||||
@@ -414,6 +470,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 {
|
||||||
@@ -478,15 +560,15 @@ func shallLogStat() bool {
|
|||||||
// 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.
|
||||||
func writeDebug(val any, fields ...LogField) {
|
func writeDebug(val any, fields ...LogField) {
|
||||||
getWriter().Debug(val, addCaller(fields...)...)
|
getWriter().Debug(val, mergeGlobalFields(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.
|
||||||
func writeError(val any, fields ...LogField) {
|
func writeError(val any, fields ...LogField) {
|
||||||
getWriter().Error(val, addCaller(fields...)...)
|
getWriter().Error(val, mergeGlobalFields(addCaller(fields...))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeInfo writes v into info log.
|
// writeInfo writes v into info log.
|
||||||
@@ -494,7 +576,7 @@ func writeError(val any, fields ...LogField) {
|
|||||||
// 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.
|
||||||
func writeInfo(val any, fields ...LogField) {
|
func writeInfo(val any, fields ...LogField) {
|
||||||
getWriter().Info(val, addCaller(fields...)...)
|
getWriter().Info(val, mergeGlobalFields(addCaller(fields...))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSevere writes v into severe log.
|
// writeSevere writes v into severe log.
|
||||||
@@ -510,7 +592,7 @@ func writeSevere(msg string) {
|
|||||||
// 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.
|
||||||
func writeSlow(val any, fields ...LogField) {
|
func writeSlow(val any, fields ...LogField) {
|
||||||
getWriter().Slow(val, addCaller(fields...)...)
|
getWriter().Slow(val, mergeGlobalFields(addCaller(fields...))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeStack writes v into stack log.
|
// writeStack writes v into stack log.
|
||||||
@@ -521,10 +603,10 @@ 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.
|
||||||
func writeStat(msg string) {
|
func writeStat(msg string) {
|
||||||
getWriter().Stat(msg, addCaller()...)
|
getWriter().Stat(msg, mergeGlobalFields(addCaller())...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,32 @@ func TestStructedLogDebugf(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructedLogDebugfn(t *testing.T) {
|
||||||
|
t.Run("debugfn with output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLog(t, levelDebug, w, func(v ...any) {
|
||||||
|
Debugfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("debugfn without output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLogEmpty(t, w, InfoLevel, func(v ...any) {
|
||||||
|
Debugfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogDebugv(t *testing.T) {
|
func TestStructedLogDebugv(t *testing.T) {
|
||||||
w := new(mockWriter)
|
w := new(mockWriter)
|
||||||
old := writer.Swap(w)
|
old := writer.Swap(w)
|
||||||
@@ -288,6 +314,32 @@ func TestStructedLogErrorf(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructedLogErrorfn(t *testing.T) {
|
||||||
|
t.Run("errorfn with output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLog(t, levelError, w, func(v ...any) {
|
||||||
|
Errorfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("errorfn without output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLogEmpty(t, w, SevereLevel, func(v ...any) {
|
||||||
|
Errorfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogErrorv(t *testing.T) {
|
func TestStructedLogErrorv(t *testing.T) {
|
||||||
w := new(mockWriter)
|
w := new(mockWriter)
|
||||||
old := writer.Swap(w)
|
old := writer.Swap(w)
|
||||||
@@ -328,6 +380,32 @@ func TestStructedLogInfof(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructedInfofn(t *testing.T) {
|
||||||
|
t.Run("infofn with output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLog(t, levelInfo, w, func(v ...any) {
|
||||||
|
Infofn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("infofn without output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLogEmpty(t, w, ErrorLevel, func(v ...any) {
|
||||||
|
Infofn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogInfov(t *testing.T) {
|
func TestStructedLogInfov(t *testing.T) {
|
||||||
w := new(mockWriter)
|
w := new(mockWriter)
|
||||||
old := writer.Swap(w)
|
old := writer.Swap(w)
|
||||||
@@ -348,6 +426,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)
|
||||||
@@ -430,6 +529,17 @@ func TestStructedLogInfoConsoleText(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInfofnWithErrorLevel(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
SetLevel(ErrorLevel)
|
||||||
|
defer SetLevel(DebugLevel)
|
||||||
|
Infofn(func() any {
|
||||||
|
called = true
|
||||||
|
return "info log"
|
||||||
|
})
|
||||||
|
assert.False(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogSlow(t *testing.T) {
|
func TestStructedLogSlow(t *testing.T) {
|
||||||
w := new(mockWriter)
|
w := new(mockWriter)
|
||||||
old := writer.Swap(w)
|
old := writer.Swap(w)
|
||||||
@@ -450,6 +560,32 @@ func TestStructedLogSlowf(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructedLogSlowfn(t *testing.T) {
|
||||||
|
t.Run("slowfn with output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLog(t, levelSlow, w, func(v ...any) {
|
||||||
|
Slowfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slowfn without output", func(t *testing.T) {
|
||||||
|
w := new(mockWriter)
|
||||||
|
old := writer.Swap(w)
|
||||||
|
defer writer.Store(old)
|
||||||
|
|
||||||
|
doTestStructedLogEmpty(t, w, SevereLevel, func(v ...any) {
|
||||||
|
Slowfn(func() any {
|
||||||
|
return fmt.Sprint(v...)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogSlowv(t *testing.T) {
|
func TestStructedLogSlowv(t *testing.T) {
|
||||||
w := new(mockWriter)
|
w := new(mockWriter)
|
||||||
old := writer.Swap(w)
|
old := writer.Swap(w)
|
||||||
@@ -570,7 +706,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 +794,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 +806,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 +820,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())
|
||||||
@@ -810,15 +962,26 @@ func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...any)) {
|
|||||||
assert.True(t, strings.Contains(w.String(), message))
|
assert.True(t, strings.Contains(w.String(), message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doTestStructedLogEmpty(t *testing.T, w *mockWriter, level uint32, write func(...any)) {
|
||||||
|
olevel := atomic.LoadUint32(&logLevel)
|
||||||
|
SetLevel(level)
|
||||||
|
defer SetLevel(olevel)
|
||||||
|
|
||||||
|
const message = "hello there"
|
||||||
|
write(message)
|
||||||
|
assert.Empty(t, w.String())
|
||||||
|
}
|
||||||
|
|
||||||
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 +1021,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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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. It’s used to avoid stacktrace flooding.
|
- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. It’s 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.
|
||||||
|
|||||||
@@ -41,67 +41,123 @@ 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) Debugfn(fn func() any) {
|
||||||
|
if shallLog(DebugLevel) {
|
||||||
|
l.debug(fn())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) Errorfn(fn func() any) {
|
||||||
|
if shallLog(ErrorLevel) {
|
||||||
|
l.err(fn())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) Infofn(fn func() any) {
|
||||||
|
if shallLog(InfoLevel) {
|
||||||
|
l.info(fn())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) Slowfn(fn func() any) {
|
||||||
|
if shallLog(ErrorLevel) {
|
||||||
|
l.slow(fn())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,28 +165,50 @@ 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 {
|
||||||
fields = append(l.fields, fields...)
|
fields = append(l.fields, fields...)
|
||||||
|
// caller field should always appear together with global fields
|
||||||
fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip)))
|
fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip)))
|
||||||
|
fields = mergeGlobalFields(fields)
|
||||||
|
|
||||||
if l.ctx == nil {
|
if l.ctx == nil {
|
||||||
return fields
|
return fields
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ func TestTraceDebug(t *testing.T) {
|
|||||||
l.WithDuration(time.Second).Debugf(testlog)
|
l.WithDuration(time.Second).Debugf(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
|
l.WithDuration(time.Second).Debugfn(func() any {
|
||||||
|
return testlog
|
||||||
|
})
|
||||||
|
validate(t, w.String(), true, true)
|
||||||
|
w.Reset()
|
||||||
l.WithDuration(time.Second).Debugv(testlog)
|
l.WithDuration(time.Second).Debugv(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
@@ -103,6 +108,11 @@ func TestTraceError(t *testing.T) {
|
|||||||
l.WithDuration(time.Second).Errorf(testlog)
|
l.WithDuration(time.Second).Errorf(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
|
l.WithDuration(time.Second).Errorfn(func() any {
|
||||||
|
return testlog
|
||||||
|
})
|
||||||
|
validate(t, w.String(), true, true)
|
||||||
|
w.Reset()
|
||||||
l.WithDuration(time.Second).Errorv(testlog)
|
l.WithDuration(time.Second).Errorv(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
@@ -140,6 +150,11 @@ func TestTraceInfo(t *testing.T) {
|
|||||||
l.WithDuration(time.Second).Infof(testlog)
|
l.WithDuration(time.Second).Infof(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
|
l.WithDuration(time.Second).Infofn(func() any {
|
||||||
|
return testlog
|
||||||
|
})
|
||||||
|
validate(t, w.String(), true, true)
|
||||||
|
w.Reset()
|
||||||
l.WithDuration(time.Second).Infov(testlog)
|
l.WithDuration(time.Second).Infov(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
@@ -213,6 +228,11 @@ func TestTraceSlow(t *testing.T) {
|
|||||||
l.WithDuration(time.Second).Slowf(testlog)
|
l.WithDuration(time.Second).Slowf(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
|
l.WithDuration(time.Second).Slowfn(func() any {
|
||||||
|
return testlog
|
||||||
|
})
|
||||||
|
validate(t, w.String(), true, true)
|
||||||
|
w.Reset()
|
||||||
l.WithDuration(time.Second).Slowv(testlog)
|
l.WithDuration(time.Second).Slowv(testlog)
|
||||||
validate(t, w.String(), true, true)
|
validate(t, w.String(), true, true)
|
||||||
w.Reset()
|
w.Reset()
|
||||||
@@ -287,6 +307,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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,37 @@ 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 (
|
||||||
|
// Writer is the interface for writing logs.
|
||||||
|
// It's designed to let users customize their own log writer,
|
||||||
|
// such as writing logs to a kafka, a database, or using third-party loggers.
|
||||||
Writer interface {
|
Writer interface {
|
||||||
|
// Alert sends an alert message, if your writer implemented alerting functionality.
|
||||||
Alert(v any)
|
Alert(v any)
|
||||||
|
// Close closes the writer.
|
||||||
Close() error
|
Close() error
|
||||||
|
// Debug logs a message at debug level.
|
||||||
Debug(v any, fields ...LogField)
|
Debug(v any, fields ...LogField)
|
||||||
|
// Error logs a message at error level.
|
||||||
Error(v any, fields ...LogField)
|
Error(v any, fields ...LogField)
|
||||||
|
// Info logs a message at info level.
|
||||||
Info(v any, fields ...LogField)
|
Info(v any, fields ...LogField)
|
||||||
|
// Severe logs a message at severe level.
|
||||||
Severe(v any)
|
Severe(v any)
|
||||||
|
// Slow logs a message at slow level.
|
||||||
Slow(v any, fields ...LogField)
|
Slow(v any, fields ...LogField)
|
||||||
|
// Stack logs a message at error level.
|
||||||
Stack(v any)
|
Stack(v any)
|
||||||
|
// Stat logs a message at stat level.
|
||||||
Stat(v any, fields ...LogField)
|
Stat(v any, fields ...LogField)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +46,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 +105,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,17 +327,30 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func combineGlobalFields(fields []LogField) []LogField {
|
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 mergeGlobalFields(fields []LogField) []LogField {
|
||||||
globals := globalFields.Load()
|
globals := globalFields.Load()
|
||||||
if globals == nil {
|
if globals == nil {
|
||||||
return fields
|
return fields
|
||||||
@@ -287,16 +374,17 @@ func output(writer io.Writer, level string, val any, fields ...LogField) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +419,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 +474,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ 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.
|
||||||
|
// support anonymous field, e.g.:
|
||||||
|
//
|
||||||
|
// type Foo struct {
|
||||||
|
// Token string `header:"token"`
|
||||||
|
// }
|
||||||
|
// type FooB struct {
|
||||||
|
// Foo
|
||||||
|
// Bar string `json:"bar"`
|
||||||
|
// }
|
||||||
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)
|
||||||
@@ -44,6 +53,16 @@ func getTag(field reflect.StructField) (string, bool) {
|
|||||||
return strings.TrimSpace(tag), false
|
return strings.TrimSpace(tag), false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insertValue(collector map[string]map[string]any, tag string, key string, val any) {
|
||||||
|
if m, ok := collector[tag]; ok {
|
||||||
|
m[key] = val
|
||||||
|
} else {
|
||||||
|
collector[tag] = map[string]any{
|
||||||
|
key: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func processMember(field reflect.StructField, value reflect.Value,
|
func processMember(field reflect.StructField, value reflect.Value,
|
||||||
collector map[string]map[string]any) error {
|
collector map[string]map[string]any) error {
|
||||||
var key string
|
var key string
|
||||||
@@ -69,15 +88,20 @@ func processMember(field reflect.StructField, value reflect.Value,
|
|||||||
val = fmt.Sprint(val)
|
val = fmt.Sprint(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
m, ok := collector[tag]
|
if field.Anonymous {
|
||||||
if ok {
|
anonCollector, err := Marshal(val)
|
||||||
m[key] = val
|
if err != nil {
|
||||||
} else {
|
return err
|
||||||
m = map[string]any{
|
|
||||||
key: val,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for anonTag, anonMap := range anonCollector {
|
||||||
|
for anonKey, anonVal := range anonMap {
|
||||||
|
insertValue(collector, anonTag, anonKey, anonVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
insertValue(collector, tag, key, val)
|
||||||
}
|
}
|
||||||
collector[tag] = m
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,124 @@ func TestMarshal(t *testing.T) {
|
|||||||
assert.True(t, m[emptyTag]["Anonymous"].(bool))
|
assert.True(t, m[emptyTag]["Anonymous"].(bool))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarshal_Anonymous(t *testing.T) {
|
||||||
|
t.Run("anonymous", func(t *testing.T) {
|
||||||
|
type BaseHeader struct {
|
||||||
|
Token string `header:"token"`
|
||||||
|
}
|
||||||
|
v := struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address,options=[beijing,shanghai]"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
BaseHeader
|
||||||
|
}{
|
||||||
|
Name: "kevin",
|
||||||
|
Address: "shanghai",
|
||||||
|
Age: 20,
|
||||||
|
BaseHeader: BaseHeader{
|
||||||
|
Token: "token_xxx",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m, err := Marshal(v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "kevin", m["json"]["name"])
|
||||||
|
assert.Equal(t, "shanghai", m["json"]["address"])
|
||||||
|
assert.Equal(t, 20, m["json"]["age"].(int))
|
||||||
|
assert.Equal(t, "token_xxx", m["header"]["token"])
|
||||||
|
|
||||||
|
v1 := struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address,options=[beijing,shanghai]"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
BaseHeader
|
||||||
|
}{
|
||||||
|
Name: "kevin",
|
||||||
|
Address: "shanghai",
|
||||||
|
Age: 20,
|
||||||
|
}
|
||||||
|
m1, err1 := Marshal(v1)
|
||||||
|
assert.Nil(t, err1)
|
||||||
|
assert.Equal(t, "kevin", m1["json"]["name"])
|
||||||
|
assert.Equal(t, "shanghai", m1["json"]["address"])
|
||||||
|
assert.Equal(t, 20, m1["json"]["age"].(int))
|
||||||
|
|
||||||
|
type AnotherHeader struct {
|
||||||
|
Version string `header:"version"`
|
||||||
|
}
|
||||||
|
v2 := struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address,options=[beijing,shanghai]"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
BaseHeader
|
||||||
|
AnotherHeader
|
||||||
|
}{
|
||||||
|
Name: "kevin",
|
||||||
|
Address: "shanghai",
|
||||||
|
Age: 20,
|
||||||
|
BaseHeader: BaseHeader{
|
||||||
|
Token: "token_xxx",
|
||||||
|
},
|
||||||
|
AnotherHeader: AnotherHeader{
|
||||||
|
Version: "v1.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m2, err2 := Marshal(v2)
|
||||||
|
assert.Nil(t, err2)
|
||||||
|
assert.Equal(t, "kevin", m2["json"]["name"])
|
||||||
|
assert.Equal(t, "shanghai", m2["json"]["address"])
|
||||||
|
assert.Equal(t, 20, m2["json"]["age"].(int))
|
||||||
|
assert.Equal(t, "token_xxx", m2["header"]["token"])
|
||||||
|
assert.Equal(t, "v1.0", m2["header"]["version"])
|
||||||
|
|
||||||
|
type PointerHeader struct {
|
||||||
|
Ref *string `header:"ref"`
|
||||||
|
}
|
||||||
|
ref := "reference"
|
||||||
|
v3 := struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address,options=[beijing,shanghai]"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
PointerHeader
|
||||||
|
}{
|
||||||
|
Name: "kevin",
|
||||||
|
Address: "shanghai",
|
||||||
|
Age: 20,
|
||||||
|
PointerHeader: PointerHeader{
|
||||||
|
Ref: &ref,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m3, err3 := Marshal(v3)
|
||||||
|
assert.Nil(t, err3)
|
||||||
|
assert.Equal(t, "kevin", m3["json"]["name"])
|
||||||
|
assert.Equal(t, "shanghai", m3["json"]["address"])
|
||||||
|
assert.Equal(t, 20, m3["json"]["age"].(int))
|
||||||
|
assert.Equal(t, "reference", *m3["header"]["ref"].(*string))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad anonymous", func(t *testing.T) {
|
||||||
|
type BaseHeader struct {
|
||||||
|
Token string `json:"token,options=[a,b]"`
|
||||||
|
}
|
||||||
|
|
||||||
|
v := struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address,options=[beijing,shanghai]"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
BaseHeader
|
||||||
|
}{
|
||||||
|
Name: "kevin",
|
||||||
|
Address: "shanghai",
|
||||||
|
Age: 20,
|
||||||
|
BaseHeader: BaseHeader{
|
||||||
|
Token: "c",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Marshal(v)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshal_Ptr(t *testing.T) {
|
func TestMarshal_Ptr(t *testing.T) {
|
||||||
v := &struct {
|
v := &struct {
|
||||||
Name string `path:"name"`
|
Name string `path:"name"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mapping
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding"
|
"encoding"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -18,8 +19,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultKeyName = "key"
|
defaultKeyName = "key"
|
||||||
delimiter = '.'
|
delimiter = '.'
|
||||||
|
ignoreKey = "-"
|
||||||
|
numberTypeString = "number"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -37,7 +40,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
|
||||||
@@ -48,6 +51,7 @@ type (
|
|||||||
|
|
||||||
unmarshalOptions struct {
|
unmarshalOptions struct {
|
||||||
fillDefault bool
|
fillDefault bool
|
||||||
|
fromArray bool
|
||||||
fromString bool
|
fromString bool
|
||||||
opaqueKeys bool
|
opaqueKeys bool
|
||||||
canonicalKey func(key string) string
|
canonicalKey func(key string) string
|
||||||
@@ -67,7 +71,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -77,41 +81,13 @@ func (u *Unmarshaler) Unmarshal(i, v any) error {
|
|||||||
return u.unmarshal(i, v, "")
|
return u.unmarshal(i, v, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
|
|
||||||
valueType := reflect.TypeOf(v)
|
|
||||||
if valueType.Kind() != reflect.Ptr {
|
|
||||||
return errValueNotSettable
|
|
||||||
}
|
|
||||||
|
|
||||||
elemType := Deref(valueType)
|
|
||||||
switch iv := i.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if elemType.Kind() != reflect.Struct {
|
|
||||||
return errTypeMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.unmarshalValuer(mapValuer(iv), v, fullName)
|
|
||||||
case []any:
|
|
||||||
if elemType.Kind() != reflect.Slice {
|
|
||||||
return errTypeMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
|
|
||||||
default:
|
|
||||||
return errUnsupportedType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.unmarshalValuer(simpleValuer{current: m}, v, "")
|
return u.unmarshalValuer(simpleValuer{current: m}, v, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
|
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value,
|
||||||
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
|
mapValue any, fullName string) error {
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
|
|
||||||
if !value.CanSet() {
|
if !value.CanSet() {
|
||||||
return errValueNotSettable
|
return errValueNotSettable
|
||||||
}
|
}
|
||||||
@@ -152,7 +128,8 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
|
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||||
|
mapValue any, fullName string) error {
|
||||||
if !value.CanSet() {
|
if !value.CanSet() {
|
||||||
return errValueNotSettable
|
return errValueNotSettable
|
||||||
}
|
}
|
||||||
@@ -168,13 +145,14 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
|||||||
baseType := fieldType.Elem()
|
baseType := fieldType.Elem()
|
||||||
dereffedBaseType := Deref(baseType)
|
dereffedBaseType := Deref(baseType)
|
||||||
dereffedBaseKind := dereffedBaseType.Kind()
|
dereffedBaseKind := dereffedBaseType.Kind()
|
||||||
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
|
|
||||||
if refValue.Len() == 0 {
|
if refValue.Len() == 0 {
|
||||||
value.Set(conv)
|
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var valid bool
|
var valid bool
|
||||||
|
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
|
||||||
|
|
||||||
for i := 0; i < refValue.Len(); i++ {
|
for i := 0; i < refValue.Len(); i++ {
|
||||||
ithValue := refValue.Index(i).Interface()
|
ithValue := refValue.Index(i).Interface()
|
||||||
if ithValue == nil {
|
if ithValue == nil {
|
||||||
@@ -186,17 +164,9 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
|||||||
|
|
||||||
switch dereffedBaseKind {
|
switch dereffedBaseKind {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
target := reflect.New(dereffedBaseType)
|
if err := u.fillStructElement(baseType, conv.Index(i), ithValue, sliceFullName); err != nil {
|
||||||
val, ok := ithValue.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return errTypeMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
|
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
|
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -221,17 +191,17 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
baseFieldType := Deref(fieldType.Elem())
|
baseFieldType := fieldType.Elem()
|
||||||
baseFieldKind := baseFieldType.Kind()
|
baseFieldKind := baseFieldType.Kind()
|
||||||
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
||||||
|
|
||||||
@@ -247,30 +217,44 @@ 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, fullName string) error {
|
baseKind reflect.Kind, value any, fullName string) error {
|
||||||
|
if value == nil {
|
||||||
|
return errNilSliceElement
|
||||||
|
}
|
||||||
|
|
||||||
ithVal := slice.Index(index)
|
ithVal := slice.Index(index)
|
||||||
|
ithValType := ithVal.Type()
|
||||||
|
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case fmt.Stringer:
|
case fmt.Stringer:
|
||||||
return setValueFromString(baseKind, ithVal, v.String())
|
return setValueFromString(baseKind, ithVal, v.String())
|
||||||
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, fullName)
|
// deref to handle both pointer and non-pointer types.
|
||||||
|
switch Deref(ithValType).Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return u.fillStructElement(ithValType, ithVal, v, fullName)
|
||||||
|
case reflect.Map:
|
||||||
|
return u.fillMap(ithValType, ithVal, value, fullName)
|
||||||
|
default:
|
||||||
|
return errTypeMismatch
|
||||||
|
}
|
||||||
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.
|
||||||
if ithVal.Kind() == reflect.Ptr {
|
if ithVal.Kind() == reflect.Ptr {
|
||||||
baseType := Deref(ithVal.Type())
|
baseType := Deref(ithValType)
|
||||||
if !reflect.TypeOf(value).AssignableTo(baseType) {
|
if !reflect.TypeOf(value).AssignableTo(baseType) {
|
||||||
return errTypeMismatch
|
return errTypeMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
target := reflect.New(baseType).Elem()
|
target := reflect.New(baseType).Elem()
|
||||||
target.Set(reflect.ValueOf(value))
|
target.Set(reflect.ValueOf(value))
|
||||||
SetValue(ithVal.Type(), ithVal, target)
|
SetValue(ithValType, ithVal, target)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.TypeOf(value).AssignableTo(ithVal.Type()) {
|
if !reflect.TypeOf(value).AssignableTo(ithValType) {
|
||||||
return errTypeMismatch
|
return errTypeMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +285,51 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
|
|||||||
return u.fillSlice(derefedType, value, slice, fullName)
|
return u.fillSlice(derefedType, value, slice, fullName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any, fullName string) (reflect.Value, error) {
|
func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
|
||||||
|
value any, fullName string) error {
|
||||||
|
val, ok := value.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return errTypeMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// use Deref(baseType) to get the base type in case the type is a pointer type.
|
||||||
|
ptr := reflect.New(Deref(baseType))
|
||||||
|
if err := u.unmarshal(val, ptr.Interface(), fullName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
SetValue(baseType, target, ptr.Elem())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -393,6 +421,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)
|
||||||
@@ -422,6 +459,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
|
||||||
@@ -437,6 +478,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)
|
||||||
}
|
}
|
||||||
@@ -559,9 +604,27 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
|
|||||||
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:
|
||||||
|
// try to find out if it's a byte slice,
|
||||||
|
// more details https://pkg.go.dev/encoding/json#Marshal
|
||||||
|
// array and slice values encode as JSON arrays,
|
||||||
|
// except that []byte encodes as a base64-encoded string,
|
||||||
|
// and a nil slice encoded as the null JSON value.
|
||||||
|
// https://stackoverflow.com/questions/34089750/marshal-byte-to-json-giving-a-strange-string
|
||||||
|
if fieldType.Elem().Kind() == reflect.Uint8 {
|
||||||
|
// check whether string type, because the kind of some other types can be string
|
||||||
|
if strVal, ok := mapValue.(string); ok {
|
||||||
|
if decodedBytes, err := base64.StdEncoding.DecodeString(strVal); err == nil {
|
||||||
|
value.Set(reflect.ValueOf(decodedBytes))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
||||||
case valueKind == reflect.String && derefedFieldType == durationType:
|
case valueKind == reflect.String && derefedFieldType == durationType:
|
||||||
return fillDurationValue(fieldType, value, mapValue.(string))
|
return fillDurationValue(fieldType, value, mapValue.(string))
|
||||||
|
case valueKind == reflect.String && typeKind == reflect.Struct && u.implementsUnmarshaler(fieldType):
|
||||||
|
return u.fillUnmarshalerStruct(fieldType, value, mapValue.(string))
|
||||||
default:
|
default:
|
||||||
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
|
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
|
||||||
}
|
}
|
||||||
@@ -604,25 +667,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
|
||||||
@@ -630,7 +696,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)
|
||||||
@@ -724,6 +790,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)
|
||||||
@@ -750,6 +820,19 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
|||||||
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
|
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.opts.fromArray {
|
||||||
|
fieldKind := field.Type.Kind()
|
||||||
|
if fieldKind != reflect.Slice && fieldKind != reflect.Array {
|
||||||
|
valueKind := reflect.TypeOf(mapValue).Kind()
|
||||||
|
if valueKind == reflect.Slice || valueKind == reflect.Array {
|
||||||
|
val := reflect.ValueOf(mapValue)
|
||||||
|
if val.Len() > 0 {
|
||||||
|
mapValue = val.Index(0).Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
|
return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
|
||||||
value: mapValue,
|
value: mapValue,
|
||||||
parent: valuer,
|
parent: valuer,
|
||||||
@@ -877,6 +960,35 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
|
||||||
|
valueType := reflect.TypeOf(v)
|
||||||
|
if valueType.Kind() != reflect.Ptr {
|
||||||
|
return errValueNotSettable
|
||||||
|
}
|
||||||
|
|
||||||
|
elemType := Deref(valueType)
|
||||||
|
switch iv := i.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
if elemType.Kind() != reflect.Struct {
|
||||||
|
return errTypeMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.unmarshalValuer(mapValuer(iv), v, fullName)
|
||||||
|
case []any:
|
||||||
|
if elemType.Kind() != reflect.Slice {
|
||||||
|
return errTypeMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
|
||||||
|
default:
|
||||||
|
return errUnsupportedType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
|
||||||
|
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -929,6 +1041,16 @@ func WithDefault() UnmarshalOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFromArray customizes an Unmarshaler with converting array values to non-array types.
|
||||||
|
// For example, if the field type is []string, and the value is [hello],
|
||||||
|
// the field type can be `string`, instead of `[]string`.
|
||||||
|
// Typically, this option is used for unmarshaling from form values.
|
||||||
|
func WithFromArray() UnmarshalOption {
|
||||||
|
return func(opt *unmarshalOptions) {
|
||||||
|
opt.fromArray = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
|
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
|
||||||
// Opaque keys are keys that are not processed by the unmarshaler.
|
// Opaque keys are keys that are not processed by the unmarshaler.
|
||||||
func WithOpaqueKeys() UnmarshalOption {
|
func WithOpaqueKeys() UnmarshalOption {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user