mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-11 00:40:00 +08:00
Compare commits
651 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290de6aa96 | ||
|
|
a7aeb8ac0e | ||
|
|
a8e7fafebf | ||
|
|
7cc64070b1 | ||
|
|
c19d2637ea | ||
|
|
fe1da14332 | ||
|
|
8e9110cedf | ||
|
|
d6ff30a570 | ||
|
|
b98d46bfd6 | ||
|
|
768936b256 | ||
|
|
c6eb1a9670 | ||
|
|
e4ab518576 | ||
|
|
dfc67b5fac | ||
|
|
62266d8f91 | ||
|
|
b8ea16a88e | ||
|
|
23deaf50e6 | ||
|
|
38a36ed8d3 | ||
|
|
49bab23c54 | ||
|
|
78ba00d3a7 | ||
|
|
787b046a70 | ||
|
|
f827a7b985 | ||
|
|
f5f2097d14 | ||
|
|
cfcfb87fd4 | ||
|
|
1d223fc114 | ||
|
|
c0647f0719 | ||
|
|
8745ed9c61 | ||
|
|
836726e710 | ||
|
|
a67c118dcf | ||
|
|
cd289465fd | ||
|
|
263e426ae1 | ||
|
|
d5e493383a | ||
|
|
6f1d27354a | ||
|
|
26101732d2 | ||
|
|
71d40e0c08 | ||
|
|
4ba2ff7cdd | ||
|
|
2cdf5e7395 | ||
|
|
8315a55b3f | ||
|
|
d1c2a31af7 | ||
|
|
3e6c217408 | ||
|
|
b299f350be | ||
|
|
8fd16c17dc | ||
|
|
5979b2aa0f | ||
|
|
0b17e0e5d9 | ||
|
|
3d8ad5e4f6 | ||
|
|
ff1752dd39 | ||
|
|
1becaeb7be | ||
|
|
171afaadb9 | ||
|
|
776e6e647d | ||
|
|
4ccdf4ec72 | ||
|
|
a7bd993c0c | ||
|
|
a290ff4486 | ||
|
|
490ef13822 | ||
|
|
1b14de2ff9 | ||
|
|
914692cc82 | ||
|
|
07191dc430 | ||
|
|
af3fb2b04d | ||
|
|
0240fa131a | ||
|
|
e96577dd38 | ||
|
|
403dd7367a | ||
|
|
8086ad120b | ||
|
|
87a445689c | ||
|
|
b6bda54870 | ||
|
|
9d528dddd6 | ||
|
|
543d590710 | ||
|
|
f1d70eb6b2 | ||
|
|
d828c3f37e | ||
|
|
038491b7bc | ||
|
|
cf683411ee | ||
|
|
de5ed6a677 | ||
|
|
3dda557410 | ||
|
|
c800f6f723 | ||
|
|
0395ba1816 | ||
|
|
86f9f63b46 | ||
|
|
a7a6753118 | ||
|
|
2e80d12d6a | ||
|
|
417a96cbf2 | ||
|
|
2d4c29ea7c | ||
|
|
67db40ed4f | ||
|
|
11c485a5ed | ||
|
|
b0573af9a9 | ||
|
|
09eb53f308 | ||
|
|
11f85d1b80 | ||
|
|
0cb86c6990 | ||
|
|
57d2f22c24 | ||
|
|
fa0c364982 | ||
|
|
a6c8113419 | ||
|
|
4f5c30e083 | ||
|
|
9d0b51fa26 | ||
|
|
ba5f8045a2 | ||
|
|
3a510a9138 | ||
|
|
d3bfa16813 | ||
|
|
28409791fa | ||
|
|
c1abe87953 | ||
|
|
f8367856e8 | ||
|
|
a72b0a689b | ||
|
|
69a4d213a3 | ||
|
|
c28e01fed3 | ||
|
|
e8efcef108 | ||
|
|
d011316997 | ||
|
|
4d22b0c497 | ||
|
|
539215d7df | ||
|
|
3ede597a15 | ||
|
|
01786c5e63 | ||
|
|
6aba5f74fc | ||
|
|
3c894a3fb7 | ||
|
|
1ece3a498f | ||
|
|
b76c7ae55d | ||
|
|
91b10bd3b9 | ||
|
|
7e3fe77e7b | ||
|
|
ba43214dae | ||
|
|
ebc90720ea | ||
|
|
785d100be9 | ||
|
|
f13e6f1149 | ||
|
|
8be0f77d96 | ||
|
|
429f85a9de | ||
|
|
b4d1c6da2c | ||
|
|
3c1cfd4c1e | ||
|
|
a71a210704 | ||
|
|
769d06c8ab | ||
|
|
cd1f8da13f | ||
|
|
8230474667 | ||
|
|
27f553bf84 | ||
|
|
d48bff8c8b | ||
|
|
59b9687f31 | ||
|
|
c1a8ccda11 | ||
|
|
9df6786b09 | ||
|
|
bef5bd4e4f | ||
|
|
68acfb1891 | ||
|
|
9fd3f752d1 | ||
|
|
9c48e9ceab | ||
|
|
bd26783b33 | ||
|
|
eda8230521 | ||
|
|
462ddbb145 | ||
|
|
496a2f341e | ||
|
|
7109d6d635 | ||
|
|
ca72241fa3 | ||
|
|
a6bdffd225 | ||
|
|
5636bf4955 | ||
|
|
a944a7fd7e | ||
|
|
a40fa405e4 | ||
|
|
eab77e21dd | ||
|
|
d41163f5c1 | ||
|
|
265b1f2459 | ||
|
|
c92ea59228 | ||
|
|
afddfea093 | ||
|
|
fa4dc151ca | ||
|
|
44202acb18 | ||
|
|
cf00786209 | ||
|
|
6a8638fc85 | ||
|
|
837a9ffa03 | ||
|
|
d28cfe5f20 | ||
|
|
022c100dc9 | ||
|
|
426b09c356 | ||
|
|
40dc21e4cf | ||
|
|
9b114e3251 | ||
|
|
4c6234f108 | ||
|
|
3cdfcb05f1 | ||
|
|
9f5bfa0088 | ||
|
|
2d42c8fa00 | ||
|
|
10e7922597 | ||
|
|
6e34b55ba7 | ||
|
|
ed15ca04f4 | ||
|
|
295ec27e1b | ||
|
|
d1e702e8a3 | ||
|
|
d1bfb5ef61 | ||
|
|
e43357164c | ||
|
|
cd21c9fa74 | ||
|
|
cdd2fcbbc9 | ||
|
|
8d2db09d45 | ||
|
|
65905b914d | ||
|
|
80e3407be1 | ||
|
|
657d27213a | ||
|
|
8ac18a9422 | ||
|
|
d3ae9cfd49 | ||
|
|
d7f42161fd | ||
|
|
e03229cabe | ||
|
|
8403ed16ae | ||
|
|
d87d203c3b | ||
|
|
3ae6a882a7 | ||
|
|
41c980f00c | ||
|
|
f34d81ca2c | ||
|
|
004ee488a6 | ||
|
|
2e12cd2c99 | ||
|
|
2695c30886 | ||
|
|
c74fb988e0 | ||
|
|
e8a340c1c0 | ||
|
|
06e114e5a3 | ||
|
|
74ad681a66 | ||
|
|
e7bbc09093 | ||
|
|
1eb1450c43 | ||
|
|
9a724fe907 | ||
|
|
30e49f2939 | ||
|
|
a5407479a6 | ||
|
|
7fb5bab26b | ||
|
|
27249e021f | ||
|
|
d809795fec | ||
|
|
c9db9588b7 | ||
|
|
872c50b71a | ||
|
|
7c83155e4f | ||
|
|
358d86b8ae | ||
|
|
f4bb9f5635 | ||
|
|
5c6a3132eb | ||
|
|
2bd95aa007 | ||
|
|
e8376936d5 | ||
|
|
71c0288023 | ||
|
|
9e2f07a842 | ||
|
|
24fd34413f | ||
|
|
3f47251892 | ||
|
|
0b6bc69afa | ||
|
|
5b9bdc8d02 | ||
|
|
ded22e296e | ||
|
|
f0ed2370a3 | ||
|
|
6bf6cfdd01 | ||
|
|
5cc9eb0de4 | ||
|
|
f070d447ef | ||
|
|
f6d9e19ecb | ||
|
|
56807aabf6 | ||
|
|
861dcf2f36 | ||
|
|
c837dc21bb | ||
|
|
96a35ecf1a | ||
|
|
bdec5f2349 | ||
|
|
bc92b57bdb | ||
|
|
d8905b9e9e | ||
|
|
dec6309c55 | ||
|
|
10805577f5 | ||
|
|
a4d8286e36 | ||
|
|
84d2b64e7c | ||
|
|
6476da4a18 | ||
|
|
79eab0ea2f | ||
|
|
3b683fd498 | ||
|
|
d179b342b2 | ||
|
|
58874779e7 | ||
|
|
8829c31c0d | ||
|
|
b42f3fa047 | ||
|
|
9bdadf2381 | ||
|
|
20f665ede8 | ||
|
|
0325d8e92d | ||
|
|
2125977281 | ||
|
|
c26c187e11 | ||
|
|
4ef1859f0b | ||
|
|
407a6cbf9c | ||
|
|
76fc1ef460 | ||
|
|
423955c55f | ||
|
|
db95b3f0e3 | ||
|
|
4bee60eb7f | ||
|
|
7618139dad | ||
|
|
6fd08027ff | ||
|
|
b9e268aae8 | ||
|
|
4c1bb1148b | ||
|
|
50a6bbe6b9 | ||
|
|
dfb3cb510a | ||
|
|
519db812b4 | ||
|
|
3203f8e06b | ||
|
|
b71ac2042a | ||
|
|
d0f9e57022 | ||
|
|
aa68210cde | ||
|
|
280e837c9e | ||
|
|
f669e1226c | ||
|
|
cd15c19250 | ||
|
|
5b35fa17de | ||
|
|
9672298fa8 | ||
|
|
bf3ce16823 | ||
|
|
189721da16 | ||
|
|
a523ab1f93 | ||
|
|
7ea8b636d9 | ||
|
|
b2fea65faa | ||
|
|
a1fe8bf6cd | ||
|
|
67ee9e4391 | ||
|
|
9c1ee50497 | ||
|
|
7c842f22d0 | ||
|
|
14ec29991c | ||
|
|
c7f5aad83a | ||
|
|
e77747cff8 | ||
|
|
f2612db4b1 | ||
|
|
a21ff71373 | ||
|
|
fc04ad7854 | ||
|
|
fbf2eebc42 | ||
|
|
dc43430812 | ||
|
|
c6642bc2e6 | ||
|
|
bdca24dd3b | ||
|
|
00c5734021 | ||
|
|
33f87cf1f0 | ||
|
|
69935c1ba3 | ||
|
|
1fb356f328 | ||
|
|
0b0406f41a | ||
|
|
cc264dcf55 | ||
|
|
e024aebb66 | ||
|
|
f204729482 | ||
|
|
d20cf56a69 | ||
|
|
54d57c7d4b | ||
|
|
28a7c9d38f | ||
|
|
872e75e10d | ||
|
|
af1730079e | ||
|
|
04521e2d24 | ||
|
|
02adcccbf4 | ||
|
|
a74aaf1823 | ||
|
|
1eb2089c69 | ||
|
|
f7f3730e1a | ||
|
|
0ee7654407 | ||
|
|
16cc990fdd | ||
|
|
00061c2e5b | ||
|
|
6793f7a1de | ||
|
|
c8428a7f65 | ||
|
|
a5e1d0d0dc | ||
|
|
8270c7deed | ||
|
|
9f4a882a1b | ||
|
|
cb7b7cb72e | ||
|
|
603c93aa4a | ||
|
|
cb8d9d413a | ||
|
|
ff7443c6a7 | ||
|
|
b812e74d6f | ||
|
|
089cdaa75f | ||
|
|
476026e393 | ||
|
|
75952308f9 | ||
|
|
df0550d6dc | ||
|
|
e481b63b21 | ||
|
|
e47079f0f4 | ||
|
|
9b2a279948 | ||
|
|
db87fd3239 | ||
|
|
598fda0c97 | ||
|
|
b0e335e7b0 | ||
|
|
efdf475da4 | ||
|
|
22a1315136 | ||
|
|
5b22823018 | ||
|
|
9ccb997ed8 | ||
|
|
01c92a6bc5 | ||
|
|
c9a2a60e28 | ||
|
|
b0739d63c0 | ||
|
|
c22f84cb5f | ||
|
|
60450bab02 | ||
|
|
3e8cec5c78 | ||
|
|
74ee163761 | ||
|
|
ea4f680052 | ||
|
|
58cdba2c5d | ||
|
|
a2fbc14c70 | ||
|
|
158df8c270 | ||
|
|
30ec236a87 | ||
|
|
ac3653b3f9 | ||
|
|
8520db4fd9 | ||
|
|
14141fed62 | ||
|
|
5d86cc2f20 | ||
|
|
8a6e4b7580 | ||
|
|
453f949638 | ||
|
|
75a330184d | ||
|
|
546fcd8bab | ||
|
|
3022f93b6d | ||
|
|
8ffc392c66 | ||
|
|
ae7d85dadf | ||
|
|
e89268ac37 | ||
|
|
aaa3623404 | ||
|
|
8998f16054 | ||
|
|
94417be018 | ||
|
|
f300408fc0 | ||
|
|
aaa39e17a3 | ||
|
|
73906f996d | ||
|
|
73417f54db | ||
|
|
491213afb8 | ||
|
|
edf743cd72 | ||
|
|
78a88be787 | ||
|
|
9f6a574f97 | ||
|
|
ea01cc78f0 | ||
|
|
a87978568a | ||
|
|
14cecb9b31 | ||
|
|
0ce54100a4 | ||
|
|
d28ac35ff7 | ||
|
|
a5962f677f | ||
|
|
8478474f7f | ||
|
|
df5ae9507f | ||
|
|
faf4d7e3bb | ||
|
|
f64fe5eb5e | ||
|
|
97d889103a | ||
|
|
9a44310d00 | ||
|
|
06eeef2cf3 | ||
|
|
9adc7d4cb9 | ||
|
|
006f78c3d5 | ||
|
|
64a8e65f4a | ||
|
|
8fd1e76d29 | ||
|
|
0466af5e49 | ||
|
|
7405d7f506 | ||
|
|
afd9ff889e | ||
|
|
7e087de6e6 | ||
|
|
5aded99df5 | ||
|
|
08fb980ad2 | ||
|
|
b94d7aa532 | ||
|
|
ee630b8b57 | ||
|
|
bd82b7d8de | ||
|
|
3d729c77a6 | ||
|
|
e944b59bb3 | ||
|
|
54b5e3f4b2 | ||
|
|
b913229028 | ||
|
|
9963ffb1c1 | ||
|
|
8cb6490724 | ||
|
|
05e37ee20f | ||
|
|
d88da4cc88 | ||
|
|
425430f67c | ||
|
|
4e0d91f6c0 | ||
|
|
8584351b6d | ||
|
|
b19c5223a9 | ||
|
|
99a2d95433 | ||
|
|
9db222bf5b | ||
|
|
ac648d08cb | ||
|
|
6df7fa619c | ||
|
|
bbb4ce586f | ||
|
|
888551627c | ||
|
|
bd623aaac3 | ||
|
|
9e6c2ba2c0 | ||
|
|
c0db8d017d | ||
|
|
52b4f8ca91 | ||
|
|
4884a7b3c6 | ||
|
|
3c6951577d | ||
|
|
fcd15c9b17 | ||
|
|
155e6061cb | ||
|
|
dda7666097 | ||
|
|
c954568b61 | ||
|
|
c2acc43a52 | ||
|
|
1a1a6f5239 | ||
|
|
60c7edf8f8 | ||
|
|
7ad86a52f3 | ||
|
|
1e4e5a02b2 | ||
|
|
39540e21d2 | ||
|
|
b321622c95 | ||
|
|
a25cba5380 | ||
|
|
f01472c9ea | ||
|
|
af531cf264 | ||
|
|
c4b2cddef7 | ||
|
|
51de0d0620 | ||
|
|
dd393351cc | ||
|
|
655ae8034c | ||
|
|
d894b88c3e | ||
|
|
791e76bcf0 | ||
|
|
c566b5ff82 | ||
|
|
490241d639 | ||
|
|
f02711a9cb | ||
|
|
ad32f9de23 | ||
|
|
f309e9f80c | ||
|
|
2087ac1e89 | ||
|
|
e6ef1fca12 | ||
|
|
ef146cf5ba | ||
|
|
04b0f26182 | ||
|
|
acdaee0fb6 | ||
|
|
56ad4776d4 | ||
|
|
904d168f18 | ||
|
|
4bd4981bfb | ||
|
|
90562df826 | ||
|
|
497762ab47 | ||
|
|
6e4c98e52d | ||
|
|
b4bb5c0323 | ||
|
|
a58fac9000 | ||
|
|
d84e3d4b53 | ||
|
|
221f923fae | ||
|
|
bbb9126302 | ||
|
|
e7c9ef16fe | ||
|
|
8872d7cbd3 | ||
|
|
334ee4213f | ||
|
|
226513ed60 | ||
|
|
dac00d10c1 | ||
|
|
84d2b6f8f5 | ||
|
|
f98c9246b2 | ||
|
|
059027bc9d | ||
|
|
af68caeaf6 | ||
|
|
fdeacfc89f | ||
|
|
5b33dd59d9 | ||
|
|
1f92bfde6a | ||
|
|
0c094cb2d7 | ||
|
|
f238290dd3 | ||
|
|
c376ffc351 | ||
|
|
802549ac7c | ||
|
|
72580dee38 | ||
|
|
086113c843 | ||
|
|
d239952d2d | ||
|
|
7472d1e70b | ||
|
|
2446d8a668 | ||
|
|
f6894448bd | ||
|
|
425be6b4a1 | ||
|
|
457048bfac | ||
|
|
f14ab70035 | ||
|
|
8f1c88e07d | ||
|
|
9602494454 | ||
|
|
38abfb80ed | ||
|
|
87938bcc09 | ||
|
|
8ebf6750b9 | ||
|
|
6f92daae12 | ||
|
|
80e1c85b50 | ||
|
|
395a1db22f | ||
|
|
28009c4224 | ||
|
|
211f3050e9 | ||
|
|
03b5fd4a10 | ||
|
|
5e969cbef0 | ||
|
|
42883d0899 | ||
|
|
06f6dc9937 | ||
|
|
1789b12db2 | ||
|
|
c7f3e6119d | ||
|
|
54414db91d | ||
|
|
9b0625bb83 | ||
|
|
0dda05fd57 | ||
|
|
5b79ba2618 | ||
|
|
22a1fa649e | ||
|
|
745e76c335 | ||
|
|
852891dbd8 | ||
|
|
316195e912 | ||
|
|
8e889d694d | ||
|
|
ec6132b754 | ||
|
|
c282bb1d86 | ||
|
|
d04b54243d | ||
|
|
b88ba14597 | ||
|
|
7b3c3de35e | ||
|
|
abab7c2852 | ||
|
|
30f5ab0b99 | ||
|
|
8b273a075c | ||
|
|
76026fc211 | ||
|
|
04284e31cd | ||
|
|
c3b9c3c5ab | ||
|
|
a8b550e7ef | ||
|
|
cbfbebed00 | ||
|
|
2b07f22672 | ||
|
|
a784982030 | ||
|
|
ebec5aafab | ||
|
|
572b32729f | ||
|
|
43e712d86a | ||
|
|
4db20677f7 | ||
|
|
6887fb22de | ||
|
|
50fbdbcfd7 | ||
|
|
c77b8489d7 | ||
|
|
eca4ed2cc0 | ||
|
|
744c18b7cb | ||
|
|
8d6f6f933e | ||
|
|
37c3b9f5c1 | ||
|
|
1f1dcd16e6 | ||
|
|
3285436f75 | ||
|
|
7f49bd8a31 | ||
|
|
9cd2015661 | ||
|
|
cf3a1020b0 | ||
|
|
ee19fb736b | ||
|
|
b0ccfb8eb4 | ||
|
|
444e5a711f | ||
|
|
8774d72ddb | ||
|
|
e3fcdbf040 | ||
|
|
2854ca03b4 | ||
|
|
6c624a6ed0 | ||
|
|
57b73d8b49 | ||
|
|
a79cee12ee | ||
|
|
7a921f66e6 | ||
|
|
12e235efb0 | ||
|
|
01060cf16d | ||
|
|
0786862a35 | ||
|
|
efa43483b2 | ||
|
|
771371e051 | ||
|
|
2ee95f8981 | ||
|
|
5bc01e4bfd | ||
|
|
510e966982 | ||
|
|
10e3b8ac80 | ||
|
|
04059bbf5a | ||
|
|
d643007c79 | ||
|
|
fc43876cc5 | ||
|
|
a926cb514f | ||
|
|
25cab2f273 | ||
|
|
8d2e2753a2 | ||
|
|
cc4c50e3eb | ||
|
|
751072bdb0 | ||
|
|
e97e1f10db | ||
|
|
0bd2a0656c | ||
|
|
71a2b20301 | ||
|
|
8df7de94e3 | ||
|
|
bf21203297 | ||
|
|
ae98375194 | ||
|
|
82d1ccf376 | ||
|
|
bb6d49c17e | ||
|
|
ed735ec47c | ||
|
|
ba4bac3a03 | ||
|
|
08433d7e04 | ||
|
|
a3b525b50d | ||
|
|
097f6886f2 | ||
|
|
07a1549634 | ||
|
|
befca26c58 | ||
|
|
3556a2eef4 | ||
|
|
807765f77e | ||
|
|
e44584e549 | ||
|
|
acd48f0abb | ||
|
|
f919bc6713 | ||
|
|
a0030b8f45 | ||
|
|
a5f0cce1b1 | ||
|
|
4d13dda605 | ||
|
|
b56cc8e459 | ||
|
|
c435811479 | ||
|
|
c686c93fb5 | ||
|
|
da8f76e6bd | ||
|
|
99596a4149 | ||
|
|
ec2a9f2c57 | ||
|
|
fd73ced6dc | ||
|
|
5071736ab4 | ||
|
|
0d7f1d23b4 | ||
|
|
84ab11ac09 | ||
|
|
67804a6bb2 | ||
|
|
65ee877236 | ||
|
|
b060867009 | ||
|
|
4d53045c6b | ||
|
|
cecd4b1b75 | ||
|
|
7cd0463953 | ||
|
|
7a82cf80ce | ||
|
|
f997aee3ba | ||
|
|
88ec89bdbd | ||
|
|
7d1b43780a | ||
|
|
4b5c2de376 | ||
|
|
e5c560e8ba | ||
|
|
bed494d904 | ||
|
|
2dfecda465 | ||
|
|
3ebb1e0221 | ||
|
|
348184904c | ||
|
|
7a27fa50a1 | ||
|
|
8d4951c990 | ||
|
|
6e57f6c527 | ||
|
|
b9ac51b6c3 | ||
|
|
702e8d79ce | ||
|
|
95a9dabf8b | ||
|
|
bae66c49c2 | ||
|
|
e0afe0b4bb | ||
|
|
24fb29a356 | ||
|
|
71083b5e64 | ||
|
|
1174f17bd9 | ||
|
|
d6d8fc21d8 | ||
|
|
9592639cb4 | ||
|
|
abcb28e506 | ||
|
|
a92f65580c | ||
|
|
3819f67cf4 | ||
|
|
295c8d2934 | ||
|
|
88da8685dd | ||
|
|
c7831ac96d | ||
|
|
e898761762 | ||
|
|
13d1c5cd00 | ||
|
|
16bfb1b7be | ||
|
|
ef4d4968d6 | ||
|
|
7b4a5e3ec6 | ||
|
|
e6df21e0d2 | ||
|
|
0a2c2d1eca | ||
|
|
a5fb29a6f0 | ||
|
|
f8da301e57 | ||
|
|
cb9075b737 | ||
|
|
3f389a55c2 | ||
|
|
afbd565d87 | ||
|
|
d629acc2b7 | ||
|
|
f32c6a9b28 | ||
|
|
95aa65efb9 | ||
|
|
3806e66cf1 | ||
|
|
bd430baf52 | ||
|
|
48f4154ea8 | ||
|
|
2599e0d28d | ||
|
|
12327fa07d | ||
|
|
57079bf4a4 | ||
|
|
7f6eceb5a3 | ||
|
|
7d7cb836af | ||
|
|
f87d9d1dda |
@@ -1,4 +1,3 @@
|
||||
comment: false
|
||||
ignore:
|
||||
- "doc"
|
||||
- "example"
|
||||
- "tools"
|
||||
- "tools"
|
||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
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
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior, if applicable:
|
||||
|
||||
1. The code is
|
||||
|
||||
```go
|
||||
|
||||
```
|
||||
|
||||
2. The error is
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environments (please complete the following information):**
|
||||
- OS: [e.g. Linux]
|
||||
- go-zero version [e.g. 1.2.1]
|
||||
- goctl version [e.g. 1.2.1, optional]
|
||||
|
||||
**More description**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question on using go-zero or goctl
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '18 19 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# 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)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
12
.github/workflows/go.yml
vendored
12
.github/workflows/go.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
go-version: ^1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -25,10 +25,14 @@ jobs:
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
go vet -stdmethods=false $(go list ./...)
|
||||
go install mvdan.cc/gofumpt@latest
|
||||
test -z "$(gofumpt -s -l -extra .)" || echo "Please run 'gofumpt -l -w -extra .'"
|
||||
|
||||
- name: Test
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v1.0.6
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
19
.github/workflows/issues.yml
vendored
Normal file
19
.github/workflows/issues.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
19
.github/workflows/reviewdog.yml
vendored
Normal file
19
.github/workflows/reviewdog.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: reviewdog
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
staticcheck:
|
||||
name: runner / staticcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: reviewdog/action-staticcheck@v1
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
|
||||
reporter: github-pr-review
|
||||
# Report all results.
|
||||
filter_mode: nofilter
|
||||
# Exit with 1 when it find at least one finding.
|
||||
fail_on_error: true
|
||||
# Set staticcheck flags
|
||||
staticcheck_flags: -checks=inherit,-SA1019,-SA1029,-SA5008
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,15 +4,19 @@
|
||||
# Unignore all with extensions
|
||||
!*.*
|
||||
!**/Dockerfile
|
||||
!**/Makefile
|
||||
|
||||
# Unignore all dirs
|
||||
!*/
|
||||
!api
|
||||
|
||||
# ignore
|
||||
.idea
|
||||
**/.DS_Store
|
||||
**/logs
|
||||
!Makefile
|
||||
|
||||
# for test purpose
|
||||
adhoc
|
||||
|
||||
# gitlab ci
|
||||
.cache
|
||||
|
||||
102
CONTRIBUTING.md
Normal file
102
CONTRIBUTING.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Contributing
|
||||
|
||||
Welcome to go-zero!
|
||||
|
||||
- [Before you get started](#before-you-get-started)
|
||||
- [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
|
||||
|
||||
Please make sure to read and observe our [Code of Conduct](/code-of-conduct.md).
|
||||
|
||||
## Community Expectations
|
||||
|
||||
go-zero is a community project driven by its community which strives to promote a healthy, friendly and productive environment.
|
||||
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
|
||||
|
||||
- Fork the repository on GitHub.
|
||||
- Make your changes on your fork repository.
|
||||
- Submit a PR.
|
||||
|
||||
|
||||
# Your First Contribution
|
||||
|
||||
We will help you to contribute in different areas like filing issues, developing features, fixing critical bugs and
|
||||
getting your work reviewed and merged.
|
||||
|
||||
If you have questions about the development process,
|
||||
feel free to [file an issue](https://github.com/tal-tech/go-zero/issues/new/choose).
|
||||
|
||||
## Find something to work on
|
||||
|
||||
We are always in need of help, be it fixing documentation, reporting bugs or writing some code.
|
||||
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
|
||||
|
||||
[go-zero](https://github.com/tal-tech/go-zero) has beginner-friendly issues that provide a good first issue.
|
||||
For example, [go-zero](https://github.com/tal-tech/go-zero) has
|
||||
[help wanted](https://github.com/tal-tech/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and
|
||||
[good first issue](https://github.com/tal-tech/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.
|
||||
Please see [Contributing](#contributing) below for the workflow.
|
||||
|
||||
#### 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.
|
||||
|
||||
### File an Issue
|
||||
|
||||
While we encourage everyone to contribute code, it is also appreciated when someone reports an issue.
|
||||
|
||||
Please follow the prompted submission guidelines while opening an issue.
|
||||
|
||||
# Contributor Workflow
|
||||
|
||||
Please do not ever hesitate to ask a question or send a pull request.
|
||||
|
||||
This is a rough outline of what a contributor's workflow looks like:
|
||||
|
||||
- Create a topic branch from where to base the contribution. This is usually master.
|
||||
- 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/tal-tech/go-zero).
|
||||
|
||||
## Creating Pull Requests
|
||||
|
||||
Pull requests are often called simply "PR".
|
||||
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`
|
||||
* Run the test with data race enabled `go test -race ./...`
|
||||
|
||||
## Code Review
|
||||
|
||||
To make it easier for your PR to receive reviews, consider the reviewers will need you to:
|
||||
|
||||
* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
* 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.
|
||||
|
||||
28
ROADMAP.md
Normal file
28
ROADMAP.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# go-zero Roadmap
|
||||
|
||||
This document defines a high level roadmap for go-zero development and upcoming releases.
|
||||
Community and contributor involvement is vital for successfully implementing all desired items for each release.
|
||||
We hope that the items listed below will inspire further engagement from the community to keep go-zero progressing and shipping exciting and valuable features.
|
||||
|
||||
## 2021 Q2
|
||||
- [x] Support service discovery through K8S client api
|
||||
- [x] Log full sql statements for easier sql problem solving
|
||||
|
||||
## 2021 Q3
|
||||
- [x] Support `goctl model pg` to support PostgreSQL code generation
|
||||
- [x] Adapt builtin tracing mechanism to opentracing solutions
|
||||
|
||||
## 2021 Q4
|
||||
- [x] Support `username/password` authentication in ETCD
|
||||
- [x] Support `SSL/TLS` in ETCD
|
||||
- [x] Support `SSL/TLS` in `zRPC`
|
||||
- [x] Support `TLS` in redis connections
|
||||
- [x] Support `goctl bug` to report bugs conveniently
|
||||
|
||||
## 2022
|
||||
- [ ] Support `goctl mock` command to start a mocking server with given `.api` file
|
||||
- [ ] Add `httpx.Client` with governance, like circuit breaker etc.
|
||||
- [ ] Support `goctl doctor` command to report potential issues for given service
|
||||
- [ ] Support `context` in redis related methods for timeout and tracing
|
||||
- [ ] Support `context` in sql related methods for timeout and tracing
|
||||
- [ ] Support `context` in mongodb related methods for timeout and tracing
|
||||
76
code-of-conduct.md
Normal file
76
code-of-conduct.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
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
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -27,43 +27,43 @@ return true
|
||||
`
|
||||
)
|
||||
|
||||
// ErrTooLargeOffset indicates the offset is too large in bitset.
|
||||
var ErrTooLargeOffset = errors.New("too large offset")
|
||||
|
||||
type (
|
||||
BitSetProvider interface {
|
||||
// A Filter is a bloom filter.
|
||||
Filter struct {
|
||||
bits uint
|
||||
bitSet bitSetProvider
|
||||
}
|
||||
|
||||
bitSetProvider interface {
|
||||
check([]uint) (bool, error)
|
||||
set([]uint) error
|
||||
}
|
||||
|
||||
BloomFilter struct {
|
||||
bits uint
|
||||
bitSet BitSetProvider
|
||||
}
|
||||
)
|
||||
|
||||
// New create a BloomFilter, store is the backed redis, key is the key for the bloom filter,
|
||||
// New create a Filter, store is the backed redis, key is the key for the bloom filter,
|
||||
// bits is how many bits will be used, maps is how many hashes for each addition.
|
||||
// best practices:
|
||||
// elements - means how many actual elements
|
||||
// when maps = 14, formula: 0.7*(bits/maps), bits = 20*elements, the error rate is 0.000067 < 1e-4
|
||||
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
|
||||
func New(store *redis.Redis, key string, bits uint) *BloomFilter {
|
||||
return &BloomFilter{
|
||||
func New(store *redis.Redis, key string, bits uint) *Filter {
|
||||
return &Filter{
|
||||
bits: bits,
|
||||
bitSet: newRedisBitSet(store, key, bits),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *BloomFilter) Add(data []byte) error {
|
||||
// Add adds data into f.
|
||||
func (f *Filter) Add(data []byte) error {
|
||||
locations := f.getLocations(data)
|
||||
err := f.bitSet.set(locations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return f.bitSet.set(locations)
|
||||
}
|
||||
|
||||
func (f *BloomFilter) Exists(data []byte) (bool, error) {
|
||||
// Exists checks if data is in f.
|
||||
func (f *Filter) Exists(data []byte) (bool, error) {
|
||||
locations := f.getLocations(data)
|
||||
isSet, err := f.bitSet.check(locations)
|
||||
if err != nil {
|
||||
@@ -76,7 +76,7 @@ func (f *BloomFilter) Exists(data []byte) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *BloomFilter) getLocations(data []byte) []uint {
|
||||
func (f *Filter) getLocations(data []byte) []uint {
|
||||
locations := make([]uint, maps)
|
||||
for i := uint(0); i < maps; i++ {
|
||||
hashValue := hash.Hash(append(data, byte(i)))
|
||||
@@ -127,11 +127,12 @@ func (r *redisBitSet) check(offsets []uint) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if exists, ok := resp.(int64); !ok {
|
||||
exists, ok := resp.(int64)
|
||||
if !ok {
|
||||
return false, nil
|
||||
} else {
|
||||
return exists == 1, nil
|
||||
}
|
||||
|
||||
return exists == 1, nil
|
||||
}
|
||||
|
||||
func (r *redisBitSet) del() error {
|
||||
@@ -152,7 +153,7 @@ func (r *redisBitSet) set(offsets []uint) error {
|
||||
_, err = r.store.Eval(setScript, []string{r.key}, args)
|
||||
if err == redis.Nil {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ const (
|
||||
timeFormat = "15:04:05"
|
||||
)
|
||||
|
||||
// ErrServiceUnavailable is returned when the CB state is open
|
||||
// ErrServiceUnavailable is returned when the Breaker state is open.
|
||||
var ErrServiceUnavailable = errors.New("circuit breaker is open")
|
||||
|
||||
type (
|
||||
// Acceptable is the func to check if the error can be accepted.
|
||||
Acceptable func(err error) bool
|
||||
|
||||
// A Breaker represents a circuit breaker.
|
||||
Breaker interface {
|
||||
// Name returns the name of the Breaker.
|
||||
Name() string
|
||||
@@ -61,10 +63,14 @@ type (
|
||||
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
|
||||
}
|
||||
|
||||
// Option defines the method to customize a Breaker.
|
||||
Option func(breaker *circuitBreaker)
|
||||
|
||||
// Promise interface defines the callbacks that returned by Breaker.Allow.
|
||||
Promise interface {
|
||||
// Accept tells the Breaker that the call is successful.
|
||||
Accept()
|
||||
// Reject tells the Breaker that the call is failed.
|
||||
Reject(reason string)
|
||||
}
|
||||
|
||||
@@ -89,6 +95,8 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewBreaker returns a Breaker object.
|
||||
// opts can be used to customize the Breaker.
|
||||
func NewBreaker(opts ...Option) Breaker {
|
||||
var b circuitBreaker
|
||||
for _, opt := range opts {
|
||||
@@ -127,6 +135,7 @@ func (cb *circuitBreaker) Name() string {
|
||||
return cb.name
|
||||
}
|
||||
|
||||
// WithName returns a function to set the name of a Breaker.
|
||||
func WithName(name string) Option {
|
||||
return func(b *circuitBreaker) {
|
||||
b.name = name
|
||||
|
||||
@@ -122,8 +122,7 @@ func BenchmarkGoogleBreaker(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
type mockedPromise struct {
|
||||
}
|
||||
type mockedPromise struct{}
|
||||
|
||||
func (m *mockedPromise) Accept() {
|
||||
}
|
||||
|
||||
@@ -7,24 +7,28 @@ var (
|
||||
breakers = make(map[string]Breaker)
|
||||
)
|
||||
|
||||
// Do calls Breaker.Do on the Breaker with given name.
|
||||
func Do(name string, req func() error) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.Do(req)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithAcceptable calls Breaker.DoWithAcceptable on the Breaker with given name.
|
||||
func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithAcceptable(req, acceptable)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallback calls Breaker.DoWithFallback on the Breaker with given name.
|
||||
func DoWithFallback(name string, req func() error, fallback func(err error) error) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithFallback(req, fallback)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallbackAcceptable calls Breaker.DoWithFallbackAcceptable on the Breaker with given name.
|
||||
func DoWithFallbackAcceptable(name string, req func() error, fallback func(err error) error,
|
||||
acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
@@ -32,6 +36,7 @@ func DoWithFallbackAcceptable(name string, req func() error, fallback func(err e
|
||||
})
|
||||
}
|
||||
|
||||
// GetBreaker returns the Breaker with the given name.
|
||||
func GetBreaker(name string) Breaker {
|
||||
lock.RLock()
|
||||
b, ok := breakers[name]
|
||||
@@ -51,7 +56,8 @@ func GetBreaker(name string) Breaker {
|
||||
return b
|
||||
}
|
||||
|
||||
func NoBreakFor(name string) {
|
||||
// NoBreakerFor disables the circuit breaker for the given name.
|
||||
func NoBreakerFor(name string) {
|
||||
lock.Lock()
|
||||
breakers[name] = newNoOpBreaker()
|
||||
lock.Unlock()
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBreakersNoBreakerFor(t *testing.T) {
|
||||
NoBreakFor("any")
|
||||
NoBreakerFor("any")
|
||||
errDummy := errors.New("any")
|
||||
for i := 0; i < 10000; i++ {
|
||||
assert.Equal(t, errDummy, GetBreaker("any").Do(func() error {
|
||||
|
||||
@@ -64,9 +64,9 @@ func (b *googleBreaker) doReq(req func() error, fallback func(err error) error,
|
||||
if err := b.accept(); err != nil {
|
||||
if fallback != nil {
|
||||
return fallback(err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@@ -94,7 +94,7 @@ func (b *googleBreaker) markFailure() {
|
||||
b.stat.Add(0)
|
||||
}
|
||||
|
||||
func (b *googleBreaker) history() (accepts int64, total int64) {
|
||||
func (b *googleBreaker) history() (accepts, total int64) {
|
||||
b.stat.Reduce(func(b *collection.Bucket) {
|
||||
accepts += int64(b.Sum)
|
||||
total += b.Count
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnterToContinue let stdin waiting for an enter key to continue.
|
||||
func EnterToContinue() {
|
||||
fmt.Print("Press 'Enter' to continue...")
|
||||
bufio.NewReader(os.Stdin).ReadBytes('\n')
|
||||
}
|
||||
|
||||
// ReadLine shows prompt to stdout and read a line from stdin.
|
||||
func ReadLine(prompt string) string {
|
||||
fmt.Print(prompt)
|
||||
input, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// ErrPaddingSize indicates bad padding size.
|
||||
var ErrPaddingSize = errors.New("padding size error")
|
||||
|
||||
type ecb struct {
|
||||
@@ -26,6 +27,7 @@ func newECB(b cipher.Block) *ecb {
|
||||
|
||||
type ecbEncrypter ecb
|
||||
|
||||
// NewECBEncrypter returns an ECB encrypter.
|
||||
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbEncrypter)(newECB(b))
|
||||
}
|
||||
@@ -52,6 +54,7 @@ func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||
|
||||
type ecbDecrypter ecb
|
||||
|
||||
// NewECBDecrypter returns an ECB decrypter.
|
||||
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbDecrypter)(newECB(b))
|
||||
}
|
||||
@@ -78,6 +81,7 @@ func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// EcbDecrypt decrypts src with the given key.
|
||||
func EcbDecrypt(key, src []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
@@ -92,6 +96,8 @@ func EcbDecrypt(key, src []byte) ([]byte, error) {
|
||||
return pkcs5Unpadding(decrypted, decrypter.BlockSize())
|
||||
}
|
||||
|
||||
// EcbDecryptBase64 decrypts base64 encoded src with the given base64 encoded key.
|
||||
// The returned string is also base64 encoded.
|
||||
func EcbDecryptBase64(key, src string) (string, error) {
|
||||
keyBytes, err := getKeyBytes(key)
|
||||
if err != nil {
|
||||
@@ -111,6 +117,7 @@ func EcbDecryptBase64(key, src string) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(decryptedBytes), nil
|
||||
}
|
||||
|
||||
// EcbEncrypt encrypts src with the given key.
|
||||
func EcbEncrypt(key, src []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
@@ -126,6 +133,8 @@ func EcbEncrypt(key, src []byte) ([]byte, error) {
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// EcbEncryptBase64 encrypts base64 encoded src with the given base64 encoded key.
|
||||
// The returned string is also base64 encoded.
|
||||
func EcbEncryptBase64(key, src string) (string, error) {
|
||||
keyBytes, err := getKeyBytes(key)
|
||||
if err != nil {
|
||||
@@ -146,15 +155,16 @@ func EcbEncryptBase64(key, src string) (string, error) {
|
||||
}
|
||||
|
||||
func getKeyBytes(key string) ([]byte, error) {
|
||||
if len(key) > 32 {
|
||||
if keyBytes, err := base64.StdEncoding.DecodeString(key); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return keyBytes, nil
|
||||
}
|
||||
if len(key) <= 32 {
|
||||
return []byte(key), nil
|
||||
}
|
||||
|
||||
return []byte(key), nil
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keyBytes, nil
|
||||
}
|
||||
|
||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
|
||||
65
core/codec/aesecb_test.go
Normal file
65
core/codec/aesecb_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAesEcb(t *testing.T) {
|
||||
var (
|
||||
key = []byte("q4t7w!z%C*F-JaNdRgUjXn2r5u8x/A?D")
|
||||
val = []byte("hello")
|
||||
badKey1 = []byte("aaaaaaaaa")
|
||||
// more than 32 chars
|
||||
badKey2 = []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
)
|
||||
_, err := EcbEncrypt(badKey1, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncrypt(badKey2, val)
|
||||
assert.NotNil(t, err)
|
||||
dst, err := EcbEncrypt(key, val)
|
||||
assert.Nil(t, err)
|
||||
_, err = EcbDecrypt(badKey1, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecrypt(badKey2, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecrypt(key, val)
|
||||
// not enough block, just nil
|
||||
assert.Nil(t, err)
|
||||
src, err := EcbDecrypt(key, dst)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, src)
|
||||
}
|
||||
|
||||
func TestAesEcbBase64(t *testing.T) {
|
||||
const (
|
||||
val = "hello"
|
||||
badKey1 = "aaaaaaaaa"
|
||||
// more than 32 chars
|
||||
badKey2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
key := []byte("q4t7w!z%C*F-JaNdRgUjXn2r5u8x/A?D")
|
||||
b64Key := base64.StdEncoding.EncodeToString(key)
|
||||
b64Val := base64.StdEncoding.EncodeToString([]byte(val))
|
||||
_, err := EcbEncryptBase64(badKey1, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncryptBase64(badKey2, val)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbEncryptBase64(b64Key, val)
|
||||
assert.NotNil(t, err)
|
||||
dst, err := EcbEncryptBase64(b64Key, b64Val)
|
||||
assert.Nil(t, err)
|
||||
_, err = EcbDecryptBase64(badKey1, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecryptBase64(badKey2, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecryptBase64(b64Key, val)
|
||||
assert.NotNil(t, err)
|
||||
src, err := EcbDecryptBase64(b64Key, dst)
|
||||
assert.Nil(t, err)
|
||||
b, err := base64.StdEncoding.DecodeString(src)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, string(b))
|
||||
}
|
||||
@@ -11,8 +11,11 @@ import (
|
||||
// 2048-bit MODP Group
|
||||
|
||||
var (
|
||||
ErrInvalidPriKey = errors.New("invalid private key")
|
||||
ErrInvalidPubKey = errors.New("invalid public key")
|
||||
// ErrInvalidPriKey indicates the invalid private key.
|
||||
ErrInvalidPriKey = errors.New("invalid private key")
|
||||
// ErrInvalidPubKey indicates the invalid public key.
|
||||
ErrInvalidPubKey = errors.New("invalid public key")
|
||||
// ErrPubKeyOutOfBound indicates the public key is out of bound.
|
||||
ErrPubKeyOutOfBound = errors.New("public key out of bound")
|
||||
|
||||
p, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
|
||||
@@ -20,11 +23,13 @@ var (
|
||||
zero = big.NewInt(0)
|
||||
)
|
||||
|
||||
// DhKey defines the Diffie Hellman key.
|
||||
type DhKey struct {
|
||||
PriKey *big.Int
|
||||
PubKey *big.Int
|
||||
}
|
||||
|
||||
// ComputeKey returns a key from public key and private key.
|
||||
func ComputeKey(pubKey, priKey *big.Int) (*big.Int, error) {
|
||||
if pubKey == nil {
|
||||
return nil, ErrInvalidPubKey
|
||||
@@ -41,6 +46,7 @@ func ComputeKey(pubKey, priKey *big.Int) (*big.Int, error) {
|
||||
return new(big.Int).Exp(pubKey, priKey, p), nil
|
||||
}
|
||||
|
||||
// GenerateKey returns a Diffie Hellman key.
|
||||
func GenerateKey() (*DhKey, error) {
|
||||
var err error
|
||||
var x *big.Int
|
||||
@@ -63,10 +69,12 @@ func GenerateKey() (*DhKey, error) {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// NewPublicKey returns a public key from the given bytes.
|
||||
func NewPublicKey(bs []byte) *big.Int {
|
||||
return new(big.Int).SetBytes(bs)
|
||||
}
|
||||
|
||||
// Bytes returns public key bytes.
|
||||
func (k *DhKey) Bytes() []byte {
|
||||
if k.PubKey == nil {
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
const unzipLimit = 100 * 1024 * 1024 // 100MB
|
||||
|
||||
// Gzip compresses bs.
|
||||
func Gzip(bs []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
|
||||
@@ -18,6 +19,7 @@ func Gzip(bs []byte) []byte {
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// Gunzip uncompresses bs.
|
||||
func Gunzip(bs []byte) ([]byte, error) {
|
||||
r, err := gzip.NewReader(bytes.NewBuffer(bs))
|
||||
if err != nil {
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Hmac returns HMAC bytes for body with the given key.
|
||||
func Hmac(key []byte, body string) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
io.WriteString(h, body)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// HmacBase64 returns the base64 encoded string of HMAC for body with the given key.
|
||||
func HmacBase64(key []byte, body string) string {
|
||||
return base64.StdEncoding.EncodeToString(Hmac(key, body))
|
||||
}
|
||||
|
||||
@@ -11,17 +11,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPrivateKey indicates the invalid private key.
|
||||
ErrPrivateKey = errors.New("private key error")
|
||||
ErrPublicKey = errors.New("failed to parse PEM block containing the public key")
|
||||
ErrNotRsaKey = errors.New("key type is not RSA")
|
||||
// ErrPublicKey indicates the invalid public key.
|
||||
ErrPublicKey = errors.New("failed to parse PEM block containing the public key")
|
||||
// ErrNotRsaKey indicates the invalid RSA key.
|
||||
ErrNotRsaKey = errors.New("key type is not RSA")
|
||||
)
|
||||
|
||||
type (
|
||||
// RsaDecrypter represents a RSA decrypter.
|
||||
RsaDecrypter interface {
|
||||
Decrypt(input []byte) ([]byte, error)
|
||||
DecryptBase64(input string) ([]byte, error)
|
||||
}
|
||||
|
||||
// RsaEncrypter represents a RSA encrypter.
|
||||
RsaEncrypter interface {
|
||||
Encrypt(input []byte) ([]byte, error)
|
||||
}
|
||||
@@ -41,6 +46,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewRsaDecrypter returns a RsaDecrypter with the given file.
|
||||
func NewRsaDecrypter(file string) (RsaDecrypter, error) {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -84,6 +90,7 @@ func (r *rsaDecrypter) DecryptBase64(input string) ([]byte, error) {
|
||||
return r.Decrypt(base64Decoded)
|
||||
}
|
||||
|
||||
// NewRsaEncrypter returns a RsaEncrypter with the given key.
|
||||
func NewRsaEncrypter(key []byte) (RsaEncrypter, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block == nil {
|
||||
|
||||
@@ -23,8 +23,10 @@ const (
|
||||
var emptyLruCache = emptyLru{}
|
||||
|
||||
type (
|
||||
// CacheOption defines the method to customize a Cache.
|
||||
CacheOption func(cache *Cache)
|
||||
|
||||
// A Cache object is a in-memory cache.
|
||||
Cache struct {
|
||||
name string
|
||||
lock sync.Mutex
|
||||
@@ -32,18 +34,19 @@ type (
|
||||
expire time.Duration
|
||||
timingWheel *TimingWheel
|
||||
lruCache lru
|
||||
barrier syncx.SharedCalls
|
||||
barrier syncx.SingleFlight
|
||||
unstableExpiry mathx.Unstable
|
||||
stats *cacheStat
|
||||
}
|
||||
)
|
||||
|
||||
// NewCache returns a Cache with given expire.
|
||||
func NewCache(expire time.Duration, opts ...CacheOption) (*Cache, error) {
|
||||
cache := &Cache{
|
||||
data: make(map[string]interface{}),
|
||||
expire: expire,
|
||||
lruCache: emptyLruCache,
|
||||
barrier: syncx.NewSharedCalls(),
|
||||
barrier: syncx.NewSingleFlight(),
|
||||
unstableExpiry: mathx.NewUnstable(expiryDeviation),
|
||||
}
|
||||
|
||||
@@ -72,6 +75,7 @@ func NewCache(expire time.Duration, opts ...CacheOption) (*Cache, error) {
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
// Del deletes the item with the given key from c.
|
||||
func (c *Cache) Del(key string) {
|
||||
c.lock.Lock()
|
||||
delete(c.data, key)
|
||||
@@ -80,6 +84,7 @@ func (c *Cache) Del(key string) {
|
||||
c.timingWheel.RemoveTimer(key)
|
||||
}
|
||||
|
||||
// Get returns the item with the given key from c.
|
||||
func (c *Cache) Get(key string) (interface{}, bool) {
|
||||
value, ok := c.doGet(key)
|
||||
if ok {
|
||||
@@ -91,6 +96,7 @@ func (c *Cache) Get(key string) (interface{}, bool) {
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Set sets value into c with key.
|
||||
func (c *Cache) Set(key string, value interface{}) {
|
||||
c.lock.Lock()
|
||||
_, ok := c.data[key]
|
||||
@@ -106,6 +112,9 @@ func (c *Cache) Set(key string, value interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Take returns the item with the given key.
|
||||
// If the item is in c, return it directly.
|
||||
// If not, use fetch method to get the item, set into c and return it.
|
||||
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
|
||||
if val, ok := c.doGet(key); ok {
|
||||
c.stats.IncrementHit()
|
||||
@@ -136,11 +145,10 @@ func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}
|
||||
if fresh {
|
||||
c.stats.IncrementMiss()
|
||||
return val, nil
|
||||
} else {
|
||||
// got the result from previous ongoing query
|
||||
c.stats.IncrementHit()
|
||||
}
|
||||
|
||||
// got the result from previous ongoing query
|
||||
c.stats.IncrementHit()
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -168,6 +176,7 @@ func (c *Cache) size() int {
|
||||
return len(c.data)
|
||||
}
|
||||
|
||||
// WithLimit customizes a Cache with items up to limit.
|
||||
func WithLimit(limit int) CacheOption {
|
||||
return func(cache *Cache) {
|
||||
if limit > 0 {
|
||||
@@ -176,6 +185,7 @@ func WithLimit(limit int) CacheOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithName customizes a Cache with the given name.
|
||||
func WithName(name string) CacheOption {
|
||||
return func(cache *Cache) {
|
||||
cache.name = name
|
||||
|
||||
@@ -2,6 +2,7 @@ package collection
|
||||
|
||||
import "sync"
|
||||
|
||||
// A Queue is a FIFO queue.
|
||||
type Queue struct {
|
||||
lock sync.Mutex
|
||||
elements []interface{}
|
||||
@@ -11,6 +12,7 @@ type Queue struct {
|
||||
count int
|
||||
}
|
||||
|
||||
// NewQueue returns a Queue object.
|
||||
func NewQueue(size int) *Queue {
|
||||
return &Queue{
|
||||
elements: make([]interface{}, size),
|
||||
@@ -18,6 +20,7 @@ func NewQueue(size int) *Queue {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty checks if q is empty.
|
||||
func (q *Queue) Empty() bool {
|
||||
q.lock.Lock()
|
||||
empty := q.count == 0
|
||||
@@ -26,6 +29,7 @@ func (q *Queue) Empty() bool {
|
||||
return empty
|
||||
}
|
||||
|
||||
// Put puts element into q at the last position.
|
||||
func (q *Queue) Put(element interface{}) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
@@ -44,6 +48,7 @@ func (q *Queue) Put(element interface{}) {
|
||||
q.count++
|
||||
}
|
||||
|
||||
// Take takes the first element out of q if not empty.
|
||||
func (q *Queue) Take() (interface{}, bool) {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package collection
|
||||
|
||||
import "sync"
|
||||
|
||||
// A Ring can be used as fixed size ring.
|
||||
type Ring struct {
|
||||
elements []interface{}
|
||||
index int
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewRing returns a Ring object with the given size n.
|
||||
func NewRing(n int) *Ring {
|
||||
if n < 1 {
|
||||
panic("n should be greater than 0")
|
||||
@@ -15,12 +20,20 @@ func NewRing(n int) *Ring {
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds v into r.
|
||||
func (r *Ring) Add(v interface{}) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.elements[r.index%len(r.elements)] = v
|
||||
r.index++
|
||||
}
|
||||
|
||||
// Take takes all items from r.
|
||||
func (r *Ring) Take() []interface{} {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
var size int
|
||||
var start int
|
||||
if r.index > len(r.elements) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package collection
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,3 +30,30 @@ func TestRingMore(t *testing.T) {
|
||||
elements := ring.Take()
|
||||
assert.ElementsMatch(t, []interface{}{6, 7, 8, 9, 10}, elements)
|
||||
}
|
||||
|
||||
func TestRingAdd(t *testing.T) {
|
||||
ring := NewRing(5051)
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 1; i <= 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := 1; j <= i; j++ {
|
||||
ring.Add(i)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
assert.Equal(t, 5050, len(ring.Take()))
|
||||
}
|
||||
|
||||
func BenchmarkRingAdd(b *testing.B) {
|
||||
ring := NewRing(500)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ring.Add(i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// RollingWindowOption let callers customize the RollingWindow.
|
||||
RollingWindowOption func(rollingWindow *RollingWindow)
|
||||
|
||||
// RollingWindow defines a rolling window to calculate the events in buckets with time interval.
|
||||
RollingWindow struct {
|
||||
lock sync.RWMutex
|
||||
size int
|
||||
@@ -17,10 +19,12 @@ type (
|
||||
interval time.Duration
|
||||
offset int
|
||||
ignoreCurrent bool
|
||||
lastTime time.Duration
|
||||
lastTime time.Duration // start time of the last bucket
|
||||
}
|
||||
)
|
||||
|
||||
// NewRollingWindow returns a RollingWindow that with size buckets and time interval,
|
||||
// use opts to customize the RollingWindow.
|
||||
func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOption) *RollingWindow {
|
||||
if size < 1 {
|
||||
panic("size must be greater than 0")
|
||||
@@ -38,6 +42,7 @@ func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOpt
|
||||
return w
|
||||
}
|
||||
|
||||
// Add adds value to current bucket.
|
||||
func (rw *RollingWindow) Add(v float64) {
|
||||
rw.lock.Lock()
|
||||
defer rw.lock.Unlock()
|
||||
@@ -45,6 +50,7 @@ func (rw *RollingWindow) Add(v float64) {
|
||||
rw.win.add(rw.offset, v)
|
||||
}
|
||||
|
||||
// Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set.
|
||||
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
|
||||
rw.lock.RLock()
|
||||
defer rw.lock.RUnlock()
|
||||
@@ -67,36 +73,30 @@ func (rw *RollingWindow) span() int {
|
||||
offset := int(timex.Since(rw.lastTime) / rw.interval)
|
||||
if 0 <= offset && offset < rw.size {
|
||||
return offset
|
||||
} else {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *RollingWindow) updateOffset() {
|
||||
span := rw.span()
|
||||
if span > 0 {
|
||||
offset := rw.offset
|
||||
// reset expired buckets
|
||||
start := offset + 1
|
||||
steps := start + span
|
||||
var remainder int
|
||||
if steps > rw.size {
|
||||
remainder = steps - rw.size
|
||||
steps = rw.size
|
||||
}
|
||||
for i := start; i < steps; i++ {
|
||||
rw.win.resetBucket(i)
|
||||
offset = i
|
||||
}
|
||||
for i := 0; i < remainder; i++ {
|
||||
rw.win.resetBucket(i)
|
||||
offset = i
|
||||
}
|
||||
rw.offset = offset
|
||||
rw.lastTime = timex.Now()
|
||||
if span <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
offset := rw.offset
|
||||
// reset expired buckets
|
||||
for i := 0; i < span; i++ {
|
||||
rw.win.resetBucket((offset + i + 1) % rw.size)
|
||||
}
|
||||
|
||||
rw.offset = (offset + span) % rw.size
|
||||
now := timex.Now()
|
||||
// align to interval time boundary
|
||||
rw.lastTime = now - (now-rw.lastTime)%rw.interval
|
||||
}
|
||||
|
||||
// Bucket defines the bucket that holds sum and num of additions.
|
||||
type Bucket struct {
|
||||
Sum float64
|
||||
Count int64
|
||||
@@ -118,9 +118,9 @@ type window struct {
|
||||
}
|
||||
|
||||
func newWindow(size int) *window {
|
||||
var buckets []*Bucket
|
||||
buckets := make([]*Bucket, size)
|
||||
for i := 0; i < size; i++ {
|
||||
buckets = append(buckets, new(Bucket))
|
||||
buckets[i] = new(Bucket)
|
||||
}
|
||||
return &window{
|
||||
buckets: buckets,
|
||||
@@ -134,14 +134,15 @@ func (w *window) add(offset int, v float64) {
|
||||
|
||||
func (w *window) reduce(start, count int, fn func(b *Bucket)) {
|
||||
for i := 0; i < count; i++ {
|
||||
fn(w.buckets[(start+i)%len(w.buckets)])
|
||||
fn(w.buckets[(start+i)%w.size])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) resetBucket(offset int) {
|
||||
w.buckets[offset].reset()
|
||||
w.buckets[offset%w.size].reset()
|
||||
}
|
||||
|
||||
// IgnoreCurrentBucket lets the Reduce call ignore current bucket.
|
||||
func IgnoreCurrentBucket() RollingWindowOption {
|
||||
return func(w *RollingWindow) {
|
||||
w.ignoreCurrent = true
|
||||
|
||||
@@ -105,10 +105,41 @@ func TestRollingWindowReduce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollingWindowBucketTimeBoundary(t *testing.T) {
|
||||
const size = 3
|
||||
interval := time.Millisecond * 30
|
||||
r := NewRollingWindow(size, interval)
|
||||
listBuckets := func() []float64 {
|
||||
var buckets []float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
buckets = append(buckets, b.Sum)
|
||||
})
|
||||
return buckets
|
||||
}
|
||||
assert.Equal(t, []float64{0, 0, 0}, listBuckets())
|
||||
r.Add(1)
|
||||
assert.Equal(t, []float64{0, 0, 1}, listBuckets())
|
||||
time.Sleep(time.Millisecond * 45)
|
||||
r.Add(2)
|
||||
r.Add(3)
|
||||
assert.Equal(t, []float64{0, 1, 5}, listBuckets())
|
||||
// sleep time should be less than interval, and make the bucket change happen
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
r.Add(4)
|
||||
r.Add(5)
|
||||
r.Add(6)
|
||||
assert.Equal(t, []float64{1, 5, 15}, listBuckets())
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
r.Add(7)
|
||||
r.Add(8)
|
||||
r.Add(9)
|
||||
assert.Equal(t, []float64{0, 0, 24}, listBuckets())
|
||||
}
|
||||
|
||||
func TestRollingWindowDataRace(t *testing.T) {
|
||||
const size = 3
|
||||
r := NewRollingWindow(size, duration)
|
||||
var stop = make(chan bool)
|
||||
stop := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -18,6 +18,7 @@ type SafeMap struct {
|
||||
dirtyNew map[interface{}]interface{}
|
||||
}
|
||||
|
||||
// NewSafeMap returns a SafeMap.
|
||||
func NewSafeMap() *SafeMap {
|
||||
return &SafeMap{
|
||||
dirtyOld: make(map[interface{}]interface{}),
|
||||
@@ -25,6 +26,7 @@ func NewSafeMap() *SafeMap {
|
||||
}
|
||||
}
|
||||
|
||||
// Del deletes the value with the given key from m.
|
||||
func (m *SafeMap) Del(key interface{}) {
|
||||
m.lock.Lock()
|
||||
if _, ok := m.dirtyOld[key]; ok {
|
||||
@@ -53,18 +55,20 @@ func (m *SafeMap) Del(key interface{}) {
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Get gets the value with the given key from m.
|
||||
func (m *SafeMap) Get(key interface{}) (interface{}, bool) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
if val, ok := m.dirtyOld[key]; ok {
|
||||
return val, true
|
||||
} else {
|
||||
val, ok := m.dirtyNew[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
val, ok := m.dirtyNew[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Set sets the value into m with the given key.
|
||||
func (m *SafeMap) Set(key, value interface{}) {
|
||||
m.lock.Lock()
|
||||
if m.deletionOld <= maxDeletion {
|
||||
@@ -83,6 +87,7 @@ func (m *SafeMap) Set(key, value interface{}) {
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Size returns the size of m.
|
||||
func (m *SafeMap) Size() int {
|
||||
m.lock.RLock()
|
||||
size := len(m.dirtyOld) + len(m.dirtyNew)
|
||||
|
||||
@@ -21,6 +21,7 @@ type Set struct {
|
||||
tp int
|
||||
}
|
||||
|
||||
// NewSet returns a managed Set, can only put the values with the same type.
|
||||
func NewSet() *Set {
|
||||
return &Set{
|
||||
data: make(map[interface{}]lang.PlaceholderType),
|
||||
@@ -28,6 +29,7 @@ func NewSet() *Set {
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnmanagedSet returns a unmanaged Set, which can put values with different types.
|
||||
func NewUnmanagedSet() *Set {
|
||||
return &Set{
|
||||
data: make(map[interface{}]lang.PlaceholderType),
|
||||
@@ -35,42 +37,49 @@ func NewUnmanagedSet() *Set {
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds i into s.
|
||||
func (s *Set) Add(i ...interface{}) {
|
||||
for _, each := range i {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// AddInt adds int values ii into s.
|
||||
func (s *Set) AddInt(ii ...int) {
|
||||
for _, each := range ii {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// AddInt64 adds int64 values ii into s.
|
||||
func (s *Set) AddInt64(ii ...int64) {
|
||||
for _, each := range ii {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// AddUint adds uint values ii into s.
|
||||
func (s *Set) AddUint(ii ...uint) {
|
||||
for _, each := range ii {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// AddUint64 adds uint64 values ii into s.
|
||||
func (s *Set) AddUint64(ii ...uint64) {
|
||||
for _, each := range ii {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// AddStr adds string values ss into s.
|
||||
func (s *Set) AddStr(ss ...string) {
|
||||
for _, each := range ss {
|
||||
s.add(each)
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks if i is in s.
|
||||
func (s *Set) Contains(i interface{}) bool {
|
||||
if len(s.data) == 0 {
|
||||
return false
|
||||
@@ -81,6 +90,7 @@ func (s *Set) Contains(i interface{}) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// Keys returns the keys in s.
|
||||
func (s *Set) Keys() []interface{} {
|
||||
var keys []interface{}
|
||||
|
||||
@@ -91,13 +101,12 @@ func (s *Set) Keys() []interface{} {
|
||||
return keys
|
||||
}
|
||||
|
||||
// KeysInt returns the int keys in s.
|
||||
func (s *Set) KeysInt() []int {
|
||||
var keys []int
|
||||
|
||||
for key := range s.data {
|
||||
if intKey, ok := key.(int); !ok {
|
||||
continue
|
||||
} else {
|
||||
if intKey, ok := key.(int); ok {
|
||||
keys = append(keys, intKey)
|
||||
}
|
||||
}
|
||||
@@ -105,13 +114,12 @@ func (s *Set) KeysInt() []int {
|
||||
return keys
|
||||
}
|
||||
|
||||
// KeysInt64 returns int64 keys in s.
|
||||
func (s *Set) KeysInt64() []int64 {
|
||||
var keys []int64
|
||||
|
||||
for key := range s.data {
|
||||
if intKey, ok := key.(int64); !ok {
|
||||
continue
|
||||
} else {
|
||||
if intKey, ok := key.(int64); ok {
|
||||
keys = append(keys, intKey)
|
||||
}
|
||||
}
|
||||
@@ -119,13 +127,12 @@ func (s *Set) KeysInt64() []int64 {
|
||||
return keys
|
||||
}
|
||||
|
||||
// KeysUint returns uint keys in s.
|
||||
func (s *Set) KeysUint() []uint {
|
||||
var keys []uint
|
||||
|
||||
for key := range s.data {
|
||||
if intKey, ok := key.(uint); !ok {
|
||||
continue
|
||||
} else {
|
||||
if intKey, ok := key.(uint); ok {
|
||||
keys = append(keys, intKey)
|
||||
}
|
||||
}
|
||||
@@ -133,13 +140,12 @@ func (s *Set) KeysUint() []uint {
|
||||
return keys
|
||||
}
|
||||
|
||||
// KeysUint64 returns uint64 keys in s.
|
||||
func (s *Set) KeysUint64() []uint64 {
|
||||
var keys []uint64
|
||||
|
||||
for key := range s.data {
|
||||
if intKey, ok := key.(uint64); !ok {
|
||||
continue
|
||||
} else {
|
||||
if intKey, ok := key.(uint64); ok {
|
||||
keys = append(keys, intKey)
|
||||
}
|
||||
}
|
||||
@@ -147,13 +153,12 @@ func (s *Set) KeysUint64() []uint64 {
|
||||
return keys
|
||||
}
|
||||
|
||||
// KeysStr returns string keys in s.
|
||||
func (s *Set) KeysStr() []string {
|
||||
var keys []string
|
||||
|
||||
for key := range s.data {
|
||||
if strKey, ok := key.(string); !ok {
|
||||
continue
|
||||
} else {
|
||||
if strKey, ok := key.(string); ok {
|
||||
keys = append(keys, strKey)
|
||||
}
|
||||
}
|
||||
@@ -161,11 +166,13 @@ func (s *Set) KeysStr() []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
// Remove removes i from s.
|
||||
func (s *Set) Remove(i interface{}) {
|
||||
s.validate(i)
|
||||
delete(s.data, i)
|
||||
}
|
||||
|
||||
// Count returns the number of items in s.
|
||||
func (s *Set) Count() int {
|
||||
return len(s.data)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
const drainWorkers = 8
|
||||
|
||||
type (
|
||||
// Execute defines the method to execute the task.
|
||||
Execute func(key, value interface{})
|
||||
|
||||
// A TimingWheel is a timing wheel object to schedule tasks.
|
||||
TimingWheel struct {
|
||||
interval time.Duration
|
||||
ticker timex.Ticker
|
||||
@@ -54,6 +56,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewTimingWheel returns a TimingWheel.
|
||||
func NewTimingWheel(interval time.Duration, numSlots int, execute Execute) (*TimingWheel, error) {
|
||||
if interval <= 0 || numSlots <= 0 || execute == nil {
|
||||
return nil, fmt.Errorf("interval: %v, slots: %d, execute: %p", interval, numSlots, execute)
|
||||
@@ -85,10 +88,12 @@ func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execu
|
||||
return tw, nil
|
||||
}
|
||||
|
||||
// Drain drains all items and executes them.
|
||||
func (tw *TimingWheel) Drain(fn func(key, value interface{})) {
|
||||
tw.drainChannel <- fn
|
||||
}
|
||||
|
||||
// MoveTimer moves the task with the given key to the given delay.
|
||||
func (tw *TimingWheel) MoveTimer(key interface{}, delay time.Duration) {
|
||||
if delay <= 0 || key == nil {
|
||||
return
|
||||
@@ -100,6 +105,7 @@ func (tw *TimingWheel) MoveTimer(key interface{}, delay time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTimer removes the task with the given key.
|
||||
func (tw *TimingWheel) RemoveTimer(key interface{}) {
|
||||
if key == nil {
|
||||
return
|
||||
@@ -108,6 +114,7 @@ func (tw *TimingWheel) RemoveTimer(key interface{}) {
|
||||
tw.removeChannel <- key
|
||||
}
|
||||
|
||||
// SetTimer sets the task value with the given key to the delay.
|
||||
func (tw *TimingWheel) SetTimer(key, value interface{}, delay time.Duration) {
|
||||
if delay <= 0 || key == nil {
|
||||
return
|
||||
@@ -122,6 +129,7 @@ func (tw *TimingWheel) SetTimer(key, value interface{}, delay time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops tw.
|
||||
func (tw *TimingWheel) Stop() {
|
||||
close(tw.stopChannel)
|
||||
}
|
||||
@@ -143,7 +151,7 @@ func (tw *TimingWheel) drainAll(fn func(key, value interface{})) {
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) {
|
||||
func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos, circle int) {
|
||||
steps := int(d / tw.interval)
|
||||
pos = (tw.tickedPos + steps) % tw.numSlots
|
||||
circle = (steps - 1) / tw.numSlots
|
||||
@@ -204,6 +212,7 @@ func (tw *TimingWheel) removeTask(key interface{}) {
|
||||
|
||||
timer := val.(*positionEntry)
|
||||
timer.item.removed = true
|
||||
tw.timers.Del(key)
|
||||
}
|
||||
|
||||
func (tw *TimingWheel) run() {
|
||||
@@ -248,7 +257,6 @@ func (tw *TimingWheel) scanAndRunTasks(l *list.List) {
|
||||
if task.removed {
|
||||
next := e.Next()
|
||||
l.Remove(e)
|
||||
tw.timers.Del(task.key)
|
||||
e = next
|
||||
continue
|
||||
} else if task.circle > 0 {
|
||||
@@ -301,6 +309,7 @@ func (tw *TimingWheel) setTask(task *timingEntry) {
|
||||
func (tw *TimingWheel) setTimerPosition(pos int, task *timingEntry) {
|
||||
if val, ok := tw.timers.Get(task.key); ok {
|
||||
timer := val.(*positionEntry)
|
||||
timer.item = task
|
||||
timer.pos = pos
|
||||
} else {
|
||||
tw.timers.Set(task.key, &positionEntry{
|
||||
|
||||
@@ -594,6 +594,31 @@ func TestTimingWheel_ElapsedAndSetThenMove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveAndRemoveTask(t *testing.T) {
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func(v int) {
|
||||
for i := 0; i < v; i++ {
|
||||
ticker.Tick()
|
||||
}
|
||||
}
|
||||
var keys []int
|
||||
tw, _ := newTimingWheelWithClock(testStep, 10, func(k, v interface{}) {
|
||||
assert.Equal(t, "any", k)
|
||||
assert.Equal(t, 3, v.(int))
|
||||
keys = append(keys, v.(int))
|
||||
ticker.Done()
|
||||
}, ticker)
|
||||
defer tw.Stop()
|
||||
tw.SetTimer("any", 3, testStep*8)
|
||||
tick(6)
|
||||
tw.MoveTimer("any", testStep*7)
|
||||
tick(3)
|
||||
tw.RemoveTimer("any")
|
||||
tick(30)
|
||||
time.Sleep(time.Millisecond)
|
||||
assert.Equal(t, 0, len(keys))
|
||||
}
|
||||
|
||||
func BenchmarkTimingWheel(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/mapping"
|
||||
@@ -15,26 +16,43 @@ var loaders = map[string]func([]byte, interface{}) error{
|
||||
".yml": LoadConfigFromYamlBytes,
|
||||
}
|
||||
|
||||
func LoadConfig(file string, v interface{}) error {
|
||||
if content, err := ioutil.ReadFile(file); err != nil {
|
||||
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||
func LoadConfig(file string, v interface{}, opts ...Option) error {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if loader, ok := loaders[path.Ext(file)]; ok {
|
||||
return loader(content, v)
|
||||
} else {
|
||||
return fmt.Errorf("unrecoginized file type: %s", file)
|
||||
}
|
||||
|
||||
loader, ok := loaders[path.Ext(file)]
|
||||
if !ok {
|
||||
return fmt.Errorf("unrecognized file type: %s", file)
|
||||
}
|
||||
|
||||
var opt options
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.env {
|
||||
return loader([]byte(os.ExpandEnv(string(content))), v)
|
||||
}
|
||||
|
||||
return loader(content, v)
|
||||
}
|
||||
|
||||
// LoadConfigFromJsonBytes loads config into v from content json bytes.
|
||||
func LoadConfigFromJsonBytes(content []byte, v interface{}) error {
|
||||
return mapping.UnmarshalJsonBytes(content, v)
|
||||
}
|
||||
|
||||
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
|
||||
func LoadConfigFromYamlBytes(content []byte, v interface{}) error {
|
||||
return mapping.UnmarshalYamlBytes(content, v)
|
||||
}
|
||||
|
||||
func MustLoad(path string, v interface{}) {
|
||||
if err := LoadConfig(path, v); err != nil {
|
||||
// MustLoad loads config into v from path, exits on error.
|
||||
func MustLoad(path string, v interface{}, opts ...Option) {
|
||||
if err := LoadConfig(path, v, opts...); err != nil {
|
||||
log.Fatalf("error: config file %s, %s", path, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,21 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/fs"
|
||||
"github.com/tal-tech/go-zero/core/hash"
|
||||
)
|
||||
|
||||
func TestLoadConfig_notExists(t *testing.T) {
|
||||
assert.NotNil(t, LoadConfig("not_a_file", nil))
|
||||
}
|
||||
|
||||
func TestLoadConfig_notRecogFile(t *testing.T) {
|
||||
filename, err := fs.TempFilenameWithText("hello")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(filename)
|
||||
assert.NotNil(t, LoadConfig(filename, nil))
|
||||
}
|
||||
|
||||
func TestConfigJson(t *testing.T) {
|
||||
tests := []string{
|
||||
".json",
|
||||
@@ -17,13 +29,15 @@ func TestConfigJson(t *testing.T) {
|
||||
}
|
||||
text := `{
|
||||
"a": "foo",
|
||||
"b": 1
|
||||
"b": 1,
|
||||
"c": "${FOO}",
|
||||
"d": "abcd!@#$112"
|
||||
}`
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
os.Setenv("FOO", "2")
|
||||
defer os.Unsetenv("FOO")
|
||||
tmpfile, err := createTempFile(test, text)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
@@ -31,10 +45,50 @@ func TestConfigJson(t *testing.T) {
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "${FOO}", val.C)
|
||||
assert.Equal(t, "abcd!@#$112", val.D)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigJsonEnv(t *testing.T) {
|
||||
tests := []string{
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
text := `{
|
||||
"a": "foo",
|
||||
"b": 1,
|
||||
"c": "${FOO}",
|
||||
"d": "abcd!@#$a12 3"
|
||||
}`
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test, func(t *testing.T) {
|
||||
os.Setenv("FOO", "2")
|
||||
defer os.Unsetenv("FOO")
|
||||
tmpfile, err := createTempFile(test, text)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val, UseEnv())
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "2", val.C)
|
||||
assert.Equal(t, "abcd!@# 3", val.D)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
17
core/conf/options.go
Normal file
17
core/conf/options.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package conf
|
||||
|
||||
type (
|
||||
// Option defines the method to customize the config options.
|
||||
Option func(opt *options)
|
||||
|
||||
options struct {
|
||||
env bool
|
||||
}
|
||||
)
|
||||
|
||||
// UseEnv customizes the config to use environment variables.
|
||||
func UseEnv() Option {
|
||||
return func(opt *options) {
|
||||
opt.env = true
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -30,14 +31,19 @@ type mapBasedProperties struct {
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Loads the properties into a properties configuration instance.
|
||||
// LoadProperties loads the properties into a properties configuration instance.
|
||||
// Returns an error that indicates if there was a problem loading the configuration.
|
||||
func LoadProperties(filename string) (Properties, error) {
|
||||
func LoadProperties(filename string, opts ...Option) (Properties, error) {
|
||||
lines, err := iox.ReadTextLines(filename, iox.WithoutBlank(), iox.OmitWithPrefix("#"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var opt options
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
raw := make(map[string]string)
|
||||
for i := range lines {
|
||||
pair := strings.Split(lines[i], "=")
|
||||
@@ -50,7 +56,11 @@ func LoadProperties(filename string) (Properties, error) {
|
||||
|
||||
key := strings.TrimSpace(pair[0])
|
||||
value := strings.TrimSpace(pair[1])
|
||||
raw[key] = value
|
||||
if opt.env {
|
||||
raw[key] = os.ExpandEnv(value)
|
||||
} else {
|
||||
raw[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return &mapBasedProperties{
|
||||
@@ -87,7 +97,7 @@ func (config *mapBasedProperties) SetInt(key string, value int) {
|
||||
config.lock.Unlock()
|
||||
}
|
||||
|
||||
// Dumps the configuration internal map into a string.
|
||||
// ToString dumps the configuration internal map into a string.
|
||||
func (config *mapBasedProperties) ToString() string {
|
||||
config.lock.RLock()
|
||||
ret := fmt.Sprintf("%s", config.properties)
|
||||
@@ -96,12 +106,12 @@ func (config *mapBasedProperties) ToString() string {
|
||||
return ret
|
||||
}
|
||||
|
||||
// Returns the error message.
|
||||
// Error returns the error message.
|
||||
func (configError *PropertyError) Error() string {
|
||||
return configError.message
|
||||
}
|
||||
|
||||
// Builds a new properties configuration structure
|
||||
// NewProperties builds a new properties configuration structure.
|
||||
func NewProperties() Properties {
|
||||
return &mapBasedProperties{
|
||||
properties: make(map[string]string),
|
||||
|
||||
@@ -24,6 +24,53 @@ func TestProperties(t *testing.T) {
|
||||
assert.Equal(t, "test", props.GetString("app.name"))
|
||||
assert.Equal(t, "app", props.GetString("app.program"))
|
||||
assert.Equal(t, 5, props.GetInt("app.threads"))
|
||||
|
||||
val := props.ToString()
|
||||
assert.Contains(t, val, "app.name")
|
||||
assert.Contains(t, val, "app.program")
|
||||
assert.Contains(t, val, "app.threads")
|
||||
}
|
||||
|
||||
func TestPropertiesEnv(t *testing.T) {
|
||||
text := `app.name = test
|
||||
|
||||
app.program=app
|
||||
|
||||
app.env1 = ${FOO}
|
||||
app.env2 = $none
|
||||
|
||||
# this is comment
|
||||
app.threads = 5`
|
||||
tmpfile, err := fs.TempFilenameWithText(text)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
|
||||
os.Setenv("FOO", "2")
|
||||
defer os.Unsetenv("FOO")
|
||||
|
||||
props, err := LoadProperties(tmpfile, UseEnv())
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test", props.GetString("app.name"))
|
||||
assert.Equal(t, "app", props.GetString("app.program"))
|
||||
assert.Equal(t, 5, props.GetInt("app.threads"))
|
||||
assert.Equal(t, "2", props.GetString("app.env1"))
|
||||
assert.Equal(t, "", props.GetString("app.env2"))
|
||||
|
||||
val := props.ToString()
|
||||
assert.Contains(t, val, "app.name")
|
||||
assert.Contains(t, val, "app.program")
|
||||
assert.Contains(t, val, "app.threads")
|
||||
assert.Contains(t, val, "app.env1")
|
||||
assert.Contains(t, val, "app.env2")
|
||||
}
|
||||
|
||||
func TestLoadProperties_badContent(t *testing.T) {
|
||||
filename, err := fs.TempFilenameWithText("hello")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(filename)
|
||||
_, err = LoadProperties(filename)
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, len(err.Error()) > 0)
|
||||
}
|
||||
|
||||
func TestSetString(t *testing.T) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ShrinkDeadline(ctx context.Context, timeout time.Duration) (context.Context, func()) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
leftTime := time.Until(deadline)
|
||||
if leftTime < timeout {
|
||||
timeout = leftTime
|
||||
}
|
||||
}
|
||||
|
||||
return context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShrinkDeadlineLess(t *testing.T) {
|
||||
deadline := time.Now().Add(time.Second)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
ctx, cancel = ShrinkDeadline(ctx, time.Minute)
|
||||
defer cancel()
|
||||
dl, ok := ctx.Deadline()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, deadline, dl)
|
||||
}
|
||||
|
||||
func TestShrinkDeadlineMore(t *testing.T) {
|
||||
deadline := time.Now().Add(time.Minute)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
ctx, cancel = ShrinkDeadline(ctx, time.Second)
|
||||
defer cancel()
|
||||
dl, ok := ctx.Deadline()
|
||||
assert.True(t, ok)
|
||||
assert.True(t, dl.Before(deadline))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func (cv contextValuer) Value(key string) (interface{}, bool) {
|
||||
return v, v != nil
|
||||
}
|
||||
|
||||
// For unmarshals ctx into v.
|
||||
func For(ctx context.Context, v interface{}) error {
|
||||
return unmarshaler.UnmarshalValuer(contextValuer{
|
||||
Context: ctx,
|
||||
|
||||
@@ -47,9 +47,11 @@ func TestUnmarshalContextWithMissing(t *testing.T) {
|
||||
Name string `ctx:"name"`
|
||||
Age int `ctx:"age"`
|
||||
}
|
||||
type name string
|
||||
const PersonNameKey name = "name"
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "name", "kevin")
|
||||
ctx = context.WithValue(ctx, PersonNameKey, "kevin")
|
||||
|
||||
var person Person
|
||||
err := For(ctx, &person)
|
||||
|
||||
@@ -21,6 +21,7 @@ func (valueOnlyContext) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValueOnlyFrom takes all values from the given ctx, without deadline and error control.
|
||||
func ValueOnlyFrom(ctx context.Context) context.Context {
|
||||
return valueOnlyContext{
|
||||
Context: ctx,
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
func TestContextCancel(t *testing.T) {
|
||||
c := context.WithValue(context.Background(), "key", "value")
|
||||
type key string
|
||||
var nameKey key = "name"
|
||||
c := context.WithValue(context.Background(), nameKey, "value")
|
||||
c1, cancel := context.WithCancel(c)
|
||||
o := ValueOnlyFrom(c1)
|
||||
c2, cancel2 := context.WithCancel(o)
|
||||
|
||||
14
core/discov/accountregistry.go
Normal file
14
core/discov/accountregistry.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package discov
|
||||
|
||||
import "github.com/tal-tech/go-zero/core/discov/internal"
|
||||
|
||||
// RegisterAccount registers the username/password to the given etcd cluster.
|
||||
func RegisterAccount(endpoints []string, user, pass string) {
|
||||
internal.AddAccount(endpoints, user, pass)
|
||||
}
|
||||
|
||||
// RegisterTLS registers the CertFile/CertKeyFile/CACertFile to the given etcd.
|
||||
func RegisterTLS(endpoints []string, certFile, certKeyFile, caFile string,
|
||||
insecureSkipVerify bool) error {
|
||||
return internal.AddTLS(endpoints, certFile, certKeyFile, caFile, insecureSkipVerify)
|
||||
}
|
||||
21
core/discov/accountregistry_test.go
Normal file
21
core/discov/accountregistry_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package discov
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/discov/internal"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
func TestRegisterAccount(t *testing.T) {
|
||||
endpoints := []string{
|
||||
"localhost:2379",
|
||||
}
|
||||
user := "foo" + stringx.Rand()
|
||||
RegisterAccount(endpoints, user, "bar")
|
||||
account, ok := internal.GetAccount(endpoints)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, user, account.User)
|
||||
assert.Equal(t, "bar", account.Pass)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const (
|
||||
|
||||
const timeToLive int64 = 10
|
||||
|
||||
// TimeToLive is seconds to live in etcd.
|
||||
var TimeToLive = timeToLive
|
||||
|
||||
func extract(etcdKey string, index int) (string, bool) {
|
||||
|
||||
@@ -28,6 +28,9 @@ func TestExtract(t *testing.T) {
|
||||
|
||||
_, ok = extract("any", -1)
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = extract("any", 10)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestMakeKey(t *testing.T) {
|
||||
|
||||
@@ -2,11 +2,29 @@ package discov
|
||||
|
||||
import "errors"
|
||||
|
||||
// EtcdConf is the config item with the given key on etcd.
|
||||
type EtcdConf struct {
|
||||
Hosts []string
|
||||
Key string
|
||||
Hosts []string
|
||||
Key string
|
||||
User string `json:",optional"`
|
||||
Pass string `json:",optional"`
|
||||
CertFile string `json:",optional"`
|
||||
CertKeyFile string `json:",optional=CertFile"`
|
||||
CACertFile string `json:",optional=CertFile"`
|
||||
InsecureSkipVerify bool `json:",optional"`
|
||||
}
|
||||
|
||||
// HasAccount returns if account provided.
|
||||
func (c EtcdConf) HasAccount() bool {
|
||||
return len(c.User) > 0 && len(c.Pass) > 0
|
||||
}
|
||||
|
||||
// HasTLS returns if TLS CertFile/CertKeyFile/CACertFile are provided.
|
||||
func (c EtcdConf) HasTLS() bool {
|
||||
return len(c.CertFile) > 0 && len(c.CertKeyFile) > 0 && len(c.CACertFile) > 0
|
||||
}
|
||||
|
||||
// Validate validates c.
|
||||
func (c EtcdConf) Validate() error {
|
||||
if len(c.Hosts) == 0 {
|
||||
return errors.New("empty etcd hosts")
|
||||
|
||||
@@ -44,3 +44,39 @@ func TestConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdConf_HasAccount(t *testing.T) {
|
||||
tests := []struct {
|
||||
EtcdConf
|
||||
hasAccount bool
|
||||
}{
|
||||
{
|
||||
EtcdConf: EtcdConf{
|
||||
Hosts: []string{"any"},
|
||||
Key: "key",
|
||||
},
|
||||
hasAccount: false,
|
||||
},
|
||||
{
|
||||
EtcdConf: EtcdConf{
|
||||
Hosts: []string{"any"},
|
||||
Key: "key",
|
||||
User: "foo",
|
||||
},
|
||||
hasAccount: false,
|
||||
},
|
||||
{
|
||||
EtcdConf: EtcdConf{
|
||||
Hosts: []string{"any"},
|
||||
Key: "key",
|
||||
User: "foo",
|
||||
Pass: "bar",
|
||||
},
|
||||
hasAccount: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.hasAccount, test.EtcdConf.HasAccount())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package discov
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/core/discov/internal"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type (
|
||||
Facade struct {
|
||||
endpoints []string
|
||||
registry *internal.Registry
|
||||
}
|
||||
|
||||
FacadeListener interface {
|
||||
OnAdd(key, val string)
|
||||
OnDelete(key string)
|
||||
}
|
||||
)
|
||||
|
||||
func NewFacade(endpoints []string) Facade {
|
||||
return Facade{
|
||||
endpoints: endpoints,
|
||||
registry: internal.GetRegistry(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f Facade) Client() internal.EtcdClient {
|
||||
conn, err := f.registry.GetConn(f.endpoints)
|
||||
logx.Must(err)
|
||||
return conn
|
||||
}
|
||||
|
||||
func (f Facade) Monitor(key string, l FacadeListener) {
|
||||
f.registry.Monitor(f.endpoints, key, listenerAdapter{l})
|
||||
}
|
||||
|
||||
type listenerAdapter struct {
|
||||
l FacadeListener
|
||||
}
|
||||
|
||||
func (la listenerAdapter) OnAdd(kv internal.KV) {
|
||||
la.l.OnAdd(kv.Key, kv.Val)
|
||||
}
|
||||
|
||||
func (la listenerAdapter) OnDelete(kv internal.KV) {
|
||||
la.l.OnDelete(kv.Key)
|
||||
}
|
||||
75
core/discov/internal/accountmanager.go
Normal file
75
core/discov/internal/accountmanager.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
accounts = make(map[string]Account)
|
||||
tlsConfigs = make(map[string]*tls.Config)
|
||||
lock sync.RWMutex
|
||||
)
|
||||
|
||||
// Account holds the username/password for an etcd cluster.
|
||||
type Account struct {
|
||||
User string
|
||||
Pass string
|
||||
}
|
||||
|
||||
// AddAccount adds the username/password for the given etcd cluster.
|
||||
func AddAccount(endpoints []string, user, pass string) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
accounts[getClusterKey(endpoints)] = Account{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
}
|
||||
}
|
||||
|
||||
// AddTLS adds the tls cert files for the given etcd cluster.
|
||||
func AddTLS(endpoints []string, certFile, certKeyFile, caFile string, insecureSkipVerify bool) error {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, certKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caData, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(caData)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
tlsConfigs[getClusterKey(endpoints)] = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: pool,
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccount gets the username/password for the given etcd cluster.
|
||||
func GetAccount(endpoints []string) (Account, bool) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
account, ok := accounts[getClusterKey(endpoints)]
|
||||
return account, ok
|
||||
}
|
||||
|
||||
// GetTLS gets the tls config for the given etcd cluster.
|
||||
func GetTLS(endpoints []string) (*tls.Config, bool) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
cfg, ok := tlsConfigs[getClusterKey(endpoints)]
|
||||
return cfg, ok
|
||||
}
|
||||
34
core/discov/internal/accountmanager_test.go
Normal file
34
core/discov/internal/accountmanager_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
func TestAccount(t *testing.T) {
|
||||
endpoints := []string{
|
||||
"192.168.0.2:2379",
|
||||
"192.168.0.3:2379",
|
||||
"192.168.0.4:2379",
|
||||
}
|
||||
username := "foo" + stringx.Rand()
|
||||
password := "bar"
|
||||
anotherPassword := "any"
|
||||
|
||||
_, ok := GetAccount(endpoints)
|
||||
assert.False(t, ok)
|
||||
|
||||
AddAccount(endpoints, username, password)
|
||||
account, ok := GetAccount(endpoints)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, username, account.User)
|
||||
assert.Equal(t, password, account.Pass)
|
||||
|
||||
AddAccount(endpoints, username, anotherPassword)
|
||||
account, ok = GetAccount(endpoints)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, username, account.User)
|
||||
assert.Equal(t, anotherPassword, account.Pass)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
//go:generate mockgen -package internal -destination etcdclient_mock.go -source etcdclient.go EtcdClient
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// EtcdClient interface represents an etcd client.
|
||||
type EtcdClient interface {
|
||||
ActiveConnection() *grpc.ClientConn
|
||||
Close() error
|
||||
|
||||
@@ -6,10 +6,11 @@ package internal
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
clientv3 "go.etcd.io/etcd/clientv3"
|
||||
grpc "google.golang.org/grpc"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// MockEtcdClient is a mock of EtcdClient interface
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package internal
|
||||
|
||||
// Listener interface wraps the OnUpdate method.
|
||||
type Listener interface {
|
||||
OnUpdate(keys []string, values []string, newKey string)
|
||||
OnUpdate(keys, values []string, newKey string)
|
||||
}
|
||||
|
||||
@@ -14,44 +14,58 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
"github.com/tal-tech/go-zero/core/threading"
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
registryInstance = Registry{
|
||||
registry = Registry{
|
||||
clusters: make(map[string]*cluster),
|
||||
}
|
||||
connManager = syncx.NewResourceManager()
|
||||
)
|
||||
|
||||
// A Registry is a registry that manages the etcd client connections.
|
||||
type Registry struct {
|
||||
clusters map[string]*cluster
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// GetRegistry returns a global Registry.
|
||||
func GetRegistry() *Registry {
|
||||
return ®istryInstance
|
||||
return ®istry
|
||||
}
|
||||
|
||||
func (r *Registry) getCluster(endpoints []string) *cluster {
|
||||
// GetConn returns an etcd client connection associated with given endpoints.
|
||||
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
|
||||
c, _ := r.getCluster(endpoints)
|
||||
return c.getClient()
|
||||
}
|
||||
|
||||
// Monitor monitors the key on given etcd endpoints, notify with the given UpdateListener.
|
||||
func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener) error {
|
||||
c, exists := r.getCluster(endpoints)
|
||||
// if exists, the existing values should be updated to the listener.
|
||||
if exists {
|
||||
kvs := c.getCurrent(key)
|
||||
for _, kv := range kvs {
|
||||
l.OnAdd(kv)
|
||||
}
|
||||
}
|
||||
|
||||
return c.monitor(key, l)
|
||||
}
|
||||
|
||||
func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) {
|
||||
clusterKey := getClusterKey(endpoints)
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
c, ok := r.clusters[clusterKey]
|
||||
if !ok {
|
||||
c, exists = r.clusters[clusterKey]
|
||||
if !exists {
|
||||
c = newCluster(endpoints)
|
||||
r.clusters[clusterKey] = c
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
|
||||
return r.getCluster(endpoints).getClient()
|
||||
}
|
||||
|
||||
func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener) error {
|
||||
return r.getCluster(endpoints).monitor(key, l)
|
||||
return
|
||||
}
|
||||
|
||||
type cluster struct {
|
||||
@@ -90,6 +104,21 @@ func (c *cluster) getClient() (EtcdClient, error) {
|
||||
return val.(EtcdClient), nil
|
||||
}
|
||||
|
||||
func (c *cluster) getCurrent(key string) []KV {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
var kvs []KV
|
||||
for k, v := range c.values[key] {
|
||||
kvs = append(kvs, KV{
|
||||
Key: k,
|
||||
Val: v,
|
||||
})
|
||||
}
|
||||
|
||||
return kvs
|
||||
}
|
||||
|
||||
func (c *cluster) handleChanges(key string, kvs []KV) {
|
||||
var add []KV
|
||||
var remove []KV
|
||||
@@ -193,14 +222,12 @@ func (c *cluster) load(cli EtcdClient, key string) {
|
||||
}
|
||||
|
||||
var kvs []KV
|
||||
c.lock.Lock()
|
||||
for _, ev := range resp.Kvs {
|
||||
kvs = append(kvs, KV{
|
||||
Key: string(ev.Key),
|
||||
Val: string(ev.Value),
|
||||
})
|
||||
}
|
||||
c.lock.Unlock()
|
||||
|
||||
c.handleChanges(key, kvs)
|
||||
}
|
||||
@@ -256,26 +283,34 @@ func (c *cluster) reload(cli EtcdClient) {
|
||||
}
|
||||
|
||||
func (c *cluster) watch(cli EtcdClient, key string) {
|
||||
for {
|
||||
if c.watchStream(cli, key) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cluster) watchStream(cli EtcdClient, key string) bool {
|
||||
rch := cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
|
||||
for {
|
||||
select {
|
||||
case wresp, ok := <-rch:
|
||||
if !ok {
|
||||
logx.Error("etcd monitor chan has been closed")
|
||||
return
|
||||
return false
|
||||
}
|
||||
if wresp.Canceled {
|
||||
logx.Error("etcd monitor chan has been canceled")
|
||||
return
|
||||
logx.Errorf("etcd monitor chan has been canceled, error: %v", wresp.Err())
|
||||
return false
|
||||
}
|
||||
if wresp.Err() != nil {
|
||||
logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err()))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
c.handleWatchEvents(key, wresp.Events)
|
||||
case <-c.done:
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,15 +323,25 @@ func (c *cluster) watchConnState(cli EtcdClient) {
|
||||
watcher.watch(cli.ActiveConnection())
|
||||
}
|
||||
|
||||
// DialClient dials an etcd cluster with given endpoints.
|
||||
func DialClient(endpoints []string) (EtcdClient, error) {
|
||||
return clientv3.New(clientv3.Config{
|
||||
cfg := clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
AutoSyncInterval: autoSyncInterval,
|
||||
DialTimeout: DialTimeout,
|
||||
DialKeepAliveTime: dialKeepAliveTime,
|
||||
DialKeepAliveTimeout: DialTimeout,
|
||||
RejectOldCluster: true,
|
||||
})
|
||||
}
|
||||
if account, ok := GetAccount(endpoints); ok {
|
||||
cfg.Username = account.User
|
||||
cfg.Password = account.Pass
|
||||
}
|
||||
if tlsCfg, ok := GetTLS(endpoints); ok {
|
||||
cfg.TLS = tlsCfg
|
||||
}
|
||||
|
||||
return clientv3.New(cfg)
|
||||
}
|
||||
|
||||
func getClusterKey(endpoints []string) string {
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/contextx"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
"go.etcd.io/etcd/mvcc/mvccpb"
|
||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
var mockLock sync.Mutex
|
||||
@@ -32,9 +33,10 @@ func setMockClient(cli EtcdClient) func() {
|
||||
}
|
||||
|
||||
func TestGetCluster(t *testing.T) {
|
||||
c1 := GetRegistry().getCluster([]string{"first"})
|
||||
c2 := GetRegistry().getCluster([]string{"second"})
|
||||
c3 := GetRegistry().getCluster([]string{"first"})
|
||||
AddAccount([]string{"first"}, "foo", "bar")
|
||||
c1, _ := GetRegistry().getCluster([]string{"first"})
|
||||
c2, _ := GetRegistry().getCluster([]string{"second"})
|
||||
c3, _ := GetRegistry().getCluster([]string{"first"})
|
||||
assert.Equal(t, c1, c3)
|
||||
assert.NotEqual(t, c1, c2)
|
||||
}
|
||||
@@ -202,11 +204,13 @@ func TestClusterWatch_RespFailures(t *testing.T) {
|
||||
restore := setMockClient(cli)
|
||||
defer restore()
|
||||
ch := make(chan clientv3.WatchResponse)
|
||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch)
|
||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
||||
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
||||
c := new(cluster)
|
||||
c.done = make(chan lang.PlaceholderType)
|
||||
go func() {
|
||||
ch <- resp
|
||||
close(c.done)
|
||||
}()
|
||||
c.watch(cli, "any")
|
||||
})
|
||||
@@ -220,11 +224,13 @@ func TestClusterWatch_CloseChan(t *testing.T) {
|
||||
restore := setMockClient(cli)
|
||||
defer restore()
|
||||
ch := make(chan clientv3.WatchResponse)
|
||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch)
|
||||
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
|
||||
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
||||
c := new(cluster)
|
||||
c.done = make(chan lang.PlaceholderType)
|
||||
go func() {
|
||||
close(ch)
|
||||
close(c.done)
|
||||
}()
|
||||
c.watch(cli, "any")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//go:generate mockgen -package internal -destination statewatcher_mock.go -source statewatcher.go etcdConn
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
@@ -18,7 +19,8 @@ type (
|
||||
disconnected bool
|
||||
currentState connectivity.State
|
||||
listeners []func()
|
||||
lock sync.Mutex
|
||||
// lock only guards listeners, because only listens can be accessed by other goroutines.
|
||||
lock sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,27 +34,33 @@ func (sw *stateWatcher) addListener(l func()) {
|
||||
sw.lock.Unlock()
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) notifyListeners() {
|
||||
sw.lock.Lock()
|
||||
defer sw.lock.Unlock()
|
||||
|
||||
for _, l := range sw.listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) updateState(conn etcdConn) {
|
||||
sw.currentState = conn.GetState()
|
||||
switch sw.currentState {
|
||||
case connectivity.TransientFailure, connectivity.Shutdown:
|
||||
sw.disconnected = true
|
||||
case connectivity.Ready:
|
||||
if sw.disconnected {
|
||||
sw.disconnected = false
|
||||
sw.notifyListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *stateWatcher) watch(conn etcdConn) {
|
||||
sw.currentState = conn.GetState()
|
||||
for {
|
||||
if conn.WaitForStateChange(context.Background(), sw.currentState) {
|
||||
newState := conn.GetState()
|
||||
sw.lock.Lock()
|
||||
sw.currentState = newState
|
||||
|
||||
switch newState {
|
||||
case connectivity.TransientFailure, connectivity.Shutdown:
|
||||
sw.disconnected = true
|
||||
case connectivity.Ready:
|
||||
if sw.disconnected {
|
||||
sw.disconnected = false
|
||||
for _, l := range sw.listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sw.lock.Unlock()
|
||||
sw.updateState(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ package internal
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
connectivity "google.golang.org/grpc/connectivity"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MocketcdConn is a mock of etcdConn interface
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//go:generate mockgen -package internal -destination updatelistener_mock.go -source updatelistener.go UpdateListener
|
||||
|
||||
package internal
|
||||
|
||||
type (
|
||||
// A KV is used to store an etcd entry with key and value.
|
||||
KV struct {
|
||||
Key string
|
||||
Val string
|
||||
}
|
||||
|
||||
// UpdateListener wraps the OnAdd and OnDelete methods.
|
||||
UpdateListener interface {
|
||||
OnAdd(kv KV)
|
||||
OnDelete(kv KV)
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockUpdateListener is a mock of UpdateListener interface
|
||||
|
||||
@@ -3,17 +3,22 @@ package internal
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// Delimiter is a separator that separates the etcd path.
|
||||
Delimiter = '/'
|
||||
|
||||
autoSyncInterval = time.Minute
|
||||
coolDownInterval = time.Second
|
||||
dialTimeout = 5 * time.Second
|
||||
dialKeepAliveTime = 5 * time.Second
|
||||
requestTimeout = 3 * time.Second
|
||||
Delimiter = '/'
|
||||
endpointsSeparator = ","
|
||||
)
|
||||
|
||||
var (
|
||||
DialTimeout = dialTimeout
|
||||
// DialTimeout is the dial timeout.
|
||||
DialTimeout = dialTimeout
|
||||
// RequestTimeout is the request timeout.
|
||||
RequestTimeout = requestTimeout
|
||||
NewClient = DialClient
|
||||
// NewClient is used to create etcd clients.
|
||||
NewClient = DialClient
|
||||
)
|
||||
|
||||
64
core/discov/kubernetes/etcd-statefulset.yaml
Normal file
64
core/discov/kubernetes/etcd-statefulset.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: "etcd"
|
||||
namespace: discov
|
||||
labels:
|
||||
app: "etcd"
|
||||
spec:
|
||||
serviceName: "etcd"
|
||||
replicas: 5
|
||||
template:
|
||||
metadata:
|
||||
name: "etcd"
|
||||
labels:
|
||||
app: "etcd"
|
||||
spec:
|
||||
volumes:
|
||||
- name: etcd-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: etcd-pvc
|
||||
containers:
|
||||
- name: "etcd"
|
||||
image: quay.io/coreos/etcd:latest
|
||||
ports:
|
||||
- containerPort: 2379
|
||||
name: client
|
||||
- containerPort: 2380
|
||||
name: peer
|
||||
env:
|
||||
- name: CLUSTER_SIZE
|
||||
value: "5"
|
||||
- name: SET_NAME
|
||||
value: "etcd"
|
||||
- name: VOLNAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
volumeMounts:
|
||||
- name: etcd-pvc
|
||||
mountPath: /var/lib/etcd
|
||||
subPathExpr: $(VOLNAME) # data mounted respectively in each pod
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-ecx"
|
||||
- |
|
||||
|
||||
chmod 700 /var/lib/etcd
|
||||
|
||||
IP=$(hostname -i)
|
||||
PEERS=""
|
||||
for i in $(seq 0 $((${CLUSTER_SIZE} - 1))); do
|
||||
PEERS="${PEERS}${PEERS:+,}${SET_NAME}-${i}=http://${SET_NAME}-${i}.${SET_NAME}:2380"
|
||||
done
|
||||
exec etcd --name ${HOSTNAME} \
|
||||
--listen-peer-urls http://0.0.0.0:2380 \
|
||||
--listen-client-urls http://0.0.0.0:2379 \
|
||||
--advertise-client-urls http://${HOSTNAME}.${SET_NAME}.discov:2379 \
|
||||
--initial-advertise-peer-urls http://${HOSTNAME}.${SET_NAME}:2380 \
|
||||
--initial-cluster ${PEERS} \
|
||||
--initial-cluster-state new \
|
||||
--logger zap \
|
||||
--data-dir /var/lib/etcd \
|
||||
--auto-compaction-retention 1
|
||||
@@ -35,7 +35,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd0:2379
|
||||
- http://etcd0.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -107,7 +107,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd1:2379
|
||||
- http://etcd1.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -179,7 +179,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd2:2379
|
||||
- http://etcd2.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -251,7 +251,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd3:2379
|
||||
- http://etcd3.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
@@ -323,7 +323,7 @@ spec:
|
||||
- --listen-client-urls
|
||||
- http://0.0.0.0:2379
|
||||
- --advertise-client-urls
|
||||
- http://etcd4:2379
|
||||
- http://etcd4.discov:2379
|
||||
- --initial-cluster
|
||||
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
|
||||
- --initial-cluster-state
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/proc"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
"github.com/tal-tech/go-zero/core/threading"
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
type (
|
||||
PublisherOption func(client *Publisher)
|
||||
// PubOption defines the method to customize a Publisher.
|
||||
PubOption func(client *Publisher)
|
||||
|
||||
// A Publisher can be used to publish the value to an etcd cluster on the given key.
|
||||
Publisher struct {
|
||||
endpoints []string
|
||||
key string
|
||||
@@ -26,7 +28,11 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func NewPublisher(endpoints []string, key, value string, opts ...PublisherOption) *Publisher {
|
||||
// NewPublisher returns a Publisher.
|
||||
// endpoints is the hosts of the etcd cluster.
|
||||
// key:value are a pair to be published.
|
||||
// opts are used to customize the Publisher.
|
||||
func NewPublisher(endpoints []string, key, value string, opts ...PubOption) *Publisher {
|
||||
publisher := &Publisher{
|
||||
endpoints: endpoints,
|
||||
key: key,
|
||||
@@ -43,6 +49,7 @@ func NewPublisher(endpoints []string, key, value string, opts ...PublisherOption
|
||||
return publisher
|
||||
}
|
||||
|
||||
// KeepAlive keeps key:value alive.
|
||||
func (p *Publisher) KeepAlive() error {
|
||||
cli, err := internal.GetRegistry().GetConn(p.endpoints)
|
||||
if err != nil {
|
||||
@@ -61,14 +68,17 @@ func (p *Publisher) KeepAlive() error {
|
||||
return p.keepAliveAsync(cli)
|
||||
}
|
||||
|
||||
// Pause pauses the renewing of key:value.
|
||||
func (p *Publisher) Pause() {
|
||||
p.pauseChan <- lang.Placeholder
|
||||
}
|
||||
|
||||
// Resume resumes the renewing of key:value.
|
||||
func (p *Publisher) Resume() {
|
||||
p.resumeChan <- lang.Placeholder
|
||||
}
|
||||
|
||||
// Stop stops the renewing and revokes the registration.
|
||||
func (p *Publisher) Stop() {
|
||||
p.quit.Close()
|
||||
}
|
||||
@@ -135,8 +145,23 @@ func (p *Publisher) revoke(cli internal.EtcdClient) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithId(id int64) PublisherOption {
|
||||
// WithId customizes a Publisher with the id.
|
||||
func WithId(id int64) PubOption {
|
||||
return func(publisher *Publisher) {
|
||||
publisher.id = id
|
||||
}
|
||||
}
|
||||
|
||||
// WithPubEtcdAccount provides the etcd username/password.
|
||||
func WithPubEtcdAccount(user, pass string) PubOption {
|
||||
return func(pub *Publisher) {
|
||||
RegisterAccount(pub.endpoints, user, pass)
|
||||
}
|
||||
}
|
||||
|
||||
// WithPubEtcdTLS provides the etcd CertFile/CertKeyFile/CACertFile.
|
||||
func WithPubEtcdTLS(certFile, certKeyFile, caFile string, insecureSkipVerify bool) PubOption {
|
||||
return func(pub *Publisher) {
|
||||
logx.Must(RegisterTLS(pub.endpoints, certFile, certKeyFile, caFile, insecureSkipVerify))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/discov/internal"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -28,7 +31,8 @@ func TestPublisher_register(t *testing.T) {
|
||||
ID: id,
|
||||
}, nil)
|
||||
cli.EXPECT().Put(gomock.Any(), makeEtcdKey("thekey", id), "thevalue", gomock.Any())
|
||||
pub := NewPublisher(nil, "thekey", "thevalue")
|
||||
pub := NewPublisher(nil, "thekey", "thevalue",
|
||||
WithPubEtcdAccount(stringx.Rand(), "bar"))
|
||||
_, err := pub.register(cli)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -152,3 +156,16 @@ func TestPublisher_keepAliveAsyncPause(t *testing.T) {
|
||||
pub.Pause()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestPublisher_Resume(t *testing.T) {
|
||||
publisher := new(Publisher)
|
||||
publisher.resumeChan = make(chan lang.PlaceholderType)
|
||||
go func() {
|
||||
publisher.Resume()
|
||||
}()
|
||||
go func() {
|
||||
time.Sleep(time.Minute)
|
||||
t.Fail()
|
||||
}()
|
||||
<-publisher.resumeChan
|
||||
}
|
||||
|
||||
@@ -5,30 +5,35 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/discov/internal"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
type (
|
||||
subOptions struct {
|
||||
exclusive bool
|
||||
}
|
||||
|
||||
SubOption func(opts *subOptions)
|
||||
// SubOption defines the method to customize a Subscriber.
|
||||
SubOption func(sub *Subscriber)
|
||||
|
||||
// A Subscriber is used to subscribe the given key on a etcd cluster.
|
||||
Subscriber struct {
|
||||
items *container
|
||||
endpoints []string
|
||||
exclusive bool
|
||||
items *container
|
||||
}
|
||||
)
|
||||
|
||||
// NewSubscriber returns a Subscriber.
|
||||
// endpoints is the hosts of the etcd cluster.
|
||||
// key is the key to subscribe.
|
||||
// opts are used to customize the Subscriber.
|
||||
func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscriber, error) {
|
||||
var subOpts subOptions
|
||||
for _, opt := range opts {
|
||||
opt(&subOpts)
|
||||
}
|
||||
|
||||
sub := &Subscriber{
|
||||
items: newContainer(subOpts.exclusive),
|
||||
endpoints: endpoints,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(sub)
|
||||
}
|
||||
sub.items = newContainer(sub.exclusive)
|
||||
|
||||
if err := internal.GetRegistry().Monitor(endpoints, key, sub.items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -36,19 +41,35 @@ func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscrib
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// AddListener adds listener to s.
|
||||
func (s *Subscriber) AddListener(listener func()) {
|
||||
s.items.addListener(listener)
|
||||
}
|
||||
|
||||
// Values returns all the subscription values.
|
||||
func (s *Subscriber) Values() []string {
|
||||
return s.items.getValues()
|
||||
}
|
||||
|
||||
// exclusive means that key value can only be 1:1,
|
||||
// Exclusive means that key value can only be 1:1,
|
||||
// which means later added value will remove the keys associated with the same value previously.
|
||||
func Exclusive() SubOption {
|
||||
return func(opts *subOptions) {
|
||||
opts.exclusive = true
|
||||
return func(sub *Subscriber) {
|
||||
sub.exclusive = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubEtcdAccount provides the etcd username/password.
|
||||
func WithSubEtcdAccount(user, pass string) SubOption {
|
||||
return func(sub *Subscriber) {
|
||||
RegisterAccount(sub.endpoints, user, pass)
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubEtcdTLS provides the etcd CertFile/CertKeyFile/CACertFile.
|
||||
func WithSubEtcdTLS(certFile, certKeyFile, caFile string, insecureSkipVerify bool) SubOption {
|
||||
return func(sub *Subscriber) {
|
||||
logx.Must(RegisterTLS(sub.endpoints, certFile, certKeyFile, caFile, insecureSkipVerify))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +121,9 @@ func (c *container) addKv(key, value string) ([]string, bool) {
|
||||
|
||||
if early {
|
||||
return previous, true
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *container) addListener(listener func()) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package discov
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/discov/internal"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -198,3 +200,28 @@ func TestContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber(t *testing.T) {
|
||||
sub := new(Subscriber)
|
||||
Exclusive()(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 TestWithSubEtcdAccount(t *testing.T) {
|
||||
endpoints := []string{"localhost:2379"}
|
||||
user := stringx.Rand()
|
||||
WithSubEtcdAccount(user, "bar")(&Subscriber{
|
||||
endpoints: endpoints,
|
||||
})
|
||||
account, ok := internal.GetAccount(endpoints)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, user, account.User)
|
||||
assert.Equal(t, "bar", account.Pass)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@ package errorx
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
// AtomicError defines an atomic error.
|
||||
type AtomicError struct {
|
||||
err atomic.Value // error
|
||||
}
|
||||
|
||||
// Set sets the error.
|
||||
func (ae *AtomicError) Set(err error) {
|
||||
ae.err.Store(err)
|
||||
if err != nil {
|
||||
ae.err.Store(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load returns the error.
|
||||
func (ae *AtomicError) Load() error {
|
||||
if v := ae.err.Load(); v != nil {
|
||||
return v.(error)
|
||||
|
||||
@@ -17,6 +17,15 @@ func TestAtomicError(t *testing.T) {
|
||||
assert.Equal(t, errDummy, err.Load())
|
||||
}
|
||||
|
||||
func TestAtomicErrorSetNil(t *testing.T) {
|
||||
var (
|
||||
errNil error
|
||||
err AtomicError
|
||||
)
|
||||
err.Set(errNil)
|
||||
assert.Equal(t, errNil, err.Load())
|
||||
}
|
||||
|
||||
func TestAtomicErrorNil(t *testing.T) {
|
||||
var err AtomicError
|
||||
assert.Nil(t, err.Load())
|
||||
|
||||
@@ -3,6 +3,7 @@ package errorx
|
||||
import "bytes"
|
||||
|
||||
type (
|
||||
// A BatchError is an error that can hold multiple errors.
|
||||
BatchError struct {
|
||||
errs errorArray
|
||||
}
|
||||
@@ -10,12 +11,14 @@ type (
|
||||
errorArray []error
|
||||
)
|
||||
|
||||
// Add adds err to be.
|
||||
func (be *BatchError) Add(err error) {
|
||||
if err != nil {
|
||||
be.errs = append(be.errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Err returns an error that represents all errors.
|
||||
func (be *BatchError) Err() error {
|
||||
switch len(be.errs) {
|
||||
case 0:
|
||||
@@ -27,10 +30,12 @@ func (be *BatchError) Err() error {
|
||||
}
|
||||
}
|
||||
|
||||
// NotNil checks if any error inside.
|
||||
func (be *BatchError) NotNil() bool {
|
||||
return len(be.errs) > 0
|
||||
}
|
||||
|
||||
// Error returns a string that represents inside errors.
|
||||
func (ea errorArray) Error() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package errorx
|
||||
|
||||
// Chain runs funs one by one until an error occurred.
|
||||
func Chain(fns ...func() error) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(); err != nil {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
var errDummy = errors.New("dummy")
|
||||
errDummy := errors.New("dummy")
|
||||
assert.Nil(t, Chain(func() error {
|
||||
return nil
|
||||
}, func() error {
|
||||
|
||||
@@ -5,8 +5,12 @@ import "time"
|
||||
const defaultBulkTasks = 1000
|
||||
|
||||
type (
|
||||
// BulkOption defines the method to customize a BulkExecutor.
|
||||
BulkOption func(options *bulkOptions)
|
||||
|
||||
// A BulkExecutor is an executor that can execute tasks on either requirement meets:
|
||||
// 1. up to given size of tasks
|
||||
// 2. flush interval time elapsed
|
||||
BulkExecutor struct {
|
||||
executor *PeriodicalExecutor
|
||||
container *bulkContainer
|
||||
@@ -18,6 +22,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewBulkExecutor returns a BulkExecutor.
|
||||
func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor {
|
||||
options := newBulkOptions()
|
||||
for _, opt := range opts {
|
||||
@@ -36,25 +41,30 @@ func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor {
|
||||
return executor
|
||||
}
|
||||
|
||||
// Add adds task into be.
|
||||
func (be *BulkExecutor) Add(task interface{}) error {
|
||||
be.executor.Add(task)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush forces be to flush and execute tasks.
|
||||
func (be *BulkExecutor) Flush() {
|
||||
be.executor.Flush()
|
||||
}
|
||||
|
||||
// Wait waits be to done with the task execution.
|
||||
func (be *BulkExecutor) Wait() {
|
||||
be.executor.Wait()
|
||||
}
|
||||
|
||||
// WithBulkTasks customizes a BulkExecutor with given tasks limit.
|
||||
func WithBulkTasks(tasks int) BulkOption {
|
||||
return func(options *bulkOptions) {
|
||||
options.cachedTasks = tasks
|
||||
}
|
||||
}
|
||||
|
||||
// WithBulkInterval customizes a BulkExecutor with given flush interval.
|
||||
func WithBulkInterval(duration time.Duration) BulkOption {
|
||||
return func(options *bulkOptions) {
|
||||
options.flushInterval = duration
|
||||
|
||||
@@ -5,8 +5,12 @@ import "time"
|
||||
const defaultChunkSize = 1024 * 1024 // 1M
|
||||
|
||||
type (
|
||||
// ChunkOption defines the method to customize a ChunkExecutor.
|
||||
ChunkOption func(options *chunkOptions)
|
||||
|
||||
// A ChunkExecutor is an executor to execute tasks when either requirement meets:
|
||||
// 1. up to given chunk size
|
||||
// 2. flush interval elapsed
|
||||
ChunkExecutor struct {
|
||||
executor *PeriodicalExecutor
|
||||
container *chunkContainer
|
||||
@@ -18,6 +22,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewChunkExecutor returns a ChunkExecutor.
|
||||
func NewChunkExecutor(execute Execute, opts ...ChunkOption) *ChunkExecutor {
|
||||
options := newChunkOptions()
|
||||
for _, opt := range opts {
|
||||
@@ -36,6 +41,7 @@ func NewChunkExecutor(execute Execute, opts ...ChunkOption) *ChunkExecutor {
|
||||
return executor
|
||||
}
|
||||
|
||||
// Add adds task with given chunk size into ce.
|
||||
func (ce *ChunkExecutor) Add(task interface{}, size int) error {
|
||||
ce.executor.Add(chunk{
|
||||
val: task,
|
||||
@@ -44,20 +50,24 @@ func (ce *ChunkExecutor) Add(task interface{}, size int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush forces ce to flush and execute tasks.
|
||||
func (ce *ChunkExecutor) Flush() {
|
||||
ce.executor.Flush()
|
||||
}
|
||||
|
||||
// Wait waits the execution to be done.
|
||||
func (ce *ChunkExecutor) Wait() {
|
||||
ce.executor.Wait()
|
||||
}
|
||||
|
||||
// WithChunkBytes customizes a ChunkExecutor with the given chunk size.
|
||||
func WithChunkBytes(size int) ChunkOption {
|
||||
return func(options *chunkOptions) {
|
||||
options.chunkSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlushInterval customizes a ChunkExecutor with the given flush interval.
|
||||
func WithFlushInterval(duration time.Duration) ChunkOption {
|
||||
return func(options *chunkOptions) {
|
||||
options.flushInterval = duration
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/threading"
|
||||
)
|
||||
|
||||
// A DelayExecutor delays a tasks on given delay interval.
|
||||
type DelayExecutor struct {
|
||||
fn func()
|
||||
delay time.Duration
|
||||
@@ -14,6 +15,7 @@ type DelayExecutor struct {
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewDelayExecutor returns a DelayExecutor with given fn and delay.
|
||||
func NewDelayExecutor(fn func(), delay time.Duration) *DelayExecutor {
|
||||
return &DelayExecutor{
|
||||
fn: fn,
|
||||
@@ -21,6 +23,7 @@ func NewDelayExecutor(fn func(), delay time.Duration) *DelayExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger triggers the task to be executed after given delay, safe to trigger more than once.
|
||||
func (de *DelayExecutor) Trigger() {
|
||||
de.lock.Lock()
|
||||
defer de.lock.Unlock()
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/timex"
|
||||
)
|
||||
|
||||
// A LessExecutor is an executor to limit execution once within given time interval.
|
||||
type LessExecutor struct {
|
||||
threshold time.Duration
|
||||
lastTime *syncx.AtomicDuration
|
||||
}
|
||||
|
||||
// NewLessExecutor returns a LessExecutor with given threshold as time interval.
|
||||
func NewLessExecutor(threshold time.Duration) *LessExecutor {
|
||||
return &LessExecutor{
|
||||
threshold: threshold,
|
||||
@@ -19,6 +21,8 @@ func NewLessExecutor(threshold time.Duration) *LessExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// DoOrDiscard executes or discards the task depends on if
|
||||
// another task was executed within the time interval.
|
||||
func (le *LessExecutor) DoOrDiscard(execute func()) bool {
|
||||
now := timex.Now()
|
||||
lastTime := le.lastTime.Load()
|
||||
|
||||
@@ -3,6 +3,7 @@ package executors
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
const idleRound = 10
|
||||
|
||||
type (
|
||||
// A type that satisfies executors.TaskContainer can be used as the underlying
|
||||
// TaskContainer interface defines a type that can be used as the underlying
|
||||
// container that used to do periodical executions.
|
||||
TaskContainer interface {
|
||||
// AddTask adds the task into the container.
|
||||
@@ -27,6 +28,7 @@ type (
|
||||
RemoveAll() interface{}
|
||||
}
|
||||
|
||||
// A PeriodicalExecutor is an executor that periodically execute tasks.
|
||||
PeriodicalExecutor struct {
|
||||
commander chan interface{}
|
||||
interval time.Duration
|
||||
@@ -35,12 +37,14 @@ type (
|
||||
// avoid race condition on waitGroup when calling wg.Add/Done/Wait(...)
|
||||
wgBarrier syncx.Barrier
|
||||
confirmChan chan lang.PlaceholderType
|
||||
inflight int32
|
||||
guarded bool
|
||||
newTicker func(duration time.Duration) timex.Ticker
|
||||
lock sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
// NewPeriodicalExecutor returns a PeriodicalExecutor with given interval and container.
|
||||
func NewPeriodicalExecutor(interval time.Duration, container TaskContainer) *PeriodicalExecutor {
|
||||
executor := &PeriodicalExecutor{
|
||||
// buffer 1 to let the caller go quickly
|
||||
@@ -49,7 +53,7 @@ func NewPeriodicalExecutor(interval time.Duration, container TaskContainer) *Per
|
||||
container: container,
|
||||
confirmChan: make(chan lang.PlaceholderType),
|
||||
newTicker: func(d time.Duration) timex.Ticker {
|
||||
return timex.NewTicker(interval)
|
||||
return timex.NewTicker(d)
|
||||
},
|
||||
}
|
||||
proc.AddShutdownListener(func() {
|
||||
@@ -59,6 +63,7 @@ func NewPeriodicalExecutor(interval time.Duration, container TaskContainer) *Per
|
||||
return executor
|
||||
}
|
||||
|
||||
// Add adds tasks into pe.
|
||||
func (pe *PeriodicalExecutor) Add(task interface{}) {
|
||||
if vals, ok := pe.addAndCheck(task); ok {
|
||||
pe.commander <- vals
|
||||
@@ -66,6 +71,7 @@ func (pe *PeriodicalExecutor) Add(task interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Flush forces pe to execute tasks.
|
||||
func (pe *PeriodicalExecutor) Flush() bool {
|
||||
pe.enterExecution()
|
||||
return pe.executeTasks(func() interface{} {
|
||||
@@ -75,13 +81,16 @@ func (pe *PeriodicalExecutor) Flush() bool {
|
||||
}())
|
||||
}
|
||||
|
||||
// Sync lets caller to run fn thread-safe with pe, especially for the underlying container.
|
||||
func (pe *PeriodicalExecutor) Sync(fn func()) {
|
||||
pe.lock.Lock()
|
||||
defer pe.lock.Unlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
// Wait waits the execution to be done.
|
||||
func (pe *PeriodicalExecutor) Wait() {
|
||||
pe.Flush()
|
||||
pe.wgBarrier.Guard(func() {
|
||||
pe.waitGroup.Wait()
|
||||
})
|
||||
@@ -90,18 +99,16 @@ func (pe *PeriodicalExecutor) Wait() {
|
||||
func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool) {
|
||||
pe.lock.Lock()
|
||||
defer func() {
|
||||
var start bool
|
||||
if !pe.guarded {
|
||||
pe.guarded = true
|
||||
start = true
|
||||
// defer to unlock quickly
|
||||
defer pe.backgroundFlush()
|
||||
}
|
||||
pe.lock.Unlock()
|
||||
if start {
|
||||
pe.backgroundFlush()
|
||||
}
|
||||
}()
|
||||
|
||||
if pe.container.AddTask(task) {
|
||||
atomic.AddInt32(&pe.inflight, 1)
|
||||
return pe.container.RemoveAll(), true
|
||||
}
|
||||
|
||||
@@ -110,6 +117,9 @@ func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool)
|
||||
|
||||
func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
threading.GoSafe(func() {
|
||||
// flush before quit goroutine to avoid missing tasks
|
||||
defer pe.Flush()
|
||||
|
||||
ticker := pe.newTicker(pe.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -119,6 +129,7 @@ func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
select {
|
||||
case vals := <-pe.commander:
|
||||
commanded = true
|
||||
atomic.AddInt32(&pe.inflight, -1)
|
||||
pe.enterExecution()
|
||||
pe.confirmChan <- lang.Placeholder
|
||||
pe.executeTasks(vals)
|
||||
@@ -128,13 +139,7 @@ func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
commanded = false
|
||||
} else if pe.Flush() {
|
||||
last = timex.Now()
|
||||
} else if timex.Since(last) > pe.interval*idleRound {
|
||||
pe.lock.Lock()
|
||||
pe.guarded = false
|
||||
pe.lock.Unlock()
|
||||
|
||||
// flush again to avoid missing tasks
|
||||
pe.Flush()
|
||||
} else if pe.shallQuit(last) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -177,3 +182,19 @@ func (pe *PeriodicalExecutor) hasTasks(tasks interface{}) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *PeriodicalExecutor) shallQuit(last time.Duration) (stop bool) {
|
||||
if timex.Since(last) <= pe.interval*idleRound {
|
||||
return
|
||||
}
|
||||
|
||||
// checking pe.inflight and setting pe.guarded should be locked together
|
||||
pe.lock.Lock()
|
||||
if atomic.LoadInt32(&pe.inflight) == 0 {
|
||||
pe.guarded = false
|
||||
stop = true
|
||||
}
|
||||
pe.lock.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,6 +140,26 @@ func TestPeriodicalExecutor_WaitFast(t *testing.T) {
|
||||
assert.Equal(t, total, cnt)
|
||||
}
|
||||
|
||||
func TestPeriodicalExecutor_Deadlock(t *testing.T) {
|
||||
executor := NewBulkExecutor(func(tasks []interface{}) {
|
||||
}, WithBulkTasks(1), WithBulkInterval(time.Millisecond))
|
||||
for i := 0; i < 1e5; i++ {
|
||||
executor.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicalExecutor_hasTasks(t *testing.T) {
|
||||
ticker := timex.NewFakeTicker()
|
||||
defer ticker.Stop()
|
||||
|
||||
exec := NewPeriodicalExecutor(time.Millisecond, newContainer(time.Millisecond, nil))
|
||||
exec.newTicker = func(d time.Duration) timex.Ticker {
|
||||
return ticker
|
||||
}
|
||||
assert.False(t, exec.hasTasks(nil))
|
||||
assert.True(t, exec.hasTasks(1))
|
||||
}
|
||||
|
||||
// go test -benchtime 10s -bench .
|
||||
func BenchmarkExecutor(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
@@ -4,4 +4,5 @@ import "time"
|
||||
|
||||
const defaultFlushInterval = time.Second
|
||||
|
||||
// Execute defines the method to execute tasks.
|
||||
type Execute func(tasks []interface{})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
const bufSize = 1024
|
||||
|
||||
// FirstLine returns the first line of the file.
|
||||
func FirstLine(filename string) (string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@@ -17,6 +18,7 @@ func FirstLine(filename string) (string, error) {
|
||||
return firstLine(file)
|
||||
}
|
||||
|
||||
// LastLine returns the last line of the file.
|
||||
func LastLine(filename string) (string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@@ -69,11 +71,11 @@ func lastLine(filename string, file *os.File) (string, error) {
|
||||
|
||||
if buf[n-1] == '\n' {
|
||||
buf = buf[:n-1]
|
||||
n -= 1
|
||||
n--
|
||||
} else {
|
||||
buf = buf[:n]
|
||||
}
|
||||
for n -= 1; n >= 0; n-- {
|
||||
for n--; n >= 0; n-- {
|
||||
if buf[n] == '\n' {
|
||||
return string(append(buf[n+1:], last...)), nil
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// OffsetRange represents a content block of a file.
|
||||
type OffsetRange struct {
|
||||
File string
|
||||
Start int64
|
||||
Stop int64
|
||||
}
|
||||
|
||||
// SplitLineChunks splits file into chunks.
|
||||
// The whole line are guaranteed to be split in the same chunk.
|
||||
func SplitLineChunks(filename string, chunks int) ([]OffsetRange, error) {
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,8 +3,11 @@ package filex
|
||||
import "gopkg.in/cheggaaa/pb.v1"
|
||||
|
||||
type (
|
||||
// A Scanner is used to read lines.
|
||||
Scanner interface {
|
||||
// Scan checks if has remaining to read.
|
||||
Scan() bool
|
||||
// Text returns next line.
|
||||
Text() string
|
||||
}
|
||||
|
||||
@@ -14,6 +17,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewProgressScanner returns a Scanner with progress indicator.
|
||||
func NewProgressScanner(scanner Scanner, bar *pb.ProgressBar) Scanner {
|
||||
return &progressScanner{
|
||||
Scanner: scanner,
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// A RangeReader is used to read a range of content from a file.
|
||||
type RangeReader struct {
|
||||
file *os.File
|
||||
start int64
|
||||
stop int64
|
||||
}
|
||||
|
||||
// NewRangeReader returns a RangeReader, which will read the range of content from file.
|
||||
func NewRangeReader(file *os.File, start, stop int64) *RangeReader {
|
||||
return &RangeReader{
|
||||
file: file,
|
||||
@@ -19,6 +21,7 @@ func NewRangeReader(file *os.File, start, stop int64) *RangeReader {
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads the range of content into p.
|
||||
func (rr *RangeReader) Read(p []byte) (n int, err error) {
|
||||
stat, err := rr.file.Stat()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package fs
|
||||
@@ -7,6 +8,7 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// CloseOnExec makes sure closing the file on process forking.
|
||||
func CloseOnExec(file *os.File) {
|
||||
if file != nil {
|
||||
syscall.CloseOnExec(int(file.Fd()))
|
||||
|
||||
400
core/fx/fn.go
400
core/fx/fn.go
@@ -1,400 +0,0 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/collection"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/threading"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWorkers = 16
|
||||
minWorkers = 1
|
||||
)
|
||||
|
||||
type (
|
||||
rxOptions struct {
|
||||
unlimitedWorkers bool
|
||||
workers int
|
||||
}
|
||||
|
||||
FilterFunc func(item interface{}) bool
|
||||
ForAllFunc func(pipe <-chan interface{})
|
||||
ForEachFunc func(item interface{})
|
||||
GenerateFunc func(source chan<- interface{})
|
||||
KeyFunc func(item interface{}) interface{}
|
||||
LessFunc func(a, b interface{}) bool
|
||||
MapFunc func(item interface{}) interface{}
|
||||
Option func(opts *rxOptions)
|
||||
ParallelFunc func(item interface{})
|
||||
ReduceFunc func(pipe <-chan interface{}) (interface{}, error)
|
||||
WalkFunc func(item interface{}, pipe chan<- interface{})
|
||||
|
||||
Stream struct {
|
||||
source <-chan interface{}
|
||||
}
|
||||
)
|
||||
|
||||
// From constructs a Stream from the given GenerateFunc.
|
||||
func From(generate GenerateFunc) Stream {
|
||||
source := make(chan interface{})
|
||||
|
||||
threading.GoSafe(func() {
|
||||
defer close(source)
|
||||
generate(source)
|
||||
})
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Just converts the given arbitrary items to a Stream.
|
||||
func Just(items ...interface{}) Stream {
|
||||
source := make(chan interface{}, len(items))
|
||||
for _, item := range items {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Range converts the given channel to a Stream.
|
||||
func Range(source <-chan interface{}) Stream {
|
||||
return Stream{
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer buffers the items into a queue with size n.
|
||||
// It can balance the producer and the consumer if their processing throughput don't match.
|
||||
func (p Stream) Buffer(n int) Stream {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
source := make(chan interface{}, n)
|
||||
go func() {
|
||||
for item := range p.source {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Count counts the number of elements in the result.
|
||||
func (p Stream) Count() (count int) {
|
||||
for range p.source {
|
||||
count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Distinct removes the duplicated items base on the given KeyFunc.
|
||||
func (p Stream) Distinct(fn KeyFunc) Stream {
|
||||
source := make(chan interface{})
|
||||
|
||||
threading.GoSafe(func() {
|
||||
defer close(source)
|
||||
|
||||
keys := make(map[interface{}]lang.PlaceholderType)
|
||||
for item := range p.source {
|
||||
key := fn(item)
|
||||
if _, ok := keys[key]; !ok {
|
||||
source <- item
|
||||
keys[key] = lang.Placeholder
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Done waits all upstreaming operations to be done.
|
||||
func (p Stream) Done() {
|
||||
for range p.source {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter filters the items by the given FilterFunc.
|
||||
func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream {
|
||||
return p.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
if fn(item) {
|
||||
pipe <- item
|
||||
}
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// ForAll handles the streaming elements from the source and no later streams.
|
||||
func (p Stream) ForAll(fn ForAllFunc) {
|
||||
fn(p.source)
|
||||
}
|
||||
|
||||
// ForEach seals the Stream with the ForEachFunc on each item, no successive operations.
|
||||
func (p Stream) ForEach(fn ForEachFunc) {
|
||||
for item := range p.source {
|
||||
fn(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Group groups the elements into different groups based on their keys.
|
||||
func (p Stream) Group(fn KeyFunc) Stream {
|
||||
groups := make(map[interface{}][]interface{})
|
||||
for item := range p.source {
|
||||
key := fn(item)
|
||||
groups[key] = append(groups[key], item)
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
go func() {
|
||||
for _, group := range groups {
|
||||
source <- group
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
func (p Stream) Head(n int64) Stream {
|
||||
if n < 1 {
|
||||
panic("n must be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
for item := range p.source {
|
||||
n--
|
||||
if n >= 0 {
|
||||
source <- item
|
||||
}
|
||||
if n == 0 {
|
||||
// let successive method go ASAP even we have more items to skip
|
||||
// why we don't just break the loop, because if break,
|
||||
// this former goroutine will block forever, which will cause goroutine leak.
|
||||
close(source)
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
close(source)
|
||||
}
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Maps converts each item to another corresponding item, which means it's a 1:1 model.
|
||||
func (p Stream) Map(fn MapFunc, opts ...Option) Stream {
|
||||
return p.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
pipe <- fn(item)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// Merge merges all the items into a slice and generates a new stream.
|
||||
func (p Stream) Merge() Stream {
|
||||
var items []interface{}
|
||||
for item := range p.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
source := make(chan interface{}, 1)
|
||||
source <- items
|
||||
close(source)
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Parallel applies the given ParallelFunc to each item concurrently with given number of workers.
|
||||
func (p Stream) Parallel(fn ParallelFunc, opts ...Option) {
|
||||
p.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
fn(item)
|
||||
}, opts...).Done()
|
||||
}
|
||||
|
||||
// Reduce is a utility method to let the caller deal with the underlying channel.
|
||||
func (p Stream) Reduce(fn ReduceFunc) (interface{}, error) {
|
||||
return fn(p.source)
|
||||
}
|
||||
|
||||
// Reverse reverses the elements in the stream.
|
||||
func (p Stream) Reverse() Stream {
|
||||
var items []interface{}
|
||||
for item := range p.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
// reverse, official method
|
||||
for i := len(items)/2 - 1; i >= 0; i-- {
|
||||
opp := len(items) - 1 - i
|
||||
items[i], items[opp] = items[opp], items[i]
|
||||
}
|
||||
|
||||
return Just(items...)
|
||||
}
|
||||
|
||||
// Sort sorts the items from the underlying source.
|
||||
func (p Stream) Sort(less LessFunc) Stream {
|
||||
var items []interface{}
|
||||
for item := range p.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return less(items[i], items[j])
|
||||
})
|
||||
|
||||
return Just(items...)
|
||||
}
|
||||
|
||||
// Split splits the elements into chunk with size up to n,
|
||||
// might be less than n on tailing elements.
|
||||
func (p Stream) Split(n int) Stream {
|
||||
if n < 1 {
|
||||
panic("n should be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
go func() {
|
||||
var chunk []interface{}
|
||||
for item := range p.source {
|
||||
chunk = append(chunk, item)
|
||||
if len(chunk) == n {
|
||||
source <- chunk
|
||||
chunk = nil
|
||||
}
|
||||
}
|
||||
if chunk != nil {
|
||||
source <- chunk
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
func (p Stream) Tail(n int64) Stream {
|
||||
if n < 1 {
|
||||
panic("n should be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
ring := collection.NewRing(int(n))
|
||||
for item := range p.source {
|
||||
ring.Add(item)
|
||||
}
|
||||
for _, item := range ring.Take() {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Walk lets the callers handle each item, the caller may write zero, one or more items base on the given item.
|
||||
func (p Stream) Walk(fn WalkFunc, opts ...Option) Stream {
|
||||
option := buildOptions(opts...)
|
||||
if option.unlimitedWorkers {
|
||||
return p.walkUnlimited(fn, option)
|
||||
} else {
|
||||
return p.walkLimited(fn, option)
|
||||
}
|
||||
}
|
||||
|
||||
func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
|
||||
pipe := make(chan interface{}, option.workers)
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
pool := make(chan lang.PlaceholderType, option.workers)
|
||||
|
||||
for {
|
||||
pool <- lang.Placeholder
|
||||
item, ok := <-p.source
|
||||
if !ok {
|
||||
<-pool
|
||||
break
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
// better to safely run caller defined method
|
||||
threading.GoSafe(func() {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
<-pool
|
||||
}()
|
||||
|
||||
fn(item, pipe)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
return Range(pipe)
|
||||
}
|
||||
|
||||
func (p Stream) walkUnlimited(fn WalkFunc, option *rxOptions) Stream {
|
||||
pipe := make(chan interface{}, defaultWorkers)
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for {
|
||||
item, ok := <-p.source
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
// better to safely run caller defined method
|
||||
threading.GoSafe(func() {
|
||||
defer wg.Done()
|
||||
fn(item, pipe)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
return Range(pipe)
|
||||
}
|
||||
|
||||
// UnlimitedWorkers lets the caller to use as many workers as the tasks.
|
||||
func UnlimitedWorkers() Option {
|
||||
return func(opts *rxOptions) {
|
||||
opts.unlimitedWorkers = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithWorkers lets the caller to customize the concurrent workers.
|
||||
func WithWorkers(workers int) Option {
|
||||
return func(opts *rxOptions) {
|
||||
if workers < minWorkers {
|
||||
opts.workers = minWorkers
|
||||
} else {
|
||||
opts.workers = workers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildOptions(opts ...Option) *rxOptions {
|
||||
options := newOptions()
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func newOptions() *rxOptions {
|
||||
return &rxOptions{
|
||||
workers: defaultWorkers,
|
||||
}
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
func TestBuffer(t *testing.T) {
|
||||
const N = 5
|
||||
var count int32
|
||||
var wait sync.WaitGroup
|
||||
wait.Add(1)
|
||||
From(func(source chan<- interface{}) {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for i := 0; i < 2*N; i++ {
|
||||
select {
|
||||
case source <- i:
|
||||
atomic.AddInt32(&count, 1)
|
||||
case <-ticker.C:
|
||||
wait.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}).Buffer(N).ForAll(func(pipe <-chan interface{}) {
|
||||
wait.Wait()
|
||||
// why N+1, because take one more to wait for sending into the channel
|
||||
assert.Equal(t, int32(N+1), atomic.LoadInt32(&count))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBufferNegative(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Buffer(-1).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestCount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
elements []interface{}
|
||||
}{
|
||||
{
|
||||
name: "no elements with nil",
|
||||
},
|
||||
{
|
||||
name: "no elements",
|
||||
elements: []interface{}{},
|
||||
},
|
||||
{
|
||||
name: "1 element",
|
||||
elements: []interface{}{1},
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
elements: []interface{}{1, 2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
val := Just(test.elements...).Count()
|
||||
assert.Equal(t, len(test.elements), val)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDone(t *testing.T) {
|
||||
var count int32
|
||||
Just(1, 2, 3).Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, int32(item.(int)))
|
||||
}).Done()
|
||||
assert.Equal(t, int32(6), count)
|
||||
}
|
||||
|
||||
func TestJust(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestDistinct(t *testing.T) {
|
||||
var result int
|
||||
Just(4, 1, 3, 2, 3, 4).Distinct(func(item interface{}) interface{} {
|
||||
return item
|
||||
}).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Filter(func(item interface{}) bool {
|
||||
return item.(int)%2 == 0
|
||||
}).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestForAll(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Filter(func(item interface{}) bool {
|
||||
return item.(int)%2 == 0
|
||||
}).ForAll(func(pipe <-chan interface{}) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
})
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
var groups [][]int
|
||||
Just(10, 11, 20, 21).Group(func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
return v / 10
|
||||
}).ForEach(func(item interface{}) {
|
||||
v := item.([]interface{})
|
||||
var group []int
|
||||
for _, each := range v {
|
||||
group = append(group, each.(int))
|
||||
}
|
||||
groups = append(groups, group)
|
||||
})
|
||||
|
||||
assert.Equal(t, 2, len(groups))
|
||||
for _, group := range groups {
|
||||
assert.Equal(t, 2, len(group))
|
||||
assert.True(t, group[0]/10 == group[1]/10)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHead(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Head(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 3, result)
|
||||
}
|
||||
|
||||
func TestHeadZero(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4).Head(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHeadMore(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Head(6).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
tests := []struct {
|
||||
mapper MapFunc
|
||||
expect int
|
||||
}{
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
return v * v
|
||||
},
|
||||
expect: 30,
|
||||
},
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
if v%2 == 0 {
|
||||
return 0
|
||||
}
|
||||
return v * v
|
||||
},
|
||||
expect: 10,
|
||||
},
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
if v%2 == 0 {
|
||||
panic(v)
|
||||
}
|
||||
return v * v
|
||||
},
|
||||
expect: 10,
|
||||
},
|
||||
}
|
||||
|
||||
// Map(...) works even WithWorkers(0)
|
||||
for i, test := range tests {
|
||||
t.Run(stringx.Rand(), func(t *testing.T) {
|
||||
var result int
|
||||
var workers int
|
||||
if i%2 == 0 {
|
||||
workers = 0
|
||||
} else {
|
||||
workers = runtime.NumCPU()
|
||||
}
|
||||
From(func(source chan<- interface{}) {
|
||||
for i := 1; i < 5; i++ {
|
||||
source <- i
|
||||
}
|
||||
}).Map(test.mapper, WithWorkers(workers)).Reduce(
|
||||
func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
assert.Equal(t, test.expect, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
Just(1, 2, 3, 4).Merge().ForEach(func(item interface{}) {
|
||||
assert.ElementsMatch(t, []interface{}{1, 2, 3, 4}, item.([]interface{}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelJust(t *testing.T) {
|
||||
var count int32
|
||||
Just(1, 2, 3).Parallel(func(item interface{}) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, int32(item.(int)))
|
||||
}, UnlimitedWorkers())
|
||||
assert.Equal(t, int32(6), count)
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
Just(1, 2, 3, 4).Reverse().Merge().ForEach(func(item interface{}) {
|
||||
assert.ElementsMatch(t, []interface{}{4, 3, 2, 1}, item.([]interface{}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
var prev int
|
||||
Just(5, 3, 7, 1, 9, 6, 4, 8, 2).Sort(func(a, b interface{}) bool {
|
||||
return a.(int) < b.(int)
|
||||
}).ForEach(func(item interface{}) {
|
||||
next := item.(int)
|
||||
assert.True(t, prev < next)
|
||||
prev = next
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(0).Done()
|
||||
})
|
||||
var chunks [][]interface{}
|
||||
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(4).ForEach(func(item interface{}) {
|
||||
chunk := item.([]interface{})
|
||||
chunks = append(chunks, chunk)
|
||||
})
|
||||
assert.EqualValues(t, [][]interface{}{
|
||||
{1, 2, 3, 4},
|
||||
{5, 6, 7, 8},
|
||||
{9, 10},
|
||||
}, chunks)
|
||||
}
|
||||
|
||||
func TestTail(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Tail(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 7, result)
|
||||
}
|
||||
|
||||
func TestTailZero(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4).Tail(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4, 5).Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
if item.(int)%2 != 0 {
|
||||
pipe <- item
|
||||
}
|
||||
}, UnlimitedWorkers()).ForEach(func(item interface{}) {
|
||||
result += item.(int)
|
||||
})
|
||||
assert.Equal(t, 9, result)
|
||||
}
|
||||
|
||||
func BenchmarkMapReduce(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
mapper := func(v interface{}) interface{} {
|
||||
return v.(int64) * v.(int64)
|
||||
}
|
||||
reducer := func(input <-chan interface{}) (interface{}, error) {
|
||||
var result int64
|
||||
for v := range input {
|
||||
result += v.(int64)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
From(func(input chan<- interface{}) {
|
||||
for j := 0; j < 2; j++ {
|
||||
input <- int64(j)
|
||||
}
|
||||
}).Map(mapper).Reduce(reducer)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package fx
|
||||
|
||||
import "github.com/tal-tech/go-zero/core/threading"
|
||||
|
||||
// Parallel runs fns parallelly and waits for done.
|
||||
func Parallel(fns ...func()) {
|
||||
group := threading.NewRoutineGroup()
|
||||
for _, fn := range fns {
|
||||
|
||||
@@ -5,6 +5,7 @@ import "github.com/tal-tech/go-zero/core/errorx"
|
||||
const defaultRetryTimes = 3
|
||||
|
||||
type (
|
||||
// RetryOption defines the method to customize DoWithRetry.
|
||||
RetryOption func(*retryOptions)
|
||||
|
||||
retryOptions struct {
|
||||
@@ -12,8 +13,9 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func DoWithRetries(fn func() error, opts ...RetryOption) error {
|
||||
var options = newRetryOptions()
|
||||
// DoWithRetry runs fn, and retries if failed. Default to retry 3 times.
|
||||
func DoWithRetry(fn func() error, opts ...RetryOption) error {
|
||||
options := newRetryOptions()
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
@@ -30,7 +32,8 @@ func DoWithRetries(fn func() error, opts ...RetryOption) error {
|
||||
return berr.Err()
|
||||
}
|
||||
|
||||
func WithRetries(times int) RetryOption {
|
||||
// WithRetry customize a DoWithRetry call with given retry times.
|
||||
func WithRetry(times int) RetryOption {
|
||||
return func(options *retryOptions) {
|
||||
options.times = times
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
)
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
assert.NotNil(t, DoWithRetries(func() error {
|
||||
assert.NotNil(t, DoWithRetry(func() error {
|
||||
return errors.New("any")
|
||||
}))
|
||||
|
||||
var times int
|
||||
assert.Nil(t, DoWithRetries(func() error {
|
||||
assert.Nil(t, DoWithRetry(func() error {
|
||||
times++
|
||||
if times == defaultRetryTimes {
|
||||
return nil
|
||||
@@ -22,7 +22,7 @@ func TestRetry(t *testing.T) {
|
||||
}))
|
||||
|
||||
times = 0
|
||||
assert.NotNil(t, DoWithRetries(func() error {
|
||||
assert.NotNil(t, DoWithRetry(func() error {
|
||||
times++
|
||||
if times == defaultRetryTimes+1 {
|
||||
return nil
|
||||
@@ -30,13 +30,13 @@ func TestRetry(t *testing.T) {
|
||||
return errors.New("any")
|
||||
}))
|
||||
|
||||
var total = 2 * defaultRetryTimes
|
||||
total := 2 * defaultRetryTimes
|
||||
times = 0
|
||||
assert.Nil(t, DoWithRetries(func() error {
|
||||
assert.Nil(t, DoWithRetry(func() error {
|
||||
times++
|
||||
if times == total {
|
||||
return nil
|
||||
}
|
||||
return errors.New("any")
|
||||
}, WithRetries(total)))
|
||||
}, WithRetry(total)))
|
||||
}
|
||||
|
||||
542
core/fx/stream.go
Normal file
542
core/fx/stream.go
Normal file
@@ -0,0 +1,542 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/collection"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/threading"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWorkers = 16
|
||||
minWorkers = 1
|
||||
)
|
||||
|
||||
type (
|
||||
rxOptions struct {
|
||||
unlimitedWorkers bool
|
||||
workers int
|
||||
}
|
||||
|
||||
// FilterFunc defines the method to filter a Stream.
|
||||
FilterFunc func(item interface{}) bool
|
||||
// ForAllFunc defines the method to handle all elements in a Stream.
|
||||
ForAllFunc func(pipe <-chan interface{})
|
||||
// ForEachFunc defines the method to handle each element in a Stream.
|
||||
ForEachFunc func(item interface{})
|
||||
// GenerateFunc defines the method to send elements into a Stream.
|
||||
GenerateFunc func(source chan<- interface{})
|
||||
// KeyFunc defines the method to generate keys for the elements in a Stream.
|
||||
KeyFunc func(item interface{}) interface{}
|
||||
// LessFunc defines the method to compare the elements in a Stream.
|
||||
LessFunc func(a, b interface{}) bool
|
||||
// MapFunc defines the method to map each element to another object in a Stream.
|
||||
MapFunc func(item interface{}) interface{}
|
||||
// Option defines the method to customize a Stream.
|
||||
Option func(opts *rxOptions)
|
||||
// ParallelFunc defines the method to handle elements parallelly.
|
||||
ParallelFunc func(item interface{})
|
||||
// ReduceFunc defines the method to reduce all the elements in a Stream.
|
||||
ReduceFunc func(pipe <-chan interface{}) (interface{}, error)
|
||||
// WalkFunc defines the method to walk through all the elements in a Stream.
|
||||
WalkFunc func(item interface{}, pipe chan<- interface{})
|
||||
|
||||
// A Stream is a stream that can be used to do stream processing.
|
||||
Stream struct {
|
||||
source <-chan interface{}
|
||||
}
|
||||
)
|
||||
|
||||
// Concat returns a concatenated Stream.
|
||||
func Concat(s Stream, others ...Stream) Stream {
|
||||
return s.Concat(others...)
|
||||
}
|
||||
|
||||
// From constructs a Stream from the given GenerateFunc.
|
||||
func From(generate GenerateFunc) Stream {
|
||||
source := make(chan interface{})
|
||||
|
||||
threading.GoSafe(func() {
|
||||
defer close(source)
|
||||
generate(source)
|
||||
})
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Just converts the given arbitrary items to a Stream.
|
||||
func Just(items ...interface{}) Stream {
|
||||
source := make(chan interface{}, len(items))
|
||||
for _, item := range items {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Range converts the given channel to a Stream.
|
||||
func Range(source <-chan interface{}) Stream {
|
||||
return Stream{
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
// AllMach 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.
|
||||
// If the stream is empty then true is returned and the predicate is not evaluated.
|
||||
func (s Stream) AllMach(predicate func(item interface{}) bool) bool {
|
||||
for item := range s.source {
|
||||
if !predicate(item) {
|
||||
// make sure the former goroutine not block, and current func returns fast.
|
||||
go drain(s.source)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AnyMach 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.
|
||||
// If the stream is empty then false is returned and the predicate is not evaluated.
|
||||
func (s Stream) AnyMach(predicate func(item interface{}) bool) bool {
|
||||
for item := range s.source {
|
||||
if predicate(item) {
|
||||
// make sure the former goroutine not block, and current func returns fast.
|
||||
go drain(s.source)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Buffer buffers the items into a queue with size n.
|
||||
// It can balance the producer and the consumer if their processing throughput don't match.
|
||||
func (s Stream) Buffer(n int) Stream {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
source := make(chan interface{}, n)
|
||||
go func() {
|
||||
for item := range s.source {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Concat returns a Stream that concatenated other streams
|
||||
func (s Stream) Concat(others ...Stream) Stream {
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
group := threading.NewRoutineGroup()
|
||||
group.Run(func() {
|
||||
for item := range s.source {
|
||||
source <- item
|
||||
}
|
||||
})
|
||||
|
||||
for _, each := range others {
|
||||
each := each
|
||||
group.Run(func() {
|
||||
for item := range each.source {
|
||||
source <- item
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
group.Wait()
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Count counts the number of elements in the result.
|
||||
func (s Stream) Count() (count int) {
|
||||
for range s.source {
|
||||
count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Distinct removes the duplicated items base on the given KeyFunc.
|
||||
func (s Stream) Distinct(fn KeyFunc) Stream {
|
||||
source := make(chan interface{})
|
||||
|
||||
threading.GoSafe(func() {
|
||||
defer close(source)
|
||||
|
||||
keys := make(map[interface{}]lang.PlaceholderType)
|
||||
for item := range s.source {
|
||||
key := fn(item)
|
||||
if _, ok := keys[key]; !ok {
|
||||
source <- item
|
||||
keys[key] = lang.Placeholder
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Done waits all upstreaming operations to be done.
|
||||
func (s Stream) Done() {
|
||||
drain(s.source)
|
||||
}
|
||||
|
||||
// Filter filters the items by the given FilterFunc.
|
||||
func (s Stream) Filter(fn FilterFunc, opts ...Option) Stream {
|
||||
return s.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
if fn(item) {
|
||||
pipe <- item
|
||||
}
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// First returns the first item, nil if no items.
|
||||
func (s Stream) First() interface{} {
|
||||
for item := range s.source {
|
||||
// make sure the former goroutine not block, and current func returns fast.
|
||||
go drain(s.source)
|
||||
return item
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForAll handles the streaming elements from the source and no later streams.
|
||||
func (s Stream) ForAll(fn ForAllFunc) {
|
||||
fn(s.source)
|
||||
// avoid goroutine leak on fn not consuming all items.
|
||||
go drain(s.source)
|
||||
}
|
||||
|
||||
// ForEach seals the Stream with the ForEachFunc on each item, no successive operations.
|
||||
func (s Stream) ForEach(fn ForEachFunc) {
|
||||
for item := range s.source {
|
||||
fn(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Group groups the elements into different groups based on their keys.
|
||||
func (s Stream) Group(fn KeyFunc) Stream {
|
||||
groups := make(map[interface{}][]interface{})
|
||||
for item := range s.source {
|
||||
key := fn(item)
|
||||
groups[key] = append(groups[key], item)
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
go func() {
|
||||
for _, group := range groups {
|
||||
source <- group
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Head returns the first n elements in p.
|
||||
func (s Stream) Head(n int64) Stream {
|
||||
if n < 1 {
|
||||
panic("n must be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
for item := range s.source {
|
||||
n--
|
||||
if n >= 0 {
|
||||
source <- item
|
||||
}
|
||||
if n == 0 {
|
||||
// let successive method go ASAP even we have more items to skip
|
||||
close(source)
|
||||
// why we don't just break the loop, and drain to consume all items.
|
||||
// because if breaks, this former goroutine will block forever,
|
||||
// which will cause goroutine leak.
|
||||
drain(s.source)
|
||||
}
|
||||
}
|
||||
// not enough items in s.source, but we need to let successive method to go ASAP.
|
||||
if n > 0 {
|
||||
close(source)
|
||||
}
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Last returns the last item, or nil if no items.
|
||||
func (s Stream) Last() (item interface{}) {
|
||||
for item = range s.source {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Map converts each item to another corresponding item, which means it's a 1:1 model.
|
||||
func (s Stream) Map(fn MapFunc, opts ...Option) Stream {
|
||||
return s.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
pipe <- fn(item)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// Merge merges all the items into a slice and generates a new stream.
|
||||
func (s Stream) Merge() Stream {
|
||||
var items []interface{}
|
||||
for item := range s.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
source := make(chan interface{}, 1)
|
||||
source <- items
|
||||
close(source)
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// NoneMatch returns whether all elements of this stream don't match the provided predicate.
|
||||
// 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.
|
||||
func (s Stream) NoneMatch(predicate func(item interface{}) bool) bool {
|
||||
for item := range s.source {
|
||||
if predicate(item) {
|
||||
// make sure the former goroutine not block, and current func returns fast.
|
||||
go drain(s.source)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Parallel applies the given ParallelFunc to each item concurrently with given number of workers.
|
||||
func (s Stream) Parallel(fn ParallelFunc, opts ...Option) {
|
||||
s.Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
fn(item)
|
||||
}, opts...).Done()
|
||||
}
|
||||
|
||||
// Reduce is a utility method to let the caller deal with the underlying channel.
|
||||
func (s Stream) Reduce(fn ReduceFunc) (interface{}, error) {
|
||||
return fn(s.source)
|
||||
}
|
||||
|
||||
// Reverse reverses the elements in the stream.
|
||||
func (s Stream) Reverse() Stream {
|
||||
var items []interface{}
|
||||
for item := range s.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
// reverse, official method
|
||||
for i := len(items)/2 - 1; i >= 0; i-- {
|
||||
opp := len(items) - 1 - i
|
||||
items[i], items[opp] = items[opp], items[i]
|
||||
}
|
||||
|
||||
return Just(items...)
|
||||
}
|
||||
|
||||
// Skip returns a Stream that skips size elements.
|
||||
func (s Stream) Skip(n int64) Stream {
|
||||
if n < 0 {
|
||||
panic("n must not be negative")
|
||||
}
|
||||
if n == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
for item := range s.source {
|
||||
n--
|
||||
if n >= 0 {
|
||||
continue
|
||||
} else {
|
||||
source <- item
|
||||
}
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Sort sorts the items from the underlying source.
|
||||
func (s Stream) Sort(less LessFunc) Stream {
|
||||
var items []interface{}
|
||||
for item := range s.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return less(items[i], items[j])
|
||||
})
|
||||
|
||||
return Just(items...)
|
||||
}
|
||||
|
||||
// Split splits the elements into chunk with size up to n,
|
||||
// might be less than n on tailing elements.
|
||||
func (s Stream) Split(n int) Stream {
|
||||
if n < 1 {
|
||||
panic("n should be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
go func() {
|
||||
var chunk []interface{}
|
||||
for item := range s.source {
|
||||
chunk = append(chunk, item)
|
||||
if len(chunk) == n {
|
||||
source <- chunk
|
||||
chunk = nil
|
||||
}
|
||||
}
|
||||
if chunk != nil {
|
||||
source <- chunk
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Tail returns the last n elements in p.
|
||||
func (s Stream) Tail(n int64) Stream {
|
||||
if n < 1 {
|
||||
panic("n should be greater than 0")
|
||||
}
|
||||
|
||||
source := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
ring := collection.NewRing(int(n))
|
||||
for item := range s.source {
|
||||
ring.Add(item)
|
||||
}
|
||||
for _, item := range ring.Take() {
|
||||
source <- item
|
||||
}
|
||||
close(source)
|
||||
}()
|
||||
|
||||
return Range(source)
|
||||
}
|
||||
|
||||
// Walk lets the callers handle each item, the caller may write zero, one or more items base on the given item.
|
||||
func (s Stream) Walk(fn WalkFunc, opts ...Option) Stream {
|
||||
option := buildOptions(opts...)
|
||||
if option.unlimitedWorkers {
|
||||
return s.walkUnlimited(fn, option)
|
||||
}
|
||||
|
||||
return s.walkLimited(fn, option)
|
||||
}
|
||||
|
||||
func (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
|
||||
pipe := make(chan interface{}, option.workers)
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
pool := make(chan lang.PlaceholderType, option.workers)
|
||||
|
||||
for item := range s.source {
|
||||
// important, used in another goroutine
|
||||
val := item
|
||||
pool <- lang.Placeholder
|
||||
wg.Add(1)
|
||||
|
||||
// better to safely run caller defined method
|
||||
threading.GoSafe(func() {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
<-pool
|
||||
}()
|
||||
|
||||
fn(val, pipe)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
return Range(pipe)
|
||||
}
|
||||
|
||||
func (s Stream) walkUnlimited(fn WalkFunc, option *rxOptions) Stream {
|
||||
pipe := make(chan interface{}, option.workers)
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for item := range s.source {
|
||||
// important, used in another goroutine
|
||||
val := item
|
||||
wg.Add(1)
|
||||
// better to safely run caller defined method
|
||||
threading.GoSafe(func() {
|
||||
defer wg.Done()
|
||||
fn(val, pipe)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
return Range(pipe)
|
||||
}
|
||||
|
||||
// UnlimitedWorkers lets the caller use as many workers as the tasks.
|
||||
func UnlimitedWorkers() Option {
|
||||
return func(opts *rxOptions) {
|
||||
opts.unlimitedWorkers = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithWorkers lets the caller customize the concurrent workers.
|
||||
func WithWorkers(workers int) Option {
|
||||
return func(opts *rxOptions) {
|
||||
if workers < minWorkers {
|
||||
opts.workers = minWorkers
|
||||
} else {
|
||||
opts.workers = workers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildOptions returns a rxOptions with given customizations.
|
||||
func buildOptions(opts ...Option) *rxOptions {
|
||||
options := newOptions()
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// drain drains the given channel.
|
||||
func drain(channel <-chan interface{}) {
|
||||
for range channel {
|
||||
}
|
||||
}
|
||||
|
||||
// newOptions returns a default rxOptions.
|
||||
func newOptions() *rxOptions {
|
||||
return &rxOptions{
|
||||
workers: defaultWorkers,
|
||||
}
|
||||
}
|
||||
571
core/fx/stream_test.go
Normal file
571
core/fx/stream_test.go
Normal file
@@ -0,0 +1,571 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
func TestBuffer(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
const N = 5
|
||||
var count int32
|
||||
var wait sync.WaitGroup
|
||||
wait.Add(1)
|
||||
From(func(source chan<- interface{}) {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for i := 0; i < 2*N; i++ {
|
||||
select {
|
||||
case source <- i:
|
||||
atomic.AddInt32(&count, 1)
|
||||
case <-ticker.C:
|
||||
wait.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}).Buffer(N).ForAll(func(pipe <-chan interface{}) {
|
||||
wait.Wait()
|
||||
// why N+1, because take one more to wait for sending into the channel
|
||||
assert.Equal(t, int32(N+1), atomic.LoadInt32(&count))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBufferNegative(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Buffer(-1).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCount(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
elements []interface{}
|
||||
}{
|
||||
{
|
||||
name: "no elements with nil",
|
||||
},
|
||||
{
|
||||
name: "no elements",
|
||||
elements: []interface{}{},
|
||||
},
|
||||
{
|
||||
name: "1 element",
|
||||
elements: []interface{}{1},
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
elements: []interface{}{1, 2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
val := Just(test.elements...).Count()
|
||||
assert.Equal(t, len(test.elements), val)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDone(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var count int32
|
||||
Just(1, 2, 3).Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, int32(item.(int)))
|
||||
}).Done()
|
||||
assert.Equal(t, int32(6), count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJust(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDistinct(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(4, 1, 3, 2, 3, 4).Distinct(func(item interface{}) interface{} {
|
||||
return item
|
||||
}).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Filter(func(item interface{}) bool {
|
||||
return item.(int)%2 == 0
|
||||
}).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 6, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assert.Nil(t, Just().First())
|
||||
assert.Equal(t, "foo", Just("foo").First())
|
||||
assert.Equal(t, "foo", Just("foo", "bar").First())
|
||||
})
|
||||
}
|
||||
|
||||
func TestForAll(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Filter(func(item interface{}) bool {
|
||||
return item.(int)%2 == 0
|
||||
}).ForAll(func(pipe <-chan interface{}) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
})
|
||||
assert.Equal(t, 6, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var groups [][]int
|
||||
Just(10, 11, 20, 21).Group(func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
return v / 10
|
||||
}).ForEach(func(item interface{}) {
|
||||
v := item.([]interface{})
|
||||
var group []int
|
||||
for _, each := range v {
|
||||
group = append(group, each.(int))
|
||||
}
|
||||
groups = append(groups, group)
|
||||
})
|
||||
|
||||
assert.Equal(t, 2, len(groups))
|
||||
for _, group := range groups {
|
||||
assert.Equal(t, 2, len(group))
|
||||
assert.True(t, group[0]/10 == group[1]/10)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHead(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Head(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHeadZero(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4).Head(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHeadMore(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Head(6).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
goroutines := runtime.NumGoroutine()
|
||||
assert.Nil(t, Just().Last())
|
||||
assert.Equal(t, "foo", Just("foo").Last())
|
||||
assert.Equal(t, "bar", Just("foo", "bar").Last())
|
||||
// let scheduler schedule first
|
||||
runtime.Gosched()
|
||||
assert.Equal(t, goroutines, runtime.NumGoroutine())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
tests := []struct {
|
||||
mapper MapFunc
|
||||
expect int
|
||||
}{
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
return v * v
|
||||
},
|
||||
expect: 30,
|
||||
},
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
if v%2 == 0 {
|
||||
return 0
|
||||
}
|
||||
return v * v
|
||||
},
|
||||
expect: 10,
|
||||
},
|
||||
{
|
||||
mapper: func(item interface{}) interface{} {
|
||||
v := item.(int)
|
||||
if v%2 == 0 {
|
||||
panic(v)
|
||||
}
|
||||
return v * v
|
||||
},
|
||||
expect: 10,
|
||||
},
|
||||
}
|
||||
|
||||
// Map(...) works even WithWorkers(0)
|
||||
for i, test := range tests {
|
||||
t.Run(stringx.Rand(), func(t *testing.T) {
|
||||
var result int
|
||||
var workers int
|
||||
if i%2 == 0 {
|
||||
workers = 0
|
||||
} else {
|
||||
workers = runtime.NumCPU()
|
||||
}
|
||||
From(func(source chan<- interface{}) {
|
||||
for i := 1; i < 5; i++ {
|
||||
source <- i
|
||||
}
|
||||
}).Map(test.mapper, WithWorkers(workers)).Reduce(
|
||||
func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
assert.Equal(t, test.expect, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
Just(1, 2, 3, 4).Merge().ForEach(func(item interface{}) {
|
||||
assert.ElementsMatch(t, []interface{}{1, 2, 3, 4}, item.([]interface{}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelJust(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var count int32
|
||||
Just(1, 2, 3).Parallel(func(item interface{}) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, int32(item.(int)))
|
||||
}, UnlimitedWorkers())
|
||||
assert.Equal(t, int32(6), count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
Just(1, 2, 3, 4).Reverse().Merge().ForEach(func(item interface{}) {
|
||||
assert.ElementsMatch(t, []interface{}{4, 3, 2, 1}, item.([]interface{}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var prev int
|
||||
Just(5, 3, 7, 1, 9, 6, 4, 8, 2).Sort(func(a, b interface{}) bool {
|
||||
return a.(int) < b.(int)
|
||||
}).ForEach(func(item interface{}) {
|
||||
next := item.(int)
|
||||
assert.True(t, prev < next)
|
||||
prev = next
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(0).Done()
|
||||
})
|
||||
var chunks [][]interface{}
|
||||
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(4).ForEach(func(item interface{}) {
|
||||
chunk := item.([]interface{})
|
||||
chunks = append(chunks, chunk)
|
||||
})
|
||||
assert.EqualValues(t, [][]interface{}{
|
||||
{1, 2, 3, 4},
|
||||
{5, 6, 7, 8},
|
||||
{9, 10},
|
||||
}, chunks)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTail(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4).Tail(2).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
for item := range pipe {
|
||||
result += item.(int)
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
assert.Equal(t, 7, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTailZero(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4).Tail(0).Reduce(func(pipe <-chan interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
var result int
|
||||
Just(1, 2, 3, 4, 5).Walk(func(item interface{}, pipe chan<- interface{}) {
|
||||
if item.(int)%2 != 0 {
|
||||
pipe <- item
|
||||
}
|
||||
}, UnlimitedWorkers()).ForEach(func(item interface{}) {
|
||||
result += item.(int)
|
||||
})
|
||||
assert.Equal(t, 9, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStream_AnyMach(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
|
||||
return item.(int) == 4
|
||||
}))
|
||||
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
|
||||
return item.(int) == 0
|
||||
}))
|
||||
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
|
||||
return item.(int) == 2
|
||||
}))
|
||||
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
|
||||
return item.(int) == 2
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStream_AllMach(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assetEqual(
|
||||
t, true, Just(1, 2, 3).AllMach(func(item interface{}) bool {
|
||||
return true
|
||||
}),
|
||||
)
|
||||
assetEqual(
|
||||
t, false, Just(1, 2, 3).AllMach(func(item interface{}) bool {
|
||||
return false
|
||||
}),
|
||||
)
|
||||
assetEqual(
|
||||
t, false, Just(1, 2, 3).AllMach(func(item interface{}) bool {
|
||||
return item.(int) == 1
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStream_NoneMatch(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assetEqual(
|
||||
t, true, Just(1, 2, 3).NoneMatch(func(item interface{}) bool {
|
||||
return false
|
||||
}),
|
||||
)
|
||||
assetEqual(
|
||||
t, false, Just(1, 2, 3).NoneMatch(func(item interface{}) bool {
|
||||
return true
|
||||
}),
|
||||
)
|
||||
assetEqual(
|
||||
t, true, Just(1, 2, 3).NoneMatch(func(item interface{}) bool {
|
||||
return item.(int) == 4
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcat(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
a1 := []interface{}{1, 2, 3}
|
||||
a2 := []interface{}{4, 5, 6}
|
||||
s1 := Just(a1...)
|
||||
s2 := Just(a2...)
|
||||
stream := Concat(s1, s2)
|
||||
var items []interface{}
|
||||
for item := range stream.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].(int) < items[j].(int)
|
||||
})
|
||||
ints := make([]interface{}, 0)
|
||||
ints = append(ints, a1...)
|
||||
ints = append(ints, a2...)
|
||||
assetEqual(t, ints, items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStream_Skip(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
assetEqual(t, 3, Just(1, 2, 3, 4).Skip(1).Count())
|
||||
assetEqual(t, 1, Just(1, 2, 3, 4).Skip(3).Count())
|
||||
assetEqual(t, 4, Just(1, 2, 3, 4).Skip(0).Count())
|
||||
equal(t, Just(1, 2, 3, 4).Skip(3), []interface{}{4})
|
||||
assert.Panics(t, func() {
|
||||
Just(1, 2, 3, 4).Skip(-1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStream_Concat(t *testing.T) {
|
||||
runCheckedTest(t, func(t *testing.T) {
|
||||
stream := Just(1).Concat(Just(2), Just(3))
|
||||
var items []interface{}
|
||||
for item := range stream.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].(int) < items[j].(int)
|
||||
})
|
||||
assetEqual(t, []interface{}{1, 2, 3}, items)
|
||||
|
||||
just := Just(1)
|
||||
equal(t, just.Concat(just), []interface{}{1})
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkParallelMapReduce(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
mapper := func(v interface{}) interface{} {
|
||||
return v.(int64) * v.(int64)
|
||||
}
|
||||
reducer := func(input <-chan interface{}) (interface{}, error) {
|
||||
var result int64
|
||||
for v := range input {
|
||||
result += v.(int64)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
b.ResetTimer()
|
||||
From(func(input chan<- interface{}) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
input <- int64(rand.Int())
|
||||
}
|
||||
})
|
||||
}).Map(mapper).Reduce(reducer)
|
||||
}
|
||||
|
||||
func BenchmarkMapReduce(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
mapper := func(v interface{}) interface{} {
|
||||
return v.(int64) * v.(int64)
|
||||
}
|
||||
reducer := func(input <-chan interface{}) (interface{}, error) {
|
||||
var result int64
|
||||
for v := range input {
|
||||
result += v.(int64)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
b.ResetTimer()
|
||||
From(func(input chan<- interface{}) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
input <- int64(rand.Int())
|
||||
}
|
||||
}).Map(mapper).Reduce(reducer)
|
||||
}
|
||||
|
||||
func assetEqual(t *testing.T, except, data interface{}) {
|
||||
if !reflect.DeepEqual(except, data) {
|
||||
t.Errorf(" %v, want %v", data, except)
|
||||
}
|
||||
}
|
||||
|
||||
func equal(t *testing.T, stream Stream, data []interface{}) {
|
||||
items := make([]interface{}, 0)
|
||||
for item := range stream.source {
|
||||
items = append(items, item)
|
||||
}
|
||||
if !reflect.DeepEqual(items, data) {
|
||||
t.Errorf(" %v, want %v", items, data)
|
||||
}
|
||||
}
|
||||
|
||||
func runCheckedTest(t *testing.T, fn func(t *testing.T)) {
|
||||
goroutines := runtime.NumGoroutine()
|
||||
fn(t)
|
||||
// let scheduler schedule first
|
||||
time.Sleep(time.Millisecond)
|
||||
assert.True(t, runtime.NumGoroutine() <= goroutines)
|
||||
}
|
||||
@@ -2,17 +2,24 @@ package fx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCanceled is the error returned when the context is canceled.
|
||||
ErrCanceled = context.Canceled
|
||||
ErrTimeout = context.DeadlineExceeded
|
||||
// ErrTimeout is the error returned when the context's deadline passes.
|
||||
ErrTimeout = context.DeadlineExceeded
|
||||
)
|
||||
|
||||
type FxOption func() context.Context
|
||||
// DoOption defines the method to customize a DoWithTimeout call.
|
||||
type DoOption func() context.Context
|
||||
|
||||
func DoWithTimeout(fn func() error, timeout time.Duration, opts ...FxOption) error {
|
||||
// DoWithTimeout runs fn with timeout control.
|
||||
func DoWithTimeout(fn func() error, timeout time.Duration, opts ...DoOption) error {
|
||||
parentCtx := context.Background()
|
||||
for _, opt := range opts {
|
||||
parentCtx = opt()
|
||||
@@ -20,16 +27,17 @@ func DoWithTimeout(fn func() error, timeout time.Duration, opts ...FxOption) err
|
||||
ctx, cancel := context.WithTimeout(parentCtx, timeout)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error)
|
||||
// create channel with buffer size 1 to avoid goroutine leak
|
||||
done := make(chan error, 1)
|
||||
panicChan := make(chan interface{}, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
// attach call stack to avoid missing in different goroutine
|
||||
panicChan <- fmt.Sprintf("%+v\n\n%s", p, strings.TrimSpace(string(debug.Stack())))
|
||||
}
|
||||
}()
|
||||
done <- fn()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
@@ -42,7 +50,8 @@ func DoWithTimeout(fn func() error, timeout time.Duration, opts ...FxOption) err
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) FxOption {
|
||||
// WithContext customizes a DoWithTimeout call with given ctx.
|
||||
func WithContext(ctx context.Context) DoOption {
|
||||
return func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// TopWeight is the top weight that one entry might set.
|
||||
TopWeight = 100
|
||||
|
||||
minReplicas = 100
|
||||
@@ -18,10 +19,12 @@ const (
|
||||
)
|
||||
|
||||
type (
|
||||
HashFunc func(data []byte) uint64
|
||||
// Func defines the hash method.
|
||||
Func func(data []byte) uint64
|
||||
|
||||
// A ConsistentHash is a ring hash implementation.
|
||||
ConsistentHash struct {
|
||||
hashFunc HashFunc
|
||||
hashFunc Func
|
||||
replicas int
|
||||
keys []uint64
|
||||
ring map[uint64][]interface{}
|
||||
@@ -30,11 +33,13 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewConsistentHash returns a ConsistentHash.
|
||||
func NewConsistentHash() *ConsistentHash {
|
||||
return NewCustomConsistentHash(minReplicas, Hash)
|
||||
}
|
||||
|
||||
func NewCustomConsistentHash(replicas int, fn HashFunc) *ConsistentHash {
|
||||
// NewCustomConsistentHash returns a ConsistentHash with given replicas and hash func.
|
||||
func NewCustomConsistentHash(replicas int, fn Func) *ConsistentHash {
|
||||
if replicas < minReplicas {
|
||||
replicas = minReplicas
|
||||
}
|
||||
@@ -78,7 +83,7 @@ func (h *ConsistentHash) AddWithReplicas(node interface{}, replicas int) {
|
||||
h.ring[hash] = append(h.ring[hash], node)
|
||||
}
|
||||
|
||||
sort.Slice(h.keys, func(i int, j int) bool {
|
||||
sort.Slice(h.keys, func(i, j int) bool {
|
||||
return h.keys[i] < h.keys[j]
|
||||
})
|
||||
}
|
||||
@@ -92,6 +97,7 @@ func (h *ConsistentHash) AddWithWeight(node interface{}, weight int) {
|
||||
h.AddWithReplicas(node, replicas)
|
||||
}
|
||||
|
||||
// Get returns the corresponding node from h base on the given v.
|
||||
func (h *ConsistentHash) Get(v interface{}) (interface{}, bool) {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
@@ -118,6 +124,7 @@ func (h *ConsistentHash) Get(v interface{}) (interface{}, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes the given node from h.
|
||||
func (h *ConsistentHash) Remove(node interface{}) {
|
||||
nodeRepr := repr(node)
|
||||
|
||||
@@ -133,7 +140,7 @@ func (h *ConsistentHash) Remove(node interface{}) {
|
||||
index := sort.Search(len(h.keys), func(i int) bool {
|
||||
return h.keys[i] >= hash
|
||||
})
|
||||
if index < len(h.keys) {
|
||||
if index < len(h.keys) && h.keys[index] == hash {
|
||||
h.keys = append(h.keys[:index], h.keys[index+1:]...)
|
||||
}
|
||||
h.removeRingNode(hash, nodeRepr)
|
||||
|
||||
@@ -74,12 +74,12 @@ func TestConsistentHashIncrementalTransfer(t *testing.T) {
|
||||
laterCh := create()
|
||||
laterCh.AddWithWeight(node, 10*(i+1))
|
||||
|
||||
for i := 0; i < requestSize; i++ {
|
||||
key, ok := laterCh.Get(requestSize + i)
|
||||
for j := 0; j < requestSize; j++ {
|
||||
key, ok := laterCh.Get(requestSize + j)
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, key)
|
||||
value := key.(string)
|
||||
assert.True(t, value == keys[i] || value == node)
|
||||
assert.True(t, value == keys[j] || value == node)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,8 +132,8 @@ func TestConsistentHash_RemoveInterface(t *testing.T) {
|
||||
assert.Equal(t, 1, len(ch.nodes))
|
||||
node, ok := ch.Get(1)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, key, node.(*MockNode).Addr)
|
||||
assert.Equal(t, 2, node.(*MockNode).Id)
|
||||
assert.Equal(t, key, node.(*mockNode).addr)
|
||||
assert.Equal(t, 2, node.(*mockNode).id)
|
||||
}
|
||||
|
||||
func getKeysBeforeAndAfterFailure(t *testing.T, prefix string, index int) (map[int]string, map[int]string) {
|
||||
@@ -164,18 +164,18 @@ func getKeysBeforeAndAfterFailure(t *testing.T, prefix string, index int) (map[i
|
||||
return keys, newKeys
|
||||
}
|
||||
|
||||
type MockNode struct {
|
||||
Addr string
|
||||
Id int
|
||||
type mockNode struct {
|
||||
addr string
|
||||
id int
|
||||
}
|
||||
|
||||
func newMockNode(addr string, id int) *MockNode {
|
||||
return &MockNode{
|
||||
Addr: addr,
|
||||
Id: id,
|
||||
func newMockNode(addr string, id int) *mockNode {
|
||||
return &mockNode{
|
||||
addr: addr,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *MockNode) String() string {
|
||||
return n.Addr
|
||||
func (n *mockNode) String() string {
|
||||
return n.addr
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user