Compare commits
527 Commits
v3.9.8
...
ea84ac094f
Author | SHA1 | Date | |
---|---|---|---|
ea84ac094f | |||
2886a7fe8a | |||
|
268b3dbff4 | ||
f9d2c14597 | |||
e6bf914dd9 | |||
b59f4a16f0 | |||
3deb572f72 | |||
0e668c0f0f | |||
2bac878845 | |||
9ee31fb5a6 | |||
ed5d30a58e | |||
|
b4b67a8b41 | ||
13f90ff716 | |||
0f8f12aee0 | |||
8b406cf963 | |||
029a434a2b | |||
|
847259bc39 | ||
a1ee8728ad | |||
88a5875cfb | |||
03ee33040c | |||
0144f175f0 | |||
b3539a32ab | |||
|
6a7223ea4a | ||
1a1b67866a | |||
b7c98da6d1 | |||
2c21cce076 | |||
c8946dcdc8 | |||
|
d342ff2626 | ||
f2d0d67d4c | |||
677dc30af0 | |||
|
7122cc873c | ||
77e370ffdc | |||
|
7b1c42e50b | ||
f3b9493ac3 | |||
e4ee705eb2 | |||
7ff7a3dbe0 | |||
7af5147f4b | |||
394fd16243 | |||
2b08c8f682 | |||
f9a7f62d02 | |||
|
f5aedf5951 | ||
a5ef231171 | |||
23f2ee9bb7 | |||
88606e89ca | |||
|
24efbb68bf | ||
|
cecdaa0fed | ||
|
9627995cee | ||
|
0f3539dc7b | ||
ff414eff2e | |||
|
fbf6832738 | ||
|
59ff1f931b | ||
2030bd2803 | |||
bb87a87ae5 | |||
0bd5aed7cc | |||
434798a574 | |||
459a951115 | |||
770c2715d4 | |||
c93286afd5 | |||
|
6bf118d978 | ||
7493de1168 | |||
|
212a685b50 | ||
3f21bafc2f | |||
a9ed8b16c1 | |||
|
740cd5931d | ||
85a78063d0 | |||
604ad9cd9d | |||
91137537a2 | |||
950e2352fd | |||
0bb29b29cf | |||
17bcd0b0ab | |||
20f9f4da3b | |||
66fa04b8dc | |||
1ef3ad6531 | |||
c95a91349d | |||
fdcf8e6ca4 | |||
8cb2d9db4a | |||
04da4388ac | |||
79fb23e644 | |||
f8fe923ab1 | |||
105f56dbfe | |||
9fed5a368b | |||
7374d41cf8 | |||
a4a8935c1f | |||
5f498c8232 | |||
a00fdf679b | |||
dc9ebe4155 | |||
87ced484b7 | |||
af99b11a59 | |||
2724b51f7c | |||
5b5d0e02b9 | |||
afc2de6819 | |||
32a8ab9c05 | |||
|
7e5401bded | ||
64b91cea06 | |||
|
0f59fdcbde | ||
50979e6708 | |||
46f3108870 | |||
|
5fed91a65f | ||
1c5bba908d | |||
|
bc8ebdcad5 | ||
fc24f3af92 | |||
1058177d1c | |||
|
fa53fac085 | ||
8c060df5e3 | |||
e1f8c62685 | |||
562b1ab9b7 | |||
|
f3c877a37b | ||
0999b2ad78 | |||
a365513177 | |||
|
d1e3f3cab2 | ||
ec94a09417 | |||
1728b88e06 | |||
d3c31da9db | |||
59095681be | |||
f11ceba225 | |||
ffa01de78f | |||
816abc2bbc | |||
f3f2a9b737 | |||
3f82cb3ba4 | |||
|
306b7a3962 | ||
a8eda9d58d | |||
7e4477dcb4 | |||
|
d846044fc6 | ||
29d956e74e | |||
fcc4faff8a | |||
5df8f83f45 | |||
|
27fa6e9173 | ||
bd55a35dc3 | |||
653bd386cc | |||
|
558c6f4d7c | ||
d7dd6fbeb2 | |||
a00cf2c8d9 | |||
|
a3e8ab2492 | ||
06da500ef4 | |||
277f04ba19 | |||
|
470263ff5f | ||
b8232e02be | |||
|
f8c5e10c1d | ||
397e71f815 | |||
74e31d99f6 | |||
f39de15d93 | |||
d291102877 | |||
37ffbb18d8 | |||
9a85dead86 | |||
a489aab1c3 | |||
d160664ef1 | |||
fa868edcaa | |||
|
6ed0b0e090 | ||
533b265d19 | |||
1ace2631a4 | |||
3dd5ca68d1 | |||
66ccd6021f | |||
e5346f7e4f | |||
|
daf19f031a | ||
5989fd54ca | |||
ed30c26324 | |||
0f8f93d09a | |||
|
f460e2f8dd | ||
70d6a79274 | |||
664b1586af | |||
8d747c64a8 | |||
94beb5ed3b | |||
98981ba86c | |||
1013f50d0e | |||
0b190997b1 | |||
69a44eb190 | |||
0476028f69 | |||
330d8b149a | |||
19b04fe070 | |||
4cd55875c6 | |||
a7896cc728 | |||
ff991bf49c | |||
5a6551b703 | |||
9406a33d60 | |||
8f185abd9d | |||
86492e0644 | |||
b21972964a | |||
f5ee065d09 | |||
8cb02f2b08 | |||
bc926cd6bd | |||
356abfd818 | |||
18444d3f98 | |||
d5f07922e8 | |||
675e717410 | |||
7b6aea235a | |||
2cb7200467 | |||
f430f97a97 | |||
0060c4377a | |||
e46579fe9a | |||
ca52973194 | |||
5bb33c7e1d | |||
b71fc25328 | |||
9345dd075a | |||
1c1b9c0a28 | |||
2969494c5a | |||
cbd3fa38ba | |||
569a36383d | |||
90bed77526 | |||
ba4478a5e0 | |||
6dc76cdfea | |||
e266683d96 | |||
2b62ad04f2 | |||
275b0a64e5 | |||
38c5fe8b5a | |||
b6a0e4d983 | |||
d9b822deff | |||
0e66688f8f | |||
9213fd212f | |||
aa360dcf51 | |||
2df259b5b8 | |||
15e9310368 | |||
|
16d8cf3434 | ||
9704ef2e5e | |||
94e8f90f00 | |||
34d1587881 | |||
bf4143cde5 | |||
36b7b9f5fb | |||
ae97023092 | |||
115ca6a018 | |||
89cf4ef8af | |||
2a6ce6d4da | |||
ad19fe2b90 | |||
49055a28ea | |||
d1c6e121c1 | |||
7cd7fb0c0a | |||
77eb5b5264 | |||
929e46c087 | |||
1fb5673d27 | |||
3bbb0cbc72 | |||
71fe0df73f | |||
f1b8ecbdb3 | |||
fd2b2762e9 | |||
82d269cfb4 | |||
6641463eed | |||
faf2454f0a | |||
de9e4d73f5 | |||
4ae7277140 | |||
a98618ed5b | |||
3aaf1182cb | |||
eb1482d789 | |||
a305f7553f | |||
|
d9b2f2a45d | ||
3ace7657dc | |||
53b40617e2 | |||
1a9236caad | |||
6c68d39081 | |||
35e62fbeb0 | |||
00b3ceb468 | |||
7dc8f088c9 | |||
c65afcea1b | |||
3eebfb5b11 | |||
fa1427014c | |||
62074965ee | |||
9c8fbb2202 | |||
7c0a5f5e2a | |||
b08f5321b0 | |||
cc0f24e012 | |||
307a08f50c | |||
edc93e8c37 | |||
391813c260 | |||
1a1459dd0e | |||
4e99680c30 | |||
92a3a547b8 | |||
849c462037 | |||
54a55c83e2 | |||
781dee03db | |||
26dd2eb405 | |||
3a21069b86 | |||
add3ce478c | |||
|
c3de003e4a | ||
7b7cf18a65 | |||
1bcf71c189 | |||
c320d8e518 | |||
b5f8316b57 | |||
d7ddd912a8 | |||
c020d90cb4 | |||
db47b62159 | |||
8254456c8b | |||
c2808679c3 | |||
f418235c16 | |||
67ba7b3753 | |||
e48d7cadf9 | |||
c906186011 | |||
dc0ff91b83 | |||
e739c2d438 | |||
bf4a036652 | |||
f83a29eb67 | |||
aef7f53d88 | |||
02c8e4fb7f | |||
f5693bd940 | |||
701afb7bea | |||
019b407e74 | |||
f29a346434 | |||
27db1876c0 | |||
f66ac9736b | |||
ed7972a1fa | |||
2cc004b01c | |||
df951e5daf | |||
5bec0cef03 | |||
34940b68d7 | |||
1c57127128 | |||
a4dd1a494c | |||
60e5e42167 | |||
b519b61fff | |||
f62b26eda3 | |||
13eda451da | |||
89cad06121 | |||
0bebf3d59f | |||
01e05e8df6 | |||
2b69a4f51c | |||
4af2b077dd | |||
de4418189d | |||
2c44550897 | |||
99b8a3c950 | |||
4c7e1607d4 | |||
897be419b4 | |||
81b9a4341f | |||
d3bb2f7236 | |||
97fd62cb21 | |||
3cd8bc33d6 | |||
f6f67af8d0 | |||
2d5acaca2f | |||
0674df3d9f | |||
2c282825ce | |||
e87ff942bb | |||
0459ea0613 | |||
d44a75d074 | |||
|
ccf92eb84d | ||
6baf1f2744 | |||
8e2eafde9c | |||
c2b97b0f20 | |||
1db017d966 | |||
debf8cb03d | |||
1dc9c1891f | |||
930859a537 | |||
3141f1ed8b | |||
47943cfb05 | |||
ed4e9d54b1 | |||
b4b8583594 | |||
fb43e8c58c | |||
8863c10ef4 | |||
8058095bcc | |||
092f5d96b1 | |||
84552513f7 | |||
80a2db264e | |||
0be09c8b3e | |||
047f479e1b | |||
8f757c953e | |||
5f1c673a24 | |||
6794ea9871 | |||
089e7b6812 | |||
1c703f0f0c | |||
d167c8c67c | |||
df4f96a2d8 | |||
fac3b20bd4 | |||
7c6bd98498 | |||
23e1174f25 | |||
52bed214cf | |||
64c4f5f47e | |||
036c612137 | |||
ca80e3ecf2 | |||
18e7bb41ca | |||
8e72fb1c35 | |||
17f21a03f4 | |||
a076d43a26 | |||
de6efaee0b | |||
9e0e657003 | |||
be5f9ab77f | |||
144dca0cae | |||
75173560e3 | |||
9b3bccd1f1 | |||
ce125b77c1 | |||
2ee8d4ed46 | |||
f58781d076 | |||
e1af4aa3a4 | |||
1d5e795443 | |||
a3a434d923 | |||
bcc06054f1 | |||
270d26f1ae | |||
646212cc08 | |||
00c2c749db | |||
2dbada0e94 | |||
7b8f4410fb | |||
45ebef5544 | |||
cf4cac0733 | |||
50d60b5825 | |||
46ef491764 | |||
a51b8b8102 | |||
15aac48f1e | |||
078069b2d7 | |||
258812304a | |||
da5d50db5b | |||
384e4d113d | |||
dfd1da7f0d | |||
8e5015e580 | |||
bd0c309b71 | |||
b4f0c3e29a | |||
8fddaa0455 | |||
2710c269a8 | |||
70ea93e466 | |||
a87d0ab1c1 | |||
2e5e102719 | |||
36e492314d | |||
0c78873277 | |||
7f57dc09d3 | |||
447206d256 | |||
33a7feb970 | |||
3950f2ed86 | |||
68c1048a7d | |||
bff40bd317 | |||
2878d0a4ea | |||
3138a9fded | |||
742b99636a | |||
34387f129d | |||
47075acb06 | |||
09cb998ba4 | |||
b9dbfb1cfc | |||
56efccc4cf | |||
ce9f896287 | |||
83d87a40e4 | |||
75fd1e43b9 | |||
395a3eed3d | |||
3ba8cb7f9e | |||
b07806b9a1 | |||
0f583218d4 | |||
f4d0237785 | |||
0f343dad0b | |||
7c29afba0b | |||
8159b9d233 | |||
45cdac5c29 | |||
98db0dc8bc | |||
453d2232bd | |||
9b387312da | |||
84024f7713 | |||
5a554f9f0c | |||
9c33cbc8e2 | |||
848fe1c0d4 | |||
6cbf23fec5 | |||
7462b0b53c | |||
cb743cee3f | |||
d18952951c | |||
|
f6b7f1b4bc | ||
|
33fa702104 | ||
4debc392d1 | |||
7137d99102 | |||
f76b3171d9 | |||
db3fc42204 | |||
f59023d741 | |||
6f17fd891a | |||
fd93308e8e | |||
2aaa0a74d8 | |||
ccbf23688b | |||
3bd6db79cb | |||
9347bb0651 | |||
0d63723ed3 | |||
a7f84e0baa | |||
c209892ce8 | |||
421842315f | |||
25350a6531 | |||
5e47cc7e8c | |||
1687b98b11 | |||
a81649d2a2 | |||
b48faa3b2b | |||
0be584ef0d | |||
26a2d18766 | |||
25a796fe4f | |||
d23de14769 | |||
2fb108519c | |||
c7ce238da3 | |||
|
67aa79f18a | ||
e6c3d734a3 | |||
1374e27531 | |||
1060f6a4c3 | |||
7d72ab05c6 | |||
42864ff1c6 | |||
49978b75c0 | |||
|
20770b6e30 | ||
b38c6106b2 | |||
|
138c4a0888 | ||
|
22f66fc258 | ||
18fafbbbab | |||
59c08c1d9a | |||
5fbb1a923e | |||
396387d1e8 | |||
|
4c2f12a419 | ||
b2abb86971 | |||
e546eef96c | |||
91701e7a45 | |||
817bf1f4d0 | |||
4120f79b55 | |||
d659db69ff | |||
416bb313fc | |||
ec43cfea6b | |||
|
60194fb42e | ||
|
945d9d16a5 | ||
1c0e5e1a85 | |||
33591e0bc9 | |||
|
75cbaf2612 | ||
f4aee3414b | |||
9f7b61eb17 | |||
5953b5aae6 | |||
4a8f490e0c | |||
eb8c1332f0 | |||
c1c27b6d1d | |||
|
bb22b203cc | ||
|
4df2f3a5a1 | ||
b8ad19a5a2 | |||
|
d32a97c846 | ||
cfe0473ae0 | |||
c26ad51e25 | |||
aefc398b71 | |||
9af23e3e74 | |||
4ab7f19ef0 | |||
d26e9d642b | |||
f9ecb9b056 | |||
dbfcfcd288 | |||
8b6bdb857b | |||
1181e9dc5e | |||
6ac7b53d75 | |||
80d342a72a | |||
8ff312e71d | |||
20e40ccdfd | |||
d4efbb9b22 | |||
b433cbcbb6 | |||
dae3c1170b | |||
a10dd3d08a | |||
b075230ae5 |
24
.github.old/ISSUE_TEMPLATE/bug_report.md
Normal file
24
.github.old/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: For reporting bugs in go-micro
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
|
||||||
|
1. What are you trying to do?
|
||||||
|
2. What did you expect to happen?
|
||||||
|
3. What happens instead?
|
||||||
|
|
||||||
|
**How to reproduce the bug:**
|
||||||
|
|
||||||
|
If possible, please include a minimal code snippet here.
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
Go Version: please paste `go version` output here
|
||||||
|
```
|
||||||
|
please paste `go env` output here
|
||||||
|
```
|
17
.github.old/ISSUE_TEMPLATE/feature-request---enhancement.md
Normal file
17
.github.old/ISSUE_TEMPLATE/feature-request---enhancement.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request / Enhancement
|
||||||
|
about: If you have a need not served by go-micro
|
||||||
|
title: "[FEATURE]"
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
14
.github.old/ISSUE_TEMPLATE/question.md
Normal file
14
.github.old/ISSUE_TEMPLATE/question.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about go-micro
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before asking, please check if your question has already been answered:
|
||||||
|
|
||||||
|
1. Check the documentation - https://micro.mu/docs/
|
||||||
|
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
||||||
|
3. Search existing issues
|
9
.github.old/PULL_REQUEST_TEMPLATE.md
Normal file
9
.github.old/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## Pull Request template
|
||||||
|
Please, go through these steps before clicking submit on this PR.
|
||||||
|
|
||||||
|
1. Give a descriptive title to your PR.
|
||||||
|
2. Provide a description of your changes.
|
||||||
|
3. Make sure you have some relevant tests.
|
||||||
|
4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).
|
||||||
|
|
||||||
|
**PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**
|
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: approve
|
- name: approve
|
||||||
uses: hmarr/auto-approve-action@v2
|
uses: hmarr/auto-approve-action@v3
|
||||||
if: github.actor == 'vtolstov' || github.actor == 'dependabot[bot]'
|
if: github.actor == 'vtolstov' || github.actor == 'dependabot[bot]'
|
||||||
id: approve
|
id: approve
|
||||||
with:
|
with:
|
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: lint
|
- name: lint
|
||||||
uses: golangci/golangci-lint-action@v3.1.0
|
uses: golangci/golangci-lint-action@v3.4.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
@@ -36,4 +36,4 @@ jobs:
|
|||||||
- name: Run coverage
|
- name: Run coverage
|
||||||
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||||
- name: codecov
|
- name: codecov
|
||||||
uses: codecov/codecov-action@v3.1.0
|
uses: codecov/codecov-action@v3.1.1
|
@@ -15,7 +15,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: metadata
|
- name: metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v1.3.1
|
uses: dependabot/fetch-metadata@v1.3.6
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.TOKEN }}"
|
github-token: "${{ secrets.TOKEN }}"
|
||||||
- name: merge
|
- name: merge
|
@@ -34,14 +34,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: lint
|
- name: lint
|
||||||
uses: golangci/golangci-lint-action@v3.1.0
|
uses: golangci/golangci-lint-action@v3.4.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
|
||||||
version: v1.30
|
version: v1.30
|
||||||
# Optional: working directory, useful for monorepos
|
|
||||||
# working-directory: somedir
|
|
||||||
# Optional: golangci-lint command line arguments.
|
|
||||||
# args: --issues-exit-code=0
|
|
||||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
|
||||||
# only-new-issues: true
|
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: For reporting bugs in go-micro
|
about: For reporting bugs in micro
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -16,9 +16,3 @@ assignees: ''
|
|||||||
**How to reproduce the bug:**
|
**How to reproduce the bug:**
|
||||||
|
|
||||||
If possible, please include a minimal code snippet here.
|
If possible, please include a minimal code snippet here.
|
||||||
|
|
||||||
**Environment:**
|
|
||||||
Go Version: please paste `go version` output here
|
|
||||||
```
|
|
||||||
please paste `go env` output here
|
|
||||||
```
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Feature request / Enhancement
|
name: Feature request / Enhancement
|
||||||
about: If you have a need not served by go-micro
|
about: If you have a need not served by micro
|
||||||
title: "[FEATURE]"
|
title: "[FEATURE]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,14 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: Question
|
name: Question
|
||||||
about: Ask a question about go-micro
|
about: Ask a question about micro
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Before asking, please check if your question has already been answered:
|
|
||||||
|
|
||||||
1. Check the documentation - https://micro.mu/docs/
|
|
||||||
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
|
||||||
3. Search existing issues
|
|
||||||
|
28
.github/autoapprove.yml
vendored
Normal file
28
.github/autoapprove.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: "autoapprove"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [assigned, opened, synchronize, reopened]
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["prbuild"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoapprove:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: approve
|
||||||
|
run: [ "curl -o tea https://dl.gitea.com/tea/main/tea-main-linux-amd64",
|
||||||
|
"chmod +x ./tea",
|
||||||
|
"./tea login add --name unistack --token ${{ secrets.GITHUB_TOKEN }} --url https://git.unistack.org",
|
||||||
|
"./tea pr --repo ${{ github.event.repository.name }}"
|
||||||
|
]
|
||||||
|
if: github.actor == 'vtolstov'
|
||||||
|
id: approve
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
53
.github/workflows/job_coverage.yml
vendored
Normal file
53
.github/workflows/job_coverage.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, v3, v4 ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: github.server_url != 'https://github.com'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- name: test coverage
|
||||||
|
run: |
|
||||||
|
go test -v -cover ./... -covermode=count -coverprofile coverage.out -coverpkg ./...
|
||||||
|
go tool cover -func coverage.out -o coverage.out
|
||||||
|
|
||||||
|
- name: coverage badge
|
||||||
|
uses: tj-actions/coverage-badge-go@v2
|
||||||
|
with:
|
||||||
|
green: 80
|
||||||
|
filename: coverage.out
|
||||||
|
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
name: autocommit
|
||||||
|
with:
|
||||||
|
commit_message: Apply Code Coverage Badge
|
||||||
|
skip_fetch: false
|
||||||
|
skip_checkout: false
|
||||||
|
file_pattern: ./README.md
|
||||||
|
|
||||||
|
- name: push
|
||||||
|
if: steps.auto-commit-action.outputs.changes_detected == 'true'
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ github.token }}
|
||||||
|
branch: ${{ github.ref }}
|
29
.github/workflows/job_lint.yml
vendored
Normal file
29
.github/workflows/job_lint.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup deps
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
94
.github/workflows/job_sync.yml
vendored
Normal file
94
.github/workflows/job_sync.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
name: sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '*/5 * * * *'
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
if: github.server_url != 'https://github.com'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: init
|
||||||
|
run: |
|
||||||
|
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
|
||||||
|
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
|
||||||
|
|
||||||
|
- name: check master
|
||||||
|
id: check_master
|
||||||
|
run: |
|
||||||
|
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
||||||
|
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
||||||
|
echo "src_hash=$src_hash"
|
||||||
|
echo "dst_hash=$dst_hash"
|
||||||
|
if [ "$src_hash" != "$dst_hash" ]; then
|
||||||
|
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: sync master
|
||||||
|
if: steps.check_master.outputs.sync_needed == 'true'
|
||||||
|
run: |
|
||||||
|
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||||
|
cd repo
|
||||||
|
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
|
||||||
|
git pull --rebase upstream master
|
||||||
|
git push upstream master --progress
|
||||||
|
git push origin master --progress
|
||||||
|
cd ../
|
||||||
|
rm -rf repo
|
||||||
|
|
||||||
|
- name: check v3
|
||||||
|
id: check_v3
|
||||||
|
run: |
|
||||||
|
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
||||||
|
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
||||||
|
echo "src_hash=$src_hash"
|
||||||
|
echo "dst_hash=$dst_hash"
|
||||||
|
if [ "$src_hash" != "$dst_hash" ]; then
|
||||||
|
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: sync v3
|
||||||
|
if: steps.check_v3.outputs.sync_needed == 'true'
|
||||||
|
run: |
|
||||||
|
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||||
|
cd repo
|
||||||
|
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
|
||||||
|
git pull --rebase upstream v3
|
||||||
|
git push upstream v3 --progress
|
||||||
|
git push origin v3 --progress
|
||||||
|
cd ../
|
||||||
|
rm -rf repo
|
||||||
|
|
||||||
|
- name: check v4
|
||||||
|
id: check_v4
|
||||||
|
run: |
|
||||||
|
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
||||||
|
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
||||||
|
echo "src_hash=$src_hash"
|
||||||
|
echo "dst_hash=$dst_hash"
|
||||||
|
if [ "$src_hash" != "$dst_hash" ]; then
|
||||||
|
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: sync v4
|
||||||
|
if: steps.check_v4.outputs.sync_needed == 'true'
|
||||||
|
run: |
|
||||||
|
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||||
|
cd repo
|
||||||
|
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
|
||||||
|
git pull --rebase upstream v4
|
||||||
|
git push upstream v4 --progress
|
||||||
|
git push origin v4 --progress
|
||||||
|
cd ../
|
||||||
|
rm -rf repo
|
31
.github/workflows/job_test.yml
vendored
Normal file
31
.github/workflows/job_test.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
push:
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup deps
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run test
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
run: go test -mod readonly -v ./...
|
50
.github/workflows/job_tests.yml
vendored
Normal file
50
.github/workflows/job_tests.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
push:
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: checkout tests
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
filter: 'blob:none'
|
||||||
|
repository: unistack-org/micro-tests
|
||||||
|
path: micro-tests
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup go work
|
||||||
|
env:
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: |
|
||||||
|
go work init
|
||||||
|
go work use .
|
||||||
|
go work use micro-tests
|
||||||
|
- name: setup deps
|
||||||
|
env:
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run tests
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: |
|
||||||
|
cd micro-tests
|
||||||
|
go test -mod readonly -v ./... || true
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
# Develop tools
|
# Develop tools
|
||||||
/.vscode/
|
/.vscode/
|
||||||
/.idea/
|
/.idea/
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
_obj
|
_obj
|
||||||
_test
|
_test
|
||||||
_build
|
_build
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
# Architecture specific extensions/prefixes
|
||||||
*.[568vq]
|
*.[568vq]
|
||||||
|
@@ -1,44 +1,5 @@
|
|||||||
run:
|
run:
|
||||||
concurrency: 4
|
concurrency: 8
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
govet:
|
|
||||||
check-shadowing: true
|
|
||||||
enable:
|
|
||||||
- fieldalignment
|
|
||||||
|
|
||||||
linters:
|
|
||||||
enable:
|
|
||||||
- govet
|
|
||||||
- deadcode
|
|
||||||
- errcheck
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
- staticcheck
|
|
||||||
- structcheck
|
|
||||||
- typecheck
|
|
||||||
- unused
|
|
||||||
- varcheck
|
|
||||||
- bodyclose
|
|
||||||
- gci
|
|
||||||
- goconst
|
|
||||||
- gocritic
|
|
||||||
- gosimple
|
|
||||||
- gofmt
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
- revive
|
|
||||||
- gosec
|
|
||||||
- makezero
|
|
||||||
- misspell
|
|
||||||
- nakedret
|
|
||||||
- nestif
|
|
||||||
- nilerr
|
|
||||||
- noctx
|
|
||||||
- prealloc
|
|
||||||
- unconvert
|
|
||||||
- unparam
|
|
||||||
disable-all: false
|
|
||||||
|
27
README.md
27
README.md
@@ -1,4 +1,9 @@
|
|||||||
# Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [](https://goreportcard.com/report/go.unistack.org/micro/v3) [](https://codecov.io/gh/unistack-org/micro)
|
# Micro
|
||||||
|

|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://pkg.go.dev/go.unistack.org/micro/v4?tab=overview)
|
||||||
|
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av4+event%3Apush)
|
||||||
|
[](https://goreportcard.com/report/go.unistack.org/micro/v4)
|
||||||
|
|
||||||
Micro is a standard library for microservices.
|
Micro is a standard library for microservices.
|
||||||
|
|
||||||
@@ -10,30 +15,20 @@ Micro provides the core requirements for distributed systems development includi
|
|||||||
|
|
||||||
Micro abstracts away the details of distributed systems. Here are the main features.
|
Micro abstracts away the details of distributed systems. Here are the main features.
|
||||||
|
|
||||||
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
|
|
||||||
zero trust networking by providing every service an identity and certificates. This additionally includes rule
|
|
||||||
based access control.
|
|
||||||
|
|
||||||
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
||||||
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
|
level config from any source such as env vars, cmdline, file, consul, vault... You can merge the sources and even define fallbacks.
|
||||||
|
|
||||||
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
||||||
CockroachDB by default. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
s3. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
||||||
|
|
||||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||||
development. When service A needs to speak to service B it needs the location of that service.
|
development. When service A needs to speak to service B it needs the location of that service.
|
||||||
|
|
||||||
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
|
||||||
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
|
||||||
across the services and retry a different node if there's a problem.
|
|
||||||
|
|
||||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||||
and server handle this by default.
|
and server handle this by default.
|
||||||
|
|
||||||
- **Transport** - gRPC or http based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
|
- **Async Messaging** - Pub/Sub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||||
|
|
||||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
|
||||||
Event notifications are a core pattern in micro service development.
|
Event notifications are a core pattern in micro service development.
|
||||||
|
|
||||||
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
||||||
@@ -42,10 +37,6 @@ leadership are built in as a Sync interface. When using an eventually consistent
|
|||||||
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
||||||
are pluggable and allows Micro to be runtime agnostic.
|
are pluggable and allows Micro to be runtime agnostic.
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
To be created.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Micro is Apache 2.0 licensed.
|
Micro is Apache 2.0 licensed.
|
||||||
|
15
SECURITY.md
15
SECURITY.md
@@ -1,15 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| 3.7.x | :white_check_mark: |
|
|
||||||
| < 3.7.0 | :x: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you find any issue, please create github issue in this repo
|
|
182
api/api.go
182
api/api.go
@@ -1,182 +0,0 @@
|
|||||||
package api // import "go.unistack.org/micro/v3/api"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
"go.unistack.org/micro/v3/register"
|
|
||||||
"go.unistack.org/micro/v3/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nolint: revive
|
|
||||||
// Api interface
|
|
||||||
type Api interface {
|
|
||||||
// Initialise options
|
|
||||||
Init(...Option) error
|
|
||||||
// Get the options
|
|
||||||
Options() Options
|
|
||||||
// Register a http handler
|
|
||||||
Register(*Endpoint) error
|
|
||||||
// Register a route
|
|
||||||
Deregister(*Endpoint) error
|
|
||||||
// Implementation of api
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options holds the options
|
|
||||||
type Options struct{}
|
|
||||||
|
|
||||||
// Option func signature
|
|
||||||
type Option func(*Options) error
|
|
||||||
|
|
||||||
// Endpoint is a mapping between an RPC method and HTTP endpoint
|
|
||||||
type Endpoint struct {
|
|
||||||
// Name Greeter.Hello
|
|
||||||
Name string
|
|
||||||
// Desciption for endpoint
|
|
||||||
Description string
|
|
||||||
// Handler e.g rpc, proxy
|
|
||||||
Handler string
|
|
||||||
// Body destination
|
|
||||||
// "*" or "" - top level message value
|
|
||||||
// "string" - inner message value
|
|
||||||
Body string
|
|
||||||
// Host e.g example.com
|
|
||||||
Host []string
|
|
||||||
// Method e.g GET, POST
|
|
||||||
Method []string
|
|
||||||
// Path e.g /greeter. Expect POSIX regex
|
|
||||||
Path []string
|
|
||||||
// Stream flag
|
|
||||||
Stream bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service represents an API service
|
|
||||||
type Service struct {
|
|
||||||
// Name of service
|
|
||||||
Name string
|
|
||||||
// Endpoint for this service
|
|
||||||
Endpoint *Endpoint
|
|
||||||
// Services that provides service
|
|
||||||
Services []*register.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode encodes an endpoint to endpoint metadata
|
|
||||||
func Encode(e *Endpoint) map[string]string {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// endpoint map
|
|
||||||
ep := make(map[string]string)
|
|
||||||
|
|
||||||
// set vals only if they exist
|
|
||||||
set := func(k, v string) {
|
|
||||||
if len(v) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ep[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
set("endpoint", e.Name)
|
|
||||||
set("description", e.Description)
|
|
||||||
set("handler", e.Handler)
|
|
||||||
set("method", strings.Join(e.Method, ","))
|
|
||||||
set("path", strings.Join(e.Path, ","))
|
|
||||||
set("host", strings.Join(e.Host, ","))
|
|
||||||
set("body", e.Body)
|
|
||||||
|
|
||||||
return ep
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes endpoint metadata into an endpoint
|
|
||||||
func Decode(e metadata.Metadata) *Endpoint {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ep := &Endpoint{}
|
|
||||||
ep.Name, _ = e.Get("endpoint")
|
|
||||||
ep.Description, _ = e.Get("description")
|
|
||||||
epmethod, _ := e.Get("method")
|
|
||||||
ep.Method = []string{epmethod}
|
|
||||||
eppath, _ := e.Get("path")
|
|
||||||
ep.Path = []string{eppath}
|
|
||||||
ephost, _ := e.Get("host")
|
|
||||||
ep.Host = []string{ephost}
|
|
||||||
ep.Handler, _ = e.Get("handler")
|
|
||||||
ep.Body, _ = e.Get("body")
|
|
||||||
|
|
||||||
return ep
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates an endpoint to guarantee it won't blow up when being served
|
|
||||||
func Validate(e *Endpoint) error {
|
|
||||||
if e == nil {
|
|
||||||
return errors.New("endpoint is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(e.Name) == 0 {
|
|
||||||
return errors.New("name required")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range e.Path {
|
|
||||||
ps := p[0]
|
|
||||||
pe := p[len(p)-1]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case ps == '^' && pe == '$':
|
|
||||||
if _, err := regexp.CompilePOSIX(p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case ps == '^' && pe != '$':
|
|
||||||
return errors.New("invalid path")
|
|
||||||
case ps != '^' && pe == '$':
|
|
||||||
return errors.New("invalid path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(e.Handler) == 0 {
|
|
||||||
return errors.New("invalid handler")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Design ideas
|
|
||||||
|
|
||||||
// Gateway is an api gateway interface
|
|
||||||
type Gateway interface {
|
|
||||||
// Register a http handler
|
|
||||||
Handle(pattern string, http.Handler)
|
|
||||||
// Register a route
|
|
||||||
RegisterRoute(r Route)
|
|
||||||
// Init initialises the command line.
|
|
||||||
// It also parses further options.
|
|
||||||
Init(...Option) error
|
|
||||||
// Run the gateway
|
|
||||||
Run() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGateway returns a new api gateway
|
|
||||||
func NewGateway() Gateway {
|
|
||||||
return newGateway()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
|
|
||||||
// &api.Endpoint{
|
|
||||||
// Name: "Greeter.Hello",
|
|
||||||
// Path: []string{"/greeter"},
|
|
||||||
// },
|
|
||||||
// ))
|
|
||||||
func WithEndpoint(e *Endpoint) server.HandlerOption {
|
|
||||||
return server.EndpointMetadata(e.Name, Encode(e))
|
|
||||||
}
|
|
152
api/api_test.go
152
api/api_test.go
@@ -1,152 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gocyclo
|
|
||||||
func TestEncoding(t *testing.T) {
|
|
||||||
testData := []*Endpoint{
|
|
||||||
nil,
|
|
||||||
{
|
|
||||||
Name: "Foo.Bar",
|
|
||||||
Description: "A test endpoint",
|
|
||||||
Handler: "meta",
|
|
||||||
Host: []string{"foo.com"},
|
|
||||||
Method: []string{"GET"},
|
|
||||||
Path: []string{"/test"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
compare := func(expect, got []string) bool {
|
|
||||||
// no data to compare, return true
|
|
||||||
if len(expect) == 0 && len(got) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// no data expected but got some return false
|
|
||||||
if len(expect) == 0 && len(got) > 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare expected with what we got
|
|
||||||
for _, e := range expect {
|
|
||||||
var seen bool
|
|
||||||
for _, g := range got {
|
|
||||||
if e == g {
|
|
||||||
seen = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !seen {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we're done, return true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range testData {
|
|
||||||
// encode
|
|
||||||
e := Encode(d)
|
|
||||||
// decode
|
|
||||||
de := Decode(e)
|
|
||||||
|
|
||||||
// nil endpoint returns nil
|
|
||||||
if d == nil {
|
|
||||||
if e != nil {
|
|
||||||
t.Fatalf("expected nil got %v", e)
|
|
||||||
}
|
|
||||||
if de != nil {
|
|
||||||
t.Fatalf("expected nil got %v", de)
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check encoded map
|
|
||||||
name := e["endpoint"]
|
|
||||||
desc := e["description"]
|
|
||||||
method := strings.Split(e["method"], ",")
|
|
||||||
path := strings.Split(e["path"], ",")
|
|
||||||
host := strings.Split(e["host"], ",")
|
|
||||||
handler := e["handler"]
|
|
||||||
|
|
||||||
if name != d.Name {
|
|
||||||
t.Fatalf("expected %v got %v", d.Name, name)
|
|
||||||
}
|
|
||||||
if desc != d.Description {
|
|
||||||
t.Fatalf("expected %v got %v", d.Description, desc)
|
|
||||||
}
|
|
||||||
if handler != d.Handler {
|
|
||||||
t.Fatalf("expected %v got %v", d.Handler, handler)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Method, method); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Method, method)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Path, path); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Path, path)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Host, host); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Host, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.Name != d.Name {
|
|
||||||
t.Fatalf("expected %v got %v", d.Name, de.Name)
|
|
||||||
}
|
|
||||||
if de.Description != d.Description {
|
|
||||||
t.Fatalf("expected %v got %v", d.Description, de.Description)
|
|
||||||
}
|
|
||||||
if de.Handler != d.Handler {
|
|
||||||
t.Fatalf("expected %v got %v", d.Handler, de.Handler)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Method, de.Method); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Method, de.Method)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Path, de.Path); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Path, de.Path)
|
|
||||||
}
|
|
||||||
if ok := compare(d.Host, de.Host); !ok {
|
|
||||||
t.Fatalf("expected %v got %v", d.Host, de.Host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate(t *testing.T) {
|
|
||||||
epPcre := &Endpoint{
|
|
||||||
Name: "Foo.Bar",
|
|
||||||
Description: "A test endpoint",
|
|
||||||
Handler: "meta",
|
|
||||||
Host: []string{"foo.com"},
|
|
||||||
Method: []string{"GET"},
|
|
||||||
Path: []string{"^/test/?$"},
|
|
||||||
}
|
|
||||||
if err := Validate(epPcre); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
epGpath := &Endpoint{
|
|
||||||
Name: "Foo.Bar",
|
|
||||||
Description: "A test endpoint",
|
|
||||||
Handler: "meta",
|
|
||||||
Host: []string{"foo.com"},
|
|
||||||
Method: []string{"GET"},
|
|
||||||
Path: []string{"/test/{id}"},
|
|
||||||
}
|
|
||||||
if err := Validate(epGpath); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
epPcreInvalid := &Endpoint{
|
|
||||||
Name: "Foo.Bar",
|
|
||||||
Description: "A test endpoint",
|
|
||||||
Handler: "meta",
|
|
||||||
Host: []string{"foo.com"},
|
|
||||||
Method: []string{"GET"},
|
|
||||||
Path: []string{"/test/?$"},
|
|
||||||
}
|
|
||||||
if err := Validate(epPcreInvalid); err == nil {
|
|
||||||
t.Fatalf("invalid pcre %v", epPcreInvalid.Path[0])
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
// Package handler provides http handlers
|
|
||||||
package handler // import "go.unistack.org/micro/v3/api/handler"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler represents a HTTP handler that manages a request
|
|
||||||
type Handler interface {
|
|
||||||
// standard http handler
|
|
||||||
http.Handler
|
|
||||||
// name of handler
|
|
||||||
String() string
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.unistack.org/micro/v3/api/router"
|
|
||||||
"go.unistack.org/micro/v3/client"
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultMaxRecvSize specifies max recv size for handler
|
|
||||||
var DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
|
|
||||||
|
|
||||||
// Options struct holds handler options
|
|
||||||
type Options struct {
|
|
||||||
Router router.Router
|
|
||||||
Client client.Client
|
|
||||||
Logger logger.Logger
|
|
||||||
Namespace string
|
|
||||||
MaxRecvSize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func signature
|
|
||||||
type Option func(o *Options)
|
|
||||||
|
|
||||||
// NewOptions creates new options struct and fills it
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
Client: client.DefaultClient,
|
|
||||||
Router: router.DefaultRouter,
|
|
||||||
Logger: logger.DefaultLogger,
|
|
||||||
MaxRecvSize: DefaultMaxRecvSize,
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set namespace if blank
|
|
||||||
if len(options.Namespace) == 0 {
|
|
||||||
WithNamespace("go.micro.api")(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNamespace specifies the namespace for the handler
|
|
||||||
func WithNamespace(s string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Namespace = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRouter specifies a router to be used by the handler
|
|
||||||
func WithRouter(r router.Router) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Router = r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClient specifies client to be used by the handler
|
|
||||||
func WithClient(c client.Client) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Client = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMaxRecvSize specifies max body size
|
|
||||||
func WithMaxRecvSize(size int64) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.MaxRecvSize = size
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,47 +0,0 @@
|
|||||||
// Package grpc resolves a grpc service like /greeter.Say/Hello to greeter service
|
|
||||||
package grpc // import "go.unistack.org/micro/v3/api/resolver/grpc"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver struct
|
|
||||||
type Resolver struct {
|
|
||||||
opts resolver.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve func to resolve enndpoint
|
|
||||||
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
|
||||||
// parse options
|
|
||||||
options := resolver.NewResolveOptions(opts...)
|
|
||||||
|
|
||||||
// /foo.Bar/Service
|
|
||||||
if req.URL.Path == "/" {
|
|
||||||
return nil, errors.New("unknown name")
|
|
||||||
}
|
|
||||||
// [foo.Bar, Service]
|
|
||||||
parts := strings.Split(req.URL.Path[1:], "/")
|
|
||||||
// [foo, Bar]
|
|
||||||
name := strings.Split(parts[0], ".")
|
|
||||||
// foo
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: strings.Join(name[:len(name)-1], "."),
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) String() string {
|
|
||||||
return "grpc"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResolver is used to create new Resolver
|
|
||||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
|
||||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
// Package host resolves using http host
|
|
||||||
package host // import "go.unistack.org/micro/v3/api/resolver/host"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hostResolver struct {
|
|
||||||
opts resolver.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve endpoint
|
|
||||||
func (r *hostResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
|
||||||
// parse options
|
|
||||||
options := resolver.NewResolveOptions(opts...)
|
|
||||||
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: req.Host,
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *hostResolver) String() string {
|
|
||||||
return "host"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResolver creates new host api resolver
|
|
||||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
|
||||||
return &hostResolver{opts: resolver.NewOptions(opts...)}
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/register"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options struct
|
|
||||||
type Options struct {
|
|
||||||
// Context is for external defined options
|
|
||||||
Context context.Context
|
|
||||||
// Handler name
|
|
||||||
Handler string
|
|
||||||
// ServicePrefix is the prefix
|
|
||||||
ServicePrefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func
|
|
||||||
type Option func(o *Options)
|
|
||||||
|
|
||||||
// WithHandler sets the handler being used
|
|
||||||
func WithHandler(h string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Handler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithServicePrefix sets the ServicePrefix option
|
|
||||||
func WithServicePrefix(p string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.ServicePrefix = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOptions returns new initialised options
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
Context: context.Background(),
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveOptions are used when resolving a request
|
|
||||||
type ResolveOptions struct {
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveOption sets an option
|
|
||||||
type ResolveOption func(*ResolveOptions)
|
|
||||||
|
|
||||||
// Domain sets the resolve Domain option
|
|
||||||
func Domain(n string) ResolveOption {
|
|
||||||
return func(o *ResolveOptions) {
|
|
||||||
o.Domain = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResolveOptions returns new initialised resolve options
|
|
||||||
func NewResolveOptions(opts ...ResolveOption) ResolveOptions {
|
|
||||||
options := ResolveOptions{Domain: register.DefaultDomain}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
// Package path resolves using http path
|
|
||||||
package path // import "go.unistack.org/micro/v3/api/resolver/path"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver the path resolver
|
|
||||||
type Resolver struct {
|
|
||||||
opts resolver.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve resolves endpoint
|
|
||||||
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
|
||||||
// parse options
|
|
||||||
options := resolver.NewResolveOptions(opts...)
|
|
||||||
|
|
||||||
if req.URL.Path == "/" {
|
|
||||||
return nil, resolver.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(req.URL.Path[1:], "/")
|
|
||||||
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: r.opts.ServicePrefix + "." + parts[0],
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String retruns the string representation
|
|
||||||
func (r *Resolver) String() string {
|
|
||||||
return "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResolver returns new path resolver
|
|
||||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
|
||||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
// Package resolver resolves a http request to an endpoint
|
|
||||||
package resolver // import "go.unistack.org/micro/v3/api/resolver"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNotFound returned when endpoint is not found
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
// ErrInvalidPath returned on invalid path
|
|
||||||
ErrInvalidPath = errors.New("invalid path")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver resolves requests to endpoints
|
|
||||||
type Resolver interface {
|
|
||||||
Resolve(r *http.Request, opts ...ResolveOption) (*Endpoint, error)
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoint is the endpoint for a http request
|
|
||||||
type Endpoint struct {
|
|
||||||
// Endpoint name e.g greeter
|
|
||||||
Name string
|
|
||||||
// HTTP Host e.g example.com
|
|
||||||
Host string
|
|
||||||
// HTTP Methods e.g GET, POST
|
|
||||||
Method string
|
|
||||||
// HTTP Path e.g /greeter.
|
|
||||||
Path string
|
|
||||||
// Domain endpoint exists within
|
|
||||||
Domain string
|
|
||||||
}
|
|
@@ -1,90 +0,0 @@
|
|||||||
// Package subdomain is a resolver which uses the subdomain to determine the domain to route to. It
|
|
||||||
// offloads the endpoint resolution to a child resolver which is provided in New.
|
|
||||||
package subdomain // import "go.unistack.org/micro/v3/api/resolver/subdomain"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
|
||||||
"golang.org/x/net/publicsuffix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewResolver creates new subdomain api resolver
|
|
||||||
func NewResolver(parent resolver.Resolver, opts ...resolver.Option) resolver.Resolver {
|
|
||||||
options := resolver.NewOptions(opts...)
|
|
||||||
return &subdomainResolver{opts: options, Resolver: parent}
|
|
||||||
}
|
|
||||||
|
|
||||||
type subdomainResolver struct {
|
|
||||||
resolver.Resolver
|
|
||||||
opts resolver.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve resolve endpoint based on subdomain
|
|
||||||
func (r *subdomainResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
|
||||||
if dom := r.Domain(req); len(dom) > 0 {
|
|
||||||
opts = append(opts, resolver.Domain(dom))
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Resolver.Resolve(req, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain returns domain
|
|
||||||
func (r *subdomainResolver) Domain(req *http.Request) string {
|
|
||||||
// determine the host, e.g. foobar.m3o.app
|
|
||||||
host := req.URL.Hostname()
|
|
||||||
if len(host) == 0 {
|
|
||||||
if h, _, err := net.SplitHostPort(req.Host); err == nil {
|
|
||||||
host = h // host does contain a port
|
|
||||||
} else if strings.Contains(err.Error(), "missing port in address") {
|
|
||||||
host = req.Host // host does not contain a port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for an ip address
|
|
||||||
if net.ParseIP(host) != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for dev environment
|
|
||||||
if host == "localhost" || host == "127.0.0.1" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the top level domain plus one (e.g. 'myapp.com')
|
|
||||||
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.DebugLevel) {
|
|
||||||
logger.Debug(r.opts.Context, "Unable to extract domain from %v", host)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// there was no subdomain
|
|
||||||
if host == domain {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the domain from the host, leaving the subdomain, e.g. "staging.foo.myapp.com" => "staging.foo"
|
|
||||||
subdomain := strings.TrimSuffix(host, "."+domain)
|
|
||||||
|
|
||||||
// ignore the API subdomain
|
|
||||||
if subdomain == "api" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the reversed subdomain as the namespace, e.g. "staging.foo" => "foo-staging"
|
|
||||||
comps := strings.Split(subdomain, ".")
|
|
||||||
for i := len(comps)/2 - 1; i >= 0; i-- {
|
|
||||||
opp := len(comps) - 1 - i
|
|
||||||
comps[i], comps[opp] = comps[opp], comps[i]
|
|
||||||
}
|
|
||||||
return strings.Join(comps, "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *subdomainResolver) String() string {
|
|
||||||
return "subdomain"
|
|
||||||
}
|
|
@@ -1,73 +0,0 @@
|
|||||||
package subdomain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver/vpath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResolve(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
Name string
|
|
||||||
Host string
|
|
||||||
Result string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "Top level domain",
|
|
||||||
Host: "micro.mu",
|
|
||||||
Result: "micro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Effective top level domain",
|
|
||||||
Host: "micro.com.au",
|
|
||||||
Result: "micro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Subdomain dev",
|
|
||||||
Host: "dev.micro.mu",
|
|
||||||
Result: "dev",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Subdomain foo",
|
|
||||||
Host: "foo.micro.mu",
|
|
||||||
Result: "foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Multi-level subdomain",
|
|
||||||
Host: "staging.myapp.m3o.app",
|
|
||||||
Result: "myapp-staging",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Dev host",
|
|
||||||
Host: "127.0.0.1",
|
|
||||||
Result: "micro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Localhost",
|
|
||||||
Host: "localhost",
|
|
||||||
Result: "micro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "IP host",
|
|
||||||
Host: "81.151.101.146",
|
|
||||||
Result: "micro",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
r := NewResolver(vpath.NewResolver())
|
|
||||||
result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if result != nil {
|
|
||||||
if tc.Result != result.Domain {
|
|
||||||
t.Fatalf("Expected %v but got %v", tc.Result, result.Domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,75 +0,0 @@
|
|||||||
// Package vpath resolves using http path and recognised versioned urls
|
|
||||||
package vpath // import "go.unistack.org/micro/v3/api/resolver/vpath"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewResolver creates new vpath api resolver
|
|
||||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
|
||||||
return &vpathResolver{opts: resolver.NewOptions(opts...)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type vpathResolver struct {
|
|
||||||
opts resolver.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
var re = regexp.MustCompile("^v[0-9]+$")
|
|
||||||
|
|
||||||
// Resolve endpoint
|
|
||||||
func (r *vpathResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
|
|
||||||
if req.URL.Path == "/" {
|
|
||||||
return nil, errors.New("unknown name")
|
|
||||||
}
|
|
||||||
|
|
||||||
options := resolver.NewResolveOptions(opts...)
|
|
||||||
|
|
||||||
parts := strings.Split(req.URL.Path[1:], "/")
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: r.withPrefix(parts...),
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /v1/foo
|
|
||||||
if re.MatchString(parts[0]) {
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: r.withPrefix(parts[0:2]...),
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resolver.Endpoint{
|
|
||||||
Name: r.withPrefix(parts[0]),
|
|
||||||
Host: req.Host,
|
|
||||||
Method: req.Method,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
Domain: options.Domain,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *vpathResolver) String() string {
|
|
||||||
return "vpath"
|
|
||||||
}
|
|
||||||
|
|
||||||
// withPrefix transforms "foo" into "go.micro.api.foo"
|
|
||||||
func (r *vpathResolver) withPrefix(parts ...string) string {
|
|
||||||
p := r.opts.ServicePrefix
|
|
||||||
if len(p) > 0 {
|
|
||||||
parts = append([]string{p}, parts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, ".")
|
|
||||||
}
|
|
@@ -1,75 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api/resolver"
|
|
||||||
"go.unistack.org/micro/v3/api/resolver/vpath"
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
|
||||||
"go.unistack.org/micro/v3/register"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options holds the options for api router
|
|
||||||
type Options struct {
|
|
||||||
// Register for service lookup
|
|
||||||
Register register.Register
|
|
||||||
// Resolver to use
|
|
||||||
Resolver resolver.Resolver
|
|
||||||
// Logger micro logger
|
|
||||||
Logger logger.Logger
|
|
||||||
// Context is for external options
|
|
||||||
Context context.Context
|
|
||||||
// Handler name
|
|
||||||
Handler string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func signature
|
|
||||||
type Option func(o *Options)
|
|
||||||
|
|
||||||
// NewOptions returns options struct filled by opts
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
Context: context.Background(),
|
|
||||||
Handler: "meta",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Resolver == nil {
|
|
||||||
options.Resolver = vpath.NewResolver(
|
|
||||||
resolver.WithHandler(options.Handler),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContext sets the context
|
|
||||||
func WithContext(ctx context.Context) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHandler sets the handler
|
|
||||||
func WithHandler(h string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Handler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRegister sets the register
|
|
||||||
func WithRegister(r register.Register) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Register = r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithResolver sets the resolver
|
|
||||||
func WithResolver(r resolver.Resolver) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Resolver = r
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
// Package router provides api service routing
|
|
||||||
package router // import "go.unistack.org/micro/v3/api/router"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultRouter contains default router implementation
|
|
||||||
var DefaultRouter Router
|
|
||||||
|
|
||||||
// Router is used to determine an endpoint for a request
|
|
||||||
type Router interface {
|
|
||||||
// Returns options
|
|
||||||
Options() Options
|
|
||||||
// Init initialize router
|
|
||||||
Init(...Option) error
|
|
||||||
// Stop the router
|
|
||||||
Close() error
|
|
||||||
// Endpoint returns an api.Service endpoint or an error if it does not exist
|
|
||||||
Endpoint(r *http.Request) (*api.Service, error)
|
|
||||||
// Register endpoint in router
|
|
||||||
Register(ep *api.Endpoint) error
|
|
||||||
// Deregister endpoint from router
|
|
||||||
Deregister(ep *api.Endpoint) error
|
|
||||||
// Route returns an api.Service route
|
|
||||||
Route(r *http.Request) (*api.Service, error)
|
|
||||||
// String representation of router
|
|
||||||
String() string
|
|
||||||
}
|
|
141
auth/auth.go
141
auth/auth.go
@@ -1,141 +0,0 @@
|
|||||||
// Package auth provides authentication and authorization capability
|
|
||||||
package auth // import "go.unistack.org/micro/v3/auth"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// BearerScheme used for Authorization header
|
|
||||||
BearerScheme = "Bearer "
|
|
||||||
// ScopePublic is the scope applied to a rule to allow access to the public
|
|
||||||
ScopePublic = ""
|
|
||||||
// ScopeAccount is the scope applied to a rule to limit to users with any valid account
|
|
||||||
ScopeAccount = "*"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// DefaultAuth holds default auth implementation
|
|
||||||
DefaultAuth Auth = NewAuth()
|
|
||||||
// ErrInvalidToken is when the token provided is not valid
|
|
||||||
ErrInvalidToken = errors.New("invalid token provided")
|
|
||||||
// ErrForbidden is when a user does not have the necessary scope to access a resource
|
|
||||||
ErrForbidden = errors.New("resource forbidden")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Auth provides authentication and authorization
|
|
||||||
type Auth interface {
|
|
||||||
// Init the auth
|
|
||||||
Init(opts ...Option) error
|
|
||||||
// Options set for auth
|
|
||||||
Options() Options
|
|
||||||
// Generate a new account
|
|
||||||
Generate(id string, opts ...GenerateOption) (*Account, error)
|
|
||||||
// Verify an account has access to a resource using the rules
|
|
||||||
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
|
|
||||||
// Inspect a token
|
|
||||||
Inspect(token string) (*Account, error)
|
|
||||||
// Token generated using refresh token or credentials
|
|
||||||
Token(opts ...TokenOption) (*Token, error)
|
|
||||||
// Grant access to a resource
|
|
||||||
Grant(rule *Rule) error
|
|
||||||
// Revoke access to a resource
|
|
||||||
Revoke(rule *Rule) error
|
|
||||||
// Rules returns all the rules used to verify requests
|
|
||||||
Rules(...RulesOption) ([]*Rule, error)
|
|
||||||
// String returns the name of the implementation
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account provided by an auth provider
|
|
||||||
type Account struct {
|
|
||||||
// Metadata any other associated metadata
|
|
||||||
Metadata metadata.Metadata `json:"metadata"`
|
|
||||||
// ID of the account e.g. email or id
|
|
||||||
ID string `json:"id"`
|
|
||||||
// Type of the account, e.g. service
|
|
||||||
Type string `json:"type"`
|
|
||||||
// Issuer of the account
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
// Secret for the account, e.g. the password
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
// Scopes the account has access to
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token can be short or long lived
|
|
||||||
type Token struct {
|
|
||||||
// Time of token creation
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
// Time of token expiry
|
|
||||||
Expiry time.Time `json:"expiry"`
|
|
||||||
// The token to be used for accessing resources
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
// RefreshToken to be used to generate a new token
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expired returns a boolean indicating if the token needs to be refreshed
|
|
||||||
func (t *Token) Expired() bool {
|
|
||||||
return t.Expiry.Unix() < time.Now().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resource is an entity such as a user or
|
|
||||||
type Resource struct {
|
|
||||||
// Name of the resource, e.g. go.micro.service.notes
|
|
||||||
Name string `json:"name"`
|
|
||||||
// Type of resource, e.g. service
|
|
||||||
Type string `json:"type"`
|
|
||||||
// Endpoint resource e.g NotesService.Create
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access defines the type of access a rule grants
|
|
||||||
type Access int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AccessGranted to a resource
|
|
||||||
AccessGranted Access = iota
|
|
||||||
// AccessDenied to a resource
|
|
||||||
AccessDenied
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rule is used to verify access to a resource
|
|
||||||
type Rule struct {
|
|
||||||
// Resource that rule belongs to
|
|
||||||
Resource *Resource
|
|
||||||
// ID of the rule
|
|
||||||
ID string
|
|
||||||
// Scope of the rule
|
|
||||||
Scope string
|
|
||||||
// Access flag allow/deny
|
|
||||||
Access Access
|
|
||||||
// Priority holds the rule priority
|
|
||||||
Priority int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type accountKey struct{}
|
|
||||||
|
|
||||||
// AccountFromContext gets the account from the context, which
|
|
||||||
// is set by the auth wrapper at the start of a call. If the account
|
|
||||||
// is not set, a nil account will be returned. The error is only returned
|
|
||||||
// when there was a problem retrieving an account
|
|
||||||
func AccountFromContext(ctx context.Context) (*Account, bool) {
|
|
||||||
if ctx == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
acc, ok := ctx.Value(accountKey{}).(*Account)
|
|
||||||
return acc, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContextWithAccount sets the account in the context
|
|
||||||
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, accountKey{}, account)
|
|
||||||
}
|
|
79
auth/noop.go
79
auth/noop.go
@@ -1,79 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.unistack.org/micro/v3/util/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type noopAuth struct {
|
|
||||||
opts Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the name of the implementation
|
|
||||||
func (n *noopAuth) String() string {
|
|
||||||
return "noop"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init the auth
|
|
||||||
func (n *noopAuth) Init(opts ...Option) error {
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&n.opts)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options set for auth
|
|
||||||
func (n *noopAuth) Options() Options {
|
|
||||||
return n.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new account
|
|
||||||
func (n *noopAuth) Generate(id string, opts ...GenerateOption) (*Account, error) {
|
|
||||||
options := NewGenerateOptions(opts...)
|
|
||||||
|
|
||||||
return &Account{
|
|
||||||
ID: id,
|
|
||||||
Secret: options.Secret,
|
|
||||||
Metadata: options.Metadata,
|
|
||||||
Scopes: options.Scopes,
|
|
||||||
Issuer: n.Options().Issuer,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grant access to a resource
|
|
||||||
func (n *noopAuth) Grant(rule *Rule) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke access to a resource
|
|
||||||
func (n *noopAuth) Revoke(rule *Rule) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rules used to verify requests
|
|
||||||
func (n *noopAuth) Rules(opts ...RulesOption) ([]*Rule, error) {
|
|
||||||
return []*Rule{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify an account has access to a resource
|
|
||||||
func (n *noopAuth) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect a token
|
|
||||||
func (n *noopAuth) Inspect(token string) (*Account, error) {
|
|
||||||
id, err := id.New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Account{ID: id, Issuer: n.Options().Issuer}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token generation using an account id and secret
|
|
||||||
func (n *noopAuth) Token(opts ...TokenOption) (*Token, error) {
|
|
||||||
return &Token{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuth returns new noop auth
|
|
||||||
func NewAuth(opts ...Option) Auth {
|
|
||||||
return &noopAuth{opts: NewOptions(opts...)}
|
|
||||||
}
|
|
311
auth/options.go
311
auth/options.go
@@ -1,311 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
"go.unistack.org/micro/v3/meter"
|
|
||||||
"go.unistack.org/micro/v3/store"
|
|
||||||
"go.unistack.org/micro/v3/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewOptions creates Options struct from slice of options
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
Tracer: tracer.DefaultTracer,
|
|
||||||
Logger: logger.DefaultLogger,
|
|
||||||
Meter: meter.DefaultMeter,
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options struct holds auth options
|
|
||||||
type Options struct {
|
|
||||||
// Context holds the external options
|
|
||||||
Context context.Context
|
|
||||||
// Meter used for metrics
|
|
||||||
Meter meter.Meter
|
|
||||||
// Logger used for logging
|
|
||||||
Logger logger.Logger
|
|
||||||
// Tracer used for tracing
|
|
||||||
Tracer tracer.Tracer
|
|
||||||
// Store used for stre data
|
|
||||||
Store store.Store
|
|
||||||
// Token is the services token used to authenticate itself
|
|
||||||
Token *Token
|
|
||||||
// LoginURL is the relative url path where a user can login
|
|
||||||
LoginURL string
|
|
||||||
// PrivateKey for encoding JWTs
|
|
||||||
PrivateKey string
|
|
||||||
// PublicKey for decoding JWTs
|
|
||||||
PublicKey string
|
|
||||||
// Secret is used to authenticate the service
|
|
||||||
Secret string
|
|
||||||
// ID is the services auth ID
|
|
||||||
ID string
|
|
||||||
// Issuer of the service's account
|
|
||||||
Issuer string
|
|
||||||
// Name holds the auth name
|
|
||||||
Name string
|
|
||||||
// Addrs sets the addresses of auth
|
|
||||||
Addrs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func
|
|
||||||
type Option func(o *Options)
|
|
||||||
|
|
||||||
// Addrs is the auth addresses to use
|
|
||||||
func Addrs(addrs ...string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Addrs = addrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name sets the name
|
|
||||||
func Name(n string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Name = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer of the services account
|
|
||||||
func Issuer(i string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Issuer = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store to back auth
|
|
||||||
func Store(s store.Store) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Store = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublicKey is the JWT public key
|
|
||||||
func PublicKey(key string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.PublicKey = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivateKey is the JWT private key
|
|
||||||
func PrivateKey(key string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.PrivateKey = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credentials sets the auth credentials
|
|
||||||
func Credentials(id, secret string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.ID = id
|
|
||||||
o.Secret = secret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientToken sets the auth token to use when making requests
|
|
||||||
func ClientToken(token *Token) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Token = token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginURL sets the auth LoginURL
|
|
||||||
func LoginURL(url string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.LoginURL = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateOptions struct
|
|
||||||
type GenerateOptions struct {
|
|
||||||
Metadata metadata.Metadata
|
|
||||||
Provider string
|
|
||||||
Type string
|
|
||||||
Secret string
|
|
||||||
Issuer string
|
|
||||||
Scopes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateOption func
|
|
||||||
type GenerateOption func(o *GenerateOptions)
|
|
||||||
|
|
||||||
// WithSecret for the generated account
|
|
||||||
func WithSecret(s string) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Secret = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithType for the generated account
|
|
||||||
func WithType(t string) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Type = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMetadata for the generated account
|
|
||||||
func WithMetadata(md metadata.Metadata) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Metadata = metadata.Copy(md)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProvider for the generated account
|
|
||||||
func WithProvider(p string) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Provider = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithScopes for the generated account
|
|
||||||
func WithScopes(s ...string) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Scopes = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithIssuer for the generated account
|
|
||||||
func WithIssuer(i string) GenerateOption {
|
|
||||||
return func(o *GenerateOptions) {
|
|
||||||
o.Issuer = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGenerateOptions from a slice of options
|
|
||||||
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
|
|
||||||
var options GenerateOptions
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenOptions struct
|
|
||||||
type TokenOptions struct {
|
|
||||||
ID string
|
|
||||||
Secret string
|
|
||||||
RefreshToken string
|
|
||||||
Issuer string
|
|
||||||
Expiry time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenOption func
|
|
||||||
type TokenOption func(o *TokenOptions)
|
|
||||||
|
|
||||||
// WithExpiry for the token
|
|
||||||
func WithExpiry(ex time.Duration) TokenOption {
|
|
||||||
return func(o *TokenOptions) {
|
|
||||||
o.Expiry = ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCredentials sets tye id and secret
|
|
||||||
func WithCredentials(id, secret string) TokenOption {
|
|
||||||
return func(o *TokenOptions) {
|
|
||||||
o.ID = id
|
|
||||||
o.Secret = secret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithToken sets the refresh token
|
|
||||||
func WithToken(rt string) TokenOption {
|
|
||||||
return func(o *TokenOptions) {
|
|
||||||
o.RefreshToken = rt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTokenIssuer sets the token issuer option
|
|
||||||
func WithTokenIssuer(iss string) TokenOption {
|
|
||||||
return func(o *TokenOptions) {
|
|
||||||
o.Issuer = iss
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTokenOptions from a slice of options
|
|
||||||
func NewTokenOptions(opts ...TokenOption) TokenOptions {
|
|
||||||
var options TokenOptions
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default expiry of token
|
|
||||||
if options.Expiry == 0 {
|
|
||||||
options.Expiry = time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyOptions struct
|
|
||||||
type VerifyOptions struct {
|
|
||||||
Context context.Context
|
|
||||||
Namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyOption func
|
|
||||||
type VerifyOption func(o *VerifyOptions)
|
|
||||||
|
|
||||||
// VerifyContext pass context to verify
|
|
||||||
func VerifyContext(ctx context.Context) VerifyOption {
|
|
||||||
return func(o *VerifyOptions) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyNamespace sets thhe namespace for verify
|
|
||||||
func VerifyNamespace(ns string) VerifyOption {
|
|
||||||
return func(o *VerifyOptions) {
|
|
||||||
o.Namespace = ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RulesOptions struct
|
|
||||||
type RulesOptions struct {
|
|
||||||
Context context.Context
|
|
||||||
Namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RulesOption func
|
|
||||||
type RulesOption func(o *RulesOptions)
|
|
||||||
|
|
||||||
// RulesContext pass rules context
|
|
||||||
func RulesContext(ctx context.Context) RulesOption {
|
|
||||||
return func(o *RulesOptions) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RulesNamespace sets the rule namespace
|
|
||||||
func RulesNamespace(ns string) RulesOption {
|
|
||||||
return func(o *RulesOptions) {
|
|
||||||
o.Namespace = ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger sets the logger
|
|
||||||
func Logger(l logger.Logger) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Logger = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meter sets the meter
|
|
||||||
func Meter(m meter.Meter) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Meter = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tracer sets the meter
|
|
||||||
func Tracer(t tracer.Tracer) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Tracer = t
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,92 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VerifyAccess an account has access to a resource using the rules provided. If the account does not have
|
|
||||||
// access an error will be returned. If there are no rules provided which match the resource, an error
|
|
||||||
// will be returned
|
|
||||||
//nolint:gocyclo
|
|
||||||
func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
|
|
||||||
// the rule is only to be applied if the type matches the resource or is catch-all (*)
|
|
||||||
validTypes := []string{"*", res.Type}
|
|
||||||
|
|
||||||
// the rule is only to be applied if the name matches the resource or is catch-all (*)
|
|
||||||
validNames := []string{"*", res.Name}
|
|
||||||
|
|
||||||
// rules can have wildcard excludes on endpoints since this can also be a path for web services,
|
|
||||||
// e.g. /foo/* would include /foo/bar. We also want to check for wildcards and the exact endpoint
|
|
||||||
validEndpoints := []string{"*", res.Endpoint}
|
|
||||||
if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 {
|
|
||||||
for i := 1; i < len(comps)+1; i++ {
|
|
||||||
wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/"))
|
|
||||||
validEndpoints = append(validEndpoints, wildcard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter the rules to the ones which match the criteria above
|
|
||||||
filteredRules := make([]*Rule, 0)
|
|
||||||
for _, rule := range rules {
|
|
||||||
if !include(validTypes, rule.Resource.Type) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !include(validNames, rule.Resource.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !include(validEndpoints, rule.Resource.Endpoint) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filteredRules = append(filteredRules, rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the filtered rules by priority, highest to lowest
|
|
||||||
sort.SliceStable(filteredRules, func(i, j int) bool {
|
|
||||||
return filteredRules[i].Priority > filteredRules[j].Priority
|
|
||||||
})
|
|
||||||
|
|
||||||
// loop through the rules and check for a rule which applies to this account
|
|
||||||
for _, rule := range filteredRules {
|
|
||||||
// a blank scope indicates the rule applies to everyone, even nil accounts
|
|
||||||
if rule.Scope == ScopePublic && rule.Access == AccessDenied {
|
|
||||||
return ErrForbidden
|
|
||||||
} else if rule.Scope == ScopePublic && rule.Access == AccessGranted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// all further checks require an account
|
|
||||||
if acc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// this rule applies to any account
|
|
||||||
if rule.Scope == ScopeAccount && rule.Access == AccessDenied {
|
|
||||||
return ErrForbidden
|
|
||||||
} else if rule.Scope == ScopeAccount && rule.Access == AccessGranted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the account has the necessary scope
|
|
||||||
if include(acc.Scopes, rule.Scope) && rule.Access == AccessDenied {
|
|
||||||
return ErrForbidden
|
|
||||||
} else if include(acc.Scopes, rule.Scope) && rule.Access == AccessGranted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no rules matched then return forbidden
|
|
||||||
return ErrForbidden
|
|
||||||
}
|
|
||||||
|
|
||||||
// include is a helper function which checks to see if the slice contains the value. includes is
|
|
||||||
// not case sensitive.
|
|
||||||
func include(slice []string, val string) bool {
|
|
||||||
for _, s := range slice {
|
|
||||||
if strings.EqualFold(s, val) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@@ -1,288 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestVerify(t *testing.T) {
|
|
||||||
srvResource := &Resource{
|
|
||||||
Type: "service",
|
|
||||||
Name: "go.micro.service.foo",
|
|
||||||
Endpoint: "Foo.Bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
webResource := &Resource{
|
|
||||||
Type: "service",
|
|
||||||
Name: "go.micro.web.foo",
|
|
||||||
Endpoint: "/foo/bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
catchallResource := &Resource{
|
|
||||||
Type: "*",
|
|
||||||
Name: "*",
|
|
||||||
Endpoint: "*",
|
|
||||||
}
|
|
||||||
|
|
||||||
tt := []struct {
|
|
||||||
Error error
|
|
||||||
Account *Account
|
|
||||||
Resource *Resource
|
|
||||||
Name string
|
|
||||||
Rules []*Rule
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "NoRules",
|
|
||||||
Rules: []*Rule{},
|
|
||||||
Account: nil,
|
|
||||||
Resource: srvResource,
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallPublicAccount",
|
|
||||||
Account: &Account{},
|
|
||||||
Resource: srvResource,
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "",
|
|
||||||
Resource: catchallResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallPublicNoAccount",
|
|
||||||
Resource: srvResource,
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "",
|
|
||||||
Resource: catchallResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallPrivateAccount",
|
|
||||||
Account: &Account{},
|
|
||||||
Resource: srvResource,
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallPrivateNoAccount",
|
|
||||||
Resource: srvResource,
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallServiceRuleMatch",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: srvResource.Type,
|
|
||||||
Name: srvResource.Name,
|
|
||||||
Endpoint: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallServiceRuleNoMatch",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: srvResource.Type,
|
|
||||||
Name: "wrongname",
|
|
||||||
Endpoint: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "ExactRuleValidScope",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{
|
|
||||||
Scopes: []string{"neededscope"},
|
|
||||||
},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "neededscope",
|
|
||||||
Resource: srvResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "ExactRuleInvalidScope",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{
|
|
||||||
Scopes: []string{"neededscope"},
|
|
||||||
},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "invalidscope",
|
|
||||||
Resource: srvResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallDenyWithAccount",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessDenied,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "CatchallDenyWithNoAccount",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessDenied,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "RulePriorityGrantFirst",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessGranted,
|
|
||||||
Priority: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessDenied,
|
|
||||||
Priority: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "RulePriorityDenyFirst",
|
|
||||||
Resource: srvResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessGranted,
|
|
||||||
Priority: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: catchallResource,
|
|
||||||
Access: AccessDenied,
|
|
||||||
Priority: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "WebExactEndpointValid",
|
|
||||||
Resource: webResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: webResource,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "WebExactEndpointInalid",
|
|
||||||
Resource: webResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: webResource.Type,
|
|
||||||
Name: webResource.Name,
|
|
||||||
Endpoint: "invalidendpoint",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "WebWildcardEndpoint",
|
|
||||||
Resource: webResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: webResource.Type,
|
|
||||||
Name: webResource.Name,
|
|
||||||
Endpoint: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "WebWildcardPathEndpointValid",
|
|
||||||
Resource: webResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: webResource.Type,
|
|
||||||
Name: webResource.Name,
|
|
||||||
Endpoint: "/foo/*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "WebWildcardPathEndpointInvalid",
|
|
||||||
Resource: webResource,
|
|
||||||
Account: &Account{},
|
|
||||||
Rules: []*Rule{
|
|
||||||
{
|
|
||||||
Scope: "*",
|
|
||||||
Resource: &Resource{
|
|
||||||
Type: webResource.Type,
|
|
||||||
Name: webResource.Name,
|
|
||||||
Endpoint: "/bar/*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error: ErrForbidden,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
if err := VerifyAccess(tc.Rules, tc.Account, tc.Resource); err != tc.Error {
|
|
||||||
t.Errorf("Expected %v but got %v", tc.Error, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
121
broker/broker.go
121
broker/broker.go
@@ -1,21 +1,29 @@
|
|||||||
// Package broker is an interface used for asynchronous messaging
|
// Package broker is an interface used for asynchronous messaging
|
||||||
package broker // import "go.unistack.org/micro/v3/broker"
|
package broker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBroker default memory broker
|
// DefaultBroker default memory broker
|
||||||
var DefaultBroker = NewBroker()
|
var DefaultBroker Broker = NewBroker()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotConnected returns when broker used but not connected yet
|
// ErrNotConnected returns when broker used but not connected yet
|
||||||
ErrNotConnected = errors.New("broker not connected")
|
ErrNotConnected = errors.New("broker not connected")
|
||||||
// ErrDisconnected returns when broker disconnected
|
// ErrDisconnected returns when broker disconnected
|
||||||
ErrDisconnected = errors.New("broker disconnected")
|
ErrDisconnected = errors.New("broker disconnected")
|
||||||
|
// ErrInvalidMessage returns when invalid Message passed
|
||||||
|
ErrInvalidMessage = errors.New("invalid message")
|
||||||
|
// ErrInvalidHandler returns when subscriber passed to Subscribe
|
||||||
|
ErrInvalidHandler = errors.New("invalid handler, ony func(Message) error and func([]Message) error supported")
|
||||||
|
// DefaultGracefulTimeout
|
||||||
|
DefaultGracefulTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Broker is an interface used for asynchronous messaging.
|
// Broker is an interface used for asynchronous messaging.
|
||||||
@@ -32,93 +40,44 @@ type Broker interface {
|
|||||||
Connect(ctx context.Context) error
|
Connect(ctx context.Context) error
|
||||||
// Disconnect disconnect from broker
|
// Disconnect disconnect from broker
|
||||||
Disconnect(ctx context.Context) error
|
Disconnect(ctx context.Context) error
|
||||||
|
// NewMessage create new broker message to publish.
|
||||||
|
NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error)
|
||||||
// Publish message to broker topic
|
// Publish message to broker topic
|
||||||
Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
|
Publish(ctx context.Context, topic string, messages ...Message) error
|
||||||
// Subscribe subscribes to topic message via handler
|
// Subscribe subscribes to topic message via handler
|
||||||
Subscribe(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
|
Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
|
||||||
// BatchPublish messages to broker with multiple topics
|
|
||||||
BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error
|
|
||||||
// BatchSubscribe subscribes to topic messages via handler
|
|
||||||
BatchSubscribe(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
|
|
||||||
// String type of broker
|
// String type of broker
|
||||||
String() string
|
String() string
|
||||||
|
// Live returns broker liveness
|
||||||
|
Live() bool
|
||||||
|
// Ready returns broker readiness
|
||||||
|
Ready() bool
|
||||||
|
// Health returns broker health
|
||||||
|
Health() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is used to process messages via a subscription of a topic.
|
type (
|
||||||
type Handler func(Event) error
|
FuncPublish func(ctx context.Context, topic string, messages ...Message) error
|
||||||
|
HookPublish func(next FuncPublish) FuncPublish
|
||||||
|
FuncSubscribe func(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
|
||||||
|
HookSubscribe func(next FuncSubscribe) FuncSubscribe
|
||||||
|
)
|
||||||
|
|
||||||
// Events contains multiple events
|
// Message is given to a subscription handler for processing
|
||||||
type Events []Event
|
type Message interface {
|
||||||
|
// Context for the message.
|
||||||
// Ack try to ack all events and return
|
Context() context.Context
|
||||||
func (evs Events) Ack() error {
|
// Topic returns message destination topic.
|
||||||
var err error
|
|
||||||
for _, ev := range evs {
|
|
||||||
if err = ev.Ack(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetError sets error on event
|
|
||||||
func (evs Events) SetError(err error) {
|
|
||||||
for _, ev := range evs {
|
|
||||||
ev.SetError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchHandler is used to process messages in batches via a subscription of a topic.
|
|
||||||
type BatchHandler func(Events) error
|
|
||||||
|
|
||||||
// Event is given to a subscription handler for processing
|
|
||||||
type Event interface {
|
|
||||||
// Topic returns event topic
|
|
||||||
Topic() string
|
Topic() string
|
||||||
// Message returns broker message
|
// Header returns message headers.
|
||||||
Message() *Message
|
Header() metadata.Metadata
|
||||||
// Ack acknowledge message
|
// Body returns broker message []byte slice
|
||||||
|
Body() []byte
|
||||||
|
// Unmarshal try to decode message body to dst.
|
||||||
|
// This is helper method that uses codec.Unmarshal.
|
||||||
|
Unmarshal(dst interface{}, opts ...codec.Option) error
|
||||||
|
// Ack acknowledge message if supported.
|
||||||
Ack() error
|
Ack() error
|
||||||
// Error returns message error (like decoding errors or some other)
|
|
||||||
Error() error
|
|
||||||
// SetError set event processing error
|
|
||||||
SetError(err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawMessage is a raw encoded JSON value.
|
|
||||||
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
|
||||||
type RawMessage []byte
|
|
||||||
|
|
||||||
// MarshalJSON returns m as the JSON encoding of m.
|
|
||||||
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
|
||||||
if m == nil {
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON sets *m to a copy of data.
|
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
|
||||||
if m == nil {
|
|
||||||
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
|
||||||
}
|
|
||||||
*m = append((*m)[0:0], data...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message is used to transfer data
|
|
||||||
type Message struct {
|
|
||||||
// Header contains message metadata
|
|
||||||
Header metadata.Metadata
|
|
||||||
// Body contains message body
|
|
||||||
Body RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMessage create broker message with topic filled
|
|
||||||
func NewMessage(topic string) *Message {
|
|
||||||
m := &Message{Header: metadata.New(2)}
|
|
||||||
m.Header.Set(metadata.HeaderTopic, topic)
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriber is a convenience return type for the Subscribe method
|
// Subscriber is a convenience return type for the Subscribe method
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Broker, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns broker from passed context
|
||||||
|
func MustContext(ctx context.Context) Broker {
|
||||||
|
b, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing broker")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext savess broker in context
|
// NewContext savess broker in context
|
||||||
func NewContext(ctx context.Context, s Broker) context.Context {
|
func NewContext(ctx context.Context, s Broker) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -33,6 +42,16 @@ func SetSubscribeOption(k, v interface{}) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMessageOption returns a function to setup a context with given value
|
||||||
|
func SetMessageOption(k, v interface{}) MessageOption {
|
||||||
|
return func(o *MessageOptions) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetOption returns a function to setup a context with given value
|
// SetOption returns a function to setup a context with given value
|
||||||
func SetOption(k, v interface{}) Option {
|
func SetOption(k, v interface{}) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -42,13 +61,3 @@ func SetOption(k, v interface{}) Option {
|
|||||||
o.Context = context.WithValue(o.Context, k, v)
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPublishOption returns a function to setup a context with given value
|
|
||||||
func SetPublishOption(k, v interface{}) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -7,16 +7,31 @@ import (
|
|||||||
|
|
||||||
func TestFromContext(t *testing.T) {
|
func TestFromContext(t *testing.T) {
|
||||||
ctx := context.WithValue(context.TODO(), brokerKey{}, NewBroker())
|
ctx := context.WithValue(context.TODO(), brokerKey{}, NewBroker())
|
||||||
|
|
||||||
c, ok := FromContext(ctx)
|
c, ok := FromContext(ctx)
|
||||||
if c == nil || !ok {
|
if c == nil || !ok {
|
||||||
t.Fatal("FromContext not works")
|
t.Fatal("FromContext not works")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
c, ok := FromContext(nil)
|
||||||
|
if ok || c != nil {
|
||||||
|
t.Fatal("FromContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewContext(t *testing.T) {
|
func TestNewContext(t *testing.T) {
|
||||||
ctx := NewContext(context.TODO(), NewBroker())
|
ctx := NewContext(context.TODO(), NewBroker())
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if c == nil || !ok {
|
||||||
|
t.Fatal("NewContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
ctx := NewContext(nil, NewBroker())
|
||||||
c, ok := FromContext(ctx)
|
c, ok := FromContext(ctx)
|
||||||
if c == nil || !ok {
|
if c == nil || !ok {
|
||||||
t.Fatal("NewContext not works")
|
t.Fatal("NewContext not works")
|
||||||
@@ -34,17 +49,6 @@ func TestSetSubscribeOption(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetPublishOption(t *testing.T) {
|
|
||||||
type key struct{}
|
|
||||||
o := SetPublishOption(key{}, "test")
|
|
||||||
opts := &PublishOptions{}
|
|
||||||
o(opts)
|
|
||||||
|
|
||||||
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
|
|
||||||
t.Fatal("SetPublishOption not works")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetOption(t *testing.T) {
|
func TestSetOption(t *testing.T) {
|
||||||
type key struct{}
|
type key struct{}
|
||||||
o := SetOption(key{}, "test")
|
o := SetOption(key{}, "test")
|
||||||
|
342
broker/memory.go
342
broker/memory.go
@@ -1,342 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
maddr "go.unistack.org/micro/v3/util/addr"
|
|
||||||
"go.unistack.org/micro/v3/util/id"
|
|
||||||
mnet "go.unistack.org/micro/v3/util/net"
|
|
||||||
"go.unistack.org/micro/v3/util/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memoryBroker struct {
|
|
||||||
subscribers map[string][]*memorySubscriber
|
|
||||||
addr string
|
|
||||||
opts Options
|
|
||||||
sync.RWMutex
|
|
||||||
connected bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type memoryEvent struct {
|
|
||||||
err error
|
|
||||||
message interface{}
|
|
||||||
topic string
|
|
||||||
opts Options
|
|
||||||
}
|
|
||||||
|
|
||||||
type memorySubscriber struct {
|
|
||||||
ctx context.Context
|
|
||||||
exit chan bool
|
|
||||||
handler Handler
|
|
||||||
batchhandler BatchHandler
|
|
||||||
id string
|
|
||||||
topic string
|
|
||||||
opts SubscribeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Options() Options {
|
|
||||||
return m.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Address() string {
|
|
||||||
return m.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Connect(ctx context.Context) error {
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.connected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// use 127.0.0.1 to avoid scan of all network interfaces
|
|
||||||
addr, err := maddr.Extract("127.0.0.1")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var rng rand.Rand
|
|
||||||
i := rng.Intn(20000)
|
|
||||||
// set addr with port
|
|
||||||
addr = mnet.HostPort(addr, 10000+i)
|
|
||||||
|
|
||||||
m.addr = addr
|
|
||||||
m.connected = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Disconnect(ctx context.Context) error {
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if !m.connected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.connected = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Init(opts ...Option) error {
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&m.opts)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
|
|
||||||
msg.Header.Set(metadata.HeaderTopic, topic)
|
|
||||||
return m.publish(ctx, []*Message{msg}, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
|
||||||
return m.publish(ctx, msgs, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
|
||||||
m.RLock()
|
|
||||||
if !m.connected {
|
|
||||||
m.RUnlock()
|
|
||||||
return ErrNotConnected
|
|
||||||
}
|
|
||||||
m.RUnlock()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
options := NewPublishOptions(opts...)
|
|
||||||
|
|
||||||
msgTopicMap := make(map[string]Events)
|
|
||||||
for _, v := range msgs {
|
|
||||||
p := &memoryEvent{opts: m.opts}
|
|
||||||
|
|
||||||
if m.opts.Codec == nil || options.BodyOnly {
|
|
||||||
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
|
||||||
p.message = v.Body
|
|
||||||
} else {
|
|
||||||
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
|
||||||
p.message, err = m.opts.Codec.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p)
|
|
||||||
}
|
|
||||||
|
|
||||||
beh := m.opts.BatchErrorHandler
|
|
||||||
eh := m.opts.ErrorHandler
|
|
||||||
|
|
||||||
for t, ms := range msgTopicMap {
|
|
||||||
m.RLock()
|
|
||||||
subs, ok := m.subscribers[t]
|
|
||||||
m.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sub := range subs {
|
|
||||||
if sub.opts.BatchErrorHandler != nil {
|
|
||||||
beh = sub.opts.BatchErrorHandler
|
|
||||||
}
|
|
||||||
if sub.opts.ErrorHandler != nil {
|
|
||||||
eh = sub.opts.ErrorHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// batch processing
|
|
||||||
case sub.batchhandler != nil:
|
|
||||||
if err = sub.batchhandler(ms); err != nil {
|
|
||||||
ms.SetError(err)
|
|
||||||
if beh != nil {
|
|
||||||
_ = beh(ms)
|
|
||||||
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
m.opts.Logger.Error(m.opts.Context, err.Error())
|
|
||||||
}
|
|
||||||
} else if sub.opts.AutoAck {
|
|
||||||
if err = ms.Ack(); err != nil {
|
|
||||||
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// single processing
|
|
||||||
case sub.handler != nil:
|
|
||||||
for _, p := range ms {
|
|
||||||
if err = sub.handler(p); err != nil {
|
|
||||||
p.SetError(err)
|
|
||||||
if eh != nil {
|
|
||||||
_ = eh(p)
|
|
||||||
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
m.opts.Logger.Error(m.opts.Context, err.Error())
|
|
||||||
}
|
|
||||||
} else if sub.opts.AutoAck {
|
|
||||||
if err = p.Ack(); err != nil {
|
|
||||||
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
|
|
||||||
m.RLock()
|
|
||||||
if !m.connected {
|
|
||||||
m.RUnlock()
|
|
||||||
return nil, ErrNotConnected
|
|
||||||
}
|
|
||||||
m.RUnlock()
|
|
||||||
|
|
||||||
sid, err := id.New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := NewSubscribeOptions(opts...)
|
|
||||||
|
|
||||||
sub := &memorySubscriber{
|
|
||||||
exit: make(chan bool, 1),
|
|
||||||
id: sid,
|
|
||||||
topic: topic,
|
|
||||||
batchhandler: handler,
|
|
||||||
opts: options,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
|
||||||
m.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-sub.exit
|
|
||||||
m.Lock()
|
|
||||||
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
|
||||||
for _, sb := range m.subscribers[topic] {
|
|
||||||
if sb.id == sub.id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newSubscribers = append(newSubscribers, sb)
|
|
||||||
}
|
|
||||||
m.subscribers[topic] = newSubscribers
|
|
||||||
m.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return sub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
|
||||||
m.RLock()
|
|
||||||
if !m.connected {
|
|
||||||
m.RUnlock()
|
|
||||||
return nil, ErrNotConnected
|
|
||||||
}
|
|
||||||
m.RUnlock()
|
|
||||||
|
|
||||||
sid, err := id.New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := NewSubscribeOptions(opts...)
|
|
||||||
|
|
||||||
sub := &memorySubscriber{
|
|
||||||
exit: make(chan bool, 1),
|
|
||||||
id: sid,
|
|
||||||
topic: topic,
|
|
||||||
handler: handler,
|
|
||||||
opts: options,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
|
||||||
m.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-sub.exit
|
|
||||||
m.Lock()
|
|
||||||
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
|
||||||
for _, sb := range m.subscribers[topic] {
|
|
||||||
if sb.id == sub.id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newSubscribers = append(newSubscribers, sb)
|
|
||||||
}
|
|
||||||
m.subscribers[topic] = newSubscribers
|
|
||||||
m.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return sub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) String() string {
|
|
||||||
return "memory"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryBroker) Name() string {
|
|
||||||
return m.opts.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryEvent) Topic() string {
|
|
||||||
return m.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryEvent) Message() *Message {
|
|
||||||
switch v := m.message.(type) {
|
|
||||||
case *Message:
|
|
||||||
return v
|
|
||||||
case []byte:
|
|
||||||
msg := &Message{}
|
|
||||||
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
|
|
||||||
if m.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryEvent) Ack() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryEvent) Error() error {
|
|
||||||
return m.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryEvent) SetError(err error) {
|
|
||||||
m.err = err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memorySubscriber) Options() SubscribeOptions {
|
|
||||||
return m.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memorySubscriber) Topic() string {
|
|
||||||
return m.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
|
|
||||||
m.exit <- true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBroker return new memory broker
|
|
||||||
func NewBroker(opts ...Option) Broker {
|
|
||||||
return &memoryBroker{
|
|
||||||
opts: NewOptions(opts...),
|
|
||||||
subscribers: make(map[string][]*memorySubscriber),
|
|
||||||
}
|
|
||||||
}
|
|
341
broker/memory/memory.go
Normal file
341
broker/memory/memory.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
|
maddr "go.unistack.org/micro/v4/util/addr"
|
||||||
|
"go.unistack.org/micro/v4/util/id"
|
||||||
|
mnet "go.unistack.org/micro/v4/util/net"
|
||||||
|
"go.unistack.org/micro/v4/util/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Broker struct {
|
||||||
|
funcPublish broker.FuncPublish
|
||||||
|
funcSubscribe broker.FuncSubscribe
|
||||||
|
subscribers map[string][]*Subscriber
|
||||||
|
addr string
|
||||||
|
opts broker.Options
|
||||||
|
mu sync.RWMutex
|
||||||
|
connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryMessage struct {
|
||||||
|
c codec.Codec
|
||||||
|
topic string
|
||||||
|
ctx context.Context
|
||||||
|
body []byte
|
||||||
|
hdr metadata.Metadata
|
||||||
|
opts broker.MessageOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Ack() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Body() []byte {
|
||||||
|
return m.body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Header() metadata.Metadata {
|
||||||
|
return m.hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Context() context.Context {
|
||||||
|
return m.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Topic() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
|
||||||
|
return m.c.Unmarshal(m.body, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscriber struct {
|
||||||
|
ctx context.Context
|
||||||
|
exit chan bool
|
||||||
|
handler interface{}
|
||||||
|
id string
|
||||||
|
topic string
|
||||||
|
opts broker.SubscribeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) newCodec(ct string) (codec.Codec, error) {
|
||||||
|
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||||
|
ct = ct[:idx]
|
||||||
|
}
|
||||||
|
b.mu.RLock()
|
||||||
|
c, ok := b.opts.Codecs[ct]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return nil, codec.ErrUnknownContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Options() broker.Options {
|
||||||
|
return b.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Address() string {
|
||||||
|
return b.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Connect(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use 127.0.0.1 to avoid scan of all network interfaces
|
||||||
|
addr, err := maddr.Extract("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var rng rand.Rand
|
||||||
|
i := rng.Intn(20000)
|
||||||
|
// set addr with port
|
||||||
|
addr = mnet.HostPort(addr, 10000+i)
|
||||||
|
|
||||||
|
b.addr = addr
|
||||||
|
b.connected = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Disconnect(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if !b.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.connected = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Init(opts ...broker.Option) error {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&b.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.funcPublish = b.fnPublish
|
||||||
|
b.funcSubscribe = b.fnSubscribe
|
||||||
|
|
||||||
|
b.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
|
switch h := hook.(type) {
|
||||||
|
case broker.HookPublish:
|
||||||
|
b.funcPublish = h(b.funcPublish)
|
||||||
|
case broker.HookSubscribe:
|
||||||
|
b.funcSubscribe = h(b.funcSubscribe)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...broker.MessageOption) (broker.Message, error) {
|
||||||
|
options := broker.NewMessageOptions(opts...)
|
||||||
|
if options.ContentType == "" {
|
||||||
|
options.ContentType = b.opts.ContentType
|
||||||
|
}
|
||||||
|
m := &memoryMessage{ctx: ctx, hdr: hdr, opts: options}
|
||||||
|
c, err := b.newCodec(m.opts.ContentType)
|
||||||
|
if err == nil {
|
||||||
|
m.body, err = c.Marshal(body)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Publish(ctx context.Context, topic string, messages ...broker.Message) error {
|
||||||
|
return b.funcPublish(ctx, topic, messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) fnPublish(ctx context.Context, topic string, messages ...broker.Message) error {
|
||||||
|
return b.publish(ctx, topic, messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) publish(ctx context.Context, topic string, messages ...broker.Message) error {
|
||||||
|
b.mu.RLock()
|
||||||
|
if !b.connected {
|
||||||
|
b.mu.RUnlock()
|
||||||
|
return broker.ErrNotConnected
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
subs, ok := b.subscribers[topic]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, sub := range subs {
|
||||||
|
switch s := sub.handler.(type) {
|
||||||
|
default:
|
||||||
|
if b.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidHandler)
|
||||||
|
}
|
||||||
|
case func(broker.Message) error:
|
||||||
|
for _, message := range messages {
|
||||||
|
msg, ok := message.(*memoryMessage)
|
||||||
|
if !ok {
|
||||||
|
if b.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.topic = topic
|
||||||
|
if err = s(msg); err == nil && sub.opts.AutoAck {
|
||||||
|
err = msg.Ack()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if b.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
b.opts.Logger.Error(ctx, "broker handler error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case func([]broker.Message) error:
|
||||||
|
if err = s(messages); err == nil && sub.opts.AutoAck {
|
||||||
|
for _, message := range messages {
|
||||||
|
err = message.Ack()
|
||||||
|
if err != nil {
|
||||||
|
if b.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
b.opts.Logger.Error(ctx, "broker handler error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
|
return b.funcSubscribe(ctx, topic, handler, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
|
if err := broker.IsValidHandler(handler); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
if !b.connected {
|
||||||
|
b.mu.RUnlock()
|
||||||
|
return nil, broker.ErrNotConnected
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
sid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := broker.NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
|
sub := &Subscriber{
|
||||||
|
exit: make(chan bool, 1),
|
||||||
|
id: sid,
|
||||||
|
topic: topic,
|
||||||
|
handler: handler,
|
||||||
|
opts: options,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
b.subscribers[topic] = append(b.subscribers[topic], sub)
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sub.exit
|
||||||
|
b.mu.Lock()
|
||||||
|
newSubscribers := make([]*Subscriber, 0, len(b.subscribers)-1)
|
||||||
|
for _, sb := range b.subscribers[topic] {
|
||||||
|
if sb.id == sub.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSubscribers = append(newSubscribers, sb)
|
||||||
|
}
|
||||||
|
b.subscribers[topic] = newSubscribers
|
||||||
|
b.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) String() string {
|
||||||
|
return "memory"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Name() string {
|
||||||
|
return b.opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Live() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Ready() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) Health() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Subscriber) Options() broker.SubscribeOptions {
|
||||||
|
return m.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Subscriber) Topic() string {
|
||||||
|
return m.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Subscriber) Unsubscribe(ctx context.Context) error {
|
||||||
|
m.exit <- true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBroker return new memory broker
|
||||||
|
func NewBroker(opts ...broker.Option) broker.Broker {
|
||||||
|
return &Broker{
|
||||||
|
opts: broker.NewOptions(opts...),
|
||||||
|
subscribers: make(map[string][]*Subscriber),
|
||||||
|
}
|
||||||
|
}
|
74
broker/memory/memory_test.go
Normal file
74
broker/memory/memory_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hldr struct {
|
||||||
|
c atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hldr) Handler(m broker.Message) error {
|
||||||
|
h.c.Add(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryBroker(t *testing.T) {
|
||||||
|
b := NewBroker(broker.Codec("application/octet-stream", codec.NewCodec()))
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := b.Init(); err != nil {
|
||||||
|
t.Fatalf("Unexpected init error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic := "test"
|
||||||
|
count := int64(10)
|
||||||
|
|
||||||
|
h := &hldr{}
|
||||||
|
|
||||||
|
sub, err := b.Subscribe(ctx, topic, h.Handler)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error subscribing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := int64(0); i < count; i++ {
|
||||||
|
message, err := b.NewMessage(ctx,
|
||||||
|
metadata.Pairs(
|
||||||
|
"foo", "bar",
|
||||||
|
"id", fmt.Sprintf("%d", i),
|
||||||
|
),
|
||||||
|
[]byte(`"hello world"`),
|
||||||
|
broker.MessageContentType("application/octet-stream"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Publish(ctx, topic, message); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sub.Unsubscribe(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Disconnect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.c.Load() != count {
|
||||||
|
t.Fatal("invalid messages count received")
|
||||||
|
}
|
||||||
|
}
|
@@ -1,105 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMemoryBatchBroker(t *testing.T) {
|
|
||||||
b := NewBroker()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if err := b.Connect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
topic := "test"
|
|
||||||
count := 10
|
|
||||||
|
|
||||||
fn := func(evts Events) error {
|
|
||||||
return evts.Ack()
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := b.BatchSubscribe(ctx, topic, fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error subscribing %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := make([]*Message, 0, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
message := &Message{
|
|
||||||
Header: map[string]string{
|
|
||||||
metadata.HeaderTopic: topic,
|
|
||||||
"foo": "bar",
|
|
||||||
"id": fmt.Sprintf("%d", i),
|
|
||||||
},
|
|
||||||
Body: []byte(`"hello world"`),
|
|
||||||
}
|
|
||||||
msgs = append(msgs, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BatchPublish(ctx, msgs); err != nil {
|
|
||||||
t.Fatalf("Unexpected error publishing %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sub.Unsubscribe(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Disconnect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMemoryBroker(t *testing.T) {
|
|
||||||
b := NewBroker()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if err := b.Connect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
topic := "test"
|
|
||||||
count := 10
|
|
||||||
|
|
||||||
fn := func(p Event) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := b.Subscribe(ctx, topic, fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error subscribing %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := make([]*Message, 0, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
message := &Message{
|
|
||||||
Header: map[string]string{
|
|
||||||
metadata.HeaderTopic: topic,
|
|
||||||
"foo": "bar",
|
|
||||||
"id": fmt.Sprintf("%d", i),
|
|
||||||
},
|
|
||||||
Body: []byte(`"hello world"`),
|
|
||||||
}
|
|
||||||
msgs = append(msgs, message)
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, topic, message); err != nil {
|
|
||||||
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BatchPublish(ctx, msgs); err != nil {
|
|
||||||
t.Fatalf("Unexpected error publishing %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sub.Unsubscribe(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Disconnect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
179
broker/noop.go
Normal file
179
broker/noop.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoopBroker struct {
|
||||||
|
funcPublish FuncPublish
|
||||||
|
funcSubscribe FuncSubscribe
|
||||||
|
opts Options
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) newCodec(ct string) (codec.Codec, error) {
|
||||||
|
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||||
|
ct = ct[:idx]
|
||||||
|
}
|
||||||
|
b.mu.RLock()
|
||||||
|
c, ok := b.opts.Codecs[ct]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return nil, codec.ErrUnknownContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBroker(opts ...Option) *NoopBroker {
|
||||||
|
b := &NoopBroker{opts: NewOptions(opts...)}
|
||||||
|
b.funcPublish = b.fnPublish
|
||||||
|
b.funcSubscribe = b.fnSubscribe
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Health() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Live() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Ready() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Name() string {
|
||||||
|
return b.opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) String() string {
|
||||||
|
return "noop"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Options() Options {
|
||||||
|
return b.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Init(opts ...Option) error {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&b.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.funcPublish = b.fnPublish
|
||||||
|
b.funcSubscribe = b.fnSubscribe
|
||||||
|
|
||||||
|
b.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
|
switch h := hook.(type) {
|
||||||
|
case HookPublish:
|
||||||
|
b.funcPublish = h(b.funcPublish)
|
||||||
|
case HookSubscribe:
|
||||||
|
b.funcSubscribe = h(b.funcSubscribe)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Connect(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Disconnect(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Address() string {
|
||||||
|
return strings.Join(b.opts.Addrs, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopMessage struct {
|
||||||
|
c codec.Codec
|
||||||
|
ctx context.Context
|
||||||
|
body []byte
|
||||||
|
hdr metadata.Metadata
|
||||||
|
opts MessageOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Ack() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Body() []byte {
|
||||||
|
return m.body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Header() metadata.Metadata {
|
||||||
|
return m.hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Context() context.Context {
|
||||||
|
return m.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Topic() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *noopMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
|
||||||
|
return m.c.Unmarshal(m.body, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error) {
|
||||||
|
options := NewMessageOptions(opts...)
|
||||||
|
if options.ContentType == "" {
|
||||||
|
options.ContentType = b.opts.ContentType
|
||||||
|
}
|
||||||
|
m := &noopMessage{ctx: ctx, hdr: hdr, opts: options}
|
||||||
|
c, err := b.newCodec(m.opts.ContentType)
|
||||||
|
if err == nil {
|
||||||
|
m.body, err = c.Marshal(body)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) fnPublish(_ context.Context, _ string, _ ...Message) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Publish(ctx context.Context, topic string, msg ...Message) error {
|
||||||
|
return b.funcPublish(ctx, topic, msg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoopSubscriber struct {
|
||||||
|
ctx context.Context
|
||||||
|
topic string
|
||||||
|
handler interface{}
|
||||||
|
opts SubscribeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), handler: handler}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NoopBroker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
return b.funcSubscribe(ctx, topic, handler, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NoopSubscriber) Options() SubscribeOptions {
|
||||||
|
return s.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NoopSubscriber) Topic() string {
|
||||||
|
return s.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NoopSubscriber) Unsubscribe(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
35
broker/noop_test.go
Normal file
35
broker/noop_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHook struct {
|
||||||
|
f bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testHook) Publish1(fn FuncPublish) FuncPublish {
|
||||||
|
return func(ctx context.Context, topic string, messages ...Message) error {
|
||||||
|
t.f = true
|
||||||
|
return fn(ctx, topic, messages...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoopHook(t *testing.T) {
|
||||||
|
h := &testHook{}
|
||||||
|
|
||||||
|
b := NewBroker(Hooks(HookPublish(h.Publish1)))
|
||||||
|
|
||||||
|
if err := b.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Publish(context.TODO(), "", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.f {
|
||||||
|
t.Fatal("hook not works")
|
||||||
|
}
|
||||||
|
}
|
@@ -5,55 +5,74 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v3/register"
|
"go.unistack.org/micro/v4/options"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v4/register"
|
||||||
|
"go.unistack.org/micro/v4/sync"
|
||||||
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options struct
|
// Options struct
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// Name holds the broker name
|
||||||
|
Name string
|
||||||
|
|
||||||
// Tracer used for tracing
|
// Tracer used for tracing
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
// Register can be used for clustering
|
// Register can be used for clustering
|
||||||
Register register.Register
|
Register register.Register
|
||||||
// Codec holds the codec for marshal/unmarshal
|
// Codecs holds the codecs for marshal/unmarshal based on content-type
|
||||||
Codec codec.Codec
|
Codecs map[string]codec.Codec
|
||||||
// Logger used for logging
|
// Logger used for logging
|
||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
// Meter used for metrics
|
// Meter used for metrics
|
||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
// Context holds external options
|
// Context holds external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
|
||||||
|
// Wait waits for a collection of goroutines to finish
|
||||||
|
Wait *sync.WaitGroup
|
||||||
// TLSConfig holds tls.TLSConfig options
|
// TLSConfig holds tls.TLSConfig options
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
// ErrorHandler used when broker can't unmarshal incoming message
|
|
||||||
ErrorHandler Handler
|
|
||||||
// BatchErrorHandler used when broker can't unmashal incoming messages
|
|
||||||
BatchErrorHandler BatchHandler
|
|
||||||
// Name holds the broker name
|
|
||||||
Name string
|
|
||||||
// Addrs holds the broker address
|
// Addrs holds the broker address
|
||||||
Addrs []string
|
Addrs []string
|
||||||
|
// Hooks can be run before broker Publish/BatchPublish and
|
||||||
|
// Subscribe/BatchSubscribe methods
|
||||||
|
Hooks options.Hooks
|
||||||
|
|
||||||
|
// GracefulTimeout contains time to wait to finish in flight requests
|
||||||
|
GracefulTimeout time.Duration
|
||||||
|
|
||||||
|
// ContentType will be used if no content-type set when creating message
|
||||||
|
ContentType string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions create new Options
|
// NewOptions create new Options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
Register: register.DefaultRegister,
|
Register: register.DefaultRegister,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Codec: codec.DefaultCodec,
|
Codecs: make(map[string]codec.Codec),
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
|
GracefulTimeout: DefaultGracefulTimeout,
|
||||||
|
ContentType: DefaultContentType,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultContentType is the default content-type if not specified
|
||||||
|
var DefaultContentType = ""
|
||||||
|
|
||||||
// Context sets the context option
|
// Context sets the context option
|
||||||
func Context(ctx context.Context) Option {
|
func Context(ctx context.Context) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -61,17 +80,27 @@ func Context(ctx context.Context) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishOptions struct
|
// ContentType used by default if not specified
|
||||||
type PublishOptions struct {
|
func ContentType(ct string) Option {
|
||||||
// Context holds external options
|
return func(o *Options) {
|
||||||
Context context.Context
|
o.ContentType = ct
|
||||||
// BodyOnly flag says the message contains raw body bytes
|
}
|
||||||
BodyOnly bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublishOptions creates PublishOptions struct
|
// MessageOptions struct
|
||||||
func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
type MessageOptions struct {
|
||||||
options := PublishOptions{
|
// ContentType for message body
|
||||||
|
ContentType string
|
||||||
|
// BodyOnly flag says the message contains raw body bytes and don't need
|
||||||
|
// codec Marshal method
|
||||||
|
BodyOnly bool
|
||||||
|
// Context holds custom options
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessageOptions creates MessageOptions struct
|
||||||
|
func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
||||||
|
options := MessageOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -84,10 +113,6 @@ func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
|||||||
type SubscribeOptions struct {
|
type SubscribeOptions struct {
|
||||||
// Context holds external options
|
// Context holds external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// ErrorHandler used when broker can't unmarshal incoming message
|
|
||||||
ErrorHandler Handler
|
|
||||||
// BatchErrorHandler used when broker can't unmashal incoming messages
|
|
||||||
BatchErrorHandler BatchHandler
|
|
||||||
// Group holds consumer group
|
// Group holds consumer group
|
||||||
Group string
|
Group string
|
||||||
// AutoAck flag specifies auto ack of incoming message when no error happens
|
// AutoAck flag specifies auto ack of incoming message when no error happens
|
||||||
@@ -103,20 +128,20 @@ type SubscribeOptions struct {
|
|||||||
// Option func
|
// Option func
|
||||||
type Option func(*Options)
|
type Option func(*Options)
|
||||||
|
|
||||||
// PublishOption func
|
// MessageOption func
|
||||||
type PublishOption func(*PublishOptions)
|
type MessageOption func(*MessageOptions)
|
||||||
|
|
||||||
// PublishBodyOnly publish only body of the message
|
// MessageContentType sets message content-type that used to Marshal
|
||||||
func PublishBodyOnly(b bool) PublishOption {
|
func MessageContentType(ct string) MessageOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *MessageOptions) {
|
||||||
o.BodyOnly = b
|
o.ContentType = ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishContext sets the context
|
// MessageBodyOnly publish only body of the message
|
||||||
func PublishContext(ctx context.Context) PublishOption {
|
func MessageBodyOnly(b bool) MessageOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *MessageOptions) {
|
||||||
o.Context = ctx
|
o.BodyOnly = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,51 +152,10 @@ func Addrs(addrs ...string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codec sets the codec used for encoding/decoding used where
|
// Codec sets the codec used for encoding/decoding messages
|
||||||
// a broker does not support headers
|
func Codec(ct string, c codec.Codec) Option {
|
||||||
func Codec(c codec.Codec) Option {
|
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Codec = c
|
o.Codecs[ct] = c
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandler will catch all broker errors that cant be handled
|
|
||||||
// in normal way, for example Codec errors
|
|
||||||
func ErrorHandler(h Handler) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.ErrorHandler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchErrorHandler will catch all broker errors that cant be handled
|
|
||||||
// in normal way, for example Codec errors
|
|
||||||
func BatchErrorHandler(h BatchHandler) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.BatchErrorHandler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
|
||||||
// in normal way, for example Codec errors
|
|
||||||
func SubscribeErrorHandler(h Handler) SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.ErrorHandler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeBatchErrorHandler will catch all broker errors that cant be handled
|
|
||||||
// in normal way, for example Codec errors
|
|
||||||
func SubscribeBatchErrorHandler(h BatchHandler) SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.BatchErrorHandler = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue sets the subscribers queue
|
|
||||||
// Deprecated
|
|
||||||
func Queue(name string) SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.Group = name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +208,13 @@ func Name(n string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hooks sets hook runs before action
|
||||||
|
func Hooks(h ...options.Hook) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Hooks = append(o.Hooks, h...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeContext set context
|
// SubscribeContext set context
|
||||||
func SubscribeContext(ctx context.Context) SubscribeOption {
|
func SubscribeContext(ctx context.Context) SubscribeOption {
|
||||||
return func(o *SubscribeOptions) {
|
return func(o *SubscribeOptions) {
|
||||||
@@ -231,14 +222,6 @@ func SubscribeContext(ctx context.Context) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableAutoAck disables auto ack
|
|
||||||
// Deprecated
|
|
||||||
func DisableAutoAck() SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.AutoAck = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeAutoAck contol auto acking of messages
|
// SubscribeAutoAck contol auto acking of messages
|
||||||
// after they have been handled.
|
// after they have been handled.
|
||||||
func SubscribeAutoAck(b bool) SubscribeOption {
|
func SubscribeAutoAck(b bool) SubscribeOption {
|
||||||
|
14
broker/subscriber.go
Normal file
14
broker/subscriber.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
// IsValidHandler func signature
|
||||||
|
func IsValidHandler(sub interface{}) error {
|
||||||
|
switch sub.(type) {
|
||||||
|
default:
|
||||||
|
return ErrInvalidHandler
|
||||||
|
case func(Message) error:
|
||||||
|
break
|
||||||
|
case func([]Message) error:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,32 +0,0 @@
|
|||||||
// Package build is for building source into a package
|
|
||||||
package build // import "go.unistack.org/micro/v3/build"
|
|
||||||
|
|
||||||
// Build is an interface for building packages
|
|
||||||
type Build interface {
|
|
||||||
// Package builds a package
|
|
||||||
Package(name string, src *Source) (*Package, error)
|
|
||||||
// Remove removes the package
|
|
||||||
Remove(*Package) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source is the source of a build
|
|
||||||
type Source struct {
|
|
||||||
// Path to the source if local
|
|
||||||
Path string
|
|
||||||
// Language is the language of code
|
|
||||||
Language string
|
|
||||||
// Location of the source
|
|
||||||
Repository string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Package is packaged format for source
|
|
||||||
type Package struct {
|
|
||||||
// Source of the package
|
|
||||||
Source *Source
|
|
||||||
// Name of the package
|
|
||||||
Name string
|
|
||||||
// Location of the package
|
|
||||||
Path string
|
|
||||||
// Type of package e.g tarball, binary, docker
|
|
||||||
Type string
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
package build
|
|
||||||
|
|
||||||
// Options struct
|
|
||||||
type Options struct {
|
|
||||||
// local path to download source
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func
|
|
||||||
type Option func(o *Options)
|
|
||||||
|
|
||||||
// Path is the Local path for repository
|
|
||||||
func Path(p string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Path = p
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/util/backoff"
|
"go.unistack.org/micro/v4/util/backoff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackoffFunc is the backoff call func
|
// BackoffFunc is the backoff call func
|
||||||
@@ -17,13 +17,13 @@ func BackoffExp(_ context.Context, _ Request, attempts int) (time.Duration, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BackoffInterval specifies randomization interval for backoff func
|
// BackoffInterval specifies randomization interval for backoff func
|
||||||
func BackoffInterval(min time.Duration, max time.Duration) BackoffFunc {
|
func BackoffInterval(minTime time.Duration, maxTime time.Duration) BackoffFunc {
|
||||||
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
||||||
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
||||||
if td < min {
|
if td < minTime {
|
||||||
return min, nil
|
return minTime, nil
|
||||||
} else if td > max {
|
} else if td > maxTime {
|
||||||
return max, nil
|
return maxTime, nil
|
||||||
}
|
}
|
||||||
return td, nil
|
return td, nil
|
||||||
}
|
}
|
||||||
|
@@ -34,23 +34,23 @@ func TestBackoffExp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackoffInterval(t *testing.T) {
|
func TestBackoffInterval(t *testing.T) {
|
||||||
min := 100 * time.Millisecond
|
minTime := 100 * time.Millisecond
|
||||||
max := 300 * time.Millisecond
|
maxTime := 300 * time.Millisecond
|
||||||
|
|
||||||
r := &testRequest{
|
r := &testRequest{
|
||||||
service: "test",
|
service: "test",
|
||||||
method: "test",
|
method: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := BackoffInterval(min, max)
|
fn := BackoffInterval(minTime, maxTime)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
d, err := fn(context.TODO(), r, i)
|
d, err := fn(context.TODO(), r, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d < min || d > max {
|
if d < minTime || d > maxTime {
|
||||||
t.Fatalf("Expected %v < %v < %v", min, d, max)
|
t.Fatalf("Expected %v < %v < %v", minTime, d, maxTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
// Package client is an interface for an RPC client
|
// Package client is an interface for an RPC client
|
||||||
package client // import "go.unistack.org/micro/v3/client"
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultClient is the global default client
|
// DefaultClient is the global default client
|
||||||
DefaultClient = NewClient()
|
DefaultClient = NewClient()
|
||||||
// DefaultContentType is the default content-type if not specified
|
// DefaultContentType is the default content-type if not specified
|
||||||
DefaultContentType = "application/json"
|
DefaultContentType = ""
|
||||||
// DefaultBackoff is the default backoff function for retries (minimum 10 millisecond and maximum 5 second)
|
// DefaultBackoff is the default backoff function for retries (minimum 10 millisecond and maximum 5 second)
|
||||||
DefaultBackoff = BackoffInterval(10*time.Millisecond, 5*time.Second)
|
DefaultBackoff = BackoffInterval(10*time.Millisecond, 5*time.Second)
|
||||||
// DefaultRetry is the default check-for-retry function for retries
|
// DefaultRetry is the default check-for-retry function for retries
|
||||||
@@ -29,28 +29,23 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Client is the interface used to make requests to services.
|
// Client is the interface used to make requests to services.
|
||||||
// It supports Request/Response via Transport and Publishing via the Broker.
|
|
||||||
// It also supports bidirectional streaming of requests.
|
// It also supports bidirectional streaming of requests.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
Name() string
|
Name() string
|
||||||
Init(opts ...Option) error
|
Init(opts ...Option) error
|
||||||
Options() Options
|
Options() Options
|
||||||
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
|
|
||||||
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
||||||
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||||
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||||
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
|
||||||
BatchPublish(ctx context.Context, msg []Message, opts ...PublishOption) error
|
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message is the interface for publishing asynchronously
|
type (
|
||||||
type Message interface {
|
FuncCall func(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||||
Topic() string
|
HookCall func(next FuncCall) FuncCall
|
||||||
Payload() interface{}
|
FuncStream func(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||||
ContentType() string
|
HookStream func(next FuncStream) FuncStream
|
||||||
Metadata() metadata.Metadata
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Request is the interface for a synchronous request used by Call or Stream
|
// Request is the interface for a synchronous request used by Call or Stream
|
||||||
type Request interface {
|
type Request interface {
|
||||||
@@ -74,7 +69,7 @@ type Request interface {
|
|||||||
type Response interface {
|
type Response interface {
|
||||||
// Read the response
|
// Read the response
|
||||||
Codec() codec.Codec
|
Codec() codec.Codec
|
||||||
// read the header
|
// Header data
|
||||||
Header() metadata.Metadata
|
Header() metadata.Metadata
|
||||||
// Read the undecoded response
|
// Read the undecoded response
|
||||||
Read() ([]byte, error)
|
Read() ([]byte, error)
|
||||||
@@ -110,11 +105,5 @@ type Option func(*Options)
|
|||||||
// CallOption used by Call or Stream
|
// CallOption used by Call or Stream
|
||||||
type CallOption func(*CallOptions)
|
type CallOption func(*CallOptions)
|
||||||
|
|
||||||
// PublishOption used by Publish
|
|
||||||
type PublishOption func(*PublishOptions)
|
|
||||||
|
|
||||||
// MessageOption used by NewMessage
|
|
||||||
type MessageOption func(*MessageOptions)
|
|
||||||
|
|
||||||
// RequestOption used by NewRequest
|
// RequestOption used by NewRequest
|
||||||
type RequestOption func(*RequestOptions)
|
type RequestOption func(*RequestOptions)
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewClientCallOptions(t *testing.T) {
|
|
||||||
var flag bool
|
|
||||||
w := func(fn CallFunc) CallFunc {
|
|
||||||
flag = true
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
c := NewClientCallOptions(NewClient(),
|
|
||||||
WithAddress("127.0.0.1"),
|
|
||||||
WithCallWrapper(w),
|
|
||||||
WithRequestTimeout(1*time.Millisecond),
|
|
||||||
WithRetries(0),
|
|
||||||
WithBackoff(BackoffInterval(10*time.Millisecond, 100*time.Millisecond)),
|
|
||||||
)
|
|
||||||
_ = c.Call(context.TODO(), c.NewRequest("service", "endpoint", nil), nil)
|
|
||||||
if !flag {
|
|
||||||
t.Fatalf("NewClientCallOptions not works")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Client, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext get client from context
|
||||||
|
func MustContext(ctx context.Context) Client {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing client")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put client in context
|
// NewContext put client in context
|
||||||
func NewContext(ctx context.Context, c Client) context.Context {
|
func NewContext(ctx context.Context, c Client) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -23,16 +32,6 @@ func NewContext(ctx context.Context, c Client) context.Context {
|
|||||||
return context.WithValue(ctx, clientKey{}, c)
|
return context.WithValue(ctx, clientKey{}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPublishOption returns a function to setup a context with given value
|
|
||||||
func SetPublishOption(k, v interface{}) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCallOption returns a function to setup a context with given value
|
// SetCallOption returns a function to setup a context with given value
|
||||||
func SetCallOption(k, v interface{}) CallOption {
|
func SetCallOption(k, v interface{}) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
|
@@ -14,23 +14,28 @@ func TestFromContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
c, ok := FromContext(nil)
|
||||||
|
if ok || c != nil {
|
||||||
|
t.Fatal("FromContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewContext(t *testing.T) {
|
func TestNewContext(t *testing.T) {
|
||||||
ctx := NewContext(context.TODO(), NewClient())
|
ctx := NewContext(context.TODO(), NewClient())
|
||||||
|
|
||||||
c, ok := FromContext(ctx)
|
c, ok := FromContext(ctx)
|
||||||
if c == nil || !ok {
|
if c == nil || !ok {
|
||||||
t.Fatal("NewContext not works")
|
t.Fatal("NewContext not works")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetPublishOption(t *testing.T) {
|
func TestNewNilContext(t *testing.T) {
|
||||||
type key struct{}
|
// nolint: staticcheck
|
||||||
o := SetPublishOption(key{}, "test")
|
ctx := NewContext(nil, NewClient())
|
||||||
opts := &PublishOptions{}
|
c, ok := FromContext(ctx)
|
||||||
o(opts)
|
if c == nil || !ok {
|
||||||
|
t.Fatal("NewContext not works")
|
||||||
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
|
|
||||||
t.Fatal("SetPublishOption not works")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/errors"
|
"go.unistack.org/micro/v4/errors"
|
||||||
"go.unistack.org/micro/v3/router"
|
"go.unistack.org/micro/v4/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LookupFunc is used to lookup routes for a service
|
// LookupFunc is used to lookup routes for a service
|
||||||
|
284
client/noop.go
284
client/noop.go
@@ -3,12 +3,16 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/broker"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/errors"
|
||||||
"go.unistack.org/micro/v3/errors"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/options"
|
||||||
|
"go.unistack.org/micro/v4/selector"
|
||||||
|
"go.unistack.org/micro/v4/semconv"
|
||||||
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultCodecs will be used to encode/decode data
|
// DefaultCodecs will be used to encode/decode data
|
||||||
@@ -17,13 +21,9 @@ var DefaultCodecs = map[string]codec.Codec{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type noopClient struct {
|
type noopClient struct {
|
||||||
opts Options
|
funcCall FuncCall
|
||||||
}
|
funcStream FuncStream
|
||||||
|
opts Options
|
||||||
type noopMessage struct {
|
|
||||||
topic string
|
|
||||||
payload interface{}
|
|
||||||
opts MessageOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type noopRequest struct {
|
type noopRequest struct {
|
||||||
@@ -38,16 +38,12 @@ type noopRequest struct {
|
|||||||
|
|
||||||
// NewClient returns new noop client
|
// NewClient returns new noop client
|
||||||
func NewClient(opts ...Option) Client {
|
func NewClient(opts ...Option) Client {
|
||||||
nc := &noopClient{opts: NewOptions(opts...)}
|
n := &noopClient{opts: NewOptions(opts...)}
|
||||||
// wrap in reverse
|
|
||||||
|
|
||||||
c := Client(nc)
|
n.funcCall = n.fnCall
|
||||||
|
n.funcStream = n.fnStream
|
||||||
|
|
||||||
for i := len(nc.opts.Wrappers); i > 0; i-- {
|
return n
|
||||||
c = nc.opts.Wrappers[i-1](c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Name() string {
|
func (n *noopClient) Name() string {
|
||||||
@@ -99,10 +95,13 @@ func (n *noopResponse) Read() ([]byte, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type noopStream struct{}
|
type noopStream struct {
|
||||||
|
err error
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
func (n *noopStream) Context() context.Context {
|
func (n *noopStream) Context() context.Context {
|
||||||
return context.Background()
|
return n.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Request() Request {
|
func (n *noopStream) Request() Request {
|
||||||
@@ -130,47 +129,40 @@ func (n *noopStream) RecvMsg(interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Error() error {
|
func (n *noopStream) Error() error {
|
||||||
return nil
|
return n.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Close() error {
|
func (n *noopStream) Close() error {
|
||||||
return nil
|
if sp, ok := tracer.SpanFromContext(n.ctx); ok && sp != nil {
|
||||||
|
if n.err != nil {
|
||||||
|
sp.SetStatus(tracer.SpanStatusError, n.err.Error())
|
||||||
|
}
|
||||||
|
sp.Finish()
|
||||||
|
}
|
||||||
|
return n.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) CloseSend() error {
|
func (n *noopStream) CloseSend() error {
|
||||||
return nil
|
return n.err
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopMessage) Topic() string {
|
|
||||||
return n.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopMessage) Payload() interface{} {
|
|
||||||
return n.payload
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopMessage) ContentType() string {
|
|
||||||
return n.opts.ContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopMessage) Metadata() metadata.Metadata {
|
|
||||||
return n.opts.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
|
|
||||||
if cf, ok := n.opts.Codecs[contentType]; ok {
|
|
||||||
return cf, nil
|
|
||||||
}
|
|
||||||
if cf, ok := DefaultCodecs[contentType]; ok {
|
|
||||||
return cf, nil
|
|
||||||
}
|
|
||||||
return nil, codec.ErrUnknownContentType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Init(opts ...Option) error {
|
func (n *noopClient) Init(opts ...Option) error {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&n.opts)
|
o(&n.opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.funcCall = n.fnCall
|
||||||
|
n.funcStream = n.fnStream
|
||||||
|
|
||||||
|
n.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
|
switch h := hook.(type) {
|
||||||
|
case HookCall:
|
||||||
|
n.funcCall = h(n.funcCall)
|
||||||
|
case HookStream:
|
||||||
|
n.funcStream = h(n.funcStream)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +175,31 @@ func (n *noopClient) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
||||||
|
ts := time.Now()
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
||||||
|
var sp tracer.Span
|
||||||
|
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
||||||
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
||||||
|
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
||||||
|
)
|
||||||
|
err := n.funcCall(ctx, req, rsp, opts...)
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
|
||||||
|
te := time.Since(ts)
|
||||||
|
n.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
||||||
|
n.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
||||||
|
|
||||||
|
if me := errors.FromError(err); me == nil {
|
||||||
|
sp.Finish()
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
|
||||||
|
} else {
|
||||||
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
||||||
// make a copy of call opts
|
// make a copy of call opts
|
||||||
callOpts := n.opts.CallOptions
|
callOpts := n.opts.CallOptions
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -211,11 +228,8 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make copy of call method
|
// make copy of call method
|
||||||
hcall := n.call
|
hcall := func(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
|
||||||
|
return nil
|
||||||
// wrap the call in reverse
|
|
||||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
|
||||||
hcall = callOpts.CallWrappers[i-1](hcall)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the router passed as a call option, or fallback to the rpc clients router
|
// use the router passed as a call option, or fallback to the rpc clients router
|
||||||
@@ -233,25 +247,14 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
callOpts.Address = []string{n.opts.Proxy}
|
callOpts.Address = []string{n.opts.Proxy}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup the route to send the reques to
|
var next selector.Next
|
||||||
// TODO apply any filtering here
|
|
||||||
routes, err := n.opts.Lookup(ctx, req, callOpts)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// balance the list of nodes
|
|
||||||
next, err := callOpts.Selector.Select(routes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// return errors.New("go.micro.client", "request timeout", 408)
|
// return errors.New("go.micro.client", "request timeout", 408)
|
||||||
call := func(i int) error {
|
call := func(i int) error {
|
||||||
// call backoff first. Someone may want an initial start delay
|
// call backoff first. Someone may want an initial start delay
|
||||||
t, err := callOpts.Backoff(ctx, req, i)
|
t, err := callOpts.Backoff(ctx, req, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -259,6 +262,22 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
time.Sleep(t)
|
time.Sleep(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if next == nil {
|
||||||
|
var routes []string
|
||||||
|
// lookup the route to send the reques to
|
||||||
|
// TODO apply any filtering here
|
||||||
|
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// balance the list of nodes
|
||||||
|
next, err = callOpts.Selector.Select(routes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node := next()
|
node := next()
|
||||||
|
|
||||||
// make the call
|
// make the call
|
||||||
@@ -309,20 +328,38 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
return gerr
|
return gerr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) call(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
|
func (n *noopClient) NewRequest(service, endpoint string, _ interface{}, _ ...RequestOption) Request {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts ...RequestOption) Request {
|
|
||||||
return &noopRequest{service: service, endpoint: endpoint}
|
return &noopRequest{service: service, endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
|
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
||||||
options := NewMessageOptions(append([]MessageOption{MessageContentType(n.opts.ContentType)}, opts...)...)
|
ts := time.Now()
|
||||||
return &noopMessage{topic: topic, payload: msg, opts: options}
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
||||||
|
var sp tracer.Span
|
||||||
|
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
||||||
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
||||||
|
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
||||||
|
)
|
||||||
|
stream, err := n.funcStream(ctx, req, opts...)
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
|
||||||
|
te := time.Since(ts)
|
||||||
|
n.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
||||||
|
n.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
||||||
|
|
||||||
|
if me := errors.FromError(err); me == nil {
|
||||||
|
sp.Finish()
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
|
||||||
|
} else {
|
||||||
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
||||||
|
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
// make a copy of call opts
|
// make a copy of call opts
|
||||||
callOpts := n.opts.CallOptions
|
callOpts := n.opts.CallOptions
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -374,24 +411,13 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
callOpts.Address = []string{n.opts.Proxy}
|
callOpts.Address = []string{n.opts.Proxy}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup the route to send the reques to
|
var next selector.Next
|
||||||
// TODO apply any filtering here
|
|
||||||
routes, err := n.opts.Lookup(ctx, req, callOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// balance the list of nodes
|
|
||||||
next, err := callOpts.Selector.Select(routes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
call := func(i int) (Stream, error) {
|
call := func(i int) (Stream, error) {
|
||||||
// call backoff first. Someone may want an initial start delay
|
// call backoff first. Someone may want an initial start delay
|
||||||
t, cerr := callOpts.Backoff(ctx, req, i)
|
t, cerr := callOpts.Backoff(ctx, req, i)
|
||||||
if cerr != nil {
|
if cerr != nil {
|
||||||
return nil, errors.InternalServerError("go.micro.client", cerr.Error())
|
return nil, errors.InternalServerError("go.micro.client", "%s", cerr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -399,6 +425,22 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
time.Sleep(t)
|
time.Sleep(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if next == nil {
|
||||||
|
var routes []string
|
||||||
|
// lookup the route to send the reques to
|
||||||
|
// TODO apply any filtering here
|
||||||
|
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// balance the list of nodes
|
||||||
|
next, err = callOpts.Selector.Select(routes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node := next()
|
node := next()
|
||||||
|
|
||||||
stream, cerr := n.stream(ctx, node, req, callOpts)
|
stream, cerr := n.stream(ctx, node, req, callOpts)
|
||||||
@@ -455,64 +497,6 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
return nil, grr
|
return nil, grr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (Stream, error) {
|
func (n *noopClient) stream(ctx context.Context, _ string, _ Request, _ CallOptions) (Stream, error) {
|
||||||
return &noopStream{}, nil
|
return &noopStream{ctx: ctx}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
|
||||||
return n.publish(ctx, ps, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
|
|
||||||
return n.publish(ctx, []Message{p}, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
|
||||||
options := NewPublishOptions(opts...)
|
|
||||||
|
|
||||||
msgs := make([]*broker.Message, 0, len(ps))
|
|
||||||
|
|
||||||
for _, p := range ps {
|
|
||||||
md, ok := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
md = metadata.New(0)
|
|
||||||
}
|
|
||||||
md[metadata.HeaderContentType] = p.ContentType()
|
|
||||||
|
|
||||||
topic := p.Topic()
|
|
||||||
|
|
||||||
// get the exchange
|
|
||||||
if len(options.Exchange) > 0 {
|
|
||||||
topic = options.Exchange
|
|
||||||
}
|
|
||||||
|
|
||||||
md[metadata.HeaderTopic] = topic
|
|
||||||
|
|
||||||
var body []byte
|
|
||||||
|
|
||||||
// passed in raw data
|
|
||||||
if d, ok := p.Payload().(*codec.Frame); ok {
|
|
||||||
body = d.Data
|
|
||||||
} else {
|
|
||||||
// use codec for payload
|
|
||||||
cf, err := n.newCodec(p.ContentType())
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the body
|
|
||||||
b, err := cf.Marshal(p.Payload())
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
}
|
|
||||||
body = b
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs = append(msgs, &broker.Message{Header: md, Body: body})
|
|
||||||
}
|
|
||||||
|
|
||||||
return n.opts.Broker.BatchPublish(ctx, msgs,
|
|
||||||
broker.PublishContext(options.Context),
|
|
||||||
broker.PublishBodyOnly(options.BodyOnly),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@@ -3,25 +3,34 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/broker"
|
"go.unistack.org/micro/v4/broker"
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v3/network/transport"
|
"go.unistack.org/micro/v4/options"
|
||||||
"go.unistack.org/micro/v3/register"
|
"go.unistack.org/micro/v4/register"
|
||||||
"go.unistack.org/micro/v3/router"
|
"go.unistack.org/micro/v4/router"
|
||||||
"go.unistack.org/micro/v3/selector"
|
"go.unistack.org/micro/v4/selector"
|
||||||
"go.unistack.org/micro/v3/selector/random"
|
"go.unistack.org/micro/v4/selector/random"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options holds client options
|
// Options holds client options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Transport used for transfer messages
|
// Codecs map
|
||||||
Transport transport.Transport
|
Codecs map[string]codec.Codec
|
||||||
|
|
||||||
|
// Proxy is used for proxy requests
|
||||||
|
Proxy string
|
||||||
|
// ContentType is used to select codec
|
||||||
|
ContentType string
|
||||||
|
// Name is the client name
|
||||||
|
Name string
|
||||||
|
|
||||||
// Selector used to select needed address
|
// Selector used to select needed address
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Logger used to log messages
|
// Logger used to log messages
|
||||||
@@ -36,22 +45,24 @@ type Options struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// Router used to get route
|
// Router used to get route
|
||||||
Router router.Router
|
Router router.Router
|
||||||
|
|
||||||
// TLSConfig specifies tls.Config for secure connection
|
// TLSConfig specifies tls.Config for secure connection
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
// Codecs map
|
|
||||||
Codecs map[string]codec.Codec
|
|
||||||
// Lookup func used to get destination addr
|
// Lookup func used to get destination addr
|
||||||
Lookup LookupFunc
|
Lookup LookupFunc
|
||||||
// Proxy is used for proxy requests
|
// ContextDialer used to connect
|
||||||
Proxy string
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
// ContentType is used to select codec
|
|
||||||
ContentType string
|
|
||||||
// Name is the client name
|
|
||||||
Name string
|
|
||||||
// Wrappers contains wrappers
|
// Wrappers contains wrappers
|
||||||
Wrappers []Wrapper
|
Wrappers []Wrapper
|
||||||
|
// Hooks can be run before broker Publish/BatchPublish and
|
||||||
|
// Subscribe/BatchSubscribe methods
|
||||||
|
Hooks options.Hooks
|
||||||
|
|
||||||
// CallOptions contains default CallOptions
|
// CallOptions contains default CallOptions
|
||||||
CallOptions CallOptions
|
CallOptions CallOptions
|
||||||
|
|
||||||
// PoolSize connection pool size
|
// PoolSize connection pool size
|
||||||
PoolSize int
|
PoolSize int
|
||||||
// PoolTTL connection pool ttl
|
// PoolTTL connection pool ttl
|
||||||
@@ -69,6 +80,16 @@ func NewCallOptions(opts ...CallOption) CallOptions {
|
|||||||
|
|
||||||
// CallOptions holds client call options
|
// CallOptions holds client call options
|
||||||
type CallOptions struct {
|
type CallOptions struct {
|
||||||
|
// RequestMetadata holds additional metadata for call
|
||||||
|
RequestMetadata metadata.Metadata
|
||||||
|
|
||||||
|
// Network name
|
||||||
|
Network string
|
||||||
|
// Content-Type
|
||||||
|
ContentType string
|
||||||
|
// AuthToken string
|
||||||
|
AuthToken string
|
||||||
|
|
||||||
// Selector selects addr
|
// Selector selects addr
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Context used for deadline
|
// Context used for deadline
|
||||||
@@ -76,31 +97,39 @@ type CallOptions struct {
|
|||||||
// Router used for route
|
// Router used for route
|
||||||
Router router.Router
|
Router router.Router
|
||||||
// Retry func used for retries
|
// Retry func used for retries
|
||||||
|
|
||||||
|
// ResponseMetadata holds additional metadata from call
|
||||||
|
ResponseMetadata *metadata.Metadata
|
||||||
|
|
||||||
Retry RetryFunc
|
Retry RetryFunc
|
||||||
// Backoff func used for backoff when retry
|
// Backoff func used for backoff when retry
|
||||||
Backoff BackoffFunc
|
Backoff BackoffFunc
|
||||||
// Network name
|
// ContextDialer used to connect
|
||||||
Network string
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
// Content-Type
|
|
||||||
ContentType string
|
|
||||||
// AuthToken string
|
|
||||||
AuthToken string
|
|
||||||
// Address specifies static addr list
|
// Address specifies static addr list
|
||||||
Address []string
|
Address []string
|
||||||
// SelectOptions selector options
|
// SelectOptions selector options
|
||||||
SelectOptions []selector.SelectOption
|
SelectOptions []selector.SelectOption
|
||||||
// CallWrappers call wrappers
|
|
||||||
CallWrappers []CallWrapper
|
|
||||||
// StreamTimeout stream timeout
|
// StreamTimeout stream timeout
|
||||||
StreamTimeout time.Duration
|
StreamTimeout time.Duration
|
||||||
// RequestTimeout request timeout
|
// RequestTimeout request timeout
|
||||||
RequestTimeout time.Duration
|
RequestTimeout time.Duration
|
||||||
|
|
||||||
// DialTimeout dial timeout
|
// DialTimeout dial timeout
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
// Retries specifies retries num
|
// Retries specifies retries num
|
||||||
Retries int
|
Retries int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContextDialer pass ContextDialer to client
|
||||||
|
func ContextDialer(fn func(context.Context, string) (net.Conn, error)) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ContextDialer = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Context pass context to client
|
// Context pass context to client
|
||||||
func Context(ctx context.Context) Option {
|
func Context(ctx context.Context) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -108,43 +137,6 @@ func Context(ctx context.Context) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublishOptions create new PublishOptions struct from option
|
|
||||||
func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
|
||||||
options := PublishOptions{}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishOptions holds publish options
|
|
||||||
type PublishOptions struct {
|
|
||||||
// Context used for external options
|
|
||||||
Context context.Context
|
|
||||||
// Exchange topic exchange name
|
|
||||||
Exchange string
|
|
||||||
// BodyOnly will publish only message body
|
|
||||||
BodyOnly bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMessageOptions creates message options struct
|
|
||||||
func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
|
||||||
options := MessageOptions{Metadata: metadata.New(1)}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageOptions holds client message options
|
|
||||||
type MessageOptions struct {
|
|
||||||
// Metadata additional metadata
|
|
||||||
Metadata metadata.Metadata
|
|
||||||
// ContentType specify content-type of message
|
|
||||||
// deprecated
|
|
||||||
ContentType string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequestOptions creates new RequestOptions struct
|
// NewRequestOptions creates new RequestOptions struct
|
||||||
func NewRequestOptions(opts ...RequestOption) RequestOptions {
|
func NewRequestOptions(opts ...RequestOption) RequestOptions {
|
||||||
options := RequestOptions{}
|
options := RequestOptions{}
|
||||||
@@ -169,25 +161,23 @@ func NewOptions(opts ...Option) Options {
|
|||||||
options := Options{
|
options := Options{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
ContentType: DefaultContentType,
|
ContentType: DefaultContentType,
|
||||||
Codecs: make(map[string]codec.Codec),
|
Codecs: DefaultCodecs,
|
||||||
CallOptions: CallOptions{
|
CallOptions: CallOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Backoff: DefaultBackoff,
|
Backoff: DefaultBackoff,
|
||||||
Retry: DefaultRetry,
|
Retry: DefaultRetry,
|
||||||
Retries: DefaultRetries,
|
Retries: DefaultRetries,
|
||||||
RequestTimeout: DefaultRequestTimeout,
|
RequestTimeout: DefaultRequestTimeout,
|
||||||
DialTimeout: transport.DefaultDialTimeout,
|
|
||||||
},
|
},
|
||||||
Lookup: LookupRoute,
|
Lookup: LookupRoute,
|
||||||
PoolSize: DefaultPoolSize,
|
PoolSize: DefaultPoolSize,
|
||||||
PoolTTL: DefaultPoolTTL,
|
PoolTTL: DefaultPoolTTL,
|
||||||
Selector: random.NewSelector(),
|
Selector: random.NewSelector(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Broker: broker.DefaultBroker,
|
Broker: broker.DefaultBroker,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
Router: router.DefaultRouter,
|
Router: router.DefaultRouter,
|
||||||
Transport: transport.DefaultTransport,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -260,13 +250,6 @@ func PoolTTL(d time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transport to use for communication e.g http, rabbitmq, etc
|
|
||||||
func Transport(t transport.Transport) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Transport = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register sets the routers register
|
// Register sets the routers register
|
||||||
func Register(r register.Register) Option {
|
func Register(r register.Register) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -290,20 +273,6 @@ func Selector(s selector.Selector) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap adds a wrapper to the list of options passed into the client
|
|
||||||
func Wrap(w Wrapper) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Wrappers = append(o.Wrappers, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapCall adds a wrapper to the list of CallFunc wrappers
|
|
||||||
func WrapCall(cw ...CallWrapper) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.CallOptions.CallWrappers = append(o.CallOptions.CallWrappers, cw...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backoff is used to set the backoff function used when retrying Calls
|
// Backoff is used to set the backoff function used when retrying Calls
|
||||||
func Backoff(fn BackoffFunc) Option {
|
func Backoff(fn BackoffFunc) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -330,14 +299,6 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
// set the internal tls
|
// set the internal tls
|
||||||
o.TLSConfig = t
|
o.TLSConfig = t
|
||||||
|
|
||||||
// set the default transport if one is not
|
|
||||||
// already set. Required for Init call below.
|
|
||||||
|
|
||||||
// set the transport tls
|
|
||||||
_ = o.Transport.Init(
|
|
||||||
transport.TLSConfig(t),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,40 +337,10 @@ func DialTimeout(d time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithExchange sets the exchange to route a message through
|
// WithContextDialer pass ContextDialer to client call
|
||||||
// Deprecated
|
func WithContextDialer(fn func(context.Context, string) (net.Conn, error)) CallOption {
|
||||||
func WithExchange(e string) PublishOption {
|
return func(o *CallOptions) {
|
||||||
return func(o *PublishOptions) {
|
o.ContextDialer = fn
|
||||||
o.Exchange = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishExchange sets the exchange to route a message through
|
|
||||||
func PublishExchange(e string) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
o.Exchange = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBodyOnly publish only message body
|
|
||||||
// DERECATED
|
|
||||||
func WithBodyOnly(b bool) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
o.BodyOnly = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishBodyOnly publish only message body
|
|
||||||
func PublishBodyOnly(b bool) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
o.BodyOnly = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishContext sets the context in publish options
|
|
||||||
func PublishContext(ctx context.Context) PublishOption {
|
|
||||||
return func(o *PublishOptions) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,13 +358,6 @@ func WithAddress(a ...string) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCallWrapper is a CallOption which adds to the existing CallFunc wrappers
|
|
||||||
func WithCallWrapper(cw ...CallWrapper) CallOption {
|
|
||||||
return func(o *CallOptions) {
|
|
||||||
o.CallWrappers = append(o.CallWrappers, cw...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBackoff is a CallOption which overrides that which
|
// WithBackoff is a CallOption which overrides that which
|
||||||
// set in Options.CallOptions
|
// set in Options.CallOptions
|
||||||
func WithBackoff(fn BackoffFunc) CallOption {
|
func WithBackoff(fn BackoffFunc) CallOption {
|
||||||
@@ -458,6 +382,20 @@ func WithRetries(i int) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithResponseMetadata is a CallOption which adds metadata.Metadata to Options.CallOptions
|
||||||
|
func WithResponseMetadata(md *metadata.Metadata) CallOption {
|
||||||
|
return func(o *CallOptions) {
|
||||||
|
o.ResponseMetadata = md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestMetadata is a CallOption which adds metadata.Metadata to Options.CallOptions
|
||||||
|
func WithRequestMetadata(md metadata.Metadata) CallOption {
|
||||||
|
return func(o *CallOptions) {
|
||||||
|
o.RequestMetadata = md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithRequestTimeout is a CallOption which overrides that which
|
// WithRequestTimeout is a CallOption which overrides that which
|
||||||
// set in Options.CallOptions
|
// set in Options.CallOptions
|
||||||
func WithRequestTimeout(d time.Duration) CallOption {
|
func WithRequestTimeout(d time.Duration) CallOption {
|
||||||
@@ -489,13 +427,6 @@ func WithAuthToken(t string) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithNetwork is a CallOption which sets the network attribute
|
|
||||||
func WithNetwork(n string) CallOption {
|
|
||||||
return func(o *CallOptions) {
|
|
||||||
o.Network = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRouter sets the router to use for this call
|
// WithRouter sets the router to use for this call
|
||||||
func WithRouter(r router.Router) CallOption {
|
func WithRouter(r router.Router) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
@@ -517,30 +448,6 @@ func WithSelectOptions(sops ...selector.SelectOption) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMessageContentType sets the message content type
|
|
||||||
// Deprecated
|
|
||||||
func WithMessageContentType(ct string) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
o.Metadata.Set(metadata.HeaderContentType, ct)
|
|
||||||
o.ContentType = ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageContentType sets the message content type
|
|
||||||
func MessageContentType(ct string) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
o.Metadata.Set(metadata.HeaderContentType, ct)
|
|
||||||
o.ContentType = ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageMetadata sets the message metadata
|
|
||||||
func MessageMetadata(k, v string) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
o.Metadata.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamingRequest specifies that request is streaming
|
// StreamingRequest specifies that request is streaming
|
||||||
func StreamingRequest(b bool) RequestOption {
|
func StreamingRequest(b bool) RequestOption {
|
||||||
return func(o *RequestOptions) {
|
return func(o *RequestOptions) {
|
||||||
@@ -554,3 +461,10 @@ func RequestContentType(ct string) RequestOption {
|
|||||||
o.ContentType = ct
|
o.ContentType = ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hooks sets hook runs before action
|
||||||
|
func Hooks(h ...options.Hook) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Hooks = append(o.Hooks, h...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/errors"
|
"go.unistack.org/micro/v4/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RetryFunc that returning either false or a non-nil error will result in the call not being retried
|
// RetryFunc that returning either false or a non-nil error will result in the call not being retried
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/errors"
|
"go.unistack.org/micro/v4/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRetryAlways(t *testing.T) {
|
func TestRetryAlways(t *testing.T) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testRequest struct {
|
type testRequest struct {
|
||||||
|
47
cluster/cluster.go
Normal file
47
cluster/cluster.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message sent to member in cluster
|
||||||
|
type Message interface {
|
||||||
|
// Header returns message headers
|
||||||
|
Header() metadata.Metadata
|
||||||
|
// Body returns broker message may be []byte slice or some go struct or interface
|
||||||
|
Body() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node interface {
|
||||||
|
// Name returns node name
|
||||||
|
Name() string
|
||||||
|
// Address returns node address
|
||||||
|
Address() string
|
||||||
|
// Metadata returns node metadata
|
||||||
|
Metadata() metadata.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster interface used for cluster communication across nodes
|
||||||
|
type Cluster interface {
|
||||||
|
// Join is used to take an existing members and performing state sync
|
||||||
|
Join(ctx context.Context, addr ...string) error
|
||||||
|
// Leave broadcast a leave message and stop listeners
|
||||||
|
Leave(ctx context.Context) error
|
||||||
|
// Ping is used to probe live status of the node
|
||||||
|
Ping(ctx context.Context, node Node, payload []byte) error
|
||||||
|
// Members returns the cluster members
|
||||||
|
Members() ([]Node, error)
|
||||||
|
// Broadcast send message for all members in cluster, if filter is not nil, nodes may be filtered
|
||||||
|
// by key/value pairs
|
||||||
|
Broadcast(ctx context.Context, msg Message, filter ...string) error
|
||||||
|
// Unicast send message to single member in cluster
|
||||||
|
Unicast(ctx context.Context, node Node, msg Message) error
|
||||||
|
// Live returns cluster liveness
|
||||||
|
Live() bool
|
||||||
|
// Ready returns cluster readiness
|
||||||
|
Ready() bool
|
||||||
|
// Health returns cluster health
|
||||||
|
Health() bool
|
||||||
|
}
|
101
codec/codec.go
101
codec/codec.go
@@ -1,19 +1,8 @@
|
|||||||
// Package codec is an interface for encoding messages
|
// Package codec is an interface for encoding messages
|
||||||
package codec // import "go.unistack.org/micro/v3/codec"
|
package codec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message types
|
|
||||||
const (
|
|
||||||
Error MessageType = iota
|
|
||||||
Request
|
|
||||||
Response
|
|
||||||
Event
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -24,63 +13,63 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultMaxMsgSize specifies how much data codec can handle
|
|
||||||
DefaultMaxMsgSize = 1024 * 1024 * 4 // 4Mb
|
|
||||||
// DefaultCodec is the global default codec
|
// DefaultCodec is the global default codec
|
||||||
DefaultCodec Codec = NewCodec()
|
DefaultCodec = NewCodec()
|
||||||
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
||||||
DefaultTagName = "codec"
|
DefaultTagName = "codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageType specifies message type for codec
|
// Codec encodes/decodes various types of messages.
|
||||||
type MessageType int
|
|
||||||
|
|
||||||
// Codec encodes/decodes various types of messages used within micro.
|
|
||||||
// ReadHeader and ReadBody are called in pairs to read requests/responses
|
|
||||||
// from the connection. Close is called when finished with the
|
|
||||||
// connection. ReadBody may be called with a nil argument to force the
|
|
||||||
// body to be read and discarded.
|
|
||||||
type Codec interface {
|
type Codec interface {
|
||||||
ReadHeader(r io.Reader, m *Message, mt MessageType) error
|
|
||||||
ReadBody(r io.Reader, v interface{}) error
|
|
||||||
Write(w io.Writer, m *Message, v interface{}) error
|
|
||||||
Marshal(v interface{}, opts ...Option) ([]byte, error)
|
Marshal(v interface{}, opts ...Option) ([]byte, error)
|
||||||
Unmarshal(b []byte, v interface{}, opts ...Option) error
|
Unmarshal(b []byte, v interface{}, opts ...Option) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message represents detailed information about
|
type CodecV2 interface {
|
||||||
// the communication, likely followed by the body.
|
Marshal(buf []byte, v interface{}, opts ...Option) ([]byte, error)
|
||||||
// In the case of an error, body may be nil.
|
Unmarshal(buf []byte, v interface{}, opts ...Option) error
|
||||||
type Message struct {
|
String() string
|
||||||
Header metadata.Metadata
|
|
||||||
Target string
|
|
||||||
Method string
|
|
||||||
Endpoint string
|
|
||||||
Error string
|
|
||||||
ID string
|
|
||||||
Body []byte
|
|
||||||
Type MessageType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessage creates new codec message
|
// RawMessage is a raw encoded JSON value.
|
||||||
func NewMessage(t MessageType) *Message {
|
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
||||||
return &Message{Type: t, Header: metadata.New(0)}
|
type RawMessage []byte
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalAppend calls codec.Marshal(v) and returns the data appended to buf.
|
// MarshalJSON returns m as the JSON encoding of m.
|
||||||
// If codec implements MarshalAppend, that is called instead.
|
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
func MarshalAppend(buf []byte, c Codec, v interface{}, opts ...Option) ([]byte, error) {
|
if m == nil {
|
||||||
if nc, ok := c.(interface {
|
return []byte("null"), nil
|
||||||
MarshalAppend([]byte, interface{}, ...Option) ([]byte, error)
|
} else if len(*m) == 0 {
|
||||||
}); ok {
|
return []byte("null"), nil
|
||||||
return nc.MarshalAppend(buf, v, opts...)
|
|
||||||
}
|
}
|
||||||
|
return *m, nil
|
||||||
mbuf, err := c.Marshal(v, opts...)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// UnmarshalJSON sets *m to a copy of data.
|
||||||
}
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
return append(buf, mbuf...), nil
|
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML returns m as the JSON encoding of m.
|
||||||
|
func (m *RawMessage) MarshalYAML() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
} else if len(*m) == 0 {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return *m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML sets *m to a copy of data.
|
||||||
|
func (m *RawMessage) UnmarshalYAML(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("RawMessage UnmarshalYAML on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], data...)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Codec, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns codec from context
|
||||||
|
func MustContext(ctx context.Context) Codec {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing codec")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put codec in context
|
// NewContext put codec in context
|
||||||
func NewContext(ctx context.Context, c Codec) context.Context {
|
func NewContext(ctx context.Context, c Codec) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -20,6 +20,17 @@ func (m *Frame) UnmarshalJSON(data []byte) error {
|
|||||||
return m.Unmarshal(data)
|
return m.Unmarshal(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML returns frame data
|
||||||
|
func (m *Frame) MarshalYAML() ([]byte, error) {
|
||||||
|
return m.Marshal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML set frame data
|
||||||
|
func (m *Frame) UnmarshalYAML(data []byte) error {
|
||||||
|
m.Data = append((m.Data)[0:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProtoMessage noop func
|
// ProtoMessage noop func
|
||||||
func (m *Frame) ProtoMessage() {}
|
func (m *Frame) ProtoMessage() {}
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ syntax = "proto3";
|
|||||||
package micro.codec;
|
package micro.codec;
|
||||||
|
|
||||||
option cc_enable_arenas = true;
|
option cc_enable_arenas = true;
|
||||||
option go_package = "go.unistack.org/micro/v3/codec;codec";
|
option go_package = "go.unistack.org/micro/v4/codec;codec";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option java_outer_classname = "MicroCodec";
|
option java_outer_classname = "MicroCodec";
|
||||||
option java_package = "micro.codec";
|
option java_package = "micro.codec";
|
||||||
|
@@ -2,70 +2,14 @@ package codec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
|
codecpb "go.unistack.org/micro-proto/v4/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type noopCodec struct {
|
type noopCodec struct {
|
||||||
opts Options
|
opts Options
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *noopCodec) ReadHeader(conn io.Reader, m *Message, t MessageType) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *noopCodec) ReadBody(conn io.Reader, b interface{}) error {
|
|
||||||
// read bytes
|
|
||||||
buf, err := io.ReadAll(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := b.(type) {
|
|
||||||
case *string:
|
|
||||||
*v = string(buf)
|
|
||||||
case *[]byte:
|
|
||||||
*v = buf
|
|
||||||
case *Frame:
|
|
||||||
v.Data = buf
|
|
||||||
default:
|
|
||||||
return json.Unmarshal(buf, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *noopCodec) Write(conn io.Writer, m *Message, b interface{}) error {
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var v []byte
|
|
||||||
switch vb := b.(type) {
|
|
||||||
case *Frame:
|
|
||||||
v = vb.Data
|
|
||||||
case string:
|
|
||||||
v = []byte(vb)
|
|
||||||
case *string:
|
|
||||||
v = []byte(*vb)
|
|
||||||
case *[]byte:
|
|
||||||
v = *vb
|
|
||||||
case []byte:
|
|
||||||
v = vb
|
|
||||||
default:
|
|
||||||
var err error
|
|
||||||
v, err = json.Marshal(vb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err := conn.Write(v)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *noopCodec) String() string {
|
func (c *noopCodec) String() string {
|
||||||
return "noop"
|
return "noop"
|
||||||
}
|
}
|
||||||
@@ -91,8 +35,8 @@ func (c *noopCodec) Marshal(v interface{}, opts ...Option) ([]byte, error) {
|
|||||||
return ve, nil
|
return ve, nil
|
||||||
case *Frame:
|
case *Frame:
|
||||||
return ve.Data, nil
|
return ve.Data, nil
|
||||||
case *Message:
|
case *codecpb.Frame:
|
||||||
return ve.Body, nil
|
return ve.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(v)
|
return json.Marshal(v)
|
||||||
@@ -106,14 +50,17 @@ func (c *noopCodec) Unmarshal(d []byte, v interface{}, opts ...Option) error {
|
|||||||
case *string:
|
case *string:
|
||||||
*ve = string(d)
|
*ve = string(d)
|
||||||
return nil
|
return nil
|
||||||
|
case []byte:
|
||||||
|
copy(ve, d)
|
||||||
|
return nil
|
||||||
case *[]byte:
|
case *[]byte:
|
||||||
*ve = d
|
*ve = d
|
||||||
return nil
|
return nil
|
||||||
case *Frame:
|
case *Frame:
|
||||||
ve.Data = d
|
ve.Data = d
|
||||||
return nil
|
return nil
|
||||||
case *Message:
|
case *codecpb.Frame:
|
||||||
ve.Body = d
|
ve.Data = d
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNoopBytes(t *testing.T) {
|
func TestNoopBytesPtr(t *testing.T) {
|
||||||
req := []byte("test req")
|
req := []byte("test req")
|
||||||
rsp := make([]byte, len(req))
|
rsp := make([]byte, len(req))
|
||||||
|
|
||||||
@@ -19,6 +19,20 @@ func TestNoopBytes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoopBytes(t *testing.T) {
|
||||||
|
req := []byte("test req")
|
||||||
|
var rsp []byte
|
||||||
|
|
||||||
|
nc := NewCodec()
|
||||||
|
if err := nc.Unmarshal(req, &rsp); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(req, rsp) {
|
||||||
|
t.Fatalf("req not eq rsp: %s != %s", req, rsp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNoopString(t *testing.T) {
|
func TestNoopString(t *testing.T) {
|
||||||
req := []byte("test req")
|
req := []byte("test req")
|
||||||
var rsp string
|
var rsp string
|
||||||
|
@@ -3,9 +3,9 @@ package codec
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -23,15 +23,8 @@ type Options struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// TagName specifies tag name in struct to control codec
|
// TagName specifies tag name in struct to control codec
|
||||||
TagName string
|
TagName string
|
||||||
// MaxMsgSize specifies max messages size that reads by codec
|
// Flatten specifies that struct must be analyzed for flatten tag
|
||||||
MaxMsgSize int
|
Flatten bool
|
||||||
}
|
|
||||||
|
|
||||||
// MaxMsgSize sets the max message size
|
|
||||||
func MaxMsgSize(n int) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.MaxMsgSize = n
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagName sets the codec tag name in struct
|
// TagName sets the codec tag name in struct
|
||||||
@@ -41,6 +34,13 @@ func TagName(n string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten enables checking for flatten tag name
|
||||||
|
func Flatten(b bool) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Flatten = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Logger sets the logger
|
// Logger sets the logger
|
||||||
func Logger(l logger.Logger) Option {
|
func Logger(l logger.Logger) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -65,12 +65,12 @@ func Meter(m meter.Meter) Option {
|
|||||||
// NewOptions returns new options
|
// NewOptions returns new options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
MaxMsgSize: DefaultMaxMsgSize,
|
TagName: DefaultTagName,
|
||||||
TagName: DefaultTagName,
|
Flatten: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
107
config/config.go
107
config/config.go
@@ -1,5 +1,5 @@
|
|||||||
// Package config is an interface for dynamic configuration.
|
// Package config is an interface for dynamic configuration.
|
||||||
package config // import "go.unistack.org/micro/v3/config"
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -50,6 +50,13 @@ type Config interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
FuncLoad func(ctx context.Context, opts ...LoadOption) error
|
||||||
|
HookLoad func(next FuncLoad) FuncLoad
|
||||||
|
FuncSave func(ctx context.Context, opts ...SaveOption) error
|
||||||
|
HookSave func(next FuncSave) FuncSave
|
||||||
|
)
|
||||||
|
|
||||||
// Watcher is the config watcher
|
// Watcher is the config watcher
|
||||||
type Watcher interface {
|
type Watcher interface {
|
||||||
// Next blocks until update happens or error returned
|
// Next blocks until update happens or error returned
|
||||||
@@ -124,35 +131,14 @@ func Validate(ctx context.Context, cfg interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultAfterLoad default func that runs after config load
|
// DefaultBeforeLoad default func that runs before config Load
|
||||||
DefaultAfterLoad = func(ctx context.Context, c Config) error {
|
|
||||||
for _, fn := range c.Options().AfterLoad {
|
|
||||||
if err := fn(ctx, c); err != nil {
|
|
||||||
c.Options().Logger.Errorf(ctx, "%s AfterLoad err: %v", c.String(), err)
|
|
||||||
if !c.Options().AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// DefaultAfterSave default func that runs after config save
|
|
||||||
DefaultAfterSave = func(ctx context.Context, c Config) error {
|
|
||||||
for _, fn := range c.Options().AfterSave {
|
|
||||||
if err := fn(ctx, c); err != nil {
|
|
||||||
c.Options().Logger.Errorf(ctx, "%s AfterSave err: %v", c.String(), err)
|
|
||||||
if !c.Options().AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// DefaultBeforeLoad default func that runs before config load
|
|
||||||
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
|
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().BeforeLoad {
|
for _, fn := range c.Options().BeforeLoad {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Errorf(ctx, "%s BeforeLoad err: %v", c.String(), err)
|
c.Options().Logger.Error(ctx, c.String()+" BeforeLoad error", err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -160,11 +146,74 @@ var (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// DefaultBeforeSave default func that runs befora config save
|
// DefaultAfterLoad default func that runs after config Load
|
||||||
|
DefaultAfterLoad = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().AfterLoad {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Error(ctx, c.String()+" AfterLoad error", err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// DefaultBeforeSave default func that runs befora config Save
|
||||||
DefaultBeforeSave = func(ctx context.Context, c Config) error {
|
DefaultBeforeSave = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().BeforeSave {
|
for _, fn := range c.Options().BeforeSave {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Errorf(ctx, "%s BeforeSave err: %v", c.String(), err)
|
c.Options().Logger.Error(ctx, c.String()+" BeforeSave error", err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// DefaultAfterSave default func that runs after config Save
|
||||||
|
DefaultAfterSave = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().AfterSave {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Error(ctx, c.String()+" AfterSave error", err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// DefaultBeforeInit default func that runs befora config Init
|
||||||
|
DefaultBeforeInit = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().BeforeInit {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Error(ctx, c.String()+" BeforeInit error", err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// DefaultAfterInit default func that runs after config Init
|
||||||
|
DefaultAfterInit = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().AfterSave {
|
||||||
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Error(ctx, c.String()+" AfterInit error", err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Config, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns store from context
|
||||||
|
func MustContext(ctx context.Context) Config {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing config")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put store in context
|
// NewContext put store in context
|
||||||
func NewContext(ctx context.Context, c Config) context.Context {
|
func NewContext(ctx context.Context, c Config) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -5,6 +5,24 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFromNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
c, ok := FromContext(nil)
|
||||||
|
if ok || c != nil {
|
||||||
|
t.Fatal("FromContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
ctx := NewContext(nil, NewConfig())
|
||||||
|
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if c == nil || !ok {
|
||||||
|
t.Fatal("NewContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFromContext(t *testing.T) {
|
func TestFromContext(t *testing.T) {
|
||||||
ctx := context.WithValue(context.TODO(), configKey{}, NewConfig())
|
ctx := context.WithValue(context.TODO(), configKey{}, NewConfig())
|
||||||
|
|
||||||
|
@@ -5,13 +5,20 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/imdario/mergo"
|
"dario.cat/mergo"
|
||||||
rutil "go.unistack.org/micro/v3/util/reflect"
|
"github.com/google/uuid"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
|
mid "go.unistack.org/micro/v4/util/id"
|
||||||
|
rutil "go.unistack.org/micro/v4/util/reflect"
|
||||||
|
mtime "go.unistack.org/micro/v4/util/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type defaultConfig struct {
|
type defaultConfig struct {
|
||||||
opts Options
|
funcLoad FuncLoad
|
||||||
|
funcSave FuncSave
|
||||||
|
opts Options
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Options() Options {
|
func (c *defaultConfig) Options() Options {
|
||||||
@@ -22,11 +29,42 @@ func (c *defaultConfig) Init(opts ...Option) error {
|
|||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&c.opts)
|
o(&c.opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := DefaultBeforeInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.funcLoad = c.fnLoad
|
||||||
|
c.funcSave = c.fnSave
|
||||||
|
|
||||||
|
c.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
|
switch h := hook.(type) {
|
||||||
|
case HookLoad:
|
||||||
|
c.funcLoad = h(c.funcLoad)
|
||||||
|
case HookSave:
|
||||||
|
c.funcSave = h(c.funcSave)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := DefaultAfterInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
||||||
if err := DefaultBeforeLoad(ctx, c); err != nil {
|
return c.funcLoad(ctx, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *defaultConfig) fnLoad(ctx context.Context, opts ...LoadOption) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if c.opts.SkipLoad != nil && c.opts.SkipLoad(ctx, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,21 +87,20 @@ func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
|||||||
if !c.opts.AllowFail {
|
if !c.opts.AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return DefaultAfterLoad(ctx, c)
|
if err = DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
|
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
|
||||||
err = mergo.Merge(dst, src, mopts...)
|
err = mergo.Merge(dst, src, mopts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil && !c.opts.AllowFail {
|
||||||
c.opts.Logger.Errorf(ctx, "default load error: %v", err)
|
return err
|
||||||
if !c.opts.AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DefaultAfterLoad(ctx, c); err != nil {
|
if err := DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +112,7 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
if !rutil.IsEmpty(value) {
|
if !rutil.IsEmpty(value) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch value.Kind() {
|
switch value.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
t := value.Type()
|
t := value.Type()
|
||||||
@@ -113,6 +151,20 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
value.Set(reflect.ValueOf(v))
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
|
switch val {
|
||||||
|
case "micro:generate uuid":
|
||||||
|
uid, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
val = uid.String()
|
||||||
|
case "micro:generate id":
|
||||||
|
uid, err := mid.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
val = uid
|
||||||
|
}
|
||||||
value.Set(reflect.ValueOf(val))
|
value.Set(reflect.ValueOf(val))
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
v, err := strconv.ParseFloat(val, 32)
|
v, err := strconv.ParseFloat(val, 32)
|
||||||
@@ -151,11 +203,26 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(int32(v)))
|
value.Set(reflect.ValueOf(int32(v)))
|
||||||
case reflect.Int64:
|
case reflect.Int64:
|
||||||
v, err := strconv.ParseInt(val, 10, 64)
|
switch {
|
||||||
if err != nil {
|
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "time":
|
||||||
return err
|
v, err := time.ParseDuration(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.Set(reflect.ValueOf(v))
|
||||||
|
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v4/util/time":
|
||||||
|
v, err := mtime.ParseDuration(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.SetInt(int64(v))
|
||||||
|
default:
|
||||||
|
v, err := strconv.ParseInt(val, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.Set(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
|
||||||
case reflect.Uint:
|
case reflect.Uint:
|
||||||
v, err := strconv.ParseUint(val, 10, 0)
|
v, err := strconv.ParseUint(val, 10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,6 +254,7 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
value.Set(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +318,14 @@ func fillValues(valueOf reflect.Value, tname string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
||||||
|
return c.funcSave(ctx, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *defaultConfig) fnSave(ctx context.Context, opts ...SaveOption) error {
|
||||||
|
if c.opts.SkipSave != nil && c.opts.SkipSave(ctx, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := DefaultBeforeSave(ctx, c); err != nil {
|
if err := DefaultBeforeSave(ctx, c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -269,7 +345,7 @@ func (c *defaultConfig) Name() string {
|
|||||||
return c.opts.Name
|
return c.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
func (c *defaultConfig) Watch(_ context.Context, _ ...WatchOption) (Watcher, error) {
|
||||||
return nil, ErrWatcherNotImplemented
|
return nil, ErrWatcherNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,5 +355,9 @@ func NewConfig(opts ...Option) Config {
|
|||||||
if len(options.StructTag) == 0 {
|
if len(options.StructTag) == 0 {
|
||||||
options.StructTag = "default"
|
options.StructTag = "default"
|
||||||
}
|
}
|
||||||
return &defaultConfig{opts: options}
|
c := &defaultConfig{opts: options}
|
||||||
|
c.funcLoad = c.fnLoad
|
||||||
|
c.funcSave = c.fnSave
|
||||||
|
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
@@ -3,16 +3,26 @@ package config_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/config"
|
"go.unistack.org/micro/v4/config"
|
||||||
|
mtime "go.unistack.org/micro/v4/util/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cfg struct {
|
type cfg struct {
|
||||||
|
MapValue map[string]bool `default:"key1=true,key2=false"`
|
||||||
|
StructValue *cfgStructValue
|
||||||
|
|
||||||
StringValue string `default:"string_value"`
|
StringValue string `default:"string_value"`
|
||||||
IgnoreValue string `json:"-"`
|
IgnoreValue string `json:"-"`
|
||||||
StructValue *cfgStructValue
|
UUIDValue string `default:"micro:generate uuid"`
|
||||||
IntValue int `default:"99"`
|
IDValue string `default:"micro:generate id"`
|
||||||
|
|
||||||
|
DurationValue time.Duration `default:"10s"`
|
||||||
|
MDurationValue mtime.Duration `default:"10s"`
|
||||||
|
IntValue int `default:"99"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type cfgStructValue struct {
|
type cfgStructValue struct {
|
||||||
@@ -33,6 +43,35 @@ func (c *cfgStructValue) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testHook struct {
|
||||||
|
f bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testHook) Load(fn config.FuncLoad) config.FuncLoad {
|
||||||
|
return func(ctx context.Context, opts ...config.LoadOption) error {
|
||||||
|
t.f = true
|
||||||
|
return fn(ctx, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHook(t *testing.T) {
|
||||||
|
h := &testHook{}
|
||||||
|
|
||||||
|
c := config.NewConfig(config.Struct(h), config.Hooks(config.HookLoad(h.Load)))
|
||||||
|
|
||||||
|
if err := c.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Load(context.TODO()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.f {
|
||||||
|
t.Fatal("hook not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefault(t *testing.T) {
|
func TestDefault(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
conf := &cfg{IntValue: 10}
|
conf := &cfg{IntValue: 10}
|
||||||
@@ -63,6 +102,19 @@ func TestDefault(t *testing.T) {
|
|||||||
if conf.StringValue != "after_load" {
|
if conf.StringValue != "after_load" {
|
||||||
t.Fatal("AfterLoad option not working")
|
t.Fatal("AfterLoad option not working")
|
||||||
}
|
}
|
||||||
|
if len(conf.MapValue) != 2 {
|
||||||
|
t.Fatalf("map value invalid: %#+v\n", conf.MapValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.UUIDValue == "" {
|
||||||
|
t.Fatalf("uuid value empty")
|
||||||
|
} else if len(conf.UUIDValue) != 36 {
|
||||||
|
t.Fatalf("uuid value invalid: %s", conf.UUIDValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.IDValue == "" {
|
||||||
|
t.Fatalf("id value empty")
|
||||||
|
}
|
||||||
_ = conf
|
_ = conf
|
||||||
// t.Logf("%#+v\n", conf)
|
// t.Logf("%#+v\n", conf)
|
||||||
}
|
}
|
||||||
@@ -82,3 +134,13 @@ func TestValidate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_SizeOf(t *testing.T) {
|
||||||
|
st := cfg{}
|
||||||
|
|
||||||
|
tVal := reflect.TypeOf(st)
|
||||||
|
for i := 0; i < tVal.NumField(); i++ {
|
||||||
|
field := tVal.Field(i)
|
||||||
|
fmt.Printf("Field: %s, Offset: %d, Size: %d\n", field.Name, field.Offset, field.Type.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v4/options"
|
||||||
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options hold the config options
|
// Options hold the config options
|
||||||
@@ -28,14 +29,26 @@ type Options struct {
|
|||||||
Name string
|
Name string
|
||||||
// StructTag name
|
// StructTag name
|
||||||
StructTag string
|
StructTag string
|
||||||
// BeforeSave contains slice of funcs that runs before save
|
// BeforeSave contains slice of funcs that runs before Save
|
||||||
BeforeSave []func(context.Context, Config) error
|
BeforeSave []func(context.Context, Config) error
|
||||||
// AfterLoad contains slice of funcs that runs after load
|
// AfterSave contains slice of funcs that runs after Save
|
||||||
AfterLoad []func(context.Context, Config) error
|
|
||||||
// BeforeLoad contains slice of funcs that runs before load
|
|
||||||
BeforeLoad []func(context.Context, Config) error
|
|
||||||
// AfterSave contains slice of funcs that runs after save
|
|
||||||
AfterSave []func(context.Context, Config) error
|
AfterSave []func(context.Context, Config) error
|
||||||
|
// BeforeLoad contains slice of funcs that runs before Load
|
||||||
|
BeforeLoad []func(context.Context, Config) error
|
||||||
|
// AfterLoad contains slice of funcs that runs after Load
|
||||||
|
AfterLoad []func(context.Context, Config) error
|
||||||
|
// BeforeInit contains slice of funcs that runs before Init
|
||||||
|
BeforeInit []func(context.Context, Config) error
|
||||||
|
// AfterInit contains slice of funcs that runs after Init
|
||||||
|
AfterInit []func(context.Context, Config) error
|
||||||
|
|
||||||
|
// SkipLoad runs only if condition returns true
|
||||||
|
SkipLoad func(context.Context, Config) bool
|
||||||
|
// SkipSave runs only if condition returns true
|
||||||
|
SkipSave func(context.Context, Config) bool
|
||||||
|
// Hooks can be run before/after config Save/Load
|
||||||
|
Hooks options.Hooks
|
||||||
|
|
||||||
// AllowFail flag to allow fail in config source
|
// AllowFail flag to allow fail in config source
|
||||||
AllowFail bool
|
AllowFail bool
|
||||||
}
|
}
|
||||||
@@ -64,9 +77,9 @@ type LoadOption func(o *LoadOptions)
|
|||||||
// LoadOptions struct
|
// LoadOptions struct
|
||||||
type LoadOptions struct {
|
type LoadOptions struct {
|
||||||
Struct interface{}
|
Struct interface{}
|
||||||
|
Context context.Context
|
||||||
Override bool
|
Override bool
|
||||||
Append bool
|
Append bool
|
||||||
Context context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoadOptions create LoadOptions struct with provided opts
|
// NewLoadOptions create LoadOptions struct with provided opts
|
||||||
@@ -131,6 +144,20 @@ func AllowFail(b bool) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeInit run funcs before config Init
|
||||||
|
func BeforeInit(fn ...func(context.Context, Config) error) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BeforeInit = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterInit run funcs after config Init
|
||||||
|
func AfterInit(fn ...func(context.Context, Config) error) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.AfterInit = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BeforeLoad run funcs before config load
|
// BeforeLoad run funcs before config load
|
||||||
func BeforeLoad(fn ...func(context.Context, Config) error) Option {
|
func BeforeLoad(fn ...func(context.Context, Config) error) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -253,10 +280,10 @@ func WatchCoalesce(b bool) WatchOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WatchInterval specifies min and max time.Duration for pulling changes
|
// WatchInterval specifies min and max time.Duration for pulling changes
|
||||||
func WatchInterval(min, max time.Duration) WatchOption {
|
func WatchInterval(minTime, maxTime time.Duration) WatchOption {
|
||||||
return func(o *WatchOptions) {
|
return func(o *WatchOptions) {
|
||||||
o.MinInterval = min
|
o.MinInterval = minTime
|
||||||
o.MaxInterval = max
|
o.MaxInterval = maxTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,3 +293,10 @@ func WatchStruct(src interface{}) WatchOption {
|
|||||||
o.Struct = src
|
o.Struct = src
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hooks sets hook runs before action
|
||||||
|
func Hooks(h ...options.Hook) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Hooks = append(o.Hooks, h...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
157
database/dsn.go
Normal file
157
database/dsn.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidDSNAddr = errors.New("invalid dsn addr")
|
||||||
|
ErrInvalidDSNUnescaped = errors.New("dsn must be escaped")
|
||||||
|
ErrInvalidDSNNoSlash = errors.New("dsn must contains slash")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Scheme string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Database string
|
||||||
|
Params []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) FormatDSN() string {
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
if len(cfg.Scheme) > 0 {
|
||||||
|
s.WriteString(cfg.Scheme + "://")
|
||||||
|
}
|
||||||
|
// [username[:password]@]
|
||||||
|
if len(cfg.Username) > 0 {
|
||||||
|
s.WriteString(cfg.Username)
|
||||||
|
if len(cfg.Password) > 0 {
|
||||||
|
s.WriteByte(':')
|
||||||
|
s.WriteString(url.PathEscape(cfg.Password))
|
||||||
|
}
|
||||||
|
s.WriteByte('@')
|
||||||
|
}
|
||||||
|
|
||||||
|
// [host:port]
|
||||||
|
if len(cfg.Host) > 0 {
|
||||||
|
s.WriteString(cfg.Host)
|
||||||
|
if len(cfg.Port) > 0 {
|
||||||
|
s.WriteByte(':')
|
||||||
|
s.WriteString(cfg.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /dbname
|
||||||
|
s.WriteByte('/')
|
||||||
|
s.WriteString(url.PathEscape(cfg.Database))
|
||||||
|
|
||||||
|
for i := 0; i < len(cfg.Params); i += 2 {
|
||||||
|
if i == 0 {
|
||||||
|
s.WriteString("?")
|
||||||
|
} else {
|
||||||
|
s.WriteString("&")
|
||||||
|
}
|
||||||
|
s.WriteString(cfg.Params[i])
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(cfg.Params[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseDSN(dsn string) (*Config, error) {
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN]
|
||||||
|
// Find last '/' that goes before dbname
|
||||||
|
foundSlash := false
|
||||||
|
for i := len(dsn) - 1; i >= 0; i-- {
|
||||||
|
if dsn[i] == '/' {
|
||||||
|
foundSlash = true
|
||||||
|
var j, k int
|
||||||
|
|
||||||
|
// left part is empty if i <= 0
|
||||||
|
if i > 0 {
|
||||||
|
// Find the first ':' in dsn
|
||||||
|
for j = i; j >= 0; j-- {
|
||||||
|
if dsn[j] == ':' {
|
||||||
|
cfg.Scheme = dsn[0:j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [username[:password]@][host]
|
||||||
|
// Find the last '@' in dsn[:i]
|
||||||
|
for j = i; j >= 0; j-- {
|
||||||
|
if dsn[j] == '@' {
|
||||||
|
// username[:password]
|
||||||
|
// Find the second ':' in dsn[:j]
|
||||||
|
for k = 0; k < j; k++ {
|
||||||
|
if dsn[k] == ':' {
|
||||||
|
if cfg.Scheme == dsn[:k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
cfg.Password, err = url.PathUnescape(dsn[k+1 : j])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Username = dsn[len(cfg.Scheme)+3 : k]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k = j + 1; k < i; k++ {
|
||||||
|
if dsn[k] == ':' {
|
||||||
|
cfg.Host = dsn[j+1 : k]
|
||||||
|
cfg.Port = dsn[k+1 : i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbname[?param1=value1&...¶mN=valueN]
|
||||||
|
// Find the first '?' in dsn[i+1:]
|
||||||
|
for j = i + 1; j < len(dsn); j++ {
|
||||||
|
if dsn[j] == '?' {
|
||||||
|
parts := strings.Split(dsn[j+1:], "&")
|
||||||
|
cfg.Params = make([]string, 0, len(parts)*2)
|
||||||
|
for _, p := range parts {
|
||||||
|
k, v, found := strings.Cut(p, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg.Params = append(cfg.Params, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
dbname := dsn[i+1 : j]
|
||||||
|
if cfg.Database, err = url.PathUnescape(dbname); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundSlash && len(dsn) > 0 {
|
||||||
|
return nil, ErrInvalidDSNNoSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
31
database/dsn_test.go
Normal file
31
database/dsn_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDSN(t *testing.T) {
|
||||||
|
cfg, err := ParseDSN("postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cfg.Password != "p@ssword#" {
|
||||||
|
t.Fatalf("parsing error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatDSN(t *testing.T) {
|
||||||
|
src := "postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2"
|
||||||
|
cfg, err := ParseDSN(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dst, err := url.PathUnescape(cfg.FormatDSN())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if src != dst {
|
||||||
|
t.Fatalf("\n%s\n%s", src, dst)
|
||||||
|
}
|
||||||
|
}
|
158
errors/errors.go
158
errors/errors.go
@@ -1,14 +1,20 @@
|
|||||||
// Package errors provides a way to return detailed information
|
// Package errors provides a way to return detailed information
|
||||||
// for an RPC request error. The error is normally JSON encoded.
|
// for an RPC request error. The error is normally JSON encoded.
|
||||||
package errors // import "go.unistack.org/micro/v3/errors"
|
package errors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -38,6 +44,20 @@ var (
|
|||||||
ErrGatewayTimeout = &Error{Code: 504}
|
ErrGatewayTimeout = &Error{Code: 504}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ProblemContentType = "application/problem+json"
|
||||||
|
|
||||||
|
type Problem struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
Instance string `json:"instance,omitempty"`
|
||||||
|
Errors []struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
} `json:"errors,omitempty"`
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Error type
|
// Error type
|
||||||
type Error struct {
|
type Error struct {
|
||||||
// ID holds error id or service, usually someting like my_service or id
|
// ID holds error id or service, usually someting like my_service or id
|
||||||
@@ -256,6 +276,10 @@ func CodeIn(err interface{}, codes ...int32) bool {
|
|||||||
|
|
||||||
// FromError try to convert go error to *Error
|
// FromError try to convert go error to *Error
|
||||||
func FromError(err error) *Error {
|
func FromError(err error) *Error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if verr, ok := err.(*Error); ok && verr != nil {
|
if verr, ok := err.(*Error); ok && verr != nil {
|
||||||
return verr
|
return verr
|
||||||
}
|
}
|
||||||
@@ -340,3 +364,135 @@ func addslashes(str string) string {
|
|||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type retryableError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retryable returns error that can be retried later
|
||||||
|
func Retryable(err error) error {
|
||||||
|
return &retryableError{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsRetryableFunc func(error) bool
|
||||||
|
|
||||||
|
var (
|
||||||
|
RetrayableOracleErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
errmsg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errmsg, `ORA-`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `can not assign`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `can't assign`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetrayablePostgresErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
errmsg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errmsg, `number of field descriptions must equal number of`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `not a pointer`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `values, but dst struct has only`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `struct doesn't have corresponding row field`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `cannot find field`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `cannot scan`) || strings.Contains(errmsg, `cannot convert`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `failed to connect to`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableMicroErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
switch verr := err.(type) {
|
||||||
|
case *Error:
|
||||||
|
switch verr.Code {
|
||||||
|
case 401, 403, 408, 500, 501, 502, 503, 504:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case *retryableError:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableGoErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
switch verr := err.(type) {
|
||||||
|
case interface{ SafeToRetry() bool }:
|
||||||
|
return verr.SafeToRetry()
|
||||||
|
case interface{ Timeout() bool }:
|
||||||
|
return verr.Timeout()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, context.DeadlineExceeded):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, io.ErrClosedPipe), errors.Is(err, io.ErrShortBuffer), errors.Is(err, io.ErrShortWrite):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableGrpcErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.Unavailable, codes.ResourceExhausted:
|
||||||
|
return true
|
||||||
|
case codes.DeadlineExceeded:
|
||||||
|
return true
|
||||||
|
case codes.Internal:
|
||||||
|
switch {
|
||||||
|
case strings.Contains(st.Message(), `transport: received the unexpected content-type "text/html; charset=UTF-8"`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(st.Message(), io.ErrUnexpectedEOF.Error()):
|
||||||
|
return true
|
||||||
|
case strings.Contains(st.Message(), `stream terminated by RST_STREAM with error code: INTERNAL_ERROR`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unwrap provides error wrapping
|
||||||
|
func (e *retryableError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error string
|
||||||
|
func (e *retryableError) Error() string {
|
||||||
|
if e.err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRetryable checks error for ability to retry later
|
||||||
|
func IsRetryable(err error, fns ...IsRetryableFunc) bool {
|
||||||
|
for _, fn := range fns {
|
||||||
|
if ok := fn(err); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@@ -17,7 +17,7 @@ syntax = "proto3";
|
|||||||
package micro.errors;
|
package micro.errors;
|
||||||
|
|
||||||
option cc_enable_arenas = true;
|
option cc_enable_arenas = true;
|
||||||
option go_package = "go.unistack.org/micro/v3/errors;errors";
|
option go_package = "go.unistack.org/micro/v4/errors;errors";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option java_outer_classname = "MicroErrors";
|
option java_outer_classname = "MicroErrors";
|
||||||
option java_package = "micro.errors";
|
option java_package = "micro.errors";
|
||||||
|
@@ -2,12 +2,19 @@ package errors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
er "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestIsRetrayable(t *testing.T) {
|
||||||
|
err := fmt.Errorf("ORA-")
|
||||||
|
if !IsRetryable(err, RetrayableOracleErrors...) {
|
||||||
|
t.Fatalf("IsRetrayable not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalJSON(t *testing.T) {
|
func TestMarshalJSON(t *testing.T) {
|
||||||
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
||||||
_, err := json.Marshal(e)
|
_, err := json.Marshal(e)
|
||||||
@@ -19,7 +26,7 @@ func TestMarshalJSON(t *testing.T) {
|
|||||||
func TestEmpty(t *testing.T) {
|
func TestEmpty(t *testing.T) {
|
||||||
msg := "test"
|
msg := "test"
|
||||||
var err *Error
|
var err *Error
|
||||||
err = FromError(fmt.Errorf(msg))
|
err = FromError(errors.New(msg))
|
||||||
if err.Detail != msg {
|
if err.Detail != msg {
|
||||||
t.Fatalf("invalid error %v", err)
|
t.Fatalf("invalid error %v", err)
|
||||||
}
|
}
|
||||||
@@ -35,7 +42,7 @@ func TestFromError(t *testing.T) {
|
|||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
}
|
}
|
||||||
err = er.New(err.Error())
|
err = errors.New(err.Error())
|
||||||
merr = FromError(err)
|
merr = FromError(err)
|
||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
@@ -50,7 +57,7 @@ func TestEqual(t *testing.T) {
|
|||||||
t.Fatal("errors must be equal")
|
t.Fatal("errors must be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
err3 := er.New("my test err")
|
err3 := errors.New("my test err")
|
||||||
if Equal(err1, err3) {
|
if Equal(err1, err3) {
|
||||||
t.Fatal("errors must be not equal")
|
t.Fatal("errors must be not equal")
|
||||||
}
|
}
|
||||||
|
27
event.go
27
event.go
@@ -1,27 +0,0 @@
|
|||||||
package micro
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event is used to publish messages to a topic
|
|
||||||
type Event interface {
|
|
||||||
// Publish publishes a message to the event topic
|
|
||||||
Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type event struct {
|
|
||||||
c client.Client
|
|
||||||
topic string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvent creates a new event publisher
|
|
||||||
func NewEvent(topic string, c client.Client) Event {
|
|
||||||
return &event{c, topic}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *event) Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error {
|
|
||||||
return e.c.Publish(ctx, e.c.NewMessage(e.topic, msg), opts...)
|
|
||||||
}
|
|
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package flow
|
package flow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,6 +7,24 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFromNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
c, ok := FromContext(nil)
|
||||||
|
if ok || c != nil {
|
||||||
|
t.Fatal("FromContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNilContext(t *testing.T) {
|
||||||
|
// nolint: staticcheck
|
||||||
|
ctx := NewContext(nil, NewFlow())
|
||||||
|
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if c == nil || !ok {
|
||||||
|
t.Fatal("NewContext not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFromContext(t *testing.T) {
|
func TestFromContext(t *testing.T) {
|
||||||
ctx := context.WithValue(context.TODO(), flowKey{}, NewFlow())
|
ctx := context.WithValue(context.TODO(), flowKey{}, NewFlow())
|
||||||
|
|
||||||
|
@@ -7,6 +7,56 @@ import (
|
|||||||
"github.com/silas/dag"
|
"github.com/silas/dag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDeps(t *testing.T) {
|
||||||
|
t.Skip()
|
||||||
|
d := &dag.AcyclicGraph{}
|
||||||
|
|
||||||
|
v0 := d.Add(&node{"v0"})
|
||||||
|
v1 := d.Add(&node{"v1"})
|
||||||
|
v2 := d.Add(&node{"v2"})
|
||||||
|
v3 := d.Add(&node{"v3"})
|
||||||
|
v4 := d.Add(&node{"v4"})
|
||||||
|
|
||||||
|
d.Connect(dag.BasicEdge(v0, v1))
|
||||||
|
d.Connect(dag.BasicEdge(v1, v2))
|
||||||
|
d.Connect(dag.BasicEdge(v2, v4))
|
||||||
|
d.Connect(dag.BasicEdge(v0, v3))
|
||||||
|
d.Connect(dag.BasicEdge(v3, v4))
|
||||||
|
|
||||||
|
if err := d.Validate(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.TransitiveReduction()
|
||||||
|
|
||||||
|
var steps [][]string
|
||||||
|
fn := func(n dag.Vertex, idx int) error {
|
||||||
|
if idx == 0 {
|
||||||
|
steps = make([][]string, 1)
|
||||||
|
steps[0] = make([]string, 0, 1)
|
||||||
|
} else if idx >= len(steps) {
|
||||||
|
tsteps := make([][]string, idx+1)
|
||||||
|
copy(tsteps, steps)
|
||||||
|
steps = tsteps
|
||||||
|
steps[idx] = make([]string, 0, 1)
|
||||||
|
}
|
||||||
|
steps[idx] = append(steps[idx], fmt.Sprintf("%s", n))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start := &node{"v0"}
|
||||||
|
err := d.SortedDepthFirstWalk([]dag.Vertex{start}, fn)
|
||||||
|
checkErr(t, err)
|
||||||
|
|
||||||
|
for idx, steps := range steps {
|
||||||
|
fmt.Printf("level %d steps %#+v\n", idx, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(steps[2]) != 1 {
|
||||||
|
t.Logf("invalid steps %#+v", steps[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkErr(t *testing.T, err error) {
|
func checkErr(t *testing.T, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
469
flow/default.go
469
flow/default.go
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
package flow
|
package flow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,13 +8,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/silas/dag"
|
"github.com/heimdalr/dag"
|
||||||
"go.unistack.org/micro/v3/client"
|
"go.unistack.org/micro/v4/client"
|
||||||
"go.unistack.org/micro/v3/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/store"
|
||||||
"go.unistack.org/micro/v3/store"
|
"go.unistack.org/micro/v4/util/id"
|
||||||
"go.unistack.org/micro/v3/util/id"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type microFlow struct {
|
type microFlow struct {
|
||||||
@@ -21,7 +22,7 @@ type microFlow struct {
|
|||||||
|
|
||||||
type microWorkflow struct {
|
type microWorkflow struct {
|
||||||
opts Options
|
opts Options
|
||||||
g *dag.AcyclicGraph
|
g *dag.DAG
|
||||||
steps map[string]Step
|
steps map[string]Step
|
||||||
id string
|
id string
|
||||||
status Status
|
status Status
|
||||||
@@ -33,20 +34,20 @@ func (w *microWorkflow) ID() string {
|
|||||||
return w.id
|
return w.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Steps() ([][]Step, error) {
|
|
||||||
return w.getSteps("", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *microWorkflow) Status() Status {
|
func (w *microWorkflow) Status() Status {
|
||||||
return w.status
|
return w.status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
||||||
|
var err error
|
||||||
w.Lock()
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
w.steps[s.String()] = s
|
w.steps[s.String()] = s
|
||||||
w.g.Add(s)
|
if _, err = w.g.AddVertex(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -55,18 +56,13 @@ func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ErrStepNotExists
|
return ErrStepNotExists
|
||||||
}
|
}
|
||||||
w.g.Connect(dag.BasicEdge(src, dst))
|
if err = w.g.AddEdge(src.String(), dst.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.g.Validate(); err != nil {
|
w.g.ReduceTransitively()
|
||||||
w.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.g.TransitiveReduction()
|
|
||||||
|
|
||||||
w.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -75,10 +71,11 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
|
|||||||
// TODO: handle case when some step requires or required by removed step
|
// TODO: handle case when some step requires or required by removed step
|
||||||
|
|
||||||
w.Lock()
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
delete(w.steps, s.String())
|
delete(w.steps, s.String())
|
||||||
w.g.Remove(s)
|
w.g.DeleteVertex(s.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -87,68 +84,15 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ErrStepNotExists
|
return ErrStepNotExists
|
||||||
}
|
}
|
||||||
w.g.Connect(dag.BasicEdge(src, dst))
|
w.g.AddEdge(src.String(), dst.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.g.Validate(); err != nil {
|
w.g.ReduceTransitively()
|
||||||
w.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.g.TransitiveReduction()
|
|
||||||
|
|
||||||
w.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) getSteps(start string, reverse bool) ([][]Step, error) {
|
|
||||||
var steps [][]Step
|
|
||||||
var root dag.Vertex
|
|
||||||
var err error
|
|
||||||
|
|
||||||
fn := func(n dag.Vertex, idx int) error {
|
|
||||||
if idx == 0 {
|
|
||||||
steps = make([][]Step, 1)
|
|
||||||
steps[0] = make([]Step, 0, 1)
|
|
||||||
} else if idx >= len(steps) {
|
|
||||||
tsteps := make([][]Step, idx+1)
|
|
||||||
copy(tsteps, steps)
|
|
||||||
steps = tsteps
|
|
||||||
steps[idx] = make([]Step, 0, 1)
|
|
||||||
}
|
|
||||||
steps[idx] = append(steps[idx], n.(Step))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if start != "" {
|
|
||||||
var ok bool
|
|
||||||
w.RLock()
|
|
||||||
root, ok = w.steps[start]
|
|
||||||
w.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrStepNotExists
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root, err = w.g.Root()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reverse {
|
|
||||||
err = w.g.SortedReverseDepthFirstWalk([]dag.Vertex{root}, fn)
|
|
||||||
} else {
|
|
||||||
err = w.g.SortedDepthFirstWalk([]dag.Vertex{root}, fn)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *microWorkflow) Abort(ctx context.Context, id string) error {
|
func (w *microWorkflow) Abort(ctx context.Context, id string) error {
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
||||||
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
|
||||||
@@ -167,11 +111,7 @@ func (w *microWorkflow) Resume(ctx context.Context, id string) error {
|
|||||||
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
|
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
|
||||||
w.Lock()
|
w.Lock()
|
||||||
if !w.init {
|
if !w.init {
|
||||||
if err := w.g.Validate(); err != nil {
|
w.g.ReduceTransitively()
|
||||||
w.Unlock()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
w.g.TransitiveReduction()
|
|
||||||
w.init = true
|
w.init = true
|
||||||
}
|
}
|
||||||
w.Unlock()
|
w.Unlock()
|
||||||
@@ -181,26 +121,11 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
||||||
|
|
||||||
options := NewExecuteOptions(opts...)
|
options := NewExecuteOptions(opts...)
|
||||||
|
|
||||||
steps, err := w.getSteps(options.Start, options.Reverse)
|
|
||||||
if err != nil {
|
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
cherr := make(chan error, 1)
|
|
||||||
chstatus := make(chan Status, 1)
|
|
||||||
|
|
||||||
nctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
nopts := make([]ExecuteOption, 0, len(opts)+5)
|
nopts := make([]ExecuteOption, 0, len(opts)+5)
|
||||||
|
|
||||||
nopts = append(nopts,
|
nopts = append(nopts,
|
||||||
@@ -210,143 +135,274 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
|
|||||||
ExecuteMeter(w.opts.Meter),
|
ExecuteMeter(w.opts.Meter),
|
||||||
)
|
)
|
||||||
nopts = append(nopts, opts...)
|
nopts = append(nopts, opts...)
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
if werr := workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
w.opts.Logger.Error(ctx, "store error: %v", werr)
|
||||||
return eid, werr
|
return eid, werr
|
||||||
}
|
}
|
||||||
for idx := range steps {
|
|
||||||
for nidx := range steps[idx] {
|
var startID string
|
||||||
cstep := steps[idx][nidx]
|
if options.Start == "" {
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
mp := w.g.GetRoots()
|
||||||
return eid, werr
|
if len(mp) != 1 {
|
||||||
|
return eid, ErrStepNotExists
|
||||||
|
}
|
||||||
|
for k := range mp {
|
||||||
|
startID = k
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for k, v := range w.g.GetVertices() {
|
||||||
|
if v == options.Start {
|
||||||
|
startID = k
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if startID == "" {
|
||||||
for idx := range steps {
|
return eid, ErrStepNotExists
|
||||||
for nidx := range steps[idx] {
|
}
|
||||||
wStatus := &codec.Frame{}
|
|
||||||
if werr := workflowStore.Read(w.opts.Context, "status", wStatus); werr != nil {
|
if options.Async {
|
||||||
cherr <- werr
|
go w.handleWorkflow(startID, nopts...)
|
||||||
return
|
return eid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return eid, w.handleWorkflow(startID, nopts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) error {
|
||||||
|
w.RLock()
|
||||||
|
defer w.RUnlock()
|
||||||
|
|
||||||
|
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
||||||
|
// workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
||||||
|
|
||||||
|
// Get IDs of all descendant vertices.
|
||||||
|
flowIDs, errDes := w.g.GetDescendants(startID)
|
||||||
|
if errDes != nil {
|
||||||
|
return errDes
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputChannels provides for input channels for each of the descendant vertices (+ the start-vertex).
|
||||||
|
inputChannels := make(map[string]chan FlowResult, len(flowIDs)+1)
|
||||||
|
|
||||||
|
// Iterate vertex IDs and create an input channel for each of them and a single
|
||||||
|
// output channel for leaves. Note, this "pre-flight" is needed to ensure we
|
||||||
|
// really have an input channel regardless of how we traverse the tree and spawn
|
||||||
|
// workers.
|
||||||
|
leafCount := 0
|
||||||
|
|
||||||
|
for id := range flowIDs {
|
||||||
|
|
||||||
|
// Get all parents of this vertex.
|
||||||
|
parents, errPar := w.g.GetParents(id)
|
||||||
|
if errPar != nil {
|
||||||
|
return errPar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffered input channel that has capacity for all parent results.
|
||||||
|
inputChannels[id] = make(chan FlowResult, len(parents))
|
||||||
|
|
||||||
|
if ok, err := w.g.IsLeaf(id); ok && err == nil {
|
||||||
|
leafCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputChannel caries the results of leaf vertices.
|
||||||
|
outputChannel := make(chan FlowResult, leafCount)
|
||||||
|
|
||||||
|
// To also process the start vertex and to have its results being passed to its
|
||||||
|
// children, add it to the vertex IDs. Also add an input channel for the start
|
||||||
|
// vertex and feed the inputs to this channel.
|
||||||
|
flowIDs[startID] = struct{}{}
|
||||||
|
inputChannels[startID] = make(chan FlowResult, len(inputs))
|
||||||
|
for _, i := range inputs {
|
||||||
|
inputChannels[startID] <- i
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
// Iterate all vertex IDs (now incl. start vertex) and handle each worker (incl.
|
||||||
|
// inputs and outputs) in a separate goroutine.
|
||||||
|
for id := range flowIDs {
|
||||||
|
|
||||||
|
// Get all children of this vertex that later need to be notified. Note, we
|
||||||
|
// collect all children before the goroutine to be able to release the read
|
||||||
|
// lock as early as possible.
|
||||||
|
children, errChildren := w.g.GetChildren(id)
|
||||||
|
if errChildren != nil {
|
||||||
|
return errChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember to wait for this goroutine.
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(id string) {
|
||||||
|
// Get this vertex's input channel.
|
||||||
|
// Note, only concurrent read here, which is fine.
|
||||||
|
c := inputChannels[id]
|
||||||
|
|
||||||
|
// Await all parent inputs and stuff them into a slice.
|
||||||
|
parentCount := cap(c)
|
||||||
|
parentResults := make([]FlowResult, parentCount)
|
||||||
|
for i := 0; i < parentCount; i++ {
|
||||||
|
parentResults[i] = <-c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the worker.
|
||||||
|
errWorker := callback(w.g, id, parentResults)
|
||||||
|
if errWorker != nil {
|
||||||
|
return errWorker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send this worker's FlowResult onto all children's input channels or, if it is
|
||||||
|
// a leaf (i.e. no children), send the result onto the output channel.
|
||||||
|
if len(children) > 0 {
|
||||||
|
for child := range children {
|
||||||
|
inputChannels[child] <- flowResult
|
||||||
}
|
}
|
||||||
if status := StringStatus[string(wStatus.Data)]; status != StatusRunning {
|
} else {
|
||||||
chstatus <- status
|
outputChannel <- flowResult
|
||||||
return
|
}
|
||||||
}
|
|
||||||
if w.opts.Logger.V(logger.TraceLevel) {
|
// "Sign off".
|
||||||
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
|
wg.Done()
|
||||||
}
|
}(id)
|
||||||
cstep := steps[idx][nidx]
|
}
|
||||||
// nolint: nestif
|
|
||||||
if len(cstep.Requires()) == 0 {
|
// Wait for all go routines to finish.
|
||||||
wg.Add(1)
|
wg.Wait()
|
||||||
go func(step Step) {
|
|
||||||
defer wg.Done()
|
// Await all leaf vertex results and stuff them into a slice.
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil {
|
resultCount := cap(outputChannel)
|
||||||
|
results := make([]FlowResult, resultCount)
|
||||||
|
for i := 0; i < resultCount; i++ {
|
||||||
|
results[i] = <-outputChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
go func() {
|
||||||
|
for idx := range steps {
|
||||||
|
for nidx := range steps[idx] {
|
||||||
|
wStatus := &codec.Frame{}
|
||||||
|
if werr := workflowStore.Read(w.opts.Context, "status", wStatus); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status := StringStatus[string(wStatus.Data)]; status != StatusRunning {
|
||||||
|
chstatus <- status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.opts.Logger.V(logger.TraceLevel) {
|
||||||
|
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
|
||||||
|
}
|
||||||
|
cstep := steps[idx][nidx]
|
||||||
|
// nolint: nestif
|
||||||
|
if len(cstep.Requires()) == 0 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(step Step) {
|
||||||
|
defer wg.Done()
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rsp, serr := step.Execute(nctx, req, nopts...)
|
||||||
|
if serr != nil {
|
||||||
|
step.SetStatus(StatusFailure)
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
cherr <- serr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(cstep)
|
||||||
|
wg.Wait()
|
||||||
|
} else {
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil {
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rsp, serr := step.Execute(nctx, req, nopts...)
|
rsp, serr := cstep.Execute(nctx, req, nopts...)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
step.SetStatus(StatusFailure)
|
cstep.SetStatus(StatusFailure)
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
}
|
}
|
||||||
cherr <- serr
|
cherr <- serr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil {
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}(cstep)
|
|
||||||
wg.Wait()
|
|
||||||
} else {
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rsp, serr := cstep.Execute(nctx, req, nopts...)
|
|
||||||
if serr != nil {
|
|
||||||
cstep.SetStatus(StatusFailure)
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
}
|
|
||||||
cherr <- serr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
close(done)
|
||||||
close(done)
|
}()
|
||||||
}()
|
|
||||||
|
|
||||||
if options.Async {
|
if options.Async {
|
||||||
return eid, nil
|
return eid, nil
|
||||||
}
|
|
||||||
|
|
||||||
logger.Tracef(ctx, "wait for finish or error")
|
|
||||||
select {
|
|
||||||
case <-nctx.Done():
|
|
||||||
err = nctx.Err()
|
|
||||||
case cerr := <-cherr:
|
|
||||||
err = cerr
|
|
||||||
case <-done:
|
|
||||||
close(cherr)
|
|
||||||
case <-chstatus:
|
|
||||||
close(chstatus)
|
|
||||||
return eid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case nctx.Err() != nil:
|
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
|
||||||
}
|
}
|
||||||
case err == nil:
|
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
|
||||||
}
|
|
||||||
case err != nil:
|
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return eid, err
|
logger.Tracef(ctx, "wait for finish or error")
|
||||||
|
select {
|
||||||
|
case <-nctx.Done():
|
||||||
|
err = nctx.Err()
|
||||||
|
case cerr := <-cherr:
|
||||||
|
err = cerr
|
||||||
|
case <-done:
|
||||||
|
close(cherr)
|
||||||
|
case <-chstatus:
|
||||||
|
close(chstatus)
|
||||||
|
return eid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case nctx.Err() != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
case err == nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
case err != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFlow create new flow
|
// NewFlow create new flow
|
||||||
@@ -386,11 +442,11 @@ func (f *microFlow) WorkflowList(ctx context.Context) ([]Workflow, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
|
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
|
||||||
w := µWorkflow{opts: f.opts, id: id, g: &dag.AcyclicGraph{}, steps: make(map[string]Step, len(steps))}
|
w := µWorkflow{opts: f.opts, id: id, g: &dag.DAG{}, steps: make(map[string]Step, len(steps))}
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
w.steps[s.String()] = s
|
w.steps[s.String()] = s
|
||||||
w.g.Add(s)
|
w.g.AddVertex(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -399,14 +455,11 @@ func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrStepNotExists
|
return nil, ErrStepNotExists
|
||||||
}
|
}
|
||||||
w.g.Connect(dag.BasicEdge(src, dst))
|
w.g.AddEdge(src.String(), dst.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.g.Validate(); err != nil {
|
w.g.ReduceTransitively()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
w.g.TransitiveReduction()
|
|
||||||
|
|
||||||
w.init = true
|
w.init = true
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
// Package flow is an interface used for saga pattern microservice workflow
|
// Package flow is an interface used for saga pattern microservice workflow
|
||||||
package flow // import "go.unistack.org/micro/v3/flow"
|
package flow // import "go.unistack.org/micro/v4/flow"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -125,8 +125,6 @@ type Workflow interface {
|
|||||||
AppendSteps(steps ...Step) error
|
AppendSteps(steps ...Step) error
|
||||||
// Status returns workflow status
|
// Status returns workflow status
|
||||||
Status() Status
|
Status() Status
|
||||||
// Steps returns steps slice where parallel steps returned on the same level
|
|
||||||
Steps() ([][]Step, error)
|
|
||||||
// Suspend suspends execution
|
// Suspend suspends execution
|
||||||
Suspend(ctx context.Context, id string) error
|
Suspend(ctx context.Context, id string) error
|
||||||
// Resume resumes execution
|
// Resume resumes execution
|
||||||
|
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/client"
|
"go.unistack.org/micro/v4/client"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v3/store"
|
"go.unistack.org/micro/v4/store"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -123,8 +123,6 @@ type ExecuteOptions struct {
|
|||||||
Start string
|
Start string
|
||||||
// Timeout for execution
|
// Timeout for execution
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// Reverse execution
|
|
||||||
Reverse bool
|
|
||||||
// Async enables async execution
|
// Async enables async execution
|
||||||
Async bool
|
Async bool
|
||||||
}
|
}
|
||||||
@@ -167,13 +165,6 @@ func ExecuteContext(ctx context.Context) ExecuteOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteReverse says that dag must be run in reverse order
|
|
||||||
func ExecuteReverse(b bool) ExecuteOption {
|
|
||||||
return func(o *ExecuteOptions) {
|
|
||||||
o.Reverse = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteTimeout pass timeout time.Duration for execution
|
// ExecuteTimeout pass timeout time.Duration for execution
|
||||||
func ExecuteTimeout(td time.Duration) ExecuteOption {
|
func ExecuteTimeout(td time.Duration) ExecuteOption {
|
||||||
return func(o *ExecuteOptions) {
|
return func(o *ExecuteOptions) {
|
||||||
|
126
fsm/default.go
Normal file
126
fsm/default.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package fsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
body interface{}
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ State = &state{}
|
||||||
|
|
||||||
|
func (s *state) Name() string {
|
||||||
|
return s.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) Body() interface{} {
|
||||||
|
return s.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// fsm is a finite state machine
|
||||||
|
type fsm struct {
|
||||||
|
statesMap map[string]StateFunc
|
||||||
|
current string
|
||||||
|
statesOrder []string
|
||||||
|
opts Options
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSM creates a new finite state machine having the specified initial state
|
||||||
|
// with specified options
|
||||||
|
func NewFSM(opts ...Option) FSM {
|
||||||
|
return &fsm{
|
||||||
|
statesMap: map[string]StateFunc{},
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current returns the current state
|
||||||
|
func (f *fsm) Current() string {
|
||||||
|
f.mu.Lock()
|
||||||
|
s := f.current
|
||||||
|
f.mu.Unlock()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current returns the current state
|
||||||
|
func (f *fsm) Reset() {
|
||||||
|
f.mu.Lock()
|
||||||
|
f.current = f.opts.Initial
|
||||||
|
f.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// State adds state to fsm
|
||||||
|
func (f *fsm) State(state string, fn StateFunc) {
|
||||||
|
f.mu.Lock()
|
||||||
|
f.statesMap[state] = fn
|
||||||
|
f.statesOrder = append(f.statesOrder, state)
|
||||||
|
f.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs state machine with provided data
|
||||||
|
func (f *fsm) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
f.mu.Lock()
|
||||||
|
options := f.opts
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
sopts := []StateOption{StateDryRun(options.DryRun)}
|
||||||
|
|
||||||
|
cstate := options.Initial
|
||||||
|
states := make(map[string]StateFunc, len(f.statesMap))
|
||||||
|
for k, v := range f.statesMap {
|
||||||
|
states[k] = v
|
||||||
|
}
|
||||||
|
f.current = cstate
|
||||||
|
f.mu.Unlock()
|
||||||
|
|
||||||
|
var s State
|
||||||
|
s = &state{name: cstate, body: args}
|
||||||
|
nstate := s.Name()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
fn, ok := states[nstate]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(`state "%s" %w`, nstate, ErrInvalidState)
|
||||||
|
}
|
||||||
|
f.mu.Lock()
|
||||||
|
f.current = nstate
|
||||||
|
f.mu.Unlock()
|
||||||
|
|
||||||
|
// wrap the handler func
|
||||||
|
for i := len(options.Wrappers); i > 0; i-- {
|
||||||
|
fn = options.Wrappers[i-1](fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err = fn(ctx, s, sopts...)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return s.Body(), err
|
||||||
|
case s.Name() == StateEnd:
|
||||||
|
return s.Body(), nil
|
||||||
|
case s.Name() == "":
|
||||||
|
for idx := range f.statesOrder {
|
||||||
|
if f.statesOrder[idx] == nstate && len(f.statesOrder) > idx+1 {
|
||||||
|
nstate = f.statesOrder[idx+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
nstate = s.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
fsm/fsm.go
176
fsm/fsm.go
@@ -1,10 +1,8 @@
|
|||||||
package fsm // import "go.unistack.org/micro/v3/fsm"
|
package fsm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -12,170 +10,20 @@ var (
|
|||||||
StateEnd = "end"
|
StateEnd = "end"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options struct holding fsm options
|
type State interface {
|
||||||
type Options struct {
|
Name() string
|
||||||
// DryRun mode
|
Body() interface{}
|
||||||
DryRun bool
|
|
||||||
// Initial state
|
|
||||||
Initial string
|
|
||||||
// HooksBefore func slice runs in order before state
|
|
||||||
HooksBefore []HookBeforeFunc
|
|
||||||
// HooksAfter func slice runs in order after state
|
|
||||||
HooksAfter []HookAfterFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookBeforeFunc func signature
|
// StateWrapper wraps the StateFunc and returns the equivalent
|
||||||
type HookBeforeFunc func(ctx context.Context, state string, args interface{})
|
type StateWrapper func(StateFunc) StateFunc
|
||||||
|
|
||||||
// HookAfterFunc func signature
|
|
||||||
type HookAfterFunc func(ctx context.Context, state string, args interface{})
|
|
||||||
|
|
||||||
// Option func signature
|
|
||||||
type Option func(*Options)
|
|
||||||
|
|
||||||
// StateOptions holds state options
|
|
||||||
type StateOptions struct {
|
|
||||||
DryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateDryRun says that state executes in dry run mode
|
|
||||||
func StateDryRun(b bool) StateOption {
|
|
||||||
return func(o *StateOptions) {
|
|
||||||
o.DryRun = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateOption func signature
|
|
||||||
type StateOption func(*StateOptions)
|
|
||||||
|
|
||||||
// InitialState sets init state for state machine
|
|
||||||
func InitialState(initial string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Initial = initial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookBefore provides hook func slice
|
|
||||||
func HookBefore(fns ...HookBeforeFunc) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.HooksBefore = fns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookAfter provides hook func slice
|
|
||||||
func HookAfter(fns ...HookAfterFunc) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.HooksAfter = fns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateFunc called on state transition and return next step and error
|
// StateFunc called on state transition and return next step and error
|
||||||
type StateFunc func(ctx context.Context, args interface{}, opts ...StateOption) (string, interface{}, error)
|
type StateFunc func(ctx context.Context, state State, opts ...StateOption) (State, error)
|
||||||
|
|
||||||
// FSM is a finite state machine
|
type FSM interface {
|
||||||
type FSM struct {
|
Start(context.Context, interface{}, ...Option) (interface{}, error)
|
||||||
mu sync.Mutex
|
Current() string
|
||||||
statesMap map[string]StateFunc
|
Reset()
|
||||||
statesOrder []string
|
State(string, StateFunc)
|
||||||
opts *Options
|
|
||||||
current string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new finite state machine having the specified initial state
|
|
||||||
// with specified options
|
|
||||||
func New(opts ...Option) *FSM {
|
|
||||||
options := &Options{}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &FSM{
|
|
||||||
statesMap: map[string]StateFunc{},
|
|
||||||
opts: options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current returns the current state
|
|
||||||
func (f *FSM) Current() string {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
return f.current
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current returns the current state
|
|
||||||
func (f *FSM) Reset() {
|
|
||||||
f.mu.Lock()
|
|
||||||
f.current = f.opts.Initial
|
|
||||||
f.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// State adds state to fsm
|
|
||||||
func (f *FSM) State(state string, fn StateFunc) {
|
|
||||||
f.mu.Lock()
|
|
||||||
f.statesMap[state] = fn
|
|
||||||
f.statesOrder = append(f.statesOrder, state)
|
|
||||||
f.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initialize fsm and check states
|
|
||||||
|
|
||||||
// Start runs state machine with provided data
|
|
||||||
func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
var fn StateFunc
|
|
||||||
var nstate string
|
|
||||||
|
|
||||||
f.mu.Lock()
|
|
||||||
options := f.opts
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
sopts := []StateOption{StateDryRun(options.DryRun)}
|
|
||||||
|
|
||||||
cstate := options.Initial
|
|
||||||
states := make(map[string]StateFunc, len(f.statesMap))
|
|
||||||
for k, v := range f.statesMap {
|
|
||||||
states[k] = v
|
|
||||||
}
|
|
||||||
f.current = cstate
|
|
||||||
f.mu.Unlock()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
fn, ok = states[cstate]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf(`state "%s" %w`, cstate, ErrInvalidState)
|
|
||||||
}
|
|
||||||
f.mu.Lock()
|
|
||||||
f.current = cstate
|
|
||||||
f.mu.Unlock()
|
|
||||||
for _, fn := range options.HooksBefore {
|
|
||||||
fn(ctx, cstate, args)
|
|
||||||
}
|
|
||||||
nstate, args, err = fn(ctx, args, sopts...)
|
|
||||||
for _, fn := range options.HooksAfter {
|
|
||||||
fn(ctx, cstate, args)
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
return args, err
|
|
||||||
case nstate == StateEnd:
|
|
||||||
return args, nil
|
|
||||||
case nstate == "":
|
|
||||||
for idx := range f.statesOrder {
|
|
||||||
if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 {
|
|
||||||
nstate = f.statesOrder[idx+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cstate = nstate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,63 +1,72 @@
|
|||||||
package fsm
|
package fsm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFSMStart(t *testing.T) {
|
func TestFSMStart(t *testing.T) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
pfb := func(_ context.Context, state string, _ interface{}) {
|
if err := logger.DefaultLogger.Init(); err != nil {
|
||||||
fmt.Fprintf(buf, "before state %s\n", state)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pfa := func(_ context.Context, state string, _ interface{}) {
|
|
||||||
fmt.Fprintf(buf, "after state %s\n", state)
|
wrapper := func(next StateFunc) StateFunc {
|
||||||
|
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||||
|
sctx = logger.NewContext(sctx, logger.DefaultLogger.Fields("state", s.Name()))
|
||||||
|
return next(sctx, s, opts...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
f := New(InitialState("1"), HookBefore(pfb), HookAfter(pfa))
|
|
||||||
f1 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
f := NewFSM(InitialState("1"), WrapState(wrapper))
|
||||||
args := req.(map[string]interface{})
|
f1 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||||
|
_, ok := logger.FromContext(sctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("f1 context does not have logger")
|
||||||
|
}
|
||||||
|
args := s.Body().(map[string]interface{})
|
||||||
if v, ok := args["request"].(string); !ok || v == "" {
|
if v, ok := args["request"].(string); !ok || v == "" {
|
||||||
return "", nil, fmt.Errorf("empty request")
|
return nil, fmt.Errorf("empty request")
|
||||||
}
|
}
|
||||||
return "2", map[string]interface{}{"response": "test2"}, nil
|
return &state{name: "", body: map[string]interface{}{"response": "state1"}}, nil
|
||||||
}
|
}
|
||||||
f2 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
f2 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||||
args := req.(map[string]interface{})
|
_, ok := logger.FromContext(sctx)
|
||||||
if v, ok := args["response"].(string); !ok || v == "" {
|
if !ok {
|
||||||
return "", nil, fmt.Errorf("empty response")
|
t.Fatal("f2 context does not have logger")
|
||||||
}
|
}
|
||||||
return "", map[string]interface{}{"response": "test"}, nil
|
args := s.Body().(map[string]interface{})
|
||||||
|
if v, ok := args["response"].(string); !ok || v == "" {
|
||||||
|
return nil, fmt.Errorf("empty response")
|
||||||
|
}
|
||||||
|
return &state{name: "", body: map[string]interface{}{"response": "state2"}}, nil
|
||||||
}
|
}
|
||||||
f3 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
f3 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||||
args := req.(map[string]interface{})
|
_, ok := logger.FromContext(sctx)
|
||||||
if v, ok := args["response"].(string); !ok || v == "" {
|
if !ok {
|
||||||
return "", nil, fmt.Errorf("empty response")
|
t.Fatal("f3 context does not have logger")
|
||||||
}
|
}
|
||||||
return StateEnd, map[string]interface{}{"response": "test_last"}, nil
|
args := s.Body().(map[string]interface{})
|
||||||
|
if v, ok := args["response"].(string); !ok || v == "" {
|
||||||
|
return nil, fmt.Errorf("empty response")
|
||||||
|
}
|
||||||
|
return &state{name: StateEnd, body: map[string]interface{}{"response": "state3"}}, nil
|
||||||
}
|
}
|
||||||
f.State("1", f1)
|
f.State("1", f1)
|
||||||
f.State("2", f2)
|
f.State("2", f2)
|
||||||
f.State("3", f3)
|
f.State("3", f3)
|
||||||
rsp, err := f.Start(ctx, map[string]interface{}{"request": "test1"})
|
rsp, err := f.Start(ctx, map[string]interface{}{"request": "state"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
args := rsp.(map[string]interface{})
|
args := rsp.(map[string]interface{})
|
||||||
if v, ok := args["response"].(string); !ok || v == "" {
|
if v, ok := args["response"].(string); !ok || v == "" {
|
||||||
t.Fatalf("nil rsp: %#+v", args)
|
t.Fatalf("nil rsp: %#+v", args)
|
||||||
} else if v != "test_last" {
|
} else if v != "state3" {
|
||||||
t.Fatalf("invalid rsp %#+v", args)
|
t.Fatalf("invalid rsp %#+v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Contains(buf.Bytes(), []byte(`before state 1`)) ||
|
|
||||||
!bytes.Contains(buf.Bytes(), []byte(`before state 2`)) ||
|
|
||||||
!bytes.Contains(buf.Bytes(), []byte(`after state 1`)) ||
|
|
||||||
!bytes.Contains(buf.Bytes(), []byte(`after state 2`)) ||
|
|
||||||
!bytes.Contains(buf.Bytes(), []byte(`after state 3`)) ||
|
|
||||||
!bytes.Contains(buf.Bytes(), []byte(`after state 3`)) {
|
|
||||||
t.Fatalf("fsm not works properly or hooks error, buf: %s", buf.Bytes())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
52
fsm/options.go
Normal file
52
fsm/options.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package fsm
|
||||||
|
|
||||||
|
// Options struct holding fsm options
|
||||||
|
type Options struct {
|
||||||
|
// Initial state
|
||||||
|
Initial string
|
||||||
|
// Wrappers runs before state
|
||||||
|
Wrappers []StateWrapper
|
||||||
|
// DryRun mode
|
||||||
|
DryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option func signature
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// StateOptions holds state options
|
||||||
|
type StateOptions struct {
|
||||||
|
DryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateDryRun says that state executes in dry run mode
|
||||||
|
func StateDryRun(b bool) StateOption {
|
||||||
|
return func(o *StateOptions) {
|
||||||
|
o.DryRun = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateOption func signature
|
||||||
|
type StateOption func(*StateOptions)
|
||||||
|
|
||||||
|
// InitialState sets init state for state machine
|
||||||
|
func InitialState(initial string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Initial = initial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapState adds a state Wrapper to a list of options passed into the fsm
|
||||||
|
func WrapState(w StateWrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Wrappers = append(o.Wrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns new Options struct filled by passed Option
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
options := Options{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
101
function.go
101
function.go
@@ -1,101 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package micro
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Function is a one time executing Service
|
|
||||||
type Function interface {
|
|
||||||
// Inherits Service interface
|
|
||||||
Service
|
|
||||||
// Done signals to complete execution
|
|
||||||
Done() error
|
|
||||||
// Handle registers an RPC handler
|
|
||||||
Handle(v interface{}) error
|
|
||||||
// Subscribe registers a subscriber
|
|
||||||
Subscribe(topic string, v interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type function struct {
|
|
||||||
cancel context.CancelFunc
|
|
||||||
Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFunction returns a new Function for a one time executing Service
|
|
||||||
func NewFunction(opts ...Option) Function {
|
|
||||||
return newFunction(opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fnHandlerWrapper(f Function) server.HandlerWrapper {
|
|
||||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
|
||||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
|
||||||
defer f.Done()
|
|
||||||
return h(ctx, req, rsp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fnSubWrapper(f Function) server.SubscriberWrapper {
|
|
||||||
return func(s server.SubscriberFunc) server.SubscriberFunc {
|
|
||||||
return func(ctx context.Context, msg server.Message) error {
|
|
||||||
defer f.Done()
|
|
||||||
return s(ctx, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFunction(opts ...Option) Function {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// force ttl/interval
|
|
||||||
fopts := []Option{
|
|
||||||
RegisterTTL(time.Minute),
|
|
||||||
RegisterInterval(time.Second * 30),
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend to opts
|
|
||||||
fopts = append(fopts, opts...)
|
|
||||||
|
|
||||||
// make context the last thing
|
|
||||||
fopts = append(fopts, Context(ctx))
|
|
||||||
|
|
||||||
service := &service{opts: NewOptions(fopts...)}
|
|
||||||
|
|
||||||
fn := &function{
|
|
||||||
cancel: cancel,
|
|
||||||
Service: service,
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Server().Init(
|
|
||||||
// ensure the service waits for requests to finish
|
|
||||||
server.Wait(nil),
|
|
||||||
// wrap handlers and subscribers to finish execution
|
|
||||||
server.WrapHandler(fnHandlerWrapper(fn)),
|
|
||||||
server.WrapSubscriber(fnSubWrapper(fn)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *function) Done() error {
|
|
||||||
f.cancel()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *function) Handle(v interface{}) error {
|
|
||||||
return f.Service.Server().Handle(
|
|
||||||
f.Service.Server().NewHandler(v),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *function) Subscribe(topic string, v interface{}) error {
|
|
||||||
return f.Service.Server().Subscribe(
|
|
||||||
f.Service.Server().NewSubscriber(topic, v),
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,67 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package micro
|
|
||||||
|
|
||||||
/*
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/register"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFunction(t *testing.T) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
r := register.NewRegister()
|
|
||||||
ctx := context.TODO()
|
|
||||||
// create service
|
|
||||||
fn := NewFunction(
|
|
||||||
Register(r),
|
|
||||||
Name("test.function"),
|
|
||||||
AfterStart(func(ctx context.Context) error {
|
|
||||||
wg.Done()
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// we can't test fn.Init as it parses the command line
|
|
||||||
// fn.Init()
|
|
||||||
|
|
||||||
ch := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// run service
|
|
||||||
ch <- fn.Run()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for start
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// test call debug
|
|
||||||
req := fn.Client().NewRequest(
|
|
||||||
"test.function",
|
|
||||||
"Debug.Health",
|
|
||||||
new(proto.HealthRequest),
|
|
||||||
)
|
|
||||||
|
|
||||||
rsp := new(proto.HealthResponse)
|
|
||||||
|
|
||||||
err := fn.Client().Call(context.TODO(), req, rsp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rsp.Status != "ok" {
|
|
||||||
t.Fatalf("function response: %s", rsp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := <-ch; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user