Compare commits
128 Commits
v3.10.109
...
e09c34c691
Author | SHA1 | Date | |
---|---|---|---|
e09c34c691 | |||
ffd5e416c8 | |||
|
3cef42f776 | ||
4de9431df0 | |||
19e3288b1d | |||
|
3c779b248f | ||
bbc7512054 | |||
|
dd810e4ae0 | ||
236ed47ab1 | |||
|
909bcf51a4 | ||
9c24001f52 | |||
680cd6f708 | |||
|
3fcf3bef6d | ||
|
0ecd6199d4 | ||
14a30fb6a7 | |||
7c613072df | |||
|
c55e212270 | ||
100bc006bb | |||
5dbfe8a7a6 | |||
6be077dbe8 | |||
b4878211ee | |||
ec9178c6d4 | |||
ae63d44866 | |||
883e79216a | |||
fa636ef6a9 | |||
cdb81a9ba3 | |||
413c6cc2f0 | |||
|
f56bd70136 | ||
b51b4107a8 | |||
2067c9de6b | |||
3f82cb3ba4 | |||
|
306b7a3962 | ||
a8eda9d58d | |||
7e4477dcb4 | |||
|
d846044fc6 | ||
29d956e74e | |||
fcc4faff8a | |||
5df8f83f45 | |||
|
27fa6e9173 | ||
bd55a35dc3 | |||
653bd386cc | |||
|
558c6f4d7c | ||
d7dd6fbeb2 | |||
a00cf2c8d9 | |||
|
a3e8ab2492 | ||
06da500ef4 | |||
277f04ba19 | |||
|
470263ff5f | ||
b8232e02be | |||
|
f8c5e10c1d | ||
397e71f815 | |||
74e31d99f6 | |||
f39de15d93 | |||
d291102877 | |||
37ffbb18d8 | |||
9a85dead86 | |||
a489aab1c3 | |||
d160664ef1 | |||
fa868edcaa | |||
|
6ed0b0e090 | ||
533b265d19 | |||
1ace2631a4 | |||
3dd5ca68d1 | |||
66ccd6021f | |||
e5346f7e4f | |||
|
daf19f031a | ||
5989fd54ca | |||
ed30c26324 | |||
0f8f93d09a | |||
|
f460e2f8dd | ||
70d6a79274 | |||
664b1586af | |||
8d747c64a8 | |||
94beb5ed3b | |||
98981ba86c | |||
1013f50d0e | |||
0b190997b1 | |||
69a44eb190 | |||
0476028f69 | |||
330d8b149a | |||
19b04fe070 | |||
4cd55875c6 | |||
a7896cc728 | |||
ff991bf49c | |||
5a6551b703 | |||
9406a33d60 | |||
8f185abd9d | |||
86492e0644 | |||
b21972964a | |||
f5ee065d09 | |||
8cb02f2b08 | |||
bc926cd6bd | |||
356abfd818 | |||
18444d3f98 | |||
d5f07922e8 | |||
675e717410 | |||
7b6aea235a | |||
2cb7200467 | |||
f430f97a97 | |||
0060c4377a | |||
e46579fe9a | |||
ca52973194 | |||
5bb33c7e1d | |||
b71fc25328 | |||
9345dd075a | |||
1c1b9c0a28 | |||
2969494c5a | |||
cbd3fa38ba | |||
569a36383d | |||
90bed77526 | |||
ba4478a5e0 | |||
6dc76cdfea | |||
e266683d96 | |||
2b62ad04f2 | |||
275b0a64e5 | |||
38c5fe8b5a | |||
b6a0e4d983 | |||
d9b822deff | |||
0e66688f8f | |||
9213fd212f | |||
aa360dcf51 | |||
2df259b5b8 | |||
15e9310368 | |||
|
16d8cf3434 | ||
9704ef2e5e | |||
94e8f90f00 | |||
34d1587881 | |||
bf4143cde5 |
@@ -1,24 +0,0 @@
|
|||||||
name: lint
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- v3
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: setup-go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: 1.21
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: deps
|
|
||||||
run: go get -v -d ./...
|
|
||||||
- name: lint
|
|
||||||
uses: https://github.com/golangci/golangci-lint-action@v3.4.0
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
version: v1.52
|
|
@@ -1,23 +0,0 @@
|
|||||||
name: pr
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- v3
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: setup-go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: 1.21
|
|
||||||
- name: deps
|
|
||||||
run: go get -v -t -d ./...
|
|
||||||
- name: test
|
|
||||||
env:
|
|
||||||
INTEGRATION_TESTS: yes
|
|
||||||
run: go test -mod readonly -v ./...
|
|
53
.github/workflows/job_coverage.yml
vendored
Normal file
53
.github/workflows/job_coverage.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, v3, v4 ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: github.server_url != 'https://github.com'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- name: test coverage
|
||||||
|
run: |
|
||||||
|
go test -v -cover ./... -covermode=count -coverprofile coverage.out -coverpkg ./...
|
||||||
|
go tool cover -func coverage.out -o coverage.out
|
||||||
|
|
||||||
|
- name: coverage badge
|
||||||
|
uses: tj-actions/coverage-badge-go@v2
|
||||||
|
with:
|
||||||
|
green: 80
|
||||||
|
filename: coverage.out
|
||||||
|
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
name: autocommit
|
||||||
|
with:
|
||||||
|
commit_message: Apply Code Coverage Badge
|
||||||
|
skip_fetch: false
|
||||||
|
skip_checkout: false
|
||||||
|
file_pattern: ./README.md
|
||||||
|
|
||||||
|
- name: push
|
||||||
|
if: steps.auto-commit-action.outputs.changes_detected == 'true'
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ github.token }}
|
||||||
|
branch: ${{ github.ref }}
|
29
.github/workflows/job_lint.yml
vendored
Normal file
29
.github/workflows/job_lint.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup deps
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
31
.github/workflows/job_test.yml
vendored
Normal file
31
.github/workflows/job_test.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
push:
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup deps
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run test
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
run: go test -mod readonly -v ./...
|
50
.github/workflows/job_tests.yml
vendored
Normal file
50
.github/workflows/job_tests.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
push:
|
||||||
|
branches: [ master, v3, v4 ]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
filter: 'blob:none'
|
||||||
|
- name: checkout tests
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
filter: 'blob:none'
|
||||||
|
repository: unistack-org/micro-tests
|
||||||
|
path: micro-tests
|
||||||
|
- name: setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache-dependency-path: "**/*.sum"
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: setup go work
|
||||||
|
env:
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: |
|
||||||
|
go work init
|
||||||
|
go work use .
|
||||||
|
go work use micro-tests
|
||||||
|
- name: setup deps
|
||||||
|
env:
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: go get -v ./...
|
||||||
|
- name: run tests
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
GOWORK: ${{ github.workspace }}/go.work
|
||||||
|
run: |
|
||||||
|
cd micro-tests
|
||||||
|
go test -mod readonly -v ./... || true
|
@@ -1,44 +1,5 @@
|
|||||||
run:
|
run:
|
||||||
concurrency: 4
|
concurrency: 8
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
govet:
|
|
||||||
check-shadowing: true
|
|
||||||
enable:
|
|
||||||
- fieldalignment
|
|
||||||
|
|
||||||
linters:
|
|
||||||
enable:
|
|
||||||
- govet
|
|
||||||
- deadcode
|
|
||||||
- errcheck
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
- staticcheck
|
|
||||||
- structcheck
|
|
||||||
- typecheck
|
|
||||||
- unused
|
|
||||||
- varcheck
|
|
||||||
- bodyclose
|
|
||||||
- gci
|
|
||||||
- goconst
|
|
||||||
- gocritic
|
|
||||||
- gosimple
|
|
||||||
- gofmt
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
- revive
|
|
||||||
- gosec
|
|
||||||
- makezero
|
|
||||||
- misspell
|
|
||||||
- nakedret
|
|
||||||
- nestif
|
|
||||||
- nilerr
|
|
||||||
- noctx
|
|
||||||
- prealloc
|
|
||||||
- unconvert
|
|
||||||
- unparam
|
|
||||||
disable-all: false
|
|
||||||
|
33
README.md
33
README.md
@@ -1,39 +1,34 @@
|
|||||||
# Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [](https://goreportcard.com/report/go.unistack.org/micro/v3) [](https://codecov.io/gh/unistack-org/micro)
|
# Micro
|
||||||
|

|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://pkg.go.dev/go.unistack.org/micro/v3?tab=overview)
|
||||||
|
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av3+event%3Apush)
|
||||||
|
[](https://goreportcard.com/report/go.unistack.org/micro/v3)
|
||||||
|
|
||||||
Micro is a standard library for microservices.
|
Micro is a standard library for microservices.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
|
Micro provides the core requirements for distributed systems development including SYNC and ASYNC communication.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Micro abstracts away the details of distributed systems. Here are the main features.
|
Micro abstracts away the details of distributed systems. Main features:
|
||||||
|
|
||||||
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
|
|
||||||
zero trust networking by providing every service an identity and certificates. This additionally includes rule
|
|
||||||
based access control.
|
|
||||||
|
|
||||||
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
||||||
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
|
level config from any source such as env vars, cmdline, file, consul, vault, etc... You can merge the sources and even define fallbacks.
|
||||||
|
|
||||||
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
||||||
CockroachDB by default. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
s3. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
||||||
|
|
||||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||||
development. When service A needs to speak to service B it needs the location of that service.
|
development.
|
||||||
|
|
||||||
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
|
||||||
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
|
||||||
across the services and retry a different node if there's a problem.
|
|
||||||
|
|
||||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||||
and server handle this by default.
|
and server handle this by default.
|
||||||
|
|
||||||
- **Transport** - gRPC or http based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
|
- **Async Messaging** - Pub/Sub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||||
|
|
||||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
|
||||||
Event notifications are a core pattern in micro service development.
|
Event notifications are a core pattern in micro service development.
|
||||||
|
|
||||||
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
|
||||||
@@ -42,10 +37,6 @@ leadership are built in as a Sync interface. When using an eventually consistent
|
|||||||
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
|
||||||
are pluggable and allows Micro to be runtime agnostic.
|
are pluggable and allows Micro to be runtime agnostic.
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
To be created.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Micro is Apache 2.0 licensed.
|
Micro is Apache 2.0 licensed.
|
||||||
|
15
SECURITY.md
15
SECURITY.md
@@ -1,15 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| 3.7.x | :white_check_mark: |
|
|
||||||
| < 3.7.0 | :x: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you find any issue, please create github issue in this repo
|
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Broker, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns broker from passed context
|
||||||
|
func MustContext(ctx context.Context) Broker {
|
||||||
|
b, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing broker")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext savess broker in context
|
// NewContext savess broker in context
|
||||||
func NewContext(ctx context.Context, s Broker) context.Context {
|
func NewContext(ctx context.Context, s Broker) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -109,7 +109,7 @@ func (m *memoryBroker) Init(opts ...broker.Option) error {
|
|||||||
m.funcSubscribe = m.fnSubscribe
|
m.funcSubscribe = m.fnSubscribe
|
||||||
m.funcBatchSubscribe = m.fnBatchSubscribe
|
m.funcBatchSubscribe = m.fnBatchSubscribe
|
||||||
|
|
||||||
m.opts.Hooks.EachNext(func(hook options.Hook) {
|
m.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
switch h := hook.(type) {
|
switch h := hook.(type) {
|
||||||
case broker.HookPublish:
|
case broker.HookPublish:
|
||||||
m.funcPublish = h(m.funcPublish)
|
m.funcPublish = h(m.funcPublish)
|
||||||
|
@@ -74,7 +74,7 @@ func TestMemoryBroker(t *testing.T) {
|
|||||||
topic := "test"
|
topic := "test"
|
||||||
count := 10
|
count := 10
|
||||||
|
|
||||||
fn := func(p broker.Event) error {
|
fn := func(_ broker.Event) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ func (b *NoopBroker) Init(opts ...Option) error {
|
|||||||
b.funcSubscribe = b.fnSubscribe
|
b.funcSubscribe = b.fnSubscribe
|
||||||
b.funcBatchSubscribe = b.fnBatchSubscribe
|
b.funcBatchSubscribe = b.fnBatchSubscribe
|
||||||
|
|
||||||
b.opts.Hooks.EachNext(func(hook options.Hook) {
|
b.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
switch h := hook.(type) {
|
switch h := hook.(type) {
|
||||||
case HookPublish:
|
case HookPublish:
|
||||||
b.funcPublish = h(b.funcPublish)
|
b.funcPublish = h(b.funcPublish)
|
||||||
|
@@ -16,6 +16,9 @@ import (
|
|||||||
|
|
||||||
// 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
|
||||||
@@ -28,23 +31,25 @@ type Options struct {
|
|||||||
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 used when broker can't unmarshal incoming message
|
||||||
ErrorHandler Handler
|
ErrorHandler Handler
|
||||||
// BatchErrorHandler used when broker can't unmashal incoming messages
|
// BatchErrorHandler used when broker can't unmashal incoming messages
|
||||||
BatchErrorHandler BatchHandler
|
BatchErrorHandler BatchHandler
|
||||||
// Name holds the broker name
|
|
||||||
Name string
|
|
||||||
// Addrs holds the broker address
|
// Addrs holds the broker address
|
||||||
Addrs []string
|
Addrs []string
|
||||||
// Wait waits for a collection of goroutines to finish
|
|
||||||
Wait *sync.WaitGroup
|
|
||||||
// GracefulTimeout contains time to wait to finish in flight requests
|
|
||||||
GracefulTimeout time.Duration
|
|
||||||
// Hooks can be run before broker Publish/BatchPublish and
|
// Hooks can be run before broker Publish/BatchPublish and
|
||||||
// Subscribe/BatchSubscribe methods
|
// Subscribe/BatchSubscribe methods
|
||||||
Hooks options.Hooks
|
Hooks options.Hooks
|
||||||
|
|
||||||
|
// GracefulTimeout contains time to wait to finish in flight requests
|
||||||
|
GracefulTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions create new Options
|
// NewOptions create new Options
|
||||||
|
@@ -17,13 +17,13 @@ func BackoffExp(_ context.Context, _ Request, attempts int) (time.Duration, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BackoffInterval specifies randomization interval for backoff func
|
// BackoffInterval specifies randomization interval for backoff func
|
||||||
func BackoffInterval(min time.Duration, max time.Duration) BackoffFunc {
|
func BackoffInterval(minTime time.Duration, maxTime time.Duration) BackoffFunc {
|
||||||
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
|
||||||
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
|
||||||
if td < min {
|
if td < minTime {
|
||||||
return min, nil
|
return minTime, nil
|
||||||
} else if td > max {
|
} else if td > maxTime {
|
||||||
return max, nil
|
return maxTime, nil
|
||||||
}
|
}
|
||||||
return td, nil
|
return td, nil
|
||||||
}
|
}
|
||||||
|
@@ -34,23 +34,23 @@ func TestBackoffExp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackoffInterval(t *testing.T) {
|
func TestBackoffInterval(t *testing.T) {
|
||||||
min := 100 * time.Millisecond
|
minTime := 100 * time.Millisecond
|
||||||
max := 300 * time.Millisecond
|
maxTime := 300 * time.Millisecond
|
||||||
|
|
||||||
r := &testRequest{
|
r := &testRequest{
|
||||||
service: "test",
|
service: "test",
|
||||||
method: "test",
|
method: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := BackoffInterval(min, max)
|
fn := BackoffInterval(minTime, maxTime)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
d, err := fn(context.TODO(), r, i)
|
d, err := fn(context.TODO(), r, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d < min || d > max {
|
if d < minTime || d > maxTime {
|
||||||
t.Fatalf("Expected %v < %v < %v", min, d, max)
|
t.Fatalf("Expected %v < %v < %v", minTime, d, maxTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Client, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext get client from context
|
||||||
|
func MustContext(ctx context.Context) Client {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing client")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put client in context
|
// NewContext put client in context
|
||||||
func NewContext(ctx context.Context, c Client) context.Context {
|
func NewContext(ctx context.Context, c Client) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -194,7 +194,7 @@ func (n *noopClient) Init(opts ...Option) error {
|
|||||||
n.funcPublish = n.fnPublish
|
n.funcPublish = n.fnPublish
|
||||||
n.funcBatchPublish = n.fnBatchPublish
|
n.funcBatchPublish = n.fnBatchPublish
|
||||||
|
|
||||||
n.opts.Hooks.EachNext(func(hook options.Hook) {
|
n.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
switch h := hook.(type) {
|
switch h := hook.(type) {
|
||||||
case HookCall:
|
case HookCall:
|
||||||
n.funcCall = h(n.funcCall)
|
n.funcCall = h(n.funcCall)
|
||||||
@@ -222,7 +222,7 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
||||||
var sp tracer.Span
|
var sp tracer.Span
|
||||||
ctx, sp = n.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client",
|
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
||||||
tracer.WithSpanKind(tracer.SpanKindClient),
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
||||||
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
||||||
)
|
)
|
||||||
@@ -298,7 +298,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", err.Error())
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -312,7 +312,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", err.Error())
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// balance the list of nodes
|
// balance the list of nodes
|
||||||
@@ -372,7 +372,7 @@ func (n *noopClient) fnCall(ctx context.Context, req Request, rsp interface{}, o
|
|||||||
return gerr
|
return gerr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts ...RequestOption) Request {
|
func (n *noopClient) NewRequest(service, endpoint string, _ interface{}, _ ...RequestOption) Request {
|
||||||
return &noopRequest{service: service, endpoint: endpoint}
|
return &noopRequest{service: service, endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
||||||
var sp tracer.Span
|
var sp tracer.Span
|
||||||
ctx, sp = n.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client",
|
ctx, sp = n.opts.Tracer.Start(ctx, "rpc-client",
|
||||||
tracer.WithSpanKind(tracer.SpanKindClient),
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
||||||
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
||||||
)
|
)
|
||||||
@@ -466,7 +466,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", cerr.Error())
|
return nil, errors.InternalServerError("go.micro.client", "%s", cerr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sleep if greater than 0
|
// only sleep if greater than 0
|
||||||
@@ -480,7 +480,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", err.Error())
|
return nil, errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// balance the list of nodes
|
// balance the list of nodes
|
||||||
@@ -546,7 +546,7 @@ func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOpti
|
|||||||
return nil, grr
|
return nil, grr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (Stream, error) {
|
func (n *noopClient) stream(ctx context.Context, _ string, _ Request, _ CallOptions) (Stream, error) {
|
||||||
return &noopStream{ctx: ctx}, nil
|
return &noopStream{ctx: ctx}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,7 +588,6 @@ func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishO
|
|||||||
|
|
||||||
for _, p := range ps {
|
for _, p := range ps {
|
||||||
md := metadata.Copy(omd)
|
md := metadata.Copy(omd)
|
||||||
md[metadata.HeaderContentType] = p.ContentType()
|
|
||||||
topic := p.Topic()
|
topic := p.Topic()
|
||||||
if len(exchange) > 0 {
|
if len(exchange) > 0 {
|
||||||
topic = exchange
|
topic = exchange
|
||||||
@@ -600,6 +599,8 @@ func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishO
|
|||||||
md.Set(k, v)
|
md.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
md[metadata.HeaderContentType] = p.ContentType()
|
||||||
|
|
||||||
var body []byte
|
var body []byte
|
||||||
|
|
||||||
// passed in raw data
|
// passed in raw data
|
||||||
@@ -609,13 +610,13 @@ func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishO
|
|||||||
// use codec for payload
|
// use codec for payload
|
||||||
cf, err := n.newCodec(p.ContentType())
|
cf, err := n.newCodec(p.ContentType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the body
|
// set the body
|
||||||
b, err := cf.Marshal(p.Payload())
|
b, err := cf.Marshal(p.Payload())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
return errors.InternalServerError("go.micro.client", "%s", err)
|
||||||
}
|
}
|
||||||
body = b
|
body = b
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,16 @@ import (
|
|||||||
|
|
||||||
// Options holds client options
|
// Options holds client options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// Codecs map
|
||||||
|
Codecs map[string]codec.Codec
|
||||||
|
|
||||||
|
// Proxy is used for proxy requests
|
||||||
|
Proxy string
|
||||||
|
// ContentType is used to select codec
|
||||||
|
ContentType string
|
||||||
|
// Name is the client name
|
||||||
|
Name string
|
||||||
|
|
||||||
// Selector used to select needed address
|
// Selector used to select needed address
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Logger used to log messages
|
// Logger used to log messages
|
||||||
@@ -35,31 +45,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
|
||||||
// Proxy is used for proxy requests
|
// ContextDialer used to connect
|
||||||
Proxy string
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
// ContentType is used to select codec
|
|
||||||
ContentType string
|
|
||||||
// Name is the client name
|
|
||||||
Name string
|
|
||||||
// Wrappers contains wrappers
|
// Wrappers contains wrappers
|
||||||
Wrappers []Wrapper
|
Wrappers []Wrapper
|
||||||
|
// Hooks can be run before broker Publish/BatchPublish and
|
||||||
|
// Subscribe/BatchSubscribe methods
|
||||||
|
Hooks options.Hooks
|
||||||
|
|
||||||
// CallOptions contains default CallOptions
|
// CallOptions contains default CallOptions
|
||||||
CallOptions CallOptions
|
CallOptions CallOptions
|
||||||
|
|
||||||
// PoolSize connection pool size
|
// PoolSize connection pool size
|
||||||
PoolSize int
|
PoolSize int
|
||||||
// PoolTTL connection pool ttl
|
// PoolTTL connection pool ttl
|
||||||
PoolTTL time.Duration
|
PoolTTL time.Duration
|
||||||
// ContextDialer used to connect
|
|
||||||
ContextDialer func(context.Context, string) (net.Conn, error)
|
|
||||||
// Hooks can be run before broker Publish/BatchPublish and
|
|
||||||
// Subscribe/BatchSubscribe methods
|
|
||||||
Hooks options.Hooks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCallOptions creates new call options struct
|
// NewCallOptions creates new call options struct
|
||||||
@@ -73,6 +80,16 @@ func NewCallOptions(opts ...CallOption) CallOptions {
|
|||||||
|
|
||||||
// CallOptions holds client call options
|
// CallOptions holds client call options
|
||||||
type CallOptions struct {
|
type CallOptions struct {
|
||||||
|
// RequestMetadata holds additional metadata for call
|
||||||
|
RequestMetadata metadata.Metadata
|
||||||
|
|
||||||
|
// Network name
|
||||||
|
Network string
|
||||||
|
// Content-Type
|
||||||
|
ContentType string
|
||||||
|
// AuthToken string
|
||||||
|
AuthToken string
|
||||||
|
|
||||||
// Selector selects addr
|
// Selector selects addr
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Context used for deadline
|
// Context used for deadline
|
||||||
@@ -80,33 +97,30 @@ type CallOptions struct {
|
|||||||
// Router used for route
|
// Router used for route
|
||||||
Router router.Router
|
Router router.Router
|
||||||
// Retry func used for retries
|
// Retry func used for retries
|
||||||
|
|
||||||
|
// ResponseMetadata holds additional metadata from call
|
||||||
|
ResponseMetadata *metadata.Metadata
|
||||||
|
|
||||||
Retry RetryFunc
|
Retry RetryFunc
|
||||||
// Backoff func used for backoff when retry
|
// Backoff func used for backoff when retry
|
||||||
Backoff BackoffFunc
|
Backoff BackoffFunc
|
||||||
// Network name
|
// ContextDialer used to connect
|
||||||
Network string
|
ContextDialer func(context.Context, string) (net.Conn, error)
|
||||||
// Content-Type
|
|
||||||
ContentType string
|
|
||||||
// AuthToken string
|
|
||||||
AuthToken string
|
|
||||||
// Address specifies static addr list
|
// Address specifies static addr list
|
||||||
Address []string
|
Address []string
|
||||||
// SelectOptions selector options
|
// SelectOptions selector options
|
||||||
SelectOptions []selector.SelectOption
|
SelectOptions []selector.SelectOption
|
||||||
|
|
||||||
// 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
|
||||||
|
235
cluster/hasql/cluster.go
Normal file
235
cluster/hasql/cluster.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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
|
||||||
|
}
|
171
cluster/hasql/cluster_test.go
Normal file
171
cluster/hasql/cluster_test.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
25
cluster/hasql/db.go
Normal file
25
cluster/hasql/db.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
|
}
|
295
cluster/hasql/driver.go
Normal file
295
cluster/hasql/driver.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
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()
|
||||||
|
}
|
141
cluster/hasql/driver_test.go
Normal file
141
cluster/hasql/driver_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
10
cluster/hasql/error.go
Normal file
10
cluster/hasql/error.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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")
|
||||||
|
)
|
110
cluster/hasql/options.go
Normal file
110
cluster/hasql/options.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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
|
||||||
|
}
|
113
cluster/hasql/picker.go
Normal file
113
cluster/hasql/picker.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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
|
||||||
|
}
|
531
cluster/sql/cluster.go
Normal file
531
cluster/sql/cluster.go
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.yandex/hasql/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNoAliveNodes = errors.New("no alive nodes")
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterQuerier interface {
|
||||||
|
Querier
|
||||||
|
WaitForNodes(ctx context.Context, criterion ...hasql.NodeStateCriterion) error
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Connection pool management
|
||||||
|
SetConnMaxLifetime(d time.Duration)
|
||||||
|
SetConnMaxIdleTime(d time.Duration)
|
||||||
|
SetMaxOpenConns(n int)
|
||||||
|
SetMaxIdleConns(n int)
|
||||||
|
Stats() sql.DBStats
|
||||||
|
|
||||||
|
Conn(ctx context.Context) (*sql.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrClusterChecker = errors.New("cluster node checker required")
|
||||||
|
ErrClusterDiscoverer = errors.New("cluster node discoverer required")
|
||||||
|
ErrClusterPicker = errors.New("cluster node picker required")
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile time guard
|
||||||
|
var _ hasql.NodePicker[Querier] = (*CustomPicker[Querier])(nil)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
fmt.Printf("CompareNodes %s %s\n", a.Node.String(), b.Node.String())
|
||||||
|
// 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 {
|
||||||
|
fmt.Printf("CompareNodes aLag > p.opts.MaxLag && bLag > p.opts.MaxLag\n")
|
||||||
|
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 {
|
||||||
|
fmt.Printf("CompareNodes aLag > p.opts.MaxLag\n")
|
||||||
|
return 1 // b is better
|
||||||
|
}
|
||||||
|
if bLag > p.opts.MaxLag {
|
||||||
|
fmt.Printf("CompareNodes bLag > p.opts.MaxLag\n")
|
||||||
|
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 {
|
||||||
|
fmt.Printf("CompareNodes aPrio == bPrio\n")
|
||||||
|
// First compare by replication lag
|
||||||
|
if aLag < bLag {
|
||||||
|
fmt.Printf("CompareNodes aLag < bLag\n")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if aLag > bLag {
|
||||||
|
fmt.Printf("CompareNodes aLag > bLag\n")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = errNoAliveNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) SetConnMaxLifetime(td time.Duration) {
|
||||||
|
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
|
||||||
|
n.DB().SetConnMaxIdleTime(td)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) SetConnMaxIdleTime(td time.Duration) {
|
||||||
|
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
|
||||||
|
n.DB().SetConnMaxIdleTime(td)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) SetMaxOpenConns(nc int) {
|
||||||
|
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
|
||||||
|
n.DB().SetMaxOpenConns(nc)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) SetMaxIdleConns(nc int) {
|
||||||
|
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
|
||||||
|
n.DB().SetMaxIdleConns(nc)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) Stats() sql.DBStats {
|
||||||
|
s := sql.DBStats{}
|
||||||
|
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
|
||||||
|
st := n.DB().Stats()
|
||||||
|
s.Idle += st.Idle
|
||||||
|
s.InUse += st.InUse
|
||||||
|
s.MaxIdleClosed += st.MaxIdleClosed
|
||||||
|
s.MaxIdleTimeClosed += st.MaxIdleTimeClosed
|
||||||
|
s.MaxOpenConnections += st.MaxOpenConnections
|
||||||
|
s.OpenConnections += st.OpenConnections
|
||||||
|
s.WaitCount += st.WaitCount
|
||||||
|
s.WaitDuration += st.WaitDuration
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) getNodeStateCriterion(ctx context.Context) hasql.NodeStateCriterion {
|
||||||
|
if v, ok := ctx.Value(nodeStateCriterionKey{}).(hasql.NodeStateCriterion); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return c.options.NodeStateCriterion
|
||||||
|
}
|
171
cluster/sql/cluster_test.go
Normal file
171
cluster/sql/cluster_test.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,8 @@ package codec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,3 +56,22 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
|||||||
*m = append((*m)[0:0], data...)
|
*m = append((*m)[0:0], data...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML returns m as the JSON encoding of m.
|
||||||
|
func (m *RawMessage) MarshalYAML() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
} else if len(*m) == 0 {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return *m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML sets *m to a copy of data.
|
||||||
|
func (m *RawMessage) UnmarshalYAML(n *yaml.Node) error {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("RawMessage UnmarshalYAML on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], []byte(n.Value)...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Codec, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns codec from context
|
||||||
|
func MustContext(ctx context.Context) Codec {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing codec")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put codec in context
|
// NewContext put codec in context
|
||||||
func NewContext(ctx context.Context, c Codec) context.Context {
|
func NewContext(ctx context.Context, c Codec) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package codec
|
package codec
|
||||||
|
|
||||||
|
import "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
// Frame gives us the ability to define raw data to send over the pipes
|
// Frame gives us the ability to define raw data to send over the pipes
|
||||||
type Frame struct {
|
type Frame struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
@@ -20,6 +22,17 @@ func (m *Frame) UnmarshalJSON(data []byte) error {
|
|||||||
return m.Unmarshal(data)
|
return m.Unmarshal(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML returns frame data
|
||||||
|
func (m *Frame) MarshalYAML() ([]byte, error) {
|
||||||
|
return m.Marshal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML set frame data
|
||||||
|
func (m *Frame) UnmarshalYAML(n *yaml.Node) error {
|
||||||
|
m.Data = []byte(n.Value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProtoMessage noop func
|
// ProtoMessage noop func
|
||||||
func (m *Frame) ProtoMessage() {}
|
func (m *Frame) ProtoMessage() {}
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ type Validator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig default config
|
// DefaultConfig default config
|
||||||
var DefaultConfig Config = NewConfig()
|
var DefaultConfig = NewConfig()
|
||||||
|
|
||||||
// DefaultWatcherMinInterval default min interval for poll changes
|
// DefaultWatcherMinInterval default min interval for poll changes
|
||||||
var DefaultWatcherMinInterval = 5 * time.Second
|
var DefaultWatcherMinInterval = 5 * time.Second
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Config, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns store from context
|
||||||
|
func MustContext(ctx context.Context) Config {
|
||||||
|
c, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing config")
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put store in context
|
// NewContext put store in context
|
||||||
func NewContext(ctx context.Context, c Config) context.Context {
|
func NewContext(ctx context.Context, c Config) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -37,7 +37,7 @@ func (c *defaultConfig) Init(opts ...Option) error {
|
|||||||
c.funcLoad = c.fnLoad
|
c.funcLoad = c.fnLoad
|
||||||
c.funcSave = c.fnSave
|
c.funcSave = c.fnSave
|
||||||
|
|
||||||
c.opts.Hooks.EachNext(func(hook options.Hook) {
|
c.opts.Hooks.EachPrev(func(hook options.Hook) {
|
||||||
switch h := hook.(type) {
|
switch h := hook.(type) {
|
||||||
case HookLoad:
|
case HookLoad:
|
||||||
c.funcLoad = h(c.funcLoad)
|
c.funcLoad = h(c.funcLoad)
|
||||||
|
@@ -3,24 +3,26 @@ package config_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/config"
|
"go.unistack.org/micro/v3/config"
|
||||||
mid "go.unistack.org/micro/v3/util/id"
|
|
||||||
mtime "go.unistack.org/micro/v3/util/time"
|
mtime "go.unistack.org/micro/v3/util/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cfg struct {
|
type cfg struct {
|
||||||
StringValue string `default:"string_value"`
|
MapValue map[string]bool `default:"key1=true,key2=false"`
|
||||||
IgnoreValue string `json:"-"`
|
StructValue *cfgStructValue
|
||||||
StructValue *cfgStructValue
|
|
||||||
IntValue int `default:"99"`
|
StringValue string `default:"string_value"`
|
||||||
DurationValue time.Duration `default:"10s"`
|
IgnoreValue string `json:"-"`
|
||||||
MDurationValue mtime.Duration `default:"10s"`
|
UUIDValue string `default:"micro:generate uuid"`
|
||||||
MapValue map[string]bool `default:"key1=true,key2=false"`
|
IDValue string `default:"micro:generate id"`
|
||||||
UUIDValue string `default:"micro:generate uuid"`
|
|
||||||
IDValue string `default:"micro:generate id"`
|
DurationValue time.Duration `default:"10s"`
|
||||||
|
MDurationValue mtime.Duration `default:"10s"`
|
||||||
|
IntValue int `default:"99"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type cfgStructValue struct {
|
type cfgStructValue struct {
|
||||||
@@ -112,8 +114,6 @@ func TestDefault(t *testing.T) {
|
|||||||
|
|
||||||
if conf.IDValue == "" {
|
if conf.IDValue == "" {
|
||||||
t.Fatalf("id value empty")
|
t.Fatalf("id value empty")
|
||||||
} else if len(conf.IDValue) != mid.DefaultSize {
|
|
||||||
t.Fatalf("id value invalid: %s", conf.IDValue)
|
|
||||||
}
|
}
|
||||||
_ = conf
|
_ = conf
|
||||||
// t.Logf("%#+v\n", conf)
|
// t.Logf("%#+v\n", conf)
|
||||||
@@ -134,3 +134,13 @@ func TestValidate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_SizeOf(t *testing.T) {
|
||||||
|
st := cfg{}
|
||||||
|
|
||||||
|
tVal := reflect.TypeOf(st)
|
||||||
|
for i := 0; i < tVal.NumField(); i++ {
|
||||||
|
field := tVal.Field(i)
|
||||||
|
fmt.Printf("Field: %s, Offset: %d, Size: %d\n", field.Name, field.Offset, field.Type.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -41,14 +41,16 @@ type Options struct {
|
|||||||
BeforeInit []func(context.Context, Config) error
|
BeforeInit []func(context.Context, Config) error
|
||||||
// AfterInit contains slice of funcs that runs after Init
|
// AfterInit contains slice of funcs that runs after Init
|
||||||
AfterInit []func(context.Context, Config) error
|
AfterInit []func(context.Context, Config) error
|
||||||
// AllowFail flag to allow fail in config source
|
|
||||||
AllowFail bool
|
|
||||||
// SkipLoad runs only if condition returns true
|
// SkipLoad runs only if condition returns true
|
||||||
SkipLoad func(context.Context, Config) bool
|
SkipLoad func(context.Context, Config) bool
|
||||||
// SkipSave runs only if condition returns true
|
// SkipSave runs only if condition returns true
|
||||||
SkipSave func(context.Context, Config) bool
|
SkipSave func(context.Context, Config) bool
|
||||||
// Hooks can be run before/after config Save/Load
|
// Hooks can be run before/after config Save/Load
|
||||||
Hooks options.Hooks
|
Hooks options.Hooks
|
||||||
|
|
||||||
|
// AllowFail flag to allow fail in config source
|
||||||
|
AllowFail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option function signature
|
// Option function signature
|
||||||
@@ -278,10 +280,10 @@ func WatchCoalesce(b bool) WatchOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WatchInterval specifies min and max time.Duration for pulling changes
|
// WatchInterval specifies min and max time.Duration for pulling changes
|
||||||
func WatchInterval(min, max time.Duration) WatchOption {
|
func WatchInterval(minTime, maxTime time.Duration) WatchOption {
|
||||||
return func(o *WatchOptions) {
|
return func(o *WatchOptions) {
|
||||||
o.MinInterval = min
|
o.MinInterval = minTime
|
||||||
o.MaxInterval = max
|
o.MaxInterval = maxTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ package errors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
er "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -26,7 +26,7 @@ func TestMarshalJSON(t *testing.T) {
|
|||||||
func TestEmpty(t *testing.T) {
|
func TestEmpty(t *testing.T) {
|
||||||
msg := "test"
|
msg := "test"
|
||||||
var err *Error
|
var err *Error
|
||||||
err = FromError(fmt.Errorf(msg))
|
err = FromError(errors.New(msg))
|
||||||
if err.Detail != msg {
|
if err.Detail != msg {
|
||||||
t.Fatalf("invalid error %v", err)
|
t.Fatalf("invalid error %v", err)
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestFromError(t *testing.T) {
|
|||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
}
|
}
|
||||||
err = er.New(err.Error())
|
err = errors.New(err.Error())
|
||||||
merr = FromError(err)
|
merr = FromError(err)
|
||||||
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
@@ -57,7 +57,7 @@ func TestEqual(t *testing.T) {
|
|||||||
t.Fatal("errors must be equal")
|
t.Fatal("errors must be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
err3 := er.New("my test err")
|
err3 := errors.New("my test err")
|
||||||
if Equal(err1, err3) {
|
if Equal(err1, err3) {
|
||||||
t.Fatal("errors must be not equal")
|
t.Fatal("errors must be not equal")
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Flow, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns Flow from context
|
||||||
|
func MustContext(ctx context.Context) Flow {
|
||||||
|
f, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing flow")
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext stores Flow to context
|
// NewContext stores Flow to context
|
||||||
func NewContext(ctx context.Context, f Flow) context.Context {
|
func NewContext(ctx context.Context, f Flow) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -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...),
|
||||||
|
48
go.mod
48
go.mod
@@ -1,32 +1,44 @@
|
|||||||
module go.unistack.org/micro/v3
|
module go.unistack.org/micro/v3
|
||||||
|
|
||||||
go 1.22
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0
|
dario.cat/mergo v1.0.1
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/KimMachineGun/automemlimit v0.6.1
|
github.com/KimMachineGun/automemlimit v0.6.1
|
||||||
github.com/google/uuid v1.3.0
|
github.com/ash3in/uuidv8 v1.2.0
|
||||||
|
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-20220518035006-a7e85ada93c5
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.unistack.org/micro-proto/v3 v3.4.1
|
go.unistack.org/micro-proto/v3 v3.4.1
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.10.0
|
||||||
google.golang.org/grpc v1.57.0
|
golang.yandex/hasql/v2 v2.1.0
|
||||||
google.golang.org/protobuf v1.33.0
|
google.golang.org/grpc v1.69.2
|
||||||
|
google.golang.org/protobuf v1.36.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cilium/ebpf v0.9.1 // indirect
|
github.com/cilium/ebpf v0.16.0 // indirect
|
||||||
github.com/containerd/cgroups/v3 v3.0.1 // indirect
|
github.com/containerd/cgroups/v3 v3.0.4 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.0.4 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/opencontainers/runtime-spec v1.0.2 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
|
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
golang.org/x/net v0.14.0 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e // indirect
|
go.uber.org/goleak v1.3.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
)
|
)
|
||||||
|
139
go.sum
139
go.sum
@@ -1,78 +1,105 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/KimMachineGun/automemlimit v0.6.1 h1:ILa9j1onAAMadBsyyUJv5cack8Y1WT26yLj/V+ulKp8=
|
github.com/KimMachineGun/automemlimit v0.6.1 h1:ILa9j1onAAMadBsyyUJv5cack8Y1WT26yLj/V+ulKp8=
|
||||||
github.com/KimMachineGun/automemlimit v0.6.1/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY=
|
github.com/KimMachineGun/automemlimit v0.6.1/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY=
|
||||||
github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4=
|
github.com/ash3in/uuidv8 v1.2.0 h1:2oogGdtCPwaVtyvPPGin4TfZLtOGE5F+W++E880G6SI=
|
||||||
github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY=
|
github.com/ash3in/uuidv8 v1.2.0/go.mod h1:BnU0wJBxnzdEKmVg4xckBkD+VZuecTFTUP3M0dWgyY4=
|
||||||
github.com/containerd/cgroups/v3 v3.0.1 h1:4hfGvu8rfGIwVIDd+nLzn/B9ZXx4BcCjzt5ToenJRaE=
|
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||||
github.com/containerd/cgroups/v3 v3.0.1/go.mod h1:/vtwk1VXrtoa5AaZLkypuOJgA/6DyPMZHJPGQNtlHnw=
|
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
github.com/containerd/cgroups/v3 v3.0.4 h1:2fs7l3P0Qxb1nKWuJNFiwhp2CqiKzho71DQkDrHJIo4=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/containerd/cgroups/v3 v3.0.4/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
|
||||||
|
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
|
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/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/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
|
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
|
||||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
||||||
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
|
||||||
|
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||||
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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
|
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q=
|
go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q=
|
||||||
go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo=
|
go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo=
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e h1:NumxXLPfHSndr3wBBdeKiVHjGVFzi9RX2HwwQke94iY=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
golang.yandex/hasql/v2 v2.1.0 h1:7CaFFWeHoK5TvA+QvZzlKHlIN5sqNpqM8NSrXskZD/k=
|
||||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
golang.yandex/hasql/v2 v2.1.0/go.mod h1:3Au1AxuJDCTXmS117BpbI6e+70kGWeyLR1qJAH6HdtA=
|
||||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
76
hooks/metadata/metadata.go
Normal file
76
hooks/metadata/metadata.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/client"
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultMetadataKeys = []string{"x-request-id"}
|
||||||
|
|
||||||
|
type hook struct {
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHook(keys ...string) *hook {
|
||||||
|
return &hook{keys: keys}
|
||||||
|
}
|
||||||
|
|
||||||
|
func metadataCopy(ctx context.Context, keys []string) context.Context {
|
||||||
|
if keys == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
|
||||||
|
omd, ook := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ook || omd == nil {
|
||||||
|
omd = metadata.New(len(keys))
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
if v, ok := imd.Get(k); ok && v != "" {
|
||||||
|
omd.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ook {
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, omd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientCall(next client.FuncCall) client.FuncCall {
|
||||||
|
return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||||
|
return next(metadataCopy(ctx, w.keys), 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) {
|
||||||
|
return next(metadataCopy(ctx, w.keys), req, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
|
||||||
|
return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
||||||
|
return next(metadataCopy(ctx, w.keys), msg, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
|
||||||
|
return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
|
||||||
|
return next(metadataCopy(ctx, w.keys), msgs, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
|
||||||
|
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||||
|
return next(metadataCopy(ctx, w.keys), req, rsp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
|
||||||
|
return func(ctx context.Context, msg server.Message) error {
|
||||||
|
return next(metadataCopy(ctx, w.keys), msg)
|
||||||
|
}
|
||||||
|
}
|
94
hooks/recovery/recovery.go
Normal file
94
hooks/recovery/recovery.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package recovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/errors"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
options := Options{
|
||||||
|
ServerHandlerFn: DefaultServerHandlerFn,
|
||||||
|
ServerSubscriberFn: DefaultServerSubscriberFn,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
ServerHandlerFn func(context.Context, server.Request, interface{}, error) error
|
||||||
|
ServerSubscriberFn func(context.Context, server.Message, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerSubscriberFunc(fn func(context.Context, server.Message, error) error) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ServerSubscriberFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultServerHandlerFn = func(ctx context.Context, req server.Request, rsp interface{}, err error) error {
|
||||||
|
return errors.BadRequest("", "%v", err)
|
||||||
|
}
|
||||||
|
DefaultServerSubscriberFn = func(ctx context.Context, req server.Message, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
|
||||||
|
return func(ctx context.Context, msg server.Message) (err error) {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
switch verr := r.(type) {
|
||||||
|
case nil:
|
||||||
|
return
|
||||||
|
case error:
|
||||||
|
err = w.opts.ServerSubscriberFn(ctx, msg, verr)
|
||||||
|
default:
|
||||||
|
err = w.opts.ServerSubscriberFn(ctx, msg, fmt.Errorf("%v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = next(ctx, msg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
139
hooks/requestid/requestid.go
Normal file
139
hooks/requestid/requestid.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package requestid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/textproto"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/client"
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
"go.unistack.org/micro/v3/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 == "" {
|
||||||
|
var id string
|
||||||
|
if id, iok = imd.Get(DefaultMetadataKey); iok && id != "" {
|
||||||
|
xid = id
|
||||||
|
}
|
||||||
|
if id, ook = omd.Get(DefaultMetadataKey); ook && id != "" {
|
||||||
|
xid = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
|
||||||
|
return func(ctx context.Context, msg server.Message) error {
|
||||||
|
var err error
|
||||||
|
if xid, ok := msg.Header()[DefaultMetadataKey]; ok {
|
||||||
|
ctx = context.WithValue(ctx, XRequestIDKey{}, xid)
|
||||||
|
}
|
||||||
|
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return next(ctx, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
|
||||||
|
return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
|
||||||
|
var err error
|
||||||
|
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return next(ctx, msgs, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
|
||||||
|
return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
||||||
|
var err error
|
||||||
|
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return next(ctx, msg, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
}
|
33
hooks/requestid/requestid_test.go
Normal file
33
hooks/requestid/requestid_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package requestid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/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")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, iok := imd.Get(DefaultMetadataKey)
|
||||||
|
_, ook := omd.Get(DefaultMetadataKey)
|
||||||
|
|
||||||
|
if !iok || !ook {
|
||||||
|
t.Fatalf("missing metadata key value")
|
||||||
|
}
|
||||||
|
}
|
51
hooks/sql/common.go
Normal file
51
hooks/sql/common.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
|
}
|
467
hooks/sql/conn.go
Normal file
467
hooks/sql/conn.go
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/hooks/requestid"
|
||||||
|
"go.unistack.org/micro/v3/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)
|
||||||
|
}
|
94
hooks/sql/driver.go
Normal file
94
hooks/sql/driver.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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
|
||||||
|
}
|
165
hooks/sql/gen.go
Normal file
165
hooks/sql/gen.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//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("\t\treturn struct {\n")
|
||||||
|
b.WriteString(strings.Join(append([]string{"\t\t\tdriver.Conn"}, ifaces...), "\n\t\t\t"))
|
||||||
|
b.WriteString("\n\t\t}{")
|
||||||
|
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("\t}\n\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\treturn c\n")
|
||||||
|
b.WriteString("}\n\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("\t\treturn struct {\n")
|
||||||
|
b.WriteString(strings.Join(append([]string{"\t\t\tdriver.Stmt"}, ifaces...), "\n\t\t\t"))
|
||||||
|
b.WriteString("\n\t\t}{")
|
||||||
|
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("\t}\n\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\treturn 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("\n// %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")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s", 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
|
||||||
|
}
|
172
hooks/sql/options.go
Normal file
172
hooks/sql/options.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/logger"
|
||||||
|
"go.unistack.org/micro/v3/meter"
|
||||||
|
"go.unistack.org/micro/v3/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()
|
||||||
|
}
|
41
hooks/sql/stats.go
Normal file
41
hooks/sql/stats.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
287
hooks/sql/stmt.go
Normal file
287
hooks/sql/stmt.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/hooks/requestid"
|
||||||
|
"go.unistack.org/micro/v3/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
|
||||||
|
}
|
63
hooks/sql/tx.go
Normal file
63
hooks/sql/tx.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/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
|
||||||
|
}
|
19
hooks/sql/wrap.go
Normal file
19
hooks/sql/wrap.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
Normal file
20699
hooks/sql/wrap_gen.go
Normal file
File diff suppressed because it is too large
Load Diff
194
hooks/validator/validator.go
Normal file
194
hooks/validator/validator.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/client"
|
||||||
|
"go.unistack.org/micro/v3/errors"
|
||||||
|
"go.unistack.org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultClientErrorFunc = func(req client.Request, rsp interface{}, err error) error {
|
||||||
|
if rsp != nil {
|
||||||
|
return errors.BadGateway(req.Service(), "%v", err)
|
||||||
|
}
|
||||||
|
return errors.BadRequest(req.Service(), "%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultServerErrorFunc = func(req server.Request, rsp interface{}, err error) error {
|
||||||
|
if rsp != nil {
|
||||||
|
return errors.BadGateway(req.Service(), "%v", err)
|
||||||
|
}
|
||||||
|
return errors.BadRequest(req.Service(), "%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultPublishErrorFunc = func(msg client.Message, err error) error {
|
||||||
|
return errors.BadRequest(msg.Topic(), "%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultSubscribeErrorFunc = func(msg server.Message, err error) error {
|
||||||
|
return errors.BadRequest(msg.Topic(), "%v", err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ClientErrorFunc func(client.Request, interface{}, error) error
|
||||||
|
ServerErrorFunc func(server.Request, interface{}, error) error
|
||||||
|
PublishErrorFunc func(client.Message, error) error
|
||||||
|
SubscribeErrorFunc func(server.Message, error) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options struct holds wrapper options
|
||||||
|
type Options struct {
|
||||||
|
ClientErrorFn ClientErrorFunc
|
||||||
|
ServerErrorFn ServerErrorFunc
|
||||||
|
PublishErrorFn PublishErrorFunc
|
||||||
|
SubscribeErrorFn SubscribeErrorFunc
|
||||||
|
ClientValidateResponse bool
|
||||||
|
ServerValidateResponse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option func signature
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
func ClientValidateResponse(b bool) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ClientValidateResponse = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerValidateResponse(b bool) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ClientValidateResponse = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientReqErrorFn(fn ClientErrorFunc) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ClientErrorFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerErrorFn(fn ServerErrorFunc) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ServerErrorFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PublishErrorFn(fn PublishErrorFunc) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.PublishErrorFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubscribeErrorFn(fn SubscribeErrorFunc) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.SubscribeErrorFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
options := Options{
|
||||||
|
ClientErrorFn: DefaultClientErrorFunc,
|
||||||
|
ServerErrorFn: DefaultServerErrorFunc,
|
||||||
|
PublishErrorFn: DefaultPublishErrorFunc,
|
||||||
|
SubscribeErrorFn: DefaultSubscribeErrorFunc,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHook(opts ...Option) *hook {
|
||||||
|
return &hook{opts: NewOptions(opts...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type hook struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientCall(next client.FuncCall) client.FuncCall {
|
||||||
|
return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||||
|
if v, ok := req.Body().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return w.opts.ClientErrorFn(req, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := next(ctx, req, rsp, opts...)
|
||||||
|
if v, ok := rsp.(validator); ok && w.opts.ClientValidateResponse {
|
||||||
|
if verr := v.Validate(); verr != nil {
|
||||||
|
return w.opts.ClientErrorFn(req, rsp, verr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientStream(next client.FuncStream) client.FuncStream {
|
||||||
|
return func(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||||
|
if v, ok := req.Body().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return nil, w.opts.ClientErrorFn(req, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(ctx, req, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
|
||||||
|
return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
||||||
|
if v, ok := msg.Payload().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return w.opts.PublishErrorFn(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(ctx, msg, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
|
||||||
|
return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
|
||||||
|
for _, msg := range msgs {
|
||||||
|
if v, ok := msg.Payload().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return w.opts.PublishErrorFn(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(ctx, msgs, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
|
||||||
|
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||||
|
if v, ok := req.Body().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return w.opts.ServerErrorFn(req, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := next(ctx, req, rsp)
|
||||||
|
if v, ok := rsp.(validator); ok && w.opts.ServerValidateResponse {
|
||||||
|
if verr := v.Validate(); verr != nil {
|
||||||
|
return w.opts.ServerErrorFn(req, rsp, verr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
|
||||||
|
return func(ctx context.Context, msg server.Message) error {
|
||||||
|
if v, ok := msg.Body().(validator); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return w.opts.SubscribeErrorFn(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(ctx, msg)
|
||||||
|
}
|
||||||
|
}
|
@@ -4,17 +4,6 @@ import "context"
|
|||||||
|
|
||||||
type loggerKey struct{}
|
type loggerKey struct{}
|
||||||
|
|
||||||
// MustContext returns logger from passed context or DefaultLogger if empty
|
|
||||||
func MustContext(ctx context.Context) Logger {
|
|
||||||
if ctx == nil {
|
|
||||||
return DefaultLogger
|
|
||||||
}
|
|
||||||
if l, ok := ctx.Value(loggerKey{}).(Logger); ok && l != nil {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
return DefaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromContext returns logger from passed context
|
// FromContext returns logger from passed context
|
||||||
func FromContext(ctx context.Context) (Logger, bool) {
|
func FromContext(ctx context.Context) (Logger, bool) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -24,6 +13,15 @@ func FromContext(ctx context.Context) (Logger, bool) {
|
|||||||
return l, ok
|
return l, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns logger from passed context or DefaultLogger if empty
|
||||||
|
func MustContext(ctx context.Context) Logger {
|
||||||
|
l, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing logger")
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext stores logger into passed context
|
// NewContext stores logger into passed context
|
||||||
func NewContext(ctx context.Context, l Logger) context.Context {
|
func NewContext(ctx context.Context, l Logger) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -11,7 +11,7 @@ var DefaultContextAttrFuncs []ContextAttrFunc
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultLogger variable
|
// DefaultLogger variable
|
||||||
DefaultLogger Logger = NewLogger()
|
DefaultLogger = NewLogger()
|
||||||
// DefaultLevel used by logger
|
// DefaultLevel used by logger
|
||||||
DefaultLevel = InfoLevel
|
DefaultLevel = InfoLevel
|
||||||
)
|
)
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
@@ -15,18 +16,6 @@ type Option func(*Options)
|
|||||||
|
|
||||||
// Options holds logger options
|
// Options holds logger options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Out holds the output writer
|
|
||||||
Out io.Writer
|
|
||||||
// Context holds exernal options
|
|
||||||
Context context.Context
|
|
||||||
// Name holds the logger name
|
|
||||||
Name string
|
|
||||||
// Fields holds additional metadata
|
|
||||||
Fields []interface{}
|
|
||||||
// callerSkipCount number of frmaes to skip
|
|
||||||
CallerSkipCount int
|
|
||||||
// ContextAttrFuncs contains funcs that executed before log func on context
|
|
||||||
ContextAttrFuncs []ContextAttrFunc
|
|
||||||
// TimeKey is the key used for the time of the log call
|
// TimeKey is the key used for the time of the log call
|
||||||
TimeKey string
|
TimeKey string
|
||||||
// LevelKey is the key used for the level of the log call
|
// LevelKey is the key used for the level of the log call
|
||||||
@@ -39,16 +28,30 @@ type Options struct {
|
|||||||
SourceKey string
|
SourceKey string
|
||||||
// StacktraceKey is the key used for the stacktrace
|
// StacktraceKey is the key used for the stacktrace
|
||||||
StacktraceKey string
|
StacktraceKey string
|
||||||
// AddStacktrace controls writing of stacktaces on error
|
// Name holds the logger name
|
||||||
AddStacktrace bool
|
Name string
|
||||||
// AddSource enabled writing source file and position in log
|
// Out holds the output writer
|
||||||
AddSource bool
|
Out io.Writer
|
||||||
// The logging level the logger should log
|
// Context holds exernal options
|
||||||
Level Level
|
Context context.Context
|
||||||
// TimeFunc used to obtain current time
|
|
||||||
TimeFunc func() time.Time
|
|
||||||
// Meter used to count logs for specific level
|
// Meter used to count logs for specific level
|
||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
|
// TimeFunc used to obtain current time
|
||||||
|
TimeFunc func() time.Time
|
||||||
|
// Fields holds additional metadata
|
||||||
|
Fields []interface{}
|
||||||
|
// ContextAttrFuncs contains funcs that executed before log func on context
|
||||||
|
ContextAttrFuncs []ContextAttrFunc
|
||||||
|
// callerSkipCount number of frmaes to skip
|
||||||
|
CallerSkipCount int
|
||||||
|
// The logging level the logger should log
|
||||||
|
Level Level
|
||||||
|
// AddSource enabled writing source file and position in log
|
||||||
|
AddSource bool
|
||||||
|
// AddStacktrace controls writing of stacktaces on error
|
||||||
|
AddStacktrace bool
|
||||||
|
// DedupKeys deduplicate keys in log output
|
||||||
|
DedupKeys bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions creates new options struct
|
// NewOptions creates new options struct
|
||||||
@@ -80,10 +83,33 @@ func WithContextAttrFuncs(fncs ...ContextAttrFunc) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDedupKeys dont log duplicate keys
|
||||||
|
func WithDedupKeys(b bool) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.DedupKeys = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithAddFields add fields for the logger
|
// WithAddFields add fields for the logger
|
||||||
func WithAddFields(fields ...interface{}) Option {
|
func WithAddFields(fields ...interface{}) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Fields = append(o.Fields, fields...)
|
if o.DedupKeys {
|
||||||
|
for i := 0; i < len(o.Fields); i += 2 {
|
||||||
|
for j := 0; j < len(fields); j += 2 {
|
||||||
|
iv, iok := o.Fields[i].(string)
|
||||||
|
jv, jok := fields[j].(string)
|
||||||
|
if iok && jok && iv == jv {
|
||||||
|
o.Fields[i+1] = fields[j+1]
|
||||||
|
fields = slices.Delete(fields, j, j+2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) > 0 {
|
||||||
|
o.Fields = append(o.Fields, fields...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
o.Fields = append(o.Fields, fields...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +179,8 @@ func WithTimeFunc(fn func() time.Time) Option {
|
|||||||
func WithZapKeys() Option {
|
func WithZapKeys() Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.TimeKey = "@timestamp"
|
o.TimeKey = "@timestamp"
|
||||||
o.LevelKey = "level"
|
o.LevelKey = slog.LevelKey
|
||||||
o.MessageKey = "msg"
|
o.MessageKey = slog.MessageKey
|
||||||
o.SourceKey = "caller"
|
o.SourceKey = "caller"
|
||||||
o.StacktraceKey = "stacktrace"
|
o.StacktraceKey = "stacktrace"
|
||||||
o.ErrorKey = "error"
|
o.ErrorKey = "error"
|
||||||
@@ -163,8 +189,8 @@ func WithZapKeys() Option {
|
|||||||
|
|
||||||
func WithZerologKeys() Option {
|
func WithZerologKeys() Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.TimeKey = "time"
|
o.TimeKey = slog.TimeKey
|
||||||
o.LevelKey = "level"
|
o.LevelKey = slog.LevelKey
|
||||||
o.MessageKey = "message"
|
o.MessageKey = "message"
|
||||||
o.SourceKey = "caller"
|
o.SourceKey = "caller"
|
||||||
o.StacktraceKey = "stacktrace"
|
o.StacktraceKey = "stacktrace"
|
||||||
@@ -186,8 +212,8 @@ func WithSlogKeys() Option {
|
|||||||
func WithMicroKeys() Option {
|
func WithMicroKeys() Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.TimeKey = "timestamp"
|
o.TimeKey = "timestamp"
|
||||||
o.LevelKey = "level"
|
o.LevelKey = slog.LevelKey
|
||||||
o.MessageKey = "msg"
|
o.MessageKey = slog.MessageKey
|
||||||
o.SourceKey = "caller"
|
o.SourceKey = "caller"
|
||||||
o.StacktraceKey = "stacktrace"
|
o.StacktraceKey = "stacktrace"
|
||||||
o.ErrorKey = "error"
|
o.ErrorKey = "error"
|
||||||
|
@@ -2,13 +2,16 @@ package slog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v3/semconv"
|
"go.unistack.org/micro/v3/semconv"
|
||||||
@@ -19,6 +22,7 @@ const (
|
|||||||
badKey = "!BADKEY"
|
badKey = "!BADKEY"
|
||||||
// defaultCallerSkipCount used by logger
|
// defaultCallerSkipCount used by logger
|
||||||
defaultCallerSkipCount = 3
|
defaultCallerSkipCount = 3
|
||||||
|
timeFormat = "2006-01-02T15:04:05.000000000Z07:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`)
|
var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`)
|
||||||
@@ -46,11 +50,11 @@ func (h *wrapper) Handle(ctx context.Context, rec slog.Record) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *wrapper) WithAttrs(attrs []slog.Attr) slog.Handler {
|
func (h *wrapper) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
return h.WithAttrs(attrs)
|
return h.h.WithAttrs(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *wrapper) WithGroup(name string) slog.Handler {
|
func (h *wrapper) WithGroup(name string) slog.Handler {
|
||||||
return h.WithGroup(name)
|
return h.h.WithGroup(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
|
func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
|
||||||
@@ -61,6 +65,7 @@ func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
|
|||||||
a.Key = s.opts.SourceKey
|
a.Key = s.opts.SourceKey
|
||||||
case slog.TimeKey:
|
case slog.TimeKey:
|
||||||
a.Key = s.opts.TimeKey
|
a.Key = s.opts.TimeKey
|
||||||
|
a.Value = slog.StringValue(a.Value.Time().Format(timeFormat))
|
||||||
case slog.MessageKey:
|
case slog.MessageKey:
|
||||||
a.Key = s.opts.MessageKey
|
a.Key = s.opts.MessageKey
|
||||||
case slog.LevelKey:
|
case slog.LevelKey:
|
||||||
@@ -89,7 +94,6 @@ func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type slogLogger struct {
|
type slogLogger struct {
|
||||||
leveler *slog.LevelVar
|
|
||||||
handler *wrapper
|
handler *wrapper
|
||||||
opts logger.Options
|
opts logger.Options
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -142,6 +146,7 @@ func (s *slogLogger) Fields(fields ...interface{}) logger.Logger {
|
|||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
l := &slogLogger{opts: options}
|
l := &slogLogger{opts: options}
|
||||||
|
logger.WithAddFields(fields...)(&l.opts)
|
||||||
|
|
||||||
if len(options.ContextAttrFuncs) == 0 {
|
if len(options.ContextAttrFuncs) == 0 {
|
||||||
options.ContextAttrFuncs = logger.DefaultContextAttrFuncs
|
options.ContextAttrFuncs = logger.DefaultContextAttrFuncs
|
||||||
@@ -172,7 +177,29 @@ func (s *slogLogger) Init(opts ...logger.Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attrs, _ := s.argsAttrs(s.opts.Fields)
|
attrs, _ := s.argsAttrs(s.opts.Fields)
|
||||||
s.handler = &wrapper{h: slog.NewJSONHandler(s.opts.Out, handleOpt).WithAttrs(attrs)}
|
|
||||||
|
var h slog.Handler
|
||||||
|
if s.opts.Context != nil {
|
||||||
|
if v, ok := s.opts.Context.Value(handlerKey{}).(slog.Handler); ok && v != nil {
|
||||||
|
h = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn := s.opts.Context.Value(handlerFnKey{}); fn != nil {
|
||||||
|
if rfn := reflect.ValueOf(fn); rfn.Kind() == reflect.Func {
|
||||||
|
if ret := rfn.Call([]reflect.Value{reflect.ValueOf(s.opts.Out), reflect.ValueOf(handleOpt)}); len(ret) == 1 {
|
||||||
|
if iface, ok := ret[0].Interface().(slog.Handler); ok && iface != nil {
|
||||||
|
h = iface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == nil {
|
||||||
|
h = slog.NewJSONHandler(s.opts.Out, handleOpt)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.handler = &wrapper{h: h.WithAttrs(attrs)}
|
||||||
s.handler.level.Store(int64(loggerToSlogLevel(s.opts.Level)))
|
s.handler.level.Store(int64(loggerToSlogLevel(s.opts.Level)))
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
@@ -201,6 +228,10 @@ func (s *slogLogger) Error(ctx context.Context, msg string, attrs ...interface{}
|
|||||||
|
|
||||||
func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
|
func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
s.printLog(ctx, logger.FatalLevel, msg, attrs...)
|
s.printLog(ctx, logger.FatalLevel, msg, attrs...)
|
||||||
|
if closer, ok := s.opts.Out.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +278,7 @@ func (s *slogLogger) printLog(ctx context.Context, lvl logger.Level, msg string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.opts.AddStacktrace && lvl == logger.ErrorLevel {
|
if s.opts.AddStacktrace && (lvl == logger.FatalLevel || lvl == logger.ErrorLevel) {
|
||||||
stackInfo := make([]byte, 1024*1024)
|
stackInfo := make([]byte, 1024*1024)
|
||||||
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
|
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
|
||||||
traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1)
|
traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1)
|
||||||
@@ -318,7 +349,7 @@ func (s *slogLogger) argsAttrs(args []interface{}) ([]slog.Attr, error) {
|
|||||||
case string:
|
case string:
|
||||||
if idx+1 < len(args) {
|
if idx+1 < len(args) {
|
||||||
attrs = append(attrs, slog.Any(arg, args[idx+1]))
|
attrs = append(attrs, slog.Any(arg, args[idx+1]))
|
||||||
idx += 1
|
idx++
|
||||||
} else {
|
} else {
|
||||||
attrs = append(attrs, slog.String(badKey, arg))
|
attrs = append(attrs, slog.String(badKey, arg))
|
||||||
}
|
}
|
||||||
@@ -330,3 +361,15 @@ func (s *slogLogger) argsAttrs(args []interface{}) ([]slog.Attr, error) {
|
|||||||
|
|
||||||
return attrs, err
|
return attrs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type handlerKey struct{}
|
||||||
|
|
||||||
|
func WithHandler(h slog.Handler) logger.Option {
|
||||||
|
return logger.SetOption(handlerKey{}, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
type handlerFnKey struct{}
|
||||||
|
|
||||||
|
func WithHandlerFunc(fn any) logger.Option {
|
||||||
|
return logger.SetOption(handlerFnKey{}, fn)
|
||||||
|
}
|
||||||
|
@@ -6,15 +6,146 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
|
"go.unistack.org/micro/v3/metadata"
|
||||||
|
"go.unistack.org/micro/v3/util/buffer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// always first to have proper check
|
||||||
|
func TestStacktrace(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.DebugLevel), logger.WithOutput(buf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
logger.WithAddStacktrace(true),
|
||||||
|
)
|
||||||
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Error(ctx, "msg1", errors.New("err"))
|
||||||
|
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`slog_test.go:32`)) {
|
||||||
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelayedBuffer(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
dbuf := buffer.NewDelayedBuffer(100, 100*time.Millisecond, buf)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(dbuf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
logger.WithAddStacktrace(true),
|
||||||
|
)
|
||||||
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Error(ctx, "msg1", errors.New("err"))
|
||||||
|
time.Sleep(120 * time.Millisecond)
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`key1=val1`)) {
|
||||||
|
t.Fatalf("logger delayed buffer not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
logger.WithAddStacktrace(true),
|
||||||
|
logger.WithTimeFunc(func() time.Time {
|
||||||
|
return time.Unix(0, 0)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Error(ctx, "msg1", errors.New("err"))
|
||||||
|
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T03:00:00.000000000+03:00`)) &&
|
||||||
|
!bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T00:00:00.000000000Z`)) {
|
||||||
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithFields(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.InfoLevel), logger.WithOutput(buf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
logger.WithDedupKeys(true),
|
||||||
|
)
|
||||||
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "msg1")
|
||||||
|
|
||||||
|
l = l.Fields("key1", "val2")
|
||||||
|
|
||||||
|
l.Info(ctx, "msg2")
|
||||||
|
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`msg=msg2 key1=val1`)) {
|
||||||
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithDedupKeysWithAddFields(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.InfoLevel), logger.WithOutput(buf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
logger.WithDedupKeys(true),
|
||||||
|
)
|
||||||
|
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "msg1")
|
||||||
|
|
||||||
|
if err := l.Init(logger.WithAddFields("key2", "val2")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "msg2")
|
||||||
|
|
||||||
|
if err := l.Init(logger.WithAddFields("key2", "val3", "key1", "val4")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "msg3")
|
||||||
|
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`msg=msg3 key1=val4 key2=val3`)) {
|
||||||
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithHandlerFunc(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.InfoLevel), logger.WithOutput(buf),
|
||||||
|
WithHandlerFunc(slog.NewTextHandler),
|
||||||
|
)
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "msg1")
|
||||||
|
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`msg=msg1`)) {
|
||||||
|
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWithAddFields(t *testing.T) {
|
func TestWithAddFields(t *testing.T) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
@@ -115,7 +246,7 @@ func TestErrorf(t *testing.T) {
|
|||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithAddStacktrace(true))
|
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithAddStacktrace(true))
|
||||||
if err := l.Init(logger.WithContextAttrFuncs(func(ctx context.Context) []interface{} {
|
if err := l.Init(logger.WithContextAttrFuncs(func(_ context.Context) []interface{} {
|
||||||
return nil
|
return nil
|
||||||
})); err != nil {
|
})); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@@ -36,14 +36,14 @@ var (
|
|||||||
circularShortBytes = []byte("<shown>")
|
circularShortBytes = []byte("<shown>")
|
||||||
invalidAngleBytes = []byte("<invalid>")
|
invalidAngleBytes = []byte("<invalid>")
|
||||||
filteredBytes = []byte("<filtered>")
|
filteredBytes = []byte("<filtered>")
|
||||||
openBracketBytes = []byte("[")
|
// openBracketBytes = []byte("[")
|
||||||
closeBracketBytes = []byte("]")
|
// closeBracketBytes = []byte("]")
|
||||||
percentBytes = []byte("%")
|
percentBytes = []byte("%")
|
||||||
precisionBytes = []byte(".")
|
precisionBytes = []byte(".")
|
||||||
openAngleBytes = []byte("<")
|
openAngleBytes = []byte("<")
|
||||||
closeAngleBytes = []byte(">")
|
closeAngleBytes = []byte(">")
|
||||||
openMapBytes = []byte("{")
|
openMapBytes = []byte("{")
|
||||||
closeMapBytes = []byte("}")
|
closeMapBytes = []byte("}")
|
||||||
)
|
)
|
||||||
|
|
||||||
type protoMessage interface {
|
type protoMessage interface {
|
||||||
@@ -52,13 +52,15 @@ type protoMessage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Wrapper struct {
|
type Wrapper struct {
|
||||||
val interface{}
|
pointers map[uintptr]int
|
||||||
s fmt.State
|
takeMap map[int]bool
|
||||||
pointers map[uintptr]int
|
|
||||||
opts *Options
|
val interface{}
|
||||||
|
s fmt.State
|
||||||
|
opts *Options
|
||||||
|
|
||||||
depth int
|
depth int
|
||||||
ignoreNextType bool
|
ignoreNextType bool
|
||||||
takeMap map[int]bool
|
|
||||||
protoWrapperType bool
|
protoWrapperType bool
|
||||||
sqlWrapperType bool
|
sqlWrapperType bool
|
||||||
}
|
}
|
||||||
|
@@ -82,12 +82,12 @@ func TestTagged(t *testing.T) {
|
|||||||
func TestTaggedNested(t *testing.T) {
|
func TestTaggedNested(t *testing.T) {
|
||||||
type val struct {
|
type val struct {
|
||||||
key string `logger:"take"`
|
key string `logger:"take"`
|
||||||
val string `logger:"omit"`
|
// val string `logger:"omit"`
|
||||||
unk string
|
unk string
|
||||||
}
|
}
|
||||||
type str struct {
|
type str struct {
|
||||||
key string `logger:"omit"`
|
// key string `logger:"omit"`
|
||||||
val *val `logger:"take"`
|
val *val `logger:"take"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var iface interface{}
|
var iface interface{}
|
||||||
|
@@ -24,6 +24,17 @@ func FromIncomingContext(ctx context.Context) (Metadata, bool) {
|
|||||||
return md.md, ok
|
return md.md, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustIncomingContext returns metadata from incoming ctx
|
||||||
|
// returned metadata shoud not be modified or race condition happens.
|
||||||
|
// If metadata not exists panics.
|
||||||
|
func MustIncomingContext(ctx context.Context) Metadata {
|
||||||
|
md, ok := FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing metadata")
|
||||||
|
}
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
// FromOutgoingContext returns metadata from outgoing ctx
|
// FromOutgoingContext returns metadata from outgoing ctx
|
||||||
// returned metadata shoud not be modified or race condition happens
|
// returned metadata shoud not be modified or race condition happens
|
||||||
func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
|
func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
|
||||||
@@ -37,6 +48,17 @@ func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
|
|||||||
return md.md, ok
|
return md.md, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustOutgoingContext returns metadata from outgoing ctx
|
||||||
|
// returned metadata shoud not be modified or race condition happens.
|
||||||
|
// If metadata not exists panics.
|
||||||
|
func MustOutgoingContext(ctx context.Context) Metadata {
|
||||||
|
md, ok := FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing metadata")
|
||||||
|
}
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
// FromContext returns metadata from the given context
|
// FromContext returns metadata from the given context
|
||||||
// returned metadata shoud not be modified or race condition happens
|
// returned metadata shoud not be modified or race condition happens
|
||||||
func FromContext(ctx context.Context) (Metadata, bool) {
|
func FromContext(ctx context.Context) (Metadata, bool) {
|
||||||
@@ -50,15 +72,22 @@ func FromContext(ctx context.Context) (Metadata, bool) {
|
|||||||
return md.md, ok
|
return md.md, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext returns metadata from the given context
|
||||||
|
// returned metadata shoud not be modified or race condition happens
|
||||||
|
func MustContext(ctx context.Context) Metadata {
|
||||||
|
md, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing metadata")
|
||||||
|
}
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext creates a new context with the given metadata
|
// NewContext creates a new context with the given metadata
|
||||||
func NewContext(ctx context.Context, md Metadata) context.Context {
|
func NewContext(ctx context.Context, md Metadata) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, mdKey{}, &rawMetadata{md})
|
return context.WithValue(ctx, mdKey{}, &rawMetadata{md})
|
||||||
ctx = context.WithValue(ctx, mdIncomingKey{}, &rawMetadata{})
|
|
||||||
ctx = context.WithValue(ctx, mdOutgoingKey{}, &rawMetadata{})
|
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOutgoingContext modify outgoing context with given metadata
|
// SetOutgoingContext modify outgoing context with given metadata
|
||||||
@@ -90,11 +119,7 @@ func NewIncomingContext(ctx context.Context, md Metadata) context.Context {
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, mdIncomingKey{}, &rawMetadata{md})
|
return context.WithValue(ctx, mdIncomingKey{}, &rawMetadata{md})
|
||||||
if v, ok := ctx.Value(mdOutgoingKey{}).(*rawMetadata); !ok || v == nil {
|
|
||||||
ctx = context.WithValue(ctx, mdOutgoingKey{}, &rawMetadata{})
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOutgoingContext creates a new context with outcoming metadata attached
|
// NewOutgoingContext creates a new context with outcoming metadata attached
|
||||||
@@ -102,11 +127,7 @@ func NewOutgoingContext(ctx context.Context, md Metadata) context.Context {
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, mdOutgoingKey{}, &rawMetadata{md})
|
return context.WithValue(ctx, mdOutgoingKey{}, &rawMetadata{md})
|
||||||
if v, ok := ctx.Value(mdIncomingKey{}).(*rawMetadata); !ok || v == nil {
|
|
||||||
ctx = context.WithValue(ctx, mdIncomingKey{}, &rawMetadata{})
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendOutgoingContext apends new md to context
|
// AppendOutgoingContext apends new md to context
|
||||||
@@ -122,7 +143,7 @@ func AppendOutgoingContext(ctx context.Context, kv ...string) context.Context {
|
|||||||
for k, v := range md {
|
for k, v := range md {
|
||||||
omd.Set(k, v)
|
omd.Set(k, v)
|
||||||
}
|
}
|
||||||
return NewOutgoingContext(ctx, omd)
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendIncomingContext apends new md to context
|
// AppendIncomingContext apends new md to context
|
||||||
@@ -138,5 +159,21 @@ func AppendIncomingContext(ctx context.Context, kv ...string) context.Context {
|
|||||||
for k, v := range md {
|
for k, v := range md {
|
||||||
omd.Set(k, v)
|
omd.Set(k, v)
|
||||||
}
|
}
|
||||||
return NewIncomingContext(ctx, omd)
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendContext apends new md to context
|
||||||
|
func AppendContext(ctx context.Context, kv ...string) context.Context {
|
||||||
|
md, ok := Pairs(kv...)
|
||||||
|
if !ok {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
omd, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return NewContext(ctx, md)
|
||||||
|
}
|
||||||
|
for k, v := range md {
|
||||||
|
omd.Set(k, v)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ package metadata
|
|||||||
import (
|
import (
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -66,6 +67,14 @@ func (md Metadata) Iterator() *Iterator {
|
|||||||
return iter
|
return iter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (md Metadata) MustGet(key string) string {
|
||||||
|
val, ok := md.Get(key)
|
||||||
|
if !ok {
|
||||||
|
panic("missing metadata key")
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns value from metadata by key
|
// Get returns value from metadata by key
|
||||||
func (md Metadata) Get(key string) (string, bool) {
|
func (md Metadata) Get(key string) (string, bool) {
|
||||||
// fast path
|
// fast path
|
||||||
@@ -73,6 +82,9 @@ func (md Metadata) Get(key string) (string, bool) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
// slow path
|
// slow path
|
||||||
val, ok = md[textproto.CanonicalMIMEHeaderKey(key)]
|
val, ok = md[textproto.CanonicalMIMEHeaderKey(key)]
|
||||||
|
if !ok {
|
||||||
|
val, ok = md[strings.ToLower(key)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return val, ok
|
return val, ok
|
||||||
}
|
}
|
||||||
@@ -94,14 +106,23 @@ func (md Metadata) Del(keys ...string) {
|
|||||||
delete(md, key)
|
delete(md, key)
|
||||||
// slow path
|
// slow path
|
||||||
delete(md, textproto.CanonicalMIMEHeaderKey(key))
|
delete(md, textproto.CanonicalMIMEHeaderKey(key))
|
||||||
|
// very slow path
|
||||||
|
delete(md, strings.ToLower(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy makes a copy of the metadata
|
||||||
|
func (md Metadata) CopyTo(dst Metadata) {
|
||||||
|
for k, v := range md {
|
||||||
|
dst[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy makes a copy of the metadata
|
// Copy makes a copy of the metadata
|
||||||
func Copy(md Metadata, exclude ...string) Metadata {
|
func Copy(md Metadata, exclude ...string) Metadata {
|
||||||
nmd := New(len(md))
|
nmd := New(len(md))
|
||||||
for key, val := range md {
|
for k, v := range md {
|
||||||
nmd.Set(key, val)
|
nmd[k] = v
|
||||||
}
|
}
|
||||||
nmd.Del(exclude...)
|
nmd.Del(exclude...)
|
||||||
return nmd
|
return nmd
|
||||||
@@ -125,7 +146,7 @@ func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata {
|
|||||||
case ok && !overwrite:
|
case ok && !overwrite:
|
||||||
continue
|
continue
|
||||||
case val != "":
|
case val != "":
|
||||||
nmd.Set(key, val)
|
nmd[key] = val
|
||||||
case ok && val == "":
|
case ok && val == "":
|
||||||
nmd.Del(key)
|
nmd.Del(key)
|
||||||
}
|
}
|
||||||
@@ -139,6 +160,8 @@ func Pairs(kv ...string) (Metadata, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
md := New(len(kv) / 2)
|
md := New(len(kv) / 2)
|
||||||
md.Set(kv...)
|
for idx := 0; idx < len(kv); idx += 2 {
|
||||||
|
md[kv[idx]] = kv[idx+1]
|
||||||
|
}
|
||||||
return md, true
|
return md, true
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestLowercase(t *testing.T) {
|
||||||
|
md := New(1)
|
||||||
|
md["x-request-id"] = "12345"
|
||||||
|
v, ok := md.Get("X-Request-Id")
|
||||||
|
if !ok || v == "" {
|
||||||
|
t.Fatalf("metadata invalid %#+v", md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMultipleUsage(t *testing.T) {
|
func TestMultipleUsage(t *testing.T) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
md := New(0)
|
md := New(0)
|
||||||
@@ -23,8 +32,8 @@ func TestMultipleUsage(t *testing.T) {
|
|||||||
m.Del("key1_3")
|
m.Del("key1_3")
|
||||||
return ctx
|
return ctx
|
||||||
}(ctx)
|
}(ctx)
|
||||||
t.Logf("imd %#+v", imd)
|
_ = imd
|
||||||
t.Logf("omd %#+v", omd)
|
_ = omd
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetadataSetMultiple(t *testing.T) {
|
func TestMetadataSetMultiple(t *testing.T) {
|
||||||
@@ -79,6 +88,13 @@ func TestPassing(t *testing.T) {
|
|||||||
|
|
||||||
ctx = NewIncomingContext(ctx, md1)
|
ctx = NewIncomingContext(ctx, md1)
|
||||||
testCtx(ctx)
|
testCtx(ctx)
|
||||||
|
_, ok := FromOutgoingContext(ctx)
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("create outgoing context")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = NewOutgoingContext(ctx, New(1))
|
||||||
|
testCtx(ctx)
|
||||||
md, ok := FromOutgoingContext(ctx)
|
md, ok := FromOutgoingContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("missing metadata from outgoing context")
|
t.Fatalf("missing metadata from outgoing context")
|
||||||
@@ -102,7 +118,7 @@ func TestMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIterator(t *testing.T) {
|
func TestIterator(_ *testing.T) {
|
||||||
md := Metadata{
|
md := Metadata{
|
||||||
"1Last": "last",
|
"1Last": "last",
|
||||||
"2First": "first",
|
"2First": "first",
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Meter, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext get meter from context
|
||||||
|
func MustContext(ctx context.Context) Meter {
|
||||||
|
m, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing meter")
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put meter in context
|
// NewContext put meter in context
|
||||||
func NewContext(ctx context.Context, c Meter) context.Context {
|
func NewContext(ctx context.Context, c Meter) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultMeter is the default meter
|
// DefaultMeter is the default meter
|
||||||
DefaultMeter Meter = NewMeter()
|
DefaultMeter = NewMeter()
|
||||||
// DefaultAddress data will be made available on this host:port
|
// DefaultAddress data will be made available on this host:port
|
||||||
DefaultAddress = ":9090"
|
DefaultAddress = ":9090"
|
||||||
// DefaultPath the meter endpoint where the Meter data will be made available
|
// DefaultPath the meter endpoint where the Meter data will be made available
|
||||||
|
@@ -37,32 +37,32 @@ func (r *noopMeter) Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Counter implements the Meter interface
|
// Counter implements the Meter interface
|
||||||
func (r *noopMeter) Counter(name string, labels ...string) Counter {
|
func (r *noopMeter) Counter(_ string, labels ...string) Counter {
|
||||||
return &noopCounter{labels: labels}
|
return &noopCounter{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FloatCounter implements the Meter interface
|
// FloatCounter implements the Meter interface
|
||||||
func (r *noopMeter) FloatCounter(name string, labels ...string) FloatCounter {
|
func (r *noopMeter) FloatCounter(_ string, labels ...string) FloatCounter {
|
||||||
return &noopFloatCounter{labels: labels}
|
return &noopFloatCounter{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gauge implements the Meter interface
|
// Gauge implements the Meter interface
|
||||||
func (r *noopMeter) Gauge(name string, f func() float64, labels ...string) Gauge {
|
func (r *noopMeter) Gauge(_ string, _ func() float64, labels ...string) Gauge {
|
||||||
return &noopGauge{labels: labels}
|
return &noopGauge{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary implements the Meter interface
|
// Summary implements the Meter interface
|
||||||
func (r *noopMeter) Summary(name string, labels ...string) Summary {
|
func (r *noopMeter) Summary(_ string, labels ...string) Summary {
|
||||||
return &noopSummary{labels: labels}
|
return &noopSummary{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SummaryExt implements the Meter interface
|
// SummaryExt implements the Meter interface
|
||||||
func (r *noopMeter) SummaryExt(name string, window time.Duration, quantiles []float64, labels ...string) Summary {
|
func (r *noopMeter) SummaryExt(_ string, _ time.Duration, _ []float64, labels ...string) Summary {
|
||||||
return &noopSummary{labels: labels}
|
return &noopSummary{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Histogram implements the Meter interface
|
// Histogram implements the Meter interface
|
||||||
func (r *noopMeter) Histogram(name string, labels ...string) Histogram {
|
func (r *noopMeter) Histogram(_ string, labels ...string) Histogram {
|
||||||
return &noopHistogram{labels: labels}
|
return &noopHistogram{labels: labels}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ func (r *noopMeter) Set(opts ...Option) Meter {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *noopMeter) Write(w io.Writer, opts ...Option) error {
|
func (r *noopMeter) Write(_ io.Writer, _ ...Option) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
micro.go
36
micro.go
@@ -65,6 +65,8 @@ func As(b any, target any) bool {
|
|||||||
break
|
break
|
||||||
case targetType.Implements(routerType):
|
case targetType.Implements(routerType):
|
||||||
break
|
break
|
||||||
|
case targetType.Implements(tracerType):
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -76,19 +78,21 @@ func As(b any, target any) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var brokerType = reflect.TypeOf((*broker.Broker)(nil)).Elem()
|
var (
|
||||||
var loggerType = reflect.TypeOf((*logger.Logger)(nil)).Elem()
|
brokerType = reflect.TypeOf((*broker.Broker)(nil)).Elem()
|
||||||
var clientType = reflect.TypeOf((*client.Client)(nil)).Elem()
|
loggerType = reflect.TypeOf((*logger.Logger)(nil)).Elem()
|
||||||
var serverType = reflect.TypeOf((*server.Server)(nil)).Elem()
|
clientType = reflect.TypeOf((*client.Client)(nil)).Elem()
|
||||||
var codecType = reflect.TypeOf((*codec.Codec)(nil)).Elem()
|
serverType = reflect.TypeOf((*server.Server)(nil)).Elem()
|
||||||
var flowType = reflect.TypeOf((*flow.Flow)(nil)).Elem()
|
codecType = reflect.TypeOf((*codec.Codec)(nil)).Elem()
|
||||||
var fsmType = reflect.TypeOf((*fsm.FSM)(nil)).Elem()
|
flowType = reflect.TypeOf((*flow.Flow)(nil)).Elem()
|
||||||
var meterType = reflect.TypeOf((*meter.Meter)(nil)).Elem()
|
fsmType = reflect.TypeOf((*fsm.FSM)(nil)).Elem()
|
||||||
var registerType = reflect.TypeOf((*register.Register)(nil)).Elem()
|
meterType = reflect.TypeOf((*meter.Meter)(nil)).Elem()
|
||||||
var resolverType = reflect.TypeOf((*resolver.Resolver)(nil)).Elem()
|
registerType = reflect.TypeOf((*register.Register)(nil)).Elem()
|
||||||
var routerType = reflect.TypeOf((*router.Router)(nil)).Elem()
|
resolverType = reflect.TypeOf((*resolver.Resolver)(nil)).Elem()
|
||||||
var selectorType = reflect.TypeOf((*selector.Selector)(nil)).Elem()
|
routerType = reflect.TypeOf((*router.Router)(nil)).Elem()
|
||||||
var storeType = reflect.TypeOf((*store.Store)(nil)).Elem()
|
selectorType = reflect.TypeOf((*selector.Selector)(nil)).Elem()
|
||||||
var syncType = reflect.TypeOf((*sync.Sync)(nil)).Elem()
|
storeType = reflect.TypeOf((*store.Store)(nil)).Elem()
|
||||||
var tracerType = reflect.TypeOf((*tracer.Tracer)(nil)).Elem()
|
syncType = reflect.TypeOf((*sync.Sync)(nil)).Elem()
|
||||||
var serviceType = reflect.TypeOf((*Service)(nil)).Elem()
|
tracerType = reflect.TypeOf((*tracer.Tracer)(nil)).Elem()
|
||||||
|
serviceType = reflect.TypeOf((*Service)(nil)).Elem()
|
||||||
|
)
|
||||||
|
@@ -18,26 +18,27 @@ func TestAs(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
b any
|
b any
|
||||||
target any
|
target any
|
||||||
match bool
|
|
||||||
want any
|
want any
|
||||||
|
|
||||||
|
match bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
broTarget,
|
b: broTarget,
|
||||||
&b,
|
target: &b,
|
||||||
true,
|
match: true,
|
||||||
broTarget,
|
want: broTarget,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nil,
|
b: nil,
|
||||||
&b,
|
target: &b,
|
||||||
false,
|
match: false,
|
||||||
nil,
|
want: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fsmTarget,
|
b: fsmTarget,
|
||||||
&b,
|
target: &b,
|
||||||
false,
|
match: false,
|
||||||
nil,
|
want: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
@@ -72,7 +73,7 @@ func (p *bro) Ready() bool { return true }
|
|||||||
|
|
||||||
func (p *bro) Health() bool { return true }
|
func (p *bro) Health() bool { return true }
|
||||||
|
|
||||||
func (p *bro) Init(opts ...broker.Option) error { return nil }
|
func (p *bro) Init(_ ...broker.Option) error { return nil }
|
||||||
|
|
||||||
// Options returns broker options
|
// Options returns broker options
|
||||||
func (p *bro) Options() broker.Options { return broker.Options{} }
|
func (p *bro) Options() broker.Options { return broker.Options{} }
|
||||||
@@ -81,28 +82,28 @@ func (p *bro) Options() broker.Options { return broker.Options{} }
|
|||||||
func (p *bro) Address() string { return "" }
|
func (p *bro) Address() string { return "" }
|
||||||
|
|
||||||
// Connect connects to broker
|
// Connect connects to broker
|
||||||
func (p *bro) Connect(ctx context.Context) error { return nil }
|
func (p *bro) Connect(_ context.Context) error { return nil }
|
||||||
|
|
||||||
// Disconnect disconnect from broker
|
// Disconnect disconnect from broker
|
||||||
func (p *bro) Disconnect(ctx context.Context) error { return nil }
|
func (p *bro) Disconnect(_ context.Context) error { return nil }
|
||||||
|
|
||||||
// Publish message, msg can be single broker.Message or []broker.Message
|
// Publish message, msg can be single broker.Message or []broker.Message
|
||||||
func (p *bro) Publish(ctx context.Context, topic string, msg *broker.Message, opts ...broker.PublishOption) error {
|
func (p *bro) Publish(_ context.Context, _ string, _ *broker.Message, _ ...broker.PublishOption) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchPublish messages to broker with multiple topics
|
// BatchPublish messages to broker with multiple topics
|
||||||
func (p *bro) BatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
|
func (p *bro) BatchPublish(_ context.Context, _ []*broker.Message, _ ...broker.PublishOption) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchSubscribe subscribes to topic messages via handler
|
// BatchSubscribe subscribes to topic messages via handler
|
||||||
func (p *bro) BatchSubscribe(ctx context.Context, topic string, h broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
func (p *bro) BatchSubscribe(_ context.Context, _ string, _ broker.BatchHandler, _ ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe subscribes to topic message via handler
|
// Subscribe subscribes to topic message via handler
|
||||||
func (p *bro) Subscribe(ctx context.Context, topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
func (p *bro) Subscribe(_ context.Context, _ string, _ broker.Handler, _ ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +114,9 @@ type fsmT struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fsmT) Start(ctx context.Context, a interface{}, o ...Option) (interface{}, error) {
|
func (f *fsmT) Start(_ context.Context, _ interface{}, _ ...Option) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (f *fsmT) Current() string { return f.name }
|
func (f *fsmT) Current() string { return f.name }
|
||||||
func (f *fsmT) Reset() {}
|
func (f *fsmT) Reset() {}
|
||||||
func (f *fsmT) State(s string, sf fsm.StateFunc) {}
|
func (f *fsmT) State(_ string, _ fsm.StateFunc) {}
|
||||||
|
@@ -8,19 +8,20 @@ import (
|
|||||||
|
|
||||||
// CertificateOptions holds options for x509.CreateCertificate
|
// CertificateOptions holds options for x509.CreateCertificate
|
||||||
type CertificateOptions struct {
|
type CertificateOptions struct {
|
||||||
Organization []string
|
|
||||||
OrganizationalUnit []string
|
|
||||||
CommonName string
|
|
||||||
OCSPServer []string
|
|
||||||
IssuingCertificateURL []string
|
|
||||||
SerialNumber *big.Int
|
SerialNumber *big.Int
|
||||||
NotAfter time.Time
|
NotAfter time.Time
|
||||||
NotBefore time.Time
|
NotBefore time.Time
|
||||||
SignatureAlgorithm x509.SignatureAlgorithm
|
CommonName string
|
||||||
PublicKeyAlgorithm x509.PublicKeyAlgorithm
|
Organization []string
|
||||||
|
OrganizationalUnit []string
|
||||||
|
OCSPServer []string
|
||||||
|
IssuingCertificateURL []string
|
||||||
ExtKeyUsage []x509.ExtKeyUsage
|
ExtKeyUsage []x509.ExtKeyUsage
|
||||||
KeyUsage x509.KeyUsage
|
|
||||||
IsCA bool
|
SignatureAlgorithm x509.SignatureAlgorithm
|
||||||
|
PublicKeyAlgorithm x509.PublicKeyAlgorithm
|
||||||
|
KeyUsage x509.KeyUsage
|
||||||
|
IsCA bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertificateOrganizationalUnit set OrganizationalUnit in certificate subject
|
// CertificateOrganizationalUnit set OrganizationalUnit in certificate subject
|
||||||
|
@@ -119,7 +119,7 @@ func Tracer(t tracer.Tracer) Option {
|
|||||||
// NewOptions returns network default options
|
// NewOptions returns network default options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
ID: id.Must(),
|
ID: id.MustNew(),
|
||||||
Name: "go.micro",
|
Name: "go.micro",
|
||||||
Address: ":0",
|
Address: ":0",
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultTransport is the global default transport
|
// DefaultTransport is the global default transport
|
||||||
DefaultTransport Transport = NewTransport()
|
DefaultTransport = NewTransport()
|
||||||
// DefaultDialTimeout the default dial timeout
|
// DefaultDialTimeout the default dial timeout
|
||||||
DefaultDialTimeout = time.Second * 5
|
DefaultDialTimeout = time.Second * 5
|
||||||
)
|
)
|
||||||
|
@@ -84,7 +84,7 @@ func (t *tunBroker) Disconnect(ctx context.Context) error {
|
|||||||
return t.tunnel.Close(ctx)
|
return t.tunnel.Close(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tunBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
|
func (t *tunBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, _ ...broker.PublishOption) error {
|
||||||
// TODO: this is probably inefficient, we might want to just maintain an open connection
|
// TODO: this is probably inefficient, we might want to just maintain an open connection
|
||||||
// it may be easier to add broadcast to the tunnel
|
// it may be easier to add broadcast to the tunnel
|
||||||
topicMap := make(map[string]tunnel.Session)
|
topicMap := make(map[string]tunnel.Session)
|
||||||
@@ -114,7 +114,7 @@ func (t *tunBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, op
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tunBroker) Publish(ctx context.Context, topic string, m *broker.Message, opts ...broker.PublishOption) error {
|
func (t *tunBroker) Publish(ctx context.Context, topic string, m *broker.Message, _ ...broker.PublishOption) error {
|
||||||
// TODO: this is probably inefficient, we might want to just maintain an open connection
|
// TODO: this is probably inefficient, we might want to just maintain an open connection
|
||||||
// it may be easier to add broadcast to the tunnel
|
// it may be easier to add broadcast to the tunnel
|
||||||
c, err := t.tunnel.Dial(ctx, topic, tunnel.DialMode(tunnel.Multicast))
|
c, err := t.tunnel.Dial(ctx, topic, tunnel.DialMode(tunnel.Multicast))
|
||||||
|
@@ -164,7 +164,7 @@ func DialWait(b bool) DialOption {
|
|||||||
// NewOptions returns router default options with filled values
|
// NewOptions returns router default options with filled values
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
ID: id.Must(),
|
ID: id.MustNew(),
|
||||||
Address: DefaultAddress,
|
Address: DefaultAddress,
|
||||||
Token: DefaultToken,
|
Token: DefaultToken,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -294,8 +294,8 @@ type loggerOptions struct {
|
|||||||
brokers []string
|
brokers []string
|
||||||
registers []string
|
registers []string
|
||||||
stores []string
|
stores []string
|
||||||
meters []string
|
// meters []string
|
||||||
tracers []string
|
tracers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -15,6 +15,6 @@ func (p *noopProfiler) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewProfiler returns new noop profiler
|
// NewProfiler returns new noop profiler
|
||||||
func NewProfiler(opts ...Option) Profiler {
|
func NewProfiler(_ ...Option) Profiler {
|
||||||
return &noopProfiler{}
|
return &noopProfiler{}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ type Profiler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DefaultProfiler holds the default profiler
|
// DefaultProfiler holds the default profiler
|
||||||
var DefaultProfiler Profiler = NewProfiler()
|
var DefaultProfiler = NewProfiler()
|
||||||
|
|
||||||
// Options holds the options for profiler
|
// Options holds the options for profiler
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Register, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext get register from context
|
||||||
|
func MustContext(ctx context.Context) Register {
|
||||||
|
r, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing register")
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put register in context
|
// NewContext put register in context
|
||||||
func NewContext(ctx context.Context, c Register) context.Context {
|
func NewContext(ctx context.Context, c Register) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
package register
|
package register
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtractValue from reflect.Type from specified depth
|
// ExtractValue from reflect.Type from specified depth
|
||||||
@@ -38,53 +35,6 @@ func ExtractValue(v reflect.Type, d int) string {
|
|||||||
return v.Name()
|
return v.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractEndpoint extract *Endpoint from reflect.Method
|
|
||||||
func ExtractEndpoint(method reflect.Method) *Endpoint {
|
|
||||||
if method.PkgPath != "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rspType, reqType reflect.Type
|
|
||||||
var stream bool
|
|
||||||
mt := method.Type
|
|
||||||
|
|
||||||
switch mt.NumIn() {
|
|
||||||
case 3:
|
|
||||||
reqType = mt.In(1)
|
|
||||||
rspType = mt.In(2)
|
|
||||||
case 4:
|
|
||||||
reqType = mt.In(2)
|
|
||||||
rspType = mt.In(3)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// are we dealing with a stream?
|
|
||||||
switch rspType.Kind() {
|
|
||||||
case reflect.Func, reflect.Interface:
|
|
||||||
stream = true
|
|
||||||
}
|
|
||||||
|
|
||||||
request := ExtractValue(reqType, 0)
|
|
||||||
response := ExtractValue(rspType, 0)
|
|
||||||
if request == "" || response == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ep := &Endpoint{
|
|
||||||
Name: method.Name,
|
|
||||||
Request: request,
|
|
||||||
Response: response,
|
|
||||||
Metadata: metadata.New(0),
|
|
||||||
}
|
|
||||||
|
|
||||||
if stream {
|
|
||||||
ep.Metadata.Set("stream", fmt.Sprintf("%v", stream))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ep
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractSubValue exctact *Value from reflect.Type
|
// ExtractSubValue exctact *Value from reflect.Type
|
||||||
func ExtractSubValue(typ reflect.Type) string {
|
func ExtractSubValue(typ reflect.Type) string {
|
||||||
var reqType reflect.Type
|
var reqType reflect.Type
|
||||||
|
@@ -2,8 +2,6 @@ package register
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestHandler struct{}
|
type TestHandler struct{}
|
||||||
@@ -15,40 +13,3 @@ type TestResponse struct{}
|
|||||||
func (t *TestHandler) Test(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
|
func (t *TestHandler) Test(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractEndpoint(t *testing.T) {
|
|
||||||
handler := &TestHandler{}
|
|
||||||
typ := reflect.TypeOf(handler)
|
|
||||||
|
|
||||||
var endpoints []*Endpoint
|
|
||||||
|
|
||||||
for m := 0; m < typ.NumMethod(); m++ {
|
|
||||||
if e := ExtractEndpoint(typ.Method(m)); e != nil {
|
|
||||||
endpoints = append(endpoints, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if i := len(endpoints); i != 1 {
|
|
||||||
t.Fatalf("Expected 1 endpoint, have %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints[0].Name != "Test" {
|
|
||||||
t.Fatalf("Expected handler Test, got %s", endpoints[0].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints[0].Request == "" {
|
|
||||||
t.Fatal("Expected non nil Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints[0].Response == "" {
|
|
||||||
t.Fatal("Expected non nil Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints[0].Request != "TestRequest" {
|
|
||||||
t.Fatalf("Expected TestRequest got %s", endpoints[0].Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints[0].Response != "TestResponse" {
|
|
||||||
t.Fatalf("Expected TestResponse got %s", endpoints[0].Response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -23,18 +23,17 @@ type node struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type record struct {
|
type record struct {
|
||||||
Name string
|
Name string
|
||||||
Version string
|
Version string
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
Nodes map[string]*node
|
Nodes map[string]*node
|
||||||
Endpoints []*register.Endpoint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type memory struct {
|
type memory struct {
|
||||||
sync.RWMutex
|
|
||||||
records map[string]services
|
records map[string]services
|
||||||
watchers map[string]*watcher
|
watchers map[string]*watcher
|
||||||
opts register.Options
|
opts register.Options
|
||||||
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// services is a KV map with service name as the key and a map of records as the value
|
// services is a KV map with service name as the key and a map of records as the value
|
||||||
@@ -59,7 +58,7 @@ func (m *memory) ttlPrune() {
|
|||||||
|
|
||||||
for range prune.C {
|
for range prune.C {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
for domain, services := range m.records {
|
for namespace, services := range m.records {
|
||||||
for service, versions := range services {
|
for service, versions := range services {
|
||||||
for version, record := range versions {
|
for version, record := range versions {
|
||||||
for id, n := range record.Nodes {
|
for id, n := range record.Nodes {
|
||||||
@@ -67,7 +66,7 @@ func (m *memory) ttlPrune() {
|
|||||||
if m.opts.Logger.V(logger.DebugLevel) {
|
if m.opts.Logger.V(logger.DebugLevel) {
|
||||||
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register TTL expired for node %s of service %s", n.ID, service))
|
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register TTL expired for node %s of service %s", n.ID, service))
|
||||||
}
|
}
|
||||||
delete(m.records[domain][service][version].Nodes, id)
|
delete(m.records[namespace][service][version].Nodes, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,11 +99,11 @@ func (m *memory) sendEvent(r *register.Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memory) Connect(ctx context.Context) error {
|
func (m *memory) Connect(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memory) Disconnect(ctx context.Context) error {
|
func (m *memory) Disconnect(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,24 +123,19 @@ func (m *memory) Options() register.Options {
|
|||||||
return m.opts
|
return m.opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memory) Register(ctx context.Context, s *register.Service, opts ...register.RegisterOption) error {
|
func (m *memory) Register(_ context.Context, s *register.Service, opts ...register.RegisterOption) error {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
defer m.Unlock()
|
defer m.Unlock()
|
||||||
|
|
||||||
options := register.NewRegisterOptions(opts...)
|
options := register.NewRegisterOptions(opts...)
|
||||||
|
|
||||||
// get the services for this domain from the register
|
// get the services for this domain from the register
|
||||||
srvs, ok := m.records[options.Domain]
|
srvs, ok := m.records[options.Namespace]
|
||||||
if !ok {
|
if !ok {
|
||||||
srvs = make(services)
|
srvs = make(services)
|
||||||
}
|
}
|
||||||
|
|
||||||
// domain is set in metadata so it can be passed to watchers
|
s.Namespace = options.Namespace
|
||||||
if s.Metadata == nil {
|
|
||||||
s.Metadata = map[string]string{"domain": options.Domain}
|
|
||||||
} else {
|
|
||||||
s.Metadata["domain"] = options.Domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the service name exists
|
// ensure the service name exists
|
||||||
r := serviceToRecord(s, options.TTL)
|
r := serviceToRecord(s, options.TTL)
|
||||||
@@ -154,8 +148,8 @@ func (m *memory) Register(ctx context.Context, s *register.Service, opts ...regi
|
|||||||
if m.opts.Logger.V(logger.DebugLevel) {
|
if m.opts.Logger.V(logger.DebugLevel) {
|
||||||
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register added new service: %s, version: %s", s.Name, s.Version))
|
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register added new service: %s, version: %s", s.Name, s.Version))
|
||||||
}
|
}
|
||||||
m.records[options.Domain] = srvs
|
m.records[options.Namespace] = srvs
|
||||||
go m.sendEvent(®ister.Result{Action: "create", Service: s})
|
go m.sendEvent(®ister.Result{Action: register.EventCreate, Service: s})
|
||||||
}
|
}
|
||||||
|
|
||||||
var addedNodes bool
|
var addedNodes bool
|
||||||
@@ -173,9 +167,6 @@ func (m *memory) Register(ctx context.Context, s *register.Service, opts ...regi
|
|||||||
metadata[k] = v
|
metadata[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the domain
|
|
||||||
metadata["domain"] = options.Domain
|
|
||||||
|
|
||||||
// add the node
|
// add the node
|
||||||
srvs[s.Name][s.Version].Nodes[n.ID] = &node{
|
srvs[s.Name][s.Version].Nodes[n.ID] = &node{
|
||||||
Node: ®ister.Node{
|
Node: ®ister.Node{
|
||||||
@@ -194,7 +185,7 @@ func (m *memory) Register(ctx context.Context, s *register.Service, opts ...regi
|
|||||||
if m.opts.Logger.V(logger.DebugLevel) {
|
if m.opts.Logger.V(logger.DebugLevel) {
|
||||||
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register added new node to service: %s, version: %s", s.Name, s.Version))
|
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register added new node to service: %s, version: %s", s.Name, s.Version))
|
||||||
}
|
}
|
||||||
go m.sendEvent(®ister.Result{Action: "update", Service: s})
|
go m.sendEvent(®ister.Result{Action: register.EventUpdate, Service: s})
|
||||||
} else {
|
} else {
|
||||||
// refresh TTL and timestamp
|
// refresh TTL and timestamp
|
||||||
for _, n := range s.Nodes {
|
for _, n := range s.Nodes {
|
||||||
@@ -206,7 +197,7 @@ func (m *memory) Register(ctx context.Context, s *register.Service, opts ...regi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.records[options.Domain] = srvs
|
m.records[options.Namespace] = srvs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,15 +207,8 @@ func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...re
|
|||||||
|
|
||||||
options := register.NewDeregisterOptions(opts...)
|
options := register.NewDeregisterOptions(opts...)
|
||||||
|
|
||||||
// domain is set in metadata so it can be passed to watchers
|
|
||||||
if s.Metadata == nil {
|
|
||||||
s.Metadata = map[string]string{"domain": options.Domain}
|
|
||||||
} else {
|
|
||||||
s.Metadata["domain"] = options.Domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the domain doesn't exist, there is nothing to deregister
|
// if the domain doesn't exist, there is nothing to deregister
|
||||||
services, ok := m.records[options.Domain]
|
services, ok := m.records[options.Namespace]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -253,16 +237,16 @@ func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...re
|
|||||||
// if the nodes not empty, we replace the version in the store and exist, the rest of the logic
|
// if the nodes not empty, we replace the version in the store and exist, the rest of the logic
|
||||||
// is cleanup
|
// is cleanup
|
||||||
if len(version.Nodes) > 0 {
|
if len(version.Nodes) > 0 {
|
||||||
m.records[options.Domain][s.Name][s.Version] = version
|
m.records[options.Namespace][s.Name][s.Version] = version
|
||||||
go m.sendEvent(®ister.Result{Action: "update", Service: s})
|
go m.sendEvent(®ister.Result{Action: register.EventUpdate, Service: s})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this version was the only version of the service, we can remove the whole service from the
|
// if this version was the only version of the service, we can remove the whole service from the
|
||||||
// register and exit
|
// register and exit
|
||||||
if len(versions) == 1 {
|
if len(versions) == 1 {
|
||||||
delete(m.records[options.Domain], s.Name)
|
delete(m.records[options.Namespace], s.Name)
|
||||||
go m.sendEvent(®ister.Result{Action: "delete", Service: s})
|
go m.sendEvent(®ister.Result{Action: register.EventDelete, Service: s})
|
||||||
|
|
||||||
if m.opts.Logger.V(logger.DebugLevel) {
|
if m.opts.Logger.V(logger.DebugLevel) {
|
||||||
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register removed service: %s", s.Name))
|
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register removed service: %s", s.Name))
|
||||||
@@ -271,8 +255,8 @@ func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// there are other versions of the service running, so only remove this version of it
|
// there are other versions of the service running, so only remove this version of it
|
||||||
delete(m.records[options.Domain][s.Name], s.Version)
|
delete(m.records[options.Namespace][s.Name], s.Version)
|
||||||
go m.sendEvent(®ister.Result{Action: "delete", Service: s})
|
go m.sendEvent(®ister.Result{Action: register.EventDelete, Service: s})
|
||||||
if m.opts.Logger.V(logger.DebugLevel) {
|
if m.opts.Logger.V(logger.DebugLevel) {
|
||||||
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register removed service: %s, version: %s", s.Name, s.Version))
|
m.opts.Logger.Debug(m.opts.Context, fmt.Sprintf("Register removed service: %s, version: %s", s.Name, s.Version))
|
||||||
}
|
}
|
||||||
@@ -284,15 +268,15 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...registe
|
|||||||
options := register.NewLookupOptions(opts...)
|
options := register.NewLookupOptions(opts...)
|
||||||
|
|
||||||
// if it's a wildcard domain, return from all domains
|
// if it's a wildcard domain, return from all domains
|
||||||
if options.Domain == register.WildcardDomain {
|
if options.Namespace == register.WildcardNamespace {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
recs := m.records
|
recs := m.records
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
|
|
||||||
var services []*register.Service
|
var services []*register.Service
|
||||||
|
|
||||||
for domain := range recs {
|
for namespace := range recs {
|
||||||
srvs, err := m.LookupService(ctx, name, append(opts, register.LookupDomain(domain))...)
|
srvs, err := m.LookupService(ctx, name, append(opts, register.LookupNamespace(namespace))...)
|
||||||
if err == register.ErrNotFound {
|
if err == register.ErrNotFound {
|
||||||
continue
|
continue
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -311,7 +295,7 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...registe
|
|||||||
defer m.RUnlock()
|
defer m.RUnlock()
|
||||||
|
|
||||||
// check the domain exists
|
// check the domain exists
|
||||||
services, ok := m.records[options.Domain]
|
services, ok := m.records[options.Namespace]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, register.ErrNotFound
|
return nil, register.ErrNotFound
|
||||||
}
|
}
|
||||||
@@ -328,7 +312,7 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...registe
|
|||||||
var i int
|
var i int
|
||||||
|
|
||||||
for _, r := range versions {
|
for _, r := range versions {
|
||||||
result[i] = recordToService(r, options.Domain)
|
result[i] = recordToService(r, options.Namespace)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,15 +323,15 @@ func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption)
|
|||||||
options := register.NewListOptions(opts...)
|
options := register.NewListOptions(opts...)
|
||||||
|
|
||||||
// if it's a wildcard domain, list from all domains
|
// if it's a wildcard domain, list from all domains
|
||||||
if options.Domain == register.WildcardDomain {
|
if options.Namespace == register.WildcardNamespace {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
recs := m.records
|
recs := m.records
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
|
|
||||||
var services []*register.Service
|
var services []*register.Service
|
||||||
|
|
||||||
for domain := range recs {
|
for namespace := range recs {
|
||||||
srvs, err := m.ListServices(ctx, append(opts, register.ListDomain(domain))...)
|
srvs, err := m.ListServices(ctx, append(opts, register.ListNamespace(namespace))...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -361,7 +345,7 @@ func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption)
|
|||||||
defer m.RUnlock()
|
defer m.RUnlock()
|
||||||
|
|
||||||
// ensure the domain exists
|
// ensure the domain exists
|
||||||
services, ok := m.records[options.Domain]
|
services, ok := m.records[options.Namespace]
|
||||||
if !ok {
|
if !ok {
|
||||||
return make([]*register.Service, 0), nil
|
return make([]*register.Service, 0), nil
|
||||||
}
|
}
|
||||||
@@ -371,7 +355,7 @@ func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption)
|
|||||||
|
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
for _, version := range service {
|
for _, version := range service {
|
||||||
result = append(result, recordToService(version, options.Domain))
|
result = append(result, recordToService(version, options.Namespace))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,16 +410,13 @@ func (m *watcher) Next() (*register.Result, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract domain from service metadata
|
namespace := register.DefaultNamespace
|
||||||
var domain string
|
if r.Service.Namespace != "" {
|
||||||
if r.Service.Metadata != nil && len(r.Service.Metadata["domain"]) > 0 {
|
namespace = r.Service.Namespace
|
||||||
domain = r.Service.Metadata["domain"]
|
|
||||||
} else {
|
|
||||||
domain = register.DefaultDomain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// only send the event if watching the wildcard or this specific domain
|
// only send the event if watching the wildcard or this specific domain
|
||||||
if m.wo.Domain == register.WildcardDomain || m.wo.Domain == domain {
|
if m.wo.Namespace == register.WildcardNamespace || m.wo.Namespace == namespace {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
case <-m.exit:
|
case <-m.exit:
|
||||||
@@ -454,11 +435,6 @@ func (m *watcher) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serviceToRecord(s *register.Service, ttl time.Duration) *record {
|
func serviceToRecord(s *register.Service, ttl time.Duration) *record {
|
||||||
metadata := make(map[string]string, len(s.Metadata))
|
|
||||||
for k, v := range s.Metadata {
|
|
||||||
metadata[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make(map[string]*node, len(s.Nodes))
|
nodes := make(map[string]*node, len(s.Nodes))
|
||||||
for _, n := range s.Nodes {
|
for _, n := range s.Nodes {
|
||||||
nodes[n.ID] = &node{
|
nodes[n.ID] = &node{
|
||||||
@@ -468,44 +444,19 @@ func serviceToRecord(s *register.Service, ttl time.Duration) *record {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints := make([]*register.Endpoint, len(s.Endpoints))
|
|
||||||
for i, e := range s.Endpoints {
|
|
||||||
endpoints[i] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
return &record{
|
return &record{
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Version: s.Version,
|
Version: s.Version,
|
||||||
Metadata: metadata,
|
Nodes: nodes,
|
||||||
Nodes: nodes,
|
|
||||||
Endpoints: endpoints,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordToService(r *record, domain string) *register.Service {
|
func recordToService(r *record, namespace string) *register.Service {
|
||||||
metadata := make(map[string]string, len(r.Metadata))
|
metadata := make(map[string]string, len(r.Metadata))
|
||||||
for k, v := range r.Metadata {
|
for k, v := range r.Metadata {
|
||||||
metadata[k] = v
|
metadata[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the domain in metadata so it can be determined when a wildcard query is performed
|
|
||||||
metadata["domain"] = domain
|
|
||||||
|
|
||||||
endpoints := make([]*register.Endpoint, len(r.Endpoints))
|
|
||||||
for i, e := range r.Endpoints {
|
|
||||||
md := make(map[string]string, len(e.Metadata))
|
|
||||||
for k, v := range e.Metadata {
|
|
||||||
md[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints[i] = ®ister.Endpoint{
|
|
||||||
Name: e.Name,
|
|
||||||
Request: e.Request,
|
|
||||||
Response: e.Response,
|
|
||||||
Metadata: md,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make([]*register.Node, len(r.Nodes))
|
nodes := make([]*register.Node, len(r.Nodes))
|
||||||
i := 0
|
i := 0
|
||||||
for _, n := range r.Nodes {
|
for _, n := range r.Nodes {
|
||||||
@@ -525,8 +476,7 @@ func recordToService(r *record, domain string) *register.Service {
|
|||||||
return ®ister.Service{
|
return ®ister.Service{
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Version: r.Version,
|
Version: r.Version,
|
||||||
Metadata: metadata,
|
|
||||||
Endpoints: endpoints,
|
|
||||||
Nodes: nodes,
|
Nodes: nodes,
|
||||||
|
Namespace: namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -208,9 +208,9 @@ func TestMemoryRegistryTTLConcurrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
// if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||||
// t.Logf("test will wait %v, then check TTL timeouts", waitTime)
|
// t.Logf("test will wait %v, then check TTL timeouts", waitTime)
|
||||||
//}
|
// }
|
||||||
|
|
||||||
errChan := make(chan error, concurrency)
|
errChan := make(chan error, concurrency)
|
||||||
syncChan := make(chan struct{})
|
syncChan := make(chan struct{})
|
||||||
@@ -253,32 +253,32 @@ func TestMemoryWildcard(t *testing.T) {
|
|||||||
|
|
||||||
testSrv := ®ister.Service{Name: "foo", Version: "1.0.0"}
|
testSrv := ®ister.Service{Name: "foo", Version: "1.0.0"}
|
||||||
|
|
||||||
if err := m.Register(ctx, testSrv, register.RegisterDomain("one")); err != nil {
|
if err := m.Register(ctx, testSrv, register.RegisterNamespace("one")); err != nil {
|
||||||
t.Fatalf("Register err: %v", err)
|
t.Fatalf("Register err: %v", err)
|
||||||
}
|
}
|
||||||
if err := m.Register(ctx, testSrv, register.RegisterDomain("two")); err != nil {
|
if err := m.Register(ctx, testSrv, register.RegisterNamespace("two")); err != nil {
|
||||||
t.Fatalf("Register err: %v", err)
|
t.Fatalf("Register err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if recs, err := m.ListServices(ctx, register.ListDomain("one")); err != nil {
|
if recs, err := m.ListServices(ctx, register.ListNamespace("one")); err != nil {
|
||||||
t.Errorf("List err: %v", err)
|
t.Errorf("List err: %v", err)
|
||||||
} else if len(recs) != 1 {
|
} else if len(recs) != 1 {
|
||||||
t.Errorf("Expected 1 record, got %v", len(recs))
|
t.Errorf("Expected 1 record, got %v", len(recs))
|
||||||
}
|
}
|
||||||
|
|
||||||
if recs, err := m.ListServices(ctx, register.ListDomain("*")); err != nil {
|
if recs, err := m.ListServices(ctx, register.ListNamespace("*")); err != nil {
|
||||||
t.Errorf("List err: %v", err)
|
t.Errorf("List err: %v", err)
|
||||||
} else if len(recs) != 2 {
|
} else if len(recs) != 2 {
|
||||||
t.Errorf("Expected 2 records, got %v", len(recs))
|
t.Errorf("Expected 2 records, got %v", len(recs))
|
||||||
}
|
}
|
||||||
|
|
||||||
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupDomain("one")); err != nil {
|
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupNamespace("one")); err != nil {
|
||||||
t.Errorf("Lookup err: %v", err)
|
t.Errorf("Lookup err: %v", err)
|
||||||
} else if len(recs) != 1 {
|
} else if len(recs) != 1 {
|
||||||
t.Errorf("Expected 1 record, got %v", len(recs))
|
t.Errorf("Expected 1 record, got %v", len(recs))
|
||||||
}
|
}
|
||||||
|
|
||||||
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupDomain("*")); err != nil {
|
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupNamespace("*")); err != nil {
|
||||||
t.Errorf("Lookup err: %v", err)
|
t.Errorf("Lookup err: %v", err)
|
||||||
} else if len(recs) != 2 {
|
} else if len(recs) != 2 {
|
||||||
t.Errorf("Expected 2 records, got %v", len(recs))
|
t.Errorf("Expected 2 records, got %v", len(recs))
|
||||||
@@ -290,27 +290,29 @@ func TestWatcher(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
m := NewRegister()
|
m := NewRegister()
|
||||||
m.Init()
|
if err := m.Init(); err != nil {
|
||||||
m.Connect(ctx)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := m.Connect(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
wc, err := m.Watch(ctx)
|
wc, err := m.Watch(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cant watch: %v", err)
|
t.Fatalf("cant watch: %v", err)
|
||||||
}
|
}
|
||||||
defer wc.Stop()
|
defer wc.Stop()
|
||||||
|
|
||||||
|
cherr := make(chan error, 10)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
_, err := wc.Next()
|
||||||
_, err := wc.Next()
|
if err != nil {
|
||||||
if err != nil {
|
cherr <- fmt.Errorf("unexpected err %v", err)
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
// t.Logf("changes %#+v", ch.Service)
|
|
||||||
wc.Stop()
|
|
||||||
wg.Done()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
// t.Logf("changes %#+v", ch.Service)
|
||||||
|
wc.Stop()
|
||||||
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := m.Register(ctx, testSrv); err != nil {
|
if err := m.Register(ctx, testSrv); err != nil {
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/codec"
|
||||||
"go.unistack.org/micro/v3/logger"
|
"go.unistack.org/micro/v3/logger"
|
||||||
"go.unistack.org/micro/v3/meter"
|
"go.unistack.org/micro/v3/meter"
|
||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
@@ -26,6 +27,8 @@ type Options struct {
|
|||||||
Name string
|
Name string
|
||||||
// Addrs specifies register addrs
|
// Addrs specifies register addrs
|
||||||
Addrs []string
|
Addrs []string
|
||||||
|
// Codec used to marshal/unmarshal data in register
|
||||||
|
Codec codec.Codec
|
||||||
// Timeout specifies timeout
|
// Timeout specifies timeout
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
|
Codec: codec.NewCodec(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -46,17 +50,17 @@ func NewOptions(opts ...Option) Options {
|
|||||||
|
|
||||||
// RegisterOptions holds options for register method
|
// RegisterOptions holds options for register method
|
||||||
type RegisterOptions struct { // nolint: golint,revive
|
type RegisterOptions struct { // nolint: golint,revive
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Domain string
|
Namespace string
|
||||||
TTL time.Duration
|
TTL time.Duration
|
||||||
Attempts int
|
Attempts int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegisterOptions returns register options struct filled by opts
|
// NewRegisterOptions returns register options struct filled by opts
|
||||||
func NewRegisterOptions(opts ...RegisterOption) RegisterOptions {
|
func NewRegisterOptions(opts ...RegisterOption) RegisterOptions {
|
||||||
options := RegisterOptions{
|
options := RegisterOptions{
|
||||||
Domain: DefaultDomain,
|
Namespace: DefaultNamespace,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -72,15 +76,15 @@ type WatchOptions struct {
|
|||||||
// Other options for implementations of the interface
|
// Other options for implementations of the interface
|
||||||
// can be stored in a context
|
// can be stored in a context
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Domain to watch
|
// Namespace to watch
|
||||||
Domain string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatchOptions returns watch options filled by opts
|
// NewWatchOptions returns watch options filled by opts
|
||||||
func NewWatchOptions(opts ...WatchOption) WatchOptions {
|
func NewWatchOptions(opts ...WatchOption) WatchOptions {
|
||||||
options := WatchOptions{
|
options := WatchOptions{
|
||||||
Domain: DefaultDomain,
|
Namespace: DefaultNamespace,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -91,8 +95,8 @@ func NewWatchOptions(opts ...WatchOption) WatchOptions {
|
|||||||
// DeregisterOptions holds options for deregister method
|
// DeregisterOptions holds options for deregister method
|
||||||
type DeregisterOptions struct {
|
type DeregisterOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Domain the service was registered in
|
// Namespace the service was registered in
|
||||||
Domain string
|
Namespace string
|
||||||
// Atempts specify max attempts for deregister
|
// Atempts specify max attempts for deregister
|
||||||
Attempts int
|
Attempts int
|
||||||
}
|
}
|
||||||
@@ -100,8 +104,8 @@ type DeregisterOptions struct {
|
|||||||
// NewDeregisterOptions returns options for deregister filled by opts
|
// NewDeregisterOptions returns options for deregister filled by opts
|
||||||
func NewDeregisterOptions(opts ...DeregisterOption) DeregisterOptions {
|
func NewDeregisterOptions(opts ...DeregisterOption) DeregisterOptions {
|
||||||
options := DeregisterOptions{
|
options := DeregisterOptions{
|
||||||
Domain: DefaultDomain,
|
Namespace: DefaultNamespace,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -112,15 +116,15 @@ func NewDeregisterOptions(opts ...DeregisterOption) DeregisterOptions {
|
|||||||
// LookupOptions holds lookup options
|
// LookupOptions holds lookup options
|
||||||
type LookupOptions struct {
|
type LookupOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Domain to scope the request to
|
// Namespace to scope the request to
|
||||||
Domain string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLookupOptions returns lookup options filled by opts
|
// NewLookupOptions returns lookup options filled by opts
|
||||||
func NewLookupOptions(opts ...LookupOption) LookupOptions {
|
func NewLookupOptions(opts ...LookupOption) LookupOptions {
|
||||||
options := LookupOptions{
|
options := LookupOptions{
|
||||||
Domain: DefaultDomain,
|
Namespace: DefaultNamespace,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -130,16 +134,17 @@ func NewLookupOptions(opts ...LookupOption) LookupOptions {
|
|||||||
|
|
||||||
// ListOptions holds the list options for list method
|
// ListOptions holds the list options for list method
|
||||||
type ListOptions struct {
|
type ListOptions struct {
|
||||||
|
// Context used to store additional options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Domain to scope the request to
|
// Namespace to scope the request to
|
||||||
Domain string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewListOptions returns list options filled by opts
|
// NewListOptions returns list options filled by opts
|
||||||
func NewListOptions(opts ...ListOption) ListOptions {
|
func NewListOptions(opts ...ListOption) ListOptions {
|
||||||
options := ListOptions{
|
options := ListOptions{
|
||||||
Domain: DefaultDomain,
|
Namespace: DefaultNamespace,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
@@ -217,10 +222,10 @@ func RegisterContext(ctx context.Context) RegisterOption { // nolint: golint,rev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterDomain secifies register domain
|
// RegisterNamespace secifies register Namespace
|
||||||
func RegisterDomain(d string) RegisterOption { // nolint: golint,revive
|
func RegisterNamespace(d string) RegisterOption { // nolint: golint,revive
|
||||||
return func(o *RegisterOptions) {
|
return func(o *RegisterOptions) {
|
||||||
o.Domain = d
|
o.Namespace = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,10 +243,10 @@ func WatchContext(ctx context.Context) WatchOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchDomain sets the domain for watch
|
// WatchNamespace sets the Namespace for watch
|
||||||
func WatchDomain(d string) WatchOption {
|
func WatchNamespace(d string) WatchOption {
|
||||||
return func(o *WatchOptions) {
|
return func(o *WatchOptions) {
|
||||||
o.Domain = d
|
o.Namespace = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,10 +264,10 @@ func DeregisterContext(ctx context.Context) DeregisterOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeregisterDomain specifies deregister domain
|
// DeregisterNamespace specifies deregister Namespace
|
||||||
func DeregisterDomain(d string) DeregisterOption {
|
func DeregisterNamespace(d string) DeregisterOption {
|
||||||
return func(o *DeregisterOptions) {
|
return func(o *DeregisterOptions) {
|
||||||
o.Domain = d
|
o.Namespace = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +278,10 @@ func LookupContext(ctx context.Context) LookupOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupDomain sets the domain for lookup
|
// LookupNamespace sets the Namespace for lookup
|
||||||
func LookupDomain(d string) LookupOption {
|
func LookupNamespace(d string) LookupOption {
|
||||||
return func(o *LookupOptions) {
|
return func(o *LookupOptions) {
|
||||||
o.Domain = d
|
o.Namespace = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +292,10 @@ func ListContext(ctx context.Context) ListOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDomain sets the domain for list method
|
// ListNamespace sets the Namespace for list method
|
||||||
func ListDomain(d string) ListOption {
|
func ListNamespace(d string) ListOption {
|
||||||
return func(o *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
o.Domain = d
|
o.Namespace = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,3 +305,9 @@ func Name(n string) Option {
|
|||||||
o.Name = n
|
o.Name = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Codec(c codec.Codec) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Codec = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -9,16 +9,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// WildcardDomain indicates any domain
|
// WildcardNamespace indicates any Namespace
|
||||||
WildcardDomain = "*"
|
WildcardNamespace = "*"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultDomain to use if none was provided in options
|
// DefaultNamespace to use if none was provided in options
|
||||||
var DefaultDomain = "micro"
|
var DefaultNamespace = "micro"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultRegister is the global default register
|
// DefaultRegister is the global default register
|
||||||
DefaultRegister Register = NewRegister()
|
DefaultRegister = NewRegister()
|
||||||
// ErrNotFound returned when LookupService is called and no services found
|
// ErrNotFound returned when LookupService is called and no services found
|
||||||
ErrNotFound = errors.New("service not found")
|
ErrNotFound = errors.New("service not found")
|
||||||
// ErrWatcherStopped returned when when watcher is stopped
|
// ErrWatcherStopped returned when when watcher is stopped
|
||||||
@@ -59,26 +59,17 @@ type Register interface {
|
|||||||
|
|
||||||
// Service holds service register info
|
// Service holds service register info
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version,omitempty"`
|
||||||
Metadata metadata.Metadata `json:"metadata"`
|
Nodes []*Node `json:"nodes,omitempty"`
|
||||||
Endpoints []*Endpoint `json:"endpoints"`
|
Namespace string `json:"namespace,omitempty"`
|
||||||
Nodes []*Node `json:"nodes"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node holds node register info
|
// Node holds node register info
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Metadata metadata.Metadata `json:"metadata"`
|
Metadata metadata.Metadata `json:"metadata,omitempty"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id,omitempty"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address,omitempty"`
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoint holds endpoint register info
|
|
||||||
type Endpoint struct {
|
|
||||||
Request string `json:"request"`
|
|
||||||
Response string `json:"response"`
|
|
||||||
Metadata metadata.Metadata `json:"metadata"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option func signature
|
// Option func signature
|
||||||
|
@@ -15,31 +15,31 @@ type Watcher interface {
|
|||||||
// the watcher. Actions can be create, update, delete
|
// the watcher. Actions can be create, update, delete
|
||||||
type Result struct {
|
type Result struct {
|
||||||
// Service holds register service
|
// Service holds register service
|
||||||
Service *Service
|
Service *Service `json:"service,omitempty"`
|
||||||
// Action holds the action
|
// Action holds the action
|
||||||
Action string
|
Action EventType `json:"action,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventType defines register event type
|
// EventType defines register event type
|
||||||
type EventType int
|
type EventType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Create is emitted when a new service is registered
|
// EventCreate is emitted when a new service is registered
|
||||||
Create EventType = iota
|
EventCreate EventType = iota
|
||||||
// Delete is emitted when an existing service is deregistered
|
// EventDelete is emitted when an existing service is deregistered
|
||||||
Delete
|
EventDelete
|
||||||
// Update is emitted when an existing service is updated
|
// EventUpdate is emitted when an existing service is updated
|
||||||
Update
|
EventUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns human readable event type
|
// String returns human readable event type
|
||||||
func (t EventType) String() string {
|
func (t EventType) String() string {
|
||||||
switch t {
|
switch t {
|
||||||
case Create:
|
case EventCreate:
|
||||||
return "create"
|
return "create"
|
||||||
case Delete:
|
case EventDelete:
|
||||||
return "delete"
|
return "delete"
|
||||||
case Update:
|
case EventUpdate:
|
||||||
return "update"
|
return "update"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
@@ -49,11 +49,11 @@ func (t EventType) String() string {
|
|||||||
// Event is register event
|
// Event is register event
|
||||||
type Event struct {
|
type Event struct {
|
||||||
// Timestamp is event timestamp
|
// Timestamp is event timestamp
|
||||||
Timestamp time.Time
|
Timestamp time.Time `json:"timestamp,omitempty"`
|
||||||
// Service is register service
|
// Service is register service
|
||||||
Service *Service
|
Service *Service `json:"service,omitempty"`
|
||||||
// ID is register id
|
// ID is register id
|
||||||
ID string
|
ID string `json:"id,omitempty"`
|
||||||
// Type defines type of event
|
// Type defines type of event
|
||||||
Type EventType
|
Type EventType `json:"type,omitempty"`
|
||||||
}
|
}
|
||||||
|
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
// Resolver is a DNS network resolve
|
// Resolver is a DNS network resolve
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
sync.RWMutex
|
|
||||||
goresolver *net.Resolver
|
goresolver *net.Resolver
|
||||||
Address string
|
Address string
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve tries to resolve endpoint address
|
// Resolve tries to resolve endpoint address
|
||||||
@@ -39,12 +39,12 @@ func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) {
|
|||||||
return []*resolver.Record{rec}, nil
|
return []*resolver.Record{rec}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.RLock()
|
r.mu.RLock()
|
||||||
goresolver := r.goresolver
|
goresolver := r.goresolver
|
||||||
r.RUnlock()
|
r.mu.RUnlock()
|
||||||
|
|
||||||
if goresolver == nil {
|
if goresolver == nil {
|
||||||
r.Lock()
|
r.mu.Lock()
|
||||||
r.goresolver = &net.Resolver{
|
r.goresolver = &net.Resolver{
|
||||||
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||||
d := net.Dialer{
|
d := net.Dialer{
|
||||||
@@ -53,7 +53,7 @@ func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) {
|
|||||||
return d.DialContext(ctx, "udp", r.Address)
|
return d.DialContext(ctx, "udp", r.Address)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r.Unlock()
|
r.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := goresolver.LookupIP(context.TODO(), "ip", host)
|
addrs, err := goresolver.LookupIP(context.TODO(), "ip", host)
|
||||||
|
@@ -9,6 +9,6 @@ import (
|
|||||||
type Resolver struct{}
|
type Resolver struct{}
|
||||||
|
|
||||||
// Resolve returns the list of nodes
|
// Resolve returns the list of nodes
|
||||||
func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) {
|
func (r *Resolver) Resolve(_ string) ([]*resolver.Record, error) {
|
||||||
return []*resolver.Record{}, nil
|
return []*resolver.Record{}, nil
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Router, bool) {
|
|||||||
return c, ok
|
return c, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustContext get router from context
|
||||||
|
func MustContext(ctx context.Context) Router {
|
||||||
|
r, ok := FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
panic("missing router")
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// NewContext put router in context
|
// NewContext put router in context
|
||||||
func NewContext(ctx context.Context, c Router) context.Context {
|
func NewContext(ctx context.Context, c Router) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
|
@@ -80,7 +80,7 @@ func Name(n string) Option {
|
|||||||
// NewOptions returns router default options
|
// NewOptions returns router default options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
ID: id.Must(),
|
ID: id.MustNew(),
|
||||||
Network: DefaultNetwork,
|
Network: DefaultNetwork,
|
||||||
Register: register.DefaultRegister,
|
Register: register.DefaultRegister,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultRouter is the global default router
|
// DefaultRouter is the global default router
|
||||||
DefaultRouter Router = NewRouter()
|
DefaultRouter = NewRouter()
|
||||||
// DefaultNetwork is default micro network
|
// DefaultNetwork is default micro network
|
||||||
DefaultNetwork = "micro"
|
DefaultNetwork = "micro"
|
||||||
// ErrRouteNotFound is returned when no route was found in the routing table
|
// ErrRouteNotFound is returned when no route was found in the routing table
|
||||||
|
@@ -25,7 +25,7 @@ func (r *random) Select(routes []string, opts ...selector.SelectOption) (selecto
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *random) Record(addr string, err error) error {
|
func (r *random) Record(_ string, _ error) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,14 +6,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewSelector returns an initialised round robin selector
|
// NewSelector returns an initialised round robin selector
|
||||||
func NewSelector(opts ...selector.Option) selector.Selector {
|
func NewSelector(_ ...selector.Option) selector.Selector {
|
||||||
return new(roundrobin)
|
return new(roundrobin)
|
||||||
}
|
}
|
||||||
|
|
||||||
type roundrobin struct{}
|
type roundrobin struct{}
|
||||||
|
|
||||||
// Select return routes based on algo
|
// Select return routes based on algo
|
||||||
func (r *roundrobin) Select(routes []string, opts ...selector.SelectOption) (selector.Next, error) {
|
func (r *roundrobin) Select(routes []string, _ ...selector.SelectOption) (selector.Next, error) {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
return nil, selector.ErrNoneAvailable
|
return nil, selector.ErrNoneAvailable
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ func (r *roundrobin) Select(routes []string, opts ...selector.SelectOption) (sel
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roundrobin) Record(addr string, err error) error { return nil }
|
func (r *roundrobin) Record(_ string, _ error) error { return nil }
|
||||||
|
|
||||||
func (r *roundrobin) Reset() error { return nil }
|
func (r *roundrobin) Reset() error { return nil }
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user