Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
88c7439c01 |
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: For reporting bugs in go-micro
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
|
|
||||||
1. What are you trying to do?
|
|
||||||
2. What did you expect to happen?
|
|
||||||
3. What happens instead?
|
|
||||||
|
|
||||||
**How to reproduce the bug:**
|
|
||||||
|
|
||||||
If possible, please include a minimal code snippet here.
|
|
||||||
|
|
||||||
**Environment:**
|
|
||||||
Go Version: please paste `go version` output here
|
|
||||||
```
|
|
||||||
please paste `go env` output here
|
|
||||||
```
|
|
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request / Enhancement
|
|
||||||
about: If you have a need not served by go-micro
|
|
||||||
title: "[FEATURE]"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
name: Question
|
|
||||||
about: Ask a question about go-micro
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Before asking, please check if your question has already been answered:
|
|
||||||
|
|
||||||
1. Check the documentation - https://micro.mu/docs/
|
|
||||||
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
|
||||||
3. Search existing issues
|
|
@@ -1,9 +0,0 @@
|
|||||||
## Pull Request template
|
|
||||||
Please, go through these steps before clicking submit on this PR.
|
|
||||||
|
|
||||||
1. Give a descriptive title to your PR.
|
|
||||||
2. Provide a description of your changes.
|
|
||||||
3. Make sure you have some relevant tests.
|
|
||||||
4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).
|
|
||||||
|
|
||||||
**PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**
|
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: For reporting bugs in micro
|
about: For reporting bugs in go-micro
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -16,3 +16,9 @@ assignees: ''
|
|||||||
**How to reproduce the bug:**
|
**How to reproduce the bug:**
|
||||||
|
|
||||||
If possible, please include a minimal code snippet here.
|
If possible, please include a minimal code snippet here.
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
Go Version: please paste `go version` output here
|
||||||
|
```
|
||||||
|
please paste `go env` output here
|
||||||
|
```
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Feature request / Enhancement
|
name: Feature request / Enhancement
|
||||||
about: If you have a need not served by micro
|
about: If you have a need not served by go-micro
|
||||||
title: "[FEATURE]"
|
title: "[FEATURE]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,8 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: Question
|
name: Question
|
||||||
about: Ask a question about micro
|
about: Ask a question about go-micro
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Before asking, please check if your question has already been answered:
|
||||||
|
|
||||||
|
1. Check the documentation - https://micro.mu/docs/
|
||||||
|
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
||||||
|
3. Search existing issues
|
||||||
|
28
.github/autoapprove.yml
vendored
28
.github/autoapprove.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: "autoapprove"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [assigned, opened, synchronize, reopened]
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["prbuild"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
autoapprove:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: approve
|
|
||||||
run: [ "curl -o tea https://dl.gitea.com/tea/main/tea-main-linux-amd64",
|
|
||||||
"chmod +x ./tea",
|
|
||||||
"./tea login add --name unistack --token ${{ secrets.GITHUB_TOKEN }} --url https://git.unistack.org",
|
|
||||||
"./tea pr --repo ${{ github.event.repository.name }}"
|
|
||||||
]
|
|
||||||
if: github.actor == 'vtolstov'
|
|
||||||
id: approve
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
53
.github/workflows/job_coverage.yml
vendored
53
.github/workflows/job_coverage.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: coverage
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
pull_request:
|
|
||||||
branches: [ main, v3, v4 ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
if: github.server_url != 'https://github.com'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
|
|
||||||
- name: test coverage
|
|
||||||
run: |
|
|
||||||
go test -v -cover ./... -covermode=count -coverprofile coverage.out -coverpkg ./...
|
|
||||||
go tool cover -func coverage.out -o coverage.out
|
|
||||||
|
|
||||||
- name: coverage badge
|
|
||||||
uses: tj-actions/coverage-badge-go@v2
|
|
||||||
with:
|
|
||||||
green: 80
|
|
||||||
filename: coverage.out
|
|
||||||
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
name: autocommit
|
|
||||||
with:
|
|
||||||
commit_message: Apply Code Coverage Badge
|
|
||||||
skip_fetch: false
|
|
||||||
skip_checkout: false
|
|
||||||
file_pattern: ./README.md
|
|
||||||
|
|
||||||
- name: push
|
|
||||||
if: steps.auto-commit-action.outputs.changes_detected == 'true'
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ github.token }}
|
|
||||||
branch: ${{ github.ref }}
|
|
29
.github/workflows/job_lint.yml
vendored
29
.github/workflows/job_lint.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup deps
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run lint
|
|
||||||
uses: golangci/golangci-lint-action@v6
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
94
.github/workflows/job_sync.yml
vendored
94
.github/workflows/job_sync.yml
vendored
@@ -1,94 +0,0 @@
|
|||||||
name: sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '*/5 * * * *'
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
if: github.server_url != 'https://github.com'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: init
|
|
||||||
run: |
|
|
||||||
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
|
|
||||||
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
|
|
||||||
|
|
||||||
- name: check master
|
|
||||||
id: check_master
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync master
|
|
||||||
if: steps.check_master.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream master
|
|
||||||
git push upstream master --progress
|
|
||||||
git push origin master --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
||||||
|
|
||||||
- name: check v3
|
|
||||||
id: check_v3
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync v3
|
|
||||||
if: steps.check_v3.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream v3
|
|
||||||
git push upstream v3 --progress
|
|
||||||
git push origin v3 --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
||||||
|
|
||||||
- name: check v4
|
|
||||||
id: check_v4
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync v4
|
|
||||||
if: steps.check_v4.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream v4
|
|
||||||
git push upstream v4 --progress
|
|
||||||
git push origin v4 --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
31
.github/workflows/job_test.yml
vendored
31
.github/workflows/job_test.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
push:
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup deps
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run test
|
|
||||||
env:
|
|
||||||
INTEGRATION_TESTS: yes
|
|
||||||
run: go test -mod readonly -v ./...
|
|
50
.github/workflows/job_tests.yml
vendored
50
.github/workflows/job_tests.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
push:
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: checkout tests
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
filter: 'blob:none'
|
|
||||||
repository: unistack-org/micro-tests
|
|
||||||
path: micro-tests
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup go work
|
|
||||||
env:
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: |
|
|
||||||
go work init
|
|
||||||
go work use .
|
|
||||||
go work use micro-tests
|
|
||||||
- name: setup deps
|
|
||||||
env:
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run tests
|
|
||||||
env:
|
|
||||||
INTEGRATION_TESTS: yes
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: |
|
|
||||||
cd micro-tests
|
|
||||||
go test -mod readonly -v ./... || true
|
|
@@ -37,4 +37,11 @@ jobs:
|
|||||||
uses: golangci/golangci-lint-action@v3.4.0
|
uses: golangci/golangci-lint-action@v3.4.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
version: v1.30
|
version: v1.30
|
||||||
|
# Optional: working directory, useful for monorepos
|
||||||
|
# working-directory: somedir
|
||||||
|
# Optional: golangci-lint command line arguments.
|
||||||
|
# args: --issues-exit-code=0
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
# only-new-issues: true
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,8 +1,6 @@
|
|||||||
# Develop tools
|
# Develop tools
|
||||||
/.vscode/
|
/.vscode/
|
||||||
/.idea/
|
/.idea/
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
@@ -15,7 +13,6 @@
|
|||||||
_obj
|
_obj
|
||||||
_test
|
_test
|
||||||
_build
|
_build
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
# Architecture specific extensions/prefixes
|
||||||
*.[568vq]
|
*.[568vq]
|
||||||
|
@@ -1,5 +1,44 @@
|
|||||||
run:
|
run:
|
||||||
concurrency: 8
|
concurrency: 4
|
||||||
timeout: 5m
|
deadline: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
enable:
|
||||||
|
- fieldalignment
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- govet
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- bodyclose
|
||||||
|
- gci
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gosimple
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- revive
|
||||||
|
- gosec
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- prealloc
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
disable-all: false
|
||||||
|
27
README.md
27
README.md
@@ -1,9 +1,4 @@
|
|||||||
# Micro
|
# Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [](https://goreportcard.com/report/go.unistack.org/micro/v3) [](https://codecov.io/gh/unistack-org/micro)
|
||||||

