mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-11 00:40:00 +08:00
Compare commits
353 Commits
v1.5.4
...
tools/goct
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cac41a38b | ||
|
|
f10084a3f5 | ||
|
|
040fee5669 | ||
|
|
42b3bae65a | ||
|
|
7c730b97d8 | ||
|
|
057bae92ab | ||
|
|
74331a45c9 | ||
|
|
9d551d507f | ||
|
|
02dd81c05c | ||
|
|
3095ba2b1f | ||
|
|
2afa60132c | ||
|
|
e71ed7294b | ||
|
|
95822281bf | ||
|
|
588e10daef | ||
|
|
62ba01120e | ||
|
|
527de1c50e | ||
|
|
abfe62a2d7 | ||
|
|
36f4cf97ff | ||
|
|
b3cd8a32ed | ||
|
|
a9d27cda8a | ||
|
|
04116f647d | ||
|
|
a8ccda0c06 | ||
|
|
bfddb9dae4 | ||
|
|
b337ae36e5 | ||
|
|
5e5123caa3 | ||
|
|
d371ab5479 | ||
|
|
1b9b61f505 | ||
|
|
e1f15efb3b | ||
|
|
1540bdc4c9 | ||
|
|
95b32b5779 | ||
|
|
815a4f7eed | ||
|
|
4b0bacc9c6 | ||
|
|
e9dc96af17 | ||
|
|
62c88a84d1 | ||
|
|
36088ea0d4 | ||
|
|
164f5aa86c | ||
|
|
07d07cdd23 | ||
|
|
0efe99af66 | ||
|
|
927f8bc821 | ||
|
|
2a7ada993b | ||
|
|
682460c1c8 | ||
|
|
a66ae0d4c4 | ||
|
|
d1f24ab70f | ||
|
|
d0983948b5 | ||
|
|
3343fc2cdb | ||
|
|
3866b5741a | ||
|
|
5fbe8ff5c4 | ||
|
|
6f763f71f9 | ||
|
|
80377f18e7 | ||
|
|
8690859c7d | ||
|
|
d744038198 | ||
|
|
58ad8cac8a | ||
|
|
74886a151e | ||
|
|
c5eda1f155 | ||
|
|
b5b7c054ca | ||
|
|
6c8073b691 | ||
|
|
64d430d424 | ||
|
|
f138cc792e | ||
|
|
b20ec8aedb | ||
|
|
a53254fa91 | ||
|
|
08563482e5 | ||
|
|
968727412d | ||
|
|
6f3d094eba | ||
|
|
2d3ebb9b62 | ||
|
|
8c0bb27136 | ||
|
|
cf987295df | ||
|
|
8c92b3af7d | ||
|
|
5dd9342703 | ||
|
|
3ef59f6a71 | ||
|
|
f12802abc7 | ||
|
|
6f0fe67804 | ||
|
|
f44f0e7e62 | ||
|
|
cdd95296db | ||
|
|
3e794cf991 | ||
|
|
bbce95e7e1 | ||
|
|
0449450c64 | ||
|
|
9f9a12ea57 | ||
|
|
cc2a7e97f9 | ||
|
|
09d7af76af | ||
|
|
c233a66601 | ||
|
|
94fa12560c | ||
|
|
7d90f906f5 | ||
|
|
f372b98d96 | ||
|
|
459d3025c5 | ||
|
|
e9e55125a9 | ||
|
|
159ecb7386 | ||
|
|
69bb746a1d | ||
|
|
d184f96b13 | ||
|
|
c7dacb0146 | ||
|
|
2207477b60 | ||
|
|
105ab590ff | ||
|
|
2f4c58ed73 | ||
|
|
1631aa02ad | ||
|
|
4df10eef5d | ||
|
|
3d552ea7a8 | ||
|
|
74b87ac9fd | ||
|
|
ba1d6e3664 | ||
|
|
2096cd5749 | ||
|
|
2eb2fa26f6 | ||
|
|
bc4187ca90 | ||
|
|
b7be25b98b | ||
|
|
dd01695d45 | ||
|
|
25821bdee6 | ||
|
|
b624b966f0 | ||
|
|
df96262235 | ||
|
|
2629636f64 | ||
|
|
708ad207d7 | ||
|
|
b53ba76a99 | ||
|
|
be7f93924a | ||
|
|
45be48a4ee | ||
|
|
e08ba2fee8 | ||
|
|
a5d2b971a1 | ||
|
|
9763c8b143 | ||
|
|
4e3f1776dc | ||
|
|
e38036cea2 | ||
|
|
8e97c5819f | ||
|
|
0ee44c7064 | ||
|
|
a1bacd3fc8 | ||
|
|
c98d5fdaf4 | ||
|
|
2ee43b41b8 | ||
|
|
8367af3416 | ||
|
|
03b6e377d7 | ||
|
|
ec41880476 | ||
|
|
5263805b3b | ||
|
|
a7363f0c21 | ||
|
|
52e5d85221 | ||
|
|
88aab8f635 | ||
|
|
1f63cbe9c6 | ||
|
|
0dfaf135dd | ||
|
|
914bcdcf2b | ||
|
|
e38cb0118d | ||
|
|
cb8161c799 | ||
|
|
c4dac2095f | ||
|
|
25a807afb2 | ||
|
|
6be37ad533 | ||
|
|
28cb2c5804 | ||
|
|
0f1d4c6bca | ||
|
|
bfe8335cb2 | ||
|
|
3c10ce0115 | ||
|
|
1303e0fe6f | ||
|
|
9c17499757 | ||
|
|
8ceb2885db | ||
|
|
00944894b4 | ||
|
|
609fb3d59e | ||
|
|
01c330abe7 | ||
|
|
2ccef5bb4f | ||
|
|
10f1d93e2a | ||
|
|
dd518c8eac | ||
|
|
97cf2421de | ||
|
|
786a80131e | ||
|
|
93d257f9f5 | ||
|
|
f79535057f | ||
|
|
a905f4c20c | ||
|
|
3331954a78 | ||
|
|
f54c2e384f | ||
|
|
4b83f2ebd0 | ||
|
|
1c572ee16b | ||
|
|
b3402430e8 | ||
|
|
076f5de7d9 | ||
|
|
303a74559a | ||
|
|
c08e741d7a | ||
|
|
06d2c07fce | ||
|
|
b6f00a5789 | ||
|
|
dace520654 | ||
|
|
44d347d48a | ||
|
|
408827d876 | ||
|
|
9e33b557b1 | ||
|
|
368caa7608 | ||
|
|
7822a4c1cb | ||
|
|
0441f84606 | ||
|
|
81d72b5010 | ||
|
|
7ba8adfc74 | ||
|
|
ffd2a78623 | ||
|
|
1b9b3cada7 | ||
|
|
38c8f9cf21 | ||
|
|
54dbb05bb9 | ||
|
|
9a671f6059 | ||
|
|
80aab0b3f8 | ||
|
|
2d0286646f | ||
|
|
d012fe97b1 | ||
|
|
7ca13bc25e | ||
|
|
9c20f10743 | ||
|
|
6ec38ec056 | ||
|
|
28c742a1e1 | ||
|
|
b3b6cfe947 | ||
|
|
a8ef7b51eb | ||
|
|
124968114a | ||
|
|
04ed821b65 | ||
|
|
15599ac0a0 | ||
|
|
0cf6971664 | ||
|
|
47c4f2831c | ||
|
|
2b18dd1764 | ||
|
|
27c4908342 | ||
|
|
48625fa381 | ||
|
|
83a776a190 | ||
|
|
431f9af43e | ||
|
|
8c2f4c1899 | ||
|
|
919477ffe4 | ||
|
|
400386459c | ||
|
|
ebe0801d2f | ||
|
|
b76d85f204 | ||
|
|
28ba57afb3 | ||
|
|
d6873047ce | ||
|
|
54c0f2e5cf | ||
|
|
7795231cc6 | ||
|
|
4835e4fe51 | ||
|
|
daef970091 | ||
|
|
05020a92e8 | ||
|
|
a1bbac3c6c | ||
|
|
22c98beb24 | ||
|
|
8fd710d5e7 | ||
|
|
91a735ae47 | ||
|
|
39c662eece | ||
|
|
5e63002cf8 | ||
|
|
c46bcf7e1b | ||
|
|
3c65bdbb66 | ||
|
|
5630bce286 | ||
|
|
75524da21e | ||
|
|
ede7e683fd | ||
|
|
eb14d1347e | ||
|
|
c220b5d886 | ||
|
|
5e8e21b257 | ||
|
|
0635a4ac96 | ||
|
|
c71b753c78 | ||
|
|
2f8cffc699 | ||
|
|
9c1aa6da3d | ||
|
|
da67ea2300 | ||
|
|
72dd2736f5 | ||
|
|
24695bba09 | ||
|
|
c7c43062c5 | ||
|
|
97e1ea0633 | ||
|
|
04b9737a61 | ||
|
|
b0fb246693 | ||
|
|
41140ac78c | ||
|
|
1281904572 | ||
|
|
c8a8ff7cad | ||
|
|
df2799fff1 | ||
|
|
fd8ee0b851 | ||
|
|
6ecc5e7b73 | ||
|
|
52963c2ebf | ||
|
|
07e3e14c0e | ||
|
|
34c5f6616c | ||
|
|
32600f2619 | ||
|
|
b07df1c344 | ||
|
|
a1fca3a1da | ||
|
|
9394e59597 | ||
|
|
f8adc71529 | ||
|
|
c05e03bb5a | ||
|
|
199e86050e | ||
|
|
1e2a12b3d6 | ||
|
|
922efbfc2d | ||
|
|
842c4d81cc | ||
|
|
2a335c7608 | ||
|
|
35edd6b19d | ||
|
|
36bbc6a2e2 | ||
|
|
e20ccdd011 | ||
|
|
c2ff00883a | ||
|
|
00db97fcc1 | ||
|
|
117c3a9069 | ||
|
|
172ff407f3 | ||
|
|
a242fec5e1 | ||
|
|
6286941ebf | ||
|
|
42e0a6f90c | ||
|
|
81ae7d36b5 | ||
|
|
944e76edb9 | ||
|
|
151768ef82 | ||
|
|
50581c7f5c | ||
|
|
54041ef9e4 | ||
|
|
5a9ae5ef02 | ||
|
|
19de13bb04 | ||
|
|
3ab4e82168 | ||
|
|
619e838513 | ||
|
|
423597a01c | ||
|
|
d84dfe1b20 | ||
|
|
87b7a1120d | ||
|
|
528af8a99d | ||
|
|
17fc68ac5a | ||
|
|
804a56bd14 | ||
|
|
88f60d7736 | ||
|
|
95b7a3d3ce | ||
|
|
d71c0da7b7 | ||
|
|
fd070fec91 | ||
|
|
4f22034342 | ||
|
|
b731aa38af | ||
|
|
bf996a1812 | ||
|
|
af7ce65244 | ||
|
|
952db71835 | ||
|
|
abd1fa96a9 | ||
|
|
5aedd9c076 | ||
|
|
ff230c4b1d | ||
|
|
02c95108b9 | ||
|
|
1ff541afe4 | ||
|
|
11a8cbc1e5 | ||
|
|
c063976822 | ||
|
|
cb707034ce | ||
|
|
f10db27efd | ||
|
|
4878f90546 | ||
|
|
421e6617b1 | ||
|
|
0ee7a271d3 | ||
|
|
af022b9655 | ||
|
|
98d46261d9 | ||
|
|
4222fd97bc | ||
|
|
814852f0b8 | ||
|
|
ded2888759 | ||
|
|
18d66a795d | ||
|
|
4211672bfd | ||
|
|
68df0c3620 | ||
|
|
5e435b6a76 | ||
|
|
0dcede6457 | ||
|
|
cc21f5fae2 | ||
|
|
b22ad50d59 | ||
|
|
974252980c | ||
|
|
8d83986d27 | ||
|
|
6821b0a7dd | ||
|
|
1ba1724c65 | ||
|
|
ca5a7df5b0 | ||
|
|
69a3024853 | ||
|
|
fd3abf3717 | ||
|
|
99b3750d10 | ||
|
|
33f6d7ebb8 | ||
|
|
c4ef9ceb68 | ||
|
|
e95861f28a | ||
|
|
d3cd7b17c0 | ||
|
|
a50515496c | ||
|
|
0423313d9b | ||
|
|
7bbe7de05f | ||
|
|
83a451f2f4 | ||
|
|
d2a874f21d | ||
|
|
fd85b24b25 | ||
|
|
14fcbd7658 | ||
|
|
cb3ffc76a3 | ||
|
|
45fbd7dc35 | ||
|
|
af821cf794 | ||
|
|
ec69950153 | ||
|
|
ce5e78db53 | ||
|
|
ed75802eaa | ||
|
|
76c92b571d | ||
|
|
a2e703c53e | ||
|
|
ca698deb2a | ||
|
|
a9f4aab86b | ||
|
|
c3f57e9b0a | ||
|
|
ad4cce959d | ||
|
|
279123f4a7 | ||
|
|
457eb1961b | ||
|
|
63df384a4b | ||
|
|
42bfa26e2b | ||
|
|
ff04356704 | ||
|
|
05db706c62 | ||
|
|
ef2e0d859d | ||
|
|
05ec16ae9d | ||
|
|
13e685e0db | ||
|
|
c10f44b74e | ||
|
|
57644420ed |
@@ -1,3 +1,7 @@
|
||||
coverage:
|
||||
status:
|
||||
patch: true
|
||||
project: false # disabled because project coverage is not stable
|
||||
comment:
|
||||
layout: "flags, files"
|
||||
behavior: once
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
**/.git
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
goctl
|
||||
Makefile
|
||||
readme.md
|
||||
readme-cn.md
|
||||
|
||||
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,13 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # https://gitee.com/kevwan/static/raw/master/images/sponsor.jpg
|
||||
ethereum: # 0x5052b7f6B937B02563996D23feb69b38D06Ca150 | kevwan
|
||||
github: kevwan
|
||||
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -5,6 +5,14 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "docker" # Update image tags in Dockerfile
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions" # Update GitHub Actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -35,11 +35,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -64,4 +64,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
16
.github/workflows/go.yml
vendored
16
.github/workflows/go.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: '1.19'
|
||||
check-latest: true
|
||||
cache: true
|
||||
id: go
|
||||
@@ -40,20 +40,20 @@ jobs:
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
|
||||
test-win:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
# use 1.18 to guarantee Go 1.18 compatibility
|
||||
go-version: 1.18
|
||||
# use 1.19 to guarantee Go 1.19 compatibility
|
||||
go-version: '1.19'
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
|
||||
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v6
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-issue-stale: 365
|
||||
days-before-issue-close: 90
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: zeromicro/go-zero-release-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
goversion: "https://dl.google.com/go/go1.18.10.linux-amd64.tar.gz"
|
||||
goversion: "https://dl.google.com/go/go1.19.13.linux-amd64.tar.gz"
|
||||
project_path: "tools/goctl"
|
||||
binary_name: "goctl"
|
||||
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md
|
||||
2
.github/workflows/reviewdog.yml
vendored
2
.github/workflows/reviewdog.yml
vendored
@@ -5,7 +5,7 @@ jobs:
|
||||
name: runner / staticcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: reviewdog/action-staticcheck@v1
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -1,102 +1,76 @@
|
||||
# Contributing
|
||||
# 🚀 Contributing to go-zero
|
||||
|
||||
Welcome to go-zero!
|
||||
Welcome to the go-zero community! We're thrilled to have you here. Contributing to our project is a fantastic way to be a part of the go-zero journey. Let's make this guide exciting and fun!
|
||||
|
||||
- [Before you get started](#before-you-get-started)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Community Expectations](#community-expectations)
|
||||
- [Getting started](#getting-started)
|
||||
- [Your First Contribution](#your-first-contribution)
|
||||
- [Find something to work on](#find-something-to-work-on)
|
||||
- [Find a good first topic](#find-a-good-first-topic)
|
||||
- [Work on an Issue](#work-on-an-issue)
|
||||
- [File an Issue](#file-an-issue)
|
||||
- [Contributor Workflow](#contributor-workflow)
|
||||
- [Creating Pull Requests](#creating-pull-requests)
|
||||
- [Code Review](#code-review)
|
||||
- [Testing](#testing)
|
||||
## 📜 Before You Dive In
|
||||
|
||||
# Before you get started
|
||||
### 🤝 Code of Conduct
|
||||
|
||||
## Code of Conduct
|
||||
Let's start on the right foot. Please take a moment to read and embrace our [Code of Conduct](/code-of-conduct.md). We're all about creating a welcoming and respectful environment.
|
||||
|
||||
Please make sure to read and observe our [Code of Conduct](/code-of-conduct.md).
|
||||
### 🌟 Community Expectations
|
||||
|
||||
## Community Expectations
|
||||
At go-zero, we're like a close-knit family, and we believe in creating a healthy, friendly, and productive atmosphere. It's all about sharing knowledge and building amazing things together.
|
||||
|
||||
go-zero is a community project driven by its community which strives to promote a healthy, friendly and productive environment.
|
||||
go-zero is a web and rpc framework written in Go. It's born to ensure the stability of the busy sites with resilient design. Builtin goctl greatly improves the development productivity.
|
||||
## 🚀 Getting Started
|
||||
|
||||
# Getting started
|
||||
Get your adventure rolling! Here's how to begin:
|
||||
|
||||
- Fork the repository on GitHub.
|
||||
- Make your changes on your fork repository.
|
||||
- Submit a PR.
|
||||
1. 🍴 **Fork the Repository**: Head over to the GitHub repository and fork it to your own space.
|
||||
|
||||
2. 🛠️ **Make Your Magic**: Work your magic in your forked repository. Create new features, squash bugs, or improve documentation - it's your world to conquer!
|
||||
|
||||
# Your First Contribution
|
||||
3. 🚀 **Submit a PR (Pull Request)**: When you're ready to unveil your creation, submit a Pull Request. We can't wait to see your awesome work!
|
||||
|
||||
We will help you to contribute in different areas like filing issues, developing features, fixing critical bugs and
|
||||
getting your work reviewed and merged.
|
||||
## 🌟 Your First Contribution
|
||||
|
||||
If you have questions about the development process,
|
||||
feel free to [file an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
|
||||
We're here to guide you on your quest to become a go-zero contributor. Whether you want to file issues, develop features, or tame some critical bugs, we've got you covered.
|
||||
|
||||
## Find something to work on
|
||||
If you have questions or need guidance at any stage, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
|
||||
|
||||
We are always in need of help, be it fixing documentation, reporting bugs or writing some code.
|
||||
Look at places where you feel best coding practices aren't followed, code refactoring is needed or tests are missing.
|
||||
Here is how you get started.
|
||||
## 🔍 Find Something to Work On
|
||||
|
||||
### Find a good first topic
|
||||
Ready to dive into the action? There are several ways to contribute:
|
||||
|
||||
[go-zero](https://github.com/zeromicro/go-zero) has beginner-friendly issues that provide a good first issue.
|
||||
For example, [go-zero](https://github.com/zeromicro/go-zero) has
|
||||
[help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and
|
||||
[good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
labels for issues that should not need deep knowledge of the system.
|
||||
We can help new contributors who wish to work on such issues.
|
||||
### 💼 Find a Good First Topic
|
||||
|
||||
Another good way to contribute is to find a documentation improvement, such as a missing/broken link.
|
||||
Please see [Contributing](#contributing) below for the workflow.
|
||||
Discover easy-entry issues labeled as [help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These issues are perfect for newcomers and don't require deep knowledge of the system. We're here to assist you with these tasks.
|
||||
|
||||
#### Work on an issue
|
||||
### 🪄 Work on an Issue
|
||||
|
||||
When you are willing to take on an issue, just reply on the issue. The maintainer will assign it to you.
|
||||
Once you've picked an issue that excites you, let us know by commenting on it. Our maintainers will assign it to you, and you can embark on your mission!
|
||||
|
||||
### File an Issue
|
||||
### 📢 File an Issue
|
||||
|
||||
While we encourage everyone to contribute code, it is also appreciated when someone reports an issue.
|
||||
Reporting an issue is just as valuable as code contributions. If you discover a problem, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose). Be sure to follow our guidelines when submitting an issue.
|
||||
|
||||
Please follow the prompted submission guidelines while opening an issue.
|
||||
## 🎯 Contributor Workflow
|
||||
|
||||
# Contributor Workflow
|
||||
Here's a rough guide to your contributor journey:
|
||||
|
||||
Please do not ever hesitate to ask a question or send a pull request.
|
||||
1. 🌱 Create a New Branch: Start by creating a topic branch, usually based on the 'master' branch. This is where your contribution will grow.
|
||||
|
||||
This is a rough outline of what a contributor's workflow looks like:
|
||||
2. 💡 Make Commits: Commit your work in logical units. Each commit should tell a story.
|
||||
|
||||
- Create a topic branch from where to base the contribution. This is usually master.
|
||||
- Make commits of logical units.
|
||||
- Push changes in a topic branch to a personal fork of the repository.
|
||||
- Submit a pull request to [go-zero](https://github.com/zeromicro/go-zero).
|
||||
3. 🚀 Push Changes: Push the changes in your topic branch to your personal fork of the repository.
|
||||
|
||||
## Creating Pull Requests
|
||||
4. 📦 Submit a Pull Request: When your creation is complete, submit a Pull Request to the [go-zero repository](https://github.com/zeromicro/go-zero).
|
||||
|
||||
Pull requests are often called simply "PR".
|
||||
go-zero generally follows the standard [github pull request](https://help.github.com/articles/about-pull-requests/) process.
|
||||
To submit a proposed change, please develop the code/fix and add new test cases.
|
||||
After that, run these local verifications before submitting pull request to predict the pass or
|
||||
fail of continuous integration.
|
||||
## 🌠 Creating Pull Requests
|
||||
|
||||
* Format the code with `gofmt`
|
||||
* Run the test with data race enabled `go test -race ./...`
|
||||
Pull Requests (PRs) are your way of making a grand entrance with your contribution. Here's how to do it:
|
||||
|
||||
## Code Review
|
||||
- 💼 Format Your Code: Ensure your code is beautifully formatted with `gofmt`.
|
||||
- 🏃 Run Tests: Verify that your changes pass all the tests, including data race tests. Run `go test -race ./...` for the ultimate validation.
|
||||
|
||||
To make it easier for your PR to receive reviews, consider the reviewers will need you to:
|
||||
## 👁️🗨️ Code Review
|
||||
|
||||
* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
* write [good commit messages](https://chris.beams.io/posts/git-commit/).
|
||||
* break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue.
|
||||
Getting your PR reviewed is the final step before your contribution becomes part of go-zero's magical world. To make the process smooth, keep these things in mind:
|
||||
|
||||
- 🧙♀️ Follow Good Coding Practices: Stick to [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
- 📝 Write Awesome Commit Messages: Craft [impressive commit messages](https://chris.beams.io/posts/git-commit/) - they're like spells in the wizard's book!
|
||||
- 🔍 Break It Down: For larger changes, consider breaking them into a series of smaller, logical patches. Each patch should make an understandable and meaningful improvement.
|
||||
|
||||
Congratulations on your contribution journey! We're thrilled to have you as part of our go-zero community. Let's make amazing things together! 🌟
|
||||
|
||||
Now, go out there and start your adventure! If you have any more magical ideas to enhance this guide, please share them. 🔥
|
||||
16
SECURITY.md
Normal file
16
SECURITY.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We publish releases monthly.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >= 1.4.4 | :white_check_mark: |
|
||||
| < 1.4.4 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
https://github.com/zeromicro/go-zero/security/advisories
|
||||
|
||||
Accepted vulnerabilities are expected to be fixed within a month.
|
||||
@@ -1,76 +1,127 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
@@ -2,6 +2,7 @@ package bloom
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
@@ -17,19 +18,13 @@ var (
|
||||
// ErrTooLargeOffset indicates the offset is too large in bitset.
|
||||
ErrTooLargeOffset = errors.New("too large offset")
|
||||
|
||||
setScript = redis.NewScript(`
|
||||
for _, offset in ipairs(ARGV) do
|
||||
redis.call("setbit", KEYS[1], offset, 1)
|
||||
end
|
||||
`)
|
||||
testScript = redis.NewScript(`
|
||||
for _, offset in ipairs(ARGV) do
|
||||
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
`)
|
||||
//go:embed setscript.lua
|
||||
setLuaScript string
|
||||
setScript = redis.NewScript(setLuaScript)
|
||||
|
||||
//go:embed testscript.lua
|
||||
testLuaScript string
|
||||
testScript = redis.NewScript(testLuaScript)
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -130,7 +125,7 @@ func (r *redisBitSet) check(ctx context.Context, offsets []uint) (bool, error) {
|
||||
}
|
||||
|
||||
resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args)
|
||||
if err == redis.Nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
@@ -162,7 +157,7 @@ func (r *redisBitSet) set(ctx context.Context, offsets []uint) error {
|
||||
}
|
||||
|
||||
_, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args)
|
||||
if err == redis.Nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
3
core/bloom/setscript.lua
Normal file
3
core/bloom/setscript.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
for _, offset in ipairs(ARGV) do
|
||||
redis.call("setbit", KEYS[1], offset, 1)
|
||||
end
|
||||
6
core/bloom/testscript.lua
Normal file
6
core/bloom/testscript.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
for _, offset in ipairs(ARGV) do
|
||||
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
@@ -1,6 +1,7 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -31,38 +32,53 @@ type (
|
||||
Name() string
|
||||
|
||||
// Allow checks if the request is allowed.
|
||||
// If allowed, a promise will be returned, the caller needs to call promise.Accept()
|
||||
// on success, or call promise.Reject() on failure.
|
||||
// If not allow, ErrServiceUnavailable will be returned.
|
||||
// If allowed, a promise will be returned,
|
||||
// otherwise ErrServiceUnavailable will be returned as the error.
|
||||
// The caller needs to call promise.Accept() on success,
|
||||
// or call promise.Reject() on failure.
|
||||
Allow() (Promise, error)
|
||||
// AllowCtx checks if the request is allowed when ctx isn't done.
|
||||
AllowCtx(ctx context.Context) (Promise, error)
|
||||
|
||||
// Do runs the given request if the Breaker accepts it.
|
||||
// Do returns an error instantly if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
Do(req func() error) error
|
||||
// DoCtx runs the given request if the Breaker accepts it when ctx isn't done.
|
||||
DoCtx(ctx context.Context, req func() error) error
|
||||
|
||||
// DoWithAcceptable runs the given request if the Breaker accepts it.
|
||||
// DoWithAcceptable returns an error instantly if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
// acceptable checks if it's a successful call, even if the err is not nil.
|
||||
// acceptable checks if it's a successful call, even if the error is not nil.
|
||||
DoWithAcceptable(req func() error, acceptable Acceptable) error
|
||||
// DoWithAcceptableCtx runs the given request if the Breaker accepts it when ctx isn't done.
|
||||
DoWithAcceptableCtx(ctx context.Context, req func() error, acceptable Acceptable) error
|
||||
|
||||
// DoWithFallback runs the given request if the Breaker accepts it.
|
||||
// DoWithFallback runs the fallback if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
DoWithFallback(req func() error, fallback func(err error) error) error
|
||||
DoWithFallback(req func() error, fallback Fallback) error
|
||||
// DoWithFallbackCtx runs the given request if the Breaker accepts it when ctx isn't done.
|
||||
DoWithFallbackCtx(ctx context.Context, req func() error, fallback Fallback) error
|
||||
|
||||
// DoWithFallbackAcceptable runs the given request if the Breaker accepts it.
|
||||
// DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
// acceptable checks if it's a successful call, even if the err is not nil.
|
||||
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
|
||||
// acceptable checks if it's a successful call, even if the error is not nil.
|
||||
DoWithFallbackAcceptable(req func() error, fallback Fallback, acceptable Acceptable) error
|
||||
// DoWithFallbackAcceptableCtx runs the given request if the Breaker accepts it when ctx isn't done.
|
||||
DoWithFallbackAcceptableCtx(ctx context.Context, req func() error, fallback Fallback,
|
||||
acceptable Acceptable) error
|
||||
}
|
||||
|
||||
// Fallback is the func to be called if the request is rejected.
|
||||
Fallback func(err error) error
|
||||
|
||||
// Option defines the method to customize a Breaker.
|
||||
Option func(breaker *circuitBreaker)
|
||||
|
||||
@@ -86,12 +102,12 @@ type (
|
||||
|
||||
internalThrottle interface {
|
||||
allow() (internalPromise, error)
|
||||
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error
|
||||
doReq(req func() error, fallback Fallback, acceptable Acceptable) error
|
||||
}
|
||||
|
||||
throttle interface {
|
||||
allow() (Promise, error)
|
||||
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error
|
||||
doReq(req func() error, fallback Fallback, acceptable Acceptable) error
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,23 +130,71 @@ func (cb *circuitBreaker) Allow() (Promise, error) {
|
||||
return cb.throttle.allow()
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) AllowCtx(ctx context.Context) (Promise, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
return cb.Allow()
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) Do(req func() error) error {
|
||||
return cb.throttle.doReq(req, nil, defaultAcceptable)
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoCtx(ctx context.Context, req func() error) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return cb.Do(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error {
|
||||
return cb.throttle.doReq(req, nil, acceptable)
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithFallback(req func() error, fallback func(err error) error) error {
|
||||
func (cb *circuitBreaker) DoWithAcceptableCtx(ctx context.Context, req func() error,
|
||||
acceptable Acceptable) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return cb.DoWithAcceptable(req, acceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithFallback(req func() error, fallback Fallback) error {
|
||||
return cb.throttle.doReq(req, fallback, defaultAcceptable)
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error,
|
||||
func (cb *circuitBreaker) DoWithFallbackCtx(ctx context.Context, req func() error,
|
||||
fallback Fallback) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return cb.DoWithFallback(req, fallback)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback Fallback,
|
||||
acceptable Acceptable) error {
|
||||
return cb.throttle.doReq(req, fallback, acceptable)
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) DoWithFallbackAcceptableCtx(ctx context.Context, req func() error,
|
||||
fallback Fallback, acceptable Acceptable) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return cb.DoWithFallbackAcceptable(req, fallback, acceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *circuitBreaker) Name() string {
|
||||
return cb.name
|
||||
}
|
||||
@@ -168,7 +232,7 @@ func (lt loggedThrottle) allow() (Promise, error) {
|
||||
}, lt.logError(err)
|
||||
}
|
||||
|
||||
func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
|
||||
func (lt loggedThrottle) doReq(req func() error, fallback Fallback, acceptable Acceptable) error {
|
||||
return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool {
|
||||
accept := acceptable(err)
|
||||
if !accept && err != nil {
|
||||
@@ -179,7 +243,7 @@ func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error,
|
||||
}
|
||||
|
||||
func (lt loggedThrottle) logError(err error) error {
|
||||
if err == ErrServiceUnavailable {
|
||||
if errors.Is(err, ErrServiceUnavailable) {
|
||||
// if circuit open, not possible to have empty error window
|
||||
stat.Report(fmt.Sprintf(
|
||||
"proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stat"
|
||||
@@ -16,10 +18,274 @@ func init() {
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_Allow(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
_, err := b.Allow()
|
||||
assert.Nil(t, err)
|
||||
t.Run("allow", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
_, err := b.Allow()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("allow with ctx", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
_, err := b.AllowCtx(context.Background())
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("allow with ctx timeout", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
|
||||
defer cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
_, err := b.AllowCtx(ctx)
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("allow with ctx cancel", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
cancel()
|
||||
_, err := b.AllowCtx(ctx)
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
_, err := b.AllowCtx(context.Background())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_Do(t *testing.T) {
|
||||
t.Run("do", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.Do(func() error {
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("do with ctx", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoCtx(context.Background(), func() error {
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("do with ctx timeout", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
|
||||
defer cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
err := b.DoCtx(ctx, func() error {
|
||||
return nil
|
||||
})
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("do with ctx cancel", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
cancel()
|
||||
err := b.DoCtx(ctx, func() error {
|
||||
return nil
|
||||
})
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
assert.NoError(t, b.DoCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_DoWithAcceptable(t *testing.T) {
|
||||
t.Run("doWithAcceptable", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithAcceptable(func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithAcceptable with ctx", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithAcceptableCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithAcceptable with ctx timeout", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
|
||||
defer cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
err := b.DoWithAcceptableCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("doWithAcceptable with ctx cancel", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
cancel()
|
||||
err := b.DoWithAcceptableCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
assert.NoError(t, b.DoWithAcceptableCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_DoWithFallback(t *testing.T) {
|
||||
t.Run("doWithFallback", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithFallback(func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithFallback with ctx", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithFallbackCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithFallback with ctx timeout", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
|
||||
defer cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
err := b.DoWithFallbackCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
})
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("doWithFallback with ctx cancel", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
cancel()
|
||||
err := b.DoWithFallbackCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
})
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
assert.NoError(t, b.DoWithFallbackCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_DoWithFallbackAcceptable(t *testing.T) {
|
||||
t.Run("doWithFallbackAcceptable", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithFallbackAcceptable(func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithFallbackAcceptable with ctx", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
err := b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("doWithFallbackAcceptable with ctx timeout", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
|
||||
defer cancel()
|
||||
time.Sleep(time.Millisecond)
|
||||
err := b.DoWithFallbackAcceptableCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("doWithFallbackAcceptable with ctx cancel", func(t *testing.T) {
|
||||
b := NewBreaker()
|
||||
assert.True(t, len(b.Name()) > 0)
|
||||
for i := 0; i < 100; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
cancel()
|
||||
err := b.DoWithFallbackAcceptableCtx(ctx, func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
})
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
assert.NoError(t, b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogReason(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package breaker
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
lock sync.RWMutex
|
||||
@@ -14,6 +17,13 @@ func Do(name string, req func() error) error {
|
||||
})
|
||||
}
|
||||
|
||||
// DoCtx calls Breaker.DoCtx on the Breaker with given name.
|
||||
func DoCtx(ctx context.Context, name string, req func() error) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoCtx(ctx, req)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithAcceptable calls Breaker.DoWithAcceptable on the Breaker with given name.
|
||||
func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
@@ -21,21 +31,44 @@ func DoWithAcceptable(name string, req func() error, acceptable Acceptable) erro
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithAcceptableCtx calls Breaker.DoWithAcceptableCtx on the Breaker with given name.
|
||||
func DoWithAcceptableCtx(ctx context.Context, name string, req func() error,
|
||||
acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithAcceptableCtx(ctx, req, acceptable)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallback calls Breaker.DoWithFallback on the Breaker with given name.
|
||||
func DoWithFallback(name string, req func() error, fallback func(err error) error) error {
|
||||
func DoWithFallback(name string, req func() error, fallback Fallback) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithFallback(req, fallback)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallbackCtx calls Breaker.DoWithFallbackCtx on the Breaker with given name.
|
||||
func DoWithFallbackCtx(ctx context.Context, name string, req func() error, fallback Fallback) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithFallbackCtx(ctx, req, fallback)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallbackAcceptable calls Breaker.DoWithFallbackAcceptable on the Breaker with given name.
|
||||
func DoWithFallbackAcceptable(name string, req func() error, fallback func(err error) error,
|
||||
func DoWithFallbackAcceptable(name string, req func() error, fallback Fallback,
|
||||
acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithFallbackAcceptable(req, fallback, acceptable)
|
||||
})
|
||||
}
|
||||
|
||||
// DoWithFallbackAcceptableCtx calls Breaker.DoWithFallbackAcceptableCtx on the Breaker with given name.
|
||||
func DoWithFallbackAcceptableCtx(ctx context.Context, name string, req func() error,
|
||||
fallback Fallback, acceptable Acceptable) error {
|
||||
return do(name, func(b Breaker) error {
|
||||
return b.DoWithFallbackAcceptableCtx(ctx, req, fallback, acceptable)
|
||||
})
|
||||
}
|
||||
|
||||
// GetBreaker returns the Breaker with the given name.
|
||||
func GetBreaker(name string) Breaker {
|
||||
lock.RLock()
|
||||
@@ -59,7 +92,7 @@ func GetBreaker(name string) Breaker {
|
||||
// NoBreakerFor disables the circuit breaker for the given name.
|
||||
func NoBreakerFor(name string) {
|
||||
lock.Lock()
|
||||
breakers[name] = newNoOpBreaker()
|
||||
breakers[name] = NopBreaker()
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
@@ -22,6 +23,9 @@ func TestBreakersDo(t *testing.T) {
|
||||
assert.Equal(t, errDummy, Do("any", func() error {
|
||||
return errDummy
|
||||
}))
|
||||
assert.Equal(t, errDummy, DoCtx(context.Background(), "any", func() error {
|
||||
return errDummy
|
||||
}))
|
||||
}
|
||||
|
||||
func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
@@ -30,7 +34,7 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
assert.Equal(t, errDummy, GetBreaker("anyone").DoWithAcceptable(func() error {
|
||||
return errDummy
|
||||
}, func(err error) bool {
|
||||
return err == nil || err == errDummy
|
||||
return err == nil || errors.Is(err, errDummy)
|
||||
}))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
@@ -38,6 +42,13 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
return nil
|
||||
}) == nil
|
||||
})
|
||||
verify(t, func() bool {
|
||||
return DoWithAcceptableCtx(context.Background(), "anyone", func() error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return true
|
||||
}) == nil
|
||||
})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
err := DoWithAcceptable("another", func() error {
|
||||
@@ -45,12 +56,12 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
})
|
||||
assert.True(t, err == errDummy || err == ErrServiceUnavailable)
|
||||
assert.True(t, errors.Is(err, errDummy) || errors.Is(err, ErrServiceUnavailable))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("another", func() error {
|
||||
return errors.Is(Do("another", func() error {
|
||||
return nil
|
||||
})
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,18 +86,24 @@ func TestBreakersFallback(t *testing.T) {
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
})
|
||||
assert.True(t, err == nil || err == errDummy)
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("fallback", func() error {
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
err = DoWithFallbackCtx(context.Background(), "fallback", func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
})
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return errors.Is(Do("fallback", func() error {
|
||||
return nil
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBreakersAcceptableFallback(t *testing.T) {
|
||||
errDummy := errors.New("any")
|
||||
for i := 0; i < 10000; i++ {
|
||||
for i := 0; i < 5000; i++ {
|
||||
err := DoWithFallbackAcceptable("acceptablefallback", func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
@@ -94,12 +111,20 @@ func TestBreakersAcceptableFallback(t *testing.T) {
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
})
|
||||
assert.True(t, err == nil || err == errDummy)
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
err = DoWithFallbackAcceptableCtx(context.Background(), "acceptablefallback", func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
})
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("acceptablefallback", func() error {
|
||||
return errors.Is(Do("acceptablefallback", func() error {
|
||||
return nil
|
||||
})
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
48
core/breaker/bucket.go
Normal file
48
core/breaker/bucket.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package breaker
|
||||
|
||||
const (
|
||||
success = iota
|
||||
fail
|
||||
drop
|
||||
)
|
||||
|
||||
// bucket defines the bucket that holds sum and num of additions.
|
||||
type bucket struct {
|
||||
Sum int64
|
||||
Success int64
|
||||
Failure int64
|
||||
Drop int64
|
||||
}
|
||||
|
||||
func (b *bucket) Add(v int64) {
|
||||
switch v {
|
||||
case fail:
|
||||
b.fail()
|
||||
case drop:
|
||||
b.drop()
|
||||
default:
|
||||
b.succeed()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bucket) Reset() {
|
||||
b.Sum = 0
|
||||
b.Success = 0
|
||||
b.Failure = 0
|
||||
b.Drop = 0
|
||||
}
|
||||
|
||||
func (b *bucket) drop() {
|
||||
b.Sum++
|
||||
b.Drop++
|
||||
}
|
||||
|
||||
func (b *bucket) fail() {
|
||||
b.Sum++
|
||||
b.Failure++
|
||||
}
|
||||
|
||||
func (b *bucket) succeed() {
|
||||
b.Sum++
|
||||
b.Success++
|
||||
}
|
||||
43
core/breaker/bucket_test.go
Normal file
43
core/breaker/bucket_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBucketAdd(t *testing.T) {
|
||||
b := &bucket{}
|
||||
|
||||
// Test succeed
|
||||
b.Add(0) // Using 0 for success
|
||||
assert.Equal(t, int64(1), b.Sum, "Sum should be incremented")
|
||||
assert.Equal(t, int64(1), b.Success, "Success should be incremented")
|
||||
assert.Equal(t, int64(0), b.Failure, "Failure should not be incremented")
|
||||
assert.Equal(t, int64(0), b.Drop, "Drop should not be incremented")
|
||||
|
||||
// Test failure
|
||||
b.Add(fail)
|
||||
assert.Equal(t, int64(2), b.Sum, "Sum should be incremented")
|
||||
assert.Equal(t, int64(1), b.Failure, "Failure should be incremented")
|
||||
assert.Equal(t, int64(0), b.Drop, "Drop should not be incremented")
|
||||
|
||||
// Test drop
|
||||
b.Add(drop)
|
||||
assert.Equal(t, int64(3), b.Sum, "Sum should be incremented")
|
||||
assert.Equal(t, int64(1), b.Drop, "Drop should be incremented")
|
||||
}
|
||||
|
||||
func TestBucketReset(t *testing.T) {
|
||||
b := &bucket{
|
||||
Sum: 3,
|
||||
Success: 1,
|
||||
Failure: 1,
|
||||
Drop: 1,
|
||||
}
|
||||
b.Reset()
|
||||
assert.Equal(t, int64(0), b.Sum, "Sum should be reset to 0")
|
||||
assert.Equal(t, int64(0), b.Success, "Success should be reset to 0")
|
||||
assert.Equal(t, int64(0), b.Failure, "Failure should be reset to 0")
|
||||
assert.Equal(t, int64(0), b.Drop, "Drop should be reset to 0")
|
||||
}
|
||||
@@ -1,57 +1,99 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/collection"
|
||||
"github.com/zeromicro/go-zero/core/mathx"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
)
|
||||
|
||||
const (
|
||||
// 250ms for bucket duration
|
||||
window = time.Second * 10
|
||||
buckets = 40
|
||||
k = 1.5
|
||||
protection = 5
|
||||
window = time.Second * 10
|
||||
buckets = 40
|
||||
maxFailBucketsToDecreaseK = 30
|
||||
minBucketsToSpeedUp = 3
|
||||
forcePassDuration = time.Second
|
||||
k = 1.5
|
||||
minK = 1.1
|
||||
recoveryK = 4 - k
|
||||
protection = 5
|
||||
)
|
||||
|
||||
// googleBreaker is a netflixBreaker pattern from google.
|
||||
// see Client-Side Throttling section in https://landing.google.com/sre/sre-book/chapters/handling-overload/
|
||||
type googleBreaker struct {
|
||||
k float64
|
||||
stat *collection.RollingWindow
|
||||
proba *mathx.Proba
|
||||
}
|
||||
type (
|
||||
googleBreaker struct {
|
||||
k float64
|
||||
stat *collection.RollingWindow[int64, *bucket]
|
||||
proba *mathx.Proba
|
||||
lastPass *syncx.AtomicDuration
|
||||
}
|
||||
|
||||
windowResult struct {
|
||||
accepts int64
|
||||
total int64
|
||||
failingBuckets int64
|
||||
workingBuckets int64
|
||||
}
|
||||
)
|
||||
|
||||
func newGoogleBreaker() *googleBreaker {
|
||||
bucketDuration := time.Duration(int64(window) / int64(buckets))
|
||||
st := collection.NewRollingWindow(buckets, bucketDuration)
|
||||
st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
|
||||
return new(bucket)
|
||||
}, buckets, bucketDuration)
|
||||
return &googleBreaker{
|
||||
stat: st,
|
||||
k: k,
|
||||
proba: mathx.NewProba(),
|
||||
stat: st,
|
||||
k: k,
|
||||
proba: mathx.NewProba(),
|
||||
lastPass: syncx.NewAtomicDuration(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *googleBreaker) accept() error {
|
||||
accepts, total := b.history()
|
||||
weightedAccepts := b.k * float64(accepts)
|
||||
var w float64
|
||||
history := b.history()
|
||||
if history.failingBuckets >= minBucketsToSpeedUp {
|
||||
w = b.k - float64(history.failingBuckets-1)*(b.k-minK)/maxFailBucketsToDecreaseK
|
||||
w = mathx.AtLeast(w, minK)
|
||||
} else {
|
||||
w = b.k
|
||||
}
|
||||
weightedAccepts := w * float64(history.accepts)
|
||||
// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
|
||||
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
|
||||
// for better performance, no need to care about the negative ratio
|
||||
dropRatio := (float64(history.total-protection) - weightedAccepts) / float64(history.total+1)
|
||||
if dropRatio <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastPass := b.lastPass.Load()
|
||||
if lastPass > 0 && timex.Since(lastPass) > forcePassDuration {
|
||||
b.lastPass.Set(timex.Now())
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we have more than 2 working buckets, we are in recovery mode,
|
||||
// the latest bucket is the current one, so we ignore it.
|
||||
if history.workingBuckets >= minBucketsToSpeedUp {
|
||||
dropRatio /= recoveryK
|
||||
}
|
||||
|
||||
if b.proba.TrueOnProba(dropRatio) {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
|
||||
b.lastPass.Set(timex.Now())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *googleBreaker) allow() (internalPromise, error) {
|
||||
if err := b.accept(); err != nil {
|
||||
b.markDrop()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -60,8 +102,9 @@ func (b *googleBreaker) allow() (internalPromise, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
|
||||
func (b *googleBreaker) doReq(req func() error, fallback Fallback, acceptable Acceptable) error {
|
||||
if err := b.accept(); err != nil {
|
||||
b.markDrop()
|
||||
if fallback != nil {
|
||||
return fallback(err)
|
||||
}
|
||||
@@ -69,38 +112,55 @@ func (b *googleBreaker) doReq(req func() error, fallback func(err error) error,
|
||||
return err
|
||||
}
|
||||
|
||||
var succ bool
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
// if req() panic, success is false, mark as failure
|
||||
if succ {
|
||||
b.markSuccess()
|
||||
} else {
|
||||
b.markFailure()
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
|
||||
err := req()
|
||||
if acceptable(err) {
|
||||
b.markSuccess()
|
||||
} else {
|
||||
b.markFailure()
|
||||
succ = true
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *googleBreaker) markSuccess() {
|
||||
b.stat.Add(1)
|
||||
func (b *googleBreaker) markDrop() {
|
||||
b.stat.Add(drop)
|
||||
}
|
||||
|
||||
func (b *googleBreaker) markFailure() {
|
||||
b.stat.Add(0)
|
||||
b.stat.Add(fail)
|
||||
}
|
||||
|
||||
func (b *googleBreaker) history() (accepts, total int64) {
|
||||
b.stat.Reduce(func(b *collection.Bucket) {
|
||||
accepts += int64(b.Sum)
|
||||
total += b.Count
|
||||
func (b *googleBreaker) markSuccess() {
|
||||
b.stat.Add(success)
|
||||
}
|
||||
|
||||
func (b *googleBreaker) history() windowResult {
|
||||
var result windowResult
|
||||
|
||||
b.stat.Reduce(func(b *bucket) {
|
||||
result.accepts += b.Success
|
||||
result.total += b.Sum
|
||||
if b.Failure > 0 {
|
||||
result.workingBuckets = 0
|
||||
} else if b.Success > 0 {
|
||||
result.workingBuckets++
|
||||
}
|
||||
if b.Success > 0 {
|
||||
result.failingBuckets = 0
|
||||
} else if b.Failure > 0 {
|
||||
result.failingBuckets++
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
return result
|
||||
}
|
||||
|
||||
type googlePromise struct {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/collection"
|
||||
"github.com/zeromicro/go-zero/core/mathx"
|
||||
"github.com/zeromicro/go-zero/core/stat"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,11 +23,14 @@ func init() {
|
||||
}
|
||||
|
||||
func getGoogleBreaker() *googleBreaker {
|
||||
st := collection.NewRollingWindow(testBuckets, testInterval)
|
||||
st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
|
||||
return new(bucket)
|
||||
}, testBuckets, testInterval)
|
||||
return &googleBreaker{
|
||||
stat: st,
|
||||
k: 5,
|
||||
proba: mathx.NewProba(),
|
||||
stat: st,
|
||||
k: 5,
|
||||
proba: mathx.NewProba(),
|
||||
lastPass: syncx.NewAtomicDuration(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +67,33 @@ func TestGoogleBreakerOpen(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoogleBreakerRecover(t *testing.T) {
|
||||
st := collection.NewRollingWindow[int64, *bucket](func() *bucket {
|
||||
return new(bucket)
|
||||
}, testBuckets*2, testInterval)
|
||||
b := &googleBreaker{
|
||||
stat: st,
|
||||
k: k,
|
||||
proba: mathx.NewProba(),
|
||||
lastPass: syncx.NewAtomicDuration(),
|
||||
}
|
||||
for i := 0; i < testBuckets; i++ {
|
||||
for j := 0; j < 100; j++ {
|
||||
b.stat.Add(1)
|
||||
}
|
||||
time.Sleep(testInterval)
|
||||
}
|
||||
for i := 0; i < testBuckets; i++ {
|
||||
for j := 0; j < 100; j++ {
|
||||
b.stat.Add(0)
|
||||
}
|
||||
time.Sleep(testInterval)
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return b.accept() == nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoogleBreakerFallback(t *testing.T) {
|
||||
b := getGoogleBreaker()
|
||||
markSuccess(b, 1)
|
||||
@@ -89,13 +120,50 @@ func TestGoogleBreakerReject(t *testing.T) {
|
||||
}, nil, defaultAcceptable))
|
||||
}
|
||||
|
||||
func TestGoogleBreakerMoreFallingBuckets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("more falling buckets", func(t *testing.T) {
|
||||
b := getGoogleBreaker()
|
||||
|
||||
func() {
|
||||
stopChan := time.After(testInterval * minBucketsToSpeedUp * 2)
|
||||
for {
|
||||
time.Sleep(time.Millisecond)
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
default:
|
||||
assert.Error(t, b.doReq(func() error {
|
||||
return errors.New("foo")
|
||||
}, func(err error) error {
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var count int
|
||||
for i := 0; i < 100; i++ {
|
||||
if errors.Is(b.doReq(func() error {
|
||||
return ErrServiceUnavailable
|
||||
}, nil, defaultAcceptable), ErrServiceUnavailable) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
assert.True(t, count > 90)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoogleBreakerAcceptable(t *testing.T) {
|
||||
b := getGoogleBreaker()
|
||||
errAcceptable := errors.New("any")
|
||||
assert.Equal(t, errAcceptable, b.doReq(func() error {
|
||||
return errAcceptable
|
||||
}, nil, func(err error) bool {
|
||||
return err == errAcceptable
|
||||
return errors.Is(err, errAcceptable)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -105,7 +173,7 @@ func TestGoogleBreakerNotAcceptable(t *testing.T) {
|
||||
assert.Equal(t, errAcceptable, b.doReq(func() error {
|
||||
return errAcceptable
|
||||
}, nil, func(err error) bool {
|
||||
return err != errAcceptable
|
||||
return !errors.Is(err, errAcceptable)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -164,41 +232,38 @@ func TestGoogleBreakerSelfProtection(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGoogleBreakerHistory(t *testing.T) {
|
||||
var b *googleBreaker
|
||||
var accepts, total int64
|
||||
|
||||
sleep := testInterval
|
||||
t.Run("accepts == total", func(t *testing.T) {
|
||||
b = getGoogleBreaker()
|
||||
b := getGoogleBreaker()
|
||||
markSuccessWithDuration(b, 10, sleep/2)
|
||||
accepts, total = b.history()
|
||||
assert.Equal(t, int64(10), accepts)
|
||||
assert.Equal(t, int64(10), total)
|
||||
result := b.history()
|
||||
assert.Equal(t, int64(10), result.accepts)
|
||||
assert.Equal(t, int64(10), result.total)
|
||||
})
|
||||
|
||||
t.Run("fail == total", func(t *testing.T) {
|
||||
b = getGoogleBreaker()
|
||||
b := getGoogleBreaker()
|
||||
markFailedWithDuration(b, 10, sleep/2)
|
||||
accepts, total = b.history()
|
||||
assert.Equal(t, int64(0), accepts)
|
||||
assert.Equal(t, int64(10), total)
|
||||
result := b.history()
|
||||
assert.Equal(t, int64(0), result.accepts)
|
||||
assert.Equal(t, int64(10), result.total)
|
||||
})
|
||||
|
||||
t.Run("accepts = 1/2 * total, fail = 1/2 * total", func(t *testing.T) {
|
||||
b = getGoogleBreaker()
|
||||
b := getGoogleBreaker()
|
||||
markFailedWithDuration(b, 5, sleep/2)
|
||||
markSuccessWithDuration(b, 5, sleep/2)
|
||||
accepts, total = b.history()
|
||||
assert.Equal(t, int64(5), accepts)
|
||||
assert.Equal(t, int64(10), total)
|
||||
result := b.history()
|
||||
assert.Equal(t, int64(5), result.accepts)
|
||||
assert.Equal(t, int64(10), result.total)
|
||||
})
|
||||
|
||||
t.Run("auto reset rolling counter", func(t *testing.T) {
|
||||
b = getGoogleBreaker()
|
||||
b := getGoogleBreaker()
|
||||
time.Sleep(testInterval * testBuckets)
|
||||
accepts, total = b.history()
|
||||
assert.Equal(t, int64(0), accepts)
|
||||
assert.Equal(t, int64(0), total)
|
||||
result := b.history()
|
||||
assert.Equal(t, int64(0), result.accepts)
|
||||
assert.Equal(t, int64(0), result.total)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -206,7 +271,7 @@ func BenchmarkGoogleBreakerAllow(b *testing.B) {
|
||||
breaker := getGoogleBreaker()
|
||||
b.ResetTimer()
|
||||
for i := 0; i <= b.N; i++ {
|
||||
breaker.accept()
|
||||
_ = breaker.accept()
|
||||
if i%2 == 0 {
|
||||
breaker.markSuccess()
|
||||
} else {
|
||||
@@ -215,6 +280,16 @@ func BenchmarkGoogleBreakerAllow(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleBreakerDoReq(b *testing.B) {
|
||||
breaker := getGoogleBreaker()
|
||||
b.ResetTimer()
|
||||
for i := 0; i <= b.N; i++ {
|
||||
_ = breaker.doReq(func() error {
|
||||
return nil
|
||||
}, nil, defaultAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func markSuccess(b *googleBreaker, count int) {
|
||||
for i := 0; i < count; i++ {
|
||||
p, err := b.allow()
|
||||
|
||||
@@ -1,35 +1,58 @@
|
||||
package breaker
|
||||
|
||||
const noOpBreakerName = "nopBreaker"
|
||||
import "context"
|
||||
|
||||
type noOpBreaker struct{}
|
||||
const nopBreakerName = "nopBreaker"
|
||||
|
||||
func newNoOpBreaker() Breaker {
|
||||
return noOpBreaker{}
|
||||
type nopBreaker struct{}
|
||||
|
||||
// NopBreaker returns a breaker that never trigger breaker circuit.
|
||||
func NopBreaker() Breaker {
|
||||
return nopBreaker{}
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Name() string {
|
||||
return noOpBreakerName
|
||||
func (b nopBreaker) Name() string {
|
||||
return nopBreakerName
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Allow() (Promise, error) {
|
||||
func (b nopBreaker) Allow() (Promise, error) {
|
||||
return nopPromise{}, nil
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Do(req func() error) error {
|
||||
func (b nopBreaker) AllowCtx(_ context.Context) (Promise, error) {
|
||||
return nopPromise{}, nil
|
||||
}
|
||||
|
||||
func (b nopBreaker) Do(req func() error) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
|
||||
func (b nopBreaker) DoCtx(_ context.Context, req func() error) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithFallback(req func() error, _ func(err error) error) error {
|
||||
func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error,
|
||||
_ Acceptable) error {
|
||||
func (b nopBreaker) DoWithAcceptableCtx(_ context.Context, req func() error, _ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b nopBreaker) DoWithFallback(req func() error, _ Fallback) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b nopBreaker) DoWithFallbackCtx(_ context.Context, req func() error, _ Fallback) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b nopBreaker) DoWithFallbackAcceptable(req func() error, _ Fallback, _ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b nopBreaker) DoWithFallbackAcceptableCtx(_ context.Context, req func() error,
|
||||
_ Fallback, _ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
@@ -8,10 +9,12 @@ import (
|
||||
)
|
||||
|
||||
func TestNopBreaker(t *testing.T) {
|
||||
b := newNoOpBreaker()
|
||||
assert.Equal(t, noOpBreakerName, b.Name())
|
||||
b := NopBreaker()
|
||||
assert.Equal(t, nopBreakerName, b.Name())
|
||||
p, err := b.Allow()
|
||||
assert.Nil(t, err)
|
||||
p, err = b.AllowCtx(context.Background())
|
||||
assert.Nil(t, err)
|
||||
p.Accept()
|
||||
for i := 0; i < 1000; i++ {
|
||||
p, err := b.Allow()
|
||||
@@ -21,18 +24,34 @@ func TestNopBreaker(t *testing.T) {
|
||||
assert.Nil(t, b.Do(func() error {
|
||||
return nil
|
||||
}))
|
||||
assert.Nil(t, b.DoCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}))
|
||||
assert.Nil(t, b.DoWithAcceptable(func() error {
|
||||
return nil
|
||||
}, defaultAcceptable))
|
||||
assert.Nil(t, b.DoWithAcceptableCtx(context.Background(), func() error {
|
||||
return nil
|
||||
}, defaultAcceptable))
|
||||
errDummy := errors.New("any")
|
||||
assert.Equal(t, errDummy, b.DoWithFallback(func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
}))
|
||||
assert.Equal(t, errDummy, b.DoWithFallbackCtx(context.Background(), func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
}))
|
||||
assert.Equal(t, errDummy, b.DoWithFallbackAcceptable(func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
}, defaultAcceptable))
|
||||
assert.Equal(t, errDummy, b.DoWithFallbackAcceptableCtx(context.Background(), func() error {
|
||||
return errDummy
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
}, defaultAcceptable))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
zero = big.NewInt(0)
|
||||
)
|
||||
|
||||
// DhKey defines the Diffie Hellman key.
|
||||
// DhKey defines the Diffie-Hellman key.
|
||||
type DhKey struct {
|
||||
PriKey *big.Int
|
||||
PubKey *big.Int
|
||||
@@ -46,7 +46,7 @@ func ComputeKey(pubKey, priKey *big.Int) (*big.Int, error) {
|
||||
return new(big.Int).Exp(pubKey, priKey, p), nil
|
||||
}
|
||||
|
||||
// GenerateKey returns a Diffie Hellman key.
|
||||
// GenerateKey returns a Diffie-Hellman key.
|
||||
func GenerateKey() (*DhKey, error) {
|
||||
var err error
|
||||
var x *big.Int
|
||||
|
||||
@@ -128,8 +128,8 @@ func (c *Cache) Take(key string, fetch func() (any, error)) (any, error) {
|
||||
|
||||
var fresh bool
|
||||
val, err := c.barrier.Do(key, func() (any, error) {
|
||||
// because O(1) on map search in memory, and fetch is an IO query
|
||||
// so we do double check, cache might be taken by another call
|
||||
// because O(1) on map search in memory, and fetch is an IO query,
|
||||
// so we do double-check, cache might be taken by another call
|
||||
if val, ok := c.doGet(key); ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -25,8 +25,14 @@ func (r *Ring) Add(v any) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.elements[r.index%len(r.elements)] = v
|
||||
rlen := len(r.elements)
|
||||
r.elements[r.index%rlen] = v
|
||||
r.index++
|
||||
|
||||
// prevent ring index overflow
|
||||
if r.index >= rlen<<1 {
|
||||
r.index -= rlen
|
||||
}
|
||||
}
|
||||
|
||||
// Take takes all items from r.
|
||||
@@ -36,16 +42,18 @@ func (r *Ring) Take() []any {
|
||||
|
||||
var size int
|
||||
var start int
|
||||
if r.index > len(r.elements) {
|
||||
size = len(r.elements)
|
||||
start = r.index % len(r.elements)
|
||||
rlen := len(r.elements)
|
||||
|
||||
if r.index > rlen {
|
||||
size = rlen
|
||||
start = r.index % rlen
|
||||
} else {
|
||||
size = r.index
|
||||
}
|
||||
|
||||
elements := make([]any, size)
|
||||
for i := 0; i < size; i++ {
|
||||
elements[i] = r.elements[(start+i)%len(r.elements)]
|
||||
elements[i] = r.elements[(start+i)%rlen]
|
||||
}
|
||||
|
||||
return elements
|
||||
|
||||
@@ -4,18 +4,28 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/mathx"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
)
|
||||
|
||||
type (
|
||||
// RollingWindowOption let callers customize the RollingWindow.
|
||||
RollingWindowOption func(rollingWindow *RollingWindow)
|
||||
// BucketInterface is the interface that defines the buckets.
|
||||
BucketInterface[T Numerical] interface {
|
||||
Add(v T)
|
||||
Reset()
|
||||
}
|
||||
|
||||
// RollingWindow defines a rolling window to calculate the events in buckets with time interval.
|
||||
RollingWindow struct {
|
||||
// Numerical is the interface that restricts the numerical type.
|
||||
Numerical = mathx.Numerical
|
||||
|
||||
// RollingWindowOption let callers customize the RollingWindow.
|
||||
RollingWindowOption[T Numerical, B BucketInterface[T]] func(rollingWindow *RollingWindow[T, B])
|
||||
|
||||
// RollingWindow defines a rolling window to calculate the events in buckets with the time interval.
|
||||
RollingWindow[T Numerical, B BucketInterface[T]] struct {
|
||||
lock sync.RWMutex
|
||||
size int
|
||||
win *window
|
||||
win *window[T, B]
|
||||
interval time.Duration
|
||||
offset int
|
||||
ignoreCurrent bool
|
||||
@@ -25,14 +35,15 @@ type (
|
||||
|
||||
// NewRollingWindow returns a RollingWindow that with size buckets and time interval,
|
||||
// use opts to customize the RollingWindow.
|
||||
func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOption) *RollingWindow {
|
||||
func NewRollingWindow[T Numerical, B BucketInterface[T]](newBucket func() B, size int,
|
||||
interval time.Duration, opts ...RollingWindowOption[T, B]) *RollingWindow[T, B] {
|
||||
if size < 1 {
|
||||
panic("size must be greater than 0")
|
||||
}
|
||||
|
||||
w := &RollingWindow{
|
||||
w := &RollingWindow[T, B]{
|
||||
size: size,
|
||||
win: newWindow(size),
|
||||
win: newWindow[T, B](newBucket, size),
|
||||
interval: interval,
|
||||
lastTime: timex.Now(),
|
||||
}
|
||||
@@ -43,7 +54,7 @@ func NewRollingWindow(size int, interval time.Duration, opts ...RollingWindowOpt
|
||||
}
|
||||
|
||||
// Add adds value to current bucket.
|
||||
func (rw *RollingWindow) Add(v float64) {
|
||||
func (rw *RollingWindow[T, B]) Add(v T) {
|
||||
rw.lock.Lock()
|
||||
defer rw.lock.Unlock()
|
||||
rw.updateOffset()
|
||||
@@ -51,13 +62,13 @@ func (rw *RollingWindow) Add(v float64) {
|
||||
}
|
||||
|
||||
// Reduce runs fn on all buckets, ignore current bucket if ignoreCurrent was set.
|
||||
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
|
||||
func (rw *RollingWindow[T, B]) Reduce(fn func(b B)) {
|
||||
rw.lock.RLock()
|
||||
defer rw.lock.RUnlock()
|
||||
|
||||
var diff int
|
||||
span := rw.span()
|
||||
// ignore current bucket, because of partial data
|
||||
// ignore the current bucket, because of partial data
|
||||
if span == 0 && rw.ignoreCurrent {
|
||||
diff = rw.size - 1
|
||||
} else {
|
||||
@@ -69,7 +80,7 @@ func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *RollingWindow) span() int {
|
||||
func (rw *RollingWindow[T, B]) span() int {
|
||||
offset := int(timex.Since(rw.lastTime) / rw.interval)
|
||||
if 0 <= offset && offset < rw.size {
|
||||
return offset
|
||||
@@ -78,7 +89,7 @@ func (rw *RollingWindow) span() int {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *RollingWindow) updateOffset() {
|
||||
func (rw *RollingWindow[T, B]) updateOffset() {
|
||||
span := rw.span()
|
||||
if span <= 0 {
|
||||
return
|
||||
@@ -97,54 +108,54 @@ func (rw *RollingWindow) updateOffset() {
|
||||
}
|
||||
|
||||
// Bucket defines the bucket that holds sum and num of additions.
|
||||
type Bucket struct {
|
||||
Sum float64
|
||||
type Bucket[T Numerical] struct {
|
||||
Sum T
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (b *Bucket) add(v float64) {
|
||||
func (b *Bucket[T]) Add(v T) {
|
||||
b.Sum += v
|
||||
b.Count++
|
||||
}
|
||||
|
||||
func (b *Bucket) reset() {
|
||||
func (b *Bucket[T]) Reset() {
|
||||
b.Sum = 0
|
||||
b.Count = 0
|
||||
}
|
||||
|
||||
type window struct {
|
||||
buckets []*Bucket
|
||||
type window[T Numerical, B BucketInterface[T]] struct {
|
||||
buckets []B
|
||||
size int
|
||||
}
|
||||
|
||||
func newWindow(size int) *window {
|
||||
buckets := make([]*Bucket, size)
|
||||
func newWindow[T Numerical, B BucketInterface[T]](newBucket func() B, size int) *window[T, B] {
|
||||
buckets := make([]B, size)
|
||||
for i := 0; i < size; i++ {
|
||||
buckets[i] = new(Bucket)
|
||||
buckets[i] = newBucket()
|
||||
}
|
||||
return &window{
|
||||
return &window[T, B]{
|
||||
buckets: buckets,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) add(offset int, v float64) {
|
||||
w.buckets[offset%w.size].add(v)
|
||||
func (w *window[T, B]) add(offset int, v T) {
|
||||
w.buckets[offset%w.size].Add(v)
|
||||
}
|
||||
|
||||
func (w *window) reduce(start, count int, fn func(b *Bucket)) {
|
||||
func (w *window[T, B]) reduce(start, count int, fn func(b B)) {
|
||||
for i := 0; i < count; i++ {
|
||||
fn(w.buckets[(start+i)%w.size])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) resetBucket(offset int) {
|
||||
w.buckets[offset%w.size].reset()
|
||||
func (w *window[T, B]) resetBucket(offset int) {
|
||||
w.buckets[offset%w.size].Reset()
|
||||
}
|
||||
|
||||
// IgnoreCurrentBucket lets the Reduce call ignore current bucket.
|
||||
func IgnoreCurrentBucket() RollingWindowOption {
|
||||
return func(w *RollingWindow) {
|
||||
func IgnoreCurrentBucket[T Numerical, B BucketInterface[T]]() RollingWindowOption[T, B] {
|
||||
return func(w *RollingWindow[T, B]) {
|
||||
w.ignoreCurrent = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,24 @@ import (
|
||||
const duration = time.Millisecond * 50
|
||||
|
||||
func TestNewRollingWindow(t *testing.T) {
|
||||
assert.NotNil(t, NewRollingWindow(10, time.Second))
|
||||
assert.NotNil(t, NewRollingWindow[int64, *Bucket[int64]](func() *Bucket[int64] {
|
||||
return new(Bucket[int64])
|
||||
}, 10, time.Second))
|
||||
assert.Panics(t, func() {
|
||||
NewRollingWindow(0, time.Second)
|
||||
NewRollingWindow[int64, *Bucket[int64]](func() *Bucket[int64] {
|
||||
return new(Bucket[int64])
|
||||
}, 0, time.Second)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRollingWindowAdd(t *testing.T) {
|
||||
const size = 3
|
||||
r := NewRollingWindow(size, duration)
|
||||
r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, duration)
|
||||
listBuckets := func() []float64 {
|
||||
var buckets []float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
r.Reduce(func(b *Bucket[float64]) {
|
||||
buckets = append(buckets, b.Sum)
|
||||
})
|
||||
return buckets
|
||||
@@ -47,10 +53,12 @@ func TestRollingWindowAdd(t *testing.T) {
|
||||
|
||||
func TestRollingWindowReset(t *testing.T) {
|
||||
const size = 3
|
||||
r := NewRollingWindow(size, duration, IgnoreCurrentBucket())
|
||||
r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, duration, IgnoreCurrentBucket[float64, *Bucket[float64]]())
|
||||
listBuckets := func() []float64 {
|
||||
var buckets []float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
r.Reduce(func(b *Bucket[float64]) {
|
||||
buckets = append(buckets, b.Sum)
|
||||
})
|
||||
return buckets
|
||||
@@ -72,15 +80,19 @@ func TestRollingWindowReset(t *testing.T) {
|
||||
func TestRollingWindowReduce(t *testing.T) {
|
||||
const size = 4
|
||||
tests := []struct {
|
||||
win *RollingWindow
|
||||
win *RollingWindow[float64, *Bucket[float64]]
|
||||
expect float64
|
||||
}{
|
||||
{
|
||||
win: NewRollingWindow(size, duration),
|
||||
win: NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, duration),
|
||||
expect: 10,
|
||||
},
|
||||
{
|
||||
win: NewRollingWindow(size, duration, IgnoreCurrentBucket()),
|
||||
win: NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, duration, IgnoreCurrentBucket[float64, *Bucket[float64]]()),
|
||||
expect: 4,
|
||||
},
|
||||
}
|
||||
@@ -97,7 +109,7 @@ func TestRollingWindowReduce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
var result float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
r.Reduce(func(b *Bucket[float64]) {
|
||||
result += b.Sum
|
||||
})
|
||||
assert.Equal(t, test.expect, result)
|
||||
@@ -108,10 +120,12 @@ func TestRollingWindowReduce(t *testing.T) {
|
||||
func TestRollingWindowBucketTimeBoundary(t *testing.T) {
|
||||
const size = 3
|
||||
interval := time.Millisecond * 30
|
||||
r := NewRollingWindow(size, interval)
|
||||
r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, interval)
|
||||
listBuckets := func() []float64 {
|
||||
var buckets []float64
|
||||
r.Reduce(func(b *Bucket) {
|
||||
r.Reduce(func(b *Bucket[float64]) {
|
||||
buckets = append(buckets, b.Sum)
|
||||
})
|
||||
return buckets
|
||||
@@ -138,7 +152,9 @@ func TestRollingWindowBucketTimeBoundary(t *testing.T) {
|
||||
|
||||
func TestRollingWindowDataRace(t *testing.T) {
|
||||
const size = 3
|
||||
r := NewRollingWindow(size, duration)
|
||||
r := NewRollingWindow[float64, *Bucket[float64]](func() *Bucket[float64] {
|
||||
return new(Bucket[float64])
|
||||
}, size, duration)
|
||||
stop := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
@@ -157,7 +173,7 @@ func TestRollingWindowDataRace(t *testing.T) {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
r.Reduce(func(b *Bucket) {})
|
||||
r.Reduce(func(b *Bucket[float64]) {})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -29,6 +29,8 @@ func NewSafeMap() *SafeMap {
|
||||
// Del deletes the value with the given key from m.
|
||||
func (m *SafeMap) Del(key any) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if _, ok := m.dirtyOld[key]; ok {
|
||||
delete(m.dirtyOld, key)
|
||||
m.deletionOld++
|
||||
@@ -52,7 +54,6 @@ func (m *SafeMap) Del(key any) {
|
||||
m.dirtyNew = make(map[any]any)
|
||||
m.deletionNew = 0
|
||||
}
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Get gets the value with the given key from m.
|
||||
@@ -89,6 +90,8 @@ func (m *SafeMap) Range(f func(key, val any) bool) {
|
||||
// Set sets the value into m with the given key.
|
||||
func (m *SafeMap) Set(key, value any) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.deletionOld <= maxDeletion {
|
||||
if _, ok := m.dirtyNew[key]; ok {
|
||||
delete(m.dirtyNew, key)
|
||||
@@ -102,7 +105,6 @@ func (m *SafeMap) Set(key, value any) {
|
||||
}
|
||||
m.dirtyNew[key] = value
|
||||
}
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Size returns the size of m.
|
||||
|
||||
@@ -147,3 +147,65 @@ func TestSafeMap_Range(t *testing.T) {
|
||||
assert.Equal(t, m.dirtyNew, newMap.dirtyNew)
|
||||
assert.Equal(t, m.dirtyOld, newMap.dirtyOld)
|
||||
}
|
||||
|
||||
func TestSetManyTimes(t *testing.T) {
|
||||
const iteration = maxDeletion * 2
|
||||
m := NewSafeMap()
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
var count int
|
||||
m.Range(func(k, v any) bool {
|
||||
count++
|
||||
return count < maxDeletion/2
|
||||
})
|
||||
assert.Equal(t, maxDeletion/2, count)
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
|
||||
count = 0
|
||||
m.Range(func(k, v any) bool {
|
||||
count++
|
||||
return count < maxDeletion
|
||||
})
|
||||
assert.Equal(t, maxDeletion, count)
|
||||
}
|
||||
|
||||
func TestSetManyTimesNew(t *testing.T) {
|
||||
m := NewSafeMap()
|
||||
for i := 0; i < maxDeletion*3; i++ {
|
||||
m.Set(i, i)
|
||||
}
|
||||
for i := 0; i < maxDeletion*2; i++ {
|
||||
m.Del(i)
|
||||
}
|
||||
for i := 0; i < maxDeletion*3; i++ {
|
||||
m.Set(i+maxDeletion*3, i+maxDeletion*3)
|
||||
}
|
||||
for i := 0; i < maxDeletion*2; i++ {
|
||||
m.Del(i + maxDeletion*2)
|
||||
}
|
||||
for i := 0; i < maxDeletion-copyThreshold+1; i++ {
|
||||
m.Del(i + maxDeletion*2)
|
||||
}
|
||||
assert.Equal(t, 0, len(m.dirtyNew))
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo, fullName st
|
||||
return newConflictKeyError(fullName)
|
||||
}
|
||||
|
||||
if err := mergeFields(prev, key, child.children, fullName); err != nil {
|
||||
if err := mergeFields(prev, child.children, fullName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -281,7 +281,7 @@ func getTagName(field reflect.StructField) string {
|
||||
return field.Name
|
||||
}
|
||||
|
||||
func mergeFields(prev *fieldInfo, key string, children map[string]*fieldInfo, fullName string) error {
|
||||
func mergeFields(prev *fieldInfo, children map[string]*fieldInfo, fullName string) error {
|
||||
if len(prev.children) == 0 || len(children) == 0 {
|
||||
return newConflictKeyError(fullName)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type RestfulConf struct {
|
||||
MaxConns int `json:",default=10000"`
|
||||
MaxBytes int64 `json:",default=1048576"`
|
||||
Timeout time.Duration `json:",default=3s"`
|
||||
CpuThreshold int64 `json:",default=900,range=[0:1000]"`
|
||||
CpuThreshold int64 `json:",default=900,range=[0:1000)"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/core/threading"
|
||||
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
@@ -22,6 +24,7 @@ var (
|
||||
clusters: make(map[string]*cluster),
|
||||
}
|
||||
connManager = syncx.NewResourceManager()
|
||||
errClosed = errors.New("etcd monitor chan has been closed")
|
||||
)
|
||||
|
||||
// A Registry is a registry that manages the etcd client connections.
|
||||
@@ -219,7 +222,7 @@ func (c *cluster) load(cli EtcdClient, key string) int64 {
|
||||
break
|
||||
}
|
||||
|
||||
logx.Error(err)
|
||||
logx.Errorf("%s, key is %s", err.Error(), key)
|
||||
time.Sleep(coolDownInterval)
|
||||
}
|
||||
|
||||
@@ -288,40 +291,47 @@ func (c *cluster) reload(cli EtcdClient) {
|
||||
|
||||
func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
|
||||
for {
|
||||
if c.watchStream(cli, key, rev) {
|
||||
err := c.watchStream(cli, key, rev)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if rev != 0 && errors.Is(err, rpctypes.ErrCompacted) {
|
||||
logx.Errorf("etcd watch stream has been compacted, try to reload, rev %d", rev)
|
||||
rev = c.load(cli, key)
|
||||
}
|
||||
|
||||
// log the error and retry
|
||||
logx.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool {
|
||||
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error {
|
||||
var rch clientv3.WatchChan
|
||||
if rev != 0 {
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix(),
|
||||
clientv3.WithRev(rev+1))
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
|
||||
clientv3.WithPrefix(), clientv3.WithRev(rev+1))
|
||||
} else {
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
|
||||
clientv3.WithPrefix())
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case wresp, ok := <-rch:
|
||||
if !ok {
|
||||
logx.Error("etcd monitor chan has been closed")
|
||||
return false
|
||||
return errClosed
|
||||
}
|
||||
if wresp.Canceled {
|
||||
logx.Errorf("etcd monitor chan has been canceled, error: %v", wresp.Err())
|
||||
return false
|
||||
return fmt.Errorf("etcd monitor chan has been canceled, error: %w", wresp.Err())
|
||||
}
|
||||
if wresp.Err() != nil {
|
||||
logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err()))
|
||||
return false
|
||||
return fmt.Errorf("etcd monitor chan error: %w", wresp.Err())
|
||||
}
|
||||
|
||||
c.handleWatchEvents(key, wresp.Events)
|
||||
case <-c.done:
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package errorx
|
||||
|
||||
import "bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
// A BatchError is an error that can hold multiple errors.
|
||||
BatchError struct {
|
||||
errs errorArray
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
errorArray []error
|
||||
@@ -13,6 +17,9 @@ type (
|
||||
|
||||
// Add adds errs to be, nil errors are ignored.
|
||||
func (be *BatchError) Add(errs ...error) {
|
||||
be.lock.Lock()
|
||||
defer be.lock.Unlock()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
be.errs = append(be.errs, err)
|
||||
@@ -22,6 +29,9 @@ func (be *BatchError) Add(errs ...error) {
|
||||
|
||||
// Err returns an error that represents all errors.
|
||||
func (be *BatchError) Err() error {
|
||||
be.lock.Lock()
|
||||
defer be.lock.Unlock()
|
||||
|
||||
switch len(be.errs) {
|
||||
case 0:
|
||||
return nil
|
||||
@@ -34,6 +44,9 @@ func (be *BatchError) Err() error {
|
||||
|
||||
// NotNil checks if any error inside.
|
||||
func (be *BatchError) NotNil() bool {
|
||||
be.lock.Lock()
|
||||
defer be.lock.Unlock()
|
||||
|
||||
return len(be.errs) > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package errorx
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,7 +34,7 @@ func TestBatchErrorNilFromFunc(t *testing.T) {
|
||||
func TestBatchErrorOneError(t *testing.T) {
|
||||
var batch BatchError
|
||||
batch.Add(errors.New(err1))
|
||||
assert.NotNil(t, batch)
|
||||
assert.NotNil(t, batch.Err())
|
||||
assert.Equal(t, err1, batch.Err().Error())
|
||||
assert.True(t, batch.NotNil())
|
||||
}
|
||||
@@ -42,7 +43,26 @@ func TestBatchErrorWithErrors(t *testing.T) {
|
||||
var batch BatchError
|
||||
batch.Add(errors.New(err1))
|
||||
batch.Add(errors.New(err2))
|
||||
assert.NotNil(t, batch)
|
||||
assert.NotNil(t, batch.Err())
|
||||
assert.Equal(t, fmt.Sprintf("%s\n%s", err1, err2), batch.Err().Error())
|
||||
assert.True(t, batch.NotNil())
|
||||
}
|
||||
|
||||
func TestBatchErrorConcurrentAdd(t *testing.T) {
|
||||
const count = 10000
|
||||
var batch BatchError
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(count)
|
||||
for i := 0; i < count; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
batch.Add(errors.New(err1))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
assert.NotNil(t, batch.Err())
|
||||
assert.Equal(t, count, len(batch.errs))
|
||||
assert.True(t, batch.NotNil())
|
||||
}
|
||||
|
||||
14
core/errorx/check.go
Normal file
14
core/errorx/check.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package errorx
|
||||
|
||||
import "errors"
|
||||
|
||||
// In checks if the given err is one of errs.
|
||||
func In(err error, errs ...error) bool {
|
||||
for _, each := range errs {
|
||||
if errors.Is(err, each) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
70
core/errorx/check_test.go
Normal file
70
core/errorx/check_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIn(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
err3 := errors.New("error 3")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
errs []error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Error matches one of the errors in the list",
|
||||
err: err1,
|
||||
errs: []error{err1, err2},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Error does not match any errors in the list",
|
||||
err: err3,
|
||||
errs: []error{err1, err2},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty error list",
|
||||
err: err1,
|
||||
errs: []error{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Nil error with non-nil list",
|
||||
err: nil,
|
||||
errs: []error{err1, err2},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Non-nil error with nil in list",
|
||||
err: err1,
|
||||
errs: []error{nil, err2},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Error matches nil error in the list",
|
||||
err: nil,
|
||||
errs: []error{nil, err2},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Nil error with empty list",
|
||||
err: nil,
|
||||
errs: []error{},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := In(tt.err, tt.errs...); got != tt.want {
|
||||
t.Errorf("In() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import "gopkg.in/cheggaaa/pb.v1"
|
||||
type (
|
||||
// A Scanner is used to read lines.
|
||||
Scanner interface {
|
||||
// Scan checks if has remaining to read.
|
||||
// Scan checks if it has remaining to read.
|
||||
Scan() bool
|
||||
// Text returns next line.
|
||||
Text() string
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package fx
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/threading"
|
||||
import (
|
||||
"github.com/zeromicro/go-zero/core/errorx"
|
||||
"github.com/zeromicro/go-zero/core/threading"
|
||||
)
|
||||
|
||||
// Parallel runs fns parallelly and waits for done.
|
||||
func Parallel(fns ...func()) {
|
||||
@@ -10,3 +13,20 @@ func Parallel(fns ...func()) {
|
||||
}
|
||||
group.Wait()
|
||||
}
|
||||
|
||||
func ParallelErr(fns ...func() error) error {
|
||||
var be errorx.BatchError
|
||||
|
||||
group := threading.NewRoutineGroup()
|
||||
for _, fn := range fns {
|
||||
f := fn
|
||||
group.RunSafe(func() {
|
||||
if err := f(); err != nil {
|
||||
be.Add(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
group.Wait()
|
||||
|
||||
return be.Err()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -22,3 +23,54 @@ func TestParallel(t *testing.T) {
|
||||
})
|
||||
assert.Equal(t, int32(6), count)
|
||||
}
|
||||
|
||||
func TestParallelErr(t *testing.T) {
|
||||
var count int32
|
||||
err := ParallelErr(
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 1)
|
||||
return errors.New("failed to exec #1")
|
||||
},
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 2)
|
||||
return errors.New("failed to exec #2")
|
||||
|
||||
},
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 3)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, int32(6), count)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "failed to exec #1", "failed to exec #2")
|
||||
}
|
||||
|
||||
func TestParallelErrErrorNil(t *testing.T) {
|
||||
var count int32
|
||||
err := ParallelErr(
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 1)
|
||||
return nil
|
||||
},
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 2)
|
||||
return nil
|
||||
|
||||
},
|
||||
func() error {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
atomic.AddInt32(&count, 3)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, int32(6), count)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -10,16 +10,15 @@ import (
|
||||
|
||||
const defaultRetryTimes = 3
|
||||
|
||||
var errTimeout = errors.New("retry timeout")
|
||||
|
||||
type (
|
||||
// RetryOption defines the method to customize DoWithRetry.
|
||||
RetryOption func(*retryOptions)
|
||||
|
||||
retryOptions struct {
|
||||
times int
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
times int
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
ignoreErrors []error
|
||||
}
|
||||
)
|
||||
|
||||
@@ -28,7 +27,7 @@ type (
|
||||
// and performs modification operations, it is best to lock them,
|
||||
// otherwise there may be data race issues
|
||||
func DoWithRetry(fn func() error, opts ...RetryOption) error {
|
||||
return retry(func(errChan chan error, retryCount int) {
|
||||
return retry(context.Background(), func(errChan chan error, retryCount int) {
|
||||
errChan <- fn()
|
||||
}, opts...)
|
||||
}
|
||||
@@ -40,12 +39,12 @@ func DoWithRetry(fn func() error, opts ...RetryOption) error {
|
||||
// otherwise there may be data race issues
|
||||
func DoWithRetryCtx(ctx context.Context, fn func(ctx context.Context, retryCount int) error,
|
||||
opts ...RetryOption) error {
|
||||
return retry(func(errChan chan error, retryCount int) {
|
||||
return retry(ctx, func(errChan chan error, retryCount int) {
|
||||
errChan <- fn(ctx, retryCount)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
|
||||
func retry(ctx context.Context, fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
|
||||
options := newRetryOptions()
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
@@ -53,7 +52,6 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
|
||||
|
||||
var berr errorx.BatchError
|
||||
var cancelFunc context.CancelFunc
|
||||
ctx := context.Background()
|
||||
if options.timeout > 0 {
|
||||
ctx, cancelFunc = context.WithTimeout(ctx, options.timeout)
|
||||
defer cancelFunc()
|
||||
@@ -66,19 +64,24 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
for _, ignoreErr := range options.ignoreErrors {
|
||||
if errors.Is(err, ignoreErr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
berr.Add(err)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
berr.Add(errTimeout)
|
||||
berr.Add(ctx.Err())
|
||||
return berr.Err()
|
||||
}
|
||||
|
||||
if options.interval > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
berr.Add(errTimeout)
|
||||
berr.Add(ctx.Err())
|
||||
return berr.Err()
|
||||
case <-time.After(options.interval):
|
||||
}
|
||||
@@ -88,19 +91,28 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
|
||||
return berr.Err()
|
||||
}
|
||||
|
||||
// WithRetry customize a DoWithRetry call with given retry times.
|
||||
func WithRetry(times int) RetryOption {
|
||||
// WithIgnoreErrors Ignore the specified errors
|
||||
func WithIgnoreErrors(ignoreErrors []error) RetryOption {
|
||||
return func(options *retryOptions) {
|
||||
options.times = times
|
||||
options.ignoreErrors = ignoreErrors
|
||||
}
|
||||
}
|
||||
|
||||
// WithInterval customizes a DoWithRetry call with given interval.
|
||||
func WithInterval(interval time.Duration) RetryOption {
|
||||
return func(options *retryOptions) {
|
||||
options.interval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetry customizes a DoWithRetry call with given retry times.
|
||||
func WithRetry(times int) RetryOption {
|
||||
return func(options *retryOptions) {
|
||||
options.times = times
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout customizes a DoWithRetry call with given timeout.
|
||||
func WithTimeout(timeout time.Duration) RetryOption {
|
||||
return func(options *retryOptions) {
|
||||
options.timeout = timeout
|
||||
|
||||
@@ -97,20 +97,70 @@ func TestRetryWithInterval(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestRetryCtx(t *testing.T) {
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 0 {
|
||||
return errors.New("any")
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return nil
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
func TestRetryWithWithIgnoreErrors(t *testing.T) {
|
||||
ignoreErr1 := errors.New("ignore error1")
|
||||
ignoreErr2 := errors.New("ignore error2")
|
||||
ignoreErrs := []error{ignoreErr1, ignoreErr2}
|
||||
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 1 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any ")
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
assert.Nil(t, DoWithRetry(func() error {
|
||||
return ignoreErr1
|
||||
}, WithIgnoreErrors(ignoreErrs)))
|
||||
|
||||
assert.Nil(t, DoWithRetry(func() error {
|
||||
return ignoreErr2
|
||||
}, WithIgnoreErrors(ignoreErrs)))
|
||||
|
||||
assert.NotNil(t, DoWithRetry(func() error {
|
||||
return errors.New("any")
|
||||
}))
|
||||
}
|
||||
|
||||
func TestRetryCtx(t *testing.T) {
|
||||
t.Run("with timeout", func(t *testing.T) {
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 0 {
|
||||
return errors.New("any")
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return nil
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 1 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any ")
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
})
|
||||
|
||||
t.Run("with deadline exceeded", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
|
||||
defer cancel()
|
||||
|
||||
var times int
|
||||
assert.Error(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
|
||||
times++
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any")
|
||||
}, WithInterval(time.Millisecond*150)))
|
||||
assert.Equal(t, 1, times)
|
||||
})
|
||||
|
||||
t.Run("with deadline not exceeded", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
|
||||
defer cancel()
|
||||
|
||||
var times int
|
||||
assert.NoError(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
|
||||
times++
|
||||
if times == defaultRetryTimes {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
return errors.New("any")
|
||||
}))
|
||||
assert.Equal(t, defaultRetryTimes, times)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ func (s Stream) Parallel(fn ParallelFunc, opts ...Option) {
|
||||
}, opts...).Done()
|
||||
}
|
||||
|
||||
// Reduce is an utility method to let the caller deal with the underlying channel.
|
||||
// Reduce is a utility method to let the caller deal with the underlying channel.
|
||||
func (s Stream) Reduce(fn ReduceFunc) (any, error) {
|
||||
return fn(s.source)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ func (bp *BufferPool) Get() *bytes.Buffer {
|
||||
|
||||
// Put returns buf into bp.
|
||||
func (bp *BufferPool) Put(buf *bytes.Buffer) {
|
||||
if buf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if buf.Cap() < bp.capability {
|
||||
bp.pool.Put(buf)
|
||||
}
|
||||
|
||||
@@ -13,3 +13,26 @@ func TestBufferPool(t *testing.T) {
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 2*capacity)))
|
||||
assert.True(t, pool.Get().Cap() <= capacity)
|
||||
}
|
||||
|
||||
func TestBufferPool_Put(t *testing.T) {
|
||||
t.Run("with nil buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(nil)
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
|
||||
t.Run("with less-cap buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 512)))
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
|
||||
t.Run("with more-cap buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 1024<<1)))
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
}
|
||||
|
||||
12
core/iox/nopcloser_test.go
Normal file
12
core/iox/nopcloser_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNopCloser(t *testing.T) {
|
||||
closer := NopCloser(nil)
|
||||
assert.NoError(t, closer.Close())
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package iox
|
||||
|
||||
import "os"
|
||||
|
||||
// RedirectInOut redirects stdin to r, stdout to w, and callers need to call restore afterwards.
|
||||
// RedirectInOut redirects stdin to r, stdout to w, and callers need to call restore afterward.
|
||||
func RedirectInOut() (restore func(), err error) {
|
||||
var r, w *os.File
|
||||
r, w, err = os.Pipe()
|
||||
|
||||
@@ -35,6 +35,16 @@ func KeepSpace() TextReadOption {
|
||||
}
|
||||
}
|
||||
|
||||
// LimitDupReadCloser returns two io.ReadCloser that read from the first will be written to the second.
|
||||
// But the second io.ReadCloser is limited to up to n bytes.
|
||||
// The first returned reader needs to be read first, because the content
|
||||
// read from it will be written to the underlying buffer of the second reader.
|
||||
func LimitDupReadCloser(reader io.ReadCloser, n int64) (io.ReadCloser, io.ReadCloser) {
|
||||
var buf bytes.Buffer
|
||||
tee := LimitTeeReader(reader, &buf, n)
|
||||
return io.NopCloser(tee), io.NopCloser(&buf)
|
||||
}
|
||||
|
||||
// ReadBytes reads exactly the bytes with the length of len(buf)
|
||||
func ReadBytes(reader io.Reader, buf []byte) error {
|
||||
var got int
|
||||
|
||||
@@ -51,6 +51,11 @@ b`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadTextError(t *testing.T) {
|
||||
_, err := ReadText("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestReadTextLines(t *testing.T) {
|
||||
text := `1
|
||||
|
||||
@@ -94,6 +99,11 @@ func TestReadTextLines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadTextLinesError(t *testing.T) {
|
||||
_, err := ReadTextLines("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestDupReadCloser(t *testing.T) {
|
||||
input := "hello"
|
||||
reader := io.NopCloser(bytes.NewBufferString(input))
|
||||
@@ -108,6 +118,29 @@ func TestDupReadCloser(t *testing.T) {
|
||||
verify(r2)
|
||||
}
|
||||
|
||||
func TestLimitDupReadCloser(t *testing.T) {
|
||||
input := "hello world"
|
||||
limitBytes := int64(4)
|
||||
reader := io.NopCloser(bytes.NewBufferString(input))
|
||||
r1, r2 := LimitDupReadCloser(reader, limitBytes)
|
||||
verify := func(r io.Reader) {
|
||||
output, err := io.ReadAll(r)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, input, string(output))
|
||||
}
|
||||
verifyLimit := func(r io.Reader, limit int64) {
|
||||
output, err := io.ReadAll(r)
|
||||
if limit < int64(len(input)) {
|
||||
input = input[:limit]
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, input, string(output))
|
||||
}
|
||||
|
||||
verify(r1)
|
||||
verifyLimit(r2, limitBytes)
|
||||
}
|
||||
|
||||
func TestReadBytes(t *testing.T) {
|
||||
reader := io.NopCloser(bytes.NewBufferString("helloworld"))
|
||||
buf := make([]byte, 5)
|
||||
|
||||
35
core/iox/tee.go
Normal file
35
core/iox/tee.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package iox
|
||||
|
||||
import "io"
|
||||
|
||||
// LimitTeeReader returns a Reader that writes up to n bytes to w what it reads from r.
|
||||
// First n bytes reads from r performed through it are matched with
|
||||
// corresponding writes to w. There is no internal buffering -
|
||||
// the write must complete before the first n bytes read completes.
|
||||
// Any error encountered while writing is reported as a read error.
|
||||
func LimitTeeReader(r io.Reader, w io.Writer, n int64) io.Reader {
|
||||
return &limitTeeReader{r, w, n}
|
||||
}
|
||||
|
||||
type limitTeeReader struct {
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
n int64 // limit bytes remaining
|
||||
}
|
||||
|
||||
func (t *limitTeeReader) Read(p []byte) (n int, err error) {
|
||||
n, err = t.r.Read(p)
|
||||
if n > 0 && t.n > 0 {
|
||||
limit := int64(n)
|
||||
if limit > t.n {
|
||||
limit = t.n
|
||||
}
|
||||
if n, err := t.w.Write(p[:limit]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
t.n -= limit
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
40
core/iox/tee_test.go
Normal file
40
core/iox/tee_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLimitTeeReader(t *testing.T) {
|
||||
limit := int64(4)
|
||||
src := []byte("hello, world")
|
||||
dst := make([]byte, len(src))
|
||||
rb := bytes.NewBuffer(src)
|
||||
wb := new(bytes.Buffer)
|
||||
r := LimitTeeReader(rb, wb, limit)
|
||||
if n, err := io.ReadFull(r, dst); err != nil || n != len(src) {
|
||||
t.Fatalf("ReadFull(r, dst) = %d, %v; want %d, nil", n, err, len(src))
|
||||
}
|
||||
if !bytes.Equal(dst, src) {
|
||||
t.Errorf("bytes read = %q want %q", dst, src)
|
||||
}
|
||||
if !bytes.Equal(wb.Bytes(), src[:limit]) {
|
||||
t.Errorf("bytes written = %q want %q", wb.Bytes(), src)
|
||||
}
|
||||
|
||||
n, err := r.Read(dst)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
|
||||
rb = bytes.NewBuffer(src)
|
||||
pr, pw := io.Pipe()
|
||||
if assert.NoError(t, pr.Close()) {
|
||||
r = LimitTeeReader(rb, pw, limit)
|
||||
n, err := io.ReadFull(r, dst)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.ErrClosedPipe, err)
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@ package iox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const bufSize = 32 * 1024
|
||||
|
||||
// CountLines returns the number of lines in file.
|
||||
// CountLines returns the number of lines in the file.
|
||||
func CountLines(file string) (int, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
@@ -26,7 +27,7 @@ func CountLines(file string) (int, error) {
|
||||
count += bytes.Count(buf[:c], lineSep)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
case errors.Is(err, io.EOF):
|
||||
if noEol {
|
||||
count++
|
||||
}
|
||||
|
||||
@@ -24,3 +24,8 @@ func TestCountLines(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 4, lines)
|
||||
}
|
||||
|
||||
func TestCountLinesError(t *testing.T) {
|
||||
_, err := CountLines("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package iox
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A TextLineScanner is a scanner that can scan lines from given reader.
|
||||
// A TextLineScanner is a scanner that can scan lines from the given reader.
|
||||
type TextLineScanner struct {
|
||||
reader *bufio.Reader
|
||||
hasNext bool
|
||||
@@ -14,7 +15,7 @@ type TextLineScanner struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// NewTextLineScanner returns a TextLineScanner with given reader.
|
||||
// NewTextLineScanner returns a TextLineScanner with the given reader.
|
||||
func NewTextLineScanner(reader io.Reader) *TextLineScanner {
|
||||
return &TextLineScanner{
|
||||
reader: bufio.NewReader(reader),
|
||||
@@ -30,7 +31,7 @@ func (scanner *TextLineScanner) Scan() bool {
|
||||
|
||||
line, err := scanner.reader.ReadString('\n')
|
||||
scanner.line = strings.TrimRight(line, "\n")
|
||||
if err == io.EOF {
|
||||
if errors.Is(err, io.EOF) {
|
||||
scanner.hasNext = false
|
||||
return true
|
||||
} else if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package iox
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -22,3 +23,10 @@ func TestScanner(t *testing.T) {
|
||||
}
|
||||
assert.EqualValues(t, []string{"1", "2", "3", "4"}, lines)
|
||||
}
|
||||
|
||||
func TestBadScanner(t *testing.T) {
|
||||
scanner := NewTextLineScanner(iotest.ErrReader(iotest.ErrTimeout))
|
||||
assert.False(t, scanner.Scan())
|
||||
_, err := scanner.Line()
|
||||
assert.ErrorIs(t, err, iotest.ErrTimeout)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package limit
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -28,20 +29,9 @@ var (
|
||||
// ErrUnknownCode is an error that represents unknown status code.
|
||||
ErrUnknownCode = errors.New("unknown status code")
|
||||
|
||||
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
|
||||
periodScript = redis.NewScript(`local limit = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
local current = redis.call("INCRBY", KEYS[1], 1)
|
||||
if current == 1 then
|
||||
redis.call("expire", KEYS[1], window)
|
||||
end
|
||||
if current < limit then
|
||||
return 1
|
||||
elseif current == limit then
|
||||
return 2
|
||||
else
|
||||
return 0
|
||||
end`)
|
||||
//go:embed periodscript.lua
|
||||
periodLuaScript string
|
||||
periodScript = redis.NewScript(periodLuaScript)
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
14
core/limit/periodscript.lua
Normal file
14
core/limit/periodscript.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
|
||||
local limit = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
local current = redis.call("INCRBY", KEYS[1], 1)
|
||||
if current == 1 then
|
||||
redis.call("expire", KEYS[1], window)
|
||||
end
|
||||
if current < limit then
|
||||
return 1
|
||||
elseif current == limit then
|
||||
return 2
|
||||
else
|
||||
return 0
|
||||
end
|
||||
@@ -2,6 +2,7 @@ package limit
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/errorx"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
xrate "golang.org/x/time/rate"
|
||||
@@ -20,37 +22,11 @@ const (
|
||||
pingInterval = time.Millisecond * 100
|
||||
)
|
||||
|
||||
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
|
||||
// KEYS[1] as tokens_key
|
||||
// KEYS[2] as timestamp_key
|
||||
var script = redis.NewScript(`local rate = tonumber(ARGV[1])
|
||||
local capacity = tonumber(ARGV[2])
|
||||
local now = tonumber(ARGV[3])
|
||||
local requested = tonumber(ARGV[4])
|
||||
local fill_time = capacity/rate
|
||||
local ttl = math.floor(fill_time*2)
|
||||
local last_tokens = tonumber(redis.call("get", KEYS[1]))
|
||||
if last_tokens == nil then
|
||||
last_tokens = capacity
|
||||
end
|
||||
|
||||
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
|
||||
if last_refreshed == nil then
|
||||
last_refreshed = 0
|
||||
end
|
||||
|
||||
local delta = math.max(0, now-last_refreshed)
|
||||
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
|
||||
local allowed = filled_tokens >= requested
|
||||
local new_tokens = filled_tokens
|
||||
if allowed then
|
||||
new_tokens = filled_tokens - requested
|
||||
end
|
||||
|
||||
redis.call("setex", KEYS[1], ttl, new_tokens)
|
||||
redis.call("setex", KEYS[2], ttl, now)
|
||||
|
||||
return allowed`)
|
||||
var (
|
||||
//go:embed tokenscript.lua
|
||||
tokenLuaScript string
|
||||
tokenScript = redis.NewScript(tokenLuaScript)
|
||||
)
|
||||
|
||||
// A TokenLimiter controls how frequently events are allowed to happen with in one second.
|
||||
type TokenLimiter struct {
|
||||
@@ -112,7 +88,7 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
|
||||
}
|
||||
|
||||
resp, err := lim.store.ScriptRunCtx(ctx,
|
||||
script,
|
||||
tokenScript,
|
||||
[]string{
|
||||
lim.tokenKey,
|
||||
lim.timestampKey,
|
||||
@@ -125,10 +101,10 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
|
||||
})
|
||||
// redis allowed == false
|
||||
// Lua boolean false -> r Nil bulk reply
|
||||
if err == redis.Nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||
if errorx.In(err, context.DeadlineExceeded, context.Canceled) {
|
||||
logx.Errorf("fail to use rate limiter: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
31
core/limit/tokenscript.lua
Normal file
31
core/limit/tokenscript.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
|
||||
-- KEYS[1] as tokens_key
|
||||
-- KEYS[2] as timestamp_key
|
||||
local rate = tonumber(ARGV[1])
|
||||
local capacity = tonumber(ARGV[2])
|
||||
local now = tonumber(ARGV[3])
|
||||
local requested = tonumber(ARGV[4])
|
||||
local fill_time = capacity/rate
|
||||
local ttl = math.floor(fill_time*2)
|
||||
local last_tokens = tonumber(redis.call("get", KEYS[1]))
|
||||
if last_tokens == nil then
|
||||
last_tokens = capacity
|
||||
end
|
||||
|
||||
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
|
||||
if last_refreshed == nil then
|
||||
last_refreshed = 0
|
||||
end
|
||||
|
||||
local delta = math.max(0, now-last_refreshed)
|
||||
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
|
||||
local allowed = filled_tokens >= requested
|
||||
local new_tokens = filled_tokens
|
||||
if allowed then
|
||||
new_tokens = filled_tokens - requested
|
||||
end
|
||||
|
||||
redis.call("setex", KEYS[1], ttl, new_tokens)
|
||||
redis.call("setex", KEYS[2], ttl, now)
|
||||
|
||||
return allowed
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/zeromicro/go-zero/core/collection"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/mathx"
|
||||
"github.com/zeromicro/go-zero/core/stat"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
@@ -21,8 +22,11 @@ const (
|
||||
defaultCpuThreshold = 900
|
||||
defaultMinRt = float64(time.Second / time.Millisecond)
|
||||
// moving average hyperparameter beta for calculating requests on the fly
|
||||
flyingBeta = 0.9
|
||||
coolOffDuration = time.Second
|
||||
flyingBeta = 0.9
|
||||
coolOffDuration = time.Second
|
||||
cpuMax = 1000 // millicpu
|
||||
millisecondsPerSecond = 1000
|
||||
overloadFactorLowerBound = 0.1
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -66,14 +70,14 @@ type (
|
||||
|
||||
adaptiveShedder struct {
|
||||
cpuThreshold int64
|
||||
windows int64
|
||||
windowScale float64
|
||||
flying int64
|
||||
avgFlying float64
|
||||
avgFlyingLock syncx.SpinLock
|
||||
overloadTime *syncx.AtomicDuration
|
||||
droppedRecently *syncx.AtomicBool
|
||||
passCounter *collection.RollingWindow
|
||||
rtCounter *collection.RollingWindow
|
||||
passCounter *collection.RollingWindow[int64, *collection.Bucket[int64]]
|
||||
rtCounter *collection.RollingWindow[int64, *collection.Bucket[int64]]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -103,15 +107,16 @@ func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
|
||||
opt(&options)
|
||||
}
|
||||
bucketDuration := options.window / time.Duration(options.buckets)
|
||||
newBucket := func() *collection.Bucket[int64] {
|
||||
return new(collection.Bucket[int64])
|
||||
}
|
||||
return &adaptiveShedder{
|
||||
cpuThreshold: options.cpuThreshold,
|
||||
windows: int64(time.Second / bucketDuration),
|
||||
windowScale: float64(time.Second) / float64(bucketDuration) / millisecondsPerSecond,
|
||||
overloadTime: syncx.NewAtomicDuration(),
|
||||
droppedRecently: syncx.NewAtomicBool(),
|
||||
passCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
|
||||
collection.IgnoreCurrentBucket()),
|
||||
rtCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
|
||||
collection.IgnoreCurrentBucket()),
|
||||
passCounter: collection.NewRollingWindow[int64, *collection.Bucket[int64]](newBucket, options.buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]()),
|
||||
rtCounter: collection.NewRollingWindow[int64, *collection.Bucket[int64]](newBucket, options.buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +139,10 @@ func (as *adaptiveShedder) Allow() (Promise, error) {
|
||||
func (as *adaptiveShedder) addFlying(delta int64) {
|
||||
flying := atomic.AddInt64(&as.flying, delta)
|
||||
// update avgFlying when the request is finished.
|
||||
// this strategy makes avgFlying have a little bit lag against flying, and smoother.
|
||||
// this strategy makes avgFlying have a little bit of lag against flying, and smoother.
|
||||
// when the flying requests increase rapidly, avgFlying increase slower, accept more requests.
|
||||
// when the flying requests drop rapidly, avgFlying drop slower, accept less requests.
|
||||
// it makes the service to serve as more requests as possible.
|
||||
// when the flying requests drop rapidly, avgFlying drop slower, accept fewer requests.
|
||||
// it makes the service to serve as many requests as possible.
|
||||
if delta < 0 {
|
||||
as.avgFlyingLock.Lock()
|
||||
as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta)
|
||||
@@ -149,39 +154,42 @@ func (as *adaptiveShedder) highThru() bool {
|
||||
as.avgFlyingLock.Lock()
|
||||
avgFlying := as.avgFlying
|
||||
as.avgFlyingLock.Unlock()
|
||||
maxFlight := as.maxFlight()
|
||||
return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight
|
||||
maxFlight := as.maxFlight() * as.overloadFactor()
|
||||
return avgFlying > maxFlight && float64(atomic.LoadInt64(&as.flying)) > maxFlight
|
||||
}
|
||||
|
||||
func (as *adaptiveShedder) maxFlight() int64 {
|
||||
func (as *adaptiveShedder) maxFlight() float64 {
|
||||
// windows = buckets per second
|
||||
// maxQPS = maxPASS * windows
|
||||
// minRT = min average response time in milliseconds
|
||||
// maxQPS * minRT / milliseconds_per_second
|
||||
return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3)))
|
||||
// allowedFlying = maxQPS * minRT / milliseconds_per_second
|
||||
maxFlight := float64(as.maxPass()) * as.minRt() * as.windowScale
|
||||
return mathx.AtLeast(maxFlight, 1)
|
||||
}
|
||||
|
||||
func (as *adaptiveShedder) maxPass() int64 {
|
||||
var result float64 = 1
|
||||
var result int64 = 1
|
||||
|
||||
as.passCounter.Reduce(func(b *collection.Bucket) {
|
||||
as.passCounter.Reduce(func(b *collection.Bucket[int64]) {
|
||||
if b.Sum > result {
|
||||
result = b.Sum
|
||||
}
|
||||
})
|
||||
|
||||
return int64(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (as *adaptiveShedder) minRt() float64 {
|
||||
// if no requests in previous windows, return defaultMinRt,
|
||||
// its a reasonable large value to avoid dropping requests.
|
||||
result := defaultMinRt
|
||||
|
||||
as.rtCounter.Reduce(func(b *collection.Bucket) {
|
||||
as.rtCounter.Reduce(func(b *collection.Bucket[int64]) {
|
||||
if b.Count <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
avg := math.Round(b.Sum / float64(b.Count))
|
||||
avg := math.Round(float64(b.Sum) / float64(b.Count))
|
||||
if avg < result {
|
||||
result = avg
|
||||
}
|
||||
@@ -190,6 +198,13 @@ func (as *adaptiveShedder) minRt() float64 {
|
||||
return result
|
||||
}
|
||||
|
||||
func (as *adaptiveShedder) overloadFactor() float64 {
|
||||
// as.cpuThreshold must be less than cpuMax
|
||||
factor := (cpuMax - float64(stat.CpuUsage())) / (cpuMax - float64(as.cpuThreshold))
|
||||
// at least accept 10% of acceptable requests, even cpu is highly overloaded.
|
||||
return mathx.Between(factor, overloadFactorLowerBound, 1)
|
||||
}
|
||||
|
||||
func (as *adaptiveShedder) shouldDrop() bool {
|
||||
if as.systemOverloaded() || as.stillHot() {
|
||||
if as.highThru() {
|
||||
@@ -236,14 +251,14 @@ func (as *adaptiveShedder) systemOverloaded() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// WithBuckets customizes the Shedder with given number of buckets.
|
||||
// WithBuckets customizes the Shedder with the given number of buckets.
|
||||
func WithBuckets(buckets int) ShedderOption {
|
||||
return func(opts *shedderOptions) {
|
||||
opts.buckets = buckets
|
||||
}
|
||||
}
|
||||
|
||||
// WithCpuThreshold customizes the Shedder with given cpu threshold.
|
||||
// WithCpuThreshold customizes the Shedder with the given cpu threshold.
|
||||
func WithCpuThreshold(threshold int64) ShedderOption {
|
||||
return func(opts *shedderOptions) {
|
||||
opts.cpuThreshold = threshold
|
||||
@@ -269,6 +284,6 @@ func (p *promise) Fail() {
|
||||
func (p *promise) Pass() {
|
||||
rt := float64(timex.Since(p.start)) / float64(time.Millisecond)
|
||||
p.shedder.addFlying(-1)
|
||||
p.shedder.rtCounter.Add(math.Ceil(rt))
|
||||
p.shedder.rtCounter.Add(int64(math.Ceil(rt)))
|
||||
p.shedder.passCounter.Add(1)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
const (
|
||||
buckets = 10
|
||||
bucketDuration = time.Millisecond * 50
|
||||
windowFactor = 0.01
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -57,7 +58,7 @@ func TestAdaptiveShedder(t *testing.T) {
|
||||
func TestAdaptiveShedderMaxPass(t *testing.T) {
|
||||
passCounter := newRollingWindow()
|
||||
for i := 1; i <= 10; i++ {
|
||||
passCounter.Add(float64(i * 100))
|
||||
passCounter.Add(int64(i * 100))
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
@@ -82,7 +83,7 @@ func TestAdaptiveShedderMinRt(t *testing.T) {
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
for j := i*10 + 1; j <= i*10+10; j++ {
|
||||
rtCounter.Add(float64(j))
|
||||
rtCounter.Add(int64(j))
|
||||
}
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
@@ -106,18 +107,18 @@ func TestAdaptiveShedderMaxFlight(t *testing.T) {
|
||||
if i > 0 {
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
passCounter.Add(float64((i + 1) * 100))
|
||||
passCounter.Add(int64((i + 1) * 100))
|
||||
for j := i*10 + 1; j <= i*10+10; j++ {
|
||||
rtCounter.Add(float64(j))
|
||||
rtCounter.Add(int64(j))
|
||||
}
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
passCounter: passCounter,
|
||||
rtCounter: rtCounter,
|
||||
windows: buckets,
|
||||
windowScale: windowFactor,
|
||||
droppedRecently: syncx.NewAtomicBool(),
|
||||
}
|
||||
assert.Equal(t, int64(54), shedder.maxFlight())
|
||||
assert.Equal(t, float64(54), shedder.maxFlight())
|
||||
}
|
||||
|
||||
func TestAdaptiveShedderShouldDrop(t *testing.T) {
|
||||
@@ -128,15 +129,15 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
|
||||
if i > 0 {
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
passCounter.Add(float64((i + 1) * 100))
|
||||
passCounter.Add(int64((i + 1) * 100))
|
||||
for j := i*10 + 1; j <= i*10+10; j++ {
|
||||
rtCounter.Add(float64(j))
|
||||
rtCounter.Add(int64(j))
|
||||
}
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
passCounter: passCounter,
|
||||
rtCounter: rtCounter,
|
||||
windows: buckets,
|
||||
windowScale: windowFactor,
|
||||
overloadTime: syncx.NewAtomicDuration(),
|
||||
droppedRecently: syncx.NewAtomicBool(),
|
||||
}
|
||||
@@ -149,7 +150,8 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
|
||||
|
||||
// cpu >= 800, inflight > maxPass
|
||||
shedder.avgFlying = 80
|
||||
shedder.flying = 50
|
||||
// because of the overloadFactor, so we need to make sure maxFlight is greater than flying
|
||||
shedder.flying = int64(shedder.maxFlight()*shedder.overloadFactor()) - 5
|
||||
assert.False(t, shedder.shouldDrop())
|
||||
|
||||
// cpu >= 800, inflight > maxPass
|
||||
@@ -182,15 +184,15 @@ func TestAdaptiveShedderStillHot(t *testing.T) {
|
||||
if i > 0 {
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
passCounter.Add(float64((i + 1) * 100))
|
||||
passCounter.Add(int64((i + 1) * 100))
|
||||
for j := i*10 + 1; j <= i*10+10; j++ {
|
||||
rtCounter.Add(float64(j))
|
||||
rtCounter.Add(int64(j))
|
||||
}
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
passCounter: passCounter,
|
||||
rtCounter: rtCounter,
|
||||
windows: buckets,
|
||||
windowScale: windowFactor,
|
||||
overloadTime: syncx.NewAtomicDuration(),
|
||||
droppedRecently: syncx.ForAtomicBool(true),
|
||||
}
|
||||
@@ -239,6 +241,32 @@ func BenchmarkAdaptiveShedder_Allow(b *testing.B) {
|
||||
b.Run("low load", bench)
|
||||
}
|
||||
|
||||
func newRollingWindow() *collection.RollingWindow {
|
||||
return collection.NewRollingWindow(buckets, bucketDuration, collection.IgnoreCurrentBucket())
|
||||
func BenchmarkMaxFlight(b *testing.B) {
|
||||
passCounter := newRollingWindow()
|
||||
rtCounter := newRollingWindow()
|
||||
for i := 0; i < 10; i++ {
|
||||
if i > 0 {
|
||||
time.Sleep(bucketDuration)
|
||||
}
|
||||
passCounter.Add(int64((i + 1) * 100))
|
||||
for j := i*10 + 1; j <= i*10+10; j++ {
|
||||
rtCounter.Add(int64(j))
|
||||
}
|
||||
}
|
||||
shedder := &adaptiveShedder{
|
||||
passCounter: passCounter,
|
||||
rtCounter: rtCounter,
|
||||
windowScale: windowFactor,
|
||||
droppedRecently: syncx.NewAtomicBool(),
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = shedder.maxFlight()
|
||||
}
|
||||
}
|
||||
|
||||
func newRollingWindow() *collection.RollingWindow[int64, *collection.Bucket[int64]] {
|
||||
return collection.NewRollingWindow[int64, *collection.Bucket[int64]](func() *collection.Bucket[int64] {
|
||||
return new(collection.Bucket[int64])
|
||||
}, buckets, bucketDuration, collection.IgnoreCurrentBucket[int64, *collection.Bucket[int64]]())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
// A ShedderGroup is a manager to manage key based shedders.
|
||||
// A ShedderGroup is a manager to manage key-based shedders.
|
||||
type ShedderGroup struct {
|
||||
options []ShedderOption
|
||||
manager *syncx.ResourceManager
|
||||
|
||||
@@ -42,7 +42,7 @@ func Debugv(ctx context.Context, v interface{}) {
|
||||
getLogger(ctx).Debugv(v)
|
||||
}
|
||||
|
||||
// Debugw writes msg along with fields into access log.
|
||||
// Debugw writes msg along with fields into the access log.
|
||||
func Debugw(ctx context.Context, msg string, fields ...LogField) {
|
||||
getLogger(ctx).Debugw(msg, fields...)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func Errorv(ctx context.Context, v any) {
|
||||
getLogger(ctx).Errorv(v)
|
||||
}
|
||||
|
||||
// Errorw writes msg along with fields into error log.
|
||||
// Errorw writes msg along with fields into the error log.
|
||||
func Errorw(ctx context.Context, msg string, fields ...LogField) {
|
||||
getLogger(ctx).Errorw(msg, fields...)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func Infov(ctx context.Context, v any) {
|
||||
getLogger(ctx).Infov(v)
|
||||
}
|
||||
|
||||
// Infow writes msg along with fields into access log.
|
||||
// Infow writes msg along with fields into the access log.
|
||||
func Infow(ctx context.Context, msg string, fields ...LogField) {
|
||||
getLogger(ctx).Infow(msg, fields...)
|
||||
}
|
||||
@@ -108,10 +108,11 @@ func SetLevel(level uint32) {
|
||||
logx.SetLevel(level)
|
||||
}
|
||||
|
||||
// SetUp sets up the logx. If already set up, just return nil.
|
||||
// we allow SetUp to be called multiple times, because for example
|
||||
// SetUp sets up the logx.
|
||||
// If already set up, return nil.
|
||||
// We allow SetUp to be called multiple times, because, for example,
|
||||
// we need to allow different service frameworks to initialize logx respectively.
|
||||
// the same logic for SetUp
|
||||
// The same logic for SetUp
|
||||
func SetUp(c LogConf) error {
|
||||
return logx.SetUp(c)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package logx
|
||||
|
||||
// A LessLogger is a logger that control to log once during the given duration.
|
||||
// A LessLogger is a logger that controls to log once during the given duration.
|
||||
type LessLogger struct {
|
||||
*limitedExecutor
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -17,14 +18,13 @@ import (
|
||||
const callerDepth = 4
|
||||
|
||||
var (
|
||||
timeFormat = "2006-01-02T15:04:05.000Z07:00"
|
||||
logLevel uint32
|
||||
timeFormat = "2006-01-02T15:04:05.000Z07:00"
|
||||
encoding uint32 = jsonEncodingType
|
||||
// maxContentLength is used to truncate the log content, 0 for not truncating.
|
||||
maxContentLength uint32
|
||||
// use uint32 for atomic operations
|
||||
disableLog uint32
|
||||
disableStat uint32
|
||||
logLevel uint32
|
||||
options logOptions
|
||||
writer = new(atomicWriter)
|
||||
setupOnce sync.Once
|
||||
@@ -87,7 +87,7 @@ func Debugv(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debugw writes msg along with fields into access log.
|
||||
// Debugw writes msg along with fields into the access log.
|
||||
func Debugw(msg string, fields ...LogField) {
|
||||
if shallLog(DebugLevel) {
|
||||
writeDebug(msg, fields...)
|
||||
@@ -96,7 +96,7 @@ func Debugw(msg string, fields ...LogField) {
|
||||
|
||||
// Disable disables the logging.
|
||||
func Disable() {
|
||||
atomic.StoreUint32(&disableLog, 1)
|
||||
atomic.StoreUint32(&logLevel, disableLevel)
|
||||
writer.Store(nopWriter{})
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func Errorv(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Errorw writes msg along with fields into error log.
|
||||
// Errorw writes msg along with fields into the error log.
|
||||
func Errorw(msg string, fields ...LogField) {
|
||||
if shallLog(ErrorLevel) {
|
||||
writeError(msg, fields...)
|
||||
@@ -154,11 +154,11 @@ func Errorw(msg string, fields ...LogField) {
|
||||
func Field(key string, value any) LogField {
|
||||
switch val := value.(type) {
|
||||
case error:
|
||||
return LogField{Key: key, Value: val.Error()}
|
||||
return LogField{Key: key, Value: encodeError(val)}
|
||||
case []error:
|
||||
var errs []string
|
||||
for _, err := range val {
|
||||
errs = append(errs, err.Error())
|
||||
errs = append(errs, encodeError(err))
|
||||
}
|
||||
return LogField{Key: key, Value: errs}
|
||||
case time.Duration:
|
||||
@@ -176,11 +176,11 @@ func Field(key string, value any) LogField {
|
||||
}
|
||||
return LogField{Key: key, Value: times}
|
||||
case fmt.Stringer:
|
||||
return LogField{Key: key, Value: val.String()}
|
||||
return LogField{Key: key, Value: encodeStringer(val)}
|
||||
case []fmt.Stringer:
|
||||
var strs []string
|
||||
for _, str := range val {
|
||||
strs = append(strs, str.String())
|
||||
strs = append(strs, encodeStringer(str))
|
||||
}
|
||||
return LogField{Key: key, Value: strs}
|
||||
default:
|
||||
@@ -209,7 +209,7 @@ func Infov(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Infow writes msg along with fields into access log.
|
||||
// Infow writes msg along with fields into the access log.
|
||||
func Infow(msg string, fields ...LogField) {
|
||||
if shallLog(InfoLevel) {
|
||||
writeInfo(msg, fields...)
|
||||
@@ -250,16 +250,17 @@ func SetLevel(level uint32) {
|
||||
|
||||
// SetWriter sets the logging writer. It can be used to customize the logging.
|
||||
func SetWriter(w Writer) {
|
||||
if atomic.LoadUint32(&disableLog) == 0 {
|
||||
if atomic.LoadUint32(&logLevel) != disableLevel {
|
||||
writer.Store(w)
|
||||
}
|
||||
}
|
||||
|
||||
// SetUp sets up the logx. If already set up, just return nil.
|
||||
// we allow SetUp to be called multiple times, because for example
|
||||
// SetUp sets up the logx.
|
||||
// If already set up, return nil.
|
||||
// We allow SetUp to be called multiple times, because, for example,
|
||||
// we need to allow different service frameworks to initialize logx respectively.
|
||||
func SetUp(c LogConf) (err error) {
|
||||
// Just ignore the subsequent SetUp calls.
|
||||
// Ignore the later SetUp calls.
|
||||
// Because multiple services in one process might call SetUp respectively.
|
||||
// Need to wait for the first caller to complete the execution.
|
||||
setupOnce.Do(func() {
|
||||
@@ -414,6 +415,32 @@ func createOutput(path string) (io.WriteCloser, error) {
|
||||
return NewLogger(path, rule, options.gzipEnabled)
|
||||
}
|
||||
|
||||
func encodeError(err error) (ret string) {
|
||||
return encodeWithRecover(err, func() string {
|
||||
return err.Error()
|
||||
})
|
||||
}
|
||||
|
||||
func encodeStringer(v fmt.Stringer) (ret string) {
|
||||
return encodeWithRecover(v, func() string {
|
||||
return v.String()
|
||||
})
|
||||
}
|
||||
|
||||
func encodeWithRecover(arg any, fn func() string) (ret string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if v := reflect.ValueOf(arg); v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
ret = nilAngleString
|
||||
} else {
|
||||
ret = fmt.Sprintf("panic: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
func getWriter() Writer {
|
||||
w := writer.Load()
|
||||
if w == nil {
|
||||
@@ -481,7 +508,7 @@ func writeDebug(val any, fields ...LogField) {
|
||||
getWriter().Debug(val, addCaller(fields...)...)
|
||||
}
|
||||
|
||||
// writeError writes v into error log.
|
||||
// writeError writes v into the error log.
|
||||
// Not checking shallLog here is for performance consideration.
|
||||
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
|
||||
// The caller should check shallLog before calling this function.
|
||||
@@ -521,7 +548,7 @@ func writeStack(msg string) {
|
||||
getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
|
||||
}
|
||||
|
||||
// writeStat writes v into stat log.
|
||||
// writeStat writes v into the stat log.
|
||||
// Not checking shallLog here is for performance consideration.
|
||||
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
|
||||
// The caller should check shallLog before calling this function.
|
||||
|
||||
@@ -348,6 +348,27 @@ func TestStructedLogInfow(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructedLogFieldNil(t *testing.T) {
|
||||
w := new(mockWriter)
|
||||
old := writer.Swap(w)
|
||||
defer writer.Store(old)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
var s *string
|
||||
Infow("test", Field("bb", s))
|
||||
var d *nilStringer
|
||||
Infow("test", Field("bb", d))
|
||||
var e *nilError
|
||||
Errorw("test", Field("bb", e))
|
||||
})
|
||||
assert.NotPanics(t, func() {
|
||||
var p panicStringer
|
||||
Infow("test", Field("bb", p))
|
||||
var ps innerPanicStringer
|
||||
Infow("test", Field("bb", ps))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructedLogInfoConsoleAny(t *testing.T) {
|
||||
w := new(mockWriter)
|
||||
old := writer.Swap(w)
|
||||
@@ -570,7 +591,7 @@ func TestErrorfWithWrappedError(t *testing.T) {
|
||||
old := writer.Swap(w)
|
||||
defer writer.Store(old)
|
||||
|
||||
Errorf("hello %w", errors.New(message))
|
||||
Errorf("hello %s", errors.New(message))
|
||||
assert.True(t, strings.Contains(w.String(), "hello there"))
|
||||
}
|
||||
|
||||
@@ -666,6 +687,7 @@ func TestDisable(t *testing.T) {
|
||||
WithMaxSize(1024)(&opt)
|
||||
assert.Nil(t, Close())
|
||||
assert.Nil(t, Close())
|
||||
assert.Equal(t, uint32(disableLevel), atomic.LoadUint32(&logLevel))
|
||||
}
|
||||
|
||||
func TestDisableStat(t *testing.T) {
|
||||
@@ -680,7 +702,7 @@ func TestDisableStat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetWriter(t *testing.T) {
|
||||
atomic.StoreUint32(&disableLog, 0)
|
||||
atomic.StoreUint32(&logLevel, 0)
|
||||
Reset()
|
||||
SetWriter(nopWriter{})
|
||||
assert.NotNil(t, writer.Load())
|
||||
@@ -858,3 +880,36 @@ func validateFields(t *testing.T, content string, fields map[string]any) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nilError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *nilError) Error() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
type nilStringer struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *nilStringer) String() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
type innerPanicStringer struct {
|
||||
Inner *struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
|
||||
func (s innerPanicStringer) String() string {
|
||||
return s.Inner.Name
|
||||
}
|
||||
|
||||
type panicStringer struct {
|
||||
}
|
||||
|
||||
func (s panicStringer) String() string {
|
||||
panic("panic")
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type LogConf struct {
|
||||
- `Compress`: whether or not to compress log files, only works with `file` mode.
|
||||
- `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode.
|
||||
- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. It’s used to avoid stacktrace flooding.
|
||||
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even thougth `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
|
||||
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even though `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
|
||||
- `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`.
|
||||
- `Rotation`: represents the type of log rotation rule. Default is `daily`.
|
||||
- `daily` rotate the logs by day.
|
||||
|
||||
@@ -41,67 +41,99 @@ type richLogger struct {
|
||||
}
|
||||
|
||||
func (l *richLogger) Debug(v ...any) {
|
||||
l.debug(fmt.Sprint(v...))
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugf(format string, v ...any) {
|
||||
l.debug(fmt.Sprintf(format, v...))
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugv(v any) {
|
||||
l.debug(v)
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugw(msg string, fields ...LogField) {
|
||||
l.debug(msg, fields...)
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Error(v ...any) {
|
||||
l.err(fmt.Sprint(v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorf(format string, v ...any) {
|
||||
l.err(fmt.Sprintf(format, v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorv(v any) {
|
||||
l.err(v)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorw(msg string, fields ...LogField) {
|
||||
l.err(msg, fields...)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Info(v ...any) {
|
||||
l.info(fmt.Sprint(v...))
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infof(format string, v ...any) {
|
||||
l.info(fmt.Sprintf(format, v...))
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infov(v any) {
|
||||
l.info(v)
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infow(msg string, fields ...LogField) {
|
||||
l.info(msg, fields...)
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slow(v ...any) {
|
||||
l.slow(fmt.Sprint(v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slowf(format string, v ...any) {
|
||||
l.slow(fmt.Sprintf(format, v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slowv(v any) {
|
||||
l.slow(v)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Sloww(msg string, fields ...LogField) {
|
||||
l.slow(msg, fields...)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) WithCallerSkip(skip int) Logger {
|
||||
|
||||
@@ -298,6 +298,7 @@ func (l *RotateLogger) initialize() error {
|
||||
if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.currentSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
@@ -318,7 +319,7 @@ func (l *RotateLogger) maybeCompressFile(file string) {
|
||||
}()
|
||||
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
// file not exists or other error, ignore compression
|
||||
// file doesn't exist or another error, ignore compression
|
||||
return
|
||||
}
|
||||
|
||||
@@ -381,7 +382,15 @@ func (l *RotateLogger) startWorker() {
|
||||
case event := <-l.channel:
|
||||
l.write(event)
|
||||
case <-l.done:
|
||||
return
|
||||
// avoid losing logs before closing.
|
||||
for {
|
||||
select {
|
||||
case event := <-l.channel:
|
||||
l.write(event)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -206,6 +206,27 @@ func TestRotateLoggerClose(t *testing.T) {
|
||||
_, err := logger.Write([]byte("foo"))
|
||||
assert.ErrorIs(t, err, ErrLogFileClosed)
|
||||
})
|
||||
|
||||
t.Run("close without losing logs", func(t *testing.T) {
|
||||
text := "foo"
|
||||
filename, err := fs.TempFilenameWithText(text)
|
||||
assert.Nil(t, err)
|
||||
if len(filename) > 0 {
|
||||
defer os.Remove(filename)
|
||||
}
|
||||
logger, err := NewLogger(filename, new(DailyRotateRule), false)
|
||||
assert.Nil(t, err)
|
||||
msg := []byte("foo")
|
||||
n := 100
|
||||
for i := 0; i < n; i++ {
|
||||
_, err = logger.Write(msg)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.Nil(t, logger.Close())
|
||||
bs, err := os.ReadFile(filename)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(msg)*n+len(text), len(bs))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateLoggerGetBackupFilename(t *testing.T) {
|
||||
@@ -496,6 +517,21 @@ func TestGzipFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateLogger_WithExistingFile(t *testing.T) {
|
||||
const body = "foo"
|
||||
filename, err := fs.TempFilenameWithText(body)
|
||||
assert.Nil(t, err)
|
||||
if len(filename) > 0 {
|
||||
defer os.Remove(filename)
|
||||
}
|
||||
|
||||
rule := NewSizeLimitRotateRule(filename, "-", 1, 100, 3, false)
|
||||
logger, err := NewLogger(filename, rule, false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(body)), logger.currentSize)
|
||||
assert.Nil(t, logger.Close())
|
||||
}
|
||||
|
||||
func BenchmarkRotateLogger(b *testing.B) {
|
||||
filename := "./test.log"
|
||||
filename2 := "./test2.log"
|
||||
|
||||
@@ -15,6 +15,8 @@ const (
|
||||
ErrorLevel
|
||||
// SevereLevel only log severe messages
|
||||
SevereLevel
|
||||
// disableLevel doesn't log any messages
|
||||
disableLevel = 0xff
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -46,6 +48,7 @@ const (
|
||||
levelDebug = "debug"
|
||||
|
||||
backupFileDelimiter = "-"
|
||||
nilAngleString = "<nil>"
|
||||
flags = 0x0
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -253,11 +254,10 @@ func (n nopWriter) Stack(_ any) {
|
||||
func (n nopWriter) Stat(_ any, _ ...LogField) {
|
||||
}
|
||||
|
||||
func buildPlainFields(fields ...LogField) []string {
|
||||
var items []string
|
||||
|
||||
for _, field := range fields {
|
||||
items = append(items, fmt.Sprintf("%s=%+v", field.Key, field.Value))
|
||||
func buildPlainFields(fields logEntry) []string {
|
||||
items := make([]string, 0, len(fields))
|
||||
for k, v := range fields {
|
||||
items = append(items, fmt.Sprintf("%s=%+v", k, v))
|
||||
}
|
||||
|
||||
return items
|
||||
@@ -277,6 +277,20 @@ func combineGlobalFields(fields []LogField) []LogField {
|
||||
return ret
|
||||
}
|
||||
|
||||
func marshalJson(t interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(t)
|
||||
// go 1.5+ will append a newline to the end of the json string
|
||||
// https://github.com/golang/go/issues/13520
|
||||
if l := buf.Len(); l > 0 && buf.Bytes()[l-1] == '\n' {
|
||||
buf.Truncate(l - 1)
|
||||
}
|
||||
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func output(writer io.Writer, level string, val any, fields ...LogField) {
|
||||
// only truncate string content, don't know how to truncate the values of other types.
|
||||
if v, ok := val.(string); ok {
|
||||
@@ -288,15 +302,17 @@ func output(writer io.Writer, level string, val any, fields ...LogField) {
|
||||
}
|
||||
|
||||
fields = combineGlobalFields(fields)
|
||||
// +3 for timestamp, level and content
|
||||
entry := make(logEntry, len(fields)+3)
|
||||
for _, field := range fields {
|
||||
entry[field.Key] = field.Value
|
||||
}
|
||||
|
||||
switch atomic.LoadUint32(&encoding) {
|
||||
case plainEncodingType:
|
||||
writePlainAny(writer, level, val, buildPlainFields(fields...)...)
|
||||
plainFields := buildPlainFields(entry)
|
||||
writePlainAny(writer, level, val, plainFields...)
|
||||
default:
|
||||
entry := make(logEntry)
|
||||
for _, field := range fields {
|
||||
entry[field.Key] = field.Value
|
||||
}
|
||||
entry[timestampKey] = getTimestamp()
|
||||
entry[levelKey] = level
|
||||
entry[contentKey] = val
|
||||
@@ -331,12 +347,14 @@ func wrapLevelWithColor(level string) string {
|
||||
}
|
||||
|
||||
func writeJson(writer io.Writer, info any) {
|
||||
if content, err := json.Marshal(info); err != nil {
|
||||
log.Println(err.Error())
|
||||
if content, err := marshalJson(info); err != nil {
|
||||
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
|
||||
} else if writer == nil {
|
||||
log.Println(string(content))
|
||||
} else {
|
||||
writer.Write(append(content, '\n'))
|
||||
if _, err := writer.Write(append(content, '\n')); err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +402,7 @@ func writePlainValue(writer io.Writer, level string, val any, fields ...string)
|
||||
buf.WriteString(level)
|
||||
buf.WriteByte(plainEncodingSep)
|
||||
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,23 @@ func TestWriteJson(t *testing.T) {
|
||||
log.SetOutput(&buf)
|
||||
writeJson(nil, "foo")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
|
||||
buf.Reset()
|
||||
writeJson(hardToWriteWriter{}, "foo")
|
||||
assert.Contains(t, buf.String(), "write error")
|
||||
|
||||
buf.Reset()
|
||||
writeJson(nil, make(chan int))
|
||||
assert.Contains(t, buf.String(), "unsupported type")
|
||||
|
||||
buf.Reset()
|
||||
type C struct {
|
||||
RC func()
|
||||
}
|
||||
writeJson(nil, C{
|
||||
RC: func() {},
|
||||
})
|
||||
assert.Contains(t, buf.String(), "runtime/debug.Stack")
|
||||
}
|
||||
|
||||
func TestWritePlainAny(t *testing.T) {
|
||||
@@ -165,6 +179,49 @@ func TestWritePlainAny(t *testing.T) {
|
||||
writePlainAny(hardToWriteWriter{}, levelFatal, "foo")
|
||||
assert.Contains(t, buf.String(), "write error")
|
||||
|
||||
buf.Reset()
|
||||
type C struct {
|
||||
RC func()
|
||||
}
|
||||
writePlainAny(nil, levelError, C{
|
||||
RC: func() {},
|
||||
})
|
||||
assert.Contains(t, buf.String(), "runtime/debug.Stack")
|
||||
}
|
||||
|
||||
func TestWritePlainDuplicate(t *testing.T) {
|
||||
old := atomic.SwapUint32(&encoding, plainEncodingType)
|
||||
t.Cleanup(func() {
|
||||
atomic.StoreUint32(&encoding, old)
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
output(&buf, levelInfo, "foo", LogField{
|
||||
Key: "first",
|
||||
Value: "a",
|
||||
}, LogField{
|
||||
Key: "first",
|
||||
Value: "b",
|
||||
})
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.NotContains(t, buf.String(), "first=a")
|
||||
assert.Contains(t, buf.String(), "first=b")
|
||||
|
||||
buf.Reset()
|
||||
output(&buf, levelInfo, "foo", LogField{
|
||||
Key: "first",
|
||||
Value: "a",
|
||||
}, LogField{
|
||||
Key: "first",
|
||||
Value: "b",
|
||||
}, LogField{
|
||||
Key: "second",
|
||||
Value: "c",
|
||||
})
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.NotContains(t, buf.String(), "first=a")
|
||||
assert.Contains(t, buf.String(), "first=b")
|
||||
assert.Contains(t, buf.String(), "second=c")
|
||||
}
|
||||
|
||||
func TestLogWithLimitContentLength(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
)
|
||||
|
||||
// Marshal marshals the given val and returns the map that contains the fields.
|
||||
// optional=another is not implemented, and it's hard to implement and not common used.
|
||||
// optional=another is not implemented, and it's hard to implement and not commonly used.
|
||||
func Marshal(val any) (map[string]map[string]any, error) {
|
||||
ret := make(map[string]map[string]any)
|
||||
tp := reflect.TypeOf(val)
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
ignoreKey = "-"
|
||||
numberTypeString = "number"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -37,7 +39,7 @@ var (
|
||||
)
|
||||
|
||||
type (
|
||||
// Unmarshaler is used to unmarshal with given tag key.
|
||||
// Unmarshaler is used to unmarshal with the given tag key.
|
||||
Unmarshaler struct {
|
||||
key string
|
||||
opts unmarshalOptions
|
||||
@@ -49,6 +51,7 @@ type (
|
||||
unmarshalOptions struct {
|
||||
fillDefault bool
|
||||
fromString bool
|
||||
opaqueKeys bool
|
||||
canonicalKey func(key string) string
|
||||
}
|
||||
)
|
||||
@@ -66,13 +69,17 @@ func NewUnmarshaler(key string, opts ...UnmarshalOption) *Unmarshaler {
|
||||
return &unmarshaler
|
||||
}
|
||||
|
||||
// UnmarshalKey unmarshals m into v with tag key.
|
||||
// UnmarshalKey unmarshals m into v with the tag key.
|
||||
func UnmarshalKey(m map[string]any, v any) error {
|
||||
return keyUnmarshaler.Unmarshal(m, v)
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals m into v.
|
||||
func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
func (u *Unmarshaler) Unmarshal(i, v any) error {
|
||||
return u.unmarshal(i, v, "")
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
|
||||
valueType := reflect.TypeOf(v)
|
||||
if valueType.Kind() != reflect.Ptr {
|
||||
return errValueNotSettable
|
||||
@@ -85,13 +92,13 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
return u.UnmarshalValuer(mapValuer(iv), v)
|
||||
return u.unmarshalValuer(mapValuer(iv), v, fullName)
|
||||
case []any:
|
||||
if elemType.Kind() != reflect.Slice {
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv)
|
||||
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
|
||||
default:
|
||||
return errUnsupportedType
|
||||
}
|
||||
@@ -99,17 +106,22 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
|
||||
// UnmarshalValuer unmarshals m into v.
|
||||
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
|
||||
return u.unmarshalWithFullName(simpleValuer{current: m}, v, "")
|
||||
return u.unmarshalValuer(simpleValuer{current: m}, v, "")
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any) error {
|
||||
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
|
||||
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value,
|
||||
mapValue any, fullName string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
fieldKeyType := fieldType.Key()
|
||||
fieldElemType := fieldType.Elem()
|
||||
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue)
|
||||
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue, fullName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -143,14 +155,15 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any) error {
|
||||
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||
mapValue any, fullName string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
refValue := reflect.ValueOf(mapValue)
|
||||
if refValue.Kind() != reflect.Slice {
|
||||
return errTypeMismatch
|
||||
return newTypeMismatchErrorWithHint(fullName, reflect.Slice.String(), refValue.Type().String())
|
||||
}
|
||||
if refValue.IsNil() {
|
||||
return nil
|
||||
@@ -173,6 +186,8 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
}
|
||||
|
||||
valid = true
|
||||
sliceFullName := fmt.Sprintf("%s[%d]", fullName, i)
|
||||
|
||||
switch dereffedBaseKind {
|
||||
case reflect.Struct:
|
||||
target := reflect.New(dereffedBaseType)
|
||||
@@ -181,17 +196,17 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
if err := u.Unmarshal(val, target.Interface()); err != nil {
|
||||
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
|
||||
case reflect.Slice:
|
||||
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue); err != nil {
|
||||
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue); err != nil {
|
||||
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue, sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -205,16 +220,16 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value,
|
||||
mapValue any) error {
|
||||
mapValue any, fullName string) error {
|
||||
var slice []any
|
||||
switch v := mapValue.(type) {
|
||||
case fmt.Stringer:
|
||||
if err := jsonx.UnmarshalFromString(v.String(), &slice); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("fullName: `%s`, error: `%w`", fullName, err)
|
||||
}
|
||||
case string:
|
||||
if err := jsonx.UnmarshalFromString(v, &slice); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("fullName: `%s`, error: `%w`", fullName, err)
|
||||
}
|
||||
default:
|
||||
return errUnsupportedType
|
||||
@@ -225,7 +240,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
||||
|
||||
for i := 0; i < len(slice); i++ {
|
||||
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i]); err != nil {
|
||||
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i], fullName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -235,7 +250,11 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
baseKind reflect.Kind, value any) error {
|
||||
baseKind reflect.Kind, value any, fullName string) error {
|
||||
if value == nil {
|
||||
return errNilSliceElement
|
||||
}
|
||||
|
||||
ithVal := slice.Index(index)
|
||||
switch v := value.(type) {
|
||||
case fmt.Stringer:
|
||||
@@ -243,7 +262,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
case string:
|
||||
return setValueFromString(baseKind, ithVal, v)
|
||||
case map[string]any:
|
||||
return u.fillMap(ithVal.Type(), ithVal, value)
|
||||
return u.fillMap(ithVal.Type(), ithVal, value, fullName)
|
||||
default:
|
||||
// don't need to consider the difference between int, int8, int16, int32, int64,
|
||||
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
|
||||
@@ -269,7 +288,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value,
|
||||
defaultValue string) error {
|
||||
defaultValue, fullName string) error {
|
||||
baseFieldType := Deref(derefedType.Elem())
|
||||
baseFieldKind := baseFieldType.Kind()
|
||||
defaultCacheLock.Lock()
|
||||
@@ -287,10 +306,37 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
|
||||
defaultCacheLock.Unlock()
|
||||
}
|
||||
|
||||
return u.fillSlice(derefedType, value, slice)
|
||||
return u.fillSlice(derefedType, value, slice, fullName)
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any) (reflect.Value, error) {
|
||||
func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
|
||||
value reflect.Value, targetValue string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
baseType := Deref(fieldType)
|
||||
target := reflect.New(baseType)
|
||||
switch u.key {
|
||||
case jsonTagKey:
|
||||
unmarshaler, ok := target.Interface().(json.Unmarshaler)
|
||||
if !ok {
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
if err := unmarshaler.UnmarshalJSON([]byte(targetValue)); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
value.Set(target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any,
|
||||
fullName string) (reflect.Value, error) {
|
||||
mapType := reflect.MapOf(keyType, elemType)
|
||||
valueType := reflect.TypeOf(mapValue)
|
||||
if mapType == valueType {
|
||||
@@ -309,11 +355,12 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
for _, key := range refValue.MapKeys() {
|
||||
keythValue := refValue.MapIndex(key)
|
||||
keythData := keythValue.Interface()
|
||||
mapFullName := fmt.Sprintf("%s[%s]", fullName, key.String())
|
||||
|
||||
switch dereffedElemKind {
|
||||
case reflect.Slice:
|
||||
target := reflect.New(dereffedElemType)
|
||||
if err := u.fillSlice(elemType, target.Elem(), keythData); err != nil {
|
||||
if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
|
||||
@@ -325,7 +372,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
}
|
||||
|
||||
target := reflect.New(dereffedElemType)
|
||||
if err := u.Unmarshal(keythMap, target.Interface()); err != nil {
|
||||
if err := u.unmarshal(keythMap, target.Interface(), mapFullName); err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
|
||||
@@ -336,7 +383,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
return emptyValue, errTypeMismatch
|
||||
}
|
||||
|
||||
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap)
|
||||
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap, mapFullName)
|
||||
if err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
@@ -381,6 +428,15 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
return targetValue, nil
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) implementsUnmarshaler(t reflect.Type) bool {
|
||||
switch u.key {
|
||||
case jsonTagKey:
|
||||
return t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Valuer, fullName string) (
|
||||
string, *fieldOptionsWithContext, error) {
|
||||
key, options, err := parseKeyAndOptions(u.key, field)
|
||||
@@ -410,6 +466,10 @@ func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Value
|
||||
}
|
||||
}
|
||||
|
||||
if u.opts.fillDefault {
|
||||
return key, &options.fieldOptionsWithContext, nil
|
||||
}
|
||||
|
||||
optsWithContext, err := options.toOptionsWithContext(key, m, fullName)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
@@ -425,6 +485,10 @@ func (u *Unmarshaler) processAnonymousField(field reflect.StructField, value ref
|
||||
return err
|
||||
}
|
||||
|
||||
if key == ignoreKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.optional() {
|
||||
return u.processAnonymousFieldOptional(field, value, key, m, fullName)
|
||||
}
|
||||
@@ -483,7 +547,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
|
||||
return err
|
||||
}
|
||||
|
||||
_, hasValue := getValue(m, fieldKey)
|
||||
_, hasValue := getValue(m, fieldKey, u.opts.opaqueKeys)
|
||||
if hasValue {
|
||||
if !filled {
|
||||
filled = true
|
||||
@@ -541,15 +605,17 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
|
||||
parent: vp.parent,
|
||||
}, fullName)
|
||||
case typeKind == reflect.Slice && valueKind == reflect.Slice:
|
||||
return u.fillSlice(fieldType, value, mapValue)
|
||||
return u.fillSlice(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.Map && typeKind == reflect.Map:
|
||||
return u.fillMap(fieldType, value, mapValue)
|
||||
return u.fillMap(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.String && typeKind == reflect.Map:
|
||||
return u.fillMapFromString(value, mapValue)
|
||||
case valueKind == reflect.String && typeKind == reflect.Slice:
|
||||
return u.fillSliceFromString(fieldType, value, mapValue)
|
||||
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.String && derefedFieldType == durationType:
|
||||
return fillDurationValue(fieldType, value, mapValue.(string))
|
||||
case valueKind == reflect.String && typeKind == reflect.Struct && u.implementsUnmarshaler(fieldType):
|
||||
return u.fillUnmarshalerStruct(fieldType, value, mapValue.(string))
|
||||
default:
|
||||
return u.processFieldPrimitive(fieldType, value, mapValue, opts, fullName)
|
||||
}
|
||||
@@ -592,25 +658,28 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
|
||||
target := reflect.New(Deref(fieldType)).Elem()
|
||||
|
||||
switch typeKind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
iValue, err := v.Int64()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if err := setValueFromString(typeKind, target, v.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Float32:
|
||||
fValue, err := v.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target.SetInt(iValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
iValue, err := v.Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
// if the value is a pointer, we need to check overflow with the pointer's value.
|
||||
derefedValue := value
|
||||
for derefedValue.Type().Kind() == reflect.Ptr {
|
||||
derefedValue = derefedValue.Elem()
|
||||
}
|
||||
if derefedValue.CanFloat() && derefedValue.OverflowFloat(fValue) {
|
||||
return fmt.Errorf("parsing %q as float32: value out of range", v.String())
|
||||
}
|
||||
|
||||
if iValue < 0 {
|
||||
return fmt.Errorf("unmarshal %q with bad value %q", fullName, v.String())
|
||||
}
|
||||
|
||||
target.SetUint(uint64(iValue))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
target.SetFloat(fValue)
|
||||
case reflect.Float64:
|
||||
fValue, err := v.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -618,7 +687,7 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
|
||||
|
||||
target.SetFloat(fValue)
|
||||
default:
|
||||
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), value.Type().String())
|
||||
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), numberTypeString)
|
||||
}
|
||||
|
||||
SetValue(fieldType, value, target)
|
||||
@@ -712,6 +781,10 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
||||
return err
|
||||
}
|
||||
|
||||
if key == ignoreKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
fullName = join(fullName, key)
|
||||
if opts != nil && len(opts.EnvVar) > 0 {
|
||||
envVal := proc.Env(opts.EnvVar)
|
||||
@@ -726,7 +799,7 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
||||
}
|
||||
|
||||
valuer := createValuer(m, opts)
|
||||
mapValue, hasValue := getValue(valuer, canonicalKey)
|
||||
mapValue, hasValue := getValue(valuer, canonicalKey, u.opts.opaqueKeys)
|
||||
|
||||
// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
|
||||
if u.opts.fillDefault {
|
||||
@@ -819,7 +892,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
|
||||
|
||||
switch fieldKind {
|
||||
case reflect.Array, reflect.Slice:
|
||||
return u.fillSliceWithDefault(derefedType, value, defaultValue)
|
||||
return u.fillSliceWithDefault(derefedType, value, defaultValue, fullName)
|
||||
default:
|
||||
return setValueFromString(fieldKind, value, defaultValue)
|
||||
}
|
||||
@@ -867,7 +940,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
|
||||
|
||||
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if err := ValidatePtr(&rv); err != nil {
|
||||
if err := ValidatePtr(rv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -889,11 +962,6 @@ func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName
|
||||
typeField := baseType.Field(i)
|
||||
valueField := valElem.Field(i)
|
||||
if err := u.processField(typeField, valueField, m, fullName); err != nil {
|
||||
if len(fullName) > 0 {
|
||||
err = fmt.Errorf("%w, fullName: %s, field: %s, type: %s",
|
||||
err, fullName, typeField.Name, valueField.Type().Name())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -922,6 +990,14 @@ func WithDefault() UnmarshalOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
|
||||
// Opaque keys are keys that are not processed by the unmarshaler.
|
||||
func WithOpaqueKeys() UnmarshalOption {
|
||||
return func(opt *unmarshalOptions) {
|
||||
opt.opaqueKeys = true
|
||||
}
|
||||
}
|
||||
|
||||
func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
|
||||
if opts.inherit() {
|
||||
return recursiveValuer{
|
||||
@@ -999,8 +1075,8 @@ func fillWithSameType(fieldType reflect.Type, value reflect.Value, mapValue any,
|
||||
}
|
||||
|
||||
// getValue gets the value for the specific key, the key can be in the format of parentKey.childKey
|
||||
func getValue(m valuerWithParent, key string) (any, bool) {
|
||||
keys := readKeys(key)
|
||||
func getValue(m valuerWithParent, key string, opaque bool) (any, bool) {
|
||||
keys := readKeys(key, opaque)
|
||||
return getValueWithChainedKeys(m, keys)
|
||||
}
|
||||
|
||||
@@ -1059,7 +1135,11 @@ func newTypeMismatchErrorWithHint(name, expectType, actualType string) error {
|
||||
name, expectType, actualType)
|
||||
}
|
||||
|
||||
func readKeys(key string) []string {
|
||||
func readKeys(key string, opaque bool) []string {
|
||||
if opaque {
|
||||
return []string{key}
|
||||
}
|
||||
|
||||
cacheKeysLock.Lock()
|
||||
keys, ok := cacheKeys[key]
|
||||
cacheKeysLock.Unlock()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,11 +30,13 @@ const (
|
||||
leftSquareBracket = '['
|
||||
rightSquareBracket = ']'
|
||||
segmentSeparator = ','
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedType = errors.New("unsupported type on setting field value")
|
||||
errNumberRange = errors.New("wrong number range setting")
|
||||
errNilSliceElement = errors.New("null element for slice")
|
||||
optionsCache = make(map[string]optionsCacheValue)
|
||||
cacheLock sync.RWMutex
|
||||
structRequiredCache = make(map[reflect.Type]requiredCacheValue)
|
||||
@@ -79,7 +81,7 @@ func SetMapIndexValue(tp reflect.Type, value, key, target reflect.Value) {
|
||||
}
|
||||
|
||||
// ValidatePtr validates v if it's a valid pointer.
|
||||
func ValidatePtr(v *reflect.Value) error {
|
||||
func ValidatePtr(v reflect.Value) error {
|
||||
// sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr,
|
||||
// panic otherwise
|
||||
if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
@@ -100,27 +102,30 @@ func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
|
||||
default:
|
||||
return false, errTypeMismatch
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intValue, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the value %q cannot parsed as int", str)
|
||||
}
|
||||
|
||||
return intValue, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uintValue, err := strconv.ParseUint(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the value %q cannot parsed as uint", str)
|
||||
}
|
||||
|
||||
return uintValue, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the value %q cannot parsed as float", str)
|
||||
}
|
||||
|
||||
return floatValue, nil
|
||||
case reflect.Int:
|
||||
return strconv.ParseInt(str, 10, intSize)
|
||||
case reflect.Int8:
|
||||
return strconv.ParseInt(str, 10, 8)
|
||||
case reflect.Int16:
|
||||
return strconv.ParseInt(str, 10, 16)
|
||||
case reflect.Int32:
|
||||
return strconv.ParseInt(str, 10, 32)
|
||||
case reflect.Int64:
|
||||
return strconv.ParseInt(str, 10, 64)
|
||||
case reflect.Uint:
|
||||
return strconv.ParseUint(str, 10, intSize)
|
||||
case reflect.Uint8:
|
||||
return strconv.ParseUint(str, 10, 8)
|
||||
case reflect.Uint16:
|
||||
return strconv.ParseUint(str, 10, 16)
|
||||
case reflect.Uint32:
|
||||
return strconv.ParseUint(str, 10, 32)
|
||||
case reflect.Uint64:
|
||||
return strconv.ParseUint(str, 10, 64)
|
||||
case reflect.Float32:
|
||||
return strconv.ParseFloat(str, 32)
|
||||
case reflect.Float64:
|
||||
return strconv.ParseFloat(str, 64)
|
||||
case reflect.String:
|
||||
return str, nil
|
||||
default:
|
||||
@@ -411,7 +416,7 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
|
||||
}
|
||||
|
||||
// parseOptions parses the given options in tag.
|
||||
// for example: `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"`
|
||||
// for example, `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"`
|
||||
func parseOptions(val string) []string {
|
||||
if len(val) == 0 {
|
||||
return nil
|
||||
@@ -486,19 +491,22 @@ func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) err
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
value.SetBool(v.(bool))
|
||||
return nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
value.SetInt(v.(int64))
|
||||
return nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
value.SetUint(v.(uint64))
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
value.SetFloat(v.(float64))
|
||||
return nil
|
||||
case reflect.String:
|
||||
value.SetString(v.(string))
|
||||
return nil
|
||||
default:
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error {
|
||||
@@ -575,7 +583,8 @@ func usingDifferentKeys(key string, field reflect.StructField) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string, opts *fieldOptionsWithContext) error {
|
||||
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string,
|
||||
opts *fieldOptionsWithContext) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
@@ -218,25 +218,25 @@ func TestParseSegments(t *testing.T) {
|
||||
func TestValidatePtrWithNonPtr(t *testing.T) {
|
||||
var foo string
|
||||
rve := reflect.ValueOf(foo)
|
||||
assert.NotNil(t, ValidatePtr(&rve))
|
||||
assert.NotNil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithPtr(t *testing.T) {
|
||||
var foo string
|
||||
rve := reflect.ValueOf(&foo)
|
||||
assert.Nil(t, ValidatePtr(&rve))
|
||||
assert.Nil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithNilPtr(t *testing.T) {
|
||||
var foo *string
|
||||
rve := reflect.ValueOf(foo)
|
||||
assert.NotNil(t, ValidatePtr(&rve))
|
||||
assert.NotNil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithZeroValue(t *testing.T) {
|
||||
var s string
|
||||
e := reflect.Zero(reflect.TypeOf(s))
|
||||
assert.NotNil(t, ValidatePtr(&e))
|
||||
assert.NotNil(t, ValidatePtr(e))
|
||||
}
|
||||
|
||||
func TestSetValueNotSettable(t *testing.T) {
|
||||
|
||||
@@ -26,9 +26,9 @@ type (
|
||||
parent valuerWithParent
|
||||
}
|
||||
|
||||
// mapValuer is a type for map to meet the Valuer interface.
|
||||
// mapValuer is a type for the map to meet the Valuer interface.
|
||||
mapValuer map[string]any
|
||||
// simpleValuer is a type to get value from current node.
|
||||
// simpleValuer is a type to get value from the current node.
|
||||
simpleValuer node
|
||||
// recursiveValuer is a type to get the value recursively from current and parent nodes.
|
||||
recursiveValuer node
|
||||
|
||||
@@ -1011,6 +1011,15 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
|
||||
assert.Equal(t, rune(3), v.Machine["node3"])
|
||||
}
|
||||
|
||||
func TestUnmarshalYamlStringOfInt(t *testing.T) {
|
||||
text := `password: 123456`
|
||||
var v struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
reader := strings.NewReader(text)
|
||||
assert.Error(t, UnmarshalYamlReader(reader, &v))
|
||||
}
|
||||
|
||||
func TestUnmarshalYamlBadInput(t *testing.T) {
|
||||
var v struct {
|
||||
Any string
|
||||
|
||||
@@ -15,3 +15,17 @@ func TestCalcEntropy(t *testing.T) {
|
||||
}
|
||||
assert.True(t, CalcEntropy(m) > .99)
|
||||
}
|
||||
|
||||
func TestCalcEmptyEntropy(t *testing.T) {
|
||||
m := make(map[any]int)
|
||||
assert.Equal(t, float64(1), CalcEntropy(m))
|
||||
}
|
||||
|
||||
func TestCalcDiffEntropy(t *testing.T) {
|
||||
const total = 1000
|
||||
m := make(map[any]int, total)
|
||||
for i := 0; i < total; i++ {
|
||||
m[i] = i
|
||||
}
|
||||
assert.True(t, CalcEntropy(m) < .99)
|
||||
}
|
||||
|
||||
34
core/mathx/range.go
Normal file
34
core/mathx/range.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package mathx
|
||||
|
||||
type Numerical interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// AtLeast returns the greater of x or lower.
|
||||
func AtLeast[T Numerical](x, lower T) T {
|
||||
if x < lower {
|
||||
return lower
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// AtMost returns the smaller of x or upper.
|
||||
func AtMost[T Numerical](x, upper T) T {
|
||||
if x > upper {
|
||||
return upper
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Between returns the value of x clamped to the range [lower, upper].
|
||||
func Between[T Numerical](x, lower, upper T) T {
|
||||
if x < lower {
|
||||
return lower
|
||||
}
|
||||
if x > upper {
|
||||
return upper
|
||||
}
|
||||
return x
|
||||
}
|
||||
513
core/mathx/range_test.go
Normal file
513
core/mathx/range_test.go
Normal file
@@ -0,0 +1,513 @@
|
||||
package mathx
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAtLeast(t *testing.T) {
|
||||
t.Run("test int", func(t *testing.T) {
|
||||
if got := AtLeast(10, 5); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(3, 5); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(5, 5); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int8", func(t *testing.T) {
|
||||
if got := AtLeast(int8(10), int8(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(int8(3), int8(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(int8(5), int8(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int16", func(t *testing.T) {
|
||||
if got := AtLeast(int16(10), int16(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(int16(3), int16(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(int16(5), int16(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int32", func(t *testing.T) {
|
||||
if got := AtLeast(int32(10), int32(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(int32(3), int32(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(int32(5), int32(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int64", func(t *testing.T) {
|
||||
if got := AtLeast(int64(10), int64(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(int64(3), int64(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(int64(5), int64(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint", func(t *testing.T) {
|
||||
if got := AtLeast(uint(10), uint(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(uint(3), uint(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(uint(5), uint(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint8", func(t *testing.T) {
|
||||
if got := AtLeast(uint8(10), uint8(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(uint8(3), uint8(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(uint8(5), uint8(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint16", func(t *testing.T) {
|
||||
if got := AtLeast(uint16(10), uint16(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(uint16(3), uint16(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(uint16(5), uint16(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint32", func(t *testing.T) {
|
||||
if got := AtLeast(uint32(10), uint32(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(uint32(3), uint32(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(uint32(5), uint32(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint64", func(t *testing.T) {
|
||||
if got := AtLeast(uint64(10), uint64(5)); got != 10 {
|
||||
t.Errorf("AtLeast() = %v, want 10", got)
|
||||
}
|
||||
if got := AtLeast(uint64(3), uint64(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
if got := AtLeast(uint64(5), uint64(5)); got != 5 {
|
||||
t.Errorf("AtLeast() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float32", func(t *testing.T) {
|
||||
if got := AtLeast(float32(10.0), float32(5.0)); got != 10.0 {
|
||||
t.Errorf("AtLeast() = %v, want 10.0", got)
|
||||
}
|
||||
if got := AtLeast(float32(3.0), float32(5.0)); got != 5.0 {
|
||||
t.Errorf("AtLeast() = %v, want 5.0", got)
|
||||
}
|
||||
if got := AtLeast(float32(5.0), float32(5.0)); got != 5.0 {
|
||||
t.Errorf("AtLeast() = %v, want 5.0", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float64", func(t *testing.T) {
|
||||
if got := AtLeast(10.0, 5.0); got != 10.0 {
|
||||
t.Errorf("AtLeast() = %v, want 10.0", got)
|
||||
}
|
||||
if got := AtLeast(3.0, 5.0); got != 5.0 {
|
||||
t.Errorf("AtLeast() = %v, want 5.0", got)
|
||||
}
|
||||
if got := AtLeast(5.0, 5.0); got != 5.0 {
|
||||
t.Errorf("AtLeast() = %v, want 5.0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAtMost(t *testing.T) {
|
||||
t.Run("test int", func(t *testing.T) {
|
||||
if got := AtMost(10, 5); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(3, 5); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(5, 5); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int8", func(t *testing.T) {
|
||||
if got := AtMost(int8(10), int8(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(int8(3), int8(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(int8(5), int8(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int16", func(t *testing.T) {
|
||||
if got := AtMost(int16(10), int16(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(int16(3), int16(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(int16(5), int16(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int32", func(t *testing.T) {
|
||||
if got := AtMost(int32(10), int32(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(int32(3), int32(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(int32(5), int32(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int64", func(t *testing.T) {
|
||||
if got := AtMost(int64(10), int64(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(int64(3), int64(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(int64(5), int64(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint", func(t *testing.T) {
|
||||
if got := AtMost(uint(10), uint(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(uint(3), uint(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(uint(5), uint(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint8", func(t *testing.T) {
|
||||
if got := AtMost(uint8(10), uint8(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(uint8(3), uint8(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(uint8(5), uint8(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint16", func(t *testing.T) {
|
||||
if got := AtMost(uint16(10), uint16(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(uint16(3), uint16(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(uint16(5), uint16(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint32", func(t *testing.T) {
|
||||
if got := AtMost(uint32(10), uint32(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(uint32(3), uint32(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(uint32(5), uint32(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint64", func(t *testing.T) {
|
||||
if got := AtMost(uint64(10), uint64(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
if got := AtMost(uint64(3), uint64(5)); got != 3 {
|
||||
t.Errorf("AtMost() = %v, want 3", got)
|
||||
}
|
||||
if got := AtMost(uint64(5), uint64(5)); got != 5 {
|
||||
t.Errorf("AtMost() = %v, want 5", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float32", func(t *testing.T) {
|
||||
if got := AtMost(float32(10.0), float32(5.0)); got != 5.0 {
|
||||
t.Errorf("AtMost() = %v, want 5.0", got)
|
||||
}
|
||||
if got := AtMost(float32(3.0), float32(5.0)); got != 3.0 {
|
||||
t.Errorf("AtMost() = %v, want 3.0", got)
|
||||
}
|
||||
if got := AtMost(float32(5.0), float32(5.0)); got != 5.0 {
|
||||
t.Errorf("AtMost() = %v, want 5.0", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float64", func(t *testing.T) {
|
||||
if got := AtMost(10.0, 5.0); got != 5.0 {
|
||||
t.Errorf("AtMost() = %v, want 5.0", got)
|
||||
}
|
||||
if got := AtMost(3.0, 5.0); got != 3.0 {
|
||||
t.Errorf("AtMost() = %v, want 3.0", got)
|
||||
}
|
||||
if got := AtMost(5.0, 5.0); got != 5.0 {
|
||||
t.Errorf("AtMost() = %v, want 5.0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBetween(t *testing.T) {
|
||||
t.Run("test int", func(t *testing.T) {
|
||||
if got := Between(10, 5, 15); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(3, 5, 15); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(20, 5, 15); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(5, 5, 15); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(15, 5, 15); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int8", func(t *testing.T) {
|
||||
if got := Between(int8(10), int8(5), int8(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(int8(3), int8(5), int8(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int8(20), int8(5), int8(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(int8(5), int8(5), int8(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int8(15), int8(5), int8(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int16", func(t *testing.T) {
|
||||
if got := Between(int16(10), int16(5), int16(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(int16(3), int16(5), int16(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int16(20), int16(5), int16(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(int16(5), int16(5), int16(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int16(15), int16(5), int16(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int32", func(t *testing.T) {
|
||||
if got := Between(int32(10), int32(5), int32(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(int32(3), int32(5), int32(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int32(20), int32(5), int32(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(int32(5), int32(5), int32(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int32(15), int32(5), int32(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test int64", func(t *testing.T) {
|
||||
if got := Between(int64(10), int64(5), int64(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(int64(3), int64(5), int64(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int64(20), int64(5), int64(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(int64(5), int64(5), int64(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(int64(15), int64(5), int64(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint", func(t *testing.T) {
|
||||
if got := Between(uint(10), uint(5), uint(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(uint(3), uint(5), uint(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint(20), uint(5), uint(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(uint(5), uint(5), uint(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint(15), uint(5), uint(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint8", func(t *testing.T) {
|
||||
if got := Between(uint8(10), uint8(5), uint8(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(uint8(3), uint8(5), uint8(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint8(20), uint8(5), uint8(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(uint8(5), uint8(5), uint8(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint8(15), uint8(5), uint8(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint16", func(t *testing.T) {
|
||||
if got := Between(uint16(10), uint16(5), uint16(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(uint16(3), uint16(5), uint16(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint16(20), uint16(5), uint16(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(uint16(5), uint16(5), uint16(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint16(15), uint16(5), uint16(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint32", func(t *testing.T) {
|
||||
if got := Between(uint32(10), uint32(5), uint32(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(uint32(3), uint32(5), uint32(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint32(20), uint32(5), uint32(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(uint32(5), uint32(5), uint32(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint32(15), uint32(5), uint32(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uint64", func(t *testing.T) {
|
||||
if got := Between(uint64(10), uint64(5), uint64(15)); got != 10 {
|
||||
t.Errorf("Between() = %v, want 10", got)
|
||||
}
|
||||
if got := Between(uint64(3), uint64(5), uint64(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint64(20), uint64(5), uint64(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
if got := Between(uint64(5), uint64(5), uint64(15)); got != 5 {
|
||||
t.Errorf("Between() = %v, want 5", got)
|
||||
}
|
||||
if got := Between(uint64(15), uint64(5), uint64(15)); got != 15 {
|
||||
t.Errorf("Between() = %v, want 15", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float32", func(t *testing.T) {
|
||||
if got := Between(float32(10.0), float32(5.0), float32(15.0)); got != 10.0 {
|
||||
t.Errorf("Between() = %v, want 10.0", got)
|
||||
}
|
||||
if got := Between(float32(3.0), float32(5.0), float32(15.0)); got != 5.0 {
|
||||
t.Errorf("Between() = %v, want 5.0", got)
|
||||
}
|
||||
if got := Between(float32(20.0), float32(5.0), float32(15.0)); got != 15.0 {
|
||||
t.Errorf("Between() = %v, want 15.0", got)
|
||||
}
|
||||
if got := Between(float32(5.0), float32(5.0), float32(15.0)); got != 5.0 {
|
||||
t.Errorf("Between() = %v, want 5.0", got)
|
||||
}
|
||||
if got := Between(float32(15.0), float32(5.0), float32(15.0)); got != 15.0 {
|
||||
t.Errorf("Between() = %v, want 15.0", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test float64", func(t *testing.T) {
|
||||
if got := Between(10.0, 5.0, 15.0); got != 10.0 {
|
||||
t.Errorf("Between() = %v, want 10.0", got)
|
||||
}
|
||||
if got := Between(3.0, 5.0, 15.0); got != 5.0 {
|
||||
t.Errorf("Between() = %v, want 5.0", got)
|
||||
}
|
||||
if got := Between(20.0, 5.0, 15.0); got != 15.0 {
|
||||
t.Errorf("Between() = %v, want 15.0", got)
|
||||
}
|
||||
if got := Between(5.0, 5.0, 15.0); got != 5.0 {
|
||||
t.Errorf("Between() = %v, want 5.0", got)
|
||||
}
|
||||
if got := Between(15.0, 5.0, 15.0); got != 15.0 {
|
||||
t.Errorf("Between() = %v, want 15.0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package metric
|
||||
import (
|
||||
prom "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
"github.com/zeromicro/go-zero/core/prometheus"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -47,20 +46,16 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
|
||||
return cv
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) Inc(labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
cv.counter.WithLabelValues(labels...).Inc()
|
||||
func (cv *promCounterVec) Add(v float64, labels ...string) {
|
||||
update(func() {
|
||||
cv.counter.WithLabelValues(labels...).Add(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) Add(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
cv.counter.WithLabelValues(labels...).Add(v)
|
||||
func (cv *promCounterVec) Inc(labels ...string) {
|
||||
update(func() {
|
||||
cv.counter.WithLabelValues(labels...).Inc()
|
||||
})
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) close() bool {
|
||||
|
||||
@@ -3,7 +3,6 @@ package metric
|
||||
import (
|
||||
prom "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
"github.com/zeromicro/go-zero/core/prometheus"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -16,8 +15,12 @@ type (
|
||||
Set(v float64, labels ...string)
|
||||
// Inc increments labels.
|
||||
Inc(labels ...string)
|
||||
// Dec decrements labels.
|
||||
Dec(labels ...string)
|
||||
// Add adds v to labels.
|
||||
Add(v float64, labels ...string)
|
||||
// Sub subtracts v to labels.
|
||||
Sub(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
@@ -32,13 +35,12 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
|
||||
return nil
|
||||
}
|
||||
|
||||
vec := prom.NewGaugeVec(
|
||||
prom.GaugeOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
}, cfg.Labels)
|
||||
vec := prom.NewGaugeVec(prom.GaugeOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
}, cfg.Labels)
|
||||
prom.MustRegister(vec)
|
||||
gv := &promGaugeVec{
|
||||
gauge: vec,
|
||||
@@ -50,28 +52,34 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
|
||||
return gv
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Inc(labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Inc()
|
||||
func (gv *promGaugeVec) Add(v float64, labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Add(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Add(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
func (gv *promGaugeVec) Dec(labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Dec()
|
||||
})
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Add(v)
|
||||
func (gv *promGaugeVec) Inc(labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Inc()
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Set(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Set(v)
|
||||
})
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Set(v)
|
||||
func (gv *promGaugeVec) Sub(v float64, labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Sub(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) close() bool {
|
||||
|
||||
@@ -40,6 +40,23 @@ func TestGaugeInc(t *testing.T) {
|
||||
assert.Equal(t, float64(2), r)
|
||||
}
|
||||
|
||||
func TestGaugeDec(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
Namespace: "rpc_client",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_ms",
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Labels: []string{"path"},
|
||||
})
|
||||
defer gaugeVec.close()
|
||||
gv, _ := gaugeVec.(*promGaugeVec)
|
||||
gv.Dec("/users")
|
||||
gv.Dec("/users")
|
||||
r := testutil.ToFloat64(gv.gauge)
|
||||
assert.Equal(t, float64(-2), r)
|
||||
}
|
||||
|
||||
func TestGaugeAdd(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
@@ -57,6 +74,23 @@ func TestGaugeAdd(t *testing.T) {
|
||||
assert.Equal(t, float64(20), r)
|
||||
}
|
||||
|
||||
func TestGaugeSub(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
Namespace: "rpc_client",
|
||||
Subsystem: "request",
|
||||
Name: "duration_ms",
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Labels: []string{"path"},
|
||||
})
|
||||
defer gaugeVec.close()
|
||||
gv, _ := gaugeVec.(*promGaugeVec)
|
||||
gv.Sub(-100, "/classroom")
|
||||
gv.Sub(30, "/classroom")
|
||||
r := testutil.ToFloat64(gv.gauge)
|
||||
assert.Equal(t, float64(70), r)
|
||||
}
|
||||
|
||||
func TestGaugeSet(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
|
||||
@@ -3,24 +3,26 @@ package metric
|
||||
import (
|
||||
prom "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
"github.com/zeromicro/go-zero/core/prometheus"
|
||||
)
|
||||
|
||||
type (
|
||||
// A HistogramVecOpts is a histogram vector options.
|
||||
HistogramVecOpts struct {
|
||||
Namespace string
|
||||
Subsystem string
|
||||
Name string
|
||||
Help string
|
||||
Labels []string
|
||||
Buckets []float64
|
||||
Namespace string
|
||||
Subsystem string
|
||||
Name string
|
||||
Help string
|
||||
Labels []string
|
||||
Buckets []float64
|
||||
ConstLabels map[string]string
|
||||
}
|
||||
|
||||
// A HistogramVec interface represents a histogram vector.
|
||||
HistogramVec interface {
|
||||
// Observe adds observation v to labels.
|
||||
Observe(v int64, labels ...string)
|
||||
// ObserveFloat allow to observe float64 values.
|
||||
ObserveFloat(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
@@ -36,11 +38,12 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
|
||||
}
|
||||
|
||||
vec := prom.NewHistogramVec(prom.HistogramOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
Buckets: cfg.Buckets,
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
Buckets: cfg.Buckets,
|
||||
ConstLabels: cfg.ConstLabels,
|
||||
}, cfg.Labels)
|
||||
prom.MustRegister(vec)
|
||||
hv := &promHistogramVec{
|
||||
@@ -54,11 +57,15 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
|
||||
}
|
||||
|
||||
func (hv *promHistogramVec) Observe(v int64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
update(func() {
|
||||
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
|
||||
})
|
||||
}
|
||||
|
||||
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
|
||||
func (hv *promHistogramVec) ObserveFloat(v float64, labels ...string) {
|
||||
update(func() {
|
||||
hv.histogram.WithLabelValues(labels...).Observe(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (hv *promHistogramVec) close() bool {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
func TestNewHistogramVec(t *testing.T) {
|
||||
@@ -15,7 +14,7 @@ func TestNewHistogramVec(t *testing.T) {
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Buckets: []float64{1, 2, 3},
|
||||
})
|
||||
defer histogramVec.close()
|
||||
defer histogramVec.(*promHistogramVec).close()
|
||||
histogramVecNil := NewHistogramVec(nil)
|
||||
assert.NotNil(t, histogramVec)
|
||||
assert.Nil(t, histogramVecNil)
|
||||
@@ -29,9 +28,10 @@ func TestHistogramObserve(t *testing.T) {
|
||||
Buckets: []float64{1, 2, 3},
|
||||
Labels: []string{"method"},
|
||||
})
|
||||
defer histogramVec.close()
|
||||
defer histogramVec.(*promHistogramVec).close()
|
||||
hv, _ := histogramVec.(*promHistogramVec)
|
||||
hv.Observe(2, "/Users")
|
||||
hv.ObserveFloat(1.1, "/Users")
|
||||
|
||||
metadata := `
|
||||
# HELP counts rpc server requests duration(ms).
|
||||
@@ -39,15 +39,13 @@ func TestHistogramObserve(t *testing.T) {
|
||||
`
|
||||
val := `
|
||||
counts_bucket{method="/Users",le="1"} 0
|
||||
counts_bucket{method="/Users",le="2"} 1
|
||||
counts_bucket{method="/Users",le="3"} 1
|
||||
counts_bucket{method="/Users",le="+Inf"} 1
|
||||
counts_sum{method="/Users"} 2
|
||||
counts_count{method="/Users"} 1
|
||||
counts_bucket{method="/Users",le="2"} 2
|
||||
counts_bucket{method="/Users",le="3"} 2
|
||||
counts_bucket{method="/Users",le="+Inf"} 2
|
||||
counts_sum{method="/Users"} 3.1
|
||||
counts_count{method="/Users"} 2
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(hv.histogram, strings.NewReader(metadata+val))
|
||||
assert.Nil(t, err)
|
||||
|
||||
proc.Shutdown()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package metric
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/prometheus"
|
||||
|
||||
// A VectorOpts is a general configuration.
|
||||
type VectorOpts struct {
|
||||
Namespace string
|
||||
@@ -8,3 +10,11 @@ type VectorOpts struct {
|
||||
Help string
|
||||
Labels []string
|
||||
}
|
||||
|
||||
func update(fn func()) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
62
core/metric/summary.go
Normal file
62
core/metric/summary.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
prom "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
type (
|
||||
// A SummaryVecOpts is a summary vector options
|
||||
SummaryVecOpts struct {
|
||||
VecOpt VectorOpts
|
||||
Objectives map[float64]float64
|
||||
}
|
||||
|
||||
// A SummaryVec interface represents a summary vector.
|
||||
SummaryVec interface {
|
||||
// Observe adds observation v to labels.
|
||||
Observe(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
promSummaryVec struct {
|
||||
summary *prom.SummaryVec
|
||||
}
|
||||
)
|
||||
|
||||
// NewSummaryVec return a SummaryVec
|
||||
func NewSummaryVec(cfg *SummaryVecOpts) SummaryVec {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
vec := prom.NewSummaryVec(
|
||||
prom.SummaryOpts{
|
||||
Namespace: cfg.VecOpt.Namespace,
|
||||
Subsystem: cfg.VecOpt.Subsystem,
|
||||
Name: cfg.VecOpt.Name,
|
||||
Help: cfg.VecOpt.Help,
|
||||
Objectives: cfg.Objectives,
|
||||
},
|
||||
cfg.VecOpt.Labels,
|
||||
)
|
||||
prom.MustRegister(vec)
|
||||
sv := &promSummaryVec{
|
||||
summary: vec,
|
||||
}
|
||||
proc.AddShutdownListener(func() {
|
||||
sv.close()
|
||||
})
|
||||
|
||||
return sv
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) Observe(v float64, labels ...string) {
|
||||
update(func() {
|
||||
sv.summary.WithLabelValues(labels...).Observe(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) close() bool {
|
||||
return prom.Unregister(sv.summary)
|
||||
}
|
||||
68
core/metric/summary_test.go
Normal file
68
core/metric/summary_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
func TestNewSummaryVec(t *testing.T) {
|
||||
summaryVec := NewSummaryVec(&SummaryVecOpts{
|
||||
VecOpt: VectorOpts{
|
||||
Namespace: "http_server",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_quantiles",
|
||||
Help: "rpc client requests duration(ms) φ quantiles ",
|
||||
Labels: []string{"method"},
|
||||
},
|
||||
Objectives: map[float64]float64{
|
||||
0.5: 0.01,
|
||||
0.9: 0.01,
|
||||
},
|
||||
})
|
||||
defer summaryVec.close()
|
||||
summaryVecNil := NewSummaryVec(nil)
|
||||
assert.NotNil(t, summaryVec)
|
||||
assert.Nil(t, summaryVecNil)
|
||||
}
|
||||
|
||||
func TestSummaryObserve(t *testing.T) {
|
||||
startAgent()
|
||||
summaryVec := NewSummaryVec(&SummaryVecOpts{
|
||||
VecOpt: VectorOpts{
|
||||
Namespace: "http_server",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_quantiles",
|
||||
Help: "rpc client requests duration(ms) φ quantiles ",
|
||||
Labels: []string{"method"},
|
||||
},
|
||||
Objectives: map[float64]float64{
|
||||
0.3: 0.01,
|
||||
0.6: 0.01,
|
||||
1: 0.01,
|
||||
},
|
||||
})
|
||||
defer summaryVec.close()
|
||||
sv := summaryVec.(*promSummaryVec)
|
||||
sv.Observe(100, "GET")
|
||||
sv.Observe(200, "GET")
|
||||
sv.Observe(300, "GET")
|
||||
metadata := `
|
||||
# HELP http_server_requests_duration_quantiles rpc client requests duration(ms) φ quantiles
|
||||
# TYPE http_server_requests_duration_quantiles summary
|
||||
`
|
||||
val := `
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="0.3"} 100
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="0.6"} 200
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="1"} 300
|
||||
http_server_requests_duration_quantiles_sum{method="GET"} 600
|
||||
http_server_requests_duration_quantiles_count{method="GET"} 3
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(sv.summary, strings.NewReader(metadata+val))
|
||||
assert.Nil(t, err)
|
||||
proc.Shutdown()
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func FuzzMapReduce(f *testing.F) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
f.Add(int64(10), runtime.NumCPU())
|
||||
f.Fuzz(func(t *testing.T, n int64, workers int) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// If Fuzz stuck, we don't know why, because it only returns hung or unexpected,
|
||||
// so we need to simulate the fuzz test in test mode.
|
||||
func TestMapReduceRandom(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
const (
|
||||
times = 10000
|
||||
|
||||
@@ -36,6 +36,6 @@ type fakeCreator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (fc fakeCreator) Create(name string) (file *os.File, err error) {
|
||||
func (fc fakeCreator) Create(_ string) (file *os.File, err error) {
|
||||
return fc.file, fc.err
|
||||
}
|
||||
|
||||
@@ -52,10 +52,10 @@ func WrapUp() {
|
||||
wrapUpListeners.notifyListeners()
|
||||
}
|
||||
|
||||
func gracefulStop(signals chan os.Signal) {
|
||||
func gracefulStop(signals chan os.Signal, sig syscall.Signal) {
|
||||
signal.Stop(signals)
|
||||
|
||||
logx.Info("Got signal SIGTERM, shutting down...")
|
||||
logx.Infof("Got signal %d, shutting down...", sig)
|
||||
go wrapUpListeners.notifyListeners()
|
||||
|
||||
time.Sleep(wrapUpTime)
|
||||
@@ -63,7 +63,7 @@ func gracefulStop(signals chan os.Signal) {
|
||||
|
||||
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime)
|
||||
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
_ = syscall.Kill(syscall.Getpid(), sig)
|
||||
}
|
||||
|
||||
type listenerManager struct {
|
||||
@@ -96,4 +96,6 @@ func (lm *listenerManager) notifyListeners() {
|
||||
group.RunSafe(listener)
|
||||
}
|
||||
group.Wait()
|
||||
|
||||
lm.listeners = nil
|
||||
}
|
||||
|
||||
@@ -28,3 +28,33 @@ func TestShutdown(t *testing.T) {
|
||||
called()
|
||||
assert.Equal(t, 3, val)
|
||||
}
|
||||
|
||||
func TestNotifyMoreThanOnce(t *testing.T) {
|
||||
ch := make(chan struct{}, 1)
|
||||
|
||||
go func() {
|
||||
var val int
|
||||
called := AddWrapUpListener(func() {
|
||||
val++
|
||||
})
|
||||
WrapUp()
|
||||
WrapUp()
|
||||
called()
|
||||
assert.Equal(t, 1, val)
|
||||
|
||||
called = AddShutdownListener(func() {
|
||||
val += 2
|
||||
})
|
||||
Shutdown()
|
||||
Shutdown()
|
||||
called()
|
||||
assert.Equal(t, 3, val)
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout, check error logs")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,23 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
const timeFormat = "0102150405"
|
||||
const (
|
||||
profileDuration = time.Minute
|
||||
timeFormat = "0102150405"
|
||||
)
|
||||
|
||||
var done = make(chan struct{})
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
var profiler Stopper
|
||||
|
||||
// https://golang.org/pkg/os/signal/#Notify
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM)
|
||||
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
for {
|
||||
v := <-signals
|
||||
@@ -28,21 +30,14 @@ func init() {
|
||||
case syscall.SIGUSR1:
|
||||
dumpGoroutines(fileCreator{})
|
||||
case syscall.SIGUSR2:
|
||||
if profiler == nil {
|
||||
profiler = StartProfile()
|
||||
} else {
|
||||
profiler.Stop()
|
||||
profiler = nil
|
||||
}
|
||||
profiler := StartProfile()
|
||||
time.AfterFunc(profileDuration, profiler.Stop)
|
||||
case syscall.SIGTERM:
|
||||
select {
|
||||
case <-done:
|
||||
// already closed
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
|
||||
gracefulStop(signals)
|
||||
stopOnSignal()
|
||||
gracefulStop(signals, syscall.SIGTERM)
|
||||
case syscall.SIGINT:
|
||||
stopOnSignal()
|
||||
gracefulStop(signals, syscall.SIGINT)
|
||||
default:
|
||||
logx.Error("Got unregistered signal:", v)
|
||||
}
|
||||
@@ -54,3 +49,12 @@ func init() {
|
||||
func Done() <-chan struct{} {
|
||||
return done
|
||||
}
|
||||
|
||||
func stopOnSignal() {
|
||||
select {
|
||||
case <-done:
|
||||
// already closed
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (q *Queue) AddListener(listener Listener) {
|
||||
q.listeners = append(q.listeners, listener)
|
||||
}
|
||||
|
||||
// Broadcast broadcasts message to all event channels.
|
||||
// Broadcast broadcasts the message to all event channels.
|
||||
func (q *Queue) Broadcast(message any) {
|
||||
go func() {
|
||||
q.eventLock.Lock()
|
||||
@@ -202,7 +202,7 @@ func (q *Queue) produce() {
|
||||
}
|
||||
|
||||
func (q *Queue) produceOne(producer Producer) (string, bool) {
|
||||
// avoid panic quit the producer, just log it and continue
|
||||
// avoid panic quit the producer, log it and continue
|
||||
defer rescue.Recover()
|
||||
|
||||
return producer.Produce()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user