Compare commits

..

204 Commits

Author SHA1 Message Date
Kevin Wan
290de6aa96 docs: update roadmap (#1405) 2022-01-02 21:30:02 +08:00
Kevin Wan
a7aeb8ac0e feat: support tls for etcd client (#1390)
* feat: support tls for etcd client

* chore: fix typo

* refactor: rename TrustedCAFile to CACertFile

* docs: add comments

* fix: missing tls registration

* feat: add InsecureSkipVerify config for testing
2022-01-02 20:23:50 +08:00
Kevin Wan
a8e7fafebf refactor: optimize fx (#1404)
* refactor: optimize fx

* chore: add more comments

* ci: make test robust
2022-01-02 14:56:30 +08:00
Kevin Wan
7cc64070b1 docs: update goctl installation command (#1403) 2022-01-02 14:31:31 +08:00
Kevin Wan
c19d2637ea feat: implement fx.NoneMatch, fx.First, fx.Last (#1402)
* chore: use workers from options in fx.unlimitedWalk

* feat: add fx.NoneMatch

* feat: add fx.First, fx.Last

* chore: add more comments

* docs: add mr readme
2022-01-02 13:33:15 +08:00
Kevin Wan
fe1da14332 chore: simplify mapreduce (#1401) 2022-01-01 19:24:35 +08:00
anqiansong
8e9110cedf fix #1330 (#1382)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-30 20:44:04 +08:00
Kevin Wan
d6ff30a570 chore: fix golint issues (#1396) 2021-12-30 17:44:15 +08:00
Kevin Wan
b98d46bfd6 chore: update goctl version (#1394) 2021-12-30 15:30:16 +08:00
Kevin Wan
768936b256 ci: remove 386 binaries (#1393) 2021-12-30 15:18:24 +08:00
Kevin Wan
c6eb1a9670 ci: remove windows 386 binary (#1392)
* ci: remove windows 386 binary

* chore: update go-zero

* chore: update go-zero
2021-12-30 14:47:53 +08:00
Kevin Wan
e4ab518576 test: add more tests (#1391) 2021-12-30 14:21:55 +08:00
moyrne
dfc67b5fac fix readme-cn (#1388) 2021-12-30 10:42:23 +08:00
Kevin Wan
62266d8f91 fix #1070 (#1389)
* fix #1070

* test: add more tests
2021-12-29 21:34:28 +08:00
anqiansong
b8ea16a88e feat: Add --remote (#1387)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-29 18:16:42 +08:00
Kevin Wan
23deaf50e6 feat: support array in default and options tags (#1386)
* feat: support array in default and options tags

* feat: ignore spaces in tags

* test: add more tests
2021-12-29 17:37:36 +08:00
Kevin Wan
38a36ed8d3 docs: add go-zero users (#1381) 2021-12-28 17:12:51 +08:00
anqiansong
49bab23c54 fix #1376 (#1380)
* fix #1376

* fix #1376

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-28 16:40:26 +08:00
Leizhengzi
78ba00d3a7 fix: command system info missing go version (#1377) 2021-12-27 22:05:27 +08:00
Kevin Wan
787b046a70 docs: update slack invitation link (#1378) 2021-12-27 16:52:08 +08:00
Kevin Wan
f827a7b985 chore: update goctl version to 1.2.4 for release tools/goctl/v1.2.4 (#1372) 2021-12-27 10:57:55 +08:00
行者
f5f2097d14 Updated MySQL生成表结构体遇到关键字db部分保持原字段名定义 (#1369) 2021-12-26 21:56:04 +08:00
Kevin Wan
cfcfb87fd4 ci: add release action to auto build binaries (#1371) 2021-12-26 21:44:33 +08:00
Kevin Wan
1d223fc114 docs: update goctl markdown (#1370) 2021-12-26 20:32:31 +08:00
Kevin Wan
c0647f0719 feat: support context in MapReduce (#1368) 2021-12-25 20:42:52 +08:00
Kevin Wan
8745ed9c61 chore: add 1s for tolerance in redislock (#1367) 2021-12-25 19:44:27 +08:00
种豆得豆
836726e710 fix redis try-lock bug (#1366)
#issue_id: 1338

Co-authored-by: zhangwei <>
2021-12-25 19:20:53 +08:00
JiangYiJun
a67c118dcf go-zero tools ,fix a func,api new can not choose style (#1356) 2021-12-23 10:28:46 +08:00
Kevin Wan
cd289465fd chore: coding style and comments (#1361)
* chore: coding style and comments

* chore: optimize `ParseJsonBody` (#1353)

* chore: optimize `ParseJsonBody`

* chore: optimize `ParseJsonBody`

* fix: fix a test

* chore: optimize `ParseJsonBody`

* fix a test

* chore: add comment

* chore: refactor

Co-authored-by: chenquan <chenquan.dev@foxmail.com>
2021-12-22 21:43:37 +08:00
chenquan
263e426ae1 chore: optimize ParseJsonBody (#1353)
* chore: optimize `ParseJsonBody`

* chore: optimize `ParseJsonBody`

* fix: fix a test

* chore: optimize `ParseJsonBody`

* fix a test

* chore: add comment
2021-12-22 20:24:55 +08:00
charliecen
d5e493383a chose: cancel the assignment and judge later (#1359)
Co-authored-by: charliecen <chq@abierr.com>
2021-12-22 20:05:35 +08:00
Kevin Wan
6f1d27354a chore: put error message in error.log for verbose mode (#1355) 2021-12-21 11:36:01 +08:00
Kevin Wan
26101732d2 test: add more tests (#1352) 2021-12-20 22:42:36 +08:00
Kevin Wan
71d40e0c08 Revert "排除客户端中断导致的503错误 (#1343)" (#1351)
This reverts commit 2cdf5e7395.
2021-12-20 20:34:43 +08:00
Kevin Wan
4ba2ff7cdd feat: treat client closed requests as code 499 (#1350)
* feat: treat client closed requests as code 499

* chore: add comments
2021-12-20 19:43:38 +08:00
vic
2cdf5e7395 排除客户端中断导致的503错误 (#1343) 2021-12-20 19:43:13 +08:00
Kevin Wan
8315a55b3f Update FUNDING.yml
enable sponsorship.
2021-12-20 15:27:05 +08:00
Kevin Wan
d1c2a31af7 chore: add tests & refactor (#1346)
* chore: add tests & refactor

* chore: refactor
2021-12-18 23:11:38 +08:00
MarkJoyMa
3e6c217408 Feature: support adding custom cache to mongoc and sqlc (#1313)
* merge

* Feature: support adding custom cache to mongoc and sqlc
2021-12-18 22:45:07 +08:00
Kevin Wan
b299f350be chore: add comments (#1345) 2021-12-18 22:39:14 +08:00
Kevin Wan
8fd16c17dc chore: update goctl version to 1.2.5 (#1337) 2021-12-16 00:21:54 +08:00
anqiansong
5979b2aa0f Update template (#1335)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-15 23:24:32 +08:00
anqiansong
0b17e0e5d9 Feat goctl bug (#1332)
* Support goctl bug

* fix typo

* format code

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-15 22:43:58 +08:00
Kevin Wan
3d8ad5e4f6 feat: tidy mod, update go-zero to latest (#1334) 2021-12-15 22:34:58 +08:00
Kevin Wan
ff1752dd39 feat: tidy mod, update go-zero to latest (#1333) 2021-12-15 22:23:06 +08:00
Kevin Wan
1becaeb7be chore: refactor (#1331) 2021-12-15 20:44:23 +08:00
yangkequn
171afaadb9 Update types.go (#1314) 2021-12-15 20:16:17 +08:00
Kevin Wan
776e6e647d feat: tidy mod, add go.mod for goctl (#1328) 2021-12-15 19:44:49 +08:00
Kevin Wan
4ccdf4ec72 chore: format code (#1327) 2021-12-15 13:43:05 +08:00
CrazyZard
a7bd993c0c commit missing method for redis (#1325)
* commit `decr ` `decrby` `lindex` missing method for redis

* fix(store_test):TestRedis_DecrBy

* add unit tests for redis commands. And put the functions in alphabetical order

* put the functions in alphabetical order

* add `lindex` unit test

* sort func
2021-12-15 13:15:39 +08:00
Kevin Wan
a290ff4486 docs: add go-zero users (#1323) 2021-12-14 13:37:49 +08:00
Kevin Wan
490ef13822 style: format code (#1322) 2021-12-14 11:29:44 +08:00
anqiansong
1b14de2ff9 fix: #1318 (#1321)
* fix #1318

* fix #1318

* remove never used code

* fix unit tes

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-13 22:55:11 +08:00
Kevin Wan
914692cc82 fix #1309 (#1317) 2021-12-13 11:58:58 +08:00
anqiansong
07191dc430 fix #1305 (#1307)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-12-07 22:24:18 +08:00
BYT0723
af3fb2b04d fix: go issue 16206 (#1298) 2021-12-07 15:52:37 +08:00
Kevin Wan
0240fa131a chore: rename service context from ctx to svcCtx (#1299) 2021-12-05 22:10:47 +08:00
Kevin Wan
e96577dd38 docs: add go-zero users (#1294) 2021-12-03 22:32:35 +08:00
Kevin Wan
403dd7367a fix #1288 (#1292)
* fix #1288

* chore: make wrapup & shutdown callbacks run simulatenously
2021-12-02 22:41:57 +08:00
Kevin Wan
8086ad120b Revert "feat: reduce dependencies of framework by add go.mod in goctl (#1290)" (#1291)
This reverts commit 87a445689c.
2021-12-02 19:40:23 +08:00
Kevin Wan
87a445689c feat: reduce dependencies of framework by add go.mod in goctl (#1290) 2021-12-02 16:57:07 +08:00
Kevin Wan
b6bda54870 chore: update cli version (#1287) 2021-12-01 23:33:23 +08:00
Kevin Wan
9d528dddd6 feat: support third party orm to interact with go-zero (#1286)
* fixes #987

* chore: fix test failure

* chore: add comments

* feat: support third party orm to interact with go-zero

* chore: refactor
2021-12-01 20:22:15 +08:00
Kevin Wan
543d590710 fixes #987 (#1283)
* fixes #987

* chore: fix test failure

* chore: add comments
2021-12-01 17:45:48 +08:00
anqiansong
f1d70eb6b2 Feature api root path (#1261) 2021-12-01 10:09:07 +08:00
Kevin Wan
d828c3f37e feat: add etcd resolver scheme, fix discov minor issue (#1281) 2021-11-28 20:08:18 +08:00
Kevin Wan
038491b7bc chore: cleanup zRPC retry code (#1280) 2021-11-27 18:39:52 +08:00
chenquan
cf683411ee feature(retry): Delete retry mechanism (#1279) 2021-11-27 11:32:33 +08:00
Kevin Wan
de5ed6a677 feat: support %w in logx.Errorf (#1278) 2021-11-26 15:57:23 +08:00
Kevin Wan
3dda557410 chore: only allow cors middleware to change headers (#1276) 2021-11-26 14:14:06 +08:00
Kevin Wan
c800f6f723 chore: avoid superfluous WriteHeader call errors (#1275) 2021-11-26 11:09:57 +08:00
Kevin Wan
0395ba1816 feat: add rest.WithCustomCors to let caller customize the response (#1274) 2021-11-25 23:03:37 +08:00
Kevin Wan
86f9f63b46 Cli (#1272)
* Fix issue 1260 (#1262)

* Fix #1238 (#1266)

* docs: update readme to use goctl@cli (#1255)

* chore: update goctl version

* style: coding style

* docs: update readme to use goctl@cli

* fix #1238

* format code

* format code

Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
Co-authored-by: anqiansong <anqiansong@bytedance.com>

Co-authored-by: anqiansong <anqiansong@gmail.com>
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-11-25 11:08:49 +08:00
Kevin Wan
a7a6753118 fixes #1257 (#1271)
* fixes #1257

* chore: format code

* test: add more tests
2021-11-25 10:26:16 +08:00
Kevin Wan
2e80d12d6a docs: update readme to use goctl@cli (#1255)
* chore: update goctl version

* style: coding style

* docs: update readme to use goctl@cli
2021-11-17 21:10:45 +08:00
Kevin Wan
417a96cbf2 chore: update goctl version (#1250)
* chore: update goctl version

* style: coding style
2021-11-16 21:57:55 +08:00
Kevin Wan
2d4c29ea7c Revert "Revert "feat: enable retry for zrpc (#1237)"" (#1246) 2021-11-16 10:29:31 +08:00
Kevin Wan
67db40ed4f Revert "feat: enable retry for zrpc (#1237)" (#1245)
This reverts commit 09eb53f308.
2021-11-15 23:30:31 +08:00
FabioCircle
11c485a5ed Duplicate temporary variable (#1244)
Co-authored-by: fabiowzhang <fabiowzhang@wesure.cn>
2021-11-15 23:14:54 +08:00
anqiansong
b0573af9a9 Update template (#1243) 2021-11-15 21:02:11 +08:00
Kevin Wan
09eb53f308 feat: enable retry for zrpc (#1237) 2021-11-14 22:33:01 +08:00
Kevin Wan
11f85d1b80 chore: remove conf.CheckedDuration (#1235) 2021-11-13 23:34:30 +08:00
anqiansong
0cb86c6990 reset link goctl (#1232) 2021-11-13 18:39:07 +08:00
Kevin Wan
57d2f22c24 feat: disable grpc retry, enable it in v1.2.4 (#1233) 2021-11-13 15:38:43 +08:00
Kevin Wan
fa0c364982 fixes #1169 (#1229) 2021-11-12 14:05:28 +08:00
Kevin Wan
a6c8113419 chore: refactor, better goctl message (#1228) 2021-11-11 22:58:33 +08:00
Kevin Wan
4f5c30e083 chore: remove unused const (#1224) 2021-11-10 21:45:42 +08:00
Kevin Wan
9d0b51fa26 fixes #1222 (#1223) 2021-11-10 21:25:51 +08:00
Kevin Wan
ba5f8045a2 Update FUNDING.yml
disable sponsor button.
2021-11-10 21:22:34 +08:00
an
3a510a9138 chore: redislock use stringx.randn replace randomStr func (#1220) 2021-11-10 21:14:21 +08:00
Kevin Wan
d3bfa16813 feat: exit with non-zero code on errors (#1218)
* feat: exit with non-zero code on errors

* chore: use const for code
2021-11-09 22:42:44 +08:00
Kevin Wan
28409791fa feat: support CORS, better implementation (#1217)
* feat: support CORS, better implementation

* chore: refine code
2021-11-09 20:35:57 +08:00
Kevin Wan
c1abe87953 Create FUNDING.yml
add sponsor button
2021-11-09 14:27:36 +08:00
Kevin Wan
f8367856e8 chore: refine code (#1215) 2021-11-08 23:12:13 +08:00
Kevin Wan
a72b0a689b docs: add go-zero users (#1214) 2021-11-08 16:13:24 +08:00
anqiansong
69a4d213a3 Fix issue 1205 (#1211)
* fix #1205

* move builder into stores

* remove xrom

* Remove unused code

* Remove unused code

* refactor builderx to builder

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-11-07 22:44:37 +08:00
Kevin Wan
c28e01fed3 feat: support CORS by using rest.WithCors(...) (#1212)
* feat: support CORS by using rest.WithCors(...)

* chore: add comments

* refactor: lowercase unexported methods

* ci: fix lint errors
2021-11-07 22:42:40 +08:00
Kevin Wan
e8efcef108 update dependencies. (#1210)
* chore: update dependencies

* chore: update dependencies

* chore: update dependencies

* chore: update dependencies

* chore: fix test failure
2021-11-07 16:38:20 +08:00
Kevin Wan
d011316997 test: add more tests (#1209) 2021-11-07 11:41:24 +08:00
Kevin Wan
4d22b0c497 feat: ignore rest.WithPrefix on empty prefix (#1208) 2021-11-06 21:31:35 +08:00
晨曦中
539215d7df goctl docker command add -version (#1206)
* feature(优化): 优化goctl

goctl docker 命令新增version参数,指定builder golang 版本

* feature(优化): 优化goctl

goctl docker 命令新增version参数,指定builder golang 版本
2021-11-06 21:28:32 +08:00
Kevin Wan
3ede597a15 feat: support customizing timeout for specific route (#1203)
* feat: support customizing timeout for specific route

* test: add more tests
2021-11-03 22:20:32 +08:00
anqiansong
01786c5e63 Generate route with prefix (#1200)
* Generate route with prefix

* Update api convert

* Remove TrimSpace

* Update path join

* Format code

* Format code

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-11-03 20:57:03 +08:00
yedf2
6aba5f74fc feat: add NewSessionFromTx to interact with other orm (#1202)
Co-authored-by: yedongfu <dongfuye@163.com>
2021-11-03 20:56:02 +08:00
Kevin Wan
3c894a3fb7 feat: simplify the grpc tls authentication (#1199) 2021-11-02 20:42:22 +08:00
Kevin Wan
1ece3a498f feat: use WithBlock() by default, NonBlock can be set in config or WithNonBlock() (#1198) 2021-11-02 19:02:02 +08:00
Kevin Wan
b76c7ae55d chore: remove semicolon for routes of services in api files (#1195) 2021-11-01 20:37:05 +08:00
Kevin Wan
91b10bd3b9 feat: add rest.WithPrefix to support route prefix (#1194) 2021-11-01 20:15:10 +08:00
Kevin Wan
7e3fe77e7b chore: update goctl version to 1.2.3, prepare for release (#1193)
* feat: slow threshold customizable in rest

* chore: update goctl version to 1.2.3, prepare for release
2021-11-01 18:26:08 +08:00
Kevin Wan
ba43214dae feat: slow threshold customizable in zrpc (#1191)
* feat: slow threshold customizable in rest

* feat: slow threshold customizable in rest

* feat: slow threshold customizable in rest

* feat: slow threshold customizable in zrpc
2021-11-01 15:04:38 +08:00
Kevin Wan
ebc90720ea feat: slow threshold customizable in rest (#1189)
* feat: slow threshold customizable in rest

* feat: slow threshold customizable in rest
2021-11-01 14:48:26 +08:00
Kevin Wan
785d100be9 feat: slow threshold customizable in sqlx (#1188) 2021-11-01 08:37:44 +08:00
Kevin Wan
f13e6f1149 feat: slow threshold customizable in redis (#1187) 2021-11-01 08:20:35 +08:00
Kevin Wan
8be0f77d96 feat: slow threshold customizable in mongo (#1186) 2021-11-01 07:12:53 +08:00
Kevin Wan
429f85a9de feat: slow threshold customizable in redis (#1185)
* feat: slow threshold customizable in redis

* chore: improve config robustness
2021-10-31 22:14:20 +08:00
Kevin Wan
b4d1c6da2c docs: update roadmap (#1184) 2021-10-31 21:00:34 +08:00
Kevin Wan
3c1cfd4c1e feat: support multiple trace agents (#1183)
* feat: support multiple trace agents

* feat: support multiple trace agents, let later calls run if error happens

* test: add more tests
2021-10-31 19:58:01 +08:00
Kevin Wan
a71a210704 feat: let different services start prometheus on demand (#1182) 2021-10-31 18:54:13 +08:00
Kevin Wan
769d06c8ab refactor: simplify tls config in rest (#1181) 2021-10-31 14:10:47 +08:00
Howie
cd1f8da13f [update] add plugin config (#1180)
Signed-off-by: lihaowei <haoweili35@gmail.com>
2021-10-31 12:56:25 +08:00
Kevin Wan
8230474667 test: add more tests (#1179) 2021-10-31 11:33:13 +08:00
Kevin Wan
27f553bf84 docs: update roadmap (#1178) 2021-10-31 11:13:45 +08:00
Kevin Wan
d48bff8c8b docs: add go-zero users (#1176) 2021-10-31 10:02:46 +08:00
Kevin Wan
59b9687f31 feat: support auth account for etcd (#1174) 2021-10-31 09:05:38 +08:00
Kevin Wan
c1a8ccda11 feat: support ssl on zrpc, simplify the config (#1175) 2021-10-30 23:15:39 +08:00
workman-Lu
9df6786b09 support RpcClient Vertify With Unilateralism and Mutual (#647)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2021-10-30 22:07:15 +08:00
anqiansong
bef5bd4e4f fix the package name of grpc client (#1170)
* fix the package name of grpc client

* Remove k8s/utils

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-10-30 21:35:05 +08:00
Kevin Wan
68acfb1891 docs: add go-zero users (#1172) 2021-10-29 21:39:28 +08:00
zeromake
9fd3f752d1 fix(goctl): repeat creation protoc-gen-goctl symlink (#1162) 2021-10-29 09:56:51 +08:00
anqiansong
9c48e9ceab Feature add template version (#1152) 2021-10-29 09:55:41 +08:00
Kevin Wan
bd26783b33 test: add more tests (#1166)
* chore: reverse the order of stopping services

* chore: reverse the order of stopping services

* test: add more tests
2021-10-28 10:04:59 +08:00
Kevin Wan
eda8230521 chore: reorg imports, format code, make MaxRetires default to 0 (#1165)
* chore: reverse the order of stopping services

* chore: reverse the order of stopping services

* chore: reorg imports, format code

* chore: format code, and refactor

* feat: change MaxRetries default to 0, disable retry
2021-10-27 20:57:18 +08:00
chenquan
462ddbb145 Add grpc retry (#1160)
* Add grpc retry

* Update grpc retry

* Add tests

* Fix a bug

* Add api && some tests

* Add comment

* Add double check

* Add server retry quota

* Update optimize code

* Fix bug

* Update optimize code

* Update optimize code

* Fix bug
2021-10-27 19:46:07 +08:00
Kevin Wan
496a2f341e test: add more tests (#1163)
* chore: reverse the order of stopping services

* chore: reverse the order of stopping services

* test: add more tests
2021-10-25 21:10:08 +08:00
Kevin Wan
7109d6d635 chore: reverse the order of stopping services (#1159)
* chore: reverse the order of stopping services

* chore: reverse the order of stopping services
2021-10-24 12:01:17 +08:00
Kevin Wan
ca72241fa3 docs: update qr code (#1158) 2021-10-23 22:12:50 +08:00
Kevin Wan
a6bdffd225 test: add more tests (#1154) 2021-10-21 21:16:18 +08:00
Kevin Wan
5636bf4955 test: add more tests (#1150) 2021-10-20 17:50:01 +08:00
anqiansong
a944a7fd7e Mark deprecated syntax (#1148) 2021-10-20 10:58:25 +08:00
Kevin Wan
a40fa405e4 test: add more tests (#1149) 2021-10-19 23:48:25 +08:00
Kevin Wan
eab77e21dd test: add more tests (#1147)
* test: add more tests

* test: add more tests
2021-10-19 22:37:56 +08:00
Kevin Wan
d41163f5c1 docs: add go-zero users (#1141) 2021-10-18 18:38:01 +08:00
Kevin Wan
265b1f2459 test: add more tests (#1138) 2021-10-15 16:27:30 +08:00
Kevin Wan
c92ea59228 test: add more tests (#1137) 2021-10-15 16:07:38 +08:00
Kevin Wan
afddfea093 docs: add go-zero users (#1135) 2021-10-14 12:50:25 +08:00
Kevin Wan
fa4dc151ca test: add more tests (#1134) 2021-10-13 22:42:54 +08:00
anqiansong
44202acb18 Fix issue #1127 (#1131)
* fix #1127

* fix #1127

* fixed unit test

* add go keyword converter

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2021-10-13 20:48:47 +08:00
Kevin Wan
cf00786209 docs: add go-zero users (#1130) 2021-10-12 22:34:13 +08:00
Kevin Wan
6a8638fc85 chore: refine rpc template in goctl (#1129) 2021-10-12 22:13:14 +08:00
Mikael
837a9ffa03 go-zero/core/hash/hash_test.go 增加测试 TestMd5Hex (#1128) 2021-10-12 21:57:07 +08:00
chenquan
d28cfe5f20 Add opts ...grpc.CallOption in grpc client (#1122)
* Add `opts ...grpc.CallOption` in grpc client

* Update format

* Update format

* Add import package

* Update format
2021-10-12 21:52:50 +08:00
chenquan
022c100dc9 Add request method in http log (#1120)
* Add request method in http log

* Update log format
2021-10-12 21:50:30 +08:00
Kevin Wan
426b09c356 update goctl version to 1.2.2 (#1125) 2021-10-12 14:00:35 +08:00
Kevin Wan
40dc21e4cf add cncf landscape (#1123) 2021-10-11 15:31:29 +08:00
Kevin Wan
9b114e3251 test: add more tests (#1119) 2021-10-08 17:31:37 +08:00
Kevin Wan
4c6234f108 add more tests (#1115)
* add more tests

* fix lint errors
2021-10-04 21:08:44 +08:00
Kevin Wan
3cdfcb05f1 add more tests (#1114) 2021-10-04 20:02:25 +08:00
Kevin Wan
9f5bfa0088 add more tests (#1113) 2021-10-04 14:14:25 +08:00
Kevin Wan
2d42c8fa00 test: add more tests (#1112) 2021-10-03 21:30:48 +08:00
Kevin Wan
10e7922597 feat: opentelemetry integration, removed self designed tracing (#1111)
* feat: opentelemetry integration, removed self designed tracing

* feat: support zipkin on opentelemetry integration

* feat: support zipkin on opentelemetry integration, enable it in conf

* style: format code

* fix: support logx without exporter configured

* fix: check return values

* refactor: simplify code

* refactor: simplify opentelemetry integration

* ci: fix staticcheck errors
2021-10-03 20:53:50 +08:00
Kevin Wan
6e34b55ba7 docs: update roadmap (#1110) 2021-10-02 18:16:59 +08:00
Kevin Wan
ed15ca04f4 fix: opentelemetry traceid not correct (#1108) 2021-10-01 22:44:37 +08:00
小小小下
295ec27e1b feat: reflection grpc service (#1107)
* feat: reflection grpc service

* feat: reflection grpc service
2021-10-01 22:02:04 +08:00
Kevin Wan
d1e702e8a3 test: add more tests (#1106)
* style: format code

* test: add more tests

* fix: staticcheck errors
2021-10-01 10:03:56 +08:00
chenquan
d1bfb5ef61 Fix the resources variable not reset after the resource manager is closed (#1105)
* Fix the resource variable not reset after the resource manager is closed

* Format code
2021-09-30 16:55:36 +08:00
Kevin Wan
e43357164c chore: replace redis.NewRedis with redis.New (#1103) 2021-09-29 23:01:10 +08:00
Kevin Wan
cd21c9fa74 chore: mark redis.NewRedis as Deprecated, use redis.New instead. (#1100)
* chore: mark redis.NewRedis as Deprecated

* chore: mark redis.NewRedis as Deprecated
2021-09-29 22:07:05 +08:00
Kevin Wan
cdd2fcbbc9 update grpc package (#1099) 2021-09-29 19:30:06 +08:00
理工男
8d2db09d45 Update Makefile (#1098) 2021-09-29 18:04:08 +08:00
Kevin Wan
65905b914d ci: add reviewdog (#1096) 2021-09-29 13:09:20 +08:00
z-micro
80e3407be1 fix bug: generating dart code error (#1090) 2021-09-28 09:01:27 +08:00
Kevin Wan
657d27213a docs: update roadmap (#1094) 2021-09-26 17:41:19 +08:00
Kevin Wan
8ac18a9422 docs: update roadmap (#1093) 2021-09-26 17:38:49 +08:00
Kevin Wan
d3ae9cfd49 ci: accurate error reporting on lint check (#1089) 2021-09-25 23:25:40 +08:00
Kevin Wan
d7f42161fd update zero-doc links in readme (#1088) 2021-09-24 11:31:00 +08:00
Kevin Wan
e03229cabe docs: change organization from tal-tech to zeromicro in readme (#1087) 2021-09-23 20:24:46 +08:00
Kevin Wan
8403ed16ae ci: add Lint check on commits (#1086)
* ci: add Lint check on commits

* ci: fix Lint script error

* test: fix go vet errors

* test: fix go vet errors, remove gofumpt to check go vet

* test: fix go vet errors, try gofumpt

* test: fix go vet errors, try gofumpt, round 1

* test: fix go vet errors, try gofumpt, round 2

* ci: fix Lint errors
2021-09-23 19:57:05 +08:00
Kevin Wan
d87d203c3b Revert "chore: run unit test with go 1.14 (#1084)" (#1085)
This reverts commit 3ae6a882a7.
2021-09-23 15:34:10 +08:00
Kevin Wan
3ae6a882a7 chore: run unit test with go 1.14 (#1084) 2021-09-23 15:30:40 +08:00
Amor
41c980f00c update goctl api (#1052)
* update goctl api

* add LoadTemplate

* update new api template

* update
2021-09-23 14:31:11 +08:00
shenbaise9527
f34d81ca2c chore: when run goctl-rpc, the order of proto message aliases should be (#1078)
fixed

Co-authored-by: steven <steven.zhou@1quant.com>
2021-09-23 14:24:21 +08:00
NevS
004ee488a6 fix AtomicError panic when Set nil (#1049) (#1050) 2021-09-23 14:23:02 +08:00
Kevin Wan
2e12cd2c99 coding style (#1083) 2021-09-23 11:19:57 +08:00
neosu
2695c30886 we can use otel.ErrorHandlerFunc instead of custom struct when we update OpenTelemetry to 1.0.0 (#1081) 2021-09-23 10:42:45 +08:00
Kevin Wan
c74fb988e0 update go.mod (#1079) 2021-09-22 21:54:54 +08:00
anqiansong
e8a340c1c0 Create a symbol link file named protoc-gen-goctl from goctl (#1076) 2021-09-21 23:13:31 +08:00
Kevin Wan
06e114e5a3 update OpenTelemetry to 1.0.0 (#1075) 2021-09-21 23:11:36 +08:00
Kevin Wan
74ad681a66 update issue templates (#1074) 2021-09-21 14:13:28 +08:00
Kevin Wan
e7bbc09093 Update issue templates 2021-09-21 13:58:21 +08:00
Kevin Wan
1eb1450c43 downgrade golang-jwt to support go 1.14 (#1073) 2021-09-21 13:42:45 +08:00
anqiansong
9a724fe907 Add MustTempDir (#1069) 2021-09-21 10:13:43 +08:00
Kevin Wan
30e49f2939 fix jwt security issue by using golang-jwt package (#1066) 2021-09-19 22:33:35 +08:00
Kevin Wan
a5407479a6 upgrade grpc version & replace github.com/golang/protobuf/protoc-gen-go with google.golang.org/protobuf (#1065) 2021-09-19 21:37:55 +08:00
anqiansong
7fb5bab26b fix #1058 (#1064) 2021-09-19 08:53:41 +08:00
Kevin Wan
27249e021f add repo moving notice (#1062) 2021-09-18 22:24:45 +08:00
Kevin Wan
d809795fec add go-zero users (#1061) 2021-09-18 22:01:15 +08:00
Kevin Wan
c9db9588b7 chore: fix comment issues (#1056) 2021-09-17 17:11:03 +08:00
Kevin Wan
872c50b71a chore: make comment accurate (#1055) 2021-09-17 11:11:42 +08:00
Kevin Wan
7c83155e4f mention cncf landscape (#1054) 2021-09-17 10:44:42 +08:00
Kevin Wan
358d86b8ae add go-zero users (#1051) 2021-09-16 15:06:42 +08:00
Kevin Wan
f4bb9f5635 fix test error on ubuntu (#1048) 2021-09-15 13:31:20 +08:00
Jerry Liang
5c6a3132eb fix typo parse.go error message (#1041) 2021-09-14 10:26:44 +08:00
Kevin Wan
2bd95aa007 update goctl version to 1.2.1 (#1042) 2021-09-14 08:32:56 +08:00
Kevin Wan
e8376936d5 remove goctl config command (#1035) 2021-09-13 14:04:06 +08:00
316 changed files with 13163 additions and 8784 deletions

12
.github/FUNDING.yml vendored Normal file
View 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
View 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.

View 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
View File

@@ -0,0 +1,10 @@
---
name: Question
about: Ask a question on using go-zero or goctl
title: ''
labels: ''
assignees: ''
---

View File

@@ -25,6 +25,12 @@ 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 ./...

19
.github/workflows/reviewdog.yml vendored Normal file
View 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

4
.gitignore vendored
View File

@@ -15,8 +15,8 @@
**/.DS_Store
**/logs
# ignore adhoc test code
**/adhoc
# for test purpose
adhoc
# gitlab ci
.cache

View File

@@ -5,17 +5,24 @@ Community and contributor involvement is vital for successfully implementing all
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
- Support TLS in redis connections
- Support service discovery through K8S watch api
- Log full sql statements for easier sql problem solving
- [x] Support service discovery through K8S client api
- [x] Log full sql statements for easier sql problem solving
## 2021 Q3
- Support `goctl mock` command to start a mocking server with given `.api` file
- Adapt builtin tracing mechanism to opentracing solutions
- Support `goctl model pg` to support PostgreSQL code generation
- [x] Support `goctl model pg` to support PostgreSQL code generation
- [x] Adapt builtin tracing mechanism to opentracing solutions
## 2021 Q4
- 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
- [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

View 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)
}

View 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)
}

View File

@@ -4,8 +4,24 @@ 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.

View File

@@ -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())
}
}

View 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
}

View 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)
}

View File

@@ -37,25 +37,35 @@ func GetRegistry() *Registry {
// GetConn returns an etcd client connection associated with given endpoints.
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
return r.getCluster(endpoints).getClient()
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 {
return r.getCluster(endpoints).monitor(key, l)
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) *cluster {
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
return
}
type cluster struct {
@@ -94,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
@@ -197,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)
}
@@ -302,14 +325,23 @@ func (c *cluster) watchConnState(cli EtcdClient) {
// 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 {

View File

@@ -33,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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -11,8 +11,8 @@ import (
)
type (
// PublisherOption defines the method to customize a Publisher.
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 {
@@ -32,7 +32,7 @@ type (
// 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 ...PublisherOption) *Publisher {
func NewPublisher(endpoints []string, key, value string, opts ...PubOption) *Publisher {
publisher := &Publisher{
endpoints: endpoints,
key: key,
@@ -146,8 +146,22 @@ func (p *Publisher) revoke(cli internal.EtcdClient) {
}
// WithId customizes a Publisher with the id.
func WithId(id int64) PublisherOption {
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))
}
}

View File

@@ -11,6 +11,7 @@ import (
"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"
"github.com/tal-tech/go-zero/core/stringx"
clientv3 "go.etcd.io/etcd/client/v3"
)
@@ -30,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)
}

View File

@@ -5,20 +5,19 @@ 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 defines the method to customize a Subscriber.
SubOption func(opts *subOptions)
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
}
)
@@ -27,14 +26,14 @@ type (
// 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
}
@@ -55,8 +54,22 @@ func (s *Subscriber) Values() []string {
// 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))
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/discov/internal"
"github.com/tal-tech/go-zero/core/stringx"
)
const (
@@ -201,11 +202,9 @@ func TestContainer(t *testing.T) {
}
func TestSubscriber(t *testing.T) {
var opt subOptions
Exclusive()(&opt)
sub := new(Subscriber)
sub.items = newContainer(opt.exclusive)
Exclusive()(sub)
sub.items = newContainer(sub.exclusive)
var count int32
sub.AddListener(func() {
atomic.AddInt32(&count, 1)
@@ -214,3 +213,15 @@ func TestSubscriber(t *testing.T) {
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)
}

View File

@@ -9,7 +9,9 @@ type AtomicError struct {
// 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.

View File

@@ -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())

View File

@@ -1,3 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package fs

View File

@@ -90,6 +90,8 @@ func Range(source <-chan interface{}) Stream {
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
}
}
@@ -103,6 +105,8 @@ func (s Stream) AllMach(predicate func(item interface{}) bool) bool {
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
}
}
@@ -186,8 +190,7 @@ func (s Stream) Distinct(fn KeyFunc) Stream {
// Done waits all upstreaming operations to be done.
func (s Stream) Done() {
for range s.source {
}
drain(s.source)
}
// Filter filters the items by the given FilterFunc.
@@ -199,9 +202,22 @@ func (s Stream) Filter(fn FilterFunc, opts ...Option) Stream {
}, 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.
@@ -246,11 +262,14 @@ func (s Stream) Head(n int64) Stream {
}
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)
// 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)
}
@@ -259,6 +278,13 @@ func (s Stream) Head(n int64) Stream {
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{}) {
@@ -280,6 +306,21 @@ func (s Stream) Merge() Stream {
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{}) {
@@ -411,15 +452,12 @@ func (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
var wg sync.WaitGroup
pool := make(chan lang.PlaceholderType, option.workers)
for {
for item := range s.source {
// important, used in another goroutine
val := item
pool <- lang.Placeholder
item, ok := <-s.source
if !ok {
<-pool
break
}
wg.Add(1)
// better to safely run caller defined method
threading.GoSafe(func() {
defer func() {
@@ -427,7 +465,7 @@ func (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
<-pool
}()
fn(item, pipe)
fn(val, pipe)
})
}
@@ -439,22 +477,19 @@ func (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
}
func (s Stream) walkUnlimited(fn WalkFunc, option *rxOptions) Stream {
pipe := make(chan interface{}, defaultWorkers)
pipe := make(chan interface{}, option.workers)
go func() {
var wg sync.WaitGroup
for {
item, ok := <-s.source
if !ok {
break
}
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(item, pipe)
fn(val, pipe)
})
}
@@ -465,14 +500,14 @@ func (s Stream) walkUnlimited(fn WalkFunc, option *rxOptions) Stream {
return Range(pipe)
}
// UnlimitedWorkers lets the caller to use as many workers as the tasks.
// 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 to customize the concurrent workers.
// WithWorkers lets the caller customize the concurrent workers.
func WithWorkers(workers int) Option {
return func(opts *rxOptions) {
if workers < minWorkers {
@@ -483,6 +518,7 @@ func WithWorkers(workers int) Option {
}
}
// buildOptions returns a rxOptions with given customizations.
func buildOptions(opts ...Option) *rxOptions {
options := newOptions()
for _, opt := range opts {
@@ -492,6 +528,13 @@ func buildOptions(opts ...Option) *rxOptions {
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,

View File

@@ -17,320 +17,489 @@ import (
)
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()
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
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))
}).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
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)
})
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},
},
}
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)
})
}
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)
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) {
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
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)
})
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
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)
})
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
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())
})
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)
}
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)
})
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)
})
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)
}
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
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)
})
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
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) {
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
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())
})
assert.Equal(t, 10, result)
}
func TestMap(t *testing.T) {
log.SetOutput(ioutil.Discard)
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
tests := []struct {
mapper MapFunc
expect int
}{
{
mapper: func(item interface{}) interface{} {
v := item.(int)
return v * v
},
expect: 30,
},
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)
{
mapper: func(item interface{}) interface{} {
v := item.(int)
if v%2 == 0 {
return 0
}
return result, nil
})
return v * v
},
expect: 10,
},
{
mapper: func(item interface{}) interface{} {
v := item.(int)
if v%2 == 0 {
panic(v)
}
return v * v
},
expect: 10,
},
}
assert.Equal(t, test.expect, result)
})
}
// 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{}))
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) {
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)
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) {
Just(1, 2, 3, 4).Reverse().Merge().ForEach(func(item interface{}) {
assert.ElementsMatch(t, []interface{}{4, 3, 2, 1}, item.([]interface{}))
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) {
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
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) {
assert.Panics(t, func() {
Just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).Split(0).Done()
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)
})
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
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)
})
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
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) {
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)
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})
})
assert.Equal(t, 9, result)
}
func BenchmarkParallelMapReduce(b *testing.B) {
@@ -377,6 +546,12 @@ func BenchmarkMapReduce(b *testing.B) {
}).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 {
@@ -387,85 +562,10 @@ func equal(t *testing.T, stream Stream, data []interface{}) {
}
}
func assetEqual(t *testing.T, except, data interface{}) {
if !reflect.DeepEqual(except, data) {
t.Errorf(" %v, want %v", data, except)
}
}
func TestStream_AnyMach(t *testing.T) {
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
return 4 == item.(int)
}))
assetEqual(t, false, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
return 0 == item.(int)
}))
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
return 2 == item.(int)
}))
assetEqual(t, true, Just(1, 2, 3).AnyMach(func(item interface{}) bool {
return 2 == item.(int)
}))
}
func TestStream_AllMach(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 TestConcat(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) {
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) {
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 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)
}

View File

@@ -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)
}
}
}

View File

@@ -20,6 +20,11 @@ func TestMd5(t *testing.T) {
assert.Equal(t, md5Digest, actual)
}
func TestMd5Hex(t *testing.T) {
actual := Md5Hex([]byte(text))
assert.Equal(t, md5Digest, actual)
}
func BenchmarkHashFnv(b *testing.B) {
for i := 0; i < b.N; i++ {
h := fnv.New32()

View File

@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/globalsign/mgo/bson"
"github.com/stretchr/testify/assert"
)
@@ -106,3 +107,20 @@ func TestMilliTime_UnmarshalJSON(t *testing.T) {
})
}
}
func TestUnmarshalWithError(t *testing.T) {
var mt MilliTime
assert.NotNil(t, mt.UnmarshalJSON([]byte("hello")))
}
func TestSetBSON(t *testing.T) {
data, err := bson.Marshal(time.Now())
assert.Nil(t, err)
var raw bson.Raw
assert.Nil(t, bson.Unmarshal(data, &raw))
var mt MilliTime
assert.Nil(t, mt.SetBSON(raw))
assert.NotNil(t, mt.SetBSON(bson.Raw{}))
}

View File

@@ -4,8 +4,8 @@ package lang
var Placeholder PlaceholderType
type (
// GenericType can be used to hold any type.
GenericType = interface{}
// AnyType can be used to hold any type.
AnyType = interface{}
// PlaceholderType represents a placeholder type.
PlaceholderType = struct{}
)

View File

@@ -24,7 +24,7 @@ func TestTokenLimit_Rescue(t *testing.T) {
rate = 5
burst = 10
)
l := NewTokenLimiter(rate, burst, redis.NewRedis(s.Addr(), redis.NodeType), "tokenlimit")
l := NewTokenLimiter(rate, burst, redis.New(s.Addr()), "tokenlimit")
s.Close()
var allowed int

View File

@@ -79,8 +79,10 @@ func (l *durationLogger) WithDuration(duration time.Duration) Logger {
}
func (l *durationLogger) write(writer io.Writer, level string, val interface{}) {
l.Timestamp = getTimestamp()
l.Level = level
l.Content = val
outputJson(writer, l)
outputJson(writer, &durationLogger{
Timestamp: getTimestamp(),
Level: level,
Content: val,
Duration: l.Duration,
})
}

View File

@@ -23,6 +23,13 @@ func TestWithDurationErrorf(t *testing.T) {
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationErrorv(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Errorv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationInfo(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
@@ -37,6 +44,13 @@ func TestWithDurationInfof(t *testing.T) {
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationInfov(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Infov("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationSlow(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
@@ -50,3 +64,10 @@ func TestWithDurationSlowf(t *testing.T) {
WithDuration(time.Second).WithDuration(time.Hour).Slowf("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationSlowv(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).WithDuration(time.Hour).Slowv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}

View File

@@ -0,0 +1,62 @@
package logx
import (
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/timex"
)
func TestLimitedExecutor_logOrDiscard(t *testing.T) {
tests := []struct {
name string
threshold time.Duration
lastTime time.Duration
discarded uint32
executed bool
}{
{
name: "nil executor",
executed: true,
},
{
name: "regular",
threshold: time.Hour,
lastTime: timex.Now(),
discarded: 10,
executed: false,
},
{
name: "slow",
threshold: time.Duration(1),
lastTime: -1000,
discarded: 10,
executed: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
executor := newLimitedExecutor(0)
executor.threshold = test.threshold
executor.discarded = test.discarded
executor.lastTime.Set(test.lastTime)
var run int32
executor.logOrDiscard(func() {
atomic.AddInt32(&run, 1)
})
if test.executed {
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
} else {
assert.Equal(t, int32(0), atomic.LoadInt32(&run))
assert.Equal(t, test.discarded+1, atomic.LoadUint32(&executor.discarded))
}
})
}
}

View File

@@ -217,7 +217,7 @@ func ErrorCaller(callDepth int, v ...interface{}) {
// ErrorCallerf writes v with context in format into error log.
func ErrorCallerf(callDepth int, format string, v ...interface{}) {
errorTextSync(fmt.Sprintf(format, v...), callDepth+callerInnerDepth)
errorTextSync(fmt.Errorf(format, v...).Error(), callDepth+callerInnerDepth)
}
// Errorf writes v with format into error log.

View File

@@ -2,6 +2,7 @@ package logx
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -242,6 +243,16 @@ func TestSetLevelWithDuration(t *testing.T) {
assert.Equal(t, 0, writer.builder.Len())
}
func TestErrorfWithWrappedError(t *testing.T) {
SetLevel(ErrorLevel)
const message = "there"
writer := new(mockWriter)
errorLog = writer
atomic.StoreUint32(&initialized, 1)
Errorf("hello %w", errors.New(message))
assert.True(t, strings.Contains(writer.builder.String(), "hello there"))
}
func TestMustNil(t *testing.T) {
Must(nil)
}

View File

@@ -47,7 +47,6 @@ type (
done chan lang.PlaceholderType
rule RotateRule
compress bool
keepDays int
// can't use threading.RoutineGroup because of cycle import
waitGroup sync.WaitGroup
closeOnce sync.Once

View File

@@ -3,6 +3,7 @@ package logx
import (
"os"
"path/filepath"
"syscall"
"testing"
"time"
@@ -97,7 +98,13 @@ func TestRotateLoggerRotate(t *testing.T) {
}()
}
err = logger.rotate()
assert.Nil(t, err)
switch v := err.(type) {
case *os.LinkError:
// avoid rename error on docker container
assert.Equal(t, syscall.EXDEV, v.Err)
default:
assert.Nil(t, err)
}
}
func TestRotateLoggerWrite(t *testing.T) {

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/tal-tech/go-zero/core/timex"
"github.com/tal-tech/go-zero/core/trace/tracespec"
"go.opentelemetry.io/otel/trace"
)
@@ -78,12 +77,16 @@ func (l *traceLogger) WithDuration(duration time.Duration) Logger {
}
func (l *traceLogger) write(writer io.Writer, level string, val interface{}) {
l.Timestamp = getTimestamp()
l.Level = level
l.Content = val
l.Trace = traceIdFromContext(l.ctx)
l.Span = spanIdFromContext(l.ctx)
outputJson(writer, l)
outputJson(writer, &traceLogger{
logEntry: logEntry{
Timestamp: getTimestamp(),
Level: level,
Duration: l.Duration,
Content: val,
},
Trace: traceIdFromContext(l.ctx),
Span: spanIdFromContext(l.ctx),
})
}
// WithContext sets ctx to log, for keeping tracing information.
@@ -94,29 +97,19 @@ func WithContext(ctx context.Context) Logger {
}
func spanIdFromContext(ctx context.Context) string {
span := trace.SpanFromContext(ctx)
if span.IsRecording() {
return span.SpanContext().SpanID().String()
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasSpanID() {
return spanCtx.SpanID().String()
}
t, ok := ctx.Value(tracespec.TracingKey).(tracespec.Trace)
if !ok {
return ""
}
return t.SpanId()
return ""
}
func traceIdFromContext(ctx context.Context) string {
span := trace.SpanFromContext(ctx)
if span.IsRecording() {
return span.SpanContext().SpanID().String()
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasTraceID() {
return spanCtx.TraceID().String()
}
t, ok := ctx.Value(tracespec.TracingKey).(tracespec.Trace)
if !ok {
return ""
}
return t.TraceId()
return ""
}

View File

@@ -9,71 +9,102 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace/tracespec"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
const (
mockTraceID = "mock-trace-id"
mockSpanID = "mock-span-id"
traceKey = "trace"
spanKey = "span"
)
var mock tracespec.Trace = new(mockTrace)
func TestTraceLog(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
WithContext(ctx).(*traceLogger).write(&buf, levelInfo, testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceError(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
errorLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Error(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Errorf(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Errorv(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceInfo(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infov(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceSlow(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
slowLog = newLogWriter(log.New(&buf, "", flags))
ctx := context.WithValue(context.Background(), tracespec.TracingKey, mock)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Slow(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Slowf(testlog)
assert.True(t, strings.Contains(buf.String(), mockTraceID))
assert.True(t, strings.Contains(buf.String(), mockSpanID))
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Slowv(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceWithoutContext(t *testing.T) {
@@ -83,34 +114,10 @@ func TestTraceWithoutContext(t *testing.T) {
l := WithContext(context.Background()).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.False(t, strings.Contains(buf.String(), mockTraceID))
assert.False(t, strings.Contains(buf.String(), mockSpanID))
assert.False(t, strings.Contains(buf.String(), traceKey))
assert.False(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.False(t, strings.Contains(buf.String(), mockTraceID))
assert.False(t, strings.Contains(buf.String(), mockSpanID))
}
type mockTrace struct{}
func (t mockTrace) TraceId() string {
return mockTraceID
}
func (t mockTrace) SpanId() string {
return mockSpanID
}
func (t mockTrace) Finish() {
}
func (t mockTrace) Fork(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) {
return nil, nil
}
func (t mockTrace) Follow(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) {
return nil, nil
}
func (t mockTrace) Visit(fn func(key, val string) bool) {
assert.False(t, strings.Contains(buf.String(), traceKey))
assert.False(t, strings.Contains(buf.String(), spanKey))
}

View File

@@ -15,6 +15,11 @@ func UnmarshalJsonBytes(content []byte, v interface{}) error {
return unmarshalJsonBytes(content, v, jsonUnmarshaler)
}
// UnmarshalJsonMap unmarshals content from m into v.
func UnmarshalJsonMap(m map[string]interface{}, v interface{}) error {
return jsonUnmarshaler.Unmarshal(m, v)
}
// UnmarshalJsonReader unmarshals content from reader into v.
func UnmarshalJsonReader(reader io.Reader, v interface{}) error {
return unmarshalJsonReader(reader, v, jsonUnmarshaler)

View File

@@ -871,3 +871,50 @@ func TestUnmarshalReaderError(t *testing.T) {
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), payload))
}
func TestUnmarshalMap(t *testing.T) {
t.Run("nil map and valid", func(t *testing.T) {
var m map[string]interface{}
var v struct {
Any string `json:",optional"`
}
err := UnmarshalJsonMap(m, &v)
assert.Nil(t, err)
assert.True(t, len(v.Any) == 0)
})
t.Run("empty map but not valid", func(t *testing.T) {
m := map[string]interface{}{}
var v struct {
Any string
}
err := UnmarshalJsonMap(m, &v)
assert.NotNil(t, err)
})
t.Run("empty map and valid", func(t *testing.T) {
m := map[string]interface{}{}
var v struct {
Any string `json:",optional"`
}
err := UnmarshalJsonMap(m, &v)
assert.Nil(t, err)
assert.True(t, len(v.Any) == 0)
})
t.Run("valid map", func(t *testing.T) {
m := map[string]interface{}{
"Any": "foo",
}
var v struct {
Any string
}
err := UnmarshalJsonMap(m, &v)
assert.Nil(t, err)
assert.Equal(t, "foo", v.Any)
})
}

View File

@@ -7,7 +7,6 @@ import (
"reflect"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tal-tech/go-zero/core/jsonx"
@@ -25,15 +24,17 @@ var (
errValueNotSettable = errors.New("value is not settable")
errValueNotStruct = errors.New("value type is not struct")
keyUnmarshaler = NewUnmarshaler(defaultKeyName)
cacheKeys atomic.Value
cacheKeysLock sync.Mutex
durationType = reflect.TypeOf(time.Duration(0))
cacheKeys map[string][]string
cacheKeysLock sync.Mutex
defaultCache map[string]interface{}
defaultCacheLock sync.Mutex
emptyMap = map[string]interface{}{}
emptyValue = reflect.ValueOf(lang.Placeholder)
)
type (
// A Unmarshaler is used to unmarshal with given tag key.
// Unmarshaler is used to unmarshal with given tag key.
Unmarshaler struct {
key string
opts unmarshalOptions
@@ -46,12 +47,11 @@ type (
fromString bool
canonicalKey func(key string) string
}
keyCache map[string][]string
)
func init() {
cacheKeys.Store(make(keyCache))
cacheKeys = make(map[string][]string)
defaultCache = make(map[string]interface{})
}
// NewUnmarshaler returns a Unmarshaler.
@@ -207,6 +207,8 @@ func (u *Unmarshaler) processFieldNotFromString(field reflect.StructField, value
switch {
case valueKind == reflect.Map && typeKind == reflect.Struct:
return u.processFieldStruct(field, value, mapValue, fullName)
case valueKind == reflect.Map && typeKind == reflect.Map:
return u.fillMap(field, value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Slice:
return u.fillSliceFromString(fieldType, value, mapValue)
case valueKind == reflect.String && derefedFieldType == durationType:
@@ -386,7 +388,13 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(field reflect.StructField, v
if derefedType == durationType {
return fillDurationValue(fieldKind, value, defaultValue)
}
return setValue(fieldKind, value, defaultValue)
switch fieldKind {
case reflect.Array, reflect.Slice:
return u.fillSliceWithDefault(derefedType, value, defaultValue)
default:
return setValue(fieldKind, value, defaultValue)
}
}
switch fieldKind {
@@ -500,7 +508,8 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
return nil
}
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int, baseKind reflect.Kind, value interface{}) error {
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
baseKind reflect.Kind, value interface{}) error {
ithVal := slice.Index(index)
switch v := value.(type) {
case json.Number:
@@ -529,6 +538,28 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int, baseKind re
}
}
func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value,
defaultValue string) error {
baseFieldType := Deref(derefedType.Elem())
baseFieldKind := baseFieldType.Kind()
defaultCacheLock.Lock()
slice, ok := defaultCache[defaultValue]
defaultCacheLock.Unlock()
if !ok {
if baseFieldKind == reflect.String {
slice = parseGroupedSegments(defaultValue)
} else if err := jsonx.UnmarshalFromString(defaultValue, &slice); err != nil {
return err
}
defaultCacheLock.Lock()
defaultCache[defaultValue] = slice
defaultCacheLock.Unlock()
}
return u.fillSlice(derefedType, value, slice)
}
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue interface{}) (reflect.Value, error) {
mapType := reflect.MapOf(keyType, elemType)
valueType := reflect.TypeOf(mapValue)
@@ -584,6 +615,8 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue inter
targetValue.SetMapIndex(key, innerValue)
default:
switch v := keythData.(type) {
case bool:
targetValue.SetMapIndex(key, reflect.ValueOf(v))
case string:
targetValue.SetMapIndex(key, reflect.ValueOf(v))
case json.Number:
@@ -720,20 +753,6 @@ func getValueWithChainedKeys(m Valuer, keys []string) (interface{}, bool) {
return nil, false
}
func insertKeys(key string, cache []string) {
cacheKeysLock.Lock()
defer cacheKeysLock.Unlock()
keys := cacheKeys.Load().(keyCache)
// copy the contents into the new map, to guarantee the old map is immutable
newKeys := make(keyCache)
for k, v := range keys {
newKeys[k] = v
}
newKeys[key] = cache
cacheKeys.Store(newKeys)
}
func join(elem ...string) string {
var builder strings.Builder
@@ -764,15 +783,19 @@ func newTypeMismatchError(name string) error {
}
func readKeys(key string) []string {
cache := cacheKeys.Load().(keyCache)
if keys, ok := cache[key]; ok {
cacheKeysLock.Lock()
keys, ok := cacheKeys[key]
cacheKeysLock.Unlock()
if ok {
return keys
}
keys := strings.FieldsFunc(key, func(c rune) bool {
keys = strings.FieldsFunc(key, func(c rune) bool {
return c == delimiter
})
insertKeys(key, keys)
cacheKeysLock.Lock()
cacheKeys[key] = keys
cacheKeysLock.Unlock()
return keys
}

View File

@@ -198,6 +198,66 @@ func TestUnmarshalIntWithDefault(t *testing.T) {
assert.Equal(t, 1, in.Int)
}
func TestUnmarshalBoolSliceWithDefault(t *testing.T) {
type inner struct {
Bools []bool `key:"bools,default=[true,false]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []bool{true, false}, in.Bools)
}
func TestUnmarshalIntSliceWithDefault(t *testing.T) {
type inner struct {
Ints []int `key:"ints,default=[1,2,3]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []int{1, 2, 3}, in.Ints)
}
func TestUnmarshalIntSliceWithDefaultHasSpaces(t *testing.T) {
type inner struct {
Ints []int `key:"ints,default=[1, 2, 3]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []int{1, 2, 3}, in.Ints)
}
func TestUnmarshalFloatSliceWithDefault(t *testing.T) {
type inner struct {
Floats []float32 `key:"floats,default=[1.1,2.2,3.3]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []float32{1.1, 2.2, 3.3}, in.Floats)
}
func TestUnmarshalStringSliceWithDefault(t *testing.T) {
type inner struct {
Strs []string `key:"strs,default=[foo,bar,woo]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []string{"foo", "bar", "woo"}, in.Strs)
}
func TestUnmarshalStringSliceWithDefaultHasSpaces(t *testing.T) {
type inner struct {
Strs []string `key:"strs,default=[foo, bar, woo]"`
}
var in inner
assert.Nil(t, UnmarshalKey(nil, &in))
assert.ElementsMatch(t, []string{"foo", "bar", "woo"}, in.Strs)
}
func TestUnmarshalUint(t *testing.T) {
type inner struct {
Uint uint `key:"uint"`
@@ -861,10 +921,12 @@ func TestUnmarshalSliceOfStruct(t *testing.T) {
func TestUnmarshalWithStringOptionsCorrect(t *testing.T) {
type inner struct {
Value string `key:"value,options=first|second"`
Foo string `key:"foo,options=[bar,baz]"`
Correct string `key:"correct,options=1|2"`
}
m := map[string]interface{}{
"value": "first",
"foo": "bar",
"correct": "2",
}
@@ -872,6 +934,7 @@ func TestUnmarshalWithStringOptionsCorrect(t *testing.T) {
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &in))
ast.Equal("first", in.Value)
ast.Equal("bar", in.Foo)
ast.Equal("2", in.Correct)
}
@@ -943,6 +1006,22 @@ func TestUnmarshalStringOptionsWithStringOptionsIncorrect(t *testing.T) {
ast.NotNil(unmarshaler.Unmarshal(m, &in))
}
func TestUnmarshalStringOptionsWithStringOptionsIncorrectGrouped(t *testing.T) {
type inner struct {
Value string `key:"value,options=[first,second]"`
Correct string `key:"correct,options=1|2"`
}
m := map[string]interface{}{
"value": "third",
"correct": "2",
}
var in inner
unmarshaler := NewUnmarshaler(defaultKeyName, WithStringValues())
ast := assert.New(t)
ast.NotNil(unmarshaler.Unmarshal(m, &in))
}
func TestUnmarshalWithStringOptionsIncorrect(t *testing.T) {
type inner struct {
Value string `key:"value,options=first|second"`
@@ -2518,3 +2597,29 @@ func TestUnmarshalJsonReaderPtrArray(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 3, len(res.B))
}
func TestUnmarshalJsonWithoutKey(t *testing.T) {
payload := `{"A": "1", "B": "2"}`
var res struct {
A string `json:""`
B string `json:","`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, "1", res.A)
assert.Equal(t, "2", res.B)
}
func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ {
var a struct {
Ints []int `json:"ints,default=[1,2,3]"`
Strs []string `json:"strs,default=[foo,bar,baz]"`
}
_ = UnmarshalJsonMap(nil, &a)
if len(a.Strs) != 3 || len(a.Ints) != 3 {
b.Fatal("failed")
}
}
}

View File

@@ -14,13 +14,19 @@ import (
)
const (
defaultOption = "default"
stringOption = "string"
optionalOption = "optional"
optionsOption = "options"
rangeOption = "range"
optionSeparator = "|"
equalToken = "="
defaultOption = "default"
stringOption = "string"
optionalOption = "optional"
optionsOption = "options"
rangeOption = "range"
optionSeparator = "|"
equalToken = "="
escapeChar = '\\'
leftBracket = '('
rightBracket = ')'
leftSquareBracket = '['
rightSquareBracket = ']'
segmentSeparator = ','
)
var (
@@ -118,7 +124,7 @@ func convertType(kind reflect.Kind, str string) (interface{}, error) {
}
func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fieldOptions, error) {
segments := strings.Split(value, ",")
segments := parseSegments(value)
key := strings.TrimSpace(segments[0])
options := segments[1:]
@@ -198,6 +204,16 @@ func maybeNewValue(field reflect.StructField, value reflect.Value) {
}
}
func parseGroupedSegments(val string) []string {
val = strings.TrimLeftFunc(val, func(r rune) bool {
return r == leftBracket || r == leftSquareBracket
})
val = strings.TrimRightFunc(val, func(r rune) bool {
return r == rightBracket || r == rightSquareBracket
})
return parseSegments(val)
}
// don't modify returned fieldOptions, it's cached and shared among different calls.
func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fieldOptions, error) {
value := field.Tag.Get(tagName)
@@ -309,7 +325,7 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
return fmt.Errorf("field %s has wrong options", fieldName)
}
fieldOpts.Options = strings.Split(segs[1], optionSeparator)
fieldOpts.Options = parseOptions(segs[1])
case strings.HasPrefix(option, defaultOption):
segs := strings.Split(option, equalToken)
if len(segs) != 2 {
@@ -334,6 +350,69 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
return nil
}
// parseOptions parses the given options in tag.
// for example: `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"`
func parseOptions(val string) []string {
if len(val) == 0 {
return nil
}
if val[0] == leftSquareBracket {
return parseGroupedSegments(val)
}
return strings.Split(val, optionSeparator)
}
func parseSegments(val string) []string {
var segments []string
var escaped, grouped bool
var buf strings.Builder
for _, ch := range val {
if escaped {
buf.WriteRune(ch)
escaped = false
continue
}
switch ch {
case segmentSeparator:
if grouped {
buf.WriteRune(ch)
} else {
// need to trim spaces, but we cannot ignore empty string,
// because the first segment stands for the key might be empty.
// if ignored, the later tag will be used as the key.
segments = append(segments, strings.TrimSpace(buf.String()))
buf.Reset()
}
case escapeChar:
if grouped {
buf.WriteRune(ch)
} else {
escaped = true
}
case leftBracket, leftSquareBracket:
buf.WriteRune(ch)
grouped = true
case rightBracket, rightSquareBracket:
buf.WriteRune(ch)
grouped = false
default:
buf.WriteRune(ch)
}
}
last := strings.TrimSpace(buf.String())
// ignore last empty string
if len(last) > 0 {
segments = append(segments, last)
}
return segments
}
func reprOfValue(val reflect.Value) string {
switch vt := val.Interface().(type) {
case bool:

View File

@@ -90,6 +90,82 @@ func TestParseKeyAndOptionWithTagAndOption(t *testing.T) {
assert.True(t, options.FromString)
}
func TestParseSegments(t *testing.T) {
tests := []struct {
input string
expect []string
}{
{
input: "",
expect: []string{},
},
{
input: ",",
expect: []string{""},
},
{
input: "foo,",
expect: []string{"foo"},
},
{
input: ",foo",
// the first empty string cannot be ignored, it's the key.
expect: []string{"", "foo"},
},
{
input: "foo",
expect: []string{"foo"},
},
{
input: "foo,bar",
expect: []string{"foo", "bar"},
},
{
input: "foo,bar,baz",
expect: []string{"foo", "bar", "baz"},
},
{
input: "foo,options=a|b",
expect: []string{"foo", "options=a|b"},
},
{
input: "foo,bar,default=[baz,qux]",
expect: []string{"foo", "bar", "default=[baz,qux]"},
},
{
input: "foo,bar,options=[baz,qux]",
expect: []string{"foo", "bar", "options=[baz,qux]"},
},
{
input: `foo\,bar,options=[baz,qux]`,
expect: []string{`foo,bar`, "options=[baz,qux]"},
},
{
input: `foo,bar,options=\[baz,qux]`,
expect: []string{"foo", "bar", "options=[baz", "qux]"},
},
{
input: `foo,bar,options=[baz\,qux]`,
expect: []string{"foo", "bar", `options=[baz\,qux]`},
},
{
input: `foo\,bar,options=[baz,qux],default=baz`,
expect: []string{`foo,bar`, "options=[baz,qux]", "default=baz"},
},
{
input: `foo\,bar,options=[baz,qux, quux],default=[qux, baz]`,
expect: []string{`foo,bar`, "options=[baz,qux, quux]", "default=[qux, baz]"},
},
}
for _, test := range tests {
test := test
t.Run(test.input, func(t *testing.T) {
assert.ElementsMatch(t, test.expect, parseSegments(test.input))
})
}
}
func TestValidatePtrWithNonPtr(t *testing.T) {
var foo string
rve := reflect.ValueOf(foo)
@@ -209,6 +285,12 @@ func TestRepr(t *testing.T) {
newMockPtr(),
"mockptr",
},
{
&mockOpacity{
val: 1,
},
"{1}",
},
{
true,
"true",

View File

@@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/utils/io"
)
func TestUnmarshalYamlBytes(t *testing.T) {
@@ -18,6 +19,22 @@ func TestUnmarshalYamlBytes(t *testing.T) {
assert.Equal(t, "liao", c.Name)
}
func TestUnmarshalYamlBytesErrorInput(t *testing.T) {
var c struct {
Name string
}
content := []byte(`liao`)
assert.NotNil(t, UnmarshalYamlBytes(content, &c))
}
func TestUnmarshalYamlBytesEmptyInput(t *testing.T) {
var c struct {
Name string
}
content := []byte(``)
assert.NotNil(t, UnmarshalYamlBytes(content, &c))
}
func TestUnmarshalYamlBytesOptional(t *testing.T) {
var c struct {
Name string
@@ -918,3 +935,82 @@ func TestUnmarshalYamlReaderError(t *testing.T) {
err := UnmarshalYamlReader(reader, &v)
assert.NotNil(t, err)
}
func TestUnmarshalYamlBadReader(t *testing.T) {
var v struct {
Any string
}
err := UnmarshalYamlReader(new(badReader), &v)
assert.NotNil(t, err)
}
func TestUnmarshalYamlMapBool(t *testing.T) {
text := `machine:
node1: true
node2: true
node3: true
`
var v struct {
Machine map[string]bool `json:"machine,optional"`
}
reader := strings.NewReader(text)
assert.Nil(t, UnmarshalYamlReader(reader, &v))
assert.True(t, v.Machine["node1"])
assert.True(t, v.Machine["node2"])
assert.True(t, v.Machine["node3"])
}
func TestUnmarshalYamlMapInt(t *testing.T) {
text := `machine:
node1: 1
node2: 2
node3: 3
`
var v struct {
Machine map[string]int `json:"machine,optional"`
}
reader := strings.NewReader(text)
assert.Nil(t, UnmarshalYamlReader(reader, &v))
assert.Equal(t, 1, v.Machine["node1"])
assert.Equal(t, 2, v.Machine["node2"])
assert.Equal(t, 3, v.Machine["node3"])
}
func TestUnmarshalYamlMapByte(t *testing.T) {
text := `machine:
node1: 1
node2: 2
node3: 3
`
var v struct {
Machine map[string]byte `json:"machine,optional"`
}
reader := strings.NewReader(text)
assert.Nil(t, UnmarshalYamlReader(reader, &v))
assert.Equal(t, byte(1), v.Machine["node1"])
assert.Equal(t, byte(2), v.Machine["node2"])
assert.Equal(t, byte(3), v.Machine["node3"])
}
func TestUnmarshalYamlMapRune(t *testing.T) {
text := `machine:
node1: 1
node2: 2
node3: 3
`
var v struct {
Machine map[string]rune `json:"machine,optional"`
}
reader := strings.NewReader(text)
assert.Nil(t, UnmarshalYamlReader(reader, &v))
assert.Equal(t, rune(1), v.Machine["node1"])
assert.Equal(t, rune(2), v.Machine["node2"])
assert.Equal(t, rune(3), v.Machine["node3"])
}
type badReader struct{}
func (b *badReader) Read(p []byte) (n int, err error) {
return 0, io.ErrLimitReached
}

View File

@@ -1,13 +1,13 @@
package mr
import (
"context"
"errors"
"fmt"
"sync"
"github.com/tal-tech/go-zero/core/errorx"
"github.com/tal-tech/go-zero/core/lang"
"github.com/tal-tech/go-zero/core/syncx"
"github.com/tal-tech/go-zero/core/threading"
)
@@ -43,6 +43,7 @@ type (
Option func(opts *mapReduceOptions)
mapReduceOptions struct {
ctx context.Context
workers int
}
@@ -93,16 +94,17 @@ func Map(generate GenerateFunc, mapper MapFunc, opts ...Option) chan interface{}
options := buildOptions(opts...)
source := buildSource(generate)
collector := make(chan interface{}, options.workers)
done := syncx.NewDoneChan()
done := make(chan lang.PlaceholderType)
go executeMappers(mapper, source, collector, done.Done(), options.workers)
go executeMappers(options.ctx, mapper, source, collector, done, options.workers)
return collector
}
// MapReduce maps all elements generated from given generate func,
// and reduces the output elements with given reducer.
func MapReduce(generate GenerateFunc, mapper MapperFunc, reducer ReducerFunc, opts ...Option) (interface{}, error) {
func MapReduce(generate GenerateFunc, mapper MapperFunc, reducer ReducerFunc,
opts ...Option) (interface{}, error) {
source := buildSource(generate)
return MapReduceWithSource(source, mapper, reducer, opts...)
}
@@ -119,13 +121,13 @@ func MapReduceWithSource(source <-chan interface{}, mapper MapperFunc, reducer R
}()
collector := make(chan interface{}, options.workers)
done := syncx.NewDoneChan()
writer := newGuardedWriter(output, done.Done())
done := make(chan lang.PlaceholderType)
writer := newGuardedWriter(options.ctx, output, done)
var closeOnce sync.Once
var retErr errorx.AtomicError
finish := func() {
closeOnce.Do(func() {
done.Close()
close(done)
close(output)
})
}
@@ -154,9 +156,9 @@ func MapReduceWithSource(source <-chan interface{}, mapper MapperFunc, reducer R
reducer(collector, writer, cancel)
}()
go executeMappers(func(item interface{}, w Writer) {
go executeMappers(options.ctx, func(item interface{}, w Writer) {
mapper(item, w, cancel)
}, source, collector, done.Done(), options.workers)
}, source, collector, done, options.workers)
value, ok := <-output
if err := retErr.Load(); err != nil {
@@ -187,6 +189,13 @@ func MapVoid(generate GenerateFunc, mapper VoidMapFunc, opts ...Option) {
}, opts...))
}
// WithContext customizes a mapreduce processing accepts a given ctx.
func WithContext(ctx context.Context) Option {
return func(opts *mapReduceOptions) {
opts.ctx = ctx
}
}
// WithWorkers customizes a mapreduce processing with given workers.
func WithWorkers(workers int) Option {
return func(opts *mapReduceOptions) {
@@ -224,8 +233,8 @@ func drain(channel <-chan interface{}) {
}
}
func executeMappers(mapper MapFunc, input <-chan interface{}, collector chan<- interface{},
done <-chan lang.PlaceholderType, workers int) {
func executeMappers(ctx context.Context, mapper MapFunc, input <-chan interface{},
collector chan<- interface{}, done <-chan lang.PlaceholderType, workers int) {
var wg sync.WaitGroup
defer func() {
wg.Wait()
@@ -233,9 +242,11 @@ func executeMappers(mapper MapFunc, input <-chan interface{}, collector chan<- i
}()
pool := make(chan lang.PlaceholderType, workers)
writer := newGuardedWriter(collector, done)
writer := newGuardedWriter(ctx, collector, done)
for {
select {
case <-ctx.Done():
return
case <-done:
return
case pool <- lang.Placeholder:
@@ -261,6 +272,7 @@ func executeMappers(mapper MapFunc, input <-chan interface{}, collector chan<- i
func newOptions() *mapReduceOptions {
return &mapReduceOptions{
ctx: context.Background(),
workers: defaultWorkers,
}
}
@@ -275,12 +287,15 @@ func once(fn func(error)) func(error) {
}
type guardedWriter struct {
ctx context.Context
channel chan<- interface{}
done <-chan lang.PlaceholderType
}
func newGuardedWriter(channel chan<- interface{}, done <-chan lang.PlaceholderType) guardedWriter {
func newGuardedWriter(ctx context.Context, channel chan<- interface{},
done <-chan lang.PlaceholderType) guardedWriter {
return guardedWriter{
ctx: ctx,
channel: channel,
done: done,
}
@@ -288,6 +303,8 @@ func newGuardedWriter(channel chan<- interface{}, done <-chan lang.PlaceholderTy
func (gw guardedWriter) Write(v interface{}) {
select {
case <-gw.ctx.Done():
return
case <-gw.done:
return
default:

View File

@@ -1,6 +1,7 @@
package mr
import (
"context"
"errors"
"io/ioutil"
"log"
@@ -410,6 +411,50 @@ func TestMapReduceWithoutReducerWrite(t *testing.T) {
assert.Nil(t, res)
}
func TestMapReduceVoidPanicInReducer(t *testing.T) {
const message = "foo"
var done syncx.AtomicBool
err := MapReduceVoid(func(source chan<- interface{}) {
for i := 0; i < defaultWorkers*2; i++ {
source <- i
}
done.Set(true)
}, func(item interface{}, writer Writer, cancel func(error)) {
i := item.(int)
writer.Write(i)
}, func(pipe <-chan interface{}, cancel func(error)) {
panic(message)
}, WithWorkers(1))
assert.NotNil(t, err)
assert.Equal(t, message, err.Error())
assert.True(t, done.True())
}
func TestMapReduceWithContext(t *testing.T) {
var done syncx.AtomicBool
var result []int
ctx, cancel := context.WithCancel(context.Background())
err := MapReduceVoid(func(source chan<- interface{}) {
for i := 0; i < defaultWorkers*2; i++ {
source <- i
}
done.Set(true)
}, func(item interface{}, writer Writer, c func(error)) {
i := item.(int)
if i == defaultWorkers/2 {
cancel()
}
writer.Write(i)
}, func(pipe <-chan interface{}, cancel func(error)) {
for item := range pipe {
i := item.(int)
result = append(result, i)
}
}, WithContext(ctx))
assert.NotNil(t, err)
assert.Equal(t, ErrReduceNoOutput, err)
}
func BenchmarkMapReduce(b *testing.B) {
b.ReportAllocs()

89
core/mr/readme-cn.md Normal file
View File

@@ -0,0 +1,89 @@
# mapreduce
[English](readme.md) | 简体中文
## 为什么需要 MapReduce
在实际的业务场景中我们常常需要从不同的 rpc 服务中获取相应属性来组装成复杂对象。
比如要查询商品详情:
1. 商品服务-查询商品属性
2. 库存服务-查询库存属性
3. 价格服务-查询价格属性
4. 营销服务-查询营销属性
如果是串行调用的话响应时间会随着 rpc 调用次数呈线性增长,所以我们要优化性能一般会将串行改并行。
简单的场景下使用 `WaitGroup` 也能够满足需求,但是如果我们需要对 rpc 调用返回的数据进行校验、数据加工转换、数据汇总呢?继续使用 `WaitGroup` 就有点力不从心了go 的官方库中并没有这种工具java 中提供了 CompleteFuture我们依据 MapReduce 架构思想实现了进程内的数据批处理 MapReduce 并发工具类。
## 设计思路
我们尝试把自己代入到作者的角色梳理一下并发工具可能的业务场景:
1. 查询商品详情:支持并发调用多个服务来组合产品属性,支持调用错误可以立即结束。
2. 商品详情页自动推荐用户卡券:支持并发校验卡券,校验失败自动剔除,返回全部卡券。
以上实际都是在进行对输入数据进行处理最后输出清洗后的数据,针对数据处理有个非常经典的异步模式:生产者消费者模式。于是我们可以抽象一下数据批处理的生命周期,大致可以分为三个阶段:
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-serial-cn.png" width="500">
1. 数据生产 generate
2. 数据加工 mapper
3. 数据聚合 reducer
其中数据生产是不可或缺的阶段,数据加工、数据聚合是可选阶段,数据生产与加工支持并发调用,数据聚合基本属于纯内存操作单协程即可。
再来思考一下不同阶段之间数据应该如何流转,既然不同阶段的数据处理都是由不同 goroutine 执行的,那么很自然的可以考虑采用 channel 来实现 goroutine 之间的通信。
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-cn.png" width="500">
如何实现随时终止流程呢?
`goroutine` 中监听一个全局的结束 `channel` 和调用方提供的 `ctx` 就行。
## 简单示例
并行求平方和(不要嫌弃示例简单,只是模拟并发)
```go
package main
import (
"fmt"
"log"
"github.com/tal-tech/go-zero/core/mr"
)
func main() {
val, err := mr.MapReduce(func(source chan<- interface{}) {
// generator
for i := 0; i < 10; i++ {
source <- i
}
}, func(item interface{}, writer mr.Writer, cancel func(error)) {
// mapper
i := item.(int)
writer.Write(i * i)
}, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {
// reducer
var sum int
for i := range pipe {
sum += i.(int)
}
writer.Write(sum)
})
if err != nil {
log.Fatal(err)
}
fmt.Println("result:", val)
}
```
更多示例:[https://github.com/zeromicro/zero-examples/tree/main/mapreduce](https://github.com/zeromicro/zero-examples/tree/main/mapreduce)
## 欢迎 star
如果你正在使用或者觉得这个项目对你有帮助,请 **star** 支持,感谢!

90
core/mr/readme.md Normal file
View File

@@ -0,0 +1,90 @@
<img align="right" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
# mapreduce
English | [简体中文](readme-cn.md)
## Why MapReduce is needed
In practical business scenarios we often need to get the corresponding properties from different rpc services to assemble complex objects.
For example, to query product details.
1. product service - query product attributes
2. inventory service - query inventory properties
3. price service - query price attributes
4. marketing service - query marketing properties
If it is a serial call, the response time will increase linearly with the number of rpc calls, so we will generally change serial to parallel to optimize response time.
Simple scenarios using `WaitGroup` can also meet the needs, but what if we need to check the data returned by the rpc call, data processing, data aggregation? The official go library does not have such a tool (CompleteFuture is provided in java), so we implemented an in-process data batching MapReduce concurrent tool based on the MapReduce architecture.
## Design ideas
Let's try to put ourselves in the author's shoes and sort out the possible business scenarios for the concurrency tool:
1. querying product details: supporting concurrent calls to multiple services to combine product attributes, and supporting call errors that can be ended immediately.
2. automatic recommendation of user card coupons on product details page: support concurrently verifying card coupons, automatically rejecting them if they fail, and returning all of them.
The above is actually processing the input data and finally outputting the cleaned data. There is a very classic asynchronous pattern for data processing: the producer-consumer pattern. So we can abstract the life cycle of data batch processing, which can be roughly divided into three phases.
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-serial-en.png" width="500">
1. data production generate
2. data processing mapper
3. data aggregation reducer
Data producing is an indispensable stage, data processing and data aggregation are optional stages, data producing and processing support concurrent calls, data aggregation is basically a pure memory operation, so a single concurrent process can do it.
Since different stages of data processing are performed by different goroutines, it is natural to consider the use of channel to achieve communication between goroutines.
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-en.png" width="500">
How can I terminate the process at any time?
It's simple, just receive from a channel or the given context in the goroutine.
## A simple example
Calculate the sum of squares, simulating the concurrency.
```go
package main
import (
"fmt"
"log"
"github.com/tal-tech/go-zero/core/mr"
)
func main() {
val, err := mr.MapReduce(func(source chan<- interface{}) {
// generator
for i := 0; i < 10; i++ {
source <- i
}
}, func(item interface{}, writer mr.Writer, cancel func(error)) {
// mapper
i := item.(int)
writer.Write(i * i)
}, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {
// reducer
var sum int
for i := range pipe {
sum += i.(int)
}
writer.Write(sum)
})
if err != nil {
log.Fatal(err)
}
fmt.Println("result:", val)
}
```
More examples: [https://github.com/zeromicro/zero-examples/tree/main/mapreduce](https://github.com/zeromicro/zero-examples/tree/main/mapreduce)
## Give a Star! ⭐
If you like or are using this project to learn or start your solution, please give it a star. Thanks!

View File

@@ -1,3 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc
@@ -10,6 +11,7 @@ import (
"time"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/threading"
)
const (
@@ -45,10 +47,10 @@ func gracefulStop(signals chan os.Signal) {
signal.Stop(signals)
logx.Info("Got signal SIGTERM, shutting down...")
wrapUpListeners.notifyListeners()
go wrapUpListeners.notifyListeners()
time.Sleep(wrapUpTime)
shutdownListeners.notifyListeners()
go shutdownListeners.notifyListeners()
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime)
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit)
@@ -80,7 +82,9 @@ func (lm *listenerManager) notifyListeners() {
lm.lock.Lock()
defer lm.lock.Unlock()
group := threading.NewRoutineGroup()
for _, listener := range lm.listeners {
listener()
group.RunSafe(listener)
}
group.Wait()
}

View File

@@ -23,11 +23,11 @@ func Enabled() bool {
// StartAgent starts a prometheus agent.
func StartAgent(c Config) {
once.Do(func() {
if len(c.Host) == 0 {
return
}
if len(c.Host) == 0 {
return
}
once.Do(func() {
enabled.Set(true)
threading.GoSafe(func() {
http.Handle(c.Path, promhttp.Handler())

View File

@@ -1,3 +1,4 @@
//go:build debug
// +build debug
package search

View File

@@ -1,9 +1,12 @@
package search
import (
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stringx"
)
type mockedRoute struct {
@@ -139,11 +142,9 @@ func TestStrictSearchSibling(t *testing.T) {
tree.Add(r.route, r.value)
}
for i := 0; i < 1000; i++ {
result, ok := tree.Search(query)
assert.True(t, ok)
assert.Equal(t, 3, result.Item.(int))
}
result, ok := tree.Search(query)
assert.True(t, ok)
assert.Equal(t, 3, result.Item.(int))
}
func TestAddDuplicate(t *testing.T) {
@@ -185,3 +186,41 @@ func TestSearchInvalidItem(t *testing.T) {
err := tree.Add("/", nil)
assert.Equal(t, errEmptyItem, err)
}
func BenchmarkSearchTree(b *testing.B) {
const (
avgLen = 1000
entries = 10000
)
tree := NewTree()
generate := func() string {
var buf strings.Builder
size := rand.Intn(avgLen) + avgLen/2
val := stringx.Randn(size)
prev := 0
for j := rand.Intn(9) + 1; j < size; j += rand.Intn(9) + 1 {
buf.WriteRune('/')
buf.WriteString(val[prev:j])
prev = j
}
if prev < size {
buf.WriteRune('/')
buf.WriteString(val[prev:])
}
return buf.String()
}
index := rand.Intn(entries)
var query string
for i := 0; i < entries; i++ {
val := generate()
if i == index {
query = val
}
tree.Add(val, i)
}
for i := 0; i < b.N; i++ {
tree.Search(query)
}
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/prometheus"
"github.com/tal-tech/go-zero/core/stat"
"github.com/tal-tech/go-zero/core/trace"
)
const (
@@ -29,8 +30,7 @@ type ServiceConf struct {
Mode string `json:",default=pro,options=dev|test|rt|pre|pro"`
MetricsUrl string `json:",optional"`
Prometheus prometheus.Config `json:",optional"`
// TODO: enable it in v1.2.1
// Telemetry opentelemetry.Config `json:",optional"`
Telemetry trace.Config `json:",optional"`
}
// MustSetUp sets up the service, exits on error.
@@ -52,11 +52,10 @@ func (sc ServiceConf) SetUp() error {
sc.initMode()
prometheus.StartAgent(sc.Prometheus)
// TODO: enable it in v1.2.1
// if len(sc.Telemetry.Name) == 0 {
// sc.Telemetry.Name = sc.Name
// }
// opentelemetry.StartAgent(sc.Telemetry)
if len(sc.Telemetry.Name) == 0 {
sc.Telemetry.Name = sc.Name
}
trace.StartAgent(sc.Telemetry)
if len(sc.MetricsUrl) > 0 {
stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl))

View File

@@ -26,6 +26,7 @@ type (
}
// A ServiceGroup is a group of services.
// Attention: the starting order of the added services is not guaranteed.
ServiceGroup struct {
services []Service
stopOnce func()
@@ -41,7 +42,8 @@ func NewServiceGroup() *ServiceGroup {
// Add adds service into sg.
func (sg *ServiceGroup) Add(service Service) {
sg.services = append(sg.services, service)
// push front, stop with reverse order.
sg.services = append([]Service{service}, sg.services...)
}
// Start starts the ServiceGroup.

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux
package stat

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux
package stat

View File

@@ -7,7 +7,9 @@ import (
)
func TestRefreshCpu(t *testing.T) {
assert.True(t, RefreshCpu() >= 0)
assert.NotPanics(t, func() {
RefreshCpu()
})
}
func BenchmarkRefreshCpu(b *testing.B) {

View File

@@ -1,3 +1,4 @@
//go:build !linux
// +build !linux
package internal

View File

@@ -0,0 +1,63 @@
package builder
import (
"fmt"
"reflect"
"strings"
)
const dbTag = "db"
// RawFieldNames converts golang struct field into slice string.
func RawFieldNames(in interface{}, postgresSql ...bool) []string {
out := make([]string, 0)
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
var pg bool
if len(postgresSql) > 0 {
pg = postgresSql[0]
}
// we only accept structs
if v.Kind() != reflect.Struct {
panic(fmt.Errorf("ToMap only accepts structs; got %T", v))
}
typ := v.Type()
for i := 0; i < v.NumField(); i++ {
// gets us a StructField
fi := typ.Field(i)
if tagv := fi.Tag.Get(dbTag); tagv != "" {
if pg {
out = append(out, tagv)
} else {
out = append(out, fmt.Sprintf("`%s`", tagv))
}
} else {
if pg {
out = append(out, fi.Name)
} else {
out = append(out, fmt.Sprintf("`%s`", fi.Name))
}
}
}
return out
}
// PostgreSqlJoin concatenates the given elements into a string.
func PostgreSqlJoin(elems []string) string {
b := new(strings.Builder)
for index, e := range elems {
b.WriteString(fmt.Sprintf("%s = $%d, ", e, index+2))
}
if b.Len() == 0 {
return b.String()
}
return b.String()[0 : b.Len()-2]
}

View File

@@ -0,0 +1,24 @@
package builder
import (
"testing"
"github.com/stretchr/testify/assert"
)
type mockedUser struct {
ID string `db:"id" json:"id,omitempty"`
UserName string `db:"user_name" json:"userName,omitempty"`
Sex int `db:"sex" json:"sex,omitempty"`
UUID string `db:"uuid" uuid:"uuid,omitempty"`
Age int `db:"age" json:"age"`
}
func TestFieldNames(t *testing.T) {
t.Run("new", func(t *testing.T) {
var u mockedUser
out := RawFieldNames(&u)
expected := []string{"`id`", "`user_name`", "`sex`", "`uuid`", "`age`"}
assert.Equal(t, expected, out)
})
}

View File

@@ -53,8 +53,8 @@ func TestCacheNode_DelCache(t *testing.T) {
func TestCacheNode_DelCacheWithErrors(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store.Type = redis.ClusterType
clean()
cn := cacheNode{
rds: store,
@@ -73,7 +73,7 @@ func TestCacheNode_InvalidCache(t *testing.T) {
defer s.Close()
cn := cacheNode{
rds: redis.NewRedis(s.Addr(), redis.NodeType),
rds: redis.New(s.Addr()),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),

View File

@@ -16,6 +16,8 @@ var ErrNoRedisNode = errors.New("no redis node")
type (
// Store interface represents a KV store.
Store interface {
Decr(key string) (int64, error)
Decrby(key string, increment int64) (int64, error)
Del(keys ...string) (int, error)
Eval(script, key string, args ...interface{}) (interface{}, error)
Exists(key string) (bool, error)
@@ -36,6 +38,7 @@ type (
Hvals(key string) ([]string, error)
Incr(key string) (int64, error)
Incrby(key string, increment int64) (int64, error)
Lindex(key string, index int64) (string, error)
Llen(key string) (int, error)
Lpop(key string) (string, error)
Lpush(key string, values ...interface{}) (int, error)
@@ -102,6 +105,24 @@ func NewStore(c KvConf) Store {
}
}
func (cs clusterStore) Decr(key string) (int64, error) {
node, err := cs.getRedis(key)
if err != nil {
return 0, err
}
return node.Decr(key)
}
func (cs clusterStore) Decrby(key string, increment int64) (int64, error) {
node, err := cs.getRedis(key)
if err != nil {
return 0, err
}
return node.Decrby(key, increment)
}
func (cs clusterStore) Del(keys ...string) (int, error) {
var val int
var be errorx.BatchError
@@ -303,6 +324,15 @@ func (cs clusterStore) Llen(key string) (int, error) {
return node.Llen(key)
}
func (cs clusterStore) Lindex(key string, index int64) (string, error) {
node, err := cs.getRedis(key)
if err != nil {
return "", err
}
return node.Lindex(key, index)
}
func (cs clusterStore) Lpop(key string) (string, error) {
node, err := cs.getRedis(key)
if err != nil {

View File

@@ -17,6 +17,36 @@ var (
s2, _ = miniredis.Run()
)
func TestRedis_Decr(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Decr("a")
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Decr("a")
assert.Nil(t, err)
assert.Equal(t, int64(-1), val)
val, err = client.Decr("a")
assert.Nil(t, err)
assert.Equal(t, int64(-2), val)
})
}
func TestRedis_DecrBy(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Incrby("a", 2)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Decrby("a", 2)
assert.Nil(t, err)
assert.Equal(t, int64(-2), val)
val, err = client.Decrby("a", 3)
assert.Nil(t, err)
assert.Equal(t, int64(-5), val)
})
}
func TestRedis_Exists(t *testing.T) {
store := clusterStore{dispatcher: hash.NewConsistentHash()}
_, err := store.Exists("foo")
@@ -234,6 +264,8 @@ func TestRedis_List(t *testing.T) {
assert.NotNil(t, err)
_, err = store.Lrem("key", 0, "val")
assert.NotNil(t, err)
_, err = store.Lindex("key", 0)
assert.NotNil(t, err)
runOnCluster(t, func(client Store) {
val, err := client.Lpush("key", "value1", "value2")
@@ -245,6 +277,9 @@ func TestRedis_List(t *testing.T) {
val, err = client.Llen("key")
assert.Nil(t, err)
assert.Equal(t, 4, val)
value, err := client.Lindex("key", 0)
assert.Nil(t, err)
assert.Equal(t, "value2", value)
vals, err := client.Lrange("key", 0, 10)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value2", "value1", "value3", "value4"}, vals)
@@ -667,10 +702,12 @@ func TestRedis_HyperLogLog(t *testing.T) {
assert.NotNil(t, err)
runOnCluster(t, func(cluster Store) {
_, err := cluster.Pfadd("key")
assert.NotNil(t, err)
_, err = cluster.Pfcount("key")
assert.NotNil(t, err)
ok, err := cluster.Pfadd("key", "value")
assert.Nil(t, err)
assert.True(t, ok)
val, err := cluster.Pfcount("key")
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
})
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/tal-tech/go-zero/core/timex"
)
const slowThreshold = time.Millisecond * 500
const defaultSlowThreshold = time.Millisecond * 500
// ErrNotFound is an alias of mgo.ErrNotFound.
var ErrNotFound = mgo.ErrNotFound
@@ -203,7 +203,7 @@ func (c *decoratedCollection) logDuration(method string, duration time.Duration,
if e != nil {
logx.Error(err)
} else if err != nil {
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
c.name, method, err.Error(), string(content))
} else {
@@ -211,7 +211,7 @@ func (c *decoratedCollection) logDuration(method string, duration time.Duration,
c.name, method, err.Error(), string(content))
}
} else {
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
c.name, method, string(content))
} else {

View File

@@ -5,9 +5,10 @@
package internal
import (
reflect "reflect"
mgo "github.com/globalsign/mgo"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockMgoCollection is a mock of MgoCollection interface

View File

@@ -5,9 +5,10 @@
package mongo
import (
reflect "reflect"
bson "github.com/globalsign/mgo/bson"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockIter is a mock of Iter interface

View File

@@ -8,23 +8,14 @@ import (
"github.com/tal-tech/go-zero/core/breaker"
)
type (
options struct {
timeout time.Duration
}
// Option defines the method to customize a mongo model.
Option func(opts *options)
// A Model is a mongo model.
Model struct {
session *concurrentSession
db *mgo.Database
collection string
brk breaker.Breaker
opts []Option
}
)
// A Model is a mongo model.
type Model struct {
session *concurrentSession
db *mgo.Database
collection string
brk breaker.Breaker
opts []Option
}
// MustNewModel returns a Model, exits on errors.
func MustNewModel(url, collection string, opts ...Option) *Model {

View File

@@ -0,0 +1,14 @@
package mongo
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestWithTimeout(t *testing.T) {
o := defaultOptions()
WithTimeout(time.Second)(o)
assert.Equal(t, time.Second, o.timeout)
}

View File

@@ -0,0 +1,29 @@
package mongo
import (
"time"
"github.com/tal-tech/go-zero/core/syncx"
)
var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
type (
options struct {
timeout time.Duration
}
// Option defines the method to customize a mongo model.
Option func(opts *options)
)
// SetSlowThreshold sets the slow threshold.
func SetSlowThreshold(threshold time.Duration) {
slowThreshold.Set(threshold)
}
func defaultOptions() *options {
return &options{
timeout: defaultTimeout,
}
}

View File

@@ -0,0 +1,14 @@
package mongo
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSetSlowThreshold(t *testing.T) {
assert.Equal(t, defaultSlowThreshold, slowThreshold.Load())
SetSlowThreshold(time.Second)
assert.Equal(t, time.Second, slowThreshold.Load())
}

View File

@@ -57,9 +57,7 @@ func (cs *concurrentSession) putSession(session *mgo.Session) {
}
func (cs *concurrentSession) takeSession(opts ...Option) (*mgo.Session, error) {
o := &options{
timeout: defaultTimeout,
}
o := defaultOptions()
for _, opt := range opts {
opt(o)
}

View File

@@ -120,7 +120,7 @@ func TestStatCacheFails(t *testing.T) {
log.SetOutput(ioutil.Discard)
defer log.SetOutput(os.Stdout)
r := redis.NewRedis("localhost:59999", redis.NodeType)
r := redis.New("localhost:59999")
cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
c := newCollection(dummyConn{}, cach)

View File

@@ -36,20 +36,23 @@ func MustNewModel(url, collection string, c cache.CacheConf, opts ...cache.Optio
return model
}
// NewNodeModel returns a Model with a cache node.
func NewNodeModel(url, collection string, rds *redis.Redis, opts ...cache.Option) (*Model, error) {
c := cache.NewNode(rds, sharedCalls, stats, mgo.ErrNotFound, opts...)
// NewModel returns a Model with a cache cluster.
func NewModel(url, collection string, conf cache.CacheConf, opts ...cache.Option) (*Model, error) {
c := cache.New(conf, sharedCalls, stats, mgo.ErrNotFound, opts...)
return NewModelWithCache(url, collection, c)
}
// NewModelWithCache returns a Model with a custom cache.
func NewModelWithCache(url, collection string, c cache.Cache) (*Model, error) {
return createModel(url, collection, c, func(collection mongo.Collection) CachedCollection {
return newCollection(collection, c)
})
}
// NewModel returns a Model with a cache cluster.
func NewModel(url, collection string, conf cache.CacheConf, opts ...cache.Option) (*Model, error) {
c := cache.New(conf, sharedCalls, stats, mgo.ErrNotFound, opts...)
return createModel(url, collection, c, func(collection mongo.Collection) CachedCollection {
return newCollection(collection, c)
})
// NewNodeModel returns a Model with a cache node.
func NewNodeModel(url, collection string, rds *redis.Redis, opts ...cache.Option) (*Model, error) {
c := cache.NewNode(rds, sharedCalls, stats, mgo.ErrNotFound, opts...)
return NewModelWithCache(url, collection, c)
}
// Count returns the count of given query.

View File

@@ -17,7 +17,7 @@ type (
Host string
Type string `json:",default=node,options=node|cluster"`
Pass string `json:",optional"`
Tls bool `json:",default=false,options=true|false"`
Tls bool `json:",optional"`
}
// A RedisKeyConf is a redis config with key.

View File

@@ -9,13 +9,13 @@ import (
"github.com/tal-tech/go-zero/core/timex"
)
func process(proc func(red.Cmder) error) func(red.Cmder) error {
func checkDuration(proc func(red.Cmder) error) func(red.Cmder) error {
return func(cmd red.Cmder) error {
start := timex.Now()
defer func() {
duration := timex.Since(start)
if duration > slowThreshold {
if duration > slowThreshold.Load() {
var buf strings.Builder
for i, arg := range cmd.Args() {
if i > 0 {

View File

@@ -9,6 +9,7 @@ import (
red "github.com/go-redis/redis"
"github.com/tal-tech/go-zero/core/breaker"
"github.com/tal-tech/go-zero/core/mapping"
"github.com/tal-tech/go-zero/core/syncx"
)
const (
@@ -21,12 +22,14 @@ const (
blockingQueryTimeout = 5 * time.Second
readWriteTimeout = 2 * time.Second
slowThreshold = time.Millisecond * 100
defaultSlowThreshold = time.Millisecond * 100
)
// ErrNilNode is an error that indicates a nil redis node.
var ErrNilNode = errors.New("nil redis node")
var (
// ErrNilNode is an error that indicates a nil redis node.
ErrNilNode = errors.New("nil redis node")
slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
)
type (
// Option defines the method to customize a Redis.
@@ -90,6 +93,7 @@ func New(addr string, opts ...Option) *Redis {
return r
}
// Deprecated: use New instead, will be removed in v2.
// NewRedis returns a Redis.
func NewRedis(redisAddr, redisType string, redisPass ...string) *Redis {
var opts []Option
@@ -234,6 +238,36 @@ func (s *Redis) BlpopEx(redisNode RedisNode, key string) (string, bool, error) {
return vals[1], true, nil
}
// Decr is the implementation of redis decr command.
func (s *Redis) Decr(key string) (val int64, err error) {
err = s.brk.DoWithAcceptable(func() error {
conn, err := getRedis(s)
if err != nil {
return err
}
val, err = conn.Decr(key).Result()
return err
}, acceptable)
return
}
// Decrby is the implementation of redis decrby command.
func (s *Redis) Decrby(key string, increment int64) (val int64, err error) {
err = s.brk.DoWithAcceptable(func() error {
conn, err := getRedis(s)
if err != nil {
return err
}
val, err = conn.DecrBy(key, increment).Result()
return err
}, acceptable)
return
}
// Del deletes keys.
func (s *Redis) Del(keys ...string) (val int, err error) {
err = s.brk.DoWithAcceptable(func() error {
@@ -761,6 +795,21 @@ func (s *Redis) Llen(key string) (val int, err error) {
return
}
// Lindex is the implementation of redis lindex command.
func (s *Redis) Lindex(key string, index int64) (val string, err error) {
err = s.brk.DoWithAcceptable(func() error {
conn, err := getRedis(s)
if err != nil {
return err
}
val, err = conn.LIndex(key, index).Result()
return err
}, acceptable)
return
}
// Lpop is the implementation of redis lpop command.
func (s *Redis) Lpop(key string) (val string, err error) {
err = s.brk.DoWithAcceptable(func() error {
@@ -1757,6 +1806,11 @@ func Cluster() Option {
}
}
// SetSlowThreshold sets the slow threshold.
func SetSlowThreshold(threshold time.Duration) {
slowThreshold.Set(threshold)
}
// WithPass customizes the given Redis with given password.
func WithPass(pass string) Option {
return func(r *Redis) {

View File

@@ -11,8 +11,35 @@ import (
"github.com/alicebob/miniredis/v2"
red "github.com/go-redis/redis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stringx"
)
func TestRedis_Decr(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := New(client.Addr, badType()).Decr("a")
assert.NotNil(t, err)
val, err := client.Decr("a")
assert.Nil(t, err)
assert.Equal(t, int64(-1), val)
val, err = client.Decr("a")
assert.Nil(t, err)
assert.Equal(t, int64(-2), val)
})
}
func TestRedis_DecrBy(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := New(client.Addr, badType()).Decrby("a", 2)
assert.NotNil(t, err)
val, err := client.Decrby("a", 2)
assert.Nil(t, err)
assert.Equal(t, int64(-2), val)
val, err = client.Decrby("a", 3)
assert.Nil(t, err)
assert.Equal(t, int64(-5), val)
})
}
func TestRedis_Exists(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := New(client.Addr, badType()).Exists("a")
@@ -186,7 +213,7 @@ func TestRedis_Hscan(t *testing.T) {
key := "hash:test"
fieldsAndValues := make(map[string]string)
for i := 0; i < 1550; i++ {
fieldsAndValues["filed_"+strconv.Itoa(i)] = randomStr(i)
fieldsAndValues["filed_"+strconv.Itoa(i)] = stringx.Randn(i)
}
err := client.Hmset(key, fieldsAndValues)
assert.Nil(t, err)
@@ -256,13 +283,24 @@ func TestRedis_Keys(t *testing.T) {
func TestRedis_HyperLogLog(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
r := New(client.Addr, badType())
_, err := r.Pfadd("key1")
assert.NotNil(t, err)
_, err = r.Pfcount("*")
assert.NotNil(t, err)
err = r.Pfmerge("*")
assert.NotNil(t, err)
r := New(client.Addr)
ok, err := r.Pfadd("key1", "val1")
assert.Nil(t, err)
assert.True(t, ok)
val, err := r.Pfcount("key1")
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
ok, err = r.Pfadd("key2", "val2")
assert.Nil(t, err)
assert.True(t, ok)
val, err = r.Pfcount("key2")
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
err = r.Pfmerge("key1", "key2")
assert.Nil(t, err)
val, err = r.Pfcount("key1")
assert.Nil(t, err)
assert.Equal(t, int64(2), val)
})
}
@@ -283,6 +321,11 @@ func TestRedis_List(t *testing.T) {
val, err = client.Llen("key")
assert.Nil(t, err)
assert.Equal(t, 4, val)
_, err = New(client.Addr, badType()).Lindex("key", 1)
assert.NotNil(t, err)
value, err := client.Lindex("key", 0)
assert.Nil(t, err)
assert.Equal(t, "value2", value)
vals, err := client.Lrange("key", 0, 10)
assert.Nil(t, err)
assert.EqualValues(t, []string{"value2", "value1", "value3", "value4"}, vals)
@@ -539,7 +582,7 @@ func TestRedis_Sscan(t *testing.T) {
key := "list"
var list []string
for i := 0; i < 1550; i++ {
list = append(list, randomStr(i))
list = append(list, stringx.Randn(i))
}
lens, err := client.Sadd(key, list)
assert.Nil(t, err)
@@ -963,7 +1006,7 @@ func TestRedis_Pipelined(t *testing.T) {
func TestRedisString(t *testing.T) {
runOnRedis(t, func(client *Redis) {
client.Ping()
_, err := getRedis(NewRedis(client.Addr, ClusterType))
_, err := getRedis(New(client.Addr, Cluster()))
assert.Nil(t, err)
assert.Equal(t, client.Addr, client.String())
assert.NotNil(t, New(client.Addr, badType()).Ping())
@@ -1073,9 +1116,15 @@ func TestRedisGeo(t *testing.T) {
})
}
func TestSetSlowThreshold(t *testing.T) {
assert.Equal(t, defaultSlowThreshold, slowThreshold.Load())
SetSlowThreshold(time.Second)
assert.Equal(t, time.Second, slowThreshold.Load())
}
func TestRedis_WithPass(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := NewRedis(client.Addr, NodeType, "any").Ping()
err := New(client.Addr, WithPass("any")).Ping()
assert.NotNil(t, err)
})
}
@@ -1095,7 +1144,7 @@ func runOnRedis(t *testing.T, fn func(client *Redis)) {
client.Close()
}
}()
fn(NewRedis(s.Addr(), NodeType))
fn(New(s.Addr()))
}
func runOnRedisTLS(t *testing.T, fn func(client *Redis)) {

View File

@@ -10,10 +10,10 @@ import (
func TestBlockingNode(t *testing.T) {
r, err := miniredis.Run()
assert.Nil(t, err)
node, err := CreateBlockingNode(NewRedis(r.Addr(), NodeType))
node, err := CreateBlockingNode(New(r.Addr()))
assert.Nil(t, err)
node.Close()
node, err = CreateBlockingNode(NewRedis(r.Addr(), ClusterType))
node, err = CreateBlockingNode(New(r.Addr(), Cluster()))
assert.Nil(t, err)
node.Close()
}

View File

@@ -32,7 +32,7 @@ func getClient(r *Redis) (*red.Client, error) {
MinIdleConns: idleConns,
TLSConfig: tlsConfig,
})
store.WrapProcess(process)
store.WrapProcess(checkDuration)
return store, nil
})
if err != nil {

View File

@@ -25,7 +25,7 @@ func getCluster(r *Redis) (*red.ClusterClient, error) {
MinIdleConns: idleConns,
TLSConfig: tlsConfig,
})
store.WrapProcess(process)
store.WrapProcess(checkDuration)
return store, nil
})

View File

@@ -2,36 +2,28 @@ package redis
import (
"math/rand"
"strconv"
"sync/atomic"
"time"
red "github.com/go-redis/redis"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stringx"
)
const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
randomLen = 16
tolerance = 500 // milliseconds
millisPerSecond = 1000
randomLen = 16
)
// A RedisLock is a redis lock.
type RedisLock struct {
store *Redis
seconds uint32
count int32
key string
id string
}
@@ -45,36 +37,41 @@ func NewRedisLock(store *Redis, key string) *RedisLock {
return &RedisLock{
store: store,
key: key,
id: randomStr(randomLen),
id: stringx.Randn(randomLen),
}
}
// Acquire acquires the lock.
func (rl *RedisLock) Acquire() (bool, error) {
seconds := atomic.LoadUint32(&rl.seconds)
resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
})
if err == red.Nil {
return false, nil
} else if err != nil {
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if resp == nil {
return false, nil
}
reply, ok := resp.(string)
if ok && reply == "OK" {
newCount := atomic.AddInt32(&rl.count, 1)
if newCount > 1 {
return true, nil
}
logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
return false, nil
seconds := atomic.LoadUint32(&rl.seconds)
ok, err := rl.store.SetnxEx(rl.key, rl.id, int(seconds+1)) // +1s for tolerance
if err == red.Nil {
atomic.AddInt32(&rl.count, -1)
return false, nil
} else if err != nil {
atomic.AddInt32(&rl.count, -1)
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if !ok {
atomic.AddInt32(&rl.count, -1)
return false, nil
}
return true, nil
}
// Release releases the lock.
func (rl *RedisLock) Release() (bool, error) {
newCount := atomic.AddInt32(&rl.count, -1)
if newCount > 0 {
return true, nil
}
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
if err != nil {
return false, err
@@ -88,15 +85,7 @@ func (rl *RedisLock) Release() (bool, error) {
return reply == 1, nil
}
// SetExpire sets the expire.
// SetExpire sets the expiration.
func (rl *RedisLock) SetExpire(seconds int) {
atomic.StoreUint32(&rl.seconds, uint32(seconds))
}
func randomStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View File

@@ -29,5 +29,25 @@ func TestRedisLock(t *testing.T) {
endAcquire, err := secondLock.Acquire()
assert.Nil(t, err)
assert.True(t, endAcquire)
endAcquire, err = secondLock.Acquire()
assert.Nil(t, err)
assert.True(t, endAcquire)
release, err = secondLock.Release()
assert.Nil(t, err)
assert.True(t, release)
againAcquire, err = firstLock.Acquire()
assert.Nil(t, err)
assert.False(t, againAcquire)
release, err = secondLock.Release()
assert.Nil(t, err)
assert.True(t, release)
firstAcquire, err = firstLock.Acquire()
assert.Nil(t, err)
assert.True(t, firstAcquire)
})
}

View File

@@ -15,7 +15,7 @@ func CreateRedis() (r *redis.Redis, clean func(), err error) {
return nil, nil, err
}
return redis.NewRedis(mr.Addr(), redis.NodeType), func() {
return redis.New(mr.Addr()), func() {
ch := make(chan lang.PlaceholderType)
go func() {
mr.Close()

View File

@@ -39,20 +39,24 @@ type (
}
)
// NewNodeConn returns a CachedConn with a redis node cache.
func NewNodeConn(db sqlx.SqlConn, rds *redis.Redis, opts ...cache.Option) CachedConn {
// NewConn returns a CachedConn with a redis cluster cache.
func NewConn(db sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) CachedConn {
cc := cache.New(c, exclusiveCalls, stats, sql.ErrNoRows, opts...)
return NewConnWithCache(db, cc)
}
// NewConnWithCache returns a CachedConn with a custom cache.
func NewConnWithCache(db sqlx.SqlConn, c cache.Cache) CachedConn {
return CachedConn{
db: db,
cache: cache.NewNode(rds, exclusiveCalls, stats, sql.ErrNoRows, opts...),
cache: c,
}
}
// NewConn returns a CachedConn with a redis cluster cache.
func NewConn(db sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) CachedConn {
return CachedConn{
db: db,
cache: cache.New(c, exclusiveCalls, stats, sql.ErrNoRows, opts...),
}
// NewNodeConn returns a CachedConn with a redis node cache.
func NewNodeConn(db sqlx.SqlConn, rds *redis.Redis, opts ...cache.Option) CachedConn {
c := cache.NewNode(rds, exclusiveCalls, stats, sql.ErrNoRows, opts...)
return NewConnWithCache(db, c)
}
// DelCache deletes cache with keys.

View File

@@ -286,7 +286,7 @@ func TestStatCacheFails(t *testing.T) {
log.SetOutput(ioutil.Discard)
defer log.SetOutput(os.Stdout)
r := redis.NewRedis("localhost:59999", redis.NodeType)
r := redis.New("localhost:59999")
c := NewNodeConn(dummySqlConn{}, r, cache.WithExpiry(time.Second*10))
for i := 0; i < 20; i++ {
@@ -485,7 +485,7 @@ func TestCachedConnExecDropCache(t *testing.T) {
value = "any"
)
var conn trackedConn
c := NewNodeConn(&conn, redis.NewRedis(r.Addr(), redis.NodeType), cache.WithExpiry(time.Second*30))
c := NewNodeConn(&conn, redis.New(r.Addr()), cache.WithExpiry(time.Second*30))
assert.Nil(t, c.SetCache(key, value))
_, err = c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
return conn.Exec("delete from user_table where id='kevin'")
@@ -503,7 +503,7 @@ func TestCachedConnExecDropCache(t *testing.T) {
func TestCachedConnExecDropCacheFailed(t *testing.T) {
const key = "user"
var conn trackedConn
r := redis.NewRedis("anyredis:8888", redis.NodeType)
r := redis.New("anyredis:8888")
c := NewNodeConn(&conn, r, cache.WithExpiry(time.Second*10))
_, err := c.Exec(func(conn sqlx.SqlConn) (result sql.Result, e error) {
return conn.Exec("delete from user_table where id='kevin'")
@@ -562,6 +562,18 @@ func TestQueryRowNoCache(t *testing.T) {
assert.True(t, ran)
}
func TestNewConnWithCache(t *testing.T) {
r, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
var conn trackedConn
c := NewConnWithCache(&conn, cache.NewNode(r, exclusiveCalls, stats, sql.ErrNoRows))
_, err = c.ExecNoCache("delete from user_table where id='kevin'")
assert.Nil(t, err)
assert.True(t, conn.execValue)
}
func resetStats() {
atomic.StoreUint64(&stats.Total, 0)
atomic.StoreUint64(&stats.Hit, 0)

View File

@@ -25,6 +25,7 @@ type (
SqlConn interface {
Session
// RawDB is for other ORM to operate with, use it with caution.
// Notice: don't close it.
RawDB() (*sql.DB, error)
Transact(func(session Session) error) error
}

View File

@@ -5,10 +5,18 @@ import (
"time"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/syncx"
"github.com/tal-tech/go-zero/core/timex"
)
const slowThreshold = time.Millisecond * 500
const defaultSlowThreshold = time.Millisecond * 500
var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
// SetSlowThreshold sets the slow threshold.
func SetSlowThreshold(threshold time.Duration) {
slowThreshold.Set(threshold)
}
func exec(conn sessionConn, q string, args ...interface{}) (sql.Result, error) {
stmt, err := format(q, args...)
@@ -19,7 +27,7 @@ func exec(conn sessionConn, q string, args ...interface{}) (sql.Result, error) {
startTime := timex.Now()
result, err := conn.Exec(q, args...)
duration := timex.Since(startTime)
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[SQL] exec: slowcall - %s", stmt)
} else {
logx.WithDuration(duration).Infof("sql exec: %s", stmt)
@@ -40,7 +48,7 @@ func execStmt(conn stmtConn, q string, args ...interface{}) (sql.Result, error)
startTime := timex.Now()
result, err := conn.Exec(args...)
duration := timex.Since(startTime)
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[SQL] execStmt: slowcall - %s", stmt)
} else {
logx.WithDuration(duration).Infof("sql execStmt: %s", stmt)
@@ -61,7 +69,7 @@ func query(conn sessionConn, scanner func(*sql.Rows) error, q string, args ...in
startTime := timex.Now()
rows, err := conn.Query(q, args...)
duration := timex.Since(startTime)
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[SQL] query: slowcall - %s", stmt)
} else {
logx.WithDuration(duration).Infof("sql query: %s", stmt)
@@ -84,7 +92,7 @@ func queryStmt(conn stmtConn, scanner func(*sql.Rows) error, q string, args ...i
startTime := timex.Now()
rows, err := conn.Query(args...)
duration := timex.Since(startTime)
if duration > slowThreshold {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[SQL] queryStmt: slowcall - %s", stmt)
} else {
logx.WithDuration(duration).Infof("sql queryStmt: %s", stmt)

View File

@@ -171,6 +171,12 @@ func TestStmt_query(t *testing.T) {
}
}
func TestSetSlowThreshold(t *testing.T) {
assert.Equal(t, defaultSlowThreshold, slowThreshold.Load())
SetSlowThreshold(time.Second)
assert.Equal(t, time.Second, slowThreshold.Load())
}
type mockedSessionConn struct {
lastInsertId int64
rowsAffected int64
@@ -180,7 +186,7 @@ type mockedSessionConn struct {
func (m *mockedSessionConn) Exec(query string, args ...interface{}) (sql.Result, error) {
if m.delay {
time.Sleep(slowThreshold + time.Millisecond)
time.Sleep(defaultSlowThreshold + time.Millisecond)
}
return mockedResult{
lastInsertId: m.lastInsertId,
@@ -190,7 +196,7 @@ func (m *mockedSessionConn) Exec(query string, args ...interface{}) (sql.Result,
func (m *mockedSessionConn) Query(query string, args ...interface{}) (*sql.Rows, error) {
if m.delay {
time.Sleep(slowThreshold + time.Millisecond)
time.Sleep(defaultSlowThreshold + time.Millisecond)
}
err := errMockedPlaceholder
@@ -209,7 +215,7 @@ type mockedStmtConn struct {
func (m *mockedStmtConn) Exec(args ...interface{}) (sql.Result, error) {
if m.delay {
time.Sleep(slowThreshold + time.Millisecond)
time.Sleep(defaultSlowThreshold + time.Millisecond)
}
return mockedResult{
lastInsertId: m.lastInsertId,
@@ -219,7 +225,7 @@ func (m *mockedStmtConn) Exec(args ...interface{}) (sql.Result, error) {
func (m *mockedStmtConn) Query(args ...interface{}) (*sql.Rows, error) {
if m.delay {
time.Sleep(slowThreshold + time.Millisecond)
time.Sleep(defaultSlowThreshold + time.Millisecond)
}
err := errMockedPlaceholder

View File

@@ -19,6 +19,12 @@ type (
}
)
// NewSessionFromTx returns a Session with the given sql.Tx.
// Use it with caution, it's provided for other ORM to interact with.
func NewSessionFromTx(tx *sql.Tx) Session {
return txSession{Tx: tx}
}
func (t txSession) Exec(q string, args ...interface{}) (sql.Result, error) {
return exec(t.Tx, q, args...)
}

View File

@@ -19,10 +19,8 @@ func TestDoneChanDone(t *testing.T) {
waitGroup.Add(1)
go func() {
select {
case <-doneChan.Done():
waitGroup.Done()
}
<-doneChan.Done()
waitGroup.Done()
}()
for i := 0; i < 5; i++ {

View File

@@ -17,10 +17,11 @@ func TestPoolGet(t *testing.T) {
ch := make(chan lang.PlaceholderType)
for i := 0; i < limit; i++ {
var fail AtomicBool
go func() {
v := stack.Get()
if v.(int) != 1 {
t.Fatal("unmatch value")
fail.Set(true)
}
ch <- lang.Placeholder
}()
@@ -30,6 +31,10 @@ func TestPoolGet(t *testing.T) {
case <-time.After(time.Second):
t.Fail()
}
if fail.True() {
t.Fatal("unmatch value")
}
}
}

View File

@@ -23,6 +23,7 @@ func NewResourceManager() *ResourceManager {
}
// Close closes the manager.
// Don't use the ResourceManager after Close() called.
func (manager *ResourceManager) Close() error {
manager.lock.Lock()
defer manager.lock.Unlock()
@@ -34,6 +35,9 @@ func (manager *ResourceManager) Close() error {
}
}
// release resources to avoid using it later
manager.resources = nil
return be.Err()
}

View File

@@ -44,3 +44,31 @@ func TestResourceManager_GetResourceError(t *testing.T) {
assert.NotNil(t, err)
}
}
func TestResourceManager_Close(t *testing.T) {
manager := NewResourceManager()
for i := 0; i < 10; i++ {
_, err := manager.GetResource("key", func() (io.Closer, error) {
return nil, errors.New("fail")
})
assert.NotNil(t, err)
}
if assert.NoError(t, manager.Close()) {
assert.Equal(t, 0, len(manager.resources))
}
}
func TestResourceManager_UseAfterClose(t *testing.T) {
manager := NewResourceManager()
_, err := manager.GetResource("key", func() (io.Closer, error) {
return nil, errors.New("fail")
})
assert.NotNil(t, err)
if assert.NoError(t, manager.Close()) {
_, err = manager.GetResource("key", func() (io.Closer, error) {
return nil, errors.New("fail")
})
assert.NotNil(t, err)
}
}

View File

@@ -1,11 +1,14 @@
package syncx
import (
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/lang"
)
func TestTryLock(t *testing.T) {
@@ -30,8 +33,6 @@ func TestSpinLockRace(t *testing.T) {
var wait sync.WaitGroup
wait.Add(1)
go func() {
lock.Lock()
lock.Unlock()
wait.Done()
}()
time.Sleep(time.Millisecond * 100)
@@ -39,3 +40,31 @@ func TestSpinLockRace(t *testing.T) {
wait.Wait()
assert.True(t, lock.TryLock())
}
func TestSpinLock_TryLock(t *testing.T) {
var lock SpinLock
var count int32
var wait sync.WaitGroup
wait.Add(2)
sig := make(chan lang.PlaceholderType)
go func() {
lock.TryLock()
sig <- lang.Placeholder
atomic.AddInt32(&count, 1)
runtime.Gosched()
lock.Unlock()
wait.Done()
}()
go func() {
<-sig
lock.Lock()
atomic.AddInt32(&count, 1)
lock.Unlock()
wait.Done()
}()
wait.Wait()
assert.Equal(t, int32(2), atomic.LoadInt32(&count))
}

View File

@@ -41,3 +41,10 @@ func TestFakeTicker(t *testing.T) {
assert.Nil(t, ticker.Wait(time.Second))
assert.Equal(t, int32(total), atomic.LoadInt32(&count))
}
func TestFakeTickerTimeout(t *testing.T) {
ticker := NewFakeTicker()
defer ticker.Stop()
assert.NotNil(t, ticker.Wait(time.Millisecond))
}

86
core/trace/agent.go Normal file
View File

@@ -0,0 +1,86 @@
package trace
import (
"fmt"
"sync"
"github.com/tal-tech/go-zero/core/lang"
"github.com/tal-tech/go-zero/core/logx"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/exporters/zipkin"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
const (
kindJaeger = "jaeger"
kindZipkin = "zipkin"
)
var (
agents = make(map[string]lang.PlaceholderType)
lock sync.Mutex
)
// StartAgent starts a opentelemetry agent.
func StartAgent(c Config) {
lock.Lock()
defer lock.Unlock()
_, ok := agents[c.Endpoint]
if ok {
return
}
// if error happens, let later calls run.
if err := startAgent(c); err != nil {
return
}
agents[c.Endpoint] = lang.Placeholder
}
func createExporter(c Config) (sdktrace.SpanExporter, error) {
// Just support jaeger and zipkin now, more for later
switch c.Batcher {
case kindJaeger:
return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(c.Endpoint)))
case kindZipkin:
return zipkin.New(c.Endpoint)
default:
return nil, fmt.Errorf("unknown exporter: %s", c.Batcher)
}
}
func startAgent(c Config) error {
opts := []sdktrace.TracerProviderOption{
// Set the sampling rate based on the parent span to 100%
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(c.Sampler))),
// Record information about this application in an Resource.
sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String(c.Name))),
}
if len(c.Endpoint) > 0 {
exp, err := createExporter(c)
if err != nil {
logx.Error(err)
return err
}
// Always be sure to batch in production.
opts = append(opts, sdktrace.WithBatcher(exp))
}
tp := sdktrace.NewTracerProvider(opts...)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, propagation.Baggage{}))
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
logx.Errorf("[otel] error: %v", err)
}))
return nil
}

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