|
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
|
||||||
[](https://pkg.go.dev/go.unistack.org/micro/v4?tab=overview)
|
|
||||||
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av4+event%3Apush)
|
|
||||||
[](https://goreportcard.com/report/go.unistack.org/micro/v4)
|
|
||||||
|
|
||||||
Micro is a standard library for microservices.
|
Micro is a standard library for microservices.
|
||||||
|
|
||||||
@@ -15,20 +10,30 @@ Micro provides the core requirements for distributed systems development includi
|
|||||||
|
|
||||||
Micro abstracts away the details of distributed systems. Here are the main features.
|
Micro abstracts away the details of distributed systems. Here are the main features.
|
||||||
|
|
||||||
|
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
|
||||||
|
zero trust networking by providing every service an identity and certificates. This additionally includes rule
|
||||||
|
based access control.
|
||||||
|
|
||||||
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
||||||
level config from any source such as env vars, cmdline, file, consul, vault... You can merge the sources and even define fallbacks.
|
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
|
||||||
|
|
||||||
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
||||||
s3. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
CockroachDB by default. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
||||||
|
|
||||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||||
development. When service A needs to speak to service B it needs the location of that service.
|
development. When service A needs to speak to service B it needs the location of that service.
|
||||||
|
|
||||||
|
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
||||||
|
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
||||||
|
across the services and retry a different node if there's a problem.
|
||||||
|
|
||||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||||
and server handle this by default.
|
and server handle this by default.
|
||||||
|
|
||||||
- **Async Messaging** - Pub/Sub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
- **Transport** - gRPC or http based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
|
||||||
|
|
||||||
|
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||||
Event notifications are a core pattern in micro service development.
|
Event notifications are a core pattern in micro service development.
|
||||||
|
|
||||||
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
||||||
@@ -37,6 +42,10 @@ leadership are built in as a Sync interface. When using an eventually consistent
|
|||||||
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
||||||
are pluggable and allows Micro to be runtime agnostic.
|
are pluggable and allows Micro to be runtime agnostic.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To be created.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Micro is Apache 2.0 licensed.
|
Micro is Apache 2.0 licensed.
|
||||||
|
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.7.x | :white_check_mark: |
|
||||||
|
| < 3.7.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you find any issue, please create github issue in this repo
|
182
api/api.go
Normal file
182
api/api.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package api // import "go.unistack.org/micro/v3/api"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
"go.unistack.org/micro/v3/register"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint: revive
|
||||||
|
// Api interface
|
||||||
|
type Api interface {
|
||||||
|
// Initialise options
|
||||||
|
Init(...Option) error
|
||||||
|
// Get the options
|
||||||
|
Options() Options
|
||||||
|
// Register a http handler
|
||||||
|
Register(*Endpoint) error
|
||||||
|
// Register a route
|
||||||
|
Deregister(*Endpoint) error
|
||||||
|
// Implementation of api
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options holds the options
|
||||||
|
type Options struct{}
|
||||||
|
|
||||||
|
// Option func signature
|
||||||
|
type Option func(*Options) error
|
||||||
|
|
||||||
|
// Endpoint is a mapping between an RPC method and HTTP endpoint
|
||||||
|
type Endpoint struct {
|
||||||
|
// Name Greeter.Hello
|
||||||
|
Name string
|
||||||
|
// Desciption for endpoint
|
||||||
|
Description string
|
||||||
|
// Handler e.g rpc, proxy
|
||||||
|
Handler string
|
||||||
|
// Body destination
|
||||||
|
// "*" or "" - top level message value
|
||||||
|
// "string" - inner message value
|
||||||
|
Body string
|
||||||
|
// Host e.g example.com
|
||||||
|
Host []string
|
||||||
|
// Method e.g GET, POST
|
||||||
|
Method []string
|
||||||
|
// Path e.g /greeter. Expect POSIX regex
|
||||||
|
Path []string
|
||||||
|
// Stream flag
|
||||||
|
Stream bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service represents an API service
|
||||||
|
type Service struct {
|
||||||
|
// Name of service
|
||||||
|
Name string
|
||||||
|
// Endpoint for this service
|
||||||
|
Endpoint *Endpoint
|
||||||
|
// Services that provides service
|
||||||
|
Services []*register.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes an endpoint to endpoint metadata
|
||||||
|
func Encode(e *Endpoint) map[string]string {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endpoint map
|
||||||
|
ep := make(map[string]string)
|
||||||
|
|
||||||
|
// set vals only if they exist
|
||||||
|
set := func(k, v string) {
|
||||||
|
if len(v) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ep[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
set("endpoint", e.Name)
|
||||||
|
set("description", e.Description)
|
||||||
|
set("handler", e.Handler)
|
||||||
|
set("method", strings.Join(e.Method, ","))
|
||||||
|
set("path", strings.Join(e.Path, ","))
|
||||||
|
set("host", strings.Join(e.Host, ","))
|
||||||
|
set("body", e.Body)
|
||||||
|
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes endpoint metadata into an endpoint
|
||||||
|
func Decode(e metadata.Metadata) *Endpoint {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ep := &Endpoint{}
|
||||||
|
ep.Name, _ = e.Get("endpoint")
|
||||||
|
ep.Description, _ = e.Get("description")
|
||||||
|
epmethod, _ := e.Get("method")
|
||||||
|
ep.Method = []string{epmethod}
|
||||||
|
eppath, _ := e.Get("path")
|
||||||
|
ep.Path = []string{eppath}
|
||||||
|
ephost, _ := e.Get("host")
|
||||||
|
ep.Host = []string{ephost}
|
||||||
|
ep.Handler, _ = e.Get("handler")
|
||||||
|
ep.Body, _ = e.Get("body")
|
||||||
|
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates an endpoint to guarantee it won't blow up when being served
|
||||||
|
func Validate(e *Endpoint) error {
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("endpoint is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Name) == 0 {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range e.Path {
|
||||||
|
ps := p[0]
|
||||||
|
pe := p[len(p)-1]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ps == '^' && pe == '$':
|
||||||
|
if _, err := regexp.CompilePOSIX(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case ps == '^' && pe != '$':
|
||||||
|
return errors.New("invalid path")
|
||||||
|
case ps != '^' && pe == '$':
|
||||||
|
return errors.New("invalid path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Handler) == 0 {
|
||||||
|
return errors.New("invalid handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Design ideas
|
||||||
|
|
||||||
|
// Gateway is an api gateway interface
|
||||||
|
type Gateway interface {
|
||||||
|
// Register a http handler
|
||||||
|
Handle(pattern string, http.Handler)
|
||||||
|
// Register a route
|
||||||
|
RegisterRoute(r Route)
|
||||||
|
// Init initialises the command line.
|
||||||
|
// It also parses further options.
|
||||||
|
Init(...Option) error
|
||||||
|
// Run the gateway
|
||||||
|
Run() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGateway returns a new api gateway
|
||||||
|
func NewGateway() Gateway {
|
||||||
|
return newGateway()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
|
||||||
|
// &api.Endpoint{
|
||||||
|
// Name: "Greeter.Hello",
|
||||||
|
// Path: []string{"/greeter"},
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
func WithEndpoint(e *Endpoint) server.HandlerOption {
|
||||||
|
return server.EndpointMetadata(e.Name, Encode(e))
|
||||||
|
}
|
245
api/api_test.go
Normal file
245
api/api_test.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
md := metadata.New(0)
|
||||||
|
md.Set("host", "localhost", "method", "GET", "path", "/")
|
||||||
|
ep := Decode(md)
|
||||||
|
if md == nil {
|
||||||
|
t.Fatalf("failed to decode md %#+v", md)
|
||||||
|
} else if len(ep.Host) != 1 || len(ep.Method) != 1 || len(ep.Path) != 1 {
|
||||||
|
t.Fatalf("ep invalid after decode %#+v", ep)
|
||||||
|
}
|
||||||
|
if ep.Host[0] != "localhost" || ep.Method[0] != "GET" || ep.Path[0] != "/" {
|
||||||
|
t.Fatalf("ep invalid after decode %#+v", ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
testData := []*Endpoint{
|
||||||
|
nil,
|
||||||
|
{
|
||||||
|
Name: "Foo.Bar",
|
||||||
|
Description: "A test endpoint",
|
||||||
|
Handler: "meta",
|
||||||
|
Host: []string{"foo.com"},
|
||||||
|
Method: []string{"GET"},
|
||||||
|
Path: []string{"/test"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
compare := func(expect, got []string) bool {
|
||||||
|
// no data to compare, return true
|
||||||
|
if len(expect) == 0 && len(got) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// no data expected but got some return false
|
||||||
|
if len(expect) == 0 && len(got) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare expected with what we got
|
||||||
|
for _, e := range expect {
|
||||||
|
var seen bool
|
||||||
|
for _, g := range got {
|
||||||
|
if e == g {
|
||||||
|
seen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done, return true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range testData {
|
||||||
|
// encode
|
||||||
|
e := Encode(d)
|
||||||
|
// decode
|
||||||
|
de := Decode(e)
|
||||||
|
|
||||||
|
// nil endpoint returns nil
|
||||||
|
if d == nil {
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("expected nil got %v", e)
|
||||||
|
}
|
||||||
|
if de != nil {
|
||||||
|
t.Fatalf("expected nil got %v", de)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check encoded map
|
||||||
|
name := e["endpoint"]
|
||||||
|
desc := e["description"]
|
||||||
|
method := strings.Split(e["method"], ",")
|
||||||
|
path := strings.Split(e["path"], ",")
|
||||||
|
host := strings.Split(e["host"], ",")
|
||||||
|
handler := e["handler"]
|
||||||
|
|
||||||
|
if name != d.Name {
|
||||||
|
t.Fatalf("expected %v got %v", d.Name, name)
|
||||||
|
}
|
||||||
|
if desc != d.Description {
|
||||||
|
t.Fatalf("expected %v got %v", d.Description, desc)
|
||||||
|
}
|
||||||
|
if handler != d.Handler {
|
||||||
|
t.Fatalf("expected %v got %v", d.Handler, handler)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Method, method); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Method, method)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Path, path); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Path, path)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Host, host); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Host, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.Name != d.Name {
|
||||||
|
t.Fatalf("expected %v got %v", d.Name, de.Name)
|
||||||
|
}
|
||||||
|
if de.Description != d.Description {
|
||||||
|
t.Fatalf("expected %v got %v", d.Description, de.Description)
|
||||||
|
}
|
||||||
|
if de.Handler != d.Handler {
|
||||||
|
t.Fatalf("expected %v got %v", d.Handler, de.Handler)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Method, de.Method); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Method, de.Method)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Path, de.Path); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Path, de.Path)
|
||||||
|
}
|
||||||
|
if ok := compare(d.Host, de.Host); !ok {
|
||||||
|
t.Fatalf("expected %v got %v", d.Host, de.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
epPcre := &Endpoint{
|
||||||
|
Name: "Foo.Bar",
|
||||||
|
Description: "A test endpoint",
|
||||||
|
Handler: "meta",
|
||||||
|
Host: []string{"foo.com"},
|
||||||
|
Method: []string{"GET"},
|
||||||
|
Path: []string{"^/test/?$"},
|
||||||
|
}
|
||||||
|
if err := Validate(epPcre); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
epGpath := &Endpoint{
|
||||||
|
Name: "Foo.Bar",
|
||||||
|
Description: "A test endpoint",
|
||||||
|
Handler: "meta",
|
||||||
|
Host: []string{"foo.com"},
|
||||||
|
Method: []string{"GET"},
|
||||||
|
Path: []string{"/test/{id}"},
|
||||||
|
}
|
||||||
|
if err := Validate(epGpath); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
epPcreInvalid := &Endpoint{
|
||||||
|
Name: "Foo.Bar",
|
||||||
|
Description: "A test endpoint",
|
||||||
|
Handler: "meta",
|
||||||
|
Host: []string{"foo.com"},
|
||||||
|
Method: []string{"GET"},
|
||||||
|
Path: []string{"/test/?$"},
|
||||||
|
}
|
||||||
|
if err := Validate(epPcreInvalid); err == nil {
|
||||||
|
t.Fatalf("invalid pcre %v", epPcreInvalid.Path[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithEndpoint(t *testing.T) {
|
||||||
|
ep := &Endpoint{
|
||||||
|
Name: "Foo.Bar",
|
||||||
|
Description: "A test endpoint",
|
||||||
|
Handler: "meta",
|
||||||
|
Host: []string{"foo.com"},
|
||||||
|
Method: []string{"GET"},
|
||||||
|
Path: []string{"/test/{id}"},
|
||||||
|
}
|
||||||
|
o := WithEndpoint(ep)
|
||||||
|
opts := server.NewHandlerOptions(o)
|
||||||
|
if opts.Metadata == nil {
|
||||||
|
t.Fatalf("WithEndpoint not works %#+v", opts)
|
||||||
|
}
|
||||||
|
md, ok := opts.Metadata[ep.Name]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("WithEndpoint not works %#+v", opts)
|
||||||
|
}
|
||||||
|
if v, ok := md.Get("Endpoint"); !ok || v != "Foo.Bar" {
|
||||||
|
t.Fatalf("WithEndpoint not works %#+v", md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNilErr(t *testing.T) {
|
||||||
|
var ep *Endpoint
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateMissingNameErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{}
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateMissingHandlerErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "test"}
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRegexpStartErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "test", Handler: "test"}
|
||||||
|
ep.Path = []string{"^/"}
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRegexpEndErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "test", Handler: "test", Path: []string{""}}
|
||||||
|
ep.Path[0] = "/$"
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRegexpNonErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "test", Handler: "test", Path: []string{""}}
|
||||||
|
ep.Path[0] = "^/(.*)$"
|
||||||
|
if err := Validate(ep); err != nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRegexpErr(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "test", Handler: "test", Path: []string{""}}
|
||||||
|
ep.Path[0] = "^/(.$"
|
||||||
|
if err := Validate(ep); err == nil {
|
||||||
|
t.Fatalf("Validate not works")
|
||||||
|
}
|
||||||
|
}
|
121
broker/broker.go
121
broker/broker.go
@@ -1,29 +1,21 @@
|
|||||||
// Package broker is an interface used for asynchronous messaging
|
// Package broker is an interface used for asynchronous messaging
|
||||||
package broker
|
package broker // import "go.unistack.org/micro/v3/broker"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBroker default memory broker
|
// DefaultBroker default memory broker
|
||||||
var DefaultBroker Broker = NewBroker()
|
var DefaultBroker = NewBroker()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotConnected returns when broker used but not connected yet
|
// ErrNotConnected returns when broker used but not connected yet
|
||||||
ErrNotConnected = errors.New("broker not connected")
|
ErrNotConnected = errors.New("broker not connected")
|
||||||
// ErrDisconnected returns when broker disconnected
|
// ErrDisconnected returns when broker disconnected
|
||||||
ErrDisconnected = errors.New("broker disconnected")
|
ErrDisconnected = errors.New("broker disconnected")
|
||||||
// ErrInvalidMessage returns when invalid Message passed
|
|
||||||
ErrInvalidMessage = errors.New("invalid message")
|
|
||||||
// ErrInvalidHandler returns when subscriber passed to Subscribe
|
|
||||||
ErrInvalidHandler = errors.New("invalid handler, ony func(Message) error and func([]Message) error supported")
|
|
||||||
// DefaultGracefulTimeout
|
|
||||||
DefaultGracefulTimeout = 5 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Broker is an interface used for asynchronous messaging.
|
// Broker is an interface used for asynchronous messaging.
|
||||||
@@ -40,44 +32,93 @@ type Broker interface {
|
|||||||
Connect(ctx context.Context) error
|
Connect(ctx context.Context) error
|
||||||
// Disconnect disconnect from broker
|
// Disconnect disconnect from broker
|
||||||
Disconnect(ctx context.Context) error
|
Disconnect(ctx context.Context) error
|
||||||
// NewMessage create new broker message to publish.
|
|
||||||
NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error)
|
|
||||||
// Publish message to broker topic
|
// Publish message to broker topic
|
||||||
Publish(ctx context.Context, topic string, messages ...Message) error
|
Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
|
||||||
// Subscribe subscribes to topic message via handler
|
// Subscribe subscribes to topic message via handler
|
||||||
Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
|
Subscribe(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
|
// BatchPublish messages to broker with multiple topics
|
||||||
|
BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error
|
||||||
|
// BatchSubscribe subscribes to topic messages via handler
|
||||||
|
BatchSubscribe(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
// String type of broker
|
// String type of broker
|
||||||
String() string
|
String() string
|
||||||
// Live returns broker liveness
|
|
||||||
Live() bool
|
|
||||||
// Ready returns broker readiness
|
|
||||||
Ready() bool
|
|
||||||
// Health returns broker health
|
|
||||||
Health() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
// Handler is used to process messages via a subscription of a topic.
|
||||||
FuncPublish func(ctx context.Context, topic string, messages ...Message) error
|
type Handler func(Event) error
|
||||||
HookPublish func(next FuncPublish) FuncPublish
|
|
||||||
FuncSubscribe func(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
|
|
||||||
HookSubscribe func(next FuncSubscribe) FuncSubscribe
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message is given to a subscription handler for processing
|
// Events contains multiple events
|
||||||
type Message interface {
|
type Events []Event
|
||||||
// Context for the message.
|
|
||||||
Context() context.Context
|
// Ack try to ack all events and return
|
||||||
// Topic returns message destination topic.
|
func (evs Events) Ack() error {
|
||||||
|
var err error
|
||||||
|
for _, ev := range evs {
|
||||||
|
if err = ev.Ack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError sets error on event
|
||||||
|
func (evs Events) SetError(err error) {
|
||||||
|
for _, ev := range evs {
|
||||||
|
ev.SetError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchHandler is used to process messages in batches via a subscription of a topic.
|
||||||
|
type BatchHandler func(Events) error
|
||||||
|
|
||||||
|
// Event is given to a subscription handler for processing
|
||||||
|
type Event interface {
|
||||||
|
// Topic returns event topic
|
||||||
Topic() string
|
Topic() string
|
||||||
// Header returns message headers.
|
// Message returns broker message
|
||||||
Header() metadata.Metadata
|
Message() *Message
|
||||||
// Body returns broker message []byte slice
|
// Ack acknowledge message
|
||||||
Body() []byte
|
|
||||||
// Unmarshal try to decode message body to dst.
|
|
||||||
// This is helper method that uses codec.Unmarshal.
|
|
||||||
Unmarshal(dst interface{}, opts ...codec.Option) error
|
|
||||||
// Ack acknowledge message if supported.
|
|
||||||
Ack() error
|
Ack() error
|
||||||
|
// Error returns message error (like decoding errors or some other)
|
||||||
|
Error() error
|
||||||
|
// SetError set event processing error
|
||||||
|
SetError(err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawMessage is a raw encoded JSON value.
|
||||||
|
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
// MarshalJSON returns m as the JSON encoding of m.
|
||||||
|
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return *m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON sets *m to a copy of data.
|
||||||
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message is used to transfer data
|
||||||
|
type Message struct {
|
||||||
|
// Header contains message metadata
|
||||||
|
Header metadata.Metadata
|
||||||
|
// Body contains message body
|
||||||
|
Body RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessage create broker message with topic filled
|
||||||
|
func NewMessage(topic string) *Message {
|
||||||
|
m := &Message{Header: metadata.New(2)}
|
||||||
|
m.Header.Set(metadata.HeaderTopic, topic)
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriber is a convenience return type for the Subscribe method
|
// Subscriber is a convenience return type for the Subscribe method
|
||||||
|
@@ -15,15 +15,6 @@ func FromContext(ctx context.Context) (Broker, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustContext returns broker from passed context
|
|
||||||
func MustContext(ctx context.Context) Broker {
|
|
||||||
b, ok := FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
panic("missing broker")
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContext savess broker in context
|
// NewContext savess broker in context
|
||||||
func NewContext(ctx context.Context, s Broker) context.Context {
|
func NewContext(ctx context.Context, s Broker) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -42,16 +33,6 @@ func SetSubscribeOption(k, v interface{}) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMessageOption returns a function to setup a context with given value
|
|
||||||
func SetMessageOption(k, v interface{}) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOption returns a function to setup a context with given value
|
// SetOption returns a function to setup a context with given value
|
||||||
func SetOption(k, v interface{}) Option {
|
func SetOption(k, v interface{}) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -61,3 +42,13 @@ func SetOption(k, v interface{}) Option {
|
|||||||
o.Context = context.WithValue(o.Context, k, v)
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPublishOption returns a function to setup a context with given value
|
||||||
|
func SetPublishOption(k, v interface{}) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -49,6 +49,17 @@ func TestSetSubscribeOption(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetPublishOption(t *testing.T) {
|
||||||
|
type key struct{}
|
||||||
|
o := SetPublishOption(key{}, "test")
|
||||||
|
opts := &PublishOptions{}
|
||||||
|
o(opts)
|
||||||
|
|
||||||
|
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
|
||||||
|
t.Fatal("SetPublishOption not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetOption(t *testing.T) {
|
func TestSetOption(t *testing.T) {
|
||||||
type key struct{}
|
type key struct{}
|
||||||
o := SetOption(key{}, "test")
|
o := SetOption(key{}, "test")
|
||||||
|
342
broker/memory.go
Normal file
342
broker/memory.go
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/logger"
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
maddr "go.unistack.org/micro/v3/util/addr"
|
||||||
|
"go.unistack.org/micro/v3/util/id"
|
||||||
|
mnet "go.unistack.org/micro/v3/util/net"
|
||||||
|
"go.unistack.org/micro/v3/util/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryBroker struct {
|
||||||
|
subscribers map[string][]*memorySubscriber
|
||||||
|
addr string
|
||||||
|
opts Options
|
||||||
|
sync.RWMutex
|
||||||
|
connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryEvent struct {
|
||||||
|
err error
|
||||||
|
message interface{}
|
||||||
|
topic string
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
type memorySubscriber struct {
|
||||||
|
ctx context.Context
|
||||||
|
exit chan bool
|
||||||
|
handler Handler
|
||||||
|
batchhandler BatchHandler
|
||||||
|
id string
|
||||||
|
topic string
|
||||||
|
opts SubscribeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Options() Options {
|
||||||
|
return m.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Address() string {
|
||||||
|
return m.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Connect(ctx context.Context) error {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
if m.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use 127.0.0.1 to avoid scan of all network interfaces
|
||||||
|
addr, err := maddr.Extract("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var rng rand.Rand
|
||||||
|
i := rng.Intn(20000)
|
||||||
|
// set addr with port
|
||||||
|
addr = mnet.HostPort(addr, 10000+i)
|
||||||
|
|
||||||
|
m.addr = addr
|
||||||
|
m.connected = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Disconnect(ctx context.Context) error {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
if !m.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.connected = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Init(opts ...Option) error {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&m.opts)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
|
||||||
|
msg.Header.Set(metadata.HeaderTopic, topic)
|
||||||
|
return m.publish(ctx, []*Message{msg}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
|
return m.publish(ctx, msgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
|
m.RLock()
|
||||||
|
if !m.connected {
|
||||||
|
m.RUnlock()
|
||||||
|
return ErrNotConnected
|
||||||
|
}
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
options := NewPublishOptions(opts...)
|
||||||
|
|
||||||
|
msgTopicMap := make(map[string]Events)
|
||||||
|
for _, v := range msgs {
|
||||||
|
p := &memoryEvent{opts: m.opts}
|
||||||
|
|
||||||
|
if m.opts.Codec == nil || options.BodyOnly {
|
||||||
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
|
p.message = v.Body
|
||||||
|
} else {
|
||||||
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
|
p.message, err = m.opts.Codec.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p)
|
||||||
|
}
|
||||||
|
|
||||||
|
beh := m.opts.BatchErrorHandler
|
||||||
|
eh := m.opts.ErrorHandler
|
||||||
|
|
||||||
|
for t, ms := range msgTopicMap {
|
||||||
|
m.RLock()
|
||||||
|
subs, ok := m.subscribers[t]
|
||||||
|
m.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range subs {
|
||||||
|
if sub.opts.BatchErrorHandler != nil {
|
||||||
|
beh = sub.opts.BatchErrorHandler
|
||||||
|
}
|
||||||
|
if sub.opts.ErrorHandler != nil {
|
||||||
|
eh = sub.opts.ErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// batch processing
|
||||||
|
case sub.batchhandler != nil:
|
||||||
|
if err = sub.batchhandler(ms); err != nil {
|
||||||
|
ms.SetError(err)
|
||||||
|
if beh != nil {
|
||||||
|
_ = beh(ms)
|
||||||
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
} else if sub.opts.AutoAck {
|
||||||
|
if err = ms.Ack(); err != nil {
|
||||||
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// single processing
|
||||||
|
case sub.handler != nil:
|
||||||
|
for _, p := range ms {
|
||||||
|
if err = sub.handler(p); err != nil {
|
||||||
|
p.SetError(err)
|
||||||
|
if eh != nil {
|
||||||
|
_ = eh(p)
|
||||||
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
} else if sub.opts.AutoAck {
|
||||||
|
if err = p.Ack(); err != nil {
|
||||||
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
m.RLock()
|
||||||
|
if !m.connected {
|
||||||
|
m.RUnlock()
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
sid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
|
sub := &memorySubscriber{
|
||||||
|
exit: make(chan bool, 1),
|
||||||
|
id: sid,
|
||||||
|
topic: topic,
|
||||||
|
batchhandler: handler,
|
||||||
|
opts: options,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sub.exit
|
||||||
|
m.Lock()
|
||||||
|
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
||||||
|
for _, sb := range m.subscribers[topic] {
|
||||||
|
if sb.id == sub.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSubscribers = append(newSubscribers, sb)
|
||||||
|
}
|
||||||
|
m.subscribers[topic] = newSubscribers
|
||||||
|
m.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
m.RLock()
|
||||||
|
if !m.connected {
|
||||||
|
m.RUnlock()
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
sid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
|
sub := &memorySubscriber{
|
||||||
|
exit: make(chan bool, 1),
|
||||||
|
id: sid,
|
||||||
|
topic: topic,
|
||||||
|
handler: handler,
|
||||||
|
opts: options,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sub.exit
|
||||||
|
m.Lock()
|
||||||
|
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
||||||
|
for _, sb := range m.subscribers[topic] {
|
||||||
|
if sb.id == sub.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSubscribers = append(newSubscribers, sb)
|
||||||
|
}
|
||||||
|
m.subscribers[topic] = newSubscribers
|
||||||
|
m.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) String() string {
|
||||||
|
return "memory"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Name() string {
|
||||||
|
return m.opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) Topic() string {
|
||||||
|
return m.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) Message() *Message {
|
||||||
|
switch v := m.message.(type) {
|
||||||
|
case *Message:
|
||||||
|
return v
|
||||||
|
case []byte:
|
||||||
|
msg := &Message{}
|
||||||
|
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
|
||||||
|
if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) Ack() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) Error() error {
|
||||||
|
return m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) SetError(err error) {
|
||||||
|
m.err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memorySubscriber) Options() SubscribeOptions {
|
||||||
|
return m.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memorySubscriber) Topic() string {
|
||||||
|
return m.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
|
||||||
|
m.exit <- true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBroker return new memory broker
|
||||||
|
func NewBroker(opts ...Option) Broker {
|
||||||
|
return &memoryBroker{
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
subscribers: make(map[string][]*memorySubscriber),
|
||||||
|
}
|
||||||
|
}
|
@@ -1,341 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/broker"
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/options"
|
|
||||||
maddr "go.unistack.org/micro/v4/util/addr"
|
|
||||||
"go.unistack.org/micro/v4/util/id"
|
|
||||||
mnet "go.unistack.org/micro/v4/util/net"
|
|
||||||
"go.unistack.org/micro/v4/util/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Broker struct {
|
|
||||||
funcPublish broker.FuncPublish
|
|
||||||
funcSubscribe broker.FuncSubscribe
|
|
||||||
subscribers map[string][]*Subscriber
|
|
||||||
addr string
|
|
||||||
opts broker.Options
|
|
||||||
mu sync.RWMutex
|
|
||||||
connected bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type memoryMessage struct {
|
|
||||||
c codec.Codec
|
|
||||||
topic string
|
|
||||||
ctx context.Context
|
|
||||||
body []byte
|
|
||||||
hdr metadata.Metadata
|
|
||||||
opts broker.MessageOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Ack() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Body() []byte {
|
|
||||||
return m.body
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Header() metadata.Metadata {
|
|
||||||
return m.hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Context() context.Context {
|
|
||||||
return m.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Topic() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
|
|
||||||
return m.c.Unmarshal(m.body, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Subscriber struct {
|
|
||||||
ctx context.Context
|
|
||||||
exit chan bool
|
|
||||||
handler interface{}
|
|
||||||
id string
|
|
||||||
topic string
|
|
||||||
opts broker.SubscribeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) newCodec(ct string) (codec.Codec, error) {
|
|
||||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
|
||||||
ct = ct[:idx]
|
|
||||||
}
|
|
||||||
b.mu.RLock()
|
|
||||||
c, ok := b.opts.Codecs[ct]
|
|
||||||
b.mu.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
return nil, codec.ErrUnknownContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Options() broker.Options {
|
|
||||||
return b.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Address() string {
|
|
||||||
return b.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Connect(ctx context.Context) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
if b.connected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// use 127.0.0.1 to avoid scan of all network interfaces
|
|
||||||
addr, err := maddr.Extract("127.0.0.1")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var rng rand.Rand
|
|
||||||
i := rng.Intn(20000)
|
|
||||||
// set addr with port
|
|
||||||
addr = mnet.HostPort(addr, 10000+i)
|
|
||||||
|
|
||||||
b.addr = addr
|
|
||||||
b.connected = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Disconnect(ctx context.Context) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
if !b.connected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b.connected = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Init(opts ...broker.Option) error {
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&b.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.funcPublish = b.fnPublish
|
|
||||||
b.funcSubscribe = b.fnSubscribe
|
|
||||||
|
|
||||||
b.opts.Hooks.EachPrev(func(hook options.Hook) {
|
|
||||||
switch h := hook.(type) {
|
|
||||||
case broker.HookPublish:
|
|
||||||
b.funcPublish = h(b.funcPublish)
|
|
||||||
case broker.HookSubscribe:
|
|
||||||
b.funcSubscribe = h(b.funcSubscribe)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...broker.MessageOption) (broker.Message, error) {
|
|
||||||
options := broker.NewMessageOptions(opts...)
|
|
||||||
if options.ContentType == "" {
|
|
||||||
options.ContentType = b.opts.ContentType
|
|
||||||
}
|
|
||||||
m := &memoryMessage{ctx: ctx, hdr: hdr, opts: options}
|
|
||||||
c, err := b.newCodec(m.opts.ContentType)
|
|
||||||
if err == nil {
|
|
||||||
m.body, err = c.Marshal(body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Publish(ctx context.Context, topic string, messages ...broker.Message) error {
|
|
||||||
return b.funcPublish(ctx, topic, messages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) fnPublish(ctx context.Context, topic string, messages ...broker.Message) error {
|
|
||||||
return b.publish(ctx, topic, messages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) publish(ctx context.Context, topic string, messages ...broker.Message) error {
|
|
||||||
b.mu.RLock()
|
|
||||||
if !b.connected {
|
|
||||||
b.mu.RUnlock()
|
|
||||||
return broker.ErrNotConnected
|
|
||||||
}
|
|
||||||
b.mu.RUnlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.RLock()
|
|
||||||
subs, ok := b.subscribers[topic]
|
|
||||||
b.mu.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
for _, sub := range subs {
|
|
||||||
switch s := sub.handler.(type) {
|
|
||||||
default:
|
|
||||||
if b.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidHandler)
|
|
||||||
}
|
|
||||||
case func(broker.Message) error:
|
|
||||||
for _, message := range messages {
|
|
||||||
msg, ok := message.(*memoryMessage)
|
|
||||||
if !ok {
|
|
||||||
if b.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg.topic = topic
|
|
||||||
if err = s(msg); err == nil && sub.opts.AutoAck {
|
|
||||||
err = msg.Ack()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if b.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
b.opts.Logger.Error(ctx, "broker handler error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case func([]broker.Message) error:
|
|
||||||
if err = s(messages); err == nil && sub.opts.AutoAck {
|
|
||||||
for _, message := range messages {
|
|
||||||
err = message.Ack()
|
|
||||||
if err != nil {
|
|
||||||
if b.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
b.opts.Logger.Error(ctx, "broker handler error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
|
||||||
return b.funcSubscribe(ctx, topic, handler, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
|
||||||
if err := broker.IsValidHandler(handler); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.RLock()
|
|
||||||
if !b.connected {
|
|
||||||
b.mu.RUnlock()
|
|
||||||
return nil, broker.ErrNotConnected
|
|
||||||
}
|
|
||||||
b.mu.RUnlock()
|
|
||||||
|
|
||||||
sid, err := id.New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := broker.NewSubscribeOptions(opts...)
|
|
||||||
|
|
||||||
sub := &Subscriber{
|
|
||||||
exit: make(chan bool, 1),
|
|
||||||
id: sid,
|
|
||||||
topic: topic,
|
|
||||||
handler: handler,
|
|
||||||
opts: options,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
b.subscribers[topic] = append(b.subscribers[topic], sub)
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-sub.exit
|
|
||||||
b.mu.Lock()
|
|
||||||
newSubscribers := make([]*Subscriber, 0, len(b.subscribers)-1)
|
|
||||||
for _, sb := range b.subscribers[topic] {
|
|
||||||
if sb.id == sub.id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newSubscribers = append(newSubscribers, sb)
|
|
||||||
}
|
|
||||||
b.subscribers[topic] = newSubscribers
|
|
||||||
b.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return sub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) String() string {
|
|
||||||
return "memory"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Name() string {
|
|
||||||
return b.opts.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Live() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Ready() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Broker) Health() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Subscriber) Options() broker.SubscribeOptions {
|
|
||||||
return m.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Subscriber) Topic() string {
|
|
||||||
return m.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Subscriber) Unsubscribe(ctx context.Context) error {
|
|
||||||
m.exit <- true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBroker return new memory broker
|
|
||||||
func NewBroker(opts ...broker.Option) broker.Broker {
|
|
||||||
return &Broker{
|
|
||||||
opts: broker.NewOptions(opts...),
|
|
||||||
subscribers: make(map[string][]*Subscriber),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,74 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
|
||||||
"go.unistack.org/micro/v4/broker"
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hldr struct {
|
|
||||||
c atomic.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hldr) Handler(m broker.Message) error {
|
|
||||||
h.c.Add(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMemoryBroker(t *testing.T) {
|
|
||||||
b := NewBroker(broker.Codec("application/octet-stream", codec.NewCodec()))
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if err := b.Init(); err != nil {
|
|
||||||
t.Fatalf("Unexpected init error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Connect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
topic := "test"
|
|
||||||
count := int64(10)
|
|
||||||
|
|
||||||
h := &hldr{}
|
|
||||||
|
|
||||||
sub, err := b.Subscribe(ctx, topic, h.Handler)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error subscribing %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := int64(0); i < count; i++ {
|
|
||||||
message, err := b.NewMessage(ctx,
|
|
||||||
metadata.Pairs(
|
|
||||||
"foo", "bar",
|
|
||||||
"id", fmt.Sprintf("%d", i),
|
|
||||||
),
|
|
||||||
[]byte(`"hello world"`),
|
|
||||||
broker.MessageContentType("application/octet-stream"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, topic, message); err != nil {
|
|
||||||
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sub.Unsubscribe(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Disconnect(ctx); err != nil {
|
|
||||||
t.Fatalf("Unexpected connect error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.c.Load() != count {
|
|
||||||
t.Fatal("invalid messages count received")
|
|
||||||
}
|
|
||||||
}
|
|
105
broker/memory_test.go
Normal file
105
broker/memory_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemoryBatchBroker(t *testing.T) {
|
||||||
|
b := NewBroker()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic := "test"
|
||||||
|
count := 10
|
||||||
|
|
||||||
|
fn := func(evts Events) error {
|
||||||
|
return evts.Ack()
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := b.BatchSubscribe(ctx, topic, fn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error subscribing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]*Message, 0, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
message := &Message{
|
||||||
|
Header: map[string]string{
|
||||||
|
metadata.HeaderTopic: topic,
|
||||||
|
"foo": "bar",
|
||||||
|
"id": fmt.Sprintf("%d", i),
|
||||||
|
},
|
||||||
|
Body: []byte(`"hello world"`),
|
||||||
|
}
|
||||||
|
msgs = append(msgs, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sub.Unsubscribe(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Disconnect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryBroker(t *testing.T) {
|
||||||
|
b := NewBroker()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic := "test"
|
||||||
|
count := 10
|
||||||
|
|
||||||
|
fn := func(p Event) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := b.Subscribe(ctx, topic, fn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error subscribing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]*Message, 0, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
message := &Message{
|
||||||
|
Header: map[string]string{
|
||||||
|
metadata.HeaderTopic: topic,
|
||||||
|
"foo": "bar",
|
||||||
|
"id": fmt.Sprintf("%d", i),
|
||||||
|
},
|
||||||
|
Body: []byte(`"hello world"`),
|
||||||
|
}
|
||||||
|
msgs = append(msgs, message)
|
||||||
|
|
||||||
|
if err := b.Publish(ctx, topic, message); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sub.Unsubscribe(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Disconnect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
}
|
179
broker/noop.go
179
broker/noop.go
@@ -1,179 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NoopBroker struct {
|
|
||||||
funcPublish FuncPublish
|
|
||||||
funcSubscribe FuncSubscribe
|
|
||||||
opts Options
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) newCodec(ct string) (codec.Codec, error) {
|
|
||||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
|
||||||
ct = ct[:idx]
|
|
||||||
}
|
|
||||||
b.mu.RLock()
|
|
||||||
c, ok := b.opts.Codecs[ct]
|
|
||||||
b.mu.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
return nil, codec.ErrUnknownContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBroker(opts ...Option) *NoopBroker {
|
|
||||||
b := &NoopBroker{opts: NewOptions(opts...)}
|
|
||||||
b.funcPublish = b.fnPublish
|
|
||||||
b.funcSubscribe = b.fnSubscribe
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Health() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Live() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Ready() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Name() string {
|
|
||||||
return b.opts.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) String() string {
|
|
||||||
return "noop"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Options() Options {
|
|
||||||
return b.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Init(opts ...Option) error {
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&b.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.funcPublish = b.fnPublish
|
|
||||||
b.funcSubscribe = b.fnSubscribe
|
|
||||||
|
|
||||||
b.opts.Hooks.EachPrev(func(hook options.Hook) {
|
|
||||||
switch h := hook.(type) {
|
|
||||||
case HookPublish:
|
|
||||||
b.funcPublish = h(b.funcPublish)
|
|
||||||
case HookSubscribe:
|
|
||||||
b.funcSubscribe = h(b.funcSubscribe)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Connect(_ context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Disconnect(_ context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Address() string {
|
|
||||||
return strings.Join(b.opts.Addrs, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
type noopMessage struct {
|
|
||||||
c codec.Codec
|
|
||||||
ctx context.Context
|
|
||||||
body []byte
|
|
||||||
hdr metadata.Metadata
|
|
||||||
opts MessageOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Ack() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Body() []byte {
|
|
||||||
return m.body
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Header() metadata.Metadata {
|
|
||||||
return m.hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Context() context.Context {
|
|
||||||
return m.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Topic() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *noopMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
|
|
||||||
return m.c.Unmarshal(m.body, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error) {
|
|
||||||
options := NewMessageOptions(opts...)
|
|
||||||
if options.ContentType == "" {
|
|
||||||
options.ContentType = b.opts.ContentType
|
|
||||||
}
|
|
||||||
m := &noopMessage{ctx: ctx, hdr: hdr, opts: options}
|
|
||||||
c, err := b.newCodec(m.opts.ContentType)
|
|
||||||
if err == nil {
|
|
||||||
m.body, err = c.Marshal(body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) fnPublish(_ context.Context, _ string, _ ...Message) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Publish(ctx context.Context, topic string, msg ...Message) error {
|
|
||||||
return b.funcPublish(ctx, topic, msg...)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NoopSubscriber struct {
|
|
||||||
ctx context.Context
|
|
||||||
topic string
|
|
||||||
handler interface{}
|
|
||||||
opts SubscribeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) {
|
|
||||||
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), handler: handler}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NoopBroker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) {
|
|
||||||
return b.funcSubscribe(ctx, topic, handler, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NoopSubscriber) Options() SubscribeOptions {
|
|
||||||
return s.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NoopSubscriber) Topic() string {
|
|
||||||
return s.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NoopSubscriber) Unsubscribe(_ context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testHook struct {
|
|
||||||
f bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHook) Publish1(fn FuncPublish) FuncPublish {
|
|
||||||
return func(ctx context.Context, topic string, messages ...Message) error {
|
|
||||||
t.f = true
|
|
||||||
return fn(ctx, topic, messages...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoopHook(t *testing.T) {
|
|
||||||
h := &testHook{}
|
|
||||||
|
|
||||||
b := NewBroker(Hooks(HookPublish(h.Publish1)))
|
|
||||||
|
|
||||||
if err := b.Init(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Publish(context.TODO(), "", nil); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.f {
|
|
||||||
t.Fatal("hook not works")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,74 +5,55 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v4/options"
|
"go.unistack.org/micro/v3/register"
|
||||||
"go.unistack.org/micro/v4/register"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
"go.unistack.org/micro/v4/sync"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options struct
|
// Options struct
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Name holds the broker name
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Tracer used for tracing
|
// Tracer used for tracing
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
// Register can be used for clustering
|
// Register can be used for clustering
|
||||||
Register register.Register
|
Register register.Register
|
||||||
// Codecs holds the codecs for marshal/unmarshal based on content-type
|
// Codec holds the codec for marshal/unmarshal
|
||||||
Codecs map[string]codec.Codec
|
Codec codec.Codec
|
||||||
// Logger used for logging
|
// Logger used for logging
|
||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
// Meter used for metrics
|
// Meter used for metrics
|
||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
// Context holds external options
|
// Context holds external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
|
||||||
// Wait waits for a collection of goroutines to finish
|
|
||||||
Wait *sync.WaitGroup
|
|
||||||
// TLSConfig holds tls.TLSConfig options
|
// TLSConfig holds tls.TLSConfig options
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
|
// ErrorHandler used when broker can't unmarshal incoming message
|
||||||
|
ErrorHandler Handler
|
||||||
|
// BatchErrorHandler used when broker can't unmashal incoming messages
|
||||||
|
BatchErrorHandler BatchHandler
|
||||||
|
// Name holds the broker name
|
||||||
|
Name string
|
||||||
// Addrs holds the broker address
|
// Addrs holds the broker address
|
||||||
Addrs []string
|
Addrs []string
|
||||||
// Hooks can be run before broker Publish/BatchPublish and
|
|
||||||
// Subscribe/BatchSubscribe methods
|
|
||||||
Hooks options.Hooks
|
|
||||||
|
|
||||||
// GracefulTimeout contains time to wait to finish in flight requests
|
|
||||||
GracefulTimeout time.Duration
|
|
||||||
|
|
||||||
// ContentType will be used if no content-type set when creating message
|
|
||||||
ContentType string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions create new Options
|
// NewOptions create new Options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
Register: register.DefaultRegister,
|
Register: register.DefaultRegister,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Codecs: make(map[string]codec.Codec),
|
Codec: codec.DefaultCodec,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
GracefulTimeout: DefaultGracefulTimeout,
|
|
||||||
ContentType: DefaultContentType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultContentType is the default content-type if not specified
|
|
||||||
var DefaultContentType = ""
|
|
||||||
|
|
||||||
// Context sets the context option
|
// Context sets the context option
|
||||||
func Context(ctx context.Context) Option {
|
func Context(ctx context.Context) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -80,27 +61,17 @@ func Context(ctx context.Context) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentType used by default if not specified
|
// PublishOptions struct
|
||||||
func ContentType(ct string) Option {
|
type PublishOptions struct {
|
||||||
return func(o *Options) {
|
// Context holds external options
|
||||||
o.ContentType = ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageOptions struct
|
|
||||||
type MessageOptions struct {
|
|
||||||
// ContentType for message body
|
|
||||||
ContentType string
|
|
||||||
// BodyOnly flag says the message contains raw body bytes and don't need
|
|
||||||
// codec Marshal method
|
|
||||||
BodyOnly bool
|
|
||||||
// Context holds custom options
|
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
// BodyOnly flag says the message contains raw body bytes
|
||||||
|
BodyOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageOptions creates MessageOptions struct
|
// NewPublishOptions creates PublishOptions struct
|
||||||
func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
||||||
options := MessageOptions{
|
options := PublishOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -113,6 +84,10 @@ func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
|||||||
type SubscribeOptions struct {
|
type SubscribeOptions struct {
|
||||||
// Context holds external options
|
// Context holds external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
// ErrorHandler used when broker can't unmarshal incoming message
|
||||||
|
ErrorHandler Handler
|
||||||
|
// BatchErrorHandler used when broker can't unmashal incoming messages
|
||||||
|
BatchErrorHandler BatchHandler
|
||||||
// Group holds consumer group
|
// Group holds consumer group
|
||||||
Group string
|
Group string
|
||||||
// AutoAck flag specifies auto ack of incoming message when no error happens
|
// AutoAck flag specifies auto ack of incoming message when no error happens
|
||||||
@@ -128,20 +103,20 @@ type SubscribeOptions struct {
|
|||||||
// Option func
|
// Option func
|
||||||
type Option func(*Options)
|
type Option func(*Options)
|
||||||
|
|
||||||
// MessageOption func
|
// PublishOption func
|
||||||
type MessageOption func(*MessageOptions)
|
type PublishOption func(*PublishOptions)
|
||||||
|
|
||||||
// MessageContentType sets message content-type that used to Marshal
|
// PublishBodyOnly publish only body of the message
|
||||||
func MessageContentType(ct string) MessageOption {
|
func PublishBodyOnly(b bool) PublishOption {
|
||||||
return func(o *MessageOptions) {
|
return func(o *PublishOptions) {
|
||||||
o.ContentType = ct
|
o.BodyOnly = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageBodyOnly publish only body of the message
|
// PublishContext sets the context
|
||||||
func MessageBodyOnly(b bool) MessageOption {
|
func PublishContext(ctx context.Context) PublishOption {
|
||||||
return func(o *MessageOptions) {
|
return func(o *PublishOptions) {
|
||||||
o.BodyOnly = b
|
o.Context = ctx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +127,51 @@ func Addrs(addrs ...string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codec sets the codec used for encoding/decoding messages
|
// Codec sets the codec used for encoding/decoding used where
|
||||||
func Codec(ct string, c codec.Codec) Option {
|
// a broker does not support headers
|
||||||
|
func Codec(c codec.Codec) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Codecs[ct] = c
|
o.Codec = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func ErrorHandler(h Handler) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func BatchErrorHandler(h BatchHandler) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func SubscribeErrorHandler(h Handler) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.ErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeBatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func SubscribeBatchErrorHandler(h BatchHandler) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue sets the subscribers queue
|
||||||
|
// Deprecated
|
||||||
|
func Queue(name string) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.Group = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,13 +224,6 @@ func Name(n string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks sets hook runs before action
|
|
||||||
func Hooks(h ...options.Hook) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Hooks = append(o.Hooks, h...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeContext set context
|
// SubscribeContext set context
|
||||||
func SubscribeContext(ctx context.Context) SubscribeOption {
|
func SubscribeContext(ctx context.Context) SubscribeOption {
|
||||||
return func(o *SubscribeOptions) {
|
return func(o *SubscribeOptions) {
|
||||||
@@ -222,6 +231,14 @@ func SubscribeContext(ctx context.Context) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableAutoAck disables auto ack
|
||||||
|
// Deprecated
|
||||||
|
func DisableAutoAck() SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.AutoAck = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeAutoAck contol auto acking of messages
|
// SubscribeAutoAck contol auto acking of messages
|
||||||
// after they have been handled.
|
// after they have been handled.
|
||||||
func SubscribeAutoAck(b bool) SubscribeOption {
|
func SubscribeAutoAck(b bool) SubscribeOption {
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
// IsValidHandler func signature
|
|
||||||
func IsValidHandler(sub interface{}) error {
|
|
||||||
switch sub.(type) {
|
|
||||||
default:
|
|
||||||
return ErrInvalidHandler
|
|
||||||
case func(Message) error:
|
|
||||||
break
|
|
||||||
case func([]Message) error:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/util/backoff"
|
"go.unistack.org/micro/v3/util/backoff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackoffFunc is the backoff call func
|
// BackoffFunc is the backoff call func
|
||||||
@@ -17,13 +17,13 @@ func BackoffExp(_ context.Context, _ Request, attempts int) (time.Duration, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BackoffInterval specifies randomization interval for backoff func
|
// BackoffInterval specifies randomization interval for backoff func
|
||||||
func BackoffInterval(minTime time.Duration, maxTime time.Duration) BackoffFunc {
|
func BackoffInterval(min time.Duration, max time.Duration) BackoffFunc {
|
||||||
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
||||||
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
||||||
if td < minTime {
|
if td < min {
|
||||||
return minTime, nil
|
return min, nil
|
||||||
} else if td > maxTime {
|
} else if td > max {
|
||||||
return maxTime, nil
|
return max, nil
|
||||||
}
|
}
|
||||||
return td, nil
|
return td, nil
|
||||||
}
|
}
|
||||||
|
@@ -34,23 +34,23 @@ func TestBackoffExp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackoffInterval(t *testing.T) {
|
func TestBackoffInterval(t *testing.T) {
|
||||||
minTime := 100 * time.Millisecond
|
min := 100 * time.Millisecond
|
||||||
maxTime := 300 * time.Millisecond
|
max := 300 * time.Millisecond
|
||||||
|
|
||||||
r := &testRequest{
|
r := &testRequest{
|
||||||
service: "test",
|
service: "test",
|
||||||
method: "test",
|
method: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := BackoffInterval(minTime, maxTime)
|
fn := BackoffInterval(min, max)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
d, err := fn(context.TODO(), r, i)
|
d, err := fn(context.TODO(), r, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d < minTime || d > maxTime {
|
if d < min || d > max {
|
||||||
t.Fatalf("Expected %v < %v < %v", minTime, d, maxTime)
|
t.Fatalf("Expected %v < %v < %v", min, d, max)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
// Package client is an interface for an RPC client
|
// Package client is an interface for an RPC client
|
||||||
package client
|
package client // import "go.unistack.org/micro/v3/client"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -29,23 +29,28 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Client is the interface used to make requests to services.
|
// Client is the interface used to make requests to services.
|
||||||
|
// It supports Request/Response via Transport and Publishing via the Broker.
|
||||||
// It also supports bidirectional streaming of requests.
|
// It also supports bidirectional streaming of requests.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
Name() string
|
Name() string
|
||||||
Init(opts ...Option) error
|
Init(opts ...Option) error
|
||||||
Options() Options
|
Options() Options
|
||||||
|
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
|
||||||
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
||||||
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||||
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||||
|
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
||||||
|
BatchPublish(ctx context.Context, msg []Message, opts ...PublishOption) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
// Message is the interface for publishing asynchronously
|
||||||
FuncCall func(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
type Message interface {
|
||||||
HookCall func(next FuncCall) FuncCall
|
Topic() string
|
||||||
FuncStream func(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
Payload() interface{}
|
||||||
HookStream func(next FuncStream) FuncStream
|
ContentType() string
|
||||||
)
|
Metadata() metadata.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
// Request is the interface for a synchronous request used by Call or Stream
|
// Request is the interface for a synchronous request used by Call or Stream
|
||||||
type Request interface {
|
type Request interface {
|
||||||
@@ -105,5 +110,11 @@ type Option func(*Options)
|
|||||||
// CallOption used by Call or Stream
|
// CallOption used by Call or Stream
|
||||||
type CallOption func(*CallOptions)
|
type CallOption func(*CallOptions)
|
||||||
|
|
||||||
|
// PublishOption used by Publish
|
||||||
|
type PublishOption func(*PublishOptions)
|
||||||
|
|
||||||
|
// MessageOption used by NewMessage
|
||||||
|
type MessageOption func(*MessageOptions)
|
||||||
|
|
||||||
// RequestOption used by NewRequest
|
// RequestOption used by NewRequest
|
||||||
type RequestOption func(*RequestOptions)
|
type RequestOption func(*RequestOptions)
|
||||||
|
26
client/client_call_options_test.go
Normal file
26
client/client_call_options_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewClientCallOptions(t *testing.T) {
|
||||||
|
var flag bool
|
||||||
|
w := func(fn CallFunc) CallFunc {
|
||||||
|
flag = true
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
c := NewClientCallOptions(NewClient(),
|
||||||
|
WithAddress("127.0.0.1"),
|
||||||
|
WithCallWrapper(w),
|
||||||
|
WithRequestTimeout(1*time.Millisecond),
|
||||||
|
WithRetries(0),
|
||||||
|
WithBackoff(BackoffInterval(10*time.Millisecond, 100*time.Millisecond)),
|
||||||
|
)
|
||||||
|
_ = c.Call(context.TODO(), c.NewRequest("service", "endpoint", nil), nil)
|
||||||
|
if !flag {
|
||||||
|
t.Fatalf("NewClientCallOptions not works")
|
||||||
|
}
|
||||||
|
}
|
@@ -15,15 +15,6 @@ func FromContext(ctx context.Context) (Client, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustContext get client from context
|
|
||||||
func MustContext(ctx context.Context) Client {
|
|
||||||
c, ok := FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
panic("missing client")
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContext put client in context
|
// NewContext put client in context
|
||||||
func NewContext(ctx context.Context, c Client) context.Context {
|
func NewContext(ctx context.Context, c Client) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -32,6 +23,16 @@ func NewContext(ctx context.Context, c Client) context.Context {
|
|||||||
return context.WithValue(ctx, clientKey{}, c)
|
return context.WithValue(ctx, clientKey{}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPublishOption returns a function to setup a context with given value
|
||||||
|
func SetPublishOption(k, v interface{}) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetCallOption returns a function to setup a context with given value
|
// SetCallOption returns a function to setup a context with given value
|
||||||
func SetCallOption(k, v interface{}) CallOption {
|
func SetCallOption(k, v interface{}) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
|
@@ -39,6 +39,17 @@ func TestNewNilContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetPublishOption(t *testing.T) {
|
||||||
|
type key struct{}
|
||||||
|
o := SetPublishOption(key{}, "test")
|
||||||
|
opts := &PublishOptions{}
|
||||||
|
o(opts)
|
||||||
|
|
||||||
|
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
|
||||||
|
t.Fatal("SetPublishOption not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetCallOption(t *testing.T) {
|
func TestSetCallOption(t *testing.T) {
|
||||||
type key struct{}
|
type key struct{}
|
||||||
o := SetCallOption(key{}, "test")
|
o := SetCallOption(key{}, "test")
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
"go.unistack.org/micro/v3/errors"
|
||||||
"go.unistack.org/micro/v4/router"
|
"go.unistack.org/micro/v3/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LookupFunc is used to lookup routes for a service
|
// LookupFunc is used to lookup routes for a service
|
||||||
|
229
client/noop.go
229
client/noop.go
@@ -3,16 +3,13 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/broker"
|
||||||
"go.unistack.org/micro/v4/errors"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v3/errors"
|
||||||
"go.unistack.org/micro/v4/options"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
"go.unistack.org/micro/v4/selector"
|
"go.unistack.org/micro/v3/selector"
|
||||||
"go.unistack.org/micro/v4/semconv"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultCodecs will be used to encode/decode data
|
// DefaultCodecs will be used to encode/decode data
|
||||||
@@ -21,9 +18,13 @@ var DefaultCodecs = map[string]codec.Codec{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type noopClient struct {
|
type noopClient struct {
|
||||||
funcCall FuncCall
|
opts Options
|
||||||
funcStream FuncStream
|
}
|
||||||
opts Options
|
|
||||||
|
type noopMessage struct {
|
||||||
|
topic string
|
||||||
|
payload interface{}
|
||||||
|
opts MessageOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type noopRequest struct {
|
type noopRequest struct {
|
||||||
@@ -38,12 +39,16 @@ type noopRequest struct {
|
|||||||
|
|
||||||
// NewClient returns new noop client
|
// NewClient returns new noop client
|
||||||
func NewClient(opts ...Option) Client {
|
func NewClient(opts ...Option) Client {
|
||||||
n := &noopClient{opts: NewOptions(opts...)}
|
nc := &noopClient{opts: NewOptions(opts...)}
|
||||||
|
// wrap in reverse
|
||||||
|
|
||||||
n.funcCall = n.fnCall
|
c := Client(nc)
|
||||||
n.funcStream = n.fnStream
|
|
||||||
|
|
||||||
return n
|
for i := len(nc.opts.Wrappers); i > 0; i-- {
|
||||||
|
c = nc.opts.Wrappers[i-1](c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Name() string {
|
func (n *noopClient) Name() string {
|
||||||
@@ -95,13 +100,10 @@ func (n *noopResponse) Read() ([]byte, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type noopStream struct {
|
type noopStream struct{}
|
||||||
err error
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopStream) Context() context.Context {
|
func (n *noopStream) Context() context.Context {
|
||||||
return n.ctx
|
return context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Request() Request {
|
func (n *noopStream) Request() Request {
|
||||||
@@ -129,40 +131,47 @@ func (n *noopStream) RecvMsg(interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Error() error {
|
func (n *noopStream) Error() error {
|
||||||
return n.err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) Close() error {
|
func (n *noopStream) Close() error {
|
||||||
if sp, ok := tracer.SpanFromContext(n.ctx); ok && sp != nil {
|
return nil
|
||||||
if n.err != nil {
|
|
||||||
sp.SetStatus(tracer.SpanStatusError, n.err.Error())
|
|
||||||
}
|
|
||||||
sp.Finish()
|
|
||||||
}
|
|
||||||
return n.err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopStream) CloseSend() error {
|
func (n *noopStream) CloseSend() error {
|
||||||
return n.err
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopMessage) Topic() string {
|
||||||
|
return n.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopMessage) Payload() interface{} {
|
||||||
|
return n.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopMessage) ContentType() string {
|
||||||
|
return n.opts.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopMessage) Metadata() metadata.Metadata {
|
||||||
|
return n.opts.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
|
||||||
|
if cf, ok := n.opts.Codecs[contentType]; ok {
|
||||||
|
return cf, nil
|
||||||
|
}
|
||||||
|
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||||
|
return cf, nil
|
||||||
|
}
|
||||||
|
return nil, codec.ErrUnknownContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Init(opts ...Option) error {
|
func (n *noopClient) Init(opts ...Option) error {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&n.opts)
|
o(&n.opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
n.funcCall = n.fnCall
|
|
||||||
n.funcStream = n.fnStream
|
|
||||||
|
|
||||||
n.opts.Hooks.EachPrev(func(hook options.Hook) {
|
|
||||||
switch h := hook.(type) {
|
|
||||||
case HookCall:
|
|
||||||
n.funcCall = h(n.funcCall)
|
|
||||||
case HookStream:
|
|
||||||
n.funcStream = h(n.funcStream)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,31 +184,6 @@ func (n *noopClient) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
||||||
ts := time.Now()
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
|
||||||
var sp tracer.Span
|
|
||||||
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
|
||||||
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
||||||
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
|
||||||
)
|
|
||||||
err := n.funcCall(ctx, req, rsp, opts...)
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
|
|
||||||
te := time.Since(ts)
|
|
||||||
n.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
n.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
|
|
||||||
if me := errors.FromError(err); me == nil {
|
|
||||||
sp.Finish()
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
|
|
||||||
} else {
|
|
||||||
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
|
|
||||||
// make a copy of call opts
|
// make a copy of call opts
|
||||||
callOpts := n.opts.CallOptions
|
callOpts := n.opts.CallOptions
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -228,8 +212,11 @@ func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make copy of call method
|
// make copy of call method
|
||||||
hcall := func(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
|
hcall := n.call
|
||||||
return nil
|
|
||||||
|
// wrap the call in reverse
|
||||||
|
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||||
|
hcall = callOpts.CallWrappers[i-1](hcall)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the router passed as a call option, or fallback to the rpc clients router
|
// use the router passed as a call option, or fallback to the rpc clients router
|
||||||
@@ -254,7 +241,7 @@ func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, o
|
|||||||
// call backoff first. Someone may want an initial start delay
|
// call backoff first. Someone may want an initial start delay
|
||||||
t, err := callOpts.Backoff(ctx, req, i)
|
t, err := callOpts.Backoff(ctx, req, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", "%s", err)
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -268,7 +255,7 @@ func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, o
|
|||||||
// TODO apply any filtering here
|
// TODO apply any filtering here
|
||||||
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", "%s", err)
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// balance the list of nodes
|
// balance the list of nodes
|
||||||
@@ -328,36 +315,20 @@ func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, o
|
|||||||
return gerr
|
return gerr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) NewRequest(service, endpoint string, _ interface{}, _ ...RequestOption) Request {
|
func (n *noopClient) call(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts ...RequestOption) Request {
|
||||||
return &noopRequest{service: service, endpoint: endpoint}
|
return &noopRequest{service: service, endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
|
||||||
ts := time.Now()
|
options := NewMessageOptions(append([]MessageOption{MessageContentType(n.opts.ContentType)}, opts...)...)
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
return &noopMessage{topic: topic, payload: msg, opts: options}
|
||||||
var sp tracer.Span
|
|
||||||
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
|
||||||
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
||||||
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
|
||||||
)
|
|
||||||
stream, err := n.funcStream(ctx, req, opts...)
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
|
|
||||||
te := time.Since(ts)
|
|
||||||
n.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
n.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
|
|
||||||
if me := errors.FromError(err); me == nil {
|
|
||||||
sp.Finish()
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
|
|
||||||
} else {
|
|
||||||
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// make a copy of call opts
|
// make a copy of call opts
|
||||||
@@ -417,7 +388,7 @@ func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOpti
|
|||||||
// call backoff first. Someone may want an initial start delay
|
// call backoff first. Someone may want an initial start delay
|
||||||
t, cerr := callOpts.Backoff(ctx, req, i)
|
t, cerr := callOpts.Backoff(ctx, req, i)
|
||||||
if cerr != nil {
|
if cerr != nil {
|
||||||
return nil, errors.InternalServerError("go.micro.client", "%s", cerr)
|
return nil, errors.InternalServerError("go.micro.client", cerr.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -431,7 +402,7 @@ func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOpti
|
|||||||
// TODO apply any filtering here
|
// TODO apply any filtering here
|
||||||
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
routes, err = n.opts.Lookup(ctx, req, callOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.InternalServerError("go.micro.client", "%s", err)
|
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// balance the list of nodes
|
// balance the list of nodes
|
||||||
@@ -497,6 +468,64 @@ func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOpti
|
|||||||
return nil, grr
|
return nil, grr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) stream(ctx context.Context, _ string, _ Request, _ CallOptions) (Stream, error) {
|
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (Stream, error) {
|
||||||
return &noopStream{ctx: ctx}, nil
|
return &noopStream{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
|
return n.publish(ctx, ps, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
|
||||||
|
return n.publish(ctx, []Message{p}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
|
options := NewPublishOptions(opts...)
|
||||||
|
|
||||||
|
msgs := make([]*broker.Message, 0, len(ps))
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
md = metadata.New(0)
|
||||||
|
}
|
||||||
|
md[metadata.HeaderContentType] = p.ContentType()
|
||||||
|
|
||||||
|
topic := p.Topic()
|
||||||
|
|
||||||
|
// get the exchange
|
||||||
|
if len(options.Exchange) > 0 {
|
||||||
|
topic = options.Exchange
|
||||||
|
}
|
||||||
|
|
||||||
|
md[metadata.HeaderTopic] = topic
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// passed in raw data
|
||||||
|
if d, ok := p.Payload().(*codec.Frame); ok {
|
||||||
|
body = d.Data
|
||||||
|
} else {
|
||||||
|
// use codec for payload
|
||||||
|
cf, err := n.newCodec(p.ContentType())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the body
|
||||||
|
b, err := cf.Marshal(p.Payload())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
body = b
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs = append(msgs, &broker.Message{Header: md, Body: body})
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.opts.Broker.BatchPublish(ctx, msgs,
|
||||||
|
broker.PublishContext(options.Context),
|
||||||
|
broker.PublishBodyOnly(options.BodyOnly),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -6,31 +6,23 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/broker"
|
"go.unistack.org/micro/v3/broker"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v4/options"
|
"go.unistack.org/micro/v3/network/transport"
|
||||||
"go.unistack.org/micro/v4/register"
|
"go.unistack.org/micro/v3/register"
|
||||||
"go.unistack.org/micro/v4/router"
|
"go.unistack.org/micro/v3/router"
|
||||||
"go.unistack.org/micro/v4/selector"
|
"go.unistack.org/micro/v3/selector"
|
||||||
"go.unistack.org/micro/v4/selector/random"
|
"go.unistack.org/micro/v3/selector/random"
|
||||||
"go.unistack.org/micro/v4/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options holds client options
|
// Options holds client options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Codecs map
|
// Transport used for transfer messages
|
||||||
Codecs map[string]codec.Codec
|
Transport transport.Transport
|
||||||
|
|
||||||
// Proxy is used for proxy requests
|
|
||||||
Proxy string
|
|
||||||
// ContentType is used to select codec
|
|
||||||
ContentType string
|
|
||||||
// Name is the client name
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Selector used to select needed address
|
// Selector used to select needed address
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Logger used to log messages
|
// Logger used to log messages
|
||||||
@@ -45,28 +37,28 @@ type Options struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// Router used to get route
|
// Router used to get route
|
||||||
Router router.Router
|
Router router.Router
|
||||||
|
|
||||||
// TLSConfig specifies tls.Config for secure connection
|
// TLSConfig specifies tls.Config for secure connection
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
|
// Codecs map
|
||||||
|
Codecs map[string]codec.Codec
|
||||||
// Lookup func used to get destination addr
|
// Lookup func used to get destination addr
|
||||||
Lookup LookupFunc
|
Lookup LookupFunc
|
||||||
// ContextDialer used to connect
|
// Proxy is used for proxy requests
|
||||||
ContextDialer func(context.Context, string) (net.Conn, error)
|
Proxy string
|
||||||
|
// ContentType is used to select codec
|
||||||
|
ContentType string
|
||||||
|
// Name is the client name
|
||||||
|
Name string
|
||||||
// Wrappers contains wrappers
|
// Wrappers contains wrappers
|
||||||
Wrappers []Wrapper
|
Wrappers []Wrapper
|
||||||
// Hooks can be run before broker Publish/BatchPublish and
|
|
||||||
// Subscribe/BatchSubscribe methods
|
|
||||||
Hooks options.Hooks
|
|
||||||
|
|
||||||
// CallOptions contains default CallOptions
|
// CallOptions contains default CallOptions
|
||||||
CallOptions CallOptions
|
CallOptions CallOptions
|
||||||
|
|
||||||
// PoolSize connection pool size
|
// PoolSize connection pool size
|
||||||
PoolSize int
|
PoolSize int
|
||||||
// PoolTTL connection pool ttl
|
// PoolTTL connection pool ttl
|
||||||
PoolTTL time.Duration
|
PoolTTL time.Duration
|
||||||
|
// ContextDialer used to connect
|
||||||
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCallOptions creates new call options struct
|
// NewCallOptions creates new call options struct
|
||||||
@@ -80,16 +72,6 @@ func NewCallOptions(opts ...CallOption) CallOptions {
|
|||||||
|
|
||||||
// CallOptions holds client call options
|
// CallOptions holds client call options
|
||||||
type CallOptions struct {
|
type CallOptions struct {
|
||||||
// RequestMetadata holds additional metadata for call
|
|
||||||
RequestMetadata metadata.Metadata
|
|
||||||
|
|
||||||
// Network name
|
|
||||||
Network string
|
|
||||||
// Content-Type
|
|
||||||
ContentType string
|
|
||||||
// AuthToken string
|
|
||||||
AuthToken string
|
|
||||||
|
|
||||||
// Selector selects addr
|
// Selector selects addr
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Context used for deadline
|
// Context used for deadline
|
||||||
@@ -97,30 +79,35 @@ type CallOptions struct {
|
|||||||
// Router used for route
|
// Router used for route
|
||||||
Router router.Router
|
Router router.Router
|
||||||
// Retry func used for retries
|
// Retry func used for retries
|
||||||
|
|
||||||
// ResponseMetadata holds additional metadata from call
|
|
||||||
ResponseMetadata *metadata.Metadata
|
|
||||||
|
|
||||||
Retry RetryFunc
|
Retry RetryFunc
|
||||||
// Backoff func used for backoff when retry
|
// Backoff func used for backoff when retry
|
||||||
Backoff BackoffFunc
|
Backoff BackoffFunc
|
||||||
// ContextDialer used to connect
|
// Network name
|
||||||
ContextDialer func(context.Context, string) (net.Conn, error)
|
Network string
|
||||||
|
// Content-Type
|
||||||
|
ContentType string
|
||||||
|
// AuthToken string
|
||||||
|
AuthToken string
|
||||||
// Address specifies static addr list
|
// Address specifies static addr list
|
||||||
Address []string
|
Address []string
|
||||||
// SelectOptions selector options
|
// SelectOptions selector options
|
||||||
SelectOptions []selector.SelectOption
|
SelectOptions []selector.SelectOption
|
||||||
|
// CallWrappers call wrappers
|
||||||
|
CallWrappers []CallWrapper
|
||||||
// StreamTimeout stream timeout
|
// StreamTimeout stream timeout
|
||||||
StreamTimeout time.Duration
|
StreamTimeout time.Duration
|
||||||
// RequestTimeout request timeout
|
// RequestTimeout request timeout
|
||||||
RequestTimeout time.Duration
|
RequestTimeout time.Duration
|
||||||
|
// RequestMetadata holds additional metadata for call
|
||||||
|
RequestMetadata metadata.Metadata
|
||||||
|
// ResponseMetadata holds additional metadata from call
|
||||||
|
ResponseMetadata *metadata.Metadata
|
||||||
// DialTimeout dial timeout
|
// DialTimeout dial timeout
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
// Retries specifies retries num
|
// Retries specifies retries num
|
||||||
Retries int
|
Retries int
|
||||||
|
// ContextDialer used to connect
|
||||||
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextDialer pass ContextDialer to client
|
// ContextDialer pass ContextDialer to client
|
||||||
@@ -137,6 +124,43 @@ func Context(ctx context.Context) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPublishOptions create new PublishOptions struct from option
|
||||||
|
func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
||||||
|
options := PublishOptions{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishOptions holds publish options
|
||||||
|
type PublishOptions struct {
|
||||||
|
// Context used for external options
|
||||||
|
Context context.Context
|
||||||
|
// Exchange topic exchange name
|
||||||
|
Exchange string
|
||||||
|
// BodyOnly will publish only message body
|
||||||
|
BodyOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessageOptions creates message options struct
|
||||||
|
func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
||||||
|
options := MessageOptions{Metadata: metadata.New(1)}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageOptions holds client message options
|
||||||
|
type MessageOptions struct {
|
||||||
|
// Metadata additional metadata
|
||||||
|
Metadata metadata.Metadata
|
||||||
|
// ContentType specify content-type of message
|
||||||
|
// deprecated
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
// NewRequestOptions creates new RequestOptions struct
|
// NewRequestOptions creates new RequestOptions struct
|
||||||
func NewRequestOptions(opts ...RequestOption) RequestOptions {
|
func NewRequestOptions(opts ...RequestOption) RequestOptions {
|
||||||
options := RequestOptions{}
|
options := RequestOptions{}
|
||||||
@@ -161,23 +185,25 @@ func NewOptions(opts ...Option) Options {
|
|||||||
options := Options{
|
options := Options{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
ContentType: DefaultContentType,
|
ContentType: DefaultContentType,
|
||||||
Codecs: DefaultCodecs,
|
Codecs: make(map[string]codec.Codec),
|
||||||
CallOptions: CallOptions{
|
CallOptions: CallOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Backoff: DefaultBackoff,
|
Backoff: DefaultBackoff,
|
||||||
Retry: DefaultRetry,
|
Retry: DefaultRetry,
|
||||||
Retries: DefaultRetries,
|
Retries: DefaultRetries,
|
||||||
RequestTimeout: DefaultRequestTimeout,
|
RequestTimeout: DefaultRequestTimeout,
|
||||||
|
DialTimeout: transport.DefaultDialTimeout,
|
||||||
},
|
},
|
||||||
Lookup: LookupRoute,
|
Lookup: LookupRoute,
|
||||||
PoolSize: DefaultPoolSize,
|
PoolSize: DefaultPoolSize,
|
||||||
PoolTTL: DefaultPoolTTL,
|
PoolTTL: DefaultPoolTTL,
|
||||||
Selector: random.NewSelector(),
|
Selector: random.NewSelector(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Broker: broker.DefaultBroker,
|
Broker: broker.DefaultBroker,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
Router: router.DefaultRouter,
|
Router: router.DefaultRouter,
|
||||||
|
Transport: transport.DefaultTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -250,6 +276,13 @@ func PoolTTL(d time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transport to use for communication e.g http, rabbitmq, etc
|
||||||
|
func Transport(t transport.Transport) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Transport = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register sets the routers register
|
// Register sets the routers register
|
||||||
func Register(r register.Register) Option {
|
func Register(r register.Register) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -273,6 +306,20 @@ func Selector(s selector.Selector) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap adds a wrapper to the list of options passed into the client
|
||||||
|
func Wrap(w Wrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Wrappers = append(o.Wrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapCall adds a wrapper to the list of CallFunc wrappers
|
||||||
|
func WrapCall(cw ...CallWrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.CallOptions.CallWrappers = append(o.CallOptions.CallWrappers, cw...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Backoff is used to set the backoff function used when retrying Calls
|
// Backoff is used to set the backoff function used when retrying Calls
|
||||||
func Backoff(fn BackoffFunc) Option {
|
func Backoff(fn BackoffFunc) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -299,6 +346,14 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
// set the internal tls
|
// set the internal tls
|
||||||
o.TLSConfig = t
|
o.TLSConfig = t
|
||||||
|
|
||||||
|
// set the default transport if one is not
|
||||||
|
// already set. Required for Init call below.
|
||||||
|
|
||||||
|
// set the transport tls
|
||||||
|
_ = o.Transport.Init(
|
||||||
|
transport.TLSConfig(t),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +392,43 @@ func DialTimeout(d time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithExchange sets the exchange to route a message through
|
||||||
|
// Deprecated
|
||||||
|
func WithExchange(e string) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.Exchange = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishExchange sets the exchange to route a message through
|
||||||
|
func PublishExchange(e string) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.Exchange = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBodyOnly publish only message body
|
||||||
|
// DERECATED
|
||||||
|
func WithBodyOnly(b bool) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.BodyOnly = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishBodyOnly publish only message body
|
||||||
|
func PublishBodyOnly(b bool) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.BodyOnly = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishContext sets the context in publish options
|
||||||
|
func PublishContext(ctx context.Context) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithContextDialer pass ContextDialer to client call
|
// WithContextDialer pass ContextDialer to client call
|
||||||
func WithContextDialer(fn func(context.Context, string) (net.Conn, error)) CallOption {
|
func WithContextDialer(fn func(context.Context, string) (net.Conn, error)) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
@@ -358,6 +450,13 @@ func WithAddress(a ...string) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCallWrapper is a CallOption which adds to the existing CallFunc wrappers
|
||||||
|
func WithCallWrapper(cw ...CallWrapper) CallOption {
|
||||||
|
return func(o *CallOptions) {
|
||||||
|
o.CallWrappers = append(o.CallWrappers, cw...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithBackoff is a CallOption which overrides that which
|
// WithBackoff is a CallOption which overrides that which
|
||||||
// set in Options.CallOptions
|
// set in Options.CallOptions
|
||||||
func WithBackoff(fn BackoffFunc) CallOption {
|
func WithBackoff(fn BackoffFunc) CallOption {
|
||||||
@@ -427,6 +526,13 @@ func WithAuthToken(t string) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithNetwork is a CallOption which sets the network attribute
|
||||||
|
func WithNetwork(n string) CallOption {
|
||||||
|
return func(o *CallOptions) {
|
||||||
|
o.Network = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithRouter sets the router to use for this call
|
// WithRouter sets the router to use for this call
|
||||||
func WithRouter(r router.Router) CallOption {
|
func WithRouter(r router.Router) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
@@ -448,6 +554,30 @@ func WithSelectOptions(sops ...selector.SelectOption) CallOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMessageContentType sets the message content type
|
||||||
|
// Deprecated
|
||||||
|
func WithMessageContentType(ct string) MessageOption {
|
||||||
|
return func(o *MessageOptions) {
|
||||||
|
o.Metadata.Set(metadata.HeaderContentType, ct)
|
||||||
|
o.ContentType = ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageContentType sets the message content type
|
||||||
|
func MessageContentType(ct string) MessageOption {
|
||||||
|
return func(o *MessageOptions) {
|
||||||
|
o.Metadata.Set(metadata.HeaderContentType, ct)
|
||||||
|
o.ContentType = ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMetadata sets the message metadata
|
||||||
|
func MessageMetadata(k, v string) MessageOption {
|
||||||
|
return func(o *MessageOptions) {
|
||||||
|
o.Metadata.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StreamingRequest specifies that request is streaming
|
// StreamingRequest specifies that request is streaming
|
||||||
func StreamingRequest(b bool) RequestOption {
|
func StreamingRequest(b bool) RequestOption {
|
||||||
return func(o *RequestOptions) {
|
return func(o *RequestOptions) {
|
||||||
@@ -461,10 +591,3 @@ func RequestContentType(ct string) RequestOption {
|
|||||||
o.ContentType = ct
|
o.ContentType = ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks sets hook runs before action
|
|
||||||
func Hooks(h ...options.Hook) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Hooks = append(o.Hooks, h...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -3,7 +3,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
"go.unistack.org/micro/v3/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RetryFunc that returning either false or a non-nil error will result in the call not being retried
|
// RetryFunc that returning either false or a non-nil error will result in the call not being retried
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
"go.unistack.org/micro/v3/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRetryAlways(t *testing.T) {
|
func TestRetryAlways(t *testing.T) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testRequest struct {
|
type testRequest struct {
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message sent to member in cluster
|
|
||||||
type Message interface {
|
|
||||||
// Header returns message headers
|
|
||||||
Header() metadata.Metadata
|
|
||||||
// Body returns broker message may be []byte slice or some go struct or interface
|
|
||||||
Body() interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Node interface {
|
|
||||||
// Name returns node name
|
|
||||||
Name() string
|
|
||||||
// Address returns node address
|
|
||||||
Address() string
|
|
||||||
// Metadata returns node metadata
|
|
||||||
Metadata() metadata.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cluster interface used for cluster communication across nodes
|
|
||||||
type Cluster interface {
|
|
||||||
// Join is used to take an existing members and performing state sync
|
|
||||||
Join(ctx context.Context, addr ...string) error
|
|
||||||
// Leave broadcast a leave message and stop listeners
|
|
||||||
Leave(ctx context.Context) error
|
|
||||||
// Ping is used to probe live status of the node
|
|
||||||
Ping(ctx context.Context, node Node, payload []byte) error
|
|
||||||
// Members returns the cluster members
|
|
||||||
Members() ([]Node, error)
|
|
||||||
// Broadcast send message for all members in cluster, if filter is not nil, nodes may be filtered
|
|
||||||
// by key/value pairs
|
|
||||||
Broadcast(ctx context.Context, msg Message, filter ...string) error
|
|
||||||
// Unicast send message to single member in cluster
|
|
||||||
Unicast(ctx context.Context, node Node, msg Message) error
|
|
||||||
// Live returns cluster liveness
|
|
||||||
Live() bool
|
|
||||||
// Ready returns cluster readiness
|
|
||||||
Ready() bool
|
|
||||||
// Health returns cluster health
|
|
||||||
Health() bool
|
|
||||||
}
|
|
@@ -1,235 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newSQLRowError() *sql.Row {
|
|
||||||
row := &sql.Row{}
|
|
||||||
t := reflect.TypeOf(row).Elem()
|
|
||||||
field, _ := t.FieldByName("err")
|
|
||||||
rowPtr := unsafe.Pointer(row)
|
|
||||||
errFieldPtr := unsafe.Pointer(uintptr(rowPtr) + field.Offset)
|
|
||||||
errPtr := (*error)(errFieldPtr)
|
|
||||||
*errPtr = ErrorNoAliveNodes
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClusterQuerier interface {
|
|
||||||
Querier
|
|
||||||
WaitForNodes(ctx context.Context, criterion ...hasql.NodeStateCriterion) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
hasql *hasql.Cluster[Querier]
|
|
||||||
options ClusterOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCluster returns [Querier] that provides cluster of nodes
|
|
||||||
func NewCluster[T Querier](opts ...ClusterOption) (ClusterQuerier, error) {
|
|
||||||
options := ClusterOptions{Context: context.Background()}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&options)
|
|
||||||
}
|
|
||||||
if options.NodeChecker == nil {
|
|
||||||
return nil, ErrClusterChecker
|
|
||||||
}
|
|
||||||
if options.NodeDiscoverer == nil {
|
|
||||||
return nil, ErrClusterDiscoverer
|
|
||||||
}
|
|
||||||
if options.NodePicker == nil {
|
|
||||||
return nil, ErrClusterPicker
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Retries < 1 {
|
|
||||||
options.Retries = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.NodeStateCriterion == 0 {
|
|
||||||
options.NodeStateCriterion = hasql.Primary
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Options = append(options.Options, hasql.WithNodePicker(options.NodePicker))
|
|
||||||
if p, ok := options.NodePicker.(*CustomPicker[Querier]); ok {
|
|
||||||
p.opts.Priority = options.NodePriority
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := hasql.NewCluster(
|
|
||||||
options.NodeDiscoverer,
|
|
||||||
options.NodeChecker,
|
|
||||||
options.Options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Cluster{hasql: c, options: options}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) {
|
|
||||||
var tx *sql.Tx
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if tx, err = n.DB().BeginTx(ctx, opts); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if tx == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) Close() error {
|
|
||||||
return c.hasql.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) Conn(ctx context.Context) (*sql.Conn, error) {
|
|
||||||
var conn *sql.Conn
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if conn, err = n.DB().Conn(ctx); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if conn == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
|
||||||
var res sql.Result
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().ExecContext(ctx, query, args...); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
|
|
||||||
var res *sql.Stmt
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().PrepareContext(ctx, query); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
|
|
||||||
var res *sql.Rows
|
|
||||||
var err error
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if res, err = n.DB().QueryContext(ctx, query); err != nil && err != sql.ErrNoRows && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil && err == nil {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
|
|
||||||
var res *sql.Row
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
res = n.DB().QueryRowContext(ctx, query, args...)
|
|
||||||
if res.Err() == nil {
|
|
||||||
return false
|
|
||||||
} else if res.Err() != nil && retries >= c.options.Retries {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if res == nil {
|
|
||||||
res = newSQLRowError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) PingContext(ctx context.Context) error {
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
retries := 0
|
|
||||||
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
|
|
||||||
ok = true
|
|
||||||
for ; retries < c.options.Retries; retries++ {
|
|
||||||
if err = n.DB().PingContext(ctx); err != nil && retries >= c.options.Retries {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
err = ErrorNoAliveNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) WaitForNodes(ctx context.Context, criterions ...hasql.NodeStateCriterion) error {
|
|
||||||
for _, criterion := range criterions {
|
|
||||||
if _, err := c.hasql.WaitForNode(ctx, criterion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,171 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewCluster(t *testing.T) {
|
|
||||||
dbMaster, dbMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbMaster.Close()
|
|
||||||
dbMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(1, 0)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("master-dc1"))
|
|
||||||
|
|
||||||
dbDRMaster, dbDRMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbDRMaster.Close()
|
|
||||||
dbDRMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 40)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster1-dc2"))
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster"))
|
|
||||||
|
|
||||||
dbSlaveDC1, dbSlaveDC1Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC1.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
dbSlaveDC2, dbSlaveDC2Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC2.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
tctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
c, err := NewCluster[Querier](
|
|
||||||
WithClusterContext(tctx),
|
|
||||||
WithClusterNodeChecker(hasql.PostgreSQLChecker),
|
|
||||||
WithClusterNodePicker(NewCustomPicker[Querier](
|
|
||||||
CustomPickerMaxLag(100),
|
|
||||||
)),
|
|
||||||
WithClusterNodes(
|
|
||||||
ClusterNode{"slave-dc1", dbSlaveDC1, 1},
|
|
||||||
ClusterNode{"master-dc1", dbMaster, 1},
|
|
||||||
ClusterNode{"slave-dc2", dbSlaveDC2, 2},
|
|
||||||
ClusterNode{"drmaster1-dc2", dbDRMaster, 0},
|
|
||||||
),
|
|
||||||
WithClusterOptions(
|
|
||||||
hasql.WithUpdateInterval[Querier](2*time.Second),
|
|
||||||
hasql.WithUpdateTimeout[Querier](1*time.Second),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err = c.WaitForNodes(tctx, hasql.Primary, hasql.Standby); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
node1Name := ""
|
|
||||||
fmt.Printf("check for Standby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.Standby), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node1Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "slave-dc1" != node1Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "slave-dc1", node1Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
node2Name := ""
|
|
||||||
fmt.Printf("check for PreferStandby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node2Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "slave-dc1" != node2Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "slave-dc1", node2Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
node3Name := ""
|
|
||||||
fmt.Printf("check for PreferPrimary\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferPrimary), "SELECT node_name as name"); row.Err() != nil {
|
|
||||||
t.Fatal(row.Err())
|
|
||||||
} else if err = row.Scan(&node3Name); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if "master-dc1" != node3Name {
|
|
||||||
t.Fatalf("invalid node name %s != %s", "master-dc1", node3Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*`).WillReturnRows(sqlmock.NewRows([]string{"role"}).RowError(1, fmt.Errorf("row error")))
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
fmt.Printf("check for PreferStandby\n")
|
|
||||||
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() == nil {
|
|
||||||
t.Fatal("must return error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbMasterErr := dbMasterMock.ExpectationsWereMet(); dbMasterErr != nil {
|
|
||||||
t.Error(dbMasterErr)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Querier interface {
|
|
||||||
// Basic connection methods
|
|
||||||
PingContext(ctx context.Context) error
|
|
||||||
Close() error
|
|
||||||
|
|
||||||
// Query methods with context
|
|
||||||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
|
||||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
|
||||||
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
|
||||||
|
|
||||||
// Prepared statements with context
|
|
||||||
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
|
||||||
|
|
||||||
// Transaction management with context
|
|
||||||
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
|
|
||||||
|
|
||||||
Conn(ctx context.Context) (*sql.Conn, error)
|
|
||||||
}
|
|
@@ -1,295 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenDBWithCluster creates a [*sql.DB] that uses the [ClusterQuerier]
|
|
||||||
func OpenDBWithCluster(db ClusterQuerier) (*sql.DB, error) {
|
|
||||||
driver := NewClusterDriver(db)
|
|
||||||
connector, err := driver.OpenConnector("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sql.OpenDB(connector), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterDriver implements [driver.Driver] and driver.Connector for an existing [Querier]
|
|
||||||
type ClusterDriver struct {
|
|
||||||
db ClusterQuerier
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClusterDriver creates a new [driver.Driver] that uses an existing [ClusterQuerier]
|
|
||||||
func NewClusterDriver(db ClusterQuerier) *ClusterDriver {
|
|
||||||
return &ClusterDriver{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open implements [driver.Driver.Open]
|
|
||||||
func (d *ClusterDriver) Open(name string) (driver.Conn, error) {
|
|
||||||
return d.Connect(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenConnector implements [driver.DriverContext.OpenConnector]
|
|
||||||
func (d *ClusterDriver) OpenConnector(name string) (driver.Connector, error) {
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect implements [driver.Connector.Connect]
|
|
||||||
func (d *ClusterDriver) Connect(ctx context.Context) (driver.Conn, error) {
|
|
||||||
conn, err := d.db.Conn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &dbConn{conn: conn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Driver implements [driver.Connector.Driver]
|
|
||||||
func (d *ClusterDriver) Driver() driver.Driver {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbConn implements driver.Conn with both context and legacy methods
|
|
||||||
type dbConn struct {
|
|
||||||
conn *sql.Conn
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare implements [driver.Conn.Prepare] (legacy method)
|
|
||||||
func (c *dbConn) Prepare(query string) (driver.Stmt, error) {
|
|
||||||
return c.PrepareContext(context.Background(), query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareContext implements [driver.ConnPrepareContext.PrepareContext]
|
|
||||||
func (c *dbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
stmt, err := c.conn.PrepareContext(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbStmt{stmt: stmt}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements [driver.Execer.Exec] (legacy method)
|
|
||||||
func (c *dbConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return c.ExecContext(context.Background(), query, namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecContext implements [driver.ExecerContext.ExecContext]
|
|
||||||
func (c *dbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// Convert driver.NamedValue to any
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.ExecContext(ctx, query, interfaceArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements [driver.Queryer.Query] (legacy method)
|
|
||||||
func (c *dbConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return c.QueryContext(context.Background(), query, namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements [driver.QueryerContext.QueryContext]
|
|
||||||
func (c *dbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// Convert driver.NamedValue to any
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := c.conn.QueryContext(ctx, query, interfaceArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbRows{rows: rows}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin implements [driver.Conn.Begin] (legacy method)
|
|
||||||
func (c *dbConn) Begin() (driver.Tx, error) {
|
|
||||||
return c.BeginTx(context.Background(), driver.TxOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeginTx implements [driver.ConnBeginTx.BeginTx]
|
|
||||||
func (c *dbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
sqlOpts := &sql.TxOptions{
|
|
||||||
Isolation: sql.IsolationLevel(opts.Isolation),
|
|
||||||
ReadOnly: opts.ReadOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := c.conn.BeginTx(ctx, sqlOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbTx{tx: tx}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping implements [driver.Pinger.Ping]
|
|
||||||
func (c *dbConn) Ping(ctx context.Context) error {
|
|
||||||
return c.conn.PingContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Conn.Close]
|
|
||||||
func (c *dbConn) Close() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid implements [driver.Validator.IsValid]
|
|
||||||
func (c *dbConn) IsValid() bool {
|
|
||||||
// Ping with a short timeout to check if the connection is still valid
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return c.conn.PingContext(ctx) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbStmt implements [driver.Stmt] with both context and legacy methods
|
|
||||||
type dbStmt struct {
|
|
||||||
stmt *sql.Stmt
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Stmt.Close]
|
|
||||||
func (s *dbStmt) Close() error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.stmt.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Stmt.NumInput]
|
|
||||||
func (s *dbStmt) NumInput() int {
|
|
||||||
return -1 // Number of parameters is unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements [driver.Stmt.Exec] (legacy method)
|
|
||||||
func (s *dbStmt) Exec(args []driver.Value) (driver.Result, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return s.ExecContext(context.Background(), namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecContext implements [driver.StmtExecContext.ExecContext]
|
|
||||||
func (s *dbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
return s.stmt.ExecContext(ctx, interfaceArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements [driver.Stmt.Query] (legacy method)
|
|
||||||
func (s *dbStmt) Query(args []driver.Value) (driver.Rows, error) {
|
|
||||||
namedArgs := make([]driver.NamedValue, len(args))
|
|
||||||
for i, value := range args {
|
|
||||||
namedArgs[i] = driver.NamedValue{Value: value}
|
|
||||||
}
|
|
||||||
return s.QueryContext(context.Background(), namedArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements [driver.StmtQueryContext.QueryContext]
|
|
||||||
func (s *dbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
interfaceArgs := make([]any, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
interfaceArgs[i] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := s.stmt.QueryContext(ctx, interfaceArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dbRows{rows: rows}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbRows implements [driver.Rows]
|
|
||||||
type dbRows struct {
|
|
||||||
rows *sql.Rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// Columns implements [driver.Rows.Columns]
|
|
||||||
func (r *dbRows) Columns() []string {
|
|
||||||
cols, err := r.rows.Columns()
|
|
||||||
if err != nil {
|
|
||||||
// This shouldn't happen if the query was successful
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return cols
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements [driver.Rows.Close]
|
|
||||||
func (r *dbRows) Close() error {
|
|
||||||
return r.rows.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next implements [driver.Rows.Next]
|
|
||||||
func (r *dbRows) Next(dest []driver.Value) error {
|
|
||||||
if !r.rows.Next() {
|
|
||||||
if err := r.rows.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a slice of interfaces to scan into
|
|
||||||
scanArgs := make([]any, len(dest))
|
|
||||||
for i := range scanArgs {
|
|
||||||
scanArgs[i] = &dest[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.rows.Scan(scanArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbTx implements [driver.Tx]
|
|
||||||
type dbTx struct {
|
|
||||||
tx *sql.Tx
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit implements [driver.Tx.Commit]
|
|
||||||
func (t *dbTx) Commit() error {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
return t.tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rollback implements [driver.Tx.Rollback]
|
|
||||||
func (t *dbTx) Rollback() error {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
return t.tx.Rollback()
|
|
||||||
}
|
|
@@ -1,141 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDriver(t *testing.T) {
|
|
||||||
dbMaster, dbMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbMaster.Close()
|
|
||||||
dbMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(1, 0)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("master-dc1"))
|
|
||||||
|
|
||||||
dbDRMaster, dbDRMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbDRMaster.Close()
|
|
||||||
dbDRMasterMock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 40)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster1-dc2"))
|
|
||||||
|
|
||||||
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("drmaster"))
|
|
||||||
|
|
||||||
dbSlaveDC1, dbSlaveDC1Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC1.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
dbSlaveDC2, dbSlaveDC2Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dbSlaveDC2.Close()
|
|
||||||
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
|
|
||||||
sqlmock.NewRowsWithColumnDefinition(
|
|
||||||
sqlmock.NewColumn("role").OfType("int8", 0),
|
|
||||||
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
|
|
||||||
AddRow(2, 50)).
|
|
||||||
RowsWillBeClosed().
|
|
||||||
WithoutArgs()
|
|
||||||
|
|
||||||
dbSlaveDC2Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
|
|
||||||
sqlmock.NewRows([]string{"name"}).
|
|
||||||
AddRow("slave-dc1"))
|
|
||||||
|
|
||||||
tctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
c, err := NewCluster[Querier](
|
|
||||||
WithClusterContext(tctx),
|
|
||||||
WithClusterNodeChecker(hasql.PostgreSQLChecker),
|
|
||||||
WithClusterNodePicker(NewCustomPicker[Querier](
|
|
||||||
CustomPickerMaxLag(100),
|
|
||||||
)),
|
|
||||||
WithClusterNodes(
|
|
||||||
ClusterNode{"slave-dc1", dbSlaveDC1, 1},
|
|
||||||
ClusterNode{"master-dc1", dbMaster, 1},
|
|
||||||
ClusterNode{"slave-dc2", dbSlaveDC2, 2},
|
|
||||||
ClusterNode{"drmaster1-dc2", dbDRMaster, 0},
|
|
||||||
),
|
|
||||||
WithClusterOptions(
|
|
||||||
hasql.WithUpdateInterval[Querier](2*time.Second),
|
|
||||||
hasql.WithUpdateTimeout[Querier](1*time.Second),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err = c.WaitForNodes(tctx, hasql.Primary, hasql.Standby); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := OpenDBWithCluster(c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use context methods
|
|
||||||
row := db.QueryRowContext(NodeStateCriterion(t.Context(), hasql.Primary), "SELECT node_name as name")
|
|
||||||
if err = row.Err(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeName := ""
|
|
||||||
if err = row.Scan(&nodeName); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodeName != "master-dc1" {
|
|
||||||
t.Fatalf("invalid node_name %s != %s", "master-dc1", nodeName)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrClusterChecker = errors.New("cluster node checker required")
|
|
||||||
ErrClusterDiscoverer = errors.New("cluster node discoverer required")
|
|
||||||
ErrClusterPicker = errors.New("cluster node picker required")
|
|
||||||
ErrorNoAliveNodes = errors.New("cluster no alive nodes")
|
|
||||||
)
|
|
@@ -1,110 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClusterOptions contains cluster specific options
|
|
||||||
type ClusterOptions struct {
|
|
||||||
NodeChecker hasql.NodeChecker
|
|
||||||
NodePicker hasql.NodePicker[Querier]
|
|
||||||
NodeDiscoverer hasql.NodeDiscoverer[Querier]
|
|
||||||
Options []hasql.ClusterOpt[Querier]
|
|
||||||
Context context.Context
|
|
||||||
Retries int
|
|
||||||
NodePriority map[string]int32
|
|
||||||
NodeStateCriterion hasql.NodeStateCriterion
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterOption apply cluster options to ClusterOptions
|
|
||||||
type ClusterOption func(*ClusterOptions)
|
|
||||||
|
|
||||||
// WithClusterNodeChecker pass hasql.NodeChecker to cluster options
|
|
||||||
func WithClusterNodeChecker(c hasql.NodeChecker) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeChecker = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodePicker pass hasql.NodePicker to cluster options
|
|
||||||
func WithClusterNodePicker(p hasql.NodePicker[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodePicker = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodeDiscoverer pass hasql.NodeDiscoverer to cluster options
|
|
||||||
func WithClusterNodeDiscoverer(d hasql.NodeDiscoverer[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeDiscoverer = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRetries retry count on other nodes in case of error
|
|
||||||
func WithRetries(n int) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Retries = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterContext pass context.Context to cluster options and used for checks
|
|
||||||
func WithClusterContext(ctx context.Context) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Context = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterOptions pass hasql.ClusterOpt
|
|
||||||
func WithClusterOptions(opts ...hasql.ClusterOpt[Querier]) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.Options = append(o.Options, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodeStateCriterion pass default hasql.NodeStateCriterion
|
|
||||||
func WithClusterNodeStateCriterion(c hasql.NodeStateCriterion) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
o.NodeStateCriterion = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClusterNode struct {
|
|
||||||
Name string
|
|
||||||
DB Querier
|
|
||||||
Priority int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClusterNodes create cluster with static NodeDiscoverer
|
|
||||||
func WithClusterNodes(cns ...ClusterNode) ClusterOption {
|
|
||||||
return func(o *ClusterOptions) {
|
|
||||||
nodes := make([]*hasql.Node[Querier], 0, len(cns))
|
|
||||||
if o.NodePriority == nil {
|
|
||||||
o.NodePriority = make(map[string]int32, len(cns))
|
|
||||||
}
|
|
||||||
for _, cn := range cns {
|
|
||||||
nodes = append(nodes, hasql.NewNode(cn.Name, cn.DB))
|
|
||||||
if cn.Priority == 0 {
|
|
||||||
cn.Priority = math.MaxInt32
|
|
||||||
}
|
|
||||||
o.NodePriority[cn.Name] = cn.Priority
|
|
||||||
}
|
|
||||||
o.NodeDiscoverer = hasql.NewStaticNodeDiscoverer(nodes...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type nodeStateCriterionKey struct{}
|
|
||||||
|
|
||||||
// NodeStateCriterion inject hasql.NodeStateCriterion to context
|
|
||||||
func NodeStateCriterion(ctx context.Context, c hasql.NodeStateCriterion) context.Context {
|
|
||||||
return context.WithValue(ctx, nodeStateCriterionKey{}, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) getNodeStateCriterion(ctx context.Context) hasql.NodeStateCriterion {
|
|
||||||
if v, ok := ctx.Value(nodeStateCriterionKey{}).(hasql.NodeStateCriterion); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return c.options.NodeStateCriterion
|
|
||||||
}
|
|
@@ -1,113 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.yandex/hasql/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// compile time guard
|
|
||||||
var _ hasql.NodePicker[Querier] = (*CustomPicker[Querier])(nil)
|
|
||||||
|
|
||||||
// CustomPickerOptions holds options to pick nodes
|
|
||||||
type CustomPickerOptions struct {
|
|
||||||
MaxLag int
|
|
||||||
Priority map[string]int32
|
|
||||||
Retries int
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomPickerOption func apply option to CustomPickerOptions
|
|
||||||
type CustomPickerOption func(*CustomPickerOptions)
|
|
||||||
|
|
||||||
// CustomPickerMaxLag specifies max lag for which node can be used
|
|
||||||
func CustomPickerMaxLag(n int) CustomPickerOption {
|
|
||||||
return func(o *CustomPickerOptions) {
|
|
||||||
o.MaxLag = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCustomPicker creates new node picker
|
|
||||||
func NewCustomPicker[T Querier](opts ...CustomPickerOption) *CustomPicker[Querier] {
|
|
||||||
options := CustomPickerOptions{}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return &CustomPicker[Querier]{opts: options}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomPicker holds node picker options
|
|
||||||
type CustomPicker[T Querier] struct {
|
|
||||||
opts CustomPickerOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// PickNode used to return specific node
|
|
||||||
func (p *CustomPicker[T]) PickNode(cnodes []hasql.CheckedNode[T]) hasql.CheckedNode[T] {
|
|
||||||
for _, n := range cnodes {
|
|
||||||
fmt.Printf("node %s\n", n.Node.String())
|
|
||||||
}
|
|
||||||
return cnodes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CustomPicker[T]) getPriority(nodeName string) int32 {
|
|
||||||
if prio, ok := p.opts.Priority[nodeName]; ok {
|
|
||||||
return prio
|
|
||||||
}
|
|
||||||
return math.MaxInt32 // Default to lowest priority
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareNodes used to sort nodes
|
|
||||||
func (p *CustomPicker[T]) CompareNodes(a, b hasql.CheckedNode[T]) int {
|
|
||||||
// Get replication lag values
|
|
||||||
aLag := a.Info.(interface{ ReplicationLag() int }).ReplicationLag()
|
|
||||||
bLag := b.Info.(interface{ ReplicationLag() int }).ReplicationLag()
|
|
||||||
|
|
||||||
// First check that lag lower then MaxLag
|
|
||||||
if aLag > p.opts.MaxLag && bLag > p.opts.MaxLag {
|
|
||||||
return 0 // both are equal
|
|
||||||
}
|
|
||||||
|
|
||||||
// If one node exceeds MaxLag and the other doesn't, prefer the one that doesn't
|
|
||||||
if aLag > p.opts.MaxLag {
|
|
||||||
return 1 // b is better
|
|
||||||
}
|
|
||||||
if bLag > p.opts.MaxLag {
|
|
||||||
return -1 // a is better
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get node priorities
|
|
||||||
aPrio := p.getPriority(a.Node.String())
|
|
||||||
bPrio := p.getPriority(b.Node.String())
|
|
||||||
|
|
||||||
// if both priority equals
|
|
||||||
if aPrio == bPrio {
|
|
||||||
// First compare by replication lag
|
|
||||||
if aLag < bLag {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if aLag > bLag {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// If replication lag is equal, compare by latency
|
|
||||||
aLatency := a.Info.(interface{ Latency() time.Duration }).Latency()
|
|
||||||
bLatency := b.Info.(interface{ Latency() time.Duration }).Latency()
|
|
||||||
|
|
||||||
if aLatency < bLatency {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if aLatency > bLatency {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// If lag and latency is equal
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// If priorities are different, prefer the node with lower priority value
|
|
||||||
if aPrio < bPrio {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
@@ -1,8 +1,19 @@
|
|||||||
// Package codec is an interface for encoding messages
|
// Package codec is an interface for encoding messages
|
||||||
package codec
|
package codec // import "go.unistack.org/micro/v3/codec"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message types
|
||||||
|
const (
|
||||||
|
Error MessageType = iota
|
||||||
|
Request
|
||||||
|
Response
|
||||||
|
Event
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -13,63 +24,63 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// DefaultMaxMsgSize specifies how much data codec can handle
|
||||||
|
DefaultMaxMsgSize = 1024 * 1024 * 4 // 4Mb
|
||||||
// DefaultCodec is the global default codec
|
// DefaultCodec is the global default codec
|
||||||
DefaultCodec = NewCodec()
|
DefaultCodec = NewCodec()
|
||||||
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
||||||
DefaultTagName = "codec"
|
DefaultTagName = "codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Codec encodes/decodes various types of messages.
|
// MessageType specifies message type for codec
|
||||||
|
type MessageType int
|
||||||
|
|
||||||
|
// Codec encodes/decodes various types of messages used within micro.
|
||||||
|
// ReadHeader and ReadBody are called in pairs to read requests/responses
|
||||||
|
// from the connection. Close is called when finished with the
|
||||||
|
// connection. ReadBody may be called with a nil argument to force the
|
||||||
|
// body to be read and discarded.
|
||||||
type Codec interface {
|
type Codec interface {
|
||||||
|
ReadHeader(r io.Reader, m *Message, mt MessageType) error
|
||||||
|
ReadBody(r io.Reader, v interface{}) error
|
||||||
|
Write(w io.Writer, m *Message, v interface{}) error
|
||||||
Marshal(v interface{}, opts ...Option) ([]byte, error)
|
Marshal(v interface{}, opts ...Option) ([]byte, error)
|
||||||
Unmarshal(b []byte, v interface{}, opts ...Option) error
|
Unmarshal(b []byte, v interface{}, opts ...Option) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodecV2 interface {
|
// Message represents detailed information about
|
||||||
Marshal(buf []byte, v interface{}, opts ...Option) ([]byte, error)
|
// the communication, likely followed by the body.
|
||||||
Unmarshal(buf []byte, v interface{}, opts ...Option) error
|
// In the case of an error, body may be nil.
|
||||||
String() string
|
type Message struct {
|
||||||
|
Header metadata.Metadata
|
||||||
|
Target string
|
||||||
|
Method string
|
||||||
|
Endpoint string
|
||||||
|
Error string
|
||||||
|
ID string
|
||||||
|
Body []byte
|
||||||
|
Type MessageType
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMessage is a raw encoded JSON value.
|
// NewMessage creates new codec message
|
||||||
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
func NewMessage(t MessageType) *Message {
|
||||||
type RawMessage []byte
|
return &Message{Type: t, Header: metadata.New(0)}
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON returns m as the JSON encoding of m.
|
// MarshalAppend calls codec.Marshal(v) and returns the data appended to buf.
|
||||||
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
// If codec implements MarshalAppend, that is called instead.
|
||||||
if m == nil {
|
func MarshalAppend(buf []byte, c Codec, v interface{}, opts ...Option) ([]byte, error) {
|
||||||
return []byte("null"), nil
|
if nc, ok := c.(interface {
|
||||||
} else if len(*m) == 0 {
|
MarshalAppend([]byte, interface{}, ...Option) ([]byte, error)
|
||||||
return []byte("null"), nil
|
}); ok {
|
||||||
|
return nc.MarshalAppend(buf, v, opts...)
|
||||||
}
|
}
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON sets *m to a copy of data.
|
mbuf, err := c.Marshal(v, opts...)
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
if err != nil {
|
||||||
if m == nil {
|
return nil, err
|
||||||
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
|
||||||
}
|
}
|
||||||
*m = append((*m)[0:0], data...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML returns m as the JSON encoding of m.
|
return append(buf, mbuf...), nil
|
||||||
func (m *RawMessage) MarshalYAML() ([]byte, error) {
|
|
||||||
if m == nil {
|
|
||||||
return []byte("null"), nil
|
|
||||||
} else if len(*m) == 0 {
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
return *m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML sets *m to a copy of data.
|
|
||||||
func (m *RawMessage) UnmarshalYAML(data []byte) error {
|
|
||||||
if m == nil {
|
|
||||||
return errors.New("RawMessage UnmarshalYAML on nil pointer")
|
|
||||||
}
|
|
||||||
*m = append((*m)[0:0], data...)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@@ -15,15 +15,6 @@ func FromContext(ctx context.Context) (Codec, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustContext returns codec from context
|
|
||||||
func MustContext(ctx context.Context) Codec {
|
|
||||||
c, ok := FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
panic("missing codec")
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContext put codec in context
|
// NewContext put codec in context
|
||||||
func NewContext(ctx context.Context, c Codec) context.Context {
|
func NewContext(ctx context.Context, c Codec) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -20,17 +20,6 @@ func (m *Frame) UnmarshalJSON(data []byte) error {
|
|||||||
return m.Unmarshal(data)
|
return m.Unmarshal(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML returns frame data
|
|
||||||
func (m *Frame) MarshalYAML() ([]byte, error) {
|
|
||||||
return m.Marshal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML set frame data
|
|
||||||
func (m *Frame) UnmarshalYAML(data []byte) error {
|
|
||||||
m.Data = append((m.Data)[0:0], data...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProtoMessage noop func
|
// ProtoMessage noop func
|
||||||
func (m *Frame) ProtoMessage() {}
|
func (m *Frame) ProtoMessage() {}
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ syntax = "proto3";
|
|||||||
package micro.codec;
|
package micro.codec;
|
||||||
|
|
||||||
option cc_enable_arenas = true;
|
option cc_enable_arenas = true;
|
||||||
option go_package = "go.unistack.org/micro/v4/codec;codec";
|
option go_package = "go.unistack.org/micro/v3/codec;codec";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option java_outer_classname = "MicroCodec";
|
option java_outer_classname = "MicroCodec";
|
||||||
option java_package = "micro.codec";
|
option java_package = "micro.codec";
|
||||||
|
@@ -2,14 +2,70 @@ package codec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
codecpb "go.unistack.org/micro-proto/v4/codec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type noopCodec struct {
|
type noopCodec struct {
|
||||||
opts Options
|
opts Options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *noopCodec) ReadHeader(conn io.Reader, m *Message, t MessageType) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *noopCodec) ReadBody(conn io.Reader, b interface{}) error {
|
||||||
|
// read bytes
|
||||||
|
buf, err := io.ReadAll(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := b.(type) {
|
||||||
|
case *string:
|
||||||
|
*v = string(buf)
|
||||||
|
case *[]byte:
|
||||||
|
*v = buf
|
||||||
|
case *Frame:
|
||||||
|
v.Data = buf
|
||||||
|
default:
|
||||||
|
return json.Unmarshal(buf, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *noopCodec) Write(conn io.Writer, m *Message, b interface{}) error {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var v []byte
|
||||||
|
switch vb := b.(type) {
|
||||||
|
case *Frame:
|
||||||
|
v = vb.Data
|
||||||
|
case string:
|
||||||
|
v = []byte(vb)
|
||||||
|
case *string:
|
||||||
|
v = []byte(*vb)
|
||||||
|
case *[]byte:
|
||||||
|
v = *vb
|
||||||
|
case []byte:
|
||||||
|
v = vb
|
||||||
|
default:
|
||||||
|
var err error
|
||||||
|
v, err = json.Marshal(vb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := conn.Write(v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *noopCodec) String() string {
|
func (c *noopCodec) String() string {
|
||||||
return "noop"
|
return "noop"
|
||||||
}
|
}
|
||||||
@@ -35,8 +91,8 @@ func (c *noopCodec) Marshal(v interface{}, opts ...Option) ([]byte, error) {
|
|||||||
return ve, nil
|
return ve, nil
|
||||||
case *Frame:
|
case *Frame:
|
||||||
return ve.Data, nil
|
return ve.Data, nil
|
||||||
case *codecpb.Frame:
|
case *Message:
|
||||||
return ve.Data, nil
|
return ve.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(v)
|
return json.Marshal(v)
|
||||||
@@ -59,8 +115,8 @@ func (c *noopCodec) Unmarshal(d []byte, v interface{}, opts ...Option) error {
|
|||||||
case *Frame:
|
case *Frame:
|
||||||
ve.Data = d
|
ve.Data = d
|
||||||
return nil
|
return nil
|
||||||
case *codecpb.Frame:
|
case *Message:
|
||||||
ve.Data = d
|
ve.Body = d
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,9 +3,9 @@ package codec
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v4/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -23,8 +23,15 @@ type Options struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// TagName specifies tag name in struct to control codec
|
// TagName specifies tag name in struct to control codec
|
||||||
TagName string
|
TagName string
|
||||||
// Flatten specifies that struct must be analyzed for flatten tag
|
// MaxMsgSize specifies max messages size that reads by codec
|
||||||
Flatten bool
|
MaxMsgSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxMsgSize sets the max message size
|
||||||
|
func MaxMsgSize(n int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.MaxMsgSize = n
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagName sets the codec tag name in struct
|
// TagName sets the codec tag name in struct
|
||||||
@@ -34,13 +41,6 @@ func TagName(n string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten enables checking for flatten tag name
|
|
||||||
func Flatten(b bool) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Flatten = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger sets the logger
|
// Logger sets the logger
|
||||||
func Logger(l logger.Logger) Option {
|
func Logger(l logger.Logger) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -65,12 +65,12 @@ func Meter(m meter.Meter) Option {
|
|||||||
// NewOptions returns new options
|
// NewOptions returns new options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
TagName: DefaultTagName,
|
MaxMsgSize: DefaultMaxMsgSize,
|
||||||
Flatten: false,
|
TagName: DefaultTagName,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
// Package config is an interface for dynamic configuration.
|
// Package config is an interface for dynamic configuration.
|
||||||
package config
|
package config // import "go.unistack.org/micro/v3/config"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -50,13 +50,6 @@ type Config interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
|
||||||
FuncLoad func(ctx context.Context, opts ...LoadOption) error
|
|
||||||
HookLoad func(next FuncLoad) FuncLoad
|
|
||||||
FuncSave func(ctx context.Context, opts ...SaveOption) error
|
|
||||||
HookSave func(next FuncSave) FuncSave
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watcher is the config watcher
|
// Watcher is the config watcher
|
||||||
type Watcher interface {
|
type Watcher interface {
|
||||||
// Next blocks until update happens or error returned
|
// Next blocks until update happens or error returned
|
||||||
@@ -131,29 +124,11 @@ func Validate(ctx context.Context, cfg interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultBeforeLoad default func that runs before config Load
|
// DefaultAfterLoad default func that runs after config load
|
||||||
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
|
|
||||||
for _, fn := range c.Options().BeforeLoad {
|
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
|
||||||
c.Options().Logger.Error(ctx, c.String()+" BeforeLoad error", err)
|
|
||||||
if !c.Options().AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// DefaultAfterLoad default func that runs after config Load
|
|
||||||
DefaultAfterLoad = func(ctx context.Context, c Config) error {
|
DefaultAfterLoad = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().AfterLoad {
|
for _, fn := range c.Options().AfterLoad {
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Error(ctx, c.String()+" AfterLoad error", err)
|
c.Options().Logger.Errorf(ctx, "%s AfterLoad err: %v", c.String(), err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -161,29 +136,11 @@ var (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// DefaultBeforeSave default func that runs befora config Save
|
// DefaultAfterSave default func that runs after config save
|
||||||
DefaultBeforeSave = func(ctx context.Context, c Config) error {
|
|
||||||
for _, fn := range c.Options().BeforeSave {
|
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
|
||||||
c.Options().Logger.Error(ctx, c.String()+" BeforeSave error", err)
|
|
||||||
if !c.Options().AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// DefaultAfterSave default func that runs after config Save
|
|
||||||
DefaultAfterSave = func(ctx context.Context, c Config) error {
|
DefaultAfterSave = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().AfterSave {
|
for _, fn := range c.Options().AfterSave {
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Error(ctx, c.String()+" AfterSave error", err)
|
c.Options().Logger.Errorf(ctx, "%s AfterSave err: %v", c.String(), err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -191,14 +148,11 @@ var (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// DefaultBeforeInit default func that runs befora config Init
|
// DefaultBeforeLoad default func that runs before config load
|
||||||
DefaultBeforeInit = func(ctx context.Context, c Config) error {
|
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().BeforeInit {
|
for _, fn := range c.Options().BeforeLoad {
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Error(ctx, c.String()+" BeforeInit error", err)
|
c.Options().Logger.Errorf(ctx, "%s BeforeLoad err: %v", c.String(), err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -206,14 +160,11 @@ var (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// DefaultAfterInit default func that runs after config Init
|
// DefaultBeforeSave default func that runs befora config save
|
||||||
DefaultAfterInit = func(ctx context.Context, c Config) error {
|
DefaultBeforeSave = func(ctx context.Context, c Config) error {
|
||||||
for _, fn := range c.Options().AfterSave {
|
for _, fn := range c.Options().BeforeSave {
|
||||||
if fn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := fn(ctx, c); err != nil {
|
if err := fn(ctx, c); err != nil {
|
||||||
c.Options().Logger.Error(ctx, c.String()+" AfterInit error", err)
|
c.Options().Logger.Errorf(ctx, "%s BeforeSave err: %v", c.String(), err)
|
||||||
if !c.Options().AllowFail {
|
if !c.Options().AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -15,15 +15,6 @@ func FromContext(ctx context.Context) (Config, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustContext returns store from context
|
|
||||||
func MustContext(ctx context.Context) Config {
|
|
||||||
c, ok := FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
panic("missing config")
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContext put store in context
|
// NewContext put store in context
|
||||||
func NewContext(ctx context.Context, c Config) context.Context {
|
func NewContext(ctx context.Context, c Config) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -7,18 +7,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"github.com/imdario/mergo"
|
||||||
"github.com/google/uuid"
|
rutil "go.unistack.org/micro/v3/util/reflect"
|
||||||
"go.unistack.org/micro/v4/options"
|
mtime "go.unistack.org/micro/v3/util/time"
|
||||||
mid "go.unistack.org/micro/v4/util/id"
|
|
||||||
rutil "go.unistack.org/micro/v4/util/reflect"
|
|
||||||
mtime "go.unistack.org/micro/v4/util/time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type defaultConfig struct {
|
type defaultConfig struct {
|
||||||
funcLoad FuncLoad
|
opts Options
|
||||||
funcSave FuncSave
|
|
||||||
opts Options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Options() Options {
|
func (c *defaultConfig) Options() Options {
|
||||||
@@ -29,42 +24,11 @@ func (c *defaultConfig) Init(opts ...Option) error {
|
|||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&c.opts)
|
o(&c.opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DefaultBeforeInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.funcLoad = c.fnLoad
|
|
||||||
c.funcSave = c.fnSave
|
|
||||||
|
|
||||||
c.opts.Hooks.EachPrev(func(hook options.Hook) {
|
|
||||||
switch h := hook.(type) {
|
|
||||||
case HookLoad:
|
|
||||||
c.funcLoad = h(c.funcLoad)
|
|
||||||
case HookSave:
|
|
||||||
c.funcSave = h(c.funcSave)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := DefaultAfterInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
||||||
return c.funcLoad(ctx, opts...)
|
if err := DefaultBeforeLoad(ctx, c); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
func (c *defaultConfig) fnLoad(ctx context.Context, opts ...LoadOption) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if c.opts.SkipLoad != nil && c.opts.SkipLoad(ctx, c) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,20 +51,21 @@ func (c *defaultConfig) fnLoad(ctx context.Context, opts ...LoadOption) error {
|
|||||||
if !c.opts.AllowFail {
|
if !c.opts.AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
return DefaultAfterLoad(ctx, c)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
|
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
|
||||||
err = mergo.Merge(dst, src, mopts...)
|
err = mergo.Merge(dst, src, mopts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !c.opts.AllowFail {
|
if err != nil {
|
||||||
return err
|
c.opts.Logger.Errorf(ctx, "default load error: %v", err)
|
||||||
|
if !c.opts.AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
if err := DefaultAfterLoad(ctx, c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,20 +116,6 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
value.Set(reflect.ValueOf(v))
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
switch val {
|
|
||||||
case "micro:generate uuid":
|
|
||||||
uid, err := uuid.NewRandom()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
val = uid.String()
|
|
||||||
case "micro:generate id":
|
|
||||||
uid, err := mid.New()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
val = uid
|
|
||||||
}
|
|
||||||
value.Set(reflect.ValueOf(val))
|
value.Set(reflect.ValueOf(val))
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
v, err := strconv.ParseFloat(val, 32)
|
v, err := strconv.ParseFloat(val, 32)
|
||||||
@@ -210,7 +161,7 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
value.Set(reflect.ValueOf(v))
|
||||||
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v4/util/time":
|
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v3/util/time":
|
||||||
v, err := mtime.ParseDuration(val)
|
v, err := mtime.ParseDuration(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -254,7 +205,6 @@ func fillValue(value reflect.Value, val string) error {
|
|||||||
}
|
}
|
||||||
value.Set(reflect.ValueOf(v))
|
value.Set(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,14 +268,6 @@ func fillValues(valueOf reflect.Value, tname string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
||||||
return c.funcSave(ctx, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *defaultConfig) fnSave(ctx context.Context, opts ...SaveOption) error {
|
|
||||||
if c.opts.SkipSave != nil && c.opts.SkipSave(ctx, c) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := DefaultBeforeSave(ctx, c); err != nil {
|
if err := DefaultBeforeSave(ctx, c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -345,7 +287,7 @@ func (c *defaultConfig) Name() string {
|
|||||||
return c.opts.Name
|
return c.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Watch(_ context.Context, _ ...WatchOption) (Watcher, error) {
|
func (c *defaultConfig) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
||||||
return nil, ErrWatcherNotImplemented
|
return nil, ErrWatcherNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,9 +297,5 @@ func NewConfig(opts ...Option) Config {
|
|||||||
if len(options.StructTag) == 0 {
|
if len(options.StructTag) == 0 {
|
||||||
options.StructTag = "default"
|
options.StructTag = "default"
|
||||||
}
|
}
|
||||||
c := &defaultConfig{opts: options}
|
return &defaultConfig{opts: options}
|
||||||
c.funcLoad = c.fnLoad
|
|
||||||
c.funcSave = c.fnSave
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
@@ -3,26 +3,20 @@ package config_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/config"
|
"go.unistack.org/micro/v3/config"
|
||||||
mtime "go.unistack.org/micro/v4/util/time"
|
mtime "go.unistack.org/micro/v3/util/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cfg struct {
|
type cfg struct {
|
||||||
MapValue map[string]bool `default:"key1=true,key2=false"`
|
StringValue string `default:"string_value"`
|
||||||
StructValue *cfgStructValue
|
IgnoreValue string `json:"-"`
|
||||||
|
StructValue *cfgStructValue
|
||||||
StringValue string `default:"string_value"`
|
IntValue int `default:"99"`
|
||||||
IgnoreValue string `json:"-"`
|
|
||||||
UUIDValue string `default:"micro:generate uuid"`
|
|
||||||
IDValue string `default:"micro:generate id"`
|
|
||||||
|
|
||||||
DurationValue time.Duration `default:"10s"`
|
DurationValue time.Duration `default:"10s"`
|
||||||
MDurationValue mtime.Duration `default:"10s"`
|
MDurationValue mtime.Duration `default:"10s"`
|
||||||
IntValue int `default:"99"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type cfgStructValue struct {
|
type cfgStructValue struct {
|
||||||
@@ -43,35 +37,6 @@ func (c *cfgStructValue) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type testHook struct {
|
|
||||||
f bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHook) Load(fn config.FuncLoad) config.FuncLoad {
|
|
||||||
return func(ctx context.Context, opts ...config.LoadOption) error {
|
|
||||||
t.f = true
|
|
||||||
return fn(ctx, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHook(t *testing.T) {
|
|
||||||
h := &testHook{}
|
|
||||||
|
|
||||||
c := config.NewConfig(config.Struct(h), config.Hooks(config.HookLoad(h.Load)))
|
|
||||||
|
|
||||||
if err := c.Init(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Load(context.TODO()); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.f {
|
|
||||||
t.Fatal("hook not works")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefault(t *testing.T) {
|
func TestDefault(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
conf := &cfg{IntValue: 10}
|
conf := &cfg{IntValue: 10}
|
||||||
@@ -102,19 +67,6 @@ func TestDefault(t *testing.T) {
|
|||||||
if conf.StringValue != "after_load" {
|
if conf.StringValue != "after_load" {
|
||||||
t.Fatal("AfterLoad option not working")
|
t.Fatal("AfterLoad option not working")
|
||||||
}
|
}
|
||||||
if len(conf.MapValue) != 2 {
|
|
||||||
t.Fatalf("map value invalid: %#+v\n", conf.MapValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.UUIDValue == "" {
|
|
||||||
t.Fatalf("uuid value empty")
|
|
||||||
} else if len(conf.UUIDValue) != 36 {
|
|
||||||
t.Fatalf("uuid value invalid: %s", conf.UUIDValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.IDValue == "" {
|
|
||||||
t.Fatalf("id value empty")
|
|
||||||
}
|
|
||||||
_ = conf
|
_ = conf
|
||||||
// t.Logf("%#+v\n", conf)
|
// t.Logf("%#+v\n", conf)
|
||||||
}
|
}
|
||||||
@@ -134,13 +86,3 @@ func TestValidate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_SizeOf(t *testing.T) {
|
|
||||||
st := cfg{}
|
|
||||||
|
|
||||||
tVal := reflect.TypeOf(st)
|
|
||||||
for i := 0; i < tVal.NumField(); i++ {
|
|
||||||
field := tVal.Field(i)
|
|
||||||
fmt.Printf("Field: %s, Offset: %d, Size: %d\n", field.Name, field.Offset, field.Type.Size())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -4,11 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v4/options"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options hold the config options
|
// Options hold the config options
|
||||||
@@ -29,26 +28,14 @@ type Options struct {
|
|||||||
Name string
|
Name string
|
||||||
// StructTag name
|
// StructTag name
|
||||||
StructTag string
|
StructTag string
|
||||||
// BeforeSave contains slice of funcs that runs before Save
|
// BeforeSave contains slice of funcs that runs before save
|
||||||
BeforeSave []func(context.Context, Config) error
|
BeforeSave []func(context.Context, Config) error
|
||||||
// AfterSave contains slice of funcs that runs after Save
|
// AfterLoad contains slice of funcs that runs after load
|
||||||
AfterSave []func(context.Context, Config) error
|
|
||||||
// BeforeLoad contains slice of funcs that runs before Load
|
|
||||||
BeforeLoad []func(context.Context, Config) error
|
|
||||||
// AfterLoad contains slice of funcs that runs after Load
|
|
||||||
AfterLoad []func(context.Context, Config) error
|
AfterLoad []func(context.Context, Config) error
|
||||||
// BeforeInit contains slice of funcs that runs before Init
|
// BeforeLoad contains slice of funcs that runs before load
|
||||||
BeforeInit []func(context.Context, Config) error
|
BeforeLoad []func(context.Context, Config) error
|
||||||
// AfterInit contains slice of funcs that runs after Init
|
// AfterSave contains slice of funcs that runs after save
|
||||||
AfterInit []func(context.Context, Config) error
|
AfterSave []func(context.Context, Config) error
|
||||||
|
|
||||||
// SkipLoad runs only if condition returns true
|
|
||||||
SkipLoad func(context.Context, Config) bool
|
|
||||||
// SkipSave runs only if condition returns true
|
|
||||||
SkipSave func(context.Context, Config) bool
|
|
||||||
// Hooks can be run before/after config Save/Load
|
|
||||||
Hooks options.Hooks
|
|
||||||
|
|
||||||
// AllowFail flag to allow fail in config source
|
// AllowFail flag to allow fail in config source
|
||||||
AllowFail bool
|
AllowFail bool
|
||||||
}
|
}
|
||||||
@@ -77,9 +64,9 @@ type LoadOption func(o *LoadOptions)
|
|||||||
// LoadOptions struct
|
// LoadOptions struct
|
||||||
type LoadOptions struct {
|
type LoadOptions struct {
|
||||||
Struct interface{}
|
Struct interface{}
|
||||||
Context context.Context
|
|
||||||
Override bool
|
Override bool
|
||||||
Append bool
|
Append bool
|
||||||
|
Context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoadOptions create LoadOptions struct with provided opts
|
// NewLoadOptions create LoadOptions struct with provided opts
|
||||||
@@ -144,20 +131,6 @@ func AllowFail(b bool) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeInit run funcs before config Init
|
|
||||||
func BeforeInit(fn ...func(context.Context, Config) error) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.BeforeInit = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterInit run funcs after config Init
|
|
||||||
func AfterInit(fn ...func(context.Context, Config) error) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.AfterInit = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeLoad run funcs before config load
|
// BeforeLoad run funcs before config load
|
||||||
func BeforeLoad(fn ...func(context.Context, Config) error) Option {
|
func BeforeLoad(fn ...func(context.Context, Config) error) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -280,10 +253,10 @@ func WatchCoalesce(b bool) WatchOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WatchInterval specifies min and max time.Duration for pulling changes
|
// WatchInterval specifies min and max time.Duration for pulling changes
|
||||||
func WatchInterval(minTime, maxTime time.Duration) WatchOption {
|
func WatchInterval(min, max time.Duration) WatchOption {
|
||||||
return func(o *WatchOptions) {
|
return func(o *WatchOptions) {
|
||||||
o.MinInterval = minTime
|
o.MinInterval = min
|
||||||
o.MaxInterval = maxTime
|
o.MaxInterval = max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,10 +266,3 @@ func WatchStruct(src interface{}) WatchOption {
|
|||||||
o.Struct = src
|
o.Struct = src
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks sets hook runs before action
|
|
||||||
func Hooks(h ...options.Hook) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Hooks = append(o.Hooks, h...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
157
database/dsn.go
157
database/dsn.go
@@ -1,157 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidDSNAddr = errors.New("invalid dsn addr")
|
|
||||||
ErrInvalidDSNUnescaped = errors.New("dsn must be escaped")
|
|
||||||
ErrInvalidDSNNoSlash = errors.New("dsn must contains slash")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Scheme string
|
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
Database string
|
|
||||||
Params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) FormatDSN() string {
|
|
||||||
var s strings.Builder
|
|
||||||
|
|
||||||
if len(cfg.Scheme) > 0 {
|
|
||||||
s.WriteString(cfg.Scheme + "://")
|
|
||||||
}
|
|
||||||
// [username[:password]@]
|
|
||||||
if len(cfg.Username) > 0 {
|
|
||||||
s.WriteString(cfg.Username)
|
|
||||||
if len(cfg.Password) > 0 {
|
|
||||||
s.WriteByte(':')
|
|
||||||
s.WriteString(url.PathEscape(cfg.Password))
|
|
||||||
}
|
|
||||||
s.WriteByte('@')
|
|
||||||
}
|
|
||||||
|
|
||||||
// [host:port]
|
|
||||||
if len(cfg.Host) > 0 {
|
|
||||||
s.WriteString(cfg.Host)
|
|
||||||
if len(cfg.Port) > 0 {
|
|
||||||
s.WriteByte(':')
|
|
||||||
s.WriteString(cfg.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /dbname
|
|
||||||
s.WriteByte('/')
|
|
||||||
s.WriteString(url.PathEscape(cfg.Database))
|
|
||||||
|
|
||||||
for i := 0; i < len(cfg.Params); i += 2 {
|
|
||||||
if i == 0 {
|
|
||||||
s.WriteString("?")
|
|
||||||
} else {
|
|
||||||
s.WriteString("&")
|
|
||||||
}
|
|
||||||
s.WriteString(cfg.Params[i])
|
|
||||||
s.WriteString("=")
|
|
||||||
s.WriteString(cfg.Params[i+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseDSN(dsn string) (*Config, error) {
|
|
||||||
cfg := &Config{}
|
|
||||||
|
|
||||||
// [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN]
|
|
||||||
// Find last '/' that goes before dbname
|
|
||||||
foundSlash := false
|
|
||||||
for i := len(dsn) - 1; i >= 0; i-- {
|
|
||||||
if dsn[i] == '/' {
|
|
||||||
foundSlash = true
|
|
||||||
var j, k int
|
|
||||||
|
|
||||||
// left part is empty if i <= 0
|
|
||||||
if i > 0 {
|
|
||||||
// Find the first ':' in dsn
|
|
||||||
for j = i; j >= 0; j-- {
|
|
||||||
if dsn[j] == ':' {
|
|
||||||
cfg.Scheme = dsn[0:j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [username[:password]@][host]
|
|
||||||
// Find the last '@' in dsn[:i]
|
|
||||||
for j = i; j >= 0; j-- {
|
|
||||||
if dsn[j] == '@' {
|
|
||||||
// username[:password]
|
|
||||||
// Find the second ':' in dsn[:j]
|
|
||||||
for k = 0; k < j; k++ {
|
|
||||||
if dsn[k] == ':' {
|
|
||||||
if cfg.Scheme == dsn[:k] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
cfg.Password, err = url.PathUnescape(dsn[k+1 : j])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.Username = dsn[len(cfg.Scheme)+3 : k]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k = j + 1; k < i; k++ {
|
|
||||||
if dsn[k] == ':' {
|
|
||||||
cfg.Host = dsn[j+1 : k]
|
|
||||||
cfg.Port = dsn[k+1 : i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbname[?param1=value1&...¶mN=valueN]
|
|
||||||
// Find the first '?' in dsn[i+1:]
|
|
||||||
for j = i + 1; j < len(dsn); j++ {
|
|
||||||
if dsn[j] == '?' {
|
|
||||||
parts := strings.Split(dsn[j+1:], "&")
|
|
||||||
cfg.Params = make([]string, 0, len(parts)*2)
|
|
||||||
for _, p := range parts {
|
|
||||||
k, v, found := strings.Cut(p, "=")
|
|
||||||
if !found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg.Params = append(cfg.Params, k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
dbname := dsn[i+1 : j]
|
|
||||||
if cfg.Database, err = url.PathUnescape(dbname); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundSlash && len(dsn) > 0 {
|
|
||||||
return nil, ErrInvalidDSNNoSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseDSN(t *testing.T) {
|
|
||||||
cfg, err := ParseDSN("postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if cfg.Password != "p@ssword#" {
|
|
||||||
t.Fatalf("parsing error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatDSN(t *testing.T) {
|
|
||||||
src := "postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2"
|
|
||||||
cfg, err := ParseDSN(src)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
dst, err := url.PathUnescape(cfg.FormatDSN())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if src != dst {
|
|
||||||
t.Fatalf("\n%s\n%s", src, dst)
|
|
||||||
}
|
|
||||||
}
|
|
158
errors/errors.go
158
errors/errors.go
@@ -1,20 +1,14 @@
|
|||||||
// Package errors provides a way to return detailed information
|
// Package errors provides a way to return detailed information
|
||||||
// for an RPC request error. The error is normally JSON encoded.
|
// for an RPC request error. The error is normally JSON encoded.
|
||||||
package errors
|
package errors // import "go.unistack.org/micro/v3/errors"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -44,20 +38,6 @@ var (
|
|||||||
ErrGatewayTimeout = &Error{Code: 504}
|
ErrGatewayTimeout = &Error{Code: 504}
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProblemContentType = "application/problem+json"
|
|
||||||
|
|
||||||
type Problem struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Detail string `json:"detail,omitempty"`
|
|
||||||
Instance string `json:"instance,omitempty"`
|
|
||||||
Errors []struct {
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Detail string `json:"detail,omitempty"`
|
|
||||||
} `json:"errors,omitempty"`
|
|
||||||
Status int `json:"status,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error type
|
// Error type
|
||||||
type Error struct {
|
type Error struct {
|
||||||
// ID holds error id or service, usually someting like my_service or id
|
// ID holds error id or service, usually someting like my_service or id
|
||||||
@@ -276,10 +256,6 @@ func CodeIn(err interface{}, codes ...int32) bool {
|
|||||||
|
|
||||||
// FromError try to convert go error to *Error
|
// FromError try to convert go error to *Error
|
||||||
func FromError(err error) *Error {
|
func FromError(err error) *Error {
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if verr, ok := err.(*Error); ok && verr != nil {
|
if verr, ok := err.(*Error); ok && verr != nil {
|
||||||
return verr
|
return verr
|
||||||
}
|
}
|
||||||
@@ -364,135 +340,3 @@ func addslashes(str string) string {
|
|||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type retryableError struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retryable returns error that can be retried later
|
|
||||||
func Retryable(err error) error {
|
|
||||||
return &retryableError{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
type IsRetryableFunc func(error) bool
|
|
||||||
|
|
||||||
var (
|
|
||||||
RetrayableOracleErrors = []IsRetryableFunc{
|
|
||||||
func(err error) bool {
|
|
||||||
errmsg := err.Error()
|
|
||||||
switch {
|
|
||||||
case strings.Contains(errmsg, `ORA-`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `can not assign`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `can't assign`):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RetrayablePostgresErrors = []IsRetryableFunc{
|
|
||||||
func(err error) bool {
|
|
||||||
errmsg := err.Error()
|
|
||||||
switch {
|
|
||||||
case strings.Contains(errmsg, `number of field descriptions must equal number of`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `not a pointer`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `values, but dst struct has only`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `struct doesn't have corresponding row field`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `cannot find field`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `cannot scan`) || strings.Contains(errmsg, `cannot convert`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(errmsg, `failed to connect to`):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RetryableMicroErrors = []IsRetryableFunc{
|
|
||||||
func(err error) bool {
|
|
||||||
switch verr := err.(type) {
|
|
||||||
case *Error:
|
|
||||||
switch verr.Code {
|
|
||||||
case 401, 403, 408, 500, 501, 502, 503, 504:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case *retryableError:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RetryableGoErrors = []IsRetryableFunc{
|
|
||||||
func(err error) bool {
|
|
||||||
switch verr := err.(type) {
|
|
||||||
case interface{ SafeToRetry() bool }:
|
|
||||||
return verr.SafeToRetry()
|
|
||||||
case interface{ Timeout() bool }:
|
|
||||||
return verr.Timeout()
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
|
|
||||||
return true
|
|
||||||
case errors.Is(err, context.DeadlineExceeded):
|
|
||||||
return true
|
|
||||||
case errors.Is(err, io.ErrClosedPipe), errors.Is(err, io.ErrShortBuffer), errors.Is(err, io.ErrShortWrite):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RetryableGrpcErrors = []IsRetryableFunc{
|
|
||||||
func(err error) bool {
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch st.Code() {
|
|
||||||
case codes.Unavailable, codes.ResourceExhausted:
|
|
||||||
return true
|
|
||||||
case codes.DeadlineExceeded:
|
|
||||||
return true
|
|
||||||
case codes.Internal:
|
|
||||||
switch {
|
|
||||||
case strings.Contains(st.Message(), `transport: received the unexpected content-type "text/html; charset=UTF-8"`):
|
|
||||||
return true
|
|
||||||
case strings.Contains(st.Message(), io.ErrUnexpectedEOF.Error()):
|
|
||||||
return true
|
|
||||||
case strings.Contains(st.Message(), `stream terminated by RST_STREAM with error code: INTERNAL_ERROR`):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unwrap provides error wrapping
|
|
||||||
func (e *retryableError) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the error string
|
|
||||||
func (e *retryableError) Error() string {
|
|
||||||
if e.err == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRetryable checks error for ability to retry later
|
|
||||||
func IsRetryable(err error, fns ...IsRetryableFunc) bool {
|
|
||||||
for _, fn := range fns {
|
|
||||||
if ok := fn(err); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
@@ -17,7 +17,7 @@ syntax = "proto3";
|
|||||||
package micro.errors;
|
package micro.errors;
|
||||||
|
|
||||||
option cc_enable_arenas = true;
|
option cc_enable_arenas = true;
|
||||||
option go_package = "go.unistack.org/micro/v4/errors;errors";
|
option go_package = "go.unistack.org/micro/v3/errors;errors";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option java_outer_classname = "MicroErrors";
|
option java_outer_classname = "MicroErrors";
|
||||||
option java_package = "micro.errors";
|
option java_package = "micro.errors";
|
||||||
|
@@ -2,19 +2,12 @@ package errors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
er "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsRetrayable(t *testing.T) {
|
|
||||||
err := fmt.Errorf("ORA-")
|
|
||||||
if !IsRetryable(err, RetrayableOracleErrors...) {
|
|
||||||
t.Fatalf("IsRetrayable not works")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshalJSON(t *testing.T) {
|
func TestMarshalJSON(t *testing.T) {
|
||||||
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
||||||
_, err := json.Marshal(e)
|
_, err := json.Marshal(e)
|
||||||
@@ -26,7 +19,7 @@ func TestMarshalJSON(t *testing.T) {
|
|||||||
func TestEmpty(t *testing.T) {
|
func TestEmpty(t *testing.T) {
|
||||||
msg := "test"
|
msg := "test"
|
||||||
var err *Error
|
var err *Error
|
||||||
err = FromError(errors.New(msg))
|
err = FromError(fmt.Errorf(msg))
|
||||||
if err.Detail != msg {
|
if err.Detail != msg {
|
||||||
t.Fatalf("invalid error %v", err)
|
t.Fatalf("invalid error %v", err)
|
||||||
}
|
}
|
||||||
@@ -42,7 +35,7 @@ func TestFromError(t *testing.T) {
|
|||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
}
|
}
|
||||||
err = errors.New(err.Error())
|
err = er.New(err.Error())
|
||||||
merr = FromError(err)
|
merr = FromError(err)
|
||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
@@ -57,7 +50,7 @@ func TestEqual(t *testing.T) {
|
|||||||
t.Fatal("errors must be equal")
|
t.Fatal("errors must be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
err3 := errors.New("my test err")
|
err3 := er.New("my test err")
|
||||||
if Equal(err1, err3) {
|
if Equal(err1, err3) {
|
||||||
t.Fatal("errors must be not equal")
|
t.Fatal("errors must be not equal")
|
||||||
}
|
}
|
||||||
|
27
event.go
Normal file
27
event.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package micro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event is used to publish messages to a topic
|
||||||
|
type Event interface {
|
||||||
|
// Publish publishes a message to the event topic
|
||||||
|
Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type event struct {
|
||||||
|
c client.Client
|
||||||
|
topic string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEvent creates a new event publisher
|
||||||
|
func NewEvent(topic string, c client.Client) Event {
|
||||||
|
return &event{c, topic}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *event) Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error {
|
||||||
|
return e.c.Publish(ctx, e.c.NewMessage(e.topic, msg), opts...)
|
||||||
|
}
|
@@ -1,5 +1,3 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package flow
|
package flow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@@ -8,7 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDeps(t *testing.T) {
|
func TestDeps(t *testing.T) {
|
||||||
t.Skip()
|
|
||||||
d := &dag.AcyclicGraph{}
|
d := &dag.AcyclicGraph{}
|
||||||
|
|
||||||
v0 := d.Add(&node{"v0"})
|
v0 := d.Add(&node{"v0"})
|
||||||
|
476
flow/default.go
476
flow/default.go
@@ -1,19 +1,17 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package flow
|
package flow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/heimdalr/dag"
|
"github.com/silas/dag"
|
||||||
"go.unistack.org/micro/v4/client"
|
"go.unistack.org/micro/v3/client"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/store"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
"go.unistack.org/micro/v4/util/id"
|
"go.unistack.org/micro/v3/store"
|
||||||
|
"go.unistack.org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type microFlow struct {
|
type microFlow struct {
|
||||||
@@ -22,7 +20,7 @@ type microFlow struct {
|
|||||||
|
|
||||||
type microWorkflow struct {
|
type microWorkflow struct {
|
||||||
opts Options
|
opts Options
|
||||||
g *dag.DAG
|
g *dag.AcyclicGraph
|
||||||
steps map[string]Step
|
steps map[string]Step
|
||||||
id string
|
id string
|
||||||
status Status
|
status Status
|
||||||
@@ -34,20 +32,20 @@ func (w *microWorkflow) ID() string {
|
|||||||
return w.id
|
return w.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Steps() ([][]Step, error) {
|
||||||
|
return w.getSteps("", false)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Status() Status {
|
func (w *microWorkflow) Status() Status {
|
||||||
return w.status
|
return w.status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
||||||
var err error
|
|
||||||
w.Lock()
|
w.Lock()
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
w.steps[s.String()] = s
|
w.steps[s.String()] = s
|
||||||
if _, err = w.g.AddVertex(s); err != nil {
|
w.g.Add(s)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -56,13 +54,18 @@ func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ErrStepNotExists
|
return ErrStepNotExists
|
||||||
}
|
}
|
||||||
if err = w.g.AddEdge(src.String(), dst.String()); err != nil {
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.g.ReduceTransitively()
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
|
w.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -71,11 +74,10 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
|
|||||||
// TODO: handle case when some step requires or required by removed step
|
// TODO: handle case when some step requires or required by removed step
|
||||||
|
|
||||||
w.Lock()
|
w.Lock()
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
delete(w.steps, s.String())
|
delete(w.steps, s.String())
|
||||||
w.g.DeleteVertex(s.String())
|
w.g.Remove(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -84,34 +86,91 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ErrStepNotExists
|
return ErrStepNotExists
|
||||||
}
|
}
|
||||||
w.g.AddEdge(src.String(), dst.String())
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.g.ReduceTransitively()
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
|
w.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) getSteps(start string, reverse bool) ([][]Step, error) {
|
||||||
|
var steps [][]Step
|
||||||
|
var root dag.Vertex
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fn := func(n dag.Vertex, idx int) error {
|
||||||
|
if idx == 0 {
|
||||||
|
steps = make([][]Step, 1)
|
||||||
|
steps[0] = make([]Step, 0, 1)
|
||||||
|
} else if idx >= len(steps) {
|
||||||
|
tsteps := make([][]Step, idx+1)
|
||||||
|
copy(tsteps, steps)
|
||||||
|
steps = tsteps
|
||||||
|
steps[idx] = make([]Step, 0, 1)
|
||||||
|
}
|
||||||
|
steps[idx] = append(steps[idx], n.(Step))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if start != "" {
|
||||||
|
var ok bool
|
||||||
|
w.RLock()
|
||||||
|
root, ok = w.steps[start]
|
||||||
|
w.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStepNotExists
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root, err = w.g.Root()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reverse {
|
||||||
|
err = w.g.SortedReverseDepthFirstWalk([]dag.Vertex{root}, fn)
|
||||||
|
} else {
|
||||||
|
err = w.g.SortedDepthFirstWalk([]dag.Vertex{root}, fn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Abort(ctx context.Context, id string) error {
|
func (w *microWorkflow) Abort(ctx context.Context, id string) error {
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
|
||||||
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Suspend(ctx context.Context, id string) error {
|
func (w *microWorkflow) Suspend(ctx context.Context, id string) error {
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
|
||||||
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())})
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Resume(ctx context.Context, id string) error {
|
func (w *microWorkflow) Resume(ctx context.Context, id string) error {
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
|
||||||
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())})
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
|
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
|
||||||
w.Lock()
|
w.Lock()
|
||||||
if !w.init {
|
if !w.init {
|
||||||
w.g.ReduceTransitively()
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
w.g.TransitiveReduction()
|
||||||
w.init = true
|
w.init = true
|
||||||
}
|
}
|
||||||
w.Unlock()
|
w.Unlock()
|
||||||
@@ -121,11 +180,26 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
stepStore := store.NewNamespaceStore(w.opts.Store, "steps"+w.opts.Store.Options().Separator+eid)
|
||||||
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+eid)
|
||||||
|
|
||||||
options := NewExecuteOptions(opts...)
|
options := NewExecuteOptions(opts...)
|
||||||
|
|
||||||
|
steps, err := w.getSteps(options.Start, options.Reverse)
|
||||||
|
if err != nil {
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
cherr := make(chan error, 1)
|
||||||
|
chstatus := make(chan Status, 1)
|
||||||
|
|
||||||
|
nctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
nopts := make([]ExecuteOption, 0, len(opts)+5)
|
nopts := make([]ExecuteOption, 0, len(opts)+5)
|
||||||
|
|
||||||
nopts = append(nopts,
|
nopts = append(nopts,
|
||||||
@@ -135,274 +209,143 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
|
|||||||
ExecuteMeter(w.opts.Meter),
|
ExecuteMeter(w.opts.Meter),
|
||||||
)
|
)
|
||||||
nopts = append(nopts, opts...)
|
nopts = append(nopts, opts...)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
if werr := workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
w.opts.Logger.Error(ctx, "store error: %v", werr)
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
return eid, werr
|
return eid, werr
|
||||||
}
|
}
|
||||||
|
for idx := range steps {
|
||||||
var startID string
|
for nidx := range steps[idx] {
|
||||||
if options.Start == "" {
|
cstep := steps[idx][nidx]
|
||||||
mp := w.g.GetRoots()
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
||||||
if len(mp) != 1 {
|
return eid, werr
|
||||||
return eid, ErrStepNotExists
|
|
||||||
}
|
|
||||||
for k := range mp {
|
|
||||||
startID = k
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for k, v := range w.g.GetVertices() {
|
|
||||||
if v == options.Start {
|
|
||||||
startID = k
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if startID == "" {
|
go func() {
|
||||||
return eid, ErrStepNotExists
|
for idx := range steps {
|
||||||
}
|
for nidx := range steps[idx] {
|
||||||
|
wStatus := &codec.Frame{}
|
||||||
if options.Async {
|
if werr := workflowStore.Read(w.opts.Context, "status", wStatus); werr != nil {
|
||||||
go w.handleWorkflow(startID, nopts...)
|
cherr <- werr
|
||||||
return eid, nil
|
return
|
||||||
}
|
|
||||||
|
|
||||||
return eid, w.handleWorkflow(startID, nopts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) error {
|
|
||||||
w.RLock()
|
|
||||||
defer w.RUnlock()
|
|
||||||
|
|
||||||
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
|
||||||
// workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
|
||||||
|
|
||||||
// Get IDs of all descendant vertices.
|
|
||||||
flowIDs, errDes := w.g.GetDescendants(startID)
|
|
||||||
if errDes != nil {
|
|
||||||
return errDes
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputChannels provides for input channels for each of the descendant vertices (+ the start-vertex).
|
|
||||||
inputChannels := make(map[string]chan FlowResult, len(flowIDs)+1)
|
|
||||||
|
|
||||||
// Iterate vertex IDs and create an input channel for each of them and a single
|
|
||||||
// output channel for leaves. Note, this "pre-flight" is needed to ensure we
|
|
||||||
// really have an input channel regardless of how we traverse the tree and spawn
|
|
||||||
// workers.
|
|
||||||
leafCount := 0
|
|
||||||
|
|
||||||
for id := range flowIDs {
|
|
||||||
|
|
||||||
// Get all parents of this vertex.
|
|
||||||
parents, errPar := w.g.GetParents(id)
|
|
||||||
if errPar != nil {
|
|
||||||
return errPar
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a buffered input channel that has capacity for all parent results.
|
|
||||||
inputChannels[id] = make(chan FlowResult, len(parents))
|
|
||||||
|
|
||||||
if ok, err := w.g.IsLeaf(id); ok && err == nil {
|
|
||||||
leafCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputChannel caries the results of leaf vertices.
|
|
||||||
outputChannel := make(chan FlowResult, leafCount)
|
|
||||||
|
|
||||||
// To also process the start vertex and to have its results being passed to its
|
|
||||||
// children, add it to the vertex IDs. Also add an input channel for the start
|
|
||||||
// vertex and feed the inputs to this channel.
|
|
||||||
flowIDs[startID] = struct{}{}
|
|
||||||
inputChannels[startID] = make(chan FlowResult, len(inputs))
|
|
||||||
for _, i := range inputs {
|
|
||||||
inputChannels[startID] <- i
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
// Iterate all vertex IDs (now incl. start vertex) and handle each worker (incl.
|
|
||||||
// inputs and outputs) in a separate goroutine.
|
|
||||||
for id := range flowIDs {
|
|
||||||
|
|
||||||
// Get all children of this vertex that later need to be notified. Note, we
|
|
||||||
// collect all children before the goroutine to be able to release the read
|
|
||||||
// lock as early as possible.
|
|
||||||
children, errChildren := w.g.GetChildren(id)
|
|
||||||
if errChildren != nil {
|
|
||||||
return errChildren
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember to wait for this goroutine.
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func(id string) {
|
|
||||||
// Get this vertex's input channel.
|
|
||||||
// Note, only concurrent read here, which is fine.
|
|
||||||
c := inputChannels[id]
|
|
||||||
|
|
||||||
// Await all parent inputs and stuff them into a slice.
|
|
||||||
parentCount := cap(c)
|
|
||||||
parentResults := make([]FlowResult, parentCount)
|
|
||||||
for i := 0; i < parentCount; i++ {
|
|
||||||
parentResults[i] = <-c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the worker.
|
|
||||||
errWorker := callback(w.g, id, parentResults)
|
|
||||||
if errWorker != nil {
|
|
||||||
return errWorker
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send this worker's FlowResult onto all children's input channels or, if it is
|
|
||||||
// a leaf (i.e. no children), send the result onto the output channel.
|
|
||||||
if len(children) > 0 {
|
|
||||||
for child := range children {
|
|
||||||
inputChannels[child] <- flowResult
|
|
||||||
}
|
}
|
||||||
} else {
|
if status := StringStatus[string(wStatus.Data)]; status != StatusRunning {
|
||||||
outputChannel <- flowResult
|
chstatus <- status
|
||||||
}
|
return
|
||||||
|
}
|
||||||
// "Sign off".
|
if w.opts.Logger.V(logger.TraceLevel) {
|
||||||
wg.Done()
|
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
|
||||||
}(id)
|
}
|
||||||
}
|
cstep := steps[idx][nidx]
|
||||||
|
// nolint: nestif
|
||||||
// Wait for all go routines to finish.
|
if len(cstep.Requires()) == 0 {
|
||||||
wg.Wait()
|
wg.Add(1)
|
||||||
|
go func(step Step) {
|
||||||
// Await all leaf vertex results and stuff them into a slice.
|
defer wg.Done()
|
||||||
resultCount := cap(outputChannel)
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil {
|
||||||
results := make([]FlowResult, resultCount)
|
|
||||||
for i := 0; i < resultCount; i++ {
|
|
||||||
results[i] = <-outputChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
go func() {
|
|
||||||
for idx := range steps {
|
|
||||||
for nidx := range steps[idx] {
|
|
||||||
wStatus := &codec.Frame{}
|
|
||||||
if werr := workflowStore.Read(w.opts.Context, "status", wStatus); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if status := StringStatus[string(wStatus.Data)]; status != StatusRunning {
|
|
||||||
chstatus <- status
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if w.opts.Logger.V(logger.TraceLevel) {
|
|
||||||
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
|
|
||||||
}
|
|
||||||
cstep := steps[idx][nidx]
|
|
||||||
// nolint: nestif
|
|
||||||
if len(cstep.Requires()) == 0 {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(step Step) {
|
|
||||||
defer wg.Done()
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rsp, serr := step.Execute(nctx, req, nopts...)
|
|
||||||
if serr != nil {
|
|
||||||
step.SetStatus(StatusFailure)
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
}
|
|
||||||
cherr <- serr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
|
||||||
cherr <- werr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(cstep)
|
|
||||||
wg.Wait()
|
|
||||||
} else {
|
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil {
|
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rsp, serr := cstep.Execute(nctx, req, nopts...)
|
rsp, serr := step.Execute(nctx, req, nopts...)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
cstep.SetStatus(StatusFailure)
|
step.SetStatus(StatusFailure)
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
}
|
}
|
||||||
cherr <- serr
|
cherr <- serr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil {
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil {
|
||||||
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
cherr <- werr
|
cherr <- werr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}(cstep)
|
||||||
|
wg.Wait()
|
||||||
|
} else {
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rsp, serr := cstep.Execute(nctx, req, nopts...)
|
||||||
|
if serr != nil {
|
||||||
|
cstep.SetStatus(StatusFailure)
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
cherr <- serr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if options.Async {
|
|
||||||
return eid, nil
|
|
||||||
}
|
}
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
logger.Tracef(ctx, "wait for finish or error")
|
if options.Async {
|
||||||
select {
|
return eid, nil
|
||||||
case <-nctx.Done():
|
}
|
||||||
err = nctx.Err()
|
|
||||||
case cerr := <-cherr:
|
|
||||||
err = cerr
|
|
||||||
case <-done:
|
|
||||||
close(cherr)
|
|
||||||
case <-chstatus:
|
|
||||||
close(chstatus)
|
|
||||||
return eid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
logger.Tracef(ctx, "wait for finish or error")
|
||||||
case nctx.Err() != nil:
|
select {
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
|
case <-nctx.Done():
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
err = nctx.Err()
|
||||||
}
|
case cerr := <-cherr:
|
||||||
case err == nil:
|
err = cerr
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
case <-done:
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
close(cherr)
|
||||||
}
|
case <-chstatus:
|
||||||
case err != nil:
|
close(chstatus)
|
||||||
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
|
return eid, nil
|
||||||
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
}
|
||||||
}
|
|
||||||
|
switch {
|
||||||
|
case nctx.Err() != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
}
|
}
|
||||||
*/
|
case err == nil:
|
||||||
return err
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
case err != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFlow create new flow
|
// NewFlow create new flow
|
||||||
@@ -442,11 +385,11 @@ func (f *microFlow) WorkflowList(ctx context.Context) ([]Workflow, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
|
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
|
||||||
w := µWorkflow{opts: f.opts, id: id, g: &dag.DAG{}, steps: make(map[string]Step, len(steps))}
|
w := µWorkflow{opts: f.opts, id: id, g: &dag.AcyclicGraph{}, steps: make(map[string]Step, len(steps))}
|
||||||
|
|
||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
w.steps[s.String()] = s
|
w.steps[s.String()] = s
|
||||||
w.g.AddVertex(s)
|
w.g.Add(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range steps {
|
for _, dst := range steps {
|
||||||
@@ -455,11 +398,14 @@ func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrStepNotExists
|
return nil, ErrStepNotExists
|
||||||
}
|
}
|
||||||
w.g.AddEdge(src.String(), dst.String())
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.g.ReduceTransitively()
|
if err := w.g.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
w.init = true
|
w.init = true
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
// Package flow is an interface used for saga pattern microservice workflow
|
// Package flow is an interface used for saga pattern microservice workflow
|
||||||
package flow // import "go.unistack.org/micro/v4/flow"
|
package flow // import "go.unistack.org/micro/v3/flow"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v3/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -125,6 +125,8 @@ type Workflow interface {
|
|||||||
AppendSteps(steps ...Step) error
|
AppendSteps(steps ...Step) error
|
||||||
// Status returns workflow status
|
// Status returns workflow status
|
||||||
Status() Status
|
Status() Status
|
||||||
|
// Steps returns steps slice where parallel steps returned on the same level
|
||||||
|
Steps() ([][]Step, error)
|
||||||
// Suspend suspends execution
|
// Suspend suspends execution
|
||||||
Suspend(ctx context.Context, id string) error
|
Suspend(ctx context.Context, id string) error
|
||||||
// Resume resumes execution
|
// Resume resumes execution
|
||||||
|
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
"go.unistack.org/micro/v3/client"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v4/store"
|
"go.unistack.org/micro/v3/store"
|
||||||
"go.unistack.org/micro/v4/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -123,6 +123,8 @@ type ExecuteOptions struct {
|
|||||||
Start string
|
Start string
|
||||||
// Timeout for execution
|
// Timeout for execution
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
// Reverse execution
|
||||||
|
Reverse bool
|
||||||
// Async enables async execution
|
// Async enables async execution
|
||||||
Async bool
|
Async bool
|
||||||
}
|
}
|
||||||
@@ -165,6 +167,13 @@ func ExecuteContext(ctx context.Context) ExecuteOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteReverse says that dag must be run in reverse order
|
||||||
|
func ExecuteReverse(b bool) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Reverse = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteTimeout pass timeout time.Duration for execution
|
// ExecuteTimeout pass timeout time.Duration for execution
|
||||||
func ExecuteTimeout(td time.Duration) ExecuteOption {
|
func ExecuteTimeout(td time.Duration) ExecuteOption {
|
||||||
return func(o *ExecuteOptions) {
|
return func(o *ExecuteOptions) {
|
||||||
|
@@ -32,7 +32,7 @@ type fsm struct {
|
|||||||
|
|
||||||
// NewFSM creates a new finite state machine having the specified initial state
|
// NewFSM creates a new finite state machine having the specified initial state
|
||||||
// with specified options
|
// with specified options
|
||||||
func NewFSM(opts ...Option) FSM {
|
func NewFSM(opts ...Option) *fsm {
|
||||||
return &fsm{
|
return &fsm{
|
||||||
statesMap: map[string]StateFunc{},
|
statesMap: map[string]StateFunc{},
|
||||||
opts: NewOptions(opts...),
|
opts: NewOptions(opts...),
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package fsm
|
package fsm // import "go.unistack.org/micro/v3/fsm"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFSMStart(t *testing.T) {
|
func TestFSMStart(t *testing.T) {
|
||||||
@@ -17,7 +17,7 @@ func TestFSMStart(t *testing.T) {
|
|||||||
|
|
||||||
wrapper := func(next StateFunc) StateFunc {
|
wrapper := func(next StateFunc) StateFunc {
|
||||||
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||||
sctx = logger.NewContext(sctx, logger.DefaultLogger.Fields("state", s.Name()))
|
sctx = logger.NewContext(sctx, logger.Fields("state", s.Name()))
|
||||||
return next(sctx, s, opts...)
|
return next(sctx, s, opts...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
go.mod
38
go.mod
@@ -1,35 +1,13 @@
|
|||||||
module go.unistack.org/micro/v4
|
module go.unistack.org/micro/v3
|
||||||
|
|
||||||
go 1.24
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.1
|
github.com/google/go-cmp v0.5.7 // indirect
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/imdario/mergo v0.3.13
|
||||||
github.com/KimMachineGun/automemlimit v0.7.0
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/goccy/go-yaml v1.17.1
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/matoous/go-nanoid v1.5.1
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35
|
||||||
github.com/spf13/cast v1.7.1
|
go.unistack.org/micro-proto/v3 v3.3.1
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
go.uber.org/atomic v1.11.0
|
|
||||||
go.uber.org/automaxprocs v1.6.0
|
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0
|
|
||||||
golang.org/x/sync v0.10.0
|
|
||||||
golang.yandex/hasql/v2 v2.1.0
|
|
||||||
google.golang.org/grpc v1.69.4
|
|
||||||
google.golang.org/protobuf v1.36.3
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
|
||||||
golang.org/x/net v0.34.0 // indirect
|
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
215
go.sum
215
go.sum
@@ -1,71 +1,160 @@
|
|||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.0 h1:7G06p/dMSf7G8E6oq+f2uOPuVncFyIlDI/pBWK49u88=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.0/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
|
||||||
|
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||||
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
|
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||||
|
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
|
|
||||||
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35 h1:4mohWoM/UGg1BvFFiqSPRl5uwJY3rVV0HQX0ETqauqQ=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.unistack.org/micro-proto/v3 v3.3.1 h1:nQ0MtWvP2G3QrpOgawVOPhpZZYkq6umTGDqs8FxJYIo=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.unistack.org/micro-proto/v3 v3.3.1/go.mod h1:cwRyv8uInM2I7EbU7O8Fx2Ls3N90Uw9UCCcq4olOdfE=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9lKUk=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.yandex/hasql/v2 v2.1.0 h1:7CaFFWeHoK5TvA+QvZzlKHlIN5sqNpqM8NSrXskZD/k=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.yandex/hasql/v2 v2.1.0/go.mod h1:3Au1AxuJDCTXmS117BpbI6e+70kGWeyLR1qJAH6HdtA=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
@@ -1,117 +0,0 @@
|
|||||||
package metadata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
type wrapper struct {
|
|
||||||
keys []string
|
|
||||||
|
|
||||||
client.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientWrapper(keys ...string) client.Wrapper {
|
|
||||||
return func(c client.Client) client.Client {
|
|
||||||
handler := &wrapper{
|
|
||||||
Client: c,
|
|
||||||
keys: keys,
|
|
||||||
}
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientCallWrapper(keys ...string) client.CallWrapper {
|
|
||||||
return func(fn client.CallFunc) client.CallFunc {
|
|
||||||
return func(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
|
||||||
if keys == nil {
|
|
||||||
return fn(ctx, addr, req, rsp, opts)
|
|
||||||
}
|
|
||||||
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
|
|
||||||
omd, ook := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ook || omd == nil {
|
|
||||||
omd = metadata.New(len(imd))
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
if v := imd.Get(k); v != nil {
|
|
||||||
omd.Set(k, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ook {
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, omd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fn(ctx, addr, req, rsp, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
|
||||||
if w.keys == nil {
|
|
||||||
return w.Client.Call(ctx, req, rsp, opts...)
|
|
||||||
}
|
|
||||||
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
|
|
||||||
omd, ook := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ook || omd == nil {
|
|
||||||
omd = metadata.New(len(imd))
|
|
||||||
}
|
|
||||||
for _, k := range w.keys {
|
|
||||||
if v := imd.Get(k); v != nil {
|
|
||||||
omd.Set(k, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ook {
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, omd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return w.Client.Call(ctx, req, rsp, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
|
||||||
if w.keys == nil {
|
|
||||||
return w.Client.Stream(ctx, req, opts...)
|
|
||||||
}
|
|
||||||
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
|
|
||||||
omd, ook := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ook || omd == nil {
|
|
||||||
omd = metadata.New(len(imd))
|
|
||||||
}
|
|
||||||
for _, k := range w.keys {
|
|
||||||
if v := imd.Get(k); v != nil {
|
|
||||||
omd.Set(k, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ook {
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, omd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return w.Client.Stream(ctx, req, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServerHandlerWrapper(keys ...string) server.HandlerWrapper {
|
|
||||||
return func(fn server.HandlerFunc) server.HandlerFunc {
|
|
||||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
|
||||||
if keys == nil {
|
|
||||||
return fn(ctx, req, rsp)
|
|
||||||
}
|
|
||||||
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
|
|
||||||
omd, ook := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ook || omd == nil {
|
|
||||||
omd = metadata.New(len(imd))
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
if v := imd.Get(k); v != nil {
|
|
||||||
omd.Set(k, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ook {
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, omd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fn(ctx, req, rsp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,63 +0,0 @@
|
|||||||
package recovery
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
|
||||||
"go.unistack.org/micro/v4/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
ServerHandlerFn: DefaultServerHandlerFn,
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
ServerHandlerFn func(context.Context, server.Request, interface{}, error) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(*Options)
|
|
||||||
|
|
||||||
func ServerHandlerFunc(fn func(context.Context, server.Request, interface{}, error) error) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.ServerHandlerFn = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultServerHandlerFn = func(ctx context.Context, req server.Request, rsp interface{}, err error) error {
|
|
||||||
return errors.BadRequest("", "%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var Hook = NewHook()
|
|
||||||
|
|
||||||
type hook struct {
|
|
||||||
opts Options
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHook(opts ...Option) *hook {
|
|
||||||
return &hook{opts: NewOptions(opts...)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
|
|
||||||
return func(ctx context.Context, req server.Request, rsp interface{}) (err error) {
|
|
||||||
defer func() {
|
|
||||||
r := recover()
|
|
||||||
switch verr := r.(type) {
|
|
||||||
case nil:
|
|
||||||
return
|
|
||||||
case error:
|
|
||||||
err = w.opts.ServerHandlerFn(ctx, req, rsp, verr)
|
|
||||||
default:
|
|
||||||
err = w.opts.ServerHandlerFn(ctx, req, rsp, fmt.Errorf("%v", r))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
err = next(ctx, req, rsp)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,103 +0,0 @@
|
|||||||
package requestid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/textproto"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/server"
|
|
||||||
"go.unistack.org/micro/v4/util/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
type XRequestIDKey struct{}
|
|
||||||
|
|
||||||
// DefaultMetadataKey contains metadata key
|
|
||||||
var DefaultMetadataKey = textproto.CanonicalMIMEHeaderKey("x-request-id")
|
|
||||||
|
|
||||||
// DefaultMetadataFunc wil be used if user not provide own func to fill metadata
|
|
||||||
var DefaultMetadataFunc = func(ctx context.Context) (context.Context, error) {
|
|
||||||
var xid string
|
|
||||||
|
|
||||||
cid, cok := ctx.Value(XRequestIDKey{}).(string)
|
|
||||||
if cok && cid != "" {
|
|
||||||
xid = cid
|
|
||||||
}
|
|
||||||
|
|
||||||
imd, iok := metadata.FromIncomingContext(ctx)
|
|
||||||
if !iok || imd == nil {
|
|
||||||
imd = metadata.New(1)
|
|
||||||
ctx = metadata.NewIncomingContext(ctx, imd)
|
|
||||||
}
|
|
||||||
|
|
||||||
omd, ook := metadata.FromOutgoingContext(ctx)
|
|
||||||
if !ook || omd == nil {
|
|
||||||
omd = metadata.New(1)
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, omd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if xid == "" {
|
|
||||||
xid = imd.GetJoined(DefaultMetadataKey)
|
|
||||||
if xid == "" {
|
|
||||||
xid = omd.GetJoined(DefaultMetadataKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if xid == "" {
|
|
||||||
var err error
|
|
||||||
xid, err = id.New()
|
|
||||||
if err != nil {
|
|
||||||
return ctx, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cok {
|
|
||||||
ctx = context.WithValue(ctx, XRequestIDKey{}, xid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !iok {
|
|
||||||
imd.Set(DefaultMetadataKey, xid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ook {
|
|
||||||
omd.Set(DefaultMetadataKey, xid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type hook struct{}
|
|
||||||
|
|
||||||
func NewHook() *hook {
|
|
||||||
return &hook{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
|
|
||||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
|
||||||
var err error
|
|
||||||
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return next(ctx, req, rsp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *hook) ClientCall(next client.FuncCall) client.FuncCall {
|
|
||||||
return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
|
||||||
var err error
|
|
||||||
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return next(ctx, req, rsp, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *hook) ClientStream(next client.FuncStream) client.FuncStream {
|
|
||||||
return func(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
|
||||||
var err error
|
|
||||||
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return next(ctx, req, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
package requestid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultMetadataFunc(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
nctx, err := DefaultMetadataFunc(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
imd, ok := metadata.FromIncomingContext(nctx)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("md missing in incoming context")
|
|
||||||
}
|
|
||||||
omd, ok := metadata.FromOutgoingContext(nctx)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("md missing in outgoing context")
|
|
||||||
}
|
|
||||||
|
|
||||||
iv := imd.Get(DefaultMetadataKey)
|
|
||||||
ov := omd.Get(DefaultMetadataKey)
|
|
||||||
|
|
||||||
if !slices.Equal(iv, ov) {
|
|
||||||
t.Fatalf("missing metadata key value %v != %v", iv, ov)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,51 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate sh -c "go run gen.go > wrap_gen.go"
|
|
||||||
|
|
||||||
// namedValueToValue converts driver arguments of NamedValue format to Value format. Implemented in the same way as in
|
|
||||||
// database/sql ctxutil.go.
|
|
||||||
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
|
|
||||||
dargs := make([]driver.Value, len(named))
|
|
||||||
for n, param := range named {
|
|
||||||
if len(param.Name) > 0 {
|
|
||||||
return nil, errors.New("sql: driver does not support the use of Named Parameters")
|
|
||||||
}
|
|
||||||
dargs[n] = param.Value
|
|
||||||
}
|
|
||||||
return dargs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// namedValueToLabels convert driver arguments to interface{} slice
|
|
||||||
func namedValueToLabels(named []driver.NamedValue) []interface{} {
|
|
||||||
largs := make([]interface{}, 0, len(named)*2)
|
|
||||||
var name string
|
|
||||||
for _, param := range named {
|
|
||||||
if param.Name != "" {
|
|
||||||
name = param.Name
|
|
||||||
} else {
|
|
||||||
name = fmt.Sprintf("$%d", param.Ordinal)
|
|
||||||
}
|
|
||||||
largs = append(largs, fmt.Sprintf("%s=%v", name, param.Value))
|
|
||||||
}
|
|
||||||
return largs
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCallerName get the name of the function A where A() -> B() -> GetFunctionCallerName()
|
|
||||||
func getCallerName() string {
|
|
||||||
pc, _, _, ok := runtime.Caller(3)
|
|
||||||
details := runtime.FuncForPC(pc)
|
|
||||||
var callerName string
|
|
||||||
if ok && details != nil {
|
|
||||||
callerName = details.Name()
|
|
||||||
} else {
|
|
||||||
callerName = labelUnknown
|
|
||||||
}
|
|
||||||
return callerName
|
|
||||||
}
|
|
@@ -1,467 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/hooks/requestid"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ driver.Conn = (*wrapperConn)(nil)
|
|
||||||
_ driver.ConnBeginTx = (*wrapperConn)(nil)
|
|
||||||
_ driver.ConnPrepareContext = (*wrapperConn)(nil)
|
|
||||||
_ driver.Pinger = (*wrapperConn)(nil)
|
|
||||||
_ driver.Validator = (*wrapperConn)(nil)
|
|
||||||
_ driver.Queryer = (*wrapperConn)(nil) // nolint:staticcheck
|
|
||||||
_ driver.QueryerContext = (*wrapperConn)(nil)
|
|
||||||
_ driver.Execer = (*wrapperConn)(nil) // nolint:staticcheck
|
|
||||||
_ driver.ExecerContext = (*wrapperConn)(nil)
|
|
||||||
// _ driver.Connector
|
|
||||||
// _ driver.Driver
|
|
||||||
// _ driver.DriverContext
|
|
||||||
)
|
|
||||||
|
|
||||||
// wrapperConn defines a wrapper for driver.Conn
|
|
||||||
type wrapperConn struct {
|
|
||||||
d *wrapperDriver
|
|
||||||
dname string
|
|
||||||
conn driver.Conn
|
|
||||||
opts Options
|
|
||||||
ctx context.Context
|
|
||||||
//span tracer.Span
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements driver.Conn Close
|
|
||||||
func (w *wrapperConn) Close() error {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Close"}
|
|
||||||
ts := time.Now()
|
|
||||||
err := w.conn.Close()
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Close", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin implements driver.Conn Begin
|
|
||||||
func (w *wrapperConn) Begin() (driver.Tx, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := []string{labelMethod, "Begin"}
|
|
||||||
ts := time.Now()
|
|
||||||
tx, err := w.conn.Begin() // nolint:staticcheck
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Begin", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Begin", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return &wrapperTx{tx: tx, opts: w.opts, ctx: ctx}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeginTx implements driver.ConnBeginTx BeginTx
|
|
||||||
func (w *wrapperConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
nctx, span := w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
span.AddLabels("db.method", "BeginTx")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "BeginTx", labelQuery, name}
|
|
||||||
|
|
||||||
connBeginTx, ok := w.conn.(driver.ConnBeginTx)
|
|
||||||
if !ok {
|
|
||||||
return w.Begin()
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
tx, err := connBeginTx.BeginTx(nctx, opts)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "BeginTx", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "BeginTx", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return &wrapperTx{tx: tx, opts: w.opts, ctx: ctx, span: span}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare implements driver.Conn Prepare
|
|
||||||
func (w *wrapperConn) Prepare(query string) (driver.Stmt, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Prepare", labelQuery, getCallerName()}
|
|
||||||
ts := time.Now()
|
|
||||||
stmt, err := w.conn.Prepare(query)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Prepare", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Prepare", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return wrapStmt(stmt, query, w.opts), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareContext implements driver.ConnPrepareContext PrepareContext
|
|
||||||
func (w *wrapperConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
|
||||||
var nctx context.Context
|
|
||||||
var span tracer.Span
|
|
||||||
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "PrepareContext")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "PrepareContext", labelQuery, name}
|
|
||||||
conn, ok := w.conn.(driver.ConnPrepareContext)
|
|
||||||
if !ok {
|
|
||||||
return w.Prepare(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
stmt, err := conn.PrepareContext(nctx, query)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "PrepareContext", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "PrepareContext", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return wrapStmt(stmt, query, w.opts), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements driver.Execer Exec
|
|
||||||
func (w *wrapperConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Exec", labelQuery, getCallerName()}
|
|
||||||
|
|
||||||
// nolint:staticcheck
|
|
||||||
conn, ok := w.conn.(driver.Execer)
|
|
||||||
if !ok {
|
|
||||||
return nil, driver.ErrSkip
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
res, err := conn.Exec(query, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Exec", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements driver.StmtExecContext ExecContext
|
|
||||||
func (w *wrapperConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
var nctx context.Context
|
|
||||||
var span tracer.Span
|
|
||||||
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "ExecContext")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
defer span.Finish()
|
|
||||||
if len(args) > 0 {
|
|
||||||
span.AddLabels("db.args", fmt.Sprintf("%v", namedValueToLabels(args)))
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "ExecContext", labelQuery, name}
|
|
||||||
|
|
||||||
conn, ok := w.conn.(driver.ExecerContext)
|
|
||||||
if !ok {
|
|
||||||
// nolint:staticcheck
|
|
||||||
return nil, driver.ErrSkip
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
res, err := conn.ExecContext(nctx, query, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "ExecContext", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping implements driver.Pinger Ping
|
|
||||||
func (w *wrapperConn) Ping(ctx context.Context) error {
|
|
||||||
conn, ok := w.conn.(driver.Pinger)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
// fallback path to check db alive
|
|
||||||
pc, err := w.d.Open(w.dname)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return pc.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
var nctx context.Context //nolint:gosimple
|
|
||||||
nctx = ctx
|
|
||||||
/*
|
|
||||||
var span tracer.Span
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "Ping")
|
|
||||||
defer span.Finish()
|
|
||||||
*/
|
|
||||||
labels := []string{labelMethod, "Ping"}
|
|
||||||
ts := time.Now()
|
|
||||||
err := conn.Ping(nctx)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
// span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Ping", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements driver.Queryer Query
|
|
||||||
func (w *wrapperConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
// nolint:staticcheck
|
|
||||||
conn, ok := w.conn.(driver.Queryer)
|
|
||||||
if !ok {
|
|
||||||
return nil, driver.ErrSkip
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := []string{labelMethod, "Query", labelQuery, getCallerName()}
|
|
||||||
ts := time.Now()
|
|
||||||
rows, err := conn.Query(query, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Query", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return rows, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements Driver.QueryerContext QueryContext
|
|
||||||
func (w *wrapperConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
var nctx context.Context
|
|
||||||
var span tracer.Span
|
|
||||||
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "QueryContext")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
defer span.Finish()
|
|
||||||
if len(args) > 0 {
|
|
||||||
span.AddLabels("db.args", fmt.Sprintf("%v", namedValueToLabels(args)))
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "QueryContext", labelQuery, name}
|
|
||||||
conn, ok := w.conn.(driver.QueryerContext)
|
|
||||||
if !ok {
|
|
||||||
return nil, driver.ErrSkip
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
rows, err := conn.QueryContext(nctx, query, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "QueryContext", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return rows, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNamedValue implements driver.NamedValueChecker
|
|
||||||
func (w *wrapperConn) CheckNamedValue(v *driver.NamedValue) error {
|
|
||||||
s, ok := w.conn.(driver.NamedValueChecker)
|
|
||||||
if !ok {
|
|
||||||
return driver.ErrSkip
|
|
||||||
}
|
|
||||||
return s.CheckNamedValue(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid implements driver.Validator
|
|
||||||
func (w *wrapperConn) IsValid() bool {
|
|
||||||
v, ok := w.conn.(driver.Validator)
|
|
||||||
if !ok {
|
|
||||||
return w.conn != nil
|
|
||||||
}
|
|
||||||
return v.IsValid()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapperConn) ResetSession(ctx context.Context) error {
|
|
||||||
s, ok := w.conn.(driver.SessionResetter)
|
|
||||||
if !ok {
|
|
||||||
return driver.ErrSkip
|
|
||||||
}
|
|
||||||
return s.ResetSession(ctx)
|
|
||||||
}
|
|
@@ -1,94 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// _ driver.DriverContext = (*wrapperDriver)(nil)
|
|
||||||
// _ driver.Connector = (*wrapperDriver)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
type conn interface {
|
|
||||||
driver.Pinger
|
|
||||||
driver.Execer
|
|
||||||
driver.ExecerContext
|
|
||||||
driver.Queryer
|
|
||||||
driver.QueryerContext
|
|
||||||
driver.Conn
|
|
||||||
driver.ConnPrepareContext
|
|
||||||
driver.ConnBeginTx
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// wrapperDriver defines a wrapper for driver.Driver
|
|
||||||
type wrapperDriver struct {
|
|
||||||
driver driver.Driver
|
|
||||||
opts Options
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWrapper creates and returns a new SQL driver with passed capabilities
|
|
||||||
func NewWrapper(d driver.Driver, opts ...Option) driver.Driver {
|
|
||||||
return &wrapperDriver{driver: d, opts: NewOptions(opts...), ctx: context.Background()}
|
|
||||||
}
|
|
||||||
|
|
||||||
type wrappedConnector struct {
|
|
||||||
connector driver.Connector
|
|
||||||
// name string
|
|
||||||
opts Options
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWrapperConnector(c driver.Connector, opts ...Option) driver.Connector {
|
|
||||||
return &wrappedConnector{connector: c, opts: NewOptions(opts...), ctx: context.Background()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect implements driver.Driver Connect
|
|
||||||
func (w *wrappedConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
|
||||||
return w.connector.Connect(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Driver implements driver.Driver Driver
|
|
||||||
func (w *wrappedConnector) Driver() driver.Driver {
|
|
||||||
return w.connector.Driver()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Connect implements driver.Driver OpenConnector
|
|
||||||
func (w *wrapperDriver) OpenConnector(name string) (driver.Conn, error) {
|
|
||||||
return &wrapperConnector{driver: w.driver, name: name, opts: w.opts}, nil
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Open implements driver.Driver Open
|
|
||||||
func (w *wrapperDriver) Open(name string) (driver.Conn, error) {
|
|
||||||
// ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Ensure eventual timeout
|
|
||||||
// defer cancel()
|
|
||||||
|
|
||||||
/*
|
|
||||||
connector, err := w.OpenConnector(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return connector.Connect(ctx)
|
|
||||||
*/
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
c, err := w.driver.Open(name)
|
|
||||||
td := time.Since(ts)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled {
|
|
||||||
w.opts.Logger.Log(w.ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(w.ctx, "Open", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
_ = td
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapConn(c, w.opts), nil
|
|
||||||
}
|
|
167
hooks/sql/gen.go
167
hooks/sql/gen.go
@@ -1,167 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var connIfaces = []string{
|
|
||||||
"driver.ConnBeginTx",
|
|
||||||
"driver.ConnPrepareContext",
|
|
||||||
"driver.Execer",
|
|
||||||
"driver.ExecerContext",
|
|
||||||
"driver.NamedValueChecker",
|
|
||||||
"driver.Pinger",
|
|
||||||
"driver.Queryer",
|
|
||||||
"driver.QueryerContext",
|
|
||||||
"driver.SessionResetter",
|
|
||||||
"driver.Validator",
|
|
||||||
}
|
|
||||||
|
|
||||||
var stmtIfaces = []string{
|
|
||||||
"driver.StmtExecContext",
|
|
||||||
"driver.StmtQueryContext",
|
|
||||||
"driver.ColumnConverter",
|
|
||||||
"driver.NamedValueChecker",
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHash(s []string) string {
|
|
||||||
h := md5.New()
|
|
||||||
io.WriteString(h, strings.Join(s, "|"))
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
comboConn := all(connIfaces)
|
|
||||||
|
|
||||||
sort.Slice(comboConn, func(i, j int) bool {
|
|
||||||
return len(comboConn[i]) < len(comboConn[j])
|
|
||||||
})
|
|
||||||
|
|
||||||
comboStmt := all(stmtIfaces)
|
|
||||||
|
|
||||||
sort.Slice(comboStmt, func(i, j int) bool {
|
|
||||||
return len(comboStmt[i]) < len(comboStmt[j])
|
|
||||||
})
|
|
||||||
|
|
||||||
b := bytes.NewBuffer(nil)
|
|
||||||
b.WriteString("// Code generated. DO NOT EDIT.\n\n")
|
|
||||||
b.WriteString("package sql\n\n")
|
|
||||||
b.WriteString(`import "database/sql/driver"`)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString("func wrapConn(dc driver.Conn, opts Options) driver.Conn {\n")
|
|
||||||
b.WriteString("\tc := &wrapperConn{conn: dc, opts: opts}\n")
|
|
||||||
|
|
||||||
for idx := len(comboConn) - 1; idx >= 0; idx-- {
|
|
||||||
ifaces := comboConn[idx]
|
|
||||||
n := len(ifaces)
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h := getHash(ifaces)
|
|
||||||
b.WriteString(fmt.Sprintf("\tif _, ok := dc.(wrapConn%04d_%s); ok {\n", n, h))
|
|
||||||
b.WriteString("\treturn struct {\n")
|
|
||||||
b.WriteString("\t\tdriver.Conn\n")
|
|
||||||
b.WriteString(fmt.Sprintf("\t\t\t%s", strings.Join(ifaces, "\n\t\t\t")))
|
|
||||||
b.WriteString("\t\t\n}{")
|
|
||||||
for idx := range ifaces {
|
|
||||||
if idx > 0 {
|
|
||||||
b.WriteString(", ")
|
|
||||||
b.WriteString("c")
|
|
||||||
} else if idx == 0 {
|
|
||||||
b.WriteString("c")
|
|
||||||
} else {
|
|
||||||
b.WriteString("c")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(", c}\n")
|
|
||||||
b.WriteString("}\n\n")
|
|
||||||
}
|
|
||||||
b.WriteString("return c\n")
|
|
||||||
b.WriteString("}\n")
|
|
||||||
|
|
||||||
for idx := len(comboConn) - 1; idx >= 0; idx-- {
|
|
||||||
ifaces := comboConn[idx]
|
|
||||||
n := len(ifaces)
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h := getHash(ifaces)
|
|
||||||
b.WriteString(fmt.Sprintf("// %s\n", strings.Join(ifaces, "|")))
|
|
||||||
b.WriteString(fmt.Sprintf("type wrapConn%04d_%s interface {\n", n, h))
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
b.WriteString(fmt.Sprintf("\t%s\n", iface))
|
|
||||||
}
|
|
||||||
b.WriteString("}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("func wrapStmt(stmt driver.Stmt, query string, opts Options) driver.Stmt {\n")
|
|
||||||
b.WriteString("\tc := &wrapperStmt{stmt: stmt, query: query, opts: opts}\n")
|
|
||||||
|
|
||||||
for idx := len(comboStmt) - 1; idx >= 0; idx-- {
|
|
||||||
ifaces := comboStmt[idx]
|
|
||||||
n := len(ifaces)
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h := getHash(ifaces)
|
|
||||||
b.WriteString(fmt.Sprintf("\tif _, ok := stmt.(wrapStmt%04d_%s); ok {\n", n, h))
|
|
||||||
b.WriteString("\treturn struct {\n")
|
|
||||||
b.WriteString("\t\tdriver.Stmt\n")
|
|
||||||
b.WriteString(fmt.Sprintf("\t\t\t%s", strings.Join(ifaces, "\n\t\t\t")))
|
|
||||||
b.WriteString("\t\t\n}{")
|
|
||||||
for idx := range ifaces {
|
|
||||||
if idx > 0 {
|
|
||||||
b.WriteString(", ")
|
|
||||||
b.WriteString("c")
|
|
||||||
} else if idx == 0 {
|
|
||||||
b.WriteString("c")
|
|
||||||
} else {
|
|
||||||
b.WriteString("c")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(", c}\n")
|
|
||||||
b.WriteString("}\n\n")
|
|
||||||
}
|
|
||||||
b.WriteString("return c\n")
|
|
||||||
b.WriteString("}\n")
|
|
||||||
|
|
||||||
for idx := len(comboStmt) - 1; idx >= 0; idx-- {
|
|
||||||
ifaces := comboStmt[idx]
|
|
||||||
n := len(ifaces)
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h := getHash(ifaces)
|
|
||||||
b.WriteString(fmt.Sprintf("// %s\n", strings.Join(ifaces, "|")))
|
|
||||||
b.WriteString(fmt.Sprintf("type wrapStmt%04d_%s interface {\n", n, h))
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
b.WriteString(fmt.Sprintf("\t%s\n", iface))
|
|
||||||
}
|
|
||||||
b.WriteString("}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s\n", b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// all returns all combinations for a given string array.
|
|
||||||
func all[T any](set []T) (subsets [][]T) {
|
|
||||||
length := uint(len(set))
|
|
||||||
for subsetBits := 1; subsetBits < (1 << length); subsetBits++ {
|
|
||||||
var subset []T
|
|
||||||
for object := uint(0); object < length; object++ {
|
|
||||||
if (subsetBits>>object)&1 == 1 {
|
|
||||||
subset = append(subset, set[object])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subsets = append(subsets, subset)
|
|
||||||
}
|
|
||||||
return subsets
|
|
||||||
}
|
|
@@ -1,172 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
|
||||||
"go.unistack.org/micro/v4/meter"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// DefaultMeterStatsInterval holds default stats interval
|
|
||||||
DefaultMeterStatsInterval = 5 * time.Second
|
|
||||||
// DefaultLoggerObserver used to prepare labels for logger
|
|
||||||
DefaultLoggerObserver = func(ctx context.Context, method string, query string, td time.Duration, err error) []interface{} {
|
|
||||||
labels := []interface{}{"db.method", method, "took", fmt.Sprintf("%v", td)}
|
|
||||||
if err != nil {
|
|
||||||
labels = append(labels, "error", err.Error())
|
|
||||||
}
|
|
||||||
if query != labelUnknown {
|
|
||||||
labels = append(labels, "query", query)
|
|
||||||
}
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
MaxOpenConnections = "micro_sql_max_open_conn"
|
|
||||||
OpenConnections = "micro_sql_open_conn"
|
|
||||||
InuseConnections = "micro_sql_inuse_conn"
|
|
||||||
IdleConnections = "micro_sql_idle_conn"
|
|
||||||
WaitConnections = "micro_sql_waited_conn"
|
|
||||||
BlockedSeconds = "micro_sql_blocked_seconds"
|
|
||||||
MaxIdleClosed = "micro_sql_max_idle_closed"
|
|
||||||
MaxIdletimeClosed = "micro_sql_closed_max_idle"
|
|
||||||
MaxLifetimeClosed = "micro_sql_closed_max_lifetime"
|
|
||||||
|
|
||||||
meterRequestTotal = "micro_sql_request_total"
|
|
||||||
meterRequestLatencyMicroseconds = "micro_sql_latency_microseconds"
|
|
||||||
meterRequestDurationSeconds = "micro_sql_request_duration_seconds"
|
|
||||||
|
|
||||||
labelUnknown = "unknown"
|
|
||||||
labelQuery = "db_statement"
|
|
||||||
labelMethod = "db_method"
|
|
||||||
labelStatus = "status"
|
|
||||||
labelSuccess = "success"
|
|
||||||
labelFailure = "failure"
|
|
||||||
labelHost = "db_host"
|
|
||||||
labelDatabase = "db_name"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options struct holds wrapper options
|
|
||||||
type Options struct {
|
|
||||||
Logger logger.Logger
|
|
||||||
Meter meter.Meter
|
|
||||||
Tracer tracer.Tracer
|
|
||||||
DatabaseHost string
|
|
||||||
DatabaseName string
|
|
||||||
MeterStatsInterval time.Duration
|
|
||||||
LoggerLevel logger.Level
|
|
||||||
LoggerEnabled bool
|
|
||||||
LoggerObserver func(ctx context.Context, method string, name string, td time.Duration, err error) []interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option func signature
|
|
||||||
type Option func(*Options)
|
|
||||||
|
|
||||||
// NewOptions create new Options struct from provided option slice
|
|
||||||
func NewOptions(opts ...Option) Options {
|
|
||||||
options := Options{
|
|
||||||
Logger: logger.DefaultLogger,
|
|
||||||
Meter: meter.DefaultMeter,
|
|
||||||
Tracer: tracer.DefaultTracer,
|
|
||||||
MeterStatsInterval: DefaultMeterStatsInterval,
|
|
||||||
LoggerLevel: logger.ErrorLevel,
|
|
||||||
LoggerObserver: DefaultLoggerObserver,
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Meter = options.Meter.Clone(
|
|
||||||
meter.Labels(
|
|
||||||
labelHost, options.DatabaseHost,
|
|
||||||
labelDatabase, options.DatabaseName,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
options.Logger = options.Logger.Clone(logger.WithAddCallerSkipCount(1))
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetricInterval specifies stats interval for *sql.DB
|
|
||||||
func MetricInterval(td time.Duration) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.MeterStatsInterval = td
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DatabaseHost(host string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.DatabaseHost = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DatabaseName(name string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.DatabaseName = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meter passes meter.Meter to wrapper
|
|
||||||
func Meter(m meter.Meter) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Meter = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger passes logger.Logger to wrapper
|
|
||||||
func Logger(l logger.Logger) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Logger = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerEnabled enable sql logging
|
|
||||||
func LoggerEnabled(b bool) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.LoggerEnabled = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerLevel passes logger.Level option
|
|
||||||
func LoggerLevel(lvl logger.Level) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.LoggerLevel = lvl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerObserver passes observer to fill logger fields
|
|
||||||
func LoggerObserver(obs func(context.Context, string, string, time.Duration, error) []interface{}) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.LoggerObserver = obs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tracer passes tracer.Tracer to wrapper
|
|
||||||
func Tracer(t tracer.Tracer) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Tracer = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type queryNameKey struct{}
|
|
||||||
|
|
||||||
// QueryName passes query name to wrapper func
|
|
||||||
func QueryName(ctx context.Context, name string) context.Context {
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, queryNameKey{}, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getQueryName(ctx context.Context) string {
|
|
||||||
if v, ok := ctx.Value(queryNameKey{}).(string); ok && v != labelUnknown {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return getCallerName()
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Statser interface {
|
|
||||||
Stats() sql.DBStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatsMeter(ctx context.Context, db Statser, opts ...Option) {
|
|
||||||
options := NewOptions(opts...)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(options.MeterStatsInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
if db == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stats := db.Stats()
|
|
||||||
options.Meter.Counter(MaxOpenConnections).Set(uint64(stats.MaxOpenConnections))
|
|
||||||
options.Meter.Counter(OpenConnections).Set(uint64(stats.OpenConnections))
|
|
||||||
options.Meter.Counter(InuseConnections).Set(uint64(stats.InUse))
|
|
||||||
options.Meter.Counter(IdleConnections).Set(uint64(stats.Idle))
|
|
||||||
options.Meter.Counter(WaitConnections).Set(uint64(stats.WaitCount))
|
|
||||||
options.Meter.FloatCounter(BlockedSeconds).Set(stats.WaitDuration.Seconds())
|
|
||||||
options.Meter.Counter(MaxIdleClosed).Set(uint64(stats.MaxIdleClosed))
|
|
||||||
options.Meter.Counter(MaxIdletimeClosed).Set(uint64(stats.MaxIdleTimeClosed))
|
|
||||||
options.Meter.Counter(MaxLifetimeClosed).Set(uint64(stats.MaxLifetimeClosed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
@@ -1,287 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
requestid "go.unistack.org/micro/v4/hooks/requestid"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ driver.Stmt = (*wrapperStmt)(nil)
|
|
||||||
_ driver.StmtQueryContext = (*wrapperStmt)(nil)
|
|
||||||
_ driver.StmtExecContext = (*wrapperStmt)(nil)
|
|
||||||
_ driver.NamedValueChecker = (*wrapperStmt)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// wrapperStmt defines a wrapper for driver.Stmt
|
|
||||||
type wrapperStmt struct {
|
|
||||||
stmt driver.Stmt
|
|
||||||
opts Options
|
|
||||||
query string
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements driver.Stmt Close
|
|
||||||
func (w *wrapperStmt) Close() error {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Close"}
|
|
||||||
ts := time.Now()
|
|
||||||
err := w.stmt.Close()
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Close", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NumInput implements driver.Stmt NumInput
|
|
||||||
func (w *wrapperStmt) NumInput() int {
|
|
||||||
return w.stmt.NumInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNamedValue implements driver.NamedValueChecker
|
|
||||||
func (w *wrapperStmt) CheckNamedValue(v *driver.NamedValue) error {
|
|
||||||
s, ok := w.stmt.(driver.NamedValueChecker)
|
|
||||||
if !ok {
|
|
||||||
return driver.ErrSkip
|
|
||||||
}
|
|
||||||
return s.CheckNamedValue(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec implements driver.Stmt Exec
|
|
||||||
func (w *wrapperStmt) Exec(args []driver.Value) (driver.Result, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Exec"}
|
|
||||||
ts := time.Now()
|
|
||||||
res, err := w.stmt.Exec(args) // nolint:staticcheck
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Exec", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query implements driver.Stmt Query
|
|
||||||
func (w *wrapperStmt) Query(args []driver.Value) (driver.Rows, error) {
|
|
||||||
var ctx context.Context
|
|
||||||
if w.ctx != nil {
|
|
||||||
ctx = w.ctx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
_ = ctx
|
|
||||||
labels := []string{labelMethod, "Query"}
|
|
||||||
ts := time.Now()
|
|
||||||
rows, err := w.stmt.Query(args) // nolint:staticcheck
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "Query", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return rows, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColumnConverter implements driver.ColumnConverter
|
|
||||||
func (w *wrapperStmt) ColumnConverter(idx int) driver.ValueConverter {
|
|
||||||
s, ok := w.stmt.(driver.ColumnConverter) // nolint:staticcheck
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.ColumnConverter(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecContext implements driver.StmtExecContext ExecContext
|
|
||||||
func (w *wrapperStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
|
||||||
var nctx context.Context
|
|
||||||
var span tracer.Span
|
|
||||||
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "ExecContext")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
defer span.Finish()
|
|
||||||
if len(args) > 0 {
|
|
||||||
span.AddLabels("db.args", fmt.Sprintf("%v", namedValueToLabels(args)))
|
|
||||||
}
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "ExecContext", labelQuery, name}
|
|
||||||
|
|
||||||
if conn, ok := w.stmt.(driver.StmtExecContext); ok {
|
|
||||||
ts := time.Now()
|
|
||||||
res, err := conn.ExecContext(nctx, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "ExecContext", name, td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
values, err := namedValueToValue(args)
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "ExecContext", name, 0, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ts := time.Now()
|
|
||||||
res, err := w.Exec(values) // nolint:staticcheck
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "ExecContext", name, td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryContext implements driver.StmtQueryContext StmtQueryContext
|
|
||||||
func (w *wrapperStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
|
||||||
var nctx context.Context
|
|
||||||
var span tracer.Span
|
|
||||||
|
|
||||||
name := getQueryName(ctx)
|
|
||||||
if w.ctx != nil {
|
|
||||||
nctx, span = w.opts.Tracer.Start(w.ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
} else {
|
|
||||||
nctx, span = w.opts.Tracer.Start(ctx, "sdk.database", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
||||||
}
|
|
||||||
span.AddLabels("db.method", "QueryContext")
|
|
||||||
span.AddLabels("db.statement", name)
|
|
||||||
defer span.Finish()
|
|
||||||
if len(args) > 0 {
|
|
||||||
span.AddLabels("db.args", fmt.Sprintf("%v", namedValueToLabels(args)))
|
|
||||||
}
|
|
||||||
if id, ok := ctx.Value(requestid.XRequestIDKey{}).(string); ok {
|
|
||||||
span.AddLabels("x-request-id", id)
|
|
||||||
}
|
|
||||||
labels := []string{labelMethod, "QueryContext", labelQuery, name}
|
|
||||||
if conn, ok := w.stmt.(driver.StmtQueryContext); ok {
|
|
||||||
ts := time.Now()
|
|
||||||
rows, err := conn.QueryContext(nctx, args)
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "QueryContext", name, td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return rows, err
|
|
||||||
}
|
|
||||||
|
|
||||||
values, err := namedValueToValue(args)
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "QueryContext", name, 0, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ts := time.Now()
|
|
||||||
rows, err := w.Query(values) // nolint:staticcheck
|
|
||||||
td := time.Since(ts)
|
|
||||||
te := td.Seconds()
|
|
||||||
if err != nil {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelFailure)...).Inc()
|
|
||||||
span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
} else {
|
|
||||||
w.opts.Meter.Counter(meterRequestTotal, append(labels, labelStatus, labelSuccess)...).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.opts.Meter.Summary(meterRequestLatencyMicroseconds, labels...).Update(te)
|
|
||||||
w.opts.Meter.Histogram(meterRequestDurationSeconds, labels...).Update(te)
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(ctx, "QueryContext", name, td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return rows, err
|
|
||||||
}
|
|
@@ -1,63 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ driver.Tx = (*wrapperTx)(nil)
|
|
||||||
|
|
||||||
// wrapperTx defines a wrapper for driver.Tx
|
|
||||||
type wrapperTx struct {
|
|
||||||
tx driver.Tx
|
|
||||||
span tracer.Span
|
|
||||||
opts Options
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit implements driver.Tx Commit
|
|
||||||
func (w *wrapperTx) Commit() error {
|
|
||||||
ts := time.Now()
|
|
||||||
err := w.tx.Commit()
|
|
||||||
td := time.Since(ts)
|
|
||||||
_ = td
|
|
||||||
if w.span != nil {
|
|
||||||
if err != nil {
|
|
||||||
w.span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
}
|
|
||||||
w.span.Finish()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(w.ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(w.ctx, "Commit", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
w.ctx = nil
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rollback implements driver.Tx Rollback
|
|
||||||
func (w *wrapperTx) Rollback() error {
|
|
||||||
ts := time.Now()
|
|
||||||
err := w.tx.Rollback()
|
|
||||||
td := time.Since(ts)
|
|
||||||
_ = td
|
|
||||||
if w.span != nil {
|
|
||||||
if err != nil {
|
|
||||||
w.span.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
}
|
|
||||||
w.span.Finish()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if w.opts.LoggerEnabled && w.opts.Logger.V(w.opts.LoggerLevel) {
|
|
||||||
w.opts.Logger.Log(w.ctx, w.opts.LoggerLevel, w.opts.LoggerObserver(w.ctx, "Rollback", getCallerName(), td, err)...)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
w.ctx = nil
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
package sql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
func wrapDriver(d driver.Driver, opts Options) driver.Driver {
|
|
||||||
if _, ok := d.(driver.DriverContext); ok {
|
|
||||||
return &wrapperDriver{driver: d, opts: opts}
|
|
||||||
}
|
|
||||||
return struct{ driver.Driver }{&wrapperDriver{driver: d, opts: opts}}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// WrapConn allows an existing driver.Conn to be wrapped.
|
|
||||||
func WrapConn(c driver.Conn, opts ...Option) driver.Conn {
|
|
||||||
return wrapConn(c, NewOptions(opts...))
|
|
||||||
}
|
|
20699
hooks/sql/wrap_gen.go
20699
hooks/sql/wrap_gen.go
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user