Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
8abc913b28 | |||
3247d144a8 | |||
7b2e3cc8aa | |||
8688179acd | |||
3e40bac5f4 | |||
e3fee6f8a6 | |||
15c020fac5 | |||
3bc046e5d4 | |||
542f36cfa5 | |||
8237e6a08e | |||
ecb60e4dc5 | |||
a1999ff81c | |||
d0f2bc8346 | |||
|
dd29bf457e | ||
d062c248e3 | |||
875f66d36e | |||
818a0e6356 | |||
56e02ec463 | |||
6ca851401d | |||
bd8216b397 | |||
2b13b3f128 | |||
9957380b6d | |||
e10f8c0fa0 | |||
45252fe4a6 | |||
faad082efe | |||
8ab35cbd9b | |||
ad58ab6943 | |||
0e97049e1d | |||
edb0bbf9cf | |||
|
1b01bd22a6 | ||
2fbaa26f0f | |||
35d3e4b332 | |||
|
e98a93d530 | ||
e3545532e8 | |||
09653c2fb2 | |||
70adfeab0d | |||
a45b672c98 | |||
4509323cae | |||
b3f4c670d5 | |||
778dd449e2 | |||
1d16983b67 | |||
f386bffd37 | |||
772bde7938 | |||
ea16f5f825 | |||
c2f34df493 | |||
efe215cd60 | |||
b4f332bf0d | |||
f47fbb1030 | |||
1e8e57a708 | |||
|
5d0959b0a1 | ||
fa8fb3aed7 | |||
cfd2d53a79 | |||
d306f77ffc | |||
e5b0a7e20d | |||
9a5b158b4d | |||
af8d81f3c6 | |||
5c9b3dae33 | |||
9f3957d101 | |||
8fd8bdcb39 | |||
80e3d239ab | |||
419cd486cf | |||
e64269b2a8 | |||
d18429e024 | |||
675e121049 | |||
d357fb1e0d | |||
e4673bcc50 | |||
a839f75a2f | |||
a7e6d61b95 | |||
650d167313 | |||
c6ba2a91e6 | |||
7ece08896f | |||
|
57f6f23294 | ||
09e6fa2fed | |||
10a09a5c6f | |||
b4e5d9462a | |||
96aa0b6906 | |||
f54658830d | |||
1e43122660 | |||
42800fa247 | |||
5b9c810653 | |||
c3def24bf4 | |||
0d1ef31764 | |||
d49afa230f | |||
e545eb4e13 | |||
f28b107372 | |||
c592fabe2a | |||
eb107020c7 | |||
bd4d4c363e |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -11,9 +11,16 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
# Maintain dependencies for Golang
|
# Maintain dependencies for Golang
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
75
.github/workflows/codeql-analysis.yml
vendored
Normal file
75
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["prbuild"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '34 1 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
66
.github/workflows/dependabot-automerge.yml
vendored
Normal file
66
.github/workflows/dependabot-automerge.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: "prautomerge"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["prbuild"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Dependabot-Automerge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Contains workaround to execute if dependabot updates the PR by checking for the base branch in the linked PR
|
||||||
|
# The the github.event.workflow_run.event value is 'push' and not 'pull_request'
|
||||||
|
# dont work with multiple workflows when last returns success
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success'
|
||||||
|
&& github.actor == 'dependabot[bot]'
|
||||||
|
&& github.event.sender.login == 'dependabot[bot]'
|
||||||
|
&& github.event.sender.type == 'Bot'
|
||||||
|
&& (github.event.workflow_run.event == 'pull_request'
|
||||||
|
|| (github.event.workflow_run.event == 'push' && github.event.workflow_run.pull_requests[0].base.ref == github.event.repository.default_branch ))
|
||||||
|
steps:
|
||||||
|
- name: Approve Changes and Merge changes if label 'dependencies' is set
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
console.log(context.payload.workflow_run);
|
||||||
|
|
||||||
|
var labelNames = await github.paginate(
|
||||||
|
github.issues.listLabelsOnIssue,
|
||||||
|
{
|
||||||
|
repo: context.repo.repo,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
issue_number: context.payload.workflow_run.pull_requests[0].number,
|
||||||
|
},
|
||||||
|
(response) => response.data.map(
|
||||||
|
(label) => label.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(labelNames);
|
||||||
|
|
||||||
|
if (labelNames.includes('dependencies')) {
|
||||||
|
console.log('Found label');
|
||||||
|
|
||||||
|
await github.pulls.createReview({
|
||||||
|
repo: context.repo.repo,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
pull_number: context.payload.workflow_run.pull_requests[0].number,
|
||||||
|
event: 'APPROVE'
|
||||||
|
});
|
||||||
|
console.log('Approved PR');
|
||||||
|
|
||||||
|
await github.pulls.merge({
|
||||||
|
repo: context.repo.repo,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
pull_number: context.payload.workflow_run.pull_requests[0].number,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Merged PR');
|
||||||
|
}
|
@@ -30,7 +30,7 @@ linters:
|
|||||||
- gofmt
|
- gofmt
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- goimports
|
- goimports
|
||||||
- golint
|
- revive
|
||||||
- gosec
|
- gosec
|
||||||
- makezero
|
- makezero
|
||||||
- misspell
|
- misspell
|
||||||
|
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.7.x | :white_check_mark: |
|
||||||
|
| < 3.7.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you find any issue, please create github issue in this repo
|
11
api/api.go
11
api/api.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/unistack-org/micro/v3/server"
|
"github.com/unistack-org/micro/v3/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// nolint: revive
|
||||||
// Api interface
|
// Api interface
|
||||||
type Api interface {
|
type Api interface {
|
||||||
// Initialise options
|
// Initialise options
|
||||||
@@ -125,14 +126,14 @@ func Validate(e *Endpoint) error {
|
|||||||
ps := p[0]
|
ps := p[0]
|
||||||
pe := p[len(p)-1]
|
pe := p[len(p)-1]
|
||||||
|
|
||||||
if ps == '^' && pe == '$' {
|
switch {
|
||||||
_, err := regexp.CompilePOSIX(p)
|
case ps == '^' && pe == '$':
|
||||||
if err != nil {
|
if _, err := regexp.CompilePOSIX(p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if ps == '^' && pe != '$' {
|
case ps == '^' && pe != '$':
|
||||||
return errors.New("invalid path")
|
return errors.New("invalid path")
|
||||||
} else if ps != '^' && pe == '$' {
|
case ps != '^' && pe == '$':
|
||||||
return errors.New("invalid path")
|
return errors.New("invalid path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -55,7 +55,7 @@ type Auth interface {
|
|||||||
type Account struct {
|
type Account struct {
|
||||||
// Metadata any other associated metadata
|
// Metadata any other associated metadata
|
||||||
Metadata metadata.Metadata `json:"metadata"`
|
Metadata metadata.Metadata `json:"metadata"`
|
||||||
// ID of the account e.g. email or uuid
|
// ID of the account e.g. email or id
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
// Type of the account, e.g. service
|
// Type of the account, e.g. service
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type noopAuth struct {
|
type noopAuth struct {
|
||||||
@@ -61,11 +61,11 @@ func (n *noopAuth) Verify(acc *Account, res *Resource, opts ...VerifyOption) err
|
|||||||
|
|
||||||
// Inspect a token
|
// Inspect a token
|
||||||
func (n *noopAuth) Inspect(token string) (*Account, error) {
|
func (n *noopAuth) Inspect(token string) (*Account, error) {
|
||||||
uid, err := uuid.NewRandom()
|
id, err := id.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Account{ID: uid.String(), Issuer: n.Options().Issuer}, nil
|
return &Account{ID: id, Issuer: n.Options().Issuer}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token generation using an account id and secret
|
// Token generation using an account id and secret
|
||||||
|
100
broker/broker.go
100
broker/broker.go
@@ -3,46 +3,128 @@ package broker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/metadata"
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBroker default broker
|
// DefaultBroker default memory broker
|
||||||
var DefaultBroker Broker = NewBroker()
|
var DefaultBroker Broker = NewBroker()
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotConnected returns when broker used but not connected yet
|
||||||
|
ErrNotConnected = errors.New("broker not connected")
|
||||||
|
// ErrDisconnected returns when broker disconnected
|
||||||
|
ErrDisconnected = errors.New("broker disconnected")
|
||||||
|
)
|
||||||
|
|
||||||
// Broker is an interface used for asynchronous messaging.
|
// Broker is an interface used for asynchronous messaging.
|
||||||
type Broker interface {
|
type Broker interface {
|
||||||
|
// Name returns broker instance name
|
||||||
Name() string
|
Name() string
|
||||||
Init(...Option) error
|
// Init initilize broker
|
||||||
|
Init(opts ...Option) error
|
||||||
|
// Options returns broker options
|
||||||
Options() Options
|
Options() Options
|
||||||
|
// Address return configured address
|
||||||
Address() string
|
Address() string
|
||||||
Connect(context.Context) error
|
// Connect connects to broker
|
||||||
Disconnect(context.Context) error
|
Connect(ctx context.Context) error
|
||||||
Publish(context.Context, string, *Message, ...PublishOption) error
|
// Disconnect disconnect from broker
|
||||||
Subscribe(context.Context, string, Handler, ...SubscribeOption) (Subscriber, error)
|
Disconnect(ctx context.Context) error
|
||||||
|
// Publish message to broker topic
|
||||||
|
Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
|
||||||
|
// Subscribe subscribes to topic message via handler
|
||||||
|
Subscribe(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
|
// BatchPublish messages to broker with multiple topics
|
||||||
|
BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error
|
||||||
|
// BatchSubscribe subscribes to topic messages via handler
|
||||||
|
BatchSubscribe(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
|
// String type of broker
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is used to process messages via a subscription of a topic.
|
// Handler is used to process messages via a subscription of a topic.
|
||||||
type Handler func(Event) error
|
type Handler func(Event) error
|
||||||
|
|
||||||
|
// Events contains multiple events
|
||||||
|
type Events []Event
|
||||||
|
|
||||||
|
func (evs Events) Ack() error {
|
||||||
|
var err error
|
||||||
|
for _, ev := range evs {
|
||||||
|
if err = ev.Ack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evs Events) SetError(err error) {
|
||||||
|
for _, ev := range evs {
|
||||||
|
ev.SetError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchHandler is used to process messages in batches via a subscription of a topic.
|
||||||
|
type BatchHandler func(Events) error
|
||||||
|
|
||||||
// Event is given to a subscription handler for processing
|
// Event is given to a subscription handler for processing
|
||||||
type Event interface {
|
type Event interface {
|
||||||
|
// Topic returns event topic
|
||||||
Topic() string
|
Topic() string
|
||||||
|
// Message returns broker message
|
||||||
Message() *Message
|
Message() *Message
|
||||||
|
// Ack acknowledge message
|
||||||
Ack() error
|
Ack() error
|
||||||
|
// Error returns message error (like decoding errors or some other)
|
||||||
Error() error
|
Error() error
|
||||||
|
// SetError set event processing error
|
||||||
|
SetError(err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawMessage is a raw encoded JSON value.
|
||||||
|
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
// MarshalJSON returns m as the JSON encoding of m.
|
||||||
|
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return *m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON sets *m to a copy of data.
|
||||||
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], data...)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message is used to transfer data
|
// Message is used to transfer data
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Header metadata.Metadata // contains message metadata
|
// Header contains message metadata
|
||||||
Body []byte // contains message body
|
Header metadata.Metadata
|
||||||
|
// Body contains message body
|
||||||
|
Body RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessage create broker message with topic filled
|
||||||
|
func NewMessage(topic string) *Message {
|
||||||
|
m := &Message{Header: metadata.New(2)}
|
||||||
|
m.Header.Set(metadata.HeaderTopic, topic)
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriber is a convenience return type for the Subscribe method
|
// Subscriber is a convenience return type for the Subscribe method
|
||||||
type Subscriber interface {
|
type Subscriber interface {
|
||||||
|
// Options returns subscriber options
|
||||||
Options() SubscribeOptions
|
Options() SubscribeOptions
|
||||||
|
// Topic returns topic for subscription
|
||||||
Topic() string
|
Topic() string
|
||||||
Unsubscribe(context.Context) error
|
// Unsubscribe from topic
|
||||||
|
Unsubscribe(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
205
broker/memory.go
205
broker/memory.go
@@ -2,18 +2,18 @@ package broker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
maddr "github.com/unistack-org/micro/v3/util/addr"
|
maddr "github.com/unistack-org/micro/v3/util/addr"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
mnet "github.com/unistack-org/micro/v3/util/net"
|
mnet "github.com/unistack-org/micro/v3/util/net"
|
||||||
"github.com/unistack-org/micro/v3/util/rand"
|
"github.com/unistack-org/micro/v3/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type memoryBroker struct {
|
type memoryBroker struct {
|
||||||
Subscribers map[string][]*memorySubscriber
|
subscribers map[string][]*memorySubscriber
|
||||||
addr string
|
addr string
|
||||||
opts Options
|
opts Options
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
@@ -28,12 +28,13 @@ type memoryEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type memorySubscriber struct {
|
type memorySubscriber struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
exit chan bool
|
exit chan bool
|
||||||
handler Handler
|
handler Handler
|
||||||
id string
|
batchhandler BatchHandler
|
||||||
topic string
|
id string
|
||||||
opts SubscribeOptions
|
topic string
|
||||||
|
opts SubscribeOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Options() Options {
|
func (m *memoryBroker) Options() Options {
|
||||||
@@ -77,7 +78,6 @@ func (m *memoryBroker) Disconnect(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.connected = false
|
m.connected = false
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,73 +89,168 @@ func (m *memoryBroker) Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
|
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
|
||||||
|
msg.Header.Set(metadata.HeaderTopic, topic)
|
||||||
|
return m.publish(ctx, []*Message{msg}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
|
return m.publish(ctx, msgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
if !m.connected {
|
if !m.connected {
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
return errors.New("not connected")
|
return ErrNotConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
subs, ok := m.Subscribers[topic]
|
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var v interface{}
|
var err error
|
||||||
if m.opts.Codec != nil {
|
|
||||||
buf, err := m.opts.Codec.Marshal(msg)
|
select {
|
||||||
if err != nil {
|
case <-ctx.Done():
|
||||||
return err
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
options := NewPublishOptions(opts...)
|
||||||
|
|
||||||
|
msgTopicMap := make(map[string]Events)
|
||||||
|
for _, v := range msgs {
|
||||||
|
p := &memoryEvent{opts: m.opts}
|
||||||
|
|
||||||
|
if m.opts.Codec == nil || options.BodyOnly {
|
||||||
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
|
p.message = v.Body
|
||||||
|
} else {
|
||||||
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
|
p.message, err = m.opts.Codec.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p)
|
||||||
}
|
}
|
||||||
v = buf
|
|
||||||
} else {
|
|
||||||
v = msg
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &memoryEvent{
|
beh := m.opts.BatchErrorHandler
|
||||||
topic: topic,
|
eh := m.opts.ErrorHandler
|
||||||
message: v,
|
|
||||||
opts: m.opts,
|
|
||||||
}
|
|
||||||
|
|
||||||
eh := m.opts.ErrorHandler
|
for t, ms := range msgTopicMap {
|
||||||
|
m.RLock()
|
||||||
for _, sub := range subs {
|
subs, ok := m.subscribers[t]
|
||||||
if err := sub.handler(p); err != nil {
|
m.RUnlock()
|
||||||
p.err = err
|
if !ok {
|
||||||
if sub.opts.ErrorHandler != nil {
|
continue
|
||||||
eh = sub.opts.ErrorHandler
|
|
||||||
}
|
}
|
||||||
if eh != nil {
|
|
||||||
eh(p)
|
for _, sub := range subs {
|
||||||
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
if sub.opts.BatchErrorHandler != nil {
|
||||||
m.opts.Logger.Error(m.opts.Context, err.Error())
|
beh = sub.opts.BatchErrorHandler
|
||||||
|
}
|
||||||
|
if sub.opts.ErrorHandler != nil {
|
||||||
|
eh = sub.opts.ErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// batch processing
|
||||||
|
case sub.batchhandler != nil:
|
||||||
|
if err = sub.batchhandler(ms); err != nil {
|
||||||
|
ms.SetError(err)
|
||||||
|
if beh != nil {
|
||||||
|
_ = beh(ms)
|
||||||
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
} else if sub.opts.AutoAck {
|
||||||
|
if err = ms.Ack(); err != nil {
|
||||||
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// single processing
|
||||||
|
case sub.handler != nil:
|
||||||
|
for _, p := range ms {
|
||||||
|
if err = sub.handler(p); err != nil {
|
||||||
|
p.SetError(err)
|
||||||
|
if eh != nil {
|
||||||
|
_ = eh(p)
|
||||||
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
} else if sub.opts.AutoAck {
|
||||||
|
if err = p.Ack(); err != nil {
|
||||||
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
if !m.connected {
|
if !m.connected {
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
return nil, errors.New("not connected")
|
return nil, ErrNotConnected
|
||||||
}
|
}
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
|
|
||||||
options := NewSubscribeOptions(opts...)
|
sid, err := id.New()
|
||||||
|
|
||||||
id, err := uuid.NewRandom()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options := NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
|
sub := &memorySubscriber{
|
||||||
|
exit: make(chan bool, 1),
|
||||||
|
id: sid,
|
||||||
|
topic: topic,
|
||||||
|
batchhandler: handler,
|
||||||
|
opts: options,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sub.exit
|
||||||
|
m.Lock()
|
||||||
|
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
||||||
|
for _, sb := range m.subscribers[topic] {
|
||||||
|
if sb.id == sub.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSubscribers = append(newSubscribers, sb)
|
||||||
|
}
|
||||||
|
m.subscribers[topic] = newSubscribers
|
||||||
|
m.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
m.RLock()
|
||||||
|
if !m.connected {
|
||||||
|
m.RUnlock()
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
sid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
sub := &memorySubscriber{
|
sub := &memorySubscriber{
|
||||||
exit: make(chan bool, 1),
|
exit: make(chan bool, 1),
|
||||||
id: id.String(),
|
id: sid,
|
||||||
topic: topic,
|
topic: topic,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
opts: options,
|
opts: options,
|
||||||
@@ -163,20 +258,20 @@ func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Hand
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
m.Subscribers[topic] = append(m.Subscribers[topic], sub)
|
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-sub.exit
|
<-sub.exit
|
||||||
m.Lock()
|
m.Lock()
|
||||||
var newSubscribers []*memorySubscriber
|
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
||||||
for _, sb := range m.Subscribers[topic] {
|
for _, sb := range m.subscribers[topic] {
|
||||||
if sb.id == sub.id {
|
if sb.id == sub.id {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newSubscribers = append(newSubscribers, sb)
|
newSubscribers = append(newSubscribers, sb)
|
||||||
}
|
}
|
||||||
m.Subscribers[topic] = newSubscribers
|
m.subscribers[topic] = newSubscribers
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -221,6 +316,10 @@ func (m *memoryEvent) Error() error {
|
|||||||
return m.err
|
return m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) SetError(err error) {
|
||||||
|
m.err = err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *memorySubscriber) Options() SubscribeOptions {
|
func (m *memorySubscriber) Options() SubscribeOptions {
|
||||||
return m.opts
|
return m.opts
|
||||||
}
|
}
|
||||||
@@ -238,6 +337,6 @@ func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
|
|||||||
func NewBroker(opts ...Option) Broker {
|
func NewBroker(opts ...Option) Broker {
|
||||||
return &memoryBroker{
|
return &memoryBroker{
|
||||||
opts: NewOptions(opts...),
|
opts: NewOptions(opts...),
|
||||||
Subscribers: make(map[string][]*memorySubscriber),
|
subscribers: make(map[string][]*memorySubscriber),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,56 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMemoryBatchBroker(t *testing.T) {
|
||||||
|
b := NewBroker()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic := "test"
|
||||||
|
count := 10
|
||||||
|
|
||||||
|
fn := func(evts Events) error {
|
||||||
|
return evts.Ack()
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := b.BatchSubscribe(ctx, topic, fn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error subscribing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]*Message, 0, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
message := &Message{
|
||||||
|
Header: map[string]string{
|
||||||
|
metadata.HeaderTopic: topic,
|
||||||
|
"foo": "bar",
|
||||||
|
"id": fmt.Sprintf("%d", i),
|
||||||
|
},
|
||||||
|
Body: []byte(`"hello world"`),
|
||||||
|
}
|
||||||
|
msgs = append(msgs, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sub.Unsubscribe(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Disconnect(ctx); err != nil {
|
||||||
|
t.Fatalf("Unexpected connect error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMemoryBroker(t *testing.T) {
|
func TestMemoryBroker(t *testing.T) {
|
||||||
b := NewBroker()
|
b := NewBroker()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -26,20 +74,27 @@ func TestMemoryBroker(t *testing.T) {
|
|||||||
t.Fatalf("Unexpected error subscribing %v", err)
|
t.Fatalf("Unexpected error subscribing %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgs := make([]*Message, 0, count)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
message := &Message{
|
message := &Message{
|
||||||
Header: map[string]string{
|
Header: map[string]string{
|
||||||
"foo": "bar",
|
metadata.HeaderTopic: topic,
|
||||||
"id": fmt.Sprintf("%d", i),
|
"foo": "bar",
|
||||||
|
"id": fmt.Sprintf("%d", i),
|
||||||
},
|
},
|
||||||
Body: []byte(`hello world`),
|
Body: []byte(`"hello world"`),
|
||||||
}
|
}
|
||||||
|
msgs = append(msgs, message)
|
||||||
|
|
||||||
if err := b.Publish(ctx, topic, message); err != nil {
|
if err := b.Publish(ctx, topic, message); err != nil {
|
||||||
t.Fatalf("Unexpected error publishing %d", i)
|
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := b.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatalf("Unexpected error publishing %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := sub.Unsubscribe(ctx); err != nil {
|
if err := sub.Unsubscribe(ctx); err != nil {
|
||||||
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package broker
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/codec"
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
@@ -29,6 +30,8 @@ type Options struct {
|
|||||||
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 BatchHandler
|
||||||
// Name holds the broker name
|
// Name holds the broker name
|
||||||
Name string
|
Name string
|
||||||
// Addrs holds the broker address
|
// Addrs holds the broker address
|
||||||
@@ -71,11 +74,9 @@ func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
|||||||
options := PublishOptions{
|
options := PublishOptions{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +86,18 @@ type SubscribeOptions struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// 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 BatchHandler
|
||||||
// Group holds consumer group
|
// Group holds consumer group
|
||||||
Group string
|
Group string
|
||||||
// AutoAck flag specifies auto ack of incoming message when no error happens
|
// AutoAck flag specifies auto ack of incoming message when no error happens
|
||||||
AutoAck bool
|
AutoAck bool
|
||||||
// BodyOnly flag specifies that message contains only body bytes without header
|
// BodyOnly flag specifies that message contains only body bytes without header
|
||||||
BodyOnly bool
|
BodyOnly bool
|
||||||
|
// BatchSize flag specifies max batch size
|
||||||
|
BatchSize int
|
||||||
|
// BatchWait flag specifies max wait time for batch filling
|
||||||
|
BatchWait time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -113,23 +120,6 @@ func PublishContext(ctx context.Context) PublishOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeOption func
|
|
||||||
type SubscribeOption func(*SubscribeOptions)
|
|
||||||
|
|
||||||
// NewSubscribeOptions creates new SubscribeOptions
|
|
||||||
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
|
||||||
options := SubscribeOptions{
|
|
||||||
AutoAck: true,
|
|
||||||
Context: context.Background(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addrs sets the host addresses to be used by the broker
|
// Addrs sets the host addresses to be used by the broker
|
||||||
func Addrs(addrs ...string) Option {
|
func Addrs(addrs ...string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -145,28 +135,6 @@ func Codec(c codec.Codec) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableAutoAck disables auto ack
|
|
||||||
func DisableAutoAck() SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.AutoAck = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeAutoAck will disable auto acking of messages
|
|
||||||
// after they have been handled.
|
|
||||||
func SubscribeAutoAck(b bool) SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.AutoAck = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeBodyOnly consumes only body of the message
|
|
||||||
func SubscribeBodyOnly(b bool) SubscribeOption {
|
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.BodyOnly = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandler will catch all broker errors that cant be handled
|
// ErrorHandler will catch all broker errors that cant be handled
|
||||||
// in normal way, for example Codec errors
|
// in normal way, for example Codec errors
|
||||||
func ErrorHandler(h Handler) Option {
|
func ErrorHandler(h Handler) Option {
|
||||||
@@ -175,6 +143,14 @@ func ErrorHandler(h Handler) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func BatchErrorHandler(h BatchHandler) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
||||||
// in normal way, for example Codec errors
|
// in normal way, for example Codec errors
|
||||||
func SubscribeErrorHandler(h Handler) SubscribeOption {
|
func SubscribeErrorHandler(h Handler) SubscribeOption {
|
||||||
@@ -183,6 +159,14 @@ func SubscribeErrorHandler(h Handler) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscribeBatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func SubscribeBatchErrorHandler(h BatchHandler) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Queue sets the subscribers queue
|
// Queue sets the subscribers queue
|
||||||
// Deprecated
|
// Deprecated
|
||||||
func Queue(name string) SubscribeOption {
|
func Queue(name string) SubscribeOption {
|
||||||
@@ -246,3 +230,55 @@ func SubscribeContext(ctx context.Context) SubscribeOption {
|
|||||||
o.Context = ctx
|
o.Context = ctx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableAutoAck disables auto ack
|
||||||
|
// Deprecated
|
||||||
|
func DisableAutoAck() SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.AutoAck = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeAutoAck contol auto acking of messages
|
||||||
|
// after they have been handled.
|
||||||
|
func SubscribeAutoAck(b bool) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.AutoAck = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeBodyOnly consumes only body of the message
|
||||||
|
func SubscribeBodyOnly(b bool) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BodyOnly = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeBatchSize specifies max batch size
|
||||||
|
func SubscribeBatchSize(n int) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BatchSize = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeBatchWait specifies max batch wait time
|
||||||
|
func SubscribeBatchWait(td time.Duration) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BatchWait = td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeOption func
|
||||||
|
type SubscribeOption func(*SubscribeOptions)
|
||||||
|
|
||||||
|
// NewSubscribeOptions creates new SubscribeOptions
|
||||||
|
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
||||||
|
options := SubscribeOptions{
|
||||||
|
AutoAck: true,
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
@@ -40,6 +40,7 @@ type Client interface {
|
|||||||
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||||
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||||
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
||||||
|
BatchPublish(ctx context.Context, msg []Message, opts ...PublishOption) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -173,7 +173,7 @@ func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
|
func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
|
||||||
options := NewMessageOptions(opts...)
|
options := NewMessageOptions(append([]MessageOption{MessageContentType(n.opts.ContentType)}, opts...)...)
|
||||||
return &noopMessage{topic: topic, payload: msg, opts: options}
|
return &noopMessage{topic: topic, payload: msg, opts: options}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,47 +181,59 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
return &noopStream{}, nil
|
return &noopStream{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
|
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
var body []byte
|
return n.publish(ctx, ps, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
|
||||||
|
return n.publish(ctx, []Message{p}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
options := NewPublishOptions(opts...)
|
options := NewPublishOptions(opts...)
|
||||||
|
|
||||||
md, ok := metadata.FromOutgoingContext(ctx)
|
msgs := make([]*broker.Message, 0, len(ps))
|
||||||
if !ok {
|
|
||||||
md = metadata.New(0)
|
|
||||||
}
|
|
||||||
md["Content-Type"] = p.ContentType()
|
|
||||||
md["Micro-Topic"] = p.Topic()
|
|
||||||
|
|
||||||
// passed in raw data
|
for _, p := range ps {
|
||||||
if d, ok := p.Payload().(*codec.Frame); ok {
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
body = d.Data
|
if !ok {
|
||||||
} else {
|
md = metadata.New(0)
|
||||||
// use codec for payload
|
}
|
||||||
cf, err := n.newCodec(p.ContentType())
|
md[metadata.HeaderContentType] = p.ContentType()
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
topic := p.Topic()
|
||||||
|
|
||||||
|
// get the exchange
|
||||||
|
if len(options.Exchange) > 0 {
|
||||||
|
topic = options.Exchange
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the body
|
md[metadata.HeaderTopic] = topic
|
||||||
b, err := cf.Marshal(p.Payload())
|
|
||||||
if err != nil {
|
var body []byte
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
|
// passed in raw data
|
||||||
|
if d, ok := p.Payload().(*codec.Frame); ok {
|
||||||
|
body = d.Data
|
||||||
|
} else {
|
||||||
|
// use codec for payload
|
||||||
|
cf, err := n.newCodec(p.ContentType())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the body
|
||||||
|
b, err := cf.Marshal(p.Payload())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
body = b
|
||||||
}
|
}
|
||||||
body = b
|
|
||||||
|
msgs = append(msgs, &broker.Message{Header: md, Body: body})
|
||||||
}
|
}
|
||||||
|
|
||||||
topic := p.Topic()
|
return n.opts.Broker.BatchPublish(ctx, msgs,
|
||||||
|
|
||||||
// get the exchange
|
|
||||||
if len(options.Exchange) > 0 {
|
|
||||||
topic = options.Exchange
|
|
||||||
}
|
|
||||||
|
|
||||||
return n.opts.Broker.Publish(ctx, topic, &broker.Message{
|
|
||||||
Header: md,
|
|
||||||
Body: body,
|
|
||||||
},
|
|
||||||
broker.PublishContext(options.Context),
|
broker.PublishContext(options.Context),
|
||||||
broker.PublishBodyOnly(options.BodyOnly),
|
broker.PublishBodyOnly(options.BodyOnly),
|
||||||
)
|
)
|
||||||
|
@@ -19,6 +19,8 @@ import (
|
|||||||
|
|
||||||
// Options holds client options
|
// Options holds client options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// Transport used for transfer messages
|
||||||
|
Transport transport.Transport
|
||||||
// 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
|
||||||
@@ -29,18 +31,16 @@ type Options struct {
|
|||||||
Broker broker.Broker
|
Broker broker.Broker
|
||||||
// Meter used for metrics
|
// Meter used for metrics
|
||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
// Router used to get route
|
|
||||||
Router router.Router
|
|
||||||
// Transport used for transfer messages
|
|
||||||
Transport transport.Transport
|
|
||||||
// Context is used for external options
|
// Context is used for external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Lookup func used to get destination addr
|
// Router used to get route
|
||||||
Lookup LookupFunc
|
Router router.Router
|
||||||
// Codecs map
|
|
||||||
Codecs map[string]codec.Codec
|
|
||||||
// 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 LookupFunc
|
||||||
// Proxy is used for proxy requests
|
// Proxy is used for proxy requests
|
||||||
Proxy string
|
Proxy string
|
||||||
// ContentType is used to select codec
|
// ContentType is used to select codec
|
||||||
@@ -68,12 +68,12 @@ func NewCallOptions(opts ...CallOption) CallOptions {
|
|||||||
|
|
||||||
// CallOptions holds client call options
|
// CallOptions holds client call options
|
||||||
type CallOptions struct {
|
type CallOptions struct {
|
||||||
// Router used for route
|
|
||||||
Router router.Router
|
|
||||||
// Selector selects addr
|
// Selector selects addr
|
||||||
Selector selector.Selector
|
Selector selector.Selector
|
||||||
// Context used for deadline
|
// Context used for deadline
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
// Router used for route
|
||||||
|
Router router.Router
|
||||||
// Retry func used for retries
|
// Retry func used for retries
|
||||||
Retry RetryFunc
|
Retry RetryFunc
|
||||||
// Backoff func used for backoff when retry
|
// Backoff func used for backoff when retry
|
||||||
@@ -82,22 +82,22 @@ type CallOptions struct {
|
|||||||
Network string
|
Network string
|
||||||
// Content-Type
|
// Content-Type
|
||||||
ContentType string
|
ContentType string
|
||||||
// CallWrappers call wrappers
|
// AuthToken string
|
||||||
CallWrappers []CallWrapper
|
AuthToken string
|
||||||
// SelectOptions selector options
|
|
||||||
SelectOptions []selector.SelectOption
|
|
||||||
// Address specifies static addr list
|
// Address specifies static addr list
|
||||||
Address []string
|
Address []string
|
||||||
// Retries specifies retries num
|
// SelectOptions selector options
|
||||||
Retries int
|
SelectOptions []selector.SelectOption
|
||||||
|
// CallWrappers call wrappers
|
||||||
|
CallWrappers []CallWrapper
|
||||||
// StreamTimeout stream timeout
|
// StreamTimeout stream timeout
|
||||||
StreamTimeout time.Duration
|
StreamTimeout time.Duration
|
||||||
// RequestTimeout request timeout
|
// RequestTimeout request timeout
|
||||||
RequestTimeout time.Duration
|
RequestTimeout time.Duration
|
||||||
// DialTimeout dial timeout
|
// DialTimeout dial timeout
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
// AuthToken flag
|
// Retries specifies retries num
|
||||||
AuthToken bool
|
Retries int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context pass context to client
|
// Context pass context to client
|
||||||
@@ -118,12 +118,12 @@ func NewPublishOptions(opts ...PublishOption) PublishOptions {
|
|||||||
|
|
||||||
// PublishOptions holds publish options
|
// PublishOptions holds publish options
|
||||||
type PublishOptions struct {
|
type PublishOptions struct {
|
||||||
// BodyOnly will publish only message body
|
|
||||||
BodyOnly bool
|
|
||||||
// Context used for external options
|
// Context used for external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Exchange topic exchange name
|
// Exchange topic exchange name
|
||||||
Exchange string
|
Exchange string
|
||||||
|
// BodyOnly will publish only message body
|
||||||
|
BodyOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageOptions creates message options struct
|
// NewMessageOptions creates message options struct
|
||||||
@@ -267,7 +267,7 @@ func Transport(t transport.Transport) Option {
|
|||||||
func Register(r register.Register) Option {
|
func Register(r register.Register) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
if o.Router != nil {
|
if o.Router != nil {
|
||||||
o.Router.Init(router.Register(r))
|
_ = o.Router.Init(router.Register(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,7 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
// already set. Required for Init call below.
|
// already set. Required for Init call below.
|
||||||
|
|
||||||
// set the transport tls
|
// set the transport tls
|
||||||
o.Transport.Init(
|
_ = o.Transport.Init(
|
||||||
transport.TLSConfig(t),
|
transport.TLSConfig(t),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -373,19 +373,35 @@ func DialTimeout(d time.Duration) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithExchange sets the exchange to route a message through
|
// WithExchange sets the exchange to route a message through
|
||||||
|
// Deprecated
|
||||||
func WithExchange(e string) PublishOption {
|
func WithExchange(e string) PublishOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *PublishOptions) {
|
||||||
o.Exchange = e
|
o.Exchange = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishExchange sets the exchange to route a message through
|
||||||
|
func PublishExchange(e string) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.Exchange = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithBodyOnly publish only message body
|
// WithBodyOnly publish only message body
|
||||||
|
// DERECATED
|
||||||
func WithBodyOnly(b bool) PublishOption {
|
func WithBodyOnly(b bool) PublishOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *PublishOptions) {
|
||||||
o.BodyOnly = b
|
o.BodyOnly = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishBodyOnly publish only message body
|
||||||
|
func PublishBodyOnly(b bool) PublishOption {
|
||||||
|
return func(o *PublishOptions) {
|
||||||
|
o.BodyOnly = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PublishContext sets the context in publish options
|
// PublishContext sets the context in publish options
|
||||||
func PublishContext(ctx context.Context) PublishOption {
|
func PublishContext(ctx context.Context) PublishOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *PublishOptions) {
|
||||||
@@ -463,9 +479,9 @@ func WithDialTimeout(d time.Duration) CallOption {
|
|||||||
|
|
||||||
// WithAuthToken is a CallOption which overrides the
|
// WithAuthToken is a CallOption which overrides the
|
||||||
// authorization header with the services own auth token
|
// authorization header with the services own auth token
|
||||||
func WithAuthToken() CallOption {
|
func WithAuthToken(t string) CallOption {
|
||||||
return func(o *CallOptions) {
|
return func(o *CallOptions) {
|
||||||
o.AuthToken = true
|
o.AuthToken = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,12 +514,20 @@ func WithSelectOptions(sops ...selector.SelectOption) CallOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithMessageContentType sets the message content type
|
// WithMessageContentType sets the message content type
|
||||||
|
// Deprecated
|
||||||
func WithMessageContentType(ct string) MessageOption {
|
func WithMessageContentType(ct string) MessageOption {
|
||||||
return func(o *MessageOptions) {
|
return func(o *MessageOptions) {
|
||||||
o.ContentType = ct
|
o.ContentType = ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MessageContentType sets the message content type
|
||||||
|
func MessageContentType(ct string) MessageOption {
|
||||||
|
return func(o *MessageOptions) {
|
||||||
|
o.ContentType = ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StreamingRequest specifies that request is streaming
|
// StreamingRequest specifies that request is streaming
|
||||||
func StreamingRequest(b bool) RequestOption {
|
func StreamingRequest(b bool) RequestOption {
|
||||||
return func(o *RequestOptions) {
|
return func(o *RequestOptions) {
|
||||||
|
@@ -25,7 +25,7 @@ var (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultMaxMsgSize specifies how much data codec can handle
|
// DefaultMaxMsgSize specifies how much data codec can handle
|
||||||
DefaultMaxMsgSize int = 1024 * 1024 * 4 // 4Mb
|
DefaultMaxMsgSize = 1024 * 1024 * 4 // 4Mb
|
||||||
// DefaultCodec is the global default codec
|
// DefaultCodec is the global default codec
|
||||||
DefaultCodec Codec = NewCodec()
|
DefaultCodec Codec = NewCodec()
|
||||||
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
|
||||||
@@ -41,11 +41,11 @@ type MessageType int
|
|||||||
// connection. ReadBody may be called with a nil argument to force the
|
// connection. ReadBody may be called with a nil argument to force the
|
||||||
// body to be read and discarded.
|
// body to be read and discarded.
|
||||||
type Codec interface {
|
type Codec interface {
|
||||||
ReadHeader(io.Reader, *Message, MessageType) error
|
ReadHeader(r io.Reader, m *Message, mt MessageType) error
|
||||||
ReadBody(io.Reader, interface{}) error
|
ReadBody(r io.Reader, v interface{}) error
|
||||||
Write(io.Writer, *Message, interface{}) error
|
Write(w io.Writer, m *Message, v interface{}) error
|
||||||
Marshal(interface{}) ([]byte, error)
|
Marshal(v interface{}, opts ...Option) ([]byte, error)
|
||||||
Unmarshal([]byte, interface{}) error
|
Unmarshal(b []byte, v interface{}, opts ...Option) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ type Message struct {
|
|||||||
Method string
|
Method string
|
||||||
Endpoint string
|
Endpoint string
|
||||||
Error string
|
Error string
|
||||||
Id string
|
ID string
|
||||||
Body []byte
|
Body []byte
|
||||||
Type MessageType
|
Type MessageType
|
||||||
}
|
}
|
||||||
@@ -67,3 +67,20 @@ type Message struct {
|
|||||||
func NewMessage(t MessageType) *Message {
|
func NewMessage(t MessageType) *Message {
|
||||||
return &Message{Type: t, Header: metadata.New(0)}
|
return &Message{Type: t, Header: metadata.New(0)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalAppend calls codec.Marshal(v) and returns the data appended to buf.
|
||||||
|
// If codec implements MarshalAppend, that is called instead.
|
||||||
|
func MarshalAppend(buf []byte, c Codec, v interface{}, opts ...Option) ([]byte, error) {
|
||||||
|
if nc, ok := c.(interface {
|
||||||
|
MarshalAppend([]byte, interface{}, ...Option) ([]byte, error)
|
||||||
|
}); ok {
|
||||||
|
return nc.MarshalAppend(buf, v, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
mbuf, err := c.Marshal(v, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(buf, mbuf...), nil
|
||||||
|
}
|
||||||
|
34
codec/context.go
Normal file
34
codec/context.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type codecKey struct{}
|
||||||
|
|
||||||
|
// FromContext returns codec from context
|
||||||
|
func FromContext(ctx context.Context) (Codec, bool) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
c, ok := ctx.Value(codecKey{}).(Codec)
|
||||||
|
return c, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext put codec in context
|
||||||
|
func NewContext(ctx context.Context, c Codec) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, codecKey{}, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOption returns a function to setup a context with given value
|
||||||
|
func SetOption(k, v interface{}) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
@@ -4,3 +4,31 @@ package codec
|
|||||||
type Frame struct {
|
type Frame struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Frame) MarshalJSON() ([]byte, error) {
|
||||||
|
return m.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Frame) UnmarshalJSON(data []byte) error {
|
||||||
|
m.Data = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Frame) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (m *Frame) Reset() {
|
||||||
|
*m = Frame{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Frame) String() string {
|
||||||
|
return string(m.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Frame) Marshal() ([]byte, error) {
|
||||||
|
return m.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Frame) Unmarshal(data []byte) error {
|
||||||
|
m.Data = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -5,7 +5,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type noopCodec struct{}
|
type noopCodec struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
func (c *noopCodec) ReadHeader(conn io.Reader, m *Message, t MessageType) error {
|
func (c *noopCodec) ReadHeader(conn io.Reader, m *Message, t MessageType) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -69,11 +71,11 @@ func (c *noopCodec) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCodec returns new noop codec
|
// NewCodec returns new noop codec
|
||||||
func NewCodec() Codec {
|
func NewCodec(opts ...Option) Codec {
|
||||||
return &noopCodec{}
|
return &noopCodec{opts: NewOptions(opts...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *noopCodec) Marshal(v interface{}) ([]byte, error) {
|
func (c *noopCodec) Marshal(v interface{}, opts ...Option) ([]byte, error) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ func (c *noopCodec) Marshal(v interface{}) ([]byte, error) {
|
|||||||
return json.Marshal(v)
|
return json.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *noopCodec) Unmarshal(d []byte, v interface{}) error {
|
func (c *noopCodec) Unmarshal(d []byte, v interface{}, opts ...Option) error {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package codec
|
package codec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/meter"
|
"github.com/unistack-org/micro/v3/meter"
|
||||||
"github.com/unistack-org/micro/v3/tracer"
|
"github.com/unistack-org/micro/v3/tracer"
|
||||||
@@ -17,6 +19,10 @@ type Options struct {
|
|||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
// Tracer used for tracing
|
// Tracer used for tracing
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
|
// Context stores additional codec options
|
||||||
|
Context context.Context
|
||||||
|
// TagName specifies tag name in struct to control codec
|
||||||
|
TagName string
|
||||||
// MaxMsgSize specifies max messages size that reads by codec
|
// MaxMsgSize specifies max messages size that reads by codec
|
||||||
MaxMsgSize int
|
MaxMsgSize int
|
||||||
}
|
}
|
||||||
@@ -28,6 +34,13 @@ func MaxMsgSize(n int) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TagName sets the codec tag name in struct
|
||||||
|
func TagName(n string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.TagName = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Logger sets the logger
|
// Logger sets the logger
|
||||||
func Logger(l logger.Logger) Option {
|
func Logger(l logger.Logger) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -52,10 +65,12 @@ func Meter(m meter.Meter) Option {
|
|||||||
// NewOptions returns new options
|
// NewOptions returns new options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
|
Context: context.Background(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
MaxMsgSize: DefaultMaxMsgSize,
|
MaxMsgSize: DefaultMaxMsgSize,
|
||||||
|
TagName: DefaultTagName,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
@@ -4,11 +4,18 @@ package config
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultConfig default config
|
// DefaultConfig default config
|
||||||
var DefaultConfig Config = NewConfig()
|
var DefaultConfig Config = NewConfig()
|
||||||
|
|
||||||
|
// DefaultWatcherMinInterval default min interval for poll changes
|
||||||
|
var DefaultWatcherMinInterval = 5 * time.Second
|
||||||
|
|
||||||
|
// DefaultWatcherMaxInterval default max interval for poll changes
|
||||||
|
var DefaultWatcherMaxInterval = 9 * time.Second
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrCodecMissing is returned when codec needed and not specified
|
// ErrCodecMissing is returned when codec needed and not specified
|
||||||
ErrCodecMissing = errors.New("codec missing")
|
ErrCodecMissing = errors.New("codec missing")
|
||||||
@@ -30,28 +37,80 @@ type Config interface {
|
|||||||
Load(context.Context, ...LoadOption) error
|
Load(context.Context, ...LoadOption) error
|
||||||
// Save config to sources
|
// Save config to sources
|
||||||
Save(context.Context, ...SaveOption) error
|
Save(context.Context, ...SaveOption) error
|
||||||
// Watch a value for changes
|
// Watch a config for changes
|
||||||
//Watch(context.Context) (Watcher, error)
|
Watch(context.Context, ...WatchOption) (Watcher, error)
|
||||||
// String returns config type name
|
// String returns config type name
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watcher is the config watcher
|
// Watcher is the config watcher
|
||||||
type Watcher interface {
|
type Watcher interface {
|
||||||
// Next() (, error)
|
// Next blocks until update happens or error returned
|
||||||
|
Next() (map[string]interface{}, error)
|
||||||
|
// Stop stops watcher
|
||||||
Stop() error
|
Stop() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads config from config sources
|
// Load loads config from config sources
|
||||||
func Load(ctx context.Context, cs ...Config) error {
|
func Load(ctx context.Context, cs []Config, opts ...LoadOption) error {
|
||||||
var err error
|
var err error
|
||||||
for _, c := range cs {
|
for _, c := range cs {
|
||||||
if err = c.Init(); err != nil {
|
if err = c.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = c.Load(ctx); err != nil {
|
if err = c.Load(ctx, opts...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultAfterLoad = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().AfterLoad {
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Errorf(ctx, "%s AfterLoad err: %v", c.String(), err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultAfterSave = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().AfterSave {
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Errorf(ctx, "%s AfterSave err: %v", c.String(), err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().BeforeLoad {
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Errorf(ctx, "%s BeforeLoad err: %v", c.String(), err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultBeforeSave = func(ctx context.Context, c Config) error {
|
||||||
|
for _, fn := range c.Options().BeforeSave {
|
||||||
|
if err := fn(ctx, c); err != nil {
|
||||||
|
c.Options().Logger.Errorf(ctx, "%s BeforeSavec err: %v", c.String(), err)
|
||||||
|
if !c.Options().AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,10 +27,8 @@ func (c *defaultConfig) Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
||||||
for _, fn := range c.opts.BeforeLoad {
|
if err := DefaultBeforeLoad(ctx, c); err != nil {
|
||||||
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options := NewLoadOptions(opts...)
|
options := NewLoadOptions(opts...)
|
||||||
@@ -41,29 +40,39 @@ func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
|||||||
mopts = append(mopts, mergo.WithAppendSlice)
|
mopts = append(mopts, mergo.WithAppendSlice)
|
||||||
}
|
}
|
||||||
|
|
||||||
src, err := rutil.Zero(c.opts.Struct)
|
dst := c.opts.Struct
|
||||||
if err == nil {
|
if options.Struct != nil {
|
||||||
valueOf := reflect.ValueOf(src)
|
dst = options.Struct
|
||||||
if err = c.fillValues(valueOf); err == nil {
|
|
||||||
err = mergo.Merge(c.opts.Struct, src, mopts...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !c.opts.AllowFail {
|
src, err := rutil.Zero(dst)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
if !c.opts.AllowFail {
|
||||||
|
|
||||||
for _, fn := range c.opts.AfterLoad {
|
|
||||||
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return DefaultAfterLoad(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
|
||||||
|
err = mergo.Merge(dst, src, mopts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.opts.Logger.Errorf(ctx, "default load error: %v", err)
|
||||||
|
if !c.opts.AllowFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DefaultAfterLoad(ctx, c); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (c *defaultConfig) fillValue(value reflect.Value, val string) error {
|
func fillValue(value reflect.Value, val string) error {
|
||||||
if !rutil.IsEmpty(value) {
|
if !rutil.IsEmpty(value) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -80,10 +89,10 @@ func (c *defaultConfig) fillValue(value reflect.Value, val string) error {
|
|||||||
kv := strings.FieldsFunc(nval, func(c rune) bool { return c == '=' })
|
kv := strings.FieldsFunc(nval, func(c rune) bool { return c == '=' })
|
||||||
mkey := reflect.Indirect(reflect.New(kt))
|
mkey := reflect.Indirect(reflect.New(kt))
|
||||||
mval := reflect.Indirect(reflect.New(et))
|
mval := reflect.Indirect(reflect.New(et))
|
||||||
if err := c.fillValue(mkey, kv[0]); err != nil {
|
if err := fillValue(mkey, kv[0]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.fillValue(mval, kv[1]); err != nil {
|
if err := fillValue(mval, kv[1]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
value.SetMapIndex(mkey, mval)
|
value.SetMapIndex(mkey, mval)
|
||||||
@@ -93,7 +102,7 @@ func (c *defaultConfig) fillValue(value reflect.Value, val string) error {
|
|||||||
value.Set(reflect.MakeSlice(reflect.SliceOf(value.Type().Elem()), len(nvals), len(nvals)))
|
value.Set(reflect.MakeSlice(reflect.SliceOf(value.Type().Elem()), len(nvals), len(nvals)))
|
||||||
for idx, nval := range nvals {
|
for idx, nval := range nvals {
|
||||||
nvalue := reflect.Indirect(reflect.New(value.Type().Elem()))
|
nvalue := reflect.Indirect(reflect.New(value.Type().Elem()))
|
||||||
if err := c.fillValue(nvalue, nval); err != nil {
|
if err := fillValue(nvalue, nval); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
value.Index(idx).Set(nvalue)
|
value.Index(idx).Set(nvalue)
|
||||||
@@ -182,7 +191,7 @@ func (c *defaultConfig) fillValue(value reflect.Value, val string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) fillValues(valueOf reflect.Value) error {
|
func fillValues(valueOf reflect.Value, tname string) error {
|
||||||
var values reflect.Value
|
var values reflect.Value
|
||||||
|
|
||||||
if valueOf.Kind() == reflect.Ptr {
|
if valueOf.Kind() == reflect.Ptr {
|
||||||
@@ -209,7 +218,7 @@ func (c *defaultConfig) fillValues(valueOf reflect.Value) error {
|
|||||||
switch value.Kind() {
|
switch value.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
value.Set(reflect.Indirect(reflect.New(value.Type())))
|
value.Set(reflect.Indirect(reflect.New(value.Type())))
|
||||||
if err := c.fillValues(value); err != nil {
|
if err := fillValues(value, tname); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -223,17 +232,17 @@ func (c *defaultConfig) fillValues(valueOf reflect.Value) error {
|
|||||||
value.Set(reflect.New(value.Type().Elem()))
|
value.Set(reflect.New(value.Type().Elem()))
|
||||||
}
|
}
|
||||||
value = value.Elem()
|
value = value.Elem()
|
||||||
if err := c.fillValues(value); err != nil {
|
if err := fillValues(value, tname); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tag, ok := field.Tag.Lookup(c.opts.StructTag)
|
tag, ok := field.Tag.Lookup(tname)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.fillValue(value, tag); err != nil {
|
if err := fillValue(value, tag); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,16 +251,12 @@ func (c *defaultConfig) fillValues(valueOf reflect.Value) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
||||||
for _, fn := range c.opts.BeforeSave {
|
if err := DefaultBeforeSave(ctx, c); err != nil {
|
||||||
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range c.opts.AfterSave {
|
if err := DefaultAfterSave(ctx, c); err != nil {
|
||||||
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -265,6 +270,10 @@ func (c *defaultConfig) Name() string {
|
|||||||
return c.opts.Name
|
return c.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *defaultConfig) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
// NewConfig returns new default config source
|
// NewConfig returns new default config source
|
||||||
func NewConfig(opts ...Option) Config {
|
func NewConfig(opts ...Option) Config {
|
||||||
options := NewOptions(opts...)
|
options := NewOptions(opts...)
|
||||||
|
@@ -47,6 +47,6 @@ func TestDefault(t *testing.T) {
|
|||||||
if conf.StringValue != "after_load" {
|
if conf.StringValue != "after_load" {
|
||||||
t.Fatal("AfterLoad option not working")
|
t.Fatal("AfterLoad option not working")
|
||||||
}
|
}
|
||||||
|
_ = conf
|
||||||
t.Logf("%#+v\n", conf)
|
// t.Logf("%#+v\n", conf)
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/codec"
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
@@ -62,6 +63,7 @@ type LoadOption func(o *LoadOptions)
|
|||||||
|
|
||||||
// LoadOptions struct
|
// LoadOptions struct
|
||||||
type LoadOptions struct {
|
type LoadOptions struct {
|
||||||
|
Struct interface{}
|
||||||
Override bool
|
Override bool
|
||||||
Append bool
|
Append bool
|
||||||
}
|
}
|
||||||
@@ -88,13 +90,29 @@ func LoadAppend(b bool) LoadOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadStruct override struct for loading
|
||||||
|
func LoadStruct(src interface{}) LoadOption {
|
||||||
|
return func(o *LoadOptions) {
|
||||||
|
o.Struct = src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SaveOption function signature
|
// SaveOption function signature
|
||||||
type SaveOption func(o *SaveOptions)
|
type SaveOption func(o *SaveOptions)
|
||||||
|
|
||||||
// SaveOptions struct
|
// SaveOptions struct
|
||||||
type SaveOptions struct {
|
type SaveOptions struct {
|
||||||
|
Struct interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveStruct override struct for save to config
|
||||||
|
func SaveStruct(src interface{}) SaveOption {
|
||||||
|
return func(o *SaveOptions) {
|
||||||
|
o.Struct = src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSaveOptions fill SaveOptions struct
|
||||||
func NewSaveOptions(opts ...SaveOption) SaveOptions {
|
func NewSaveOptions(opts ...SaveOption) SaveOptions {
|
||||||
options := SaveOptions{}
|
options := SaveOptions{}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -186,3 +204,60 @@ func Name(n string) Option {
|
|||||||
o.Name = n
|
o.Name = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WatchOptions struuct
|
||||||
|
type WatchOptions struct {
|
||||||
|
// Context used by non default options
|
||||||
|
Context context.Context
|
||||||
|
// Struct for filling
|
||||||
|
Struct interface{}
|
||||||
|
// MinInterval specifies the min time.Duration interval for poll changes
|
||||||
|
MinInterval time.Duration
|
||||||
|
// MaxInterval specifies the max time.Duration interval for poll changes
|
||||||
|
MaxInterval time.Duration
|
||||||
|
// Coalesce multiple events to one
|
||||||
|
Coalesce bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchOption func(*WatchOptions)
|
||||||
|
|
||||||
|
func NewWatchOptions(opts ...WatchOption) WatchOptions {
|
||||||
|
options := WatchOptions{
|
||||||
|
Context: context.Background(),
|
||||||
|
MinInterval: DefaultWatcherMinInterval,
|
||||||
|
MaxInterval: DefaultWatcherMaxInterval,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchContext pass context
|
||||||
|
func WatchContext(ctx context.Context) WatchOption {
|
||||||
|
return func(o *WatchOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchCoalesce controls watch event combining
|
||||||
|
func WatchCoalesce(b bool) WatchOption {
|
||||||
|
return func(o *WatchOptions) {
|
||||||
|
o.Coalesce = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchInterval specifies min and max time.Duration for pulling changes
|
||||||
|
func WatchInterval(min, max time.Duration) WatchOption {
|
||||||
|
return func(o *WatchOptions) {
|
||||||
|
o.MinInterval = min
|
||||||
|
o.MaxInterval = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchStruct overrides struct for fill
|
||||||
|
func WatchStruct(src interface{}) WatchOption {
|
||||||
|
return func(o *WatchOptions) {
|
||||||
|
o.Struct = src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
package config_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
rutil "github.com/unistack-org/micro/v3/util/reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
SubConfig *SubConfig
|
|
||||||
Config *Config
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubConfig struct {
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReflect(t *testing.T) {
|
|
||||||
cfg1 := &Config{Value: "cfg1", Config: &Config{Value: "cfg1_1"}, SubConfig: &SubConfig{Value: "cfg1"}}
|
|
||||||
cfg2, err := rutil.Zero(cfg1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Logf("dst: %#+v\n", cfg2)
|
|
||||||
}
|
|
@@ -37,8 +37,8 @@ var (
|
|||||||
|
|
||||||
// Error type
|
// Error type
|
||||||
type Error struct {
|
type Error struct {
|
||||||
// Id holds error id or service, usually someting like my_service or uuid
|
// ID holds error id or service, usually someting like my_service or id
|
||||||
Id string
|
ID string
|
||||||
// Detail holds some useful details about error
|
// Detail holds some useful details about error
|
||||||
Detail string
|
Detail string
|
||||||
// Status usually holds text of http status
|
// Status usually holds text of http status
|
||||||
@@ -56,7 +56,7 @@ func (e *Error) Error() string {
|
|||||||
// New generates a custom error
|
// New generates a custom error
|
||||||
func New(id, detail string, code int32) error {
|
func New(id, detail string, code int32) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: code,
|
Code: code,
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
Status: http.StatusText(int(code)),
|
Status: http.StatusText(int(code)),
|
||||||
@@ -77,7 +77,7 @@ func Parse(err string) *Error {
|
|||||||
// BadRequest generates a 400 error.
|
// BadRequest generates a 400 error.
|
||||||
func BadRequest(id, format string, a ...interface{}) error {
|
func BadRequest(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 400,
|
Code: 400,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(400),
|
Status: http.StatusText(400),
|
||||||
@@ -87,7 +87,7 @@ func BadRequest(id, format string, a ...interface{}) error {
|
|||||||
// Unauthorized generates a 401 error.
|
// Unauthorized generates a 401 error.
|
||||||
func Unauthorized(id, format string, a ...interface{}) error {
|
func Unauthorized(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 401,
|
Code: 401,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(401),
|
Status: http.StatusText(401),
|
||||||
@@ -97,7 +97,7 @@ func Unauthorized(id, format string, a ...interface{}) error {
|
|||||||
// Forbidden generates a 403 error.
|
// Forbidden generates a 403 error.
|
||||||
func Forbidden(id, format string, a ...interface{}) error {
|
func Forbidden(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 403,
|
Code: 403,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(403),
|
Status: http.StatusText(403),
|
||||||
@@ -107,7 +107,7 @@ func Forbidden(id, format string, a ...interface{}) error {
|
|||||||
// NotFound generates a 404 error.
|
// NotFound generates a 404 error.
|
||||||
func NotFound(id, format string, a ...interface{}) error {
|
func NotFound(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 404,
|
Code: 404,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(404),
|
Status: http.StatusText(404),
|
||||||
@@ -117,7 +117,7 @@ func NotFound(id, format string, a ...interface{}) error {
|
|||||||
// MethodNotAllowed generates a 405 error.
|
// MethodNotAllowed generates a 405 error.
|
||||||
func MethodNotAllowed(id, format string, a ...interface{}) error {
|
func MethodNotAllowed(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 405,
|
Code: 405,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(405),
|
Status: http.StatusText(405),
|
||||||
@@ -127,7 +127,7 @@ func MethodNotAllowed(id, format string, a ...interface{}) error {
|
|||||||
// Timeout generates a 408 error.
|
// Timeout generates a 408 error.
|
||||||
func Timeout(id, format string, a ...interface{}) error {
|
func Timeout(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 408,
|
Code: 408,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(408),
|
Status: http.StatusText(408),
|
||||||
@@ -137,7 +137,7 @@ func Timeout(id, format string, a ...interface{}) error {
|
|||||||
// Conflict generates a 409 error.
|
// Conflict generates a 409 error.
|
||||||
func Conflict(id, format string, a ...interface{}) error {
|
func Conflict(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 409,
|
Code: 409,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(409),
|
Status: http.StatusText(409),
|
||||||
@@ -147,7 +147,7 @@ func Conflict(id, format string, a ...interface{}) error {
|
|||||||
// InternalServerError generates a 500 error.
|
// InternalServerError generates a 500 error.
|
||||||
func InternalServerError(id, format string, a ...interface{}) error {
|
func InternalServerError(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 500,
|
Code: 500,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(500),
|
Status: http.StatusText(500),
|
||||||
@@ -157,7 +157,7 @@ func InternalServerError(id, format string, a ...interface{}) error {
|
|||||||
// NotImplemented generates a 501 error
|
// NotImplemented generates a 501 error
|
||||||
func NotImplemented(id, format string, a ...interface{}) error {
|
func NotImplemented(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 501,
|
Code: 501,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(501),
|
Status: http.StatusText(501),
|
||||||
@@ -167,7 +167,7 @@ func NotImplemented(id, format string, a ...interface{}) error {
|
|||||||
// BadGateway generates a 502 error
|
// BadGateway generates a 502 error
|
||||||
func BadGateway(id, format string, a ...interface{}) error {
|
func BadGateway(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 502,
|
Code: 502,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(502),
|
Status: http.StatusText(502),
|
||||||
@@ -177,7 +177,7 @@ func BadGateway(id, format string, a ...interface{}) error {
|
|||||||
// ServiceUnavailable generates a 503 error
|
// ServiceUnavailable generates a 503 error
|
||||||
func ServiceUnavailable(id, format string, a ...interface{}) error {
|
func ServiceUnavailable(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 503,
|
Code: 503,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(503),
|
Status: http.StatusText(503),
|
||||||
@@ -187,7 +187,7 @@ func ServiceUnavailable(id, format string, a ...interface{}) error {
|
|||||||
// GatewayTimeout generates a 504 error
|
// GatewayTimeout generates a 504 error
|
||||||
func GatewayTimeout(id, format string, a ...interface{}) error {
|
func GatewayTimeout(id, format string, a ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Id: id,
|
ID: id,
|
||||||
Code: 504,
|
Code: 504,
|
||||||
Detail: fmt.Sprintf(format, a...),
|
Detail: fmt.Sprintf(format, a...),
|
||||||
Status: http.StatusText(504),
|
Status: http.StatusText(504),
|
||||||
|
@@ -9,12 +9,12 @@ import (
|
|||||||
func TestFromError(t *testing.T) {
|
func TestFromError(t *testing.T) {
|
||||||
err := NotFound("go.micro.test", "%s", "example")
|
err := NotFound("go.micro.test", "%s", "example")
|
||||||
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)
|
||||||
}
|
}
|
||||||
err = er.New(err.Error())
|
err = er.New(err.Error())
|
||||||
merr = FromError(err)
|
merr = FromError(err)
|
||||||
if merr.Id != "go.micro.test" || merr.Code != 404 {
|
if merr.ID != "go.micro.test" || merr.Code != 404 {
|
||||||
t.Fatalf("invalid conversation %v != %v", err, merr)
|
t.Fatalf("invalid conversation %v != %v", err, merr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ func TestEqual(t *testing.T) {
|
|||||||
func TestErrors(t *testing.T) {
|
func TestErrors(t *testing.T) {
|
||||||
testData := []*Error{
|
testData := []*Error{
|
||||||
{
|
{
|
||||||
Id: "test",
|
ID: "test",
|
||||||
Code: 500,
|
Code: 500,
|
||||||
Detail: "Internal server error",
|
Detail: "Internal server error",
|
||||||
Status: http.StatusText(500),
|
Status: http.StatusText(500),
|
||||||
@@ -44,7 +44,7 @@ func TestErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range testData {
|
for _, e := range testData {
|
||||||
ne := New(e.Id, e.Detail, e.Code)
|
ne := New(e.ID, e.Detail, e.Code)
|
||||||
|
|
||||||
if e.Error() != ne.Error() {
|
if e.Error() != ne.Error() {
|
||||||
t.Fatalf("Expected %s got %s", e.Error(), ne.Error())
|
t.Fatalf("Expected %s got %s", e.Error(), ne.Error())
|
||||||
@@ -56,8 +56,8 @@ func TestErrors(t *testing.T) {
|
|||||||
t.Fatalf("Expected error got nil %v", pe)
|
t.Fatalf("Expected error got nil %v", pe)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pe.Id != e.Id {
|
if pe.ID != e.ID {
|
||||||
t.Fatalf("Expected %s got %s", e.Id, pe.Id)
|
t.Fatalf("Expected %s got %s", e.ID, pe.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pe.Detail != e.Detail {
|
if pe.Detail != e.Detail {
|
||||||
|
34
flow/context.go
Normal file
34
flow/context.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flowKey struct{}
|
||||||
|
|
||||||
|
// FromContext returns Flow from context
|
||||||
|
func FromContext(ctx context.Context) (Flow, bool) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
c, ok := ctx.Value(flowKey{}).(Flow)
|
||||||
|
return c, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext stores Flow to context
|
||||||
|
func NewContext(ctx context.Context, f Flow) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, flowKey{}, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOption returns a function to setup a context with given value
|
||||||
|
func SetOption(k, v interface{}) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
585
flow/default.go
Normal file
585
flow/default.go
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/silas/dag"
|
||||||
|
"github.com/unistack-org/micro/v3/client"
|
||||||
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
|
"github.com/unistack-org/micro/v3/store"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type microFlow struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
type microWorkflow struct {
|
||||||
|
opts Options
|
||||||
|
g *dag.AcyclicGraph
|
||||||
|
steps map[string]Step
|
||||||
|
id string
|
||||||
|
status Status
|
||||||
|
sync.RWMutex
|
||||||
|
init bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) ID() string {
|
||||||
|
return w.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Steps() ([][]Step, error) {
|
||||||
|
return w.getSteps("", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Status() Status {
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) AppendSteps(steps ...Step) error {
|
||||||
|
w.Lock()
|
||||||
|
|
||||||
|
for _, s := range steps {
|
||||||
|
w.steps[s.String()] = s
|
||||||
|
w.g.Add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dst := range steps {
|
||||||
|
for _, req := range dst.Requires() {
|
||||||
|
src, ok := w.steps[req]
|
||||||
|
if !ok {
|
||||||
|
return ErrStepNotExists
|
||||||
|
}
|
||||||
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
|
w.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) RemoveSteps(steps ...Step) error {
|
||||||
|
// TODO: handle case when some step requires or required by removed step
|
||||||
|
|
||||||
|
w.Lock()
|
||||||
|
|
||||||
|
for _, s := range steps {
|
||||||
|
delete(w.steps, s.String())
|
||||||
|
w.g.Remove(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dst := range steps {
|
||||||
|
for _, req := range dst.Requires() {
|
||||||
|
src, ok := w.steps[req]
|
||||||
|
if !ok {
|
||||||
|
return ErrStepNotExists
|
||||||
|
}
|
||||||
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
|
w.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) getSteps(start string, reverse bool) ([][]Step, error) {
|
||||||
|
var steps [][]Step
|
||||||
|
var root dag.Vertex
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fn := func(n dag.Vertex, idx int) error {
|
||||||
|
if idx == 0 {
|
||||||
|
steps = make([][]Step, 1)
|
||||||
|
steps[0] = make([]Step, 0, 1)
|
||||||
|
} else if idx >= len(steps) {
|
||||||
|
tsteps := make([][]Step, idx+1)
|
||||||
|
copy(tsteps, steps)
|
||||||
|
steps = tsteps
|
||||||
|
steps[idx] = make([]Step, 0, 1)
|
||||||
|
}
|
||||||
|
steps[idx] = append(steps[idx], n.(Step))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if start != "" {
|
||||||
|
var ok bool
|
||||||
|
w.RLock()
|
||||||
|
root, ok = w.steps[start]
|
||||||
|
w.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStepNotExists
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root, err = w.g.Root()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reverse {
|
||||||
|
err = w.g.SortedReverseDepthFirstWalk([]dag.Vertex{root}, fn)
|
||||||
|
} else {
|
||||||
|
err = w.g.SortedDepthFirstWalk([]dag.Vertex{root}, fn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Abort(ctx context.Context, id string) error {
|
||||||
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
||||||
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Suspend(ctx context.Context, id string) error {
|
||||||
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
||||||
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Resume(ctx context.Context, id string) error {
|
||||||
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id))
|
||||||
|
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
|
||||||
|
w.Lock()
|
||||||
|
if !w.init {
|
||||||
|
if err := w.g.Validate(); err != nil {
|
||||||
|
w.Unlock()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
w.init = true
|
||||||
|
}
|
||||||
|
w.Unlock()
|
||||||
|
|
||||||
|
eid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
|
||||||
|
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
|
||||||
|
|
||||||
|
options := NewExecuteOptions(opts...)
|
||||||
|
|
||||||
|
steps, err := w.getSteps(options.Start, options.Reverse)
|
||||||
|
if err != nil {
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
cherr := make(chan error, 1)
|
||||||
|
chstatus := make(chan Status, 1)
|
||||||
|
|
||||||
|
nctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
nopts := make([]ExecuteOption, 0, len(opts)+5)
|
||||||
|
|
||||||
|
nopts = append(nopts,
|
||||||
|
ExecuteClient(w.opts.Client),
|
||||||
|
ExecuteTracer(w.opts.Tracer),
|
||||||
|
ExecuteLogger(w.opts.Logger),
|
||||||
|
ExecuteMeter(w.opts.Meter),
|
||||||
|
)
|
||||||
|
nopts = append(nopts, opts...)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
return eid, werr
|
||||||
|
}
|
||||||
|
for idx := range steps {
|
||||||
|
for nidx := range steps[idx] {
|
||||||
|
cstep := steps[idx][nidx]
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
|
||||||
|
return eid, werr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for idx := range steps {
|
||||||
|
for nidx := range steps[idx] {
|
||||||
|
wStatus := &codec.Frame{}
|
||||||
|
if werr := workflowStore.Read(w.opts.Context, "status", wStatus); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status := StringStatus[string(wStatus.Data)]; status != StatusRunning {
|
||||||
|
chstatus <- status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.opts.Logger.V(logger.TraceLevel) {
|
||||||
|
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
|
||||||
|
}
|
||||||
|
cstep := steps[idx][nidx]
|
||||||
|
// nolint: nestif
|
||||||
|
if len(cstep.Requires()) == 0 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(step Step) {
|
||||||
|
defer wg.Done()
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rsp, serr := step.Execute(nctx, req, nopts...)
|
||||||
|
if serr != nil {
|
||||||
|
step.SetStatus(StatusFailure)
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
cherr <- serr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(cstep)
|
||||||
|
wg.Wait()
|
||||||
|
} else {
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rsp, serr := cstep.Execute(nctx, req, nopts...)
|
||||||
|
if serr != nil {
|
||||||
|
cstep.SetStatus(StatusFailure)
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
}
|
||||||
|
cherr <- serr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
cherr <- werr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if options.Async {
|
||||||
|
return eid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Tracef(ctx, "wait for finish or error")
|
||||||
|
select {
|
||||||
|
case <-nctx.Done():
|
||||||
|
err = nctx.Err()
|
||||||
|
case cerr := <-cherr:
|
||||||
|
err = cerr
|
||||||
|
case <-done:
|
||||||
|
close(cherr)
|
||||||
|
case <-chstatus:
|
||||||
|
close(chstatus)
|
||||||
|
return eid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case nctx.Err() != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
case err == nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
case err != nil:
|
||||||
|
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
|
||||||
|
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlow(opts ...Option) Flow {
|
||||||
|
options := NewOptions(opts...)
|
||||||
|
return µFlow{opts: options}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) Options() Options {
|
||||||
|
return f.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) Init(opts ...Option) error {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&f.opts)
|
||||||
|
}
|
||||||
|
if err := f.opts.Client.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.opts.Tracer.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.opts.Logger.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.opts.Meter.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.opts.Store.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) WorkflowList(ctx context.Context) ([]Workflow, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
|
||||||
|
w := µWorkflow{opts: f.opts, id: id, g: &dag.AcyclicGraph{}, steps: make(map[string]Step, len(steps))}
|
||||||
|
|
||||||
|
for _, s := range steps {
|
||||||
|
w.steps[s.String()] = s
|
||||||
|
w.g.Add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dst := range steps {
|
||||||
|
for _, req := range dst.Requires() {
|
||||||
|
src, ok := w.steps[req]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStepNotExists
|
||||||
|
}
|
||||||
|
w.g.Connect(dag.BasicEdge(src, dst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.g.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.g.TransitiveReduction()
|
||||||
|
|
||||||
|
w.init = true
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) WorkflowRemove(ctx context.Context, id string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) WorkflowSave(ctx context.Context, w Workflow) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *microFlow) WorkflowLoad(ctx context.Context, id string) (Workflow, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type microCallStep struct {
|
||||||
|
rsp *Message
|
||||||
|
req *Message
|
||||||
|
service string
|
||||||
|
method string
|
||||||
|
opts StepOptions
|
||||||
|
status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Request() *Message {
|
||||||
|
return s.req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Response() *Message {
|
||||||
|
return s.rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) ID() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Options() StepOptions {
|
||||||
|
return s.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Endpoint() string {
|
||||||
|
return s.method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Requires() []string {
|
||||||
|
return s.opts.Requires
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Require(steps ...Step) error {
|
||||||
|
for _, step := range steps {
|
||||||
|
s.opts.Requires = append(s.opts.Requires, step.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) String() string {
|
||||||
|
if s.opts.ID != "" {
|
||||||
|
return s.opts.ID
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s.%s", s.service, s.method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Name() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Hashcode() interface{} {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) GetStatus() Status {
|
||||||
|
return s.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) SetStatus(status Status) {
|
||||||
|
s.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microCallStep) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error) {
|
||||||
|
options := NewExecuteOptions(opts...)
|
||||||
|
if options.Client == nil {
|
||||||
|
return nil, ErrMissingClient
|
||||||
|
}
|
||||||
|
rsp := &codec.Frame{}
|
||||||
|
copts := []client.CallOption{client.WithRetries(0)}
|
||||||
|
if options.Timeout > 0 {
|
||||||
|
copts = append(copts,
|
||||||
|
client.WithRequestTimeout(options.Timeout),
|
||||||
|
client.WithDialTimeout(options.Timeout))
|
||||||
|
}
|
||||||
|
nctx := metadata.NewOutgoingContext(ctx, req.Header)
|
||||||
|
err := options.Client.Call(nctx, options.Client.NewRequest(s.service, s.method, &codec.Frame{Data: req.Body}), rsp, copts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
md, _ := metadata.FromOutgoingContext(nctx)
|
||||||
|
return &Message{Header: md, Body: rsp.Data}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type microPublishStep struct {
|
||||||
|
req *Message
|
||||||
|
rsp *Message
|
||||||
|
topic string
|
||||||
|
opts StepOptions
|
||||||
|
status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Request() *Message {
|
||||||
|
return s.req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Response() *Message {
|
||||||
|
return s.rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) ID() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Options() StepOptions {
|
||||||
|
return s.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Endpoint() string {
|
||||||
|
return s.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Requires() []string {
|
||||||
|
return s.opts.Requires
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Require(steps ...Step) error {
|
||||||
|
for _, step := range steps {
|
||||||
|
s.opts.Requires = append(s.opts.Requires, step.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) String() string {
|
||||||
|
if s.opts.ID != "" {
|
||||||
|
return s.opts.ID
|
||||||
|
}
|
||||||
|
return s.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Name() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Hashcode() interface{} {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) GetStatus() Status {
|
||||||
|
return s.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) SetStatus(status Status) {
|
||||||
|
s.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *microPublishStep) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallStep(service string, name string, method string, opts ...StepOption) Step {
|
||||||
|
options := NewStepOptions(opts...)
|
||||||
|
return µCallStep{service: service, method: name + "." + method, opts: options}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPublishStep(topic string, opts ...StepOption) Step {
|
||||||
|
options := NewStepOptions(opts...)
|
||||||
|
return µPublishStep{topic: topic, opts: options}
|
||||||
|
}
|
143
flow/flow.go
143
flow/flow.go
@@ -1,17 +1,152 @@
|
|||||||
// Package flow is an interface used for saga pattern microservice workflow
|
// Package flow is an interface used for saga pattern microservice workflow
|
||||||
package flow
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrStepNotExists = errors.New("step not exists")
|
||||||
|
ErrMissingClient = errors.New("client not set")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RawMessage is a raw encoded JSON value.
|
||||||
|
// It implements Marshaler and Unmarshaler and can be used to delay decoding or precompute a encoding.
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
// MarshalJSON returns m as the JSON encoding of m.
|
||||||
|
func (m *RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return *m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON sets *m to a copy of data.
|
||||||
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("RawMessage UnmarshalJSON on nil pointer")
|
||||||
|
}
|
||||||
|
*m = append((*m)[0:0], data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Header metadata.Metadata
|
||||||
|
Body RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step represents dedicated workflow step
|
||||||
type Step interface {
|
type Step interface {
|
||||||
|
// ID returns step id
|
||||||
|
ID() string
|
||||||
// Endpoint returns rpc endpoint service_name.service_method or broker topic
|
// Endpoint returns rpc endpoint service_name.service_method or broker topic
|
||||||
Endpoint() string
|
Endpoint() string
|
||||||
|
// Execute step run
|
||||||
|
Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error)
|
||||||
|
// Requires returns dependent steps
|
||||||
|
Requires() []string
|
||||||
|
// Options returns step options
|
||||||
|
Options() StepOptions
|
||||||
|
// Require add required steps
|
||||||
|
Require(steps ...Step) error
|
||||||
|
// String
|
||||||
|
String() string
|
||||||
|
// GetStatus returns step status
|
||||||
|
GetStatus() Status
|
||||||
|
// SetStatus sets the step status
|
||||||
|
SetStatus(Status)
|
||||||
|
// Request returns step request message
|
||||||
|
Request() *Message
|
||||||
|
// Response returns step response message
|
||||||
|
Response() *Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
func (status Status) String() string {
|
||||||
|
return StatusString[status]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusPending Status = iota
|
||||||
|
StatusRunning
|
||||||
|
StatusFailure
|
||||||
|
StatusSuccess
|
||||||
|
StatusAborted
|
||||||
|
StatusSuspend
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
StatusString = map[Status]string{
|
||||||
|
StatusPending: "StatusPending",
|
||||||
|
StatusRunning: "StatusRunning",
|
||||||
|
StatusFailure: "StatusFailure",
|
||||||
|
StatusSuccess: "StatusSuccess",
|
||||||
|
StatusAborted: "StatusAborted",
|
||||||
|
StatusSuspend: "StatusSuspend",
|
||||||
|
}
|
||||||
|
StringStatus = map[string]Status{
|
||||||
|
"StatusPending": StatusPending,
|
||||||
|
"StatusRunning": StatusRunning,
|
||||||
|
"StatusFailure": StatusFailure,
|
||||||
|
"StatusSuccess": StatusSuccess,
|
||||||
|
"StatusAborted": StatusAborted,
|
||||||
|
"StatusSuspend": StatusSuspend,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Workflow contains all steps to execute
|
||||||
type Workflow interface {
|
type Workflow interface {
|
||||||
Steps() [][]Step
|
// ID returns id of the workflow
|
||||||
Stop() error
|
ID() string
|
||||||
|
// Execute workflow with args, return execution id and error
|
||||||
|
Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error)
|
||||||
|
// RemoveSteps remove steps from workflow
|
||||||
|
RemoveSteps(steps ...Step) error
|
||||||
|
// AppendSteps append steps to workflow
|
||||||
|
AppendSteps(steps ...Step) error
|
||||||
|
// Status returns workflow status
|
||||||
|
Status() Status
|
||||||
|
// Steps returns steps slice where parallel steps returned on the same level
|
||||||
|
Steps() ([][]Step, error)
|
||||||
|
// Suspend suspends execution
|
||||||
|
Suspend(ctx context.Context, id string) error
|
||||||
|
// Resume resumes execution
|
||||||
|
Resume(ctx context.Context, id string) error
|
||||||
|
// Abort abort execution
|
||||||
|
Abort(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flow the base interface to interact with workflows
|
||||||
type Flow interface {
|
type Flow interface {
|
||||||
Start(Workflow) error
|
// Options returns options
|
||||||
Stop(Workflow)
|
Options() Options
|
||||||
|
// Init initialize
|
||||||
|
Init(...Option) error
|
||||||
|
// WorkflowCreate creates new workflow with specific id and steps
|
||||||
|
WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error)
|
||||||
|
// WorkflowSave saves workflow
|
||||||
|
WorkflowSave(ctx context.Context, w Workflow) error
|
||||||
|
// WorkflowLoad loads workflow with specific id
|
||||||
|
WorkflowLoad(ctx context.Context, id string) (Workflow, error)
|
||||||
|
// WorkflowList lists all workflows
|
||||||
|
WorkflowList(ctx context.Context) ([]Workflow, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
flowMu sync.Mutex
|
||||||
|
atomicSteps atomic.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterStep(step Step) {
|
||||||
|
flowMu.Lock()
|
||||||
|
steps, _ := atomicSteps.Load().([]Step)
|
||||||
|
atomicSteps.Store(append(steps, step))
|
||||||
|
flowMu.Unlock()
|
||||||
}
|
}
|
||||||
|
230
flow/options.go
Normal file
230
flow/options.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/client"
|
||||||
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/meter"
|
||||||
|
"github.com/unistack-org/micro/v3/store"
|
||||||
|
"github.com/unistack-org/micro/v3/tracer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option func
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// Options server struct
|
||||||
|
type Options struct {
|
||||||
|
// Context holds the external options and can be used for flow shutdown
|
||||||
|
Context context.Context
|
||||||
|
// Client holds the client.Client
|
||||||
|
Client client.Client
|
||||||
|
// Tracer holds the tracer
|
||||||
|
Tracer tracer.Tracer
|
||||||
|
// Logger holds the logger
|
||||||
|
Logger logger.Logger
|
||||||
|
// Meter holds the meter
|
||||||
|
Meter meter.Meter
|
||||||
|
// Store used for intermediate results
|
||||||
|
Store store.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns new options struct with default or passed values
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
options := Options{
|
||||||
|
Context: context.Background(),
|
||||||
|
Logger: logger.DefaultLogger,
|
||||||
|
Meter: meter.DefaultMeter,
|
||||||
|
Tracer: tracer.DefaultTracer,
|
||||||
|
Client: client.DefaultClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger sets the logger option
|
||||||
|
func Logger(l logger.Logger) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meter sets the meter option
|
||||||
|
func Meter(m meter.Meter) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Meter = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client to use for sync/async communication
|
||||||
|
func Client(c client.Client) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Client = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context specifies a context for the service.
|
||||||
|
// Can be used to signal shutdown of the flow
|
||||||
|
// Can be used for extra option values.
|
||||||
|
func Context(ctx context.Context) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracer mechanism for distributed tracking
|
||||||
|
func Tracer(t tracer.Tracer) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Tracer = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store used for intermediate results
|
||||||
|
func Store(s store.Store) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Store = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorflowOption signature
|
||||||
|
type WorkflowOption func(*WorkflowOptions)
|
||||||
|
|
||||||
|
// WorkflowOptions holds workflow options
|
||||||
|
type WorkflowOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowID set workflow id
|
||||||
|
func WorkflowID(id string) WorkflowOption {
|
||||||
|
return func(o *WorkflowOptions) {
|
||||||
|
o.ID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecuteOptions struct {
|
||||||
|
// Client holds the client.Client
|
||||||
|
Client client.Client
|
||||||
|
// Tracer holds the tracer
|
||||||
|
Tracer tracer.Tracer
|
||||||
|
// Logger holds the logger
|
||||||
|
Logger logger.Logger
|
||||||
|
// Meter holds the meter
|
||||||
|
Meter meter.Meter
|
||||||
|
// Context can be used to abort execution or pass additional opts
|
||||||
|
Context context.Context
|
||||||
|
// Start step
|
||||||
|
Start string
|
||||||
|
// Timeout for execution
|
||||||
|
Timeout time.Duration
|
||||||
|
// Reverse execution
|
||||||
|
Reverse bool
|
||||||
|
// Async enables async execution
|
||||||
|
Async bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecuteOption func(*ExecuteOptions)
|
||||||
|
|
||||||
|
func ExecuteClient(c client.Client) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Client = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteTracer(t tracer.Tracer) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Tracer = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteLogger(l logger.Logger) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteMeter(m meter.Meter) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Meter = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteContext(ctx context.Context) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteReverse(b bool) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Reverse = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteTimeout(td time.Duration) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Timeout = td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteAsync(b bool) ExecuteOption {
|
||||||
|
return func(o *ExecuteOptions) {
|
||||||
|
o.Async = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecuteOptions(opts ...ExecuteOption) ExecuteOptions {
|
||||||
|
options := ExecuteOptions{
|
||||||
|
Client: client.DefaultClient,
|
||||||
|
Logger: logger.DefaultLogger,
|
||||||
|
Tracer: tracer.DefaultTracer,
|
||||||
|
Meter: meter.DefaultMeter,
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
type StepOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
Fallback string
|
||||||
|
ID string
|
||||||
|
Requires []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StepOption func(*StepOptions)
|
||||||
|
|
||||||
|
func NewStepOptions(opts ...StepOption) StepOptions {
|
||||||
|
options := StepOptions{
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func StepID(id string) StepOption {
|
||||||
|
return func(o *StepOptions) {
|
||||||
|
o.ID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StepRequires(steps ...string) StepOption {
|
||||||
|
return func(o *StepOptions) {
|
||||||
|
o.Requires = steps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StepFallback(step string) StepOption {
|
||||||
|
return func(o *StepOptions) {
|
||||||
|
o.Fallback = step
|
||||||
|
}
|
||||||
|
}
|
9
go.mod
9
go.mod
@@ -3,11 +3,12 @@ module github.com/unistack-org/micro/v3
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
|
||||||
github.com/ef-ds/deque v1.0.4
|
github.com/ef-ds/deque v1.0.4
|
||||||
github.com/google/uuid v1.2.0
|
github.com/golang-jwt/jwt/v4 v4.1.0
|
||||||
github.com/imdario/mergo v0.3.12
|
github.com/imdario/mergo v0.3.12
|
||||||
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-20210121180416-41cf55125c34
|
github.com/silas/dag v0.0.0-20210626123444-3804bac2d6d4
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
github.com/unistack-org/micro-proto v0.0.9
|
||||||
|
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
)
|
)
|
||||||
|
27
go.sum
27
go.sum
@@ -1,22 +1,37 @@
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
|
||||||
github.com/ef-ds/deque v1.0.4 h1:iFAZNmveMT9WERAkqLJ+oaABF9AcVQ5AjXem/hroniI=
|
github.com/ef-ds/deque v1.0.4 h1:iFAZNmveMT9WERAkqLJ+oaABF9AcVQ5AjXem/hroniI=
|
||||||
github.com/ef-ds/deque v1.0.4/go.mod h1:gXDnTC3yqvBcHbq2lcExjtAcVrOnJCbMcZXmuj8Z4tg=
|
github.com/ef-ds/deque v1.0.4/go.mod h1:gXDnTC3yqvBcHbq2lcExjtAcVrOnJCbMcZXmuj8Z4tg=
|
||||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
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/silas/dag v0.0.0-20210121180416-41cf55125c34 h1:vBfVmA5mZhsQa2jr1FOL9nfA37N/jnbBmi5XUfviVTI=
|
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34 h1:vBfVmA5mZhsQa2jr1FOL9nfA37N/jnbBmi5XUfviVTI=
|
||||||
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
github.com/silas/dag v0.0.0-20210626123444-3804bac2d6d4 h1:fOH64AB0C3ixGf9emky61STvPJL3smxJg+1Zwx1oCdg=
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
github.com/silas/dag v0.0.0-20210626123444-3804bac2d6d4/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
|
github.com/unistack-org/micro-proto v0.0.9 h1:KrWLS4FUX7UAWNAilQf70uad6ZPf/0EudeddCXllRVc=
|
||||||
|
github.com/unistack-org/micro-proto v0.0.9/go.mod h1:Cckwmzd89gvS7ThxzZp9kQR/EOdksFQcsTAtDDyKwrg=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b h1:eB48h3HiRycXNy8E0Gf5e0hv7YT6Kt14L/D73G1fuwo=
|
||||||
|
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
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/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@@ -11,18 +11,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
lvl, err := GetLevel(os.Getenv("MICRO_LOG_LEVEL"))
|
|
||||||
if err != nil {
|
|
||||||
lvl = InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultLogger = NewLogger(WithLevel(lvl))
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultLogger struct {
|
type defaultLogger struct {
|
||||||
enc *json.Encoder
|
enc *json.Encoder
|
||||||
opts Options
|
logFunc LogFunc
|
||||||
|
logfFunc LogfFunc
|
||||||
|
opts Options
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +26,11 @@ func (l *defaultLogger) Init(opts ...Option) error {
|
|||||||
o(&l.opts)
|
o(&l.opts)
|
||||||
}
|
}
|
||||||
l.enc = json.NewEncoder(l.opts.Out)
|
l.enc = json.NewEncoder(l.opts.Out)
|
||||||
|
// wrap the Log func
|
||||||
|
for i := len(l.opts.Wrappers); i > 0; i-- {
|
||||||
|
l.logFunc = l.opts.Wrappers[i-1].Log(l.logFunc)
|
||||||
|
l.logfFunc = l.opts.Wrappers[i-1].Logf(l.logfFunc)
|
||||||
|
}
|
||||||
l.Unlock()
|
l.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -41,6 +39,28 @@ func (l *defaultLogger) String() string {
|
|||||||
return "micro"
|
return "micro"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Clone(opts ...Option) Logger {
|
||||||
|
newopts := NewOptions(opts...)
|
||||||
|
oldopts := l.opts
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&newopts)
|
||||||
|
o(&oldopts)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldopts.Wrappers = newopts.Wrappers
|
||||||
|
l.Lock()
|
||||||
|
cl := &defaultLogger{opts: oldopts, logFunc: l.logFunc, logfFunc: l.logfFunc}
|
||||||
|
l.Unlock()
|
||||||
|
|
||||||
|
// wrap the Log func
|
||||||
|
for i := len(newopts.Wrappers); i > 0; i-- {
|
||||||
|
cl.logFunc = newopts.Wrappers[i-1].Log(cl.logFunc)
|
||||||
|
cl.logfFunc = newopts.Wrappers[i-1].Logf(cl.logfFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) V(level Level) bool {
|
func (l *defaultLogger) V(level Level) bool {
|
||||||
l.RLock()
|
l.RLock()
|
||||||
ok := l.opts.Level.Enabled(level)
|
ok := l.opts.Level.Enabled(level)
|
||||||
@@ -48,26 +68,26 @@ func (l *defaultLogger) V(level Level) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Fields(fields map[string]interface{}) Logger {
|
func (l *defaultLogger) Level(level Level) {
|
||||||
nl := &defaultLogger{opts: l.opts, enc: l.enc}
|
l.Lock()
|
||||||
nl.opts.Fields = make(map[string]interface{}, len(l.opts.Fields)+len(fields))
|
l.opts.Level = level
|
||||||
l.RLock()
|
l.Unlock()
|
||||||
for k, v := range l.opts.Fields {
|
}
|
||||||
nl.opts.Fields[k] = v
|
|
||||||
}
|
|
||||||
l.RUnlock()
|
|
||||||
|
|
||||||
for k, v := range fields {
|
func (l *defaultLogger) Fields(fields ...interface{}) Logger {
|
||||||
nl.opts.Fields[k] = v
|
nl := &defaultLogger{opts: l.opts, enc: l.enc}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nl
|
||||||
|
} else if len(fields)%2 != 0 {
|
||||||
|
fields = fields[:len(fields)-1]
|
||||||
}
|
}
|
||||||
|
nl.opts.Fields = append(nl.opts.Fields, fields...)
|
||||||
return nl
|
return nl
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFields(src map[string]interface{}) map[string]interface{} {
|
func copyFields(src []interface{}) []interface{} {
|
||||||
dst := make(map[string]interface{}, len(src))
|
dst := make([]interface{}, len(src))
|
||||||
for k, v := range src {
|
copy(dst, src)
|
||||||
dst[k] = v
|
|
||||||
}
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,27 +141,27 @@ func (l *defaultLogger) Fatal(ctx context.Context, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Infof(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Infof(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, InfoLevel, msg, args...)
|
l.logfFunc(ctx, InfoLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Errorf(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Errorf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, ErrorLevel, msg, args...)
|
l.logfFunc(ctx, ErrorLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Debugf(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Debugf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, DebugLevel, msg, args...)
|
l.logfFunc(ctx, DebugLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Warnf(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Warnf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, WarnLevel, msg, args...)
|
l.logfFunc(ctx, WarnLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Tracef(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Tracef(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, TraceLevel, msg, args...)
|
l.logfFunc(ctx, TraceLevel, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) {
|
func (l *defaultLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
l.Logf(ctx, FatalLevel, msg, args...)
|
l.logfFunc(ctx, FatalLevel, msg, args...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,19 +174,23 @@ func (l *defaultLogger) Log(ctx context.Context, level Level, args ...interface{
|
|||||||
fields := copyFields(l.opts.Fields)
|
fields := copyFields(l.opts.Fields)
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
|
|
||||||
fields["level"] = level.String()
|
fields = append(fields, "level", level.String())
|
||||||
|
|
||||||
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
||||||
fields["caller"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
|
fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line))
|
||||||
}
|
}
|
||||||
|
fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
fields["timestamp"] = time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
fields["msg"] = fmt.Sprint(args...)
|
fields = append(fields, "msg", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := make(map[string]interface{}, len(fields)/2)
|
||||||
|
for i := 0; i < len(fields); i += 2 {
|
||||||
|
out[fields[i].(string)] = fields[i+1]
|
||||||
|
}
|
||||||
l.RLock()
|
l.RLock()
|
||||||
_ = l.enc.Encode(fields)
|
_ = l.enc.Encode(out)
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,35 +203,39 @@ func (l *defaultLogger) Logf(ctx context.Context, level Level, msg string, args
|
|||||||
fields := copyFields(l.opts.Fields)
|
fields := copyFields(l.opts.Fields)
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
|
|
||||||
fields["level"] = level.String()
|
fields = append(fields, "level", level.String())
|
||||||
|
|
||||||
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
||||||
fields["caller"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
|
fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line))
|
||||||
}
|
}
|
||||||
|
|
||||||
fields["timestamp"] = time.Now().Format("2006-01-02 15:04:05")
|
fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05"))
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
fields["msg"] = fmt.Sprintf(msg, args...)
|
fields = append(fields, "msg", fmt.Sprintf(msg, args...))
|
||||||
} else if msg != "" {
|
} else if msg != "" {
|
||||||
fields["msg"] = msg
|
fields = append(fields, "msg", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]interface{}, len(fields)/2)
|
||||||
|
for i := 0; i < len(fields); i += 2 {
|
||||||
|
out[fields[i].(string)] = fields[i+1]
|
||||||
}
|
}
|
||||||
l.RLock()
|
l.RLock()
|
||||||
_ = l.enc.Encode(fields)
|
_ = l.enc.Encode(out)
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *defaultLogger) Options() Options {
|
func (l *defaultLogger) Options() Options {
|
||||||
// not guard against options Context values
|
return l.opts
|
||||||
l.RLock()
|
|
||||||
opts := l.opts
|
|
||||||
opts.Fields = copyFields(l.opts.Fields)
|
|
||||||
l.RUnlock()
|
|
||||||
return opts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogger builds a new logger based on options
|
// NewLogger builds a new logger based on options
|
||||||
func NewLogger(opts ...Option) Logger {
|
func NewLogger(opts ...Option) Logger {
|
||||||
l := &defaultLogger{opts: NewOptions(opts...)}
|
l := &defaultLogger{
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
}
|
||||||
|
l.logFunc = l.Log
|
||||||
|
l.logfFunc = l.Logf
|
||||||
l.enc = json.NewEncoder(l.opts.Out)
|
l.enc = json.NewEncoder(l.opts.Out)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
@@ -1,24 +1,20 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Level means logger level
|
// Level means logger level
|
||||||
type Level int8
|
type Level int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TraceLevel level. Designates finer-grained informational events than the Debug.
|
// TraceLevel level usually used to find bugs, very verbose
|
||||||
TraceLevel Level = iota - 2
|
TraceLevel Level = iota - 2
|
||||||
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
// DebugLevel level used only when enabled debugging
|
||||||
DebugLevel
|
DebugLevel
|
||||||
// InfoLevel level. General operational entries about what's going on inside the application.
|
// InfoLevel level used for general info about what's going on inside the application
|
||||||
InfoLevel
|
InfoLevel
|
||||||
// WarnLevel level. Non-critical entries that deserve eyes.
|
// WarnLevel level used for non-critical entries
|
||||||
WarnLevel
|
WarnLevel
|
||||||
// ErrorLevel level. Used for errors that should definitely be noted.
|
// ErrorLevel level used for errors that should definitely be noted
|
||||||
ErrorLevel
|
ErrorLevel
|
||||||
// FatalLevel level. Logs and then calls `os.Exit(1)`. highest level of severity.
|
// FatalLevel level used for critical errors and then calls `os.Exit(1)`
|
||||||
FatalLevel
|
FatalLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +34,7 @@ func (l Level) String() string {
|
|||||||
case FatalLevel:
|
case FatalLevel:
|
||||||
return "fatal"
|
return "fatal"
|
||||||
}
|
}
|
||||||
return ""
|
return "info"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enabled returns true if the given level is at or above this level.
|
// Enabled returns true if the given level is at or above this level.
|
||||||
@@ -46,22 +42,22 @@ func (l Level) Enabled(lvl Level) bool {
|
|||||||
return lvl >= l
|
return lvl >= l
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLevel converts a level string into a logger Level value.
|
// ParseLevel converts a level string into a logger Level value.
|
||||||
// returns an error if the input string does not match known values.
|
// returns an InfoLevel if the input string does not match known values.
|
||||||
func GetLevel(levelStr string) (Level, error) {
|
func ParseLevel(lvl string) Level {
|
||||||
switch levelStr {
|
switch lvl {
|
||||||
case TraceLevel.String():
|
case TraceLevel.String():
|
||||||
return TraceLevel, nil
|
return TraceLevel
|
||||||
case DebugLevel.String():
|
case DebugLevel.String():
|
||||||
return DebugLevel, nil
|
return DebugLevel
|
||||||
case InfoLevel.String():
|
case InfoLevel.String():
|
||||||
return InfoLevel, nil
|
return InfoLevel
|
||||||
case WarnLevel.String():
|
case WarnLevel.String():
|
||||||
return WarnLevel, nil
|
return WarnLevel
|
||||||
case ErrorLevel.String():
|
case ErrorLevel.String():
|
||||||
return ErrorLevel, nil
|
return ErrorLevel
|
||||||
case FatalLevel.String():
|
case FatalLevel.String():
|
||||||
return FatalLevel, nil
|
return FatalLevel
|
||||||
}
|
}
|
||||||
return InfoLevel, fmt.Errorf("unknown Level String: '%s', use InfoLevel", levelStr)
|
return InfoLevel
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,34 @@
|
|||||||
// Package logger provides a log interface
|
// Package logger provides a log interface
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultLogger variable
|
// DefaultLogger variable
|
||||||
DefaultLogger Logger = NewLogger()
|
DefaultLogger Logger = NewLogger(WithLevel(ParseLevel(os.Getenv("MICRO_LOG_LEVEL"))))
|
||||||
// DefaultLevel used by logger
|
// DefaultLevel used by logger
|
||||||
DefaultLevel Level = InfoLevel
|
DefaultLevel Level = InfoLevel
|
||||||
|
// DefaultCallerSkipCount used by logger
|
||||||
|
DefaultCallerSkipCount = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is a generic logging interface
|
// Logger is a generic logging interface
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
// Init initialises options
|
// Init initialises options
|
||||||
Init(opts ...Option) error
|
Init(opts ...Option) error
|
||||||
|
// Clone create logger copy with new options
|
||||||
|
Clone(opts ...Option) Logger
|
||||||
// V compare provided verbosity level with current log level
|
// V compare provided verbosity level with current log level
|
||||||
V(level Level) bool
|
V(level Level) bool
|
||||||
|
// Level sets the log level for logger
|
||||||
|
Level(level Level)
|
||||||
// The Logger options
|
// The Logger options
|
||||||
Options() Options
|
Options() Options
|
||||||
// Fields set fields to always be logged
|
// Fields set fields to always be logged with keyval pairs
|
||||||
Fields(fields map[string]interface{}) Logger
|
Fields(fields ...interface{}) Logger
|
||||||
// Info level message
|
// Info level message
|
||||||
Info(ctx context.Context, args ...interface{})
|
Info(ctx context.Context, args ...interface{})
|
||||||
// Trace level message
|
// Trace level message
|
||||||
@@ -52,6 +61,9 @@ type Logger interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field contains keyval pair
|
||||||
|
type Field interface{}
|
||||||
|
|
||||||
// Info writes msg to default logger on info level
|
// Info writes msg to default logger on info level
|
||||||
func Info(ctx context.Context, args ...interface{}) {
|
func Info(ctx context.Context, args ...interface{}) {
|
||||||
DefaultLogger.Info(ctx, args...)
|
DefaultLogger.Info(ctx, args...)
|
||||||
@@ -123,6 +135,6 @@ func Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fields create logger with specific fields
|
// Fields create logger with specific fields
|
||||||
func Fields(fields map[string]interface{}) Logger {
|
func Fields(fields ...interface{}) Logger {
|
||||||
return DefaultLogger.Fields(fields)
|
return DefaultLogger.Fields(fields...)
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,117 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestClone(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nl := l.Clone(WithLevel(ErrorLevel))
|
||||||
|
if err := nl.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nl.Info(ctx, "info message")
|
||||||
|
if len(buf.Bytes()) != 0 {
|
||||||
|
t.Fatal("message must not be logged")
|
||||||
|
}
|
||||||
|
l.Info(ctx, "info message")
|
||||||
|
if len(buf.Bytes()) == 0 {
|
||||||
|
t.Fatal("message must be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirectStdLogger(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fn := RedirectStdLogger(l, ErrorLevel)
|
||||||
|
defer fn()
|
||||||
|
log.Print("test")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"level":"error","msg":"test","timestamp"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLogger(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lg := NewStdLogger(l, ErrorLevel)
|
||||||
|
lg.Print("test")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"level":"error","msg":"test","timestamp"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLogger(t *testing.T) {
|
func TestLogger(t *testing.T) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
l := NewLogger(WithLevel(TraceLevel))
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
|
||||||
if err := l.Init(); err != nil {
|
if err := l.Init(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
l.Trace(ctx, "trace_msg1")
|
l.Trace(ctx, "trace_msg1")
|
||||||
l.Warn(ctx, "warn_msg1")
|
l.Warn(ctx, "warn_msg1")
|
||||||
l.Fields(map[string]interface{}{"error": "test"}).Info(ctx, "error message")
|
l.Fields("error", "test").Info(ctx, "error message")
|
||||||
l.Warn(ctx, "first", " ", "second")
|
l.Warn(ctx, "first", " ", "second")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"level":"trace","msg":"trace_msg1"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"warn","msg":"warn_msg1"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"error":"test","level":"info","msg":"error message"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"level":"warn","msg":"first second"`)) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWrapper(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
|
||||||
|
if err := l.Init(WrapLogger(NewOmitWrapper())); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
type secret struct {
|
||||||
|
Name string
|
||||||
|
Passw string `logger:"omit"`
|
||||||
|
}
|
||||||
|
s := &secret{Name: "name", Passw: "secret"}
|
||||||
|
l.Errorf(ctx, "test %#+v", s)
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`logger.secret{Name:\"name\", Passw:\"\"}"`)) {
|
||||||
|
t.Fatalf("omit not works, struct: %v, output: %s", s, buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOmitLoggerWrapper(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewOmitLogger(NewLogger(WithLevel(TraceLevel), WithOutput(buf)))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
type secret struct {
|
||||||
|
Name string
|
||||||
|
Passw string `logger:"omit"`
|
||||||
|
}
|
||||||
|
s := &secret{Name: "name", Passw: "secret"}
|
||||||
|
l.Errorf(ctx, "test %#+v", s)
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`logger.secret{Name:\"name\", Passw:\"\"}"`)) {
|
||||||
|
t.Fatalf("omit not works, struct: %v, output: %s", s, buf.Bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,22 +16,24 @@ type Options struct {
|
|||||||
// Context holds exernal options
|
// Context holds exernal options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Fields holds additional metadata
|
// Fields holds additional metadata
|
||||||
Fields map[string]interface{}
|
Fields []interface{}
|
||||||
// Name holds the logger name
|
// Name holds the logger name
|
||||||
Name string
|
Name string
|
||||||
// CallerSkipCount number of frmaes to skip
|
// Wrappers logger wrapper that called before actual Log/Logf function
|
||||||
CallerSkipCount int
|
Wrappers []Wrapper
|
||||||
// The logging level the logger should log
|
// The logging level the logger should log
|
||||||
Level Level
|
Level Level
|
||||||
|
// CallerSkipCount number of frmaes to skip
|
||||||
|
CallerSkipCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions creates new options struct
|
// NewOptions creates new options struct
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
Level: DefaultLevel,
|
Level: DefaultLevel,
|
||||||
Fields: make(map[string]interface{}),
|
Fields: make([]interface{}, 0, 6),
|
||||||
Out: os.Stderr,
|
Out: os.Stderr,
|
||||||
CallerSkipCount: 0,
|
CallerSkipCount: DefaultCallerSkipCount,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -41,7 +43,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithFields set default fields for the logger
|
// WithFields set default fields for the logger
|
||||||
func WithFields(fields map[string]interface{}) Option {
|
func WithFields(fields ...interface{}) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Fields = fields
|
o.Fields = fields
|
||||||
}
|
}
|
||||||
@@ -81,3 +83,10 @@ func WithName(n string) Option {
|
|||||||
o.Name = n
|
o.Name = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapLogger adds a logger Wrapper to a list of options passed into the logger
|
||||||
|
func WrapLogger(w Wrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Wrappers = append(o.Wrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35
logger/stdlogger.go
Normal file
35
logger/stdlogger.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stdLogger struct {
|
||||||
|
l Logger
|
||||||
|
level Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStdLogger(l Logger, level Level) *log.Logger {
|
||||||
|
return log.New(&stdLogger{l: l, level: level}, "" /* prefix */, 0 /* flags */)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *stdLogger) Write(p []byte) (int, error) {
|
||||||
|
p = bytes.TrimSpace(p)
|
||||||
|
sl.l.Log(sl.l.Options().Context, sl.level, string(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RedirectStdLogger(l Logger, level Level) func() {
|
||||||
|
flags := log.Flags()
|
||||||
|
prefix := log.Prefix()
|
||||||
|
writer := log.Writer()
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("")
|
||||||
|
log.SetOutput(&stdLogger{l: l, level: level})
|
||||||
|
return func() {
|
||||||
|
log.SetFlags(flags)
|
||||||
|
log.SetPrefix(prefix)
|
||||||
|
log.SetOutput(writer)
|
||||||
|
}
|
||||||
|
}
|
166
logger/wrapper.go
Normal file
166
logger/wrapper.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
rutil "github.com/unistack-org/micro/v3/util/reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogFunc function used for Log method
|
||||||
|
type LogFunc func(ctx context.Context, level Level, args ...interface{})
|
||||||
|
|
||||||
|
// LogfFunc function used for Logf method
|
||||||
|
type LogfFunc func(ctx context.Context, level Level, msg string, args ...interface{})
|
||||||
|
|
||||||
|
type Wrapper interface {
|
||||||
|
// Log logs message with needed level
|
||||||
|
Log(LogFunc) LogFunc
|
||||||
|
// Logf logs message with needed level
|
||||||
|
Logf(LogfFunc) LogfFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Logger = &OmitLogger{}
|
||||||
|
|
||||||
|
type OmitLogger struct {
|
||||||
|
l Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOmitLogger(l Logger) Logger {
|
||||||
|
return &OmitLogger{l: l}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Init(opts ...Option) error {
|
||||||
|
return w.l.Init(append(opts, WrapLogger(NewOmitWrapper()))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) V(level Level) bool {
|
||||||
|
return w.l.V(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Level(level Level) {
|
||||||
|
w.l.Level(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Clone(opts ...Option) Logger {
|
||||||
|
return w.l.Clone(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Options() Options {
|
||||||
|
return w.l.Options()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Fields(fields ...interface{}) Logger {
|
||||||
|
return w.l.Fields(fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Info(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Info(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Trace(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Trace(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Debug(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Debug(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Warn(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Warn(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Error(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Error(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Fatal(ctx context.Context, args ...interface{}) {
|
||||||
|
w.l.Fatal(ctx, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Infof(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Infof(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Tracef(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Tracef(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Debugf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Debugf(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Warnf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Warnf(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Errorf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Errorf(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) {
|
||||||
|
w.l.Fatalf(ctx, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Log(ctx context.Context, level Level, args ...interface{}) {
|
||||||
|
w.l.Log(ctx, level, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) Logf(ctx context.Context, level Level, msg string, args ...interface{}) {
|
||||||
|
w.l.Logf(ctx, level, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitLogger) String() string {
|
||||||
|
return w.l.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitWrapper struct{}
|
||||||
|
|
||||||
|
func NewOmitWrapper() Wrapper {
|
||||||
|
return &OmitWrapper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArgs(args []interface{}) []interface{} {
|
||||||
|
nargs := make([]interface{}, 0, len(args))
|
||||||
|
var err error
|
||||||
|
for _, arg := range args {
|
||||||
|
val := reflect.ValueOf(arg)
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
narg := arg
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
nargs = append(nargs, narg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if narg, err = rutil.Zero(arg); err != nil {
|
||||||
|
nargs = append(nargs, narg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rutil.CopyDefaults(narg, arg)
|
||||||
|
if flds, ferr := rutil.StructFields(narg); ferr == nil {
|
||||||
|
for _, fld := range flds {
|
||||||
|
if tv, ok := fld.Field.Tag.Lookup("logger"); ok && tv == "omit" {
|
||||||
|
fld.Value.Set(reflect.Zero(fld.Value.Type()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nargs = append(nargs, narg)
|
||||||
|
}
|
||||||
|
return nargs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitWrapper) Log(fn LogFunc) LogFunc {
|
||||||
|
return func(ctx context.Context, level Level, args ...interface{}) {
|
||||||
|
fn(ctx, level, getArgs(args)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *OmitWrapper) Logf(fn LogfFunc) LogfFunc {
|
||||||
|
return func(ctx context.Context, level Level, msg string, args ...interface{}) {
|
||||||
|
fn(ctx, level, msg, getArgs(args)...)
|
||||||
|
}
|
||||||
|
}
|
@@ -6,8 +6,20 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeaderPrefix for all headers passed
|
var (
|
||||||
var HeaderPrefix = "Micro-"
|
// HeaderTopic is the header name that contains topic name
|
||||||
|
HeaderTopic = "Micro-Topic"
|
||||||
|
// HeaderContentType specifies content type of message
|
||||||
|
HeaderContentType = "Content-Type"
|
||||||
|
// HeaderEndpoint specifies endpoint in service
|
||||||
|
HeaderEndpoint = "Micro-Endpoint"
|
||||||
|
// HeaderService specifies service
|
||||||
|
HeaderService = "Micro-Service"
|
||||||
|
// HeaderTimeout specifies timeout of operation
|
||||||
|
HeaderTimeout = "Micro-Timeout"
|
||||||
|
// HeaderAuthorization specifies Authorization header
|
||||||
|
HeaderAuthorization = "Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
// Metadata is our way of representing request headers internally.
|
// Metadata is our way of representing request headers internally.
|
||||||
// They're used at the RPC level and translate back and forth
|
// They're used at the RPC level and translate back and forth
|
||||||
@@ -95,13 +107,16 @@ func New(size int) Metadata {
|
|||||||
|
|
||||||
// Merge merges metadata to existing metadata, overwriting if specified
|
// Merge merges metadata to existing metadata, overwriting if specified
|
||||||
func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata {
|
func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata {
|
||||||
|
var ok bool
|
||||||
nmd := Copy(omd)
|
nmd := Copy(omd)
|
||||||
for key, val := range mmd {
|
for key, val := range mmd {
|
||||||
if _, ok := nmd[key]; ok && !overwrite {
|
_, ok = nmd[key]
|
||||||
// skip
|
switch {
|
||||||
} else if val != "" {
|
case ok && !overwrite:
|
||||||
|
continue
|
||||||
|
case val != "":
|
||||||
nmd.Set(key, val)
|
nmd.Set(key, val)
|
||||||
} else {
|
case ok && val == "":
|
||||||
nmd.Del(key)
|
nmd.Del(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
package meter
|
package meter
|
||||||
|
|
||||||
//go:generate protoc -I./handler -I../ -I/home/vtolstov/.cache/go-path/pkg/mod/github.com/unistack-org/micro-proto@v0.0.1 --micro_out=components=micro|http|server,standalone=false,debug=true,paths=source_relative:./handler handler/handler.proto
|
//go:generate sh -c "protoc -I./handler -I../ -I$(go list -f '{{ .Dir }}' -m github.com/unistack-org/micro-proto) --go-micro_out='components=micro|http|server',standalone=false,debug=true,paths=source_relative:./handler handler/handler.proto"
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
// import required packages
|
||||||
|
_ "github.com/unistack-org/micro-proto/api"
|
||||||
|
|
||||||
|
// import required packages
|
||||||
|
_ "github.com/unistack-org/micro-proto/openapiv2"
|
||||||
|
)
|
||||||
|
@@ -11,17 +11,17 @@ service Meter {
|
|||||||
rpc Metrics(micro.codec.Frame) returns (micro.codec.Frame) {
|
rpc Metrics(micro.codec.Frame) returns (micro.codec.Frame) {
|
||||||
option (micro.openapiv2.openapiv2_operation) = {
|
option (micro.openapiv2.openapiv2_operation) = {
|
||||||
operation_id: "Metrics";
|
operation_id: "Metrics";
|
||||||
responses: {
|
responses: {
|
||||||
key: "default";
|
response_code: {
|
||||||
|
name: "default";
|
||||||
value: {
|
value: {
|
||||||
description: "Error response";
|
json_reference: {
|
||||||
schema: {
|
description: "Error response";
|
||||||
json_schema: {
|
_ref: "micro.codec.Frame";
|
||||||
ref: "micro.codec.Frame";
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
};
|
};
|
||||||
option (micro.api.http) = { get: "/metrics"; };
|
option (micro.api.http) = { get: "/metrics"; };
|
||||||
};
|
};
|
||||||
|
@@ -1,22 +1,31 @@
|
|||||||
// Code generated by protoc-gen-micro
|
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
|
||||||
|
// protoc-gen-go-micro version: v3.4.2
|
||||||
// source: handler.proto
|
// source: handler.proto
|
||||||
|
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
api "github.com/unistack-org/micro/v3/api"
|
api "github.com/unistack-org/micro/v3/api"
|
||||||
codec "github.com/unistack-org/micro/v3/codec"
|
codec "github.com/unistack-org/micro/v3/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMeterEndpoints() []*api.Endpoint {
|
var (
|
||||||
return []*api.Endpoint{
|
MeterName = "Meter"
|
||||||
&api.Endpoint{
|
|
||||||
|
MeterEndpoints = []api.Endpoint{
|
||||||
|
{
|
||||||
Name: "Meter.Metrics",
|
Name: "Meter.Metrics",
|
||||||
Path: []string{"/metrics"},
|
Path: []string{"/metrics"},
|
||||||
Method: []string{"GET"},
|
Method: []string{"GET"},
|
||||||
Handler: "rpc",
|
Handler: "rpc",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMeterEndpoints() []api.Endpoint {
|
||||||
|
return MeterEndpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
type MeterServer interface {
|
type MeterServer interface {
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
// Code generated by protoc-gen-micro
|
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
|
||||||
|
// protoc-gen-go-micro version: v3.4.2
|
||||||
// source: handler.proto
|
// source: handler.proto
|
||||||
|
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
api "github.com/unistack-org/micro/v3/api"
|
api "github.com/unistack-org/micro/v3/api"
|
||||||
codec "github.com/unistack-org/micro/v3/codec"
|
codec "github.com/unistack-org/micro/v3/codec"
|
||||||
server "github.com/unistack-org/micro/v3/server"
|
server "github.com/unistack-org/micro/v3/server"
|
||||||
@@ -26,8 +29,8 @@ func RegisterMeterServer(s server.Server, sh MeterServer, opts ...server.Handler
|
|||||||
}
|
}
|
||||||
h := &meterServer{sh}
|
h := &meterServer{sh}
|
||||||
var nopts []server.HandlerOption
|
var nopts []server.HandlerOption
|
||||||
for _, endpoint := range NewMeterEndpoints() {
|
for _, endpoint := range MeterEndpoints {
|
||||||
nopts = append(nopts, api.WithEndpoint(endpoint))
|
nopts = append(nopts, api.WithEndpoint(&endpoint))
|
||||||
}
|
}
|
||||||
return s.Handle(s.NewHandler(&Meter{h}, append(nopts, opts...)...))
|
return s.Handle(s.NewHandler(&Meter{h}, append(nopts, opts...)...))
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,9 @@ package meter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,36 +78,62 @@ type Summary interface {
|
|||||||
UpdateDuration(time.Time)
|
UpdateDuration(time.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sort labels alphabeticaly by label name
|
||||||
type byKey []string
|
type byKey []string
|
||||||
|
|
||||||
func (k byKey) Len() int { return len(k) / 2 }
|
func (k byKey) Len() int { return len(k) / 2 }
|
||||||
func (k byKey) Less(i, j int) bool { return k[i*2] < k[j*2] }
|
func (k byKey) Less(i, j int) bool { return k[i*2] < k[j*2] }
|
||||||
func (k byKey) Swap(i, j int) {
|
func (k byKey) Swap(i, j int) {
|
||||||
k[i*2], k[i*2+1], k[j*2], k[j*2+1] = k[j*2], k[j*2+1], k[i*2], k[i*2+1]
|
k[i*2], k[j*2] = k[j*2], k[i*2]
|
||||||
|
k[i*2+1], k[j*2+1] = k[j*2+1], k[i*2+1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func Sort(slice *[]string) {
|
// BuildLables used to sort labels and delete duplicates.
|
||||||
bk := byKey(*slice)
|
// Last value wins in case of duplicate label keys.
|
||||||
if bk.Len() <= 1 {
|
func BuildLabels(labels ...string) []string {
|
||||||
return
|
if len(labels)%2 == 1 {
|
||||||
|
labels = labels[:len(labels)-1]
|
||||||
}
|
}
|
||||||
sort.Sort(bk)
|
sort.Sort(byKey(labels))
|
||||||
v := reflect.ValueOf(slice).Elem()
|
return labels
|
||||||
cnt := 0
|
}
|
||||||
key := 0
|
|
||||||
val := 1
|
// BuildName used to combine metric with labels.
|
||||||
for key < v.Len() {
|
// If labels count is odd, drop last element
|
||||||
if len(bk) > key+2 && bk[key] == bk[key+2] {
|
func BuildName(name string, labels ...string) string {
|
||||||
key += 2
|
if len(labels)%2 == 1 {
|
||||||
val += 2
|
labels = labels[:len(labels)-1]
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
v.Index(cnt).Set(v.Index(key))
|
if len(labels) > 2 {
|
||||||
cnt++
|
sort.Sort(byKey(labels))
|
||||||
v.Index(cnt).Set(v.Index(val))
|
|
||||||
cnt++
|
idx := 0
|
||||||
key += 2
|
for {
|
||||||
val += 2
|
if labels[idx] == labels[idx+2] {
|
||||||
}
|
copy(labels[idx:], labels[idx+2:])
|
||||||
v.SetLen(cnt)
|
labels = labels[:len(labels)-2]
|
||||||
|
} else {
|
||||||
|
idx += 2
|
||||||
|
}
|
||||||
|
if idx+2 >= len(labels) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
_, _ = b.WriteString(name)
|
||||||
|
_, _ = b.WriteRune('{')
|
||||||
|
for idx := 0; idx < len(labels); idx += 2 {
|
||||||
|
if idx > 0 {
|
||||||
|
_, _ = b.WriteRune(',')
|
||||||
|
}
|
||||||
|
_, _ = b.WriteString(labels[idx])
|
||||||
|
_, _ = b.WriteString(`=`)
|
||||||
|
_, _ = b.WriteString(strconv.Quote(labels[idx+1]))
|
||||||
|
}
|
||||||
|
_, _ = b.WriteRune('}')
|
||||||
|
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
@@ -14,11 +14,57 @@ func TestNoopMeter(t *testing.T) {
|
|||||||
cnt.Inc()
|
cnt.Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLabelsSort(t *testing.T) {
|
func testEq(a, b []string) bool {
|
||||||
ls := []string{"server", "http", "register", "mdns", "broker", "broker1", "broker", "broker2", "server", "tcp"}
|
if len(a) != len(b) {
|
||||||
Sort(&ls)
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if ls[0] != "broker" || ls[1] != "broker2" {
|
func TestBuildLabels(t *testing.T) {
|
||||||
t.Fatalf("sort error: %v", ls)
|
type testData struct {
|
||||||
|
src []string
|
||||||
|
dst []string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []testData{
|
||||||
|
{
|
||||||
|
src: []string{"zerolabel", "value3", "firstlabel", "value2"},
|
||||||
|
dst: []string{"firstlabel", "value2", "zerolabel", "value3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range data {
|
||||||
|
if !testEq(d.dst, BuildLabels(d.src...)) {
|
||||||
|
t.Fatalf("slices not properly sorted: %v %v", d.dst, d.src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildName(t *testing.T) {
|
||||||
|
data := map[string][]string{
|
||||||
|
`my_metric{firstlabel="value2",zerolabel="value3"}`: {
|
||||||
|
"my_metric",
|
||||||
|
"zerolabel", "value3", "firstlabel", "value2",
|
||||||
|
},
|
||||||
|
`my_metric{broker="broker2",register="mdns",server="tcp"}`: {
|
||||||
|
"my_metric",
|
||||||
|
"broker", "broker1", "broker", "broker2", "server", "http", "server", "tcp", "register", "mdns",
|
||||||
|
},
|
||||||
|
`my_metric{aaa="aaa"}`: {
|
||||||
|
"my_metric",
|
||||||
|
"aaa", "aaa",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for e, d := range data {
|
||||||
|
if x := BuildName(d[0], d[1:]...); x != e {
|
||||||
|
t.Fatalf("expect: %s, result: %s", e, x)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/meter"
|
"github.com/unistack-org/micro/v3/meter"
|
||||||
"github.com/unistack-org/micro/v3/network/tunnel"
|
"github.com/unistack-org/micro/v3/network/tunnel"
|
||||||
"github.com/unistack-org/micro/v3/proxy"
|
"github.com/unistack-org/micro/v3/proxy"
|
||||||
"github.com/unistack-org/micro/v3/router"
|
"github.com/unistack-org/micro/v3/router"
|
||||||
"github.com/unistack-org/micro/v3/tracer"
|
"github.com/unistack-org/micro/v3/tracer"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
@@ -27,8 +27,8 @@ type Options struct {
|
|||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
// Tunnel used for transfer data
|
// Tunnel used for transfer data
|
||||||
Tunnel tunnel.Tunnel
|
Tunnel tunnel.Tunnel
|
||||||
// Id of the node
|
// ID of the node
|
||||||
Id string
|
ID string
|
||||||
// Name of the network
|
// Name of the network
|
||||||
Name string
|
Name string
|
||||||
// Address to bind to
|
// Address to bind to
|
||||||
@@ -39,10 +39,10 @@ type Options struct {
|
|||||||
Nodes []string
|
Nodes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id sets the id of the network node
|
// ID sets the id of the network node
|
||||||
func Id(id string) Option {
|
func ID(id string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Id = id
|
o.ID = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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: uuid.New().String(),
|
ID: id.Must(),
|
||||||
Name: "go.micro",
|
Name: "go.micro",
|
||||||
Address: ":0",
|
Address: ":0",
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -16,12 +16,14 @@ func TestMemoryTransport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
||||||
|
cherr := make(chan error, 1)
|
||||||
// accept
|
// accept
|
||||||
go func() {
|
go func() {
|
||||||
if err := l.Accept(func(sock Socket) {
|
if nerr := l.Accept(func(sock Socket) {
|
||||||
for {
|
for {
|
||||||
var m Message
|
var m Message
|
||||||
if err := sock.Recv(&m); err != nil {
|
if rerr := sock.Recv(&m); rerr != nil {
|
||||||
|
cherr <- rerr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
|
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
|
||||||
@@ -30,11 +32,12 @@ func TestMemoryTransport(t *testing.T) {
|
|||||||
if cerr := sock.Send(&Message{
|
if cerr := sock.Send(&Message{
|
||||||
Body: []byte(`pong`),
|
Body: []byte(`pong`),
|
||||||
}); cerr != nil {
|
}); cerr != nil {
|
||||||
|
cherr <- cerr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}); err != nil {
|
}); nerr != nil {
|
||||||
t.Fatalf("Unexpected error accepting %v", err)
|
cherr <- err
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -45,19 +48,24 @@ func TestMemoryTransport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// send <=> receive
|
select {
|
||||||
for i := 0; i < 3; i++ {
|
case err := <-cherr:
|
||||||
if err := c.Send(&Message{
|
t.Fatal(err)
|
||||||
Body: []byte(`ping`),
|
default:
|
||||||
}); err != nil {
|
// send <=> receive
|
||||||
return
|
for i := 0; i < 3; i++ {
|
||||||
}
|
if err := c.Send(&Message{
|
||||||
var m Message
|
Body: []byte(`ping`),
|
||||||
if err := c.Recv(&m); err != nil {
|
}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
|
var m Message
|
||||||
t.Logf("Client Received %s", string(m.Body))
|
if err := c.Recv(&m); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
|
||||||
|
t.Logf("Client Received %s", string(m.Body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/unistack-org/micro/v3/broker"
|
"github.com/unistack-org/micro/v3/broker"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
"github.com/unistack-org/micro/v3/network/transport"
|
"github.com/unistack-org/micro/v3/network/transport"
|
||||||
"github.com/unistack-org/micro/v3/network/tunnel"
|
"github.com/unistack-org/micro/v3/network/tunnel"
|
||||||
)
|
)
|
||||||
@@ -24,7 +25,16 @@ type tunSubscriber struct {
|
|||||||
opts broker.SubscribeOptions
|
opts broker.SubscribeOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tunBatchSubscriber struct {
|
||||||
|
listener tunnel.Listener
|
||||||
|
handler broker.BatchHandler
|
||||||
|
closed chan bool
|
||||||
|
topic string
|
||||||
|
opts broker.SubscribeOptions
|
||||||
|
}
|
||||||
|
|
||||||
type tunEvent struct {
|
type tunEvent struct {
|
||||||
|
err error
|
||||||
message *broker.Message
|
message *broker.Message
|
||||||
topic string
|
topic string
|
||||||
}
|
}
|
||||||
@@ -62,6 +72,36 @@ 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 {
|
||||||
|
// TODO: this is probably inefficient, we might want to just maintain an open connection
|
||||||
|
// it may be easier to add broadcast to the tunnel
|
||||||
|
topicMap := make(map[string]tunnel.Session)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, msg := range msgs {
|
||||||
|
topic, _ := msg.Header.Get(metadata.HeaderTopic)
|
||||||
|
c, ok := topicMap[topic]
|
||||||
|
if !ok {
|
||||||
|
c, err = t.tunnel.Dial(ctx, topic, tunnel.DialMode(tunnel.Multicast))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
topicMap[topic] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Send(&transport.Message{
|
||||||
|
Header: msg.Header,
|
||||||
|
Body: msg.Body,
|
||||||
|
}); err != nil {
|
||||||
|
// msg.SetError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, opts ...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
|
||||||
@@ -77,6 +117,26 @@ func (t *tunBroker) Publish(ctx context.Context, topic string, m *broker.Message
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tunBroker) BatchSubscribe(ctx context.Context, topic string, h broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
|
l, err := t.tunnel.Listen(ctx, topic, tunnel.ListenMode(tunnel.Multicast))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tunSub := &tunBatchSubscriber{
|
||||||
|
topic: topic,
|
||||||
|
handler: h,
|
||||||
|
opts: broker.NewSubscribeOptions(opts...),
|
||||||
|
closed: make(chan bool),
|
||||||
|
listener: l,
|
||||||
|
}
|
||||||
|
|
||||||
|
// start processing
|
||||||
|
go tunSub.run()
|
||||||
|
|
||||||
|
return tunSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *tunBroker) Subscribe(ctx context.Context, topic string, h broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
func (t *tunBroker) Subscribe(ctx context.Context, topic string, h broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||||
l, err := t.tunnel.Listen(ctx, topic, tunnel.ListenMode(tunnel.Multicast))
|
l, err := t.tunnel.Listen(ctx, topic, tunnel.ListenMode(tunnel.Multicast))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,6 +161,51 @@ func (t *tunBroker) String() string {
|
|||||||
return "tunnel"
|
return "tunnel"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tunBatchSubscriber) run() {
|
||||||
|
for {
|
||||||
|
// accept a new connection
|
||||||
|
c, err := t.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-t.closed:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive message
|
||||||
|
m := new(transport.Message)
|
||||||
|
if err := c.Recv(m); err != nil {
|
||||||
|
if logger.V(logger.ErrorLevel) {
|
||||||
|
logger.Error(t.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Close(); err != nil {
|
||||||
|
if logger.V(logger.ErrorLevel) {
|
||||||
|
logger.Error(t.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the connection
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
evts := broker.Events{&tunEvent{
|
||||||
|
topic: t.topic,
|
||||||
|
message: &broker.Message{
|
||||||
|
Header: m.Header,
|
||||||
|
Body: m.Body,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// handle the message
|
||||||
|
go func() {
|
||||||
|
_ = t.handler(evts)
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *tunSubscriber) run() {
|
func (t *tunSubscriber) run() {
|
||||||
for {
|
for {
|
||||||
// accept a new connection
|
// accept a new connection
|
||||||
@@ -132,13 +237,33 @@ func (t *tunSubscriber) run() {
|
|||||||
c.Close()
|
c.Close()
|
||||||
|
|
||||||
// handle the message
|
// handle the message
|
||||||
go t.handler(&tunEvent{
|
go func() {
|
||||||
topic: t.topic,
|
_ = t.handler(&tunEvent{
|
||||||
message: &broker.Message{
|
topic: t.topic,
|
||||||
Header: m.Header,
|
message: &broker.Message{
|
||||||
Body: m.Body,
|
Header: m.Header,
|
||||||
},
|
Body: m.Body,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunBatchSubscriber) Options() broker.SubscribeOptions {
|
||||||
|
return t.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunBatchSubscriber) Topic() string {
|
||||||
|
return t.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunBatchSubscriber) Unsubscribe(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-t.closed:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
close(t.closed)
|
||||||
|
return t.listener.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +298,11 @@ func (t *tunEvent) Ack() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *tunEvent) Error() error {
|
func (t *tunEvent) Error() error {
|
||||||
return nil
|
return t.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunEvent) SetError(err error) {
|
||||||
|
t.err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBroker returns new tunnel broker
|
// NewBroker returns new tunnel broker
|
||||||
|
@@ -3,11 +3,11 @@ package tunnel
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/meter"
|
"github.com/unistack-org/micro/v3/meter"
|
||||||
"github.com/unistack-org/micro/v3/network/transport"
|
"github.com/unistack-org/micro/v3/network/transport"
|
||||||
"github.com/unistack-org/micro/v3/tracer"
|
"github.com/unistack-org/micro/v3/tracer"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -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: uuid.New().String(),
|
ID: id.Must(),
|
||||||
Address: DefaultAddress,
|
Address: DefaultAddress,
|
||||||
Token: DefaultToken,
|
Token: DefaultToken,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -37,7 +37,7 @@ func (t *tunTransport) Init(opts ...transport.Option) error {
|
|||||||
// get the transport
|
// get the transport
|
||||||
tr, ok := t.options.Context.Value(transportKey{}).(transport.Transport)
|
tr, ok := t.options.Context.Value(transportKey{}).(transport.Transport)
|
||||||
if ok {
|
if ok {
|
||||||
tun.Init(tunnel.Transport(tr))
|
_ = tun.Init(tunnel.Transport(tr))
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the tunnel
|
// set the tunnel
|
||||||
|
@@ -31,7 +31,7 @@ func (p *profiler) writeHeap(f *os.File) {
|
|||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
pprof.WriteHeapProfile(f)
|
_ = pprof.WriteHeapProfile(f)
|
||||||
case <-p.exit:
|
case <-p.exit:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -378,13 +378,16 @@ func (m *memory) ListServices(ctx context.Context, opts ...ListOption) ([]*Servi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *memory) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
func (m *memory) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
||||||
|
id, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
wo := NewWatchOptions(opts...)
|
wo := NewWatchOptions(opts...)
|
||||||
|
|
||||||
// construct the watcher
|
// construct the watcher
|
||||||
w := &watcher{
|
w := &watcher{
|
||||||
exit: make(chan bool),
|
exit: make(chan bool),
|
||||||
res: make(chan *Result),
|
res: make(chan *Result),
|
||||||
id: uuid.New().String(),
|
id: id,
|
||||||
wo: wo,
|
wo: wo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: golint
|
// nolint: golint,revive
|
||||||
// RegisterOptions holds options for register method
|
// RegisterOptions holds options for register method
|
||||||
type RegisterOptions struct {
|
type RegisterOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
@@ -197,7 +197,7 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: golint
|
// nolint: golint,revive
|
||||||
// RegisterAttempts specifies register atempts count
|
// RegisterAttempts specifies register atempts count
|
||||||
func RegisterAttempts(t int) RegisterOption {
|
func RegisterAttempts(t int) RegisterOption {
|
||||||
return func(o *RegisterOptions) {
|
return func(o *RegisterOptions) {
|
||||||
@@ -205,7 +205,7 @@ func RegisterAttempts(t int) RegisterOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: golint
|
// nolint: golint,revive
|
||||||
// RegisterTTL specifies register ttl
|
// RegisterTTL specifies register ttl
|
||||||
func RegisterTTL(t time.Duration) RegisterOption {
|
func RegisterTTL(t time.Duration) RegisterOption {
|
||||||
return func(o *RegisterOptions) {
|
return func(o *RegisterOptions) {
|
||||||
@@ -213,7 +213,7 @@ func RegisterTTL(t time.Duration) RegisterOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: golint
|
// nolint: golint,revive
|
||||||
// RegisterContext sets the register context
|
// RegisterContext sets the register context
|
||||||
func RegisterContext(ctx context.Context) RegisterOption {
|
func RegisterContext(ctx context.Context) RegisterOption {
|
||||||
return func(o *RegisterOptions) {
|
return func(o *RegisterOptions) {
|
||||||
@@ -221,7 +221,7 @@ func RegisterContext(ctx context.Context) RegisterOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: golint
|
// nolint: golint,revive
|
||||||
// RegisterDomain secifies register domain
|
// RegisterDomain secifies register domain
|
||||||
func RegisterDomain(d string) RegisterOption {
|
func RegisterDomain(d string) RegisterOption {
|
||||||
return func(o *RegisterOptions) {
|
return func(o *RegisterOptions) {
|
||||||
|
@@ -68,8 +68,8 @@ type Endpoint struct {
|
|||||||
// Option func signature
|
// Option func signature
|
||||||
type Option func(*Options)
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// nolint: golint,revive
|
||||||
// RegisterOption option is used to register service
|
// RegisterOption option is used to register service
|
||||||
// nolint: golint
|
|
||||||
type RegisterOption func(*RegisterOptions)
|
type RegisterOption func(*RegisterOptions)
|
||||||
|
|
||||||
// WatchOption option is used to watch service changes
|
// WatchOption option is used to watch service changes
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/unistack-org/micro/v3/resolver"
|
"github.com/unistack-org/micro/v3/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// nolint: golint,revive
|
||||||
// HTTPResolver is a HTTP network resolver
|
// HTTPResolver is a HTTP network resolver
|
||||||
type HTTPResolver struct {
|
type HTTPResolver struct {
|
||||||
// Proto if not set, defaults to http
|
// Proto if not set, defaults to http
|
||||||
@@ -53,6 +54,7 @@ func (r *HTTPResolver) Resolve(name string) ([]*resolver.Record, error) {
|
|||||||
q.Set("name", name)
|
q.Set("name", name)
|
||||||
uri.RawQuery = q.Encode()
|
uri.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
// nolint: noctx
|
||||||
rsp, err := http.Get(uri.String())
|
rsp, err := http.Get(uri.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -40,8 +40,9 @@ func (d *dns) Lookup(opts ...QueryOption) ([]Route, error) {
|
|||||||
// check to see if we have the port provided in the service, e.g. go-micro-srv-foo:8000
|
// check to see if we have the port provided in the service, e.g. go-micro-srv-foo:8000
|
||||||
host, port, err := net.SplitHostPort(options.Service)
|
host, port, err := net.SplitHostPort(options.Service)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
var ips []string
|
||||||
// lookup the service using A records
|
// lookup the service using A records
|
||||||
ips, err := net.LookupHost(host)
|
ips, err = net.LookupHost(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ func (d *dns) Lookup(opts ...QueryOption) ([]Route, error) {
|
|||||||
for i, ip := range ips {
|
for i, ip := range ips {
|
||||||
result[i] = Route{
|
result[i] = Route{
|
||||||
Service: options.Service,
|
Service: options.Service,
|
||||||
Address: fmt.Sprintf("%s:%d", ip, uint16(p)),
|
Address: fmt.Sprintf("%s:%d", ip, p),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
@@ -3,9 +3,9 @@ package router
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/register"
|
"github.com/unistack-org/micro/v3/register"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options are router options
|
// Options are router options
|
||||||
@@ -16,15 +16,15 @@ type Options struct {
|
|||||||
Name string
|
Name string
|
||||||
Gateway string
|
Gateway string
|
||||||
Network string
|
Network string
|
||||||
Id string
|
ID string
|
||||||
Address string
|
Address string
|
||||||
Precache bool
|
Precache bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id sets Router Id
|
// ID sets Router Id
|
||||||
func Id(id string) Option {
|
func ID(id string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Id = id
|
o.ID = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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: uuid.New().String(),
|
ID: id.Must(),
|
||||||
Network: DefaultNetwork,
|
Network: DefaultNetwork,
|
||||||
Register: register.DefaultRegister,
|
Register: register.DefaultRegister,
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
@@ -38,8 +38,8 @@ func (t EventType) String() string {
|
|||||||
type Event struct {
|
type Event struct {
|
||||||
// Timestamp is event timestamp
|
// Timestamp is event timestamp
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
// Id of the event
|
// ID of the event
|
||||||
Id string
|
ID string
|
||||||
// Route is table route
|
// Route is table route
|
||||||
Route Route
|
Route Route
|
||||||
// Type defines type of event
|
// Type defines type of event
|
||||||
|
@@ -13,7 +13,7 @@ func TestError(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("error not *errors.Error")
|
t.Fatal("error not *errors.Error")
|
||||||
}
|
}
|
||||||
if merr.Id != "svc1" {
|
if merr.ID != "svc1" {
|
||||||
t.Fatal("id != svc1")
|
t.Fatal("id != svc1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
//go:generate protoc -I./health -I../ -I/home/vtolstov/.cache/go-path/pkg/mod/github.com/unistack-org/micro-proto@v0.0.1 --micro_out=components=micro|http|server,standalone=false,debug=true,paths=source_relative:./health health/health.proto
|
//go:generate sh -c "protoc -I./health -I../ -I$(go list -f '{{ .Dir }}' -m github.com/unistack-org/micro-proto) --go-micro_out='components=micro|http|server',standalone=false,debug=true,paths=source_relative:./health health/health.proto"
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
// import required packages
|
||||||
|
_ "github.com/unistack-org/micro-proto/api"
|
||||||
|
|
||||||
|
// import required packages
|
||||||
|
_ "github.com/unistack-org/micro-proto/openapiv2"
|
||||||
|
)
|
||||||
|
@@ -11,51 +11,51 @@ service Health {
|
|||||||
rpc Live(micro.codec.Frame) returns (micro.codec.Frame) {
|
rpc Live(micro.codec.Frame) returns (micro.codec.Frame) {
|
||||||
option (micro.openapiv2.openapiv2_operation) = {
|
option (micro.openapiv2.openapiv2_operation) = {
|
||||||
operation_id: "Live";
|
operation_id: "Live";
|
||||||
responses: {
|
responses: {
|
||||||
key: "default";
|
response_code: {
|
||||||
|
name: "default";
|
||||||
value: {
|
value: {
|
||||||
description: "Error response";
|
json_reference: {
|
||||||
schema: {
|
description: "Error response";
|
||||||
json_schema: {
|
_ref: "micro.codec.Frame";
|
||||||
ref: "micro.codec.Frame";
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
};
|
};
|
||||||
option (micro.api.http) = { get: "/live"; };
|
option (micro.api.http) = { get: "/live"; };
|
||||||
};
|
};
|
||||||
rpc Ready(micro.codec.Frame) returns (micro.codec.Frame) {
|
rpc Ready(micro.codec.Frame) returns (micro.codec.Frame) {
|
||||||
option (micro.openapiv2.openapiv2_operation) = {
|
option (micro.openapiv2.openapiv2_operation) = {
|
||||||
operation_id: "Ready";
|
operation_id: "Ready";
|
||||||
responses: {
|
responses: {
|
||||||
key: "default";
|
response_code: {
|
||||||
|
name: "default";
|
||||||
value: {
|
value: {
|
||||||
description: "Error response";
|
json_reference: {
|
||||||
schema: {
|
description: "Error response";
|
||||||
json_schema: {
|
_ref: "micro.codec.Frame";
|
||||||
ref: "micro.codec.Frame";
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
};
|
};
|
||||||
option (micro.api.http) = { get: "/ready"; };
|
option (micro.api.http) = { get: "/ready"; };
|
||||||
};
|
};
|
||||||
rpc Version(micro.codec.Frame) returns (micro.codec.Frame) {
|
rpc Version(micro.codec.Frame) returns (micro.codec.Frame) {
|
||||||
option (micro.openapiv2.openapiv2_operation) = {
|
option (micro.openapiv2.openapiv2_operation) = {
|
||||||
operation_id: "Version";
|
operation_id: "Version";
|
||||||
responses: {
|
responses: {
|
||||||
key: "default";
|
response_code: {
|
||||||
|
name: "default";
|
||||||
value: {
|
value: {
|
||||||
description: "Error response";
|
json_reference: {
|
||||||
schema: {
|
description: "Error response";
|
||||||
json_schema: {
|
_ref: "micro.codec.Frame";
|
||||||
ref: "micro.codec.Frame";
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
};
|
};
|
||||||
option (micro.api.http) = { get: "/version"; };
|
option (micro.api.http) = { get: "/version"; };
|
||||||
};
|
};
|
||||||
|
@@ -1,34 +1,43 @@
|
|||||||
// Code generated by protoc-gen-micro
|
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
|
||||||
|
// protoc-gen-go-micro version: v3.4.2
|
||||||
// source: health.proto
|
// source: health.proto
|
||||||
|
|
||||||
package health
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
api "github.com/unistack-org/micro/v3/api"
|
api "github.com/unistack-org/micro/v3/api"
|
||||||
codec "github.com/unistack-org/micro/v3/codec"
|
codec "github.com/unistack-org/micro/v3/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHealthEndpoints() []*api.Endpoint {
|
var (
|
||||||
return []*api.Endpoint{
|
HealthName = "Health"
|
||||||
&api.Endpoint{
|
|
||||||
|
HealthEndpoints = []api.Endpoint{
|
||||||
|
{
|
||||||
Name: "Health.Live",
|
Name: "Health.Live",
|
||||||
Path: []string{"/live"},
|
Path: []string{"/live"},
|
||||||
Method: []string{"GET"},
|
Method: []string{"GET"},
|
||||||
Handler: "rpc",
|
Handler: "rpc",
|
||||||
},
|
},
|
||||||
&api.Endpoint{
|
{
|
||||||
Name: "Health.Ready",
|
Name: "Health.Ready",
|
||||||
Path: []string{"/ready"},
|
Path: []string{"/ready"},
|
||||||
Method: []string{"GET"},
|
Method: []string{"GET"},
|
||||||
Handler: "rpc",
|
Handler: "rpc",
|
||||||
},
|
},
|
||||||
&api.Endpoint{
|
{
|
||||||
Name: "Health.Version",
|
Name: "Health.Version",
|
||||||
Path: []string{"/version"},
|
Path: []string{"/version"},
|
||||||
Method: []string{"GET"},
|
Method: []string{"GET"},
|
||||||
Handler: "rpc",
|
Handler: "rpc",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHealthEndpoints() []api.Endpoint {
|
||||||
|
return HealthEndpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthServer interface {
|
type HealthServer interface {
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
// Code generated by protoc-gen-micro
|
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
|
||||||
|
// protoc-gen-go-micro version: v3.4.2
|
||||||
// source: health.proto
|
// source: health.proto
|
||||||
|
|
||||||
package health
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
api "github.com/unistack-org/micro/v3/api"
|
api "github.com/unistack-org/micro/v3/api"
|
||||||
codec "github.com/unistack-org/micro/v3/codec"
|
codec "github.com/unistack-org/micro/v3/codec"
|
||||||
server "github.com/unistack-org/micro/v3/server"
|
server "github.com/unistack-org/micro/v3/server"
|
||||||
@@ -36,8 +39,8 @@ func RegisterHealthServer(s server.Server, sh HealthServer, opts ...server.Handl
|
|||||||
}
|
}
|
||||||
h := &healthServer{sh}
|
h := &healthServer{sh}
|
||||||
var nopts []server.HandlerOption
|
var nopts []server.HandlerOption
|
||||||
for _, endpoint := range NewHealthEndpoints() {
|
for _, endpoint := range HealthEndpoints {
|
||||||
nopts = append(nopts, api.WithEndpoint(endpoint))
|
nopts = append(nopts, api.WithEndpoint(&endpoint))
|
||||||
}
|
}
|
||||||
return s.Handle(s.NewHandler(&Health{h}, append(nopts, opts...)...))
|
return s.Handle(s.NewHandler(&Health{h}, append(nopts, opts...)...))
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// cprotorpc "github.com/unistack-org/micro-codec-protorpc"
|
|
||||||
"github.com/unistack-org/micro/v3/broker"
|
"github.com/unistack-org/micro/v3/broker"
|
||||||
"github.com/unistack-org/micro/v3/codec"
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/register"
|
"github.com/unistack-org/micro/v3/register"
|
||||||
|
maddr "github.com/unistack-org/micro/v3/util/addr"
|
||||||
|
mnet "github.com/unistack-org/micro/v3/util/net"
|
||||||
|
"github.com/unistack-org/micro/v3/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultCodecs will be used to encode/decode
|
// DefaultCodecs will be used to encode/decode
|
||||||
@@ -73,8 +75,7 @@ func (n *noopServer) Subscribe(sb Subscriber) error {
|
|||||||
sub, ok := sb.(*subscriber)
|
sub, ok := sb.(*subscriber)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid subscriber: expected *subscriber")
|
return fmt.Errorf("invalid subscriber: expected *subscriber")
|
||||||
}
|
} else if len(sub.handlers) == 0 {
|
||||||
if len(sub.handlers) == 0 {
|
|
||||||
return fmt.Errorf("invalid subscriber: no handler functions")
|
return fmt.Errorf("invalid subscriber: no handler functions")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +108,12 @@ func (n *noopServer) Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if n.handlers == nil {
|
if n.handlers == nil {
|
||||||
n.handlers = make(map[string]Handler)
|
n.handlers = make(map[string]Handler, 1)
|
||||||
}
|
}
|
||||||
if n.subscribers == nil {
|
if n.subscribers == nil {
|
||||||
n.subscribers = make(map[*subscriber][]broker.Subscriber)
|
n.subscribers = make(map[*subscriber][]broker.Subscriber, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.exit == nil {
|
if n.exit == nil {
|
||||||
n.exit = make(chan chan error)
|
n.exit = make(chan chan error)
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ func (n *noopServer) Register() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// register the service
|
// register the service
|
||||||
if err := DefaultRegisterFunc(service, config); err != nil {
|
if err = DefaultRegisterFunc(service, config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,26 +204,34 @@ func (n *noopServer) Register() error {
|
|||||||
|
|
||||||
cx := config.Context
|
cx := config.Context
|
||||||
|
|
||||||
for sb := range n.subscribers {
|
var sub broker.Subscriber
|
||||||
handler := n.createSubHandler(sb, config)
|
|
||||||
var opts []broker.SubscribeOption
|
|
||||||
if queue := sb.Options().Queue; len(queue) > 0 {
|
|
||||||
opts = append(opts, broker.SubscribeGroup(queue))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for sb := range n.subscribers {
|
||||||
if sb.Options().Context != nil {
|
if sb.Options().Context != nil {
|
||||||
cx = sb.Options().Context
|
cx = sb.Options().Context
|
||||||
}
|
}
|
||||||
|
|
||||||
opts = append(opts, broker.SubscribeContext(cx), broker.SubscribeAutoAck(sb.Options().AutoAck))
|
opts := []broker.SubscribeOption{broker.SubscribeContext(cx), broker.SubscribeAutoAck(sb.Options().AutoAck)}
|
||||||
|
if queue := sb.Options().Queue; len(queue) > 0 {
|
||||||
|
opts = append(opts, broker.SubscribeGroup(queue))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sb.Options().Batch {
|
||||||
|
// batch processing handler
|
||||||
|
sub, err = config.Broker.BatchSubscribe(cx, sb.Topic(), n.newBatchSubHandler(sb, config), opts...)
|
||||||
|
} else {
|
||||||
|
// single processing handler
|
||||||
|
sub, err = config.Broker.Subscribe(cx, sb.Topic(), n.newSubHandler(sb, config), opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if config.Logger.V(logger.InfoLevel) {
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
config.Logger.Infof(n.opts.Context, "subscribing to topic: %s", sb.Topic())
|
config.Logger.Infof(n.opts.Context, "subscribing to topic: %s", sb.Topic())
|
||||||
}
|
}
|
||||||
sub, err := config.Broker.Subscribe(cx, sb.Topic(), handler, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n.subscribers[sb] = []broker.Subscriber{sub}
|
n.subscribers[sb] = []broker.Subscriber{sub}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,9 +313,22 @@ func (n *noopServer) Start() error {
|
|||||||
config := n.Options()
|
config := n.Options()
|
||||||
n.RUnlock()
|
n.RUnlock()
|
||||||
|
|
||||||
|
// use 127.0.0.1 to avoid scan of all network interfaces
|
||||||
|
addr, err := maddr.Extract("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var rng rand.Rand
|
||||||
|
i := rng.Intn(20000)
|
||||||
|
// set addr with port
|
||||||
|
addr = mnet.HostPort(addr, 10000+i)
|
||||||
|
|
||||||
|
config.Address = addr
|
||||||
|
|
||||||
if config.Logger.V(logger.InfoLevel) {
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
config.Logger.Infof(n.opts.Context, "server [noop] Listening on %s", config.Address)
|
config.Logger.Infof(n.opts.Context, "server [noop] Listening on %s", config.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
n.Lock()
|
n.Lock()
|
||||||
if len(config.Advertise) == 0 {
|
if len(config.Advertise) == 0 {
|
||||||
config.Advertise = config.Address
|
config.Advertise = config.Address
|
||||||
|
102
server/noop_test.go
Normal file
102
server/noop_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package server_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/broker"
|
||||||
|
"github.com/unistack-org/micro/v3/client"
|
||||||
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
|
"github.com/unistack-org/micro/v3/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestHandler struct {
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestMessage struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TestHandler) SingleSubHandler(ctx context.Context, msg *codec.Frame) error {
|
||||||
|
// fmt.Printf("msg %s\n", msg.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TestHandler) BatchSubHandler(ctxs []context.Context, msgs []*codec.Frame) error {
|
||||||
|
if len(msgs) != 8 {
|
||||||
|
h.t.Fatal("invalid number of messages received")
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(msgs); idx++ {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctxs[idx])
|
||||||
|
_ = md
|
||||||
|
// fmt.Printf("msg md %v\n", md)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoopSub(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
b := broker.NewBroker()
|
||||||
|
|
||||||
|
if err := b.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := server.NewServer(
|
||||||
|
server.Broker(b),
|
||||||
|
server.Codec("application/octet-stream", codec.NewCodec()),
|
||||||
|
)
|
||||||
|
if err := s.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := client.NewClient(
|
||||||
|
client.Broker(b),
|
||||||
|
client.Codec("application/octet-stream", codec.NewCodec()),
|
||||||
|
client.ContentType("application/octet-stream"),
|
||||||
|
)
|
||||||
|
if err := c.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := &TestHandler{t: t}
|
||||||
|
|
||||||
|
if err := s.Subscribe(s.NewSubscriber("single_topic", h.SingleSubHandler,
|
||||||
|
server.SubscriberQueue("queue"),
|
||||||
|
)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Subscribe(s.NewSubscriber("batch_topic", h.BatchSubHandler,
|
||||||
|
server.SubscriberQueue("queue"),
|
||||||
|
server.SubscriberBatch(true),
|
||||||
|
)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]client.Message, 0, 8)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
msgs = append(msgs, c.NewMessage("batch_topic", &codec.Frame{Data: []byte(fmt.Sprintf(`{"name": "test_name %d"}`, i))}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := s.Stop(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
@@ -71,6 +71,8 @@ type Options struct {
|
|||||||
Version string
|
Version string
|
||||||
// SubWrappers holds the server subscribe wrappers
|
// SubWrappers holds the server subscribe wrappers
|
||||||
SubWrappers []SubscriberWrapper
|
SubWrappers []SubscriberWrapper
|
||||||
|
// BatchSubWrappers holds the server batch subscribe wrappers
|
||||||
|
BatchSubWrappers []BatchSubscriberWrapper
|
||||||
// HdlrWrappers holds the handler wrappers
|
// HdlrWrappers holds the handler wrappers
|
||||||
HdlrWrappers []HandlerWrapper
|
HdlrWrappers []HandlerWrapper
|
||||||
// RegisterAttempts holds the number of register attempts before error
|
// RegisterAttempts holds the number of register attempts before error
|
||||||
@@ -260,7 +262,7 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
// already set. Required for Init call below.
|
// already set. Required for Init call below.
|
||||||
|
|
||||||
// set the transport tls
|
// set the transport tls
|
||||||
o.Transport.Init(
|
_ = o.Transport.Init(
|
||||||
transport.TLSConfig(t),
|
transport.TLSConfig(t),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -302,6 +304,13 @@ func WrapSubscriber(w SubscriberWrapper) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapBatchSubscriber adds a batch subscriber Wrapper to a list of options passed into the server
|
||||||
|
func WrapBatchSubscriber(w BatchSubscriberWrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BatchSubWrappers = append(o.BatchSubWrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MaxConn specifies maximum number of max simultaneous connections to server
|
// MaxConn specifies maximum number of max simultaneous connections to server
|
||||||
func MaxConn(n int) Option {
|
func MaxConn(n int) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -354,6 +363,12 @@ type SubscriberOptions struct {
|
|||||||
AutoAck bool
|
AutoAck bool
|
||||||
// BodyOnly flag specifies that message without headers
|
// BodyOnly flag specifies that message without headers
|
||||||
BodyOnly bool
|
BodyOnly bool
|
||||||
|
// Batch flag specifies that message processed in batches
|
||||||
|
Batch bool
|
||||||
|
// BatchSize flag specifies max size of batch
|
||||||
|
BatchSize int
|
||||||
|
// BatchWait flag specifies max wait time for batch filling
|
||||||
|
BatchWait time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSubscriberOptions create new SubscriberOptions
|
// NewSubscriberOptions create new SubscriberOptions
|
||||||
@@ -413,3 +428,32 @@ func SubscriberContext(ctx context.Context) SubscriberOption {
|
|||||||
o.Context = ctx
|
o.Context = ctx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscriberAck control auto ack processing for handler
|
||||||
|
func SubscriberAck(b bool) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.AutoAck = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatch control batch processing for handler
|
||||||
|
func SubscriberBatch(b bool) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.Batch = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatchSize control batch filling size for handler
|
||||||
|
// Batch filling max waiting time controlled by SubscriberBatchWait
|
||||||
|
func SubscriberBatchSize(n int) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.BatchSize = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatchWait control batch filling wait time for handler
|
||||||
|
func SubscriberBatchWait(td time.Duration) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.BatchWait = td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -5,24 +5,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/codec"
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
"github.com/unistack-org/micro/v3/metadata"
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
"github.com/unistack-org/micro/v3/register"
|
"github.com/unistack-org/micro/v3/register"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultServer default server
|
// DefaultServer default server
|
||||||
var DefaultServer Server = NewServer()
|
var DefaultServer Server = NewServer()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultAddress will be used if no address passed
|
// DefaultAddress will be used if no address passed, use secure localhost
|
||||||
DefaultAddress = ":0"
|
DefaultAddress = "127.0.0.1:0"
|
||||||
// DefaultName will be used if no name passed
|
// DefaultName will be used if no name passed
|
||||||
DefaultName = "server"
|
DefaultName = "server"
|
||||||
// DefaultVersion will be used if no version passed
|
// DefaultVersion will be used if no version passed
|
||||||
DefaultVersion = "latest"
|
DefaultVersion = "latest"
|
||||||
// DefaultID will be used if no id passed
|
// DefaultID will be used if no id passed
|
||||||
DefaultID = uuid.New().String()
|
DefaultID = id.Must()
|
||||||
// DefaultRegisterCheck holds func that run before register server
|
// DefaultRegisterCheck holds func that run before register server
|
||||||
DefaultRegisterCheck = func(context.Context) error { return nil }
|
DefaultRegisterCheck = func(context.Context) error { return nil }
|
||||||
// DefaultRegisterInterval holds interval for register
|
// DefaultRegisterInterval holds interval for register
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/broker"
|
"github.com/unistack-org/micro/v3/broker"
|
||||||
|
"github.com/unistack-org/micro/v3/codec"
|
||||||
"github.com/unistack-org/micro/v3/errors"
|
"github.com/unistack-org/micro/v3/errors"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
"github.com/unistack-org/micro/v3/metadata"
|
"github.com/unistack-org/micro/v3/metadata"
|
||||||
@@ -18,7 +19,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
subSig = "func(context.Context, interface{}) error"
|
subSig = "func(context.Context, interface{}) error"
|
||||||
|
batchSubSig = "func([]context.Context, []interface{}) error"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Precompute the reflect type for error. Can't use error directly
|
// Precompute the reflect type for error. Can't use error directly
|
||||||
@@ -32,13 +34,13 @@ type handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type subscriber struct {
|
type subscriber struct {
|
||||||
opts SubscriberOptions
|
|
||||||
typ reflect.Type
|
typ reflect.Type
|
||||||
subscriber interface{}
|
subscriber interface{}
|
||||||
rcvr reflect.Value
|
rcvr reflect.Value
|
||||||
topic string
|
topic string
|
||||||
handlers []*handler
|
|
||||||
endpoints []*register.Endpoint
|
endpoints []*register.Endpoint
|
||||||
|
handlers []*handler
|
||||||
|
opts SubscriberOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this an exported - upper case - name?
|
// Is this an exported - upper case - name?
|
||||||
@@ -57,26 +59,33 @@ func isExportedOrBuiltinType(t reflect.Type) bool {
|
|||||||
return isExported(t.Name()) || t.PkgPath() == ""
|
return isExported(t.Name()) || t.PkgPath() == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateSubscriber func
|
// ValidateSubscriber func signature
|
||||||
func ValidateSubscriber(sub Subscriber) error {
|
func ValidateSubscriber(sub Subscriber) error {
|
||||||
typ := reflect.TypeOf(sub.Subscriber())
|
typ := reflect.TypeOf(sub.Subscriber())
|
||||||
var argType reflect.Type
|
var argType reflect.Type
|
||||||
|
|
||||||
switch typ.Kind() {
|
switch typ.Kind() {
|
||||||
case reflect.Func:
|
case reflect.Func:
|
||||||
name := "Func"
|
name := "Func"
|
||||||
switch typ.NumIn() {
|
switch typ.NumIn() {
|
||||||
case 2:
|
case 2:
|
||||||
argType = typ.In(1)
|
argType = typ.In(1)
|
||||||
|
if sub.Options().Batch {
|
||||||
|
if argType.Kind() != reflect.Slice {
|
||||||
|
return fmt.Errorf("subscriber %v dont have required signature %s", name, batchSubSig)
|
||||||
|
}
|
||||||
|
if strings.Compare(fmt.Sprintf("%v", argType), "[]interface{}") == 0 {
|
||||||
|
return fmt.Errorf("subscriber %v dont have required signaure %s", name, batchSubSig)
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig)
|
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s or %s", name, typ.NumIn(), subSig, batchSubSig)
|
||||||
}
|
}
|
||||||
if !isExportedOrBuiltinType(argType) {
|
if !isExportedOrBuiltinType(argType) {
|
||||||
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
||||||
}
|
}
|
||||||
if typ.NumOut() != 1 {
|
if typ.NumOut() != 1 {
|
||||||
return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s",
|
return fmt.Errorf("subscriber %v has wrong number of return values: %v require signature %s or %s",
|
||||||
name, typ.NumOut(), subSig)
|
name, typ.NumOut(), subSig, batchSubSig)
|
||||||
}
|
}
|
||||||
if returnType := typ.Out(0); returnType != typeOfError {
|
if returnType := typ.Out(0); returnType != typeOfError {
|
||||||
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
||||||
@@ -87,13 +96,12 @@ func ValidateSubscriber(sub Subscriber) error {
|
|||||||
|
|
||||||
for m := 0; m < typ.NumMethod(); m++ {
|
for m := 0; m < typ.NumMethod(); m++ {
|
||||||
method := typ.Method(m)
|
method := typ.Method(m)
|
||||||
|
|
||||||
switch method.Type.NumIn() {
|
switch method.Type.NumIn() {
|
||||||
case 3:
|
case 3:
|
||||||
argType = method.Type.In(2)
|
argType = method.Type.In(2)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s",
|
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s or %s",
|
||||||
name, method.Name, method.Type.NumIn(), subSig)
|
name, method.Name, method.Type.NumIn(), subSig, batchSubSig)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isExportedOrBuiltinType(argType) {
|
if !isExportedOrBuiltinType(argType) {
|
||||||
@@ -101,8 +109,8 @@ func ValidateSubscriber(sub Subscriber) error {
|
|||||||
}
|
}
|
||||||
if method.Type.NumOut() != 1 {
|
if method.Type.NumOut() != 1 {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"subscriber %v.%v has wrong number of outs: %v require signature %s",
|
"subscriber %v.%v has wrong number of return values: %v require signature %s or %s",
|
||||||
name, method.Name, method.Type.NumOut(), subSig)
|
name, method.Name, method.Type.NumOut(), subSig, batchSubSig)
|
||||||
}
|
}
|
||||||
if returnType := method.Type.Out(0); returnType != typeOfError {
|
if returnType := method.Type.Out(0); returnType != typeOfError {
|
||||||
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
||||||
@@ -183,7 +191,125 @@ func newSubscriber(topic string, sub interface{}, opts ...SubscriberOption) Subs
|
|||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handler {
|
func (n *noopServer) newBatchSubHandler(sb *subscriber, opts Options) broker.BatchHandler {
|
||||||
|
return func(ps broker.Events) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
n.RLock()
|
||||||
|
config := n.opts
|
||||||
|
n.RUnlock()
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Error(n.opts.Context, "panic recovered: ", r)
|
||||||
|
config.Logger.Error(n.opts.Context, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
err = errors.InternalServerError(n.opts.Name+".subscriber", "panic recovered: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
msgs := make([]Message, 0, len(ps))
|
||||||
|
ctxs := make([]context.Context, 0, len(ps))
|
||||||
|
for _, p := range ps {
|
||||||
|
msg := p.Message()
|
||||||
|
// if we don't have headers, create empty map
|
||||||
|
if msg.Header == nil {
|
||||||
|
msg.Header = metadata.New(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct, _ := msg.Header.Get(metadata.HeaderContentType)
|
||||||
|
if len(ct) == 0 {
|
||||||
|
msg.Header.Set(metadata.HeaderContentType, defaultContentType)
|
||||||
|
ct = defaultContentType
|
||||||
|
}
|
||||||
|
hdr := metadata.Copy(msg.Header)
|
||||||
|
topic, _ := msg.Header.Get(metadata.HeaderTopic)
|
||||||
|
ctxs = append(ctxs, metadata.NewIncomingContext(sb.opts.Context, hdr))
|
||||||
|
msgs = append(msgs, &rpcMessage{
|
||||||
|
topic: topic,
|
||||||
|
contentType: ct,
|
||||||
|
header: msg.Header,
|
||||||
|
body: msg.Body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
results := make(chan error, len(sb.handlers))
|
||||||
|
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
handler := sb.handlers[i]
|
||||||
|
|
||||||
|
var req reflect.Value
|
||||||
|
|
||||||
|
switch handler.reqType.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
req = reflect.New(handler.reqType.Elem())
|
||||||
|
default:
|
||||||
|
req = reflect.New(handler.reqType.Elem()).Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
reqType := handler.reqType
|
||||||
|
var cf codec.Codec
|
||||||
|
for _, msg := range msgs {
|
||||||
|
cf, err = n.newCodec(msg.ContentType())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rb := reflect.New(req.Type().Elem())
|
||||||
|
if err = cf.ReadBody(bytes.NewReader(msg.Body()), rb.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg.(*rpcMessage).codec = cf
|
||||||
|
msg.(*rpcMessage).payload = rb.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := func(ctxs []context.Context, ms []Message) error {
|
||||||
|
var vals []reflect.Value
|
||||||
|
if sb.typ.Kind() != reflect.Func {
|
||||||
|
vals = append(vals, sb.rcvr)
|
||||||
|
}
|
||||||
|
if handler.ctxType != nil {
|
||||||
|
vals = append(vals, reflect.ValueOf(ctxs))
|
||||||
|
}
|
||||||
|
payloads := reflect.MakeSlice(reqType, 0, len(ms))
|
||||||
|
for _, m := range ms {
|
||||||
|
payloads = reflect.Append(payloads, reflect.ValueOf(m.Payload()))
|
||||||
|
}
|
||||||
|
vals = append(vals, payloads)
|
||||||
|
|
||||||
|
returnValues := handler.method.Call(vals)
|
||||||
|
if rerr := returnValues[0].Interface(); rerr != nil {
|
||||||
|
return rerr.(error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(opts.BatchSubWrappers); i > 0; i-- {
|
||||||
|
fn = opts.BatchSubWrappers[i-1](fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.wg != nil {
|
||||||
|
n.wg.Add(1)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if n.wg != nil {
|
||||||
|
defer n.wg.Done()
|
||||||
|
}
|
||||||
|
results <- fn(ctxs, msgs)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
if rerr := <-results; rerr != nil {
|
||||||
|
errors = append(errors, rerr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func (n *noopServer) newSubHandler(sb *subscriber, opts Options) broker.Handler {
|
||||||
return func(p broker.Event) (err error) {
|
return func(p broker.Event) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -201,12 +327,12 @@ func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handl
|
|||||||
msg := p.Message()
|
msg := p.Message()
|
||||||
// if we don't have headers, create empty map
|
// if we don't have headers, create empty map
|
||||||
if msg.Header == nil {
|
if msg.Header == nil {
|
||||||
msg.Header = make(map[string]string)
|
msg.Header = metadata.New(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
ct := msg.Header["Content-Type"]
|
ct := msg.Header["Content-Type"]
|
||||||
if len(ct) == 0 {
|
if len(ct) == 0 {
|
||||||
msg.Header["Content-Type"] = defaultContentType
|
msg.Header.Set(metadata.HeaderContentType, defaultContentType)
|
||||||
ct = defaultContentType
|
ct = defaultContentType
|
||||||
}
|
}
|
||||||
cf, err := n.newCodec(ct)
|
cf, err := n.newCodec(ct)
|
||||||
@@ -214,12 +340,12 @@ func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handl
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdr := make(map[string]string, len(msg.Header))
|
hdr := metadata.New(len(msg.Header))
|
||||||
for k, v := range msg.Header {
|
for k, v := range msg.Header {
|
||||||
if k == "Content-Type" {
|
if k == "Content-Type" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hdr[k] = v
|
hdr.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := metadata.NewIncomingContext(sb.opts.Context, hdr)
|
ctx := metadata.NewIncomingContext(sb.opts.Context, hdr)
|
||||||
@@ -294,7 +420,6 @@ func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handl
|
|||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,20 @@ type HandlerFunc func(ctx context.Context, req Request, rsp interface{}) error
|
|||||||
// publication message.
|
// publication message.
|
||||||
type SubscriberFunc func(ctx context.Context, msg Message) error
|
type SubscriberFunc func(ctx context.Context, msg Message) error
|
||||||
|
|
||||||
|
// BatchSubscriberFunc represents a single method of a subscriber. It's used primarily
|
||||||
|
// for the wrappers. What's handed to the actual method is the concrete
|
||||||
|
// publication message. This func used by batch subscribers
|
||||||
|
type BatchSubscriberFunc func(ctxs []context.Context, msgs []Message) error
|
||||||
|
|
||||||
// HandlerWrapper wraps the HandlerFunc and returns the equivalent
|
// HandlerWrapper wraps the HandlerFunc and returns the equivalent
|
||||||
type HandlerWrapper func(HandlerFunc) HandlerFunc
|
type HandlerWrapper func(HandlerFunc) HandlerFunc
|
||||||
|
|
||||||
// SubscriberWrapper wraps the SubscriberFunc and returns the equivalent
|
// SubscriberWrapper wraps the SubscriberFunc and returns the equivalent
|
||||||
type SubscriberWrapper func(SubscriberFunc) SubscriberFunc
|
type SubscriberWrapper func(SubscriberFunc) SubscriberFunc
|
||||||
|
|
||||||
|
// BatchSubscriberWrapper wraps the SubscriberFunc and returns the equivalent
|
||||||
|
type BatchSubscriberWrapper func(BatchSubscriberFunc) BatchSubscriberFunc
|
||||||
|
|
||||||
// StreamWrapper wraps a Stream interface and returns the equivalent.
|
// StreamWrapper wraps a Stream interface and returns the equivalent.
|
||||||
// Because streams exist for the lifetime of a method invocation this
|
// Because streams exist for the lifetime of a method invocation this
|
||||||
// is a convenient way to wrap a Stream as its in use for trace, monitoring,
|
// is a convenient way to wrap a Stream as its in use for trace, monitoring,
|
||||||
|
@@ -36,16 +36,6 @@ func (m *memoryStore) key(prefix, key string) string {
|
|||||||
return filepath.Join(prefix, key)
|
return filepath.Join(prefix, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) prefix(database, table string) string {
|
|
||||||
if len(database) == 0 {
|
|
||||||
database = m.opts.Database
|
|
||||||
}
|
|
||||||
if len(table) == 0 {
|
|
||||||
table = m.opts.Table
|
|
||||||
}
|
|
||||||
return filepath.Join(database, table)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryStore) exists(prefix, key string) error {
|
func (m *memoryStore) exists(prefix, key string) error {
|
||||||
key = m.key(prefix, key)
|
key = m.key(prefix, key)
|
||||||
|
|
||||||
@@ -80,15 +70,17 @@ func (m *memoryStore) delete(prefix, key string) {
|
|||||||
|
|
||||||
func (m *memoryStore) list(prefix string, limit, offset uint) []string {
|
func (m *memoryStore) list(prefix string, limit, offset uint) []string {
|
||||||
allItems := m.store.Items()
|
allItems := m.store.Items()
|
||||||
allKeys := make([]string, len(allItems))
|
allKeys := make([]string, 0, len(allItems))
|
||||||
i := 0
|
|
||||||
|
|
||||||
for k := range allItems {
|
for k := range allItems {
|
||||||
if !strings.HasPrefix(k, prefix+"/") {
|
if !strings.HasPrefix(k, prefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allKeys[i] = strings.TrimPrefix(k, prefix+"/")
|
k = strings.TrimPrefix(k, prefix)
|
||||||
i++
|
if k[0] == '/' {
|
||||||
|
k = k[1:]
|
||||||
|
}
|
||||||
|
allKeys = append(allKeys, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit != 0 || offset != 0 {
|
if limit != 0 || offset != 0 {
|
||||||
@@ -107,7 +99,6 @@ func (m *memoryStore) list(prefix string, limit, offset uint) []string {
|
|||||||
}
|
}
|
||||||
return allKeys[offset:end]
|
return allKeys[offset:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
return allKeys
|
return allKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,37 +118,48 @@ func (m *memoryStore) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) Exists(ctx context.Context, key string, opts ...ExistsOption) error {
|
func (m *memoryStore) Exists(ctx context.Context, key string, opts ...ExistsOption) error {
|
||||||
prefix := m.prefix(m.opts.Database, m.opts.Table)
|
options := NewExistsOptions(opts...)
|
||||||
return m.exists(prefix, key)
|
if options.Namespace == "" {
|
||||||
|
options.Namespace = m.opts.Namespace
|
||||||
|
}
|
||||||
|
return m.exists(options.Namespace, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) Read(ctx context.Context, key string, val interface{}, opts ...ReadOption) error {
|
func (m *memoryStore) Read(ctx context.Context, key string, val interface{}, opts ...ReadOption) error {
|
||||||
readOpts := NewReadOptions(opts...)
|
options := NewReadOptions(opts...)
|
||||||
prefix := m.prefix(readOpts.Database, readOpts.Table)
|
if options.Namespace == "" {
|
||||||
return m.get(prefix, key, val)
|
options.Namespace = m.opts.Namespace
|
||||||
|
}
|
||||||
|
return m.get(options.Namespace, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) Write(ctx context.Context, key string, val interface{}, opts ...WriteOption) error {
|
func (m *memoryStore) Write(ctx context.Context, key string, val interface{}, opts ...WriteOption) error {
|
||||||
writeOpts := NewWriteOptions(opts...)
|
options := NewWriteOptions(opts...)
|
||||||
|
if options.Namespace == "" {
|
||||||
|
options.Namespace = m.opts.Namespace
|
||||||
|
}
|
||||||
|
if options.TTL == 0 {
|
||||||
|
options.TTL = cache.NoExpiration
|
||||||
|
}
|
||||||
|
|
||||||
prefix := m.prefix(writeOpts.Database, writeOpts.Table)
|
key = m.key(options.Namespace, key)
|
||||||
|
|
||||||
key = m.key(prefix, key)
|
|
||||||
|
|
||||||
buf, err := m.opts.Codec.Marshal(val)
|
buf, err := m.opts.Codec.Marshal(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.store.Set(key, buf, writeOpts.TTL)
|
m.store.Set(key, buf, options.TTL)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) Delete(ctx context.Context, key string, opts ...DeleteOption) error {
|
func (m *memoryStore) Delete(ctx context.Context, key string, opts ...DeleteOption) error {
|
||||||
deleteOptions := NewDeleteOptions(opts...)
|
options := NewDeleteOptions(opts...)
|
||||||
|
if options.Namespace == "" {
|
||||||
|
options.Namespace = m.opts.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
prefix := m.prefix(deleteOptions.Database, deleteOptions.Table)
|
m.delete(options.Namespace, key)
|
||||||
m.delete(prefix, key)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,25 +168,27 @@ func (m *memoryStore) Options() Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryStore) List(ctx context.Context, opts ...ListOption) ([]string, error) {
|
func (m *memoryStore) List(ctx context.Context, opts ...ListOption) ([]string, error) {
|
||||||
listOptions := NewListOptions(opts...)
|
options := NewListOptions(opts...)
|
||||||
|
if options.Namespace == "" {
|
||||||
|
options.Namespace = m.opts.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
prefix := m.prefix(listOptions.Database, listOptions.Table)
|
keys := m.list(options.Namespace, options.Limit, options.Offset)
|
||||||
keys := m.list(prefix, listOptions.Limit, listOptions.Offset)
|
|
||||||
|
|
||||||
if len(listOptions.Prefix) > 0 {
|
if len(options.Prefix) > 0 {
|
||||||
var prefixKeys []string
|
var prefixKeys []string
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if strings.HasPrefix(k, listOptions.Prefix) {
|
if strings.HasPrefix(k, options.Prefix) {
|
||||||
prefixKeys = append(prefixKeys, k)
|
prefixKeys = append(prefixKeys, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys = prefixKeys
|
keys = prefixKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(listOptions.Suffix) > 0 {
|
if len(options.Suffix) > 0 {
|
||||||
var suffixKeys []string
|
var suffixKeys []string
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if strings.HasSuffix(k, listOptions.Suffix) {
|
if strings.HasSuffix(k, options.Suffix) {
|
||||||
suffixKeys = append(suffixKeys, k)
|
suffixKeys = append(suffixKeys, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,11 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMemoryReInit(t *testing.T) {
|
func TestMemoryReInit(t *testing.T) {
|
||||||
s := store.NewStore(store.Table("aaa"))
|
s := store.NewStore(store.Namespace("aaa"))
|
||||||
if err := s.Init(store.Table("")); err != nil {
|
if err := s.Init(store.Namespace("")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(s.Options().Table) > 0 {
|
if len(s.Options().Namespace) > 0 {
|
||||||
t.Error("Init didn't reinitialise the store")
|
t.Error("Init didn't reinitialise the store")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ func TestMemoryBasic(t *testing.T) {
|
|||||||
|
|
||||||
func TestMemoryPrefix(t *testing.T) {
|
func TestMemoryPrefix(t *testing.T) {
|
||||||
s := store.NewStore()
|
s := store.NewStore()
|
||||||
if err := s.Init(store.Table("some-prefix")); err != nil {
|
if err := s.Init(store.Namespace("some-prefix")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
basictest(s, t)
|
basictest(s, t)
|
||||||
@@ -36,7 +36,7 @@ func TestMemoryPrefix(t *testing.T) {
|
|||||||
|
|
||||||
func TestMemoryNamespace(t *testing.T) {
|
func TestMemoryNamespace(t *testing.T) {
|
||||||
s := store.NewStore()
|
s := store.NewStore()
|
||||||
if err := s.Init(store.Database("some-namespace")); err != nil {
|
if err := s.Init(store.Namespace("some-namespace")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
basictest(s, t)
|
basictest(s, t)
|
||||||
@@ -44,7 +44,7 @@ func TestMemoryNamespace(t *testing.T) {
|
|||||||
|
|
||||||
func TestMemoryNamespacePrefix(t *testing.T) {
|
func TestMemoryNamespacePrefix(t *testing.T) {
|
||||||
s := store.NewStore()
|
s := store.NewStore()
|
||||||
if err := s.Init(store.Table("some-prefix"), store.Database("some-namespace")); err != nil {
|
if err := s.Init(store.Namespace("some-namespace")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
basictest(s, t)
|
basictest(s, t)
|
||||||
|
263
store/options.go
263
store/options.go
@@ -28,13 +28,12 @@ type Options struct {
|
|||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
// Name specifies store name
|
// Name specifies store name
|
||||||
Name string
|
Name string
|
||||||
// Database specifies store database
|
// Namespace of the records
|
||||||
Database string
|
Namespace string
|
||||||
// Table specifies store table
|
// Addrs contains store address
|
||||||
Table string
|
Addrs []string
|
||||||
// Nodes contains store address
|
// Wrappers store wrapper that called before actual functions
|
||||||
// TODO: replace with Addrs
|
// Wrappers []Wrapper
|
||||||
Nodes []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions creates options struct
|
// NewOptions creates options struct
|
||||||
@@ -90,13 +89,20 @@ func Meter(m meter.Meter) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name the name
|
// Name the name of the store
|
||||||
func Name(n string) Option {
|
func Name(n string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Name = n
|
o.Name = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Namespace sets namespace of the store
|
||||||
|
func Namespace(ns string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tracer sets the tracer
|
// Tracer sets the tracer
|
||||||
func Tracer(t tracer.Tracer) Option {
|
func Tracer(t tracer.Tracer) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -104,27 +110,21 @@ func Tracer(t tracer.Tracer) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nodes contains the addresses or other connection information of the backing storage.
|
// Addrs contains the addresses or other connection information of the backing storage.
|
||||||
// For example, an etcd implementation would contain the nodes of the cluster.
|
// For example, an etcd implementation would contain the nodes of the cluster.
|
||||||
// A SQL implementation could contain one or more connection strings.
|
// A SQL implementation could contain one or more connection strings.
|
||||||
func Nodes(a ...string) Option {
|
func Addrs(addrs ...string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Nodes = a
|
o.Addrs = addrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database allows multiple isolated stores to be kept in one backend, if supported.
|
// ReadOptions configures an individual Read operation
|
||||||
func Database(db string) Option {
|
type ReadOptions struct {
|
||||||
return func(o *Options) {
|
// Context holds external options
|
||||||
o.Database = db
|
Context context.Context
|
||||||
}
|
// Namespace holds namespace
|
||||||
}
|
Namespace string
|
||||||
|
|
||||||
// Table is analag for a table in database backends or a key prefix in KV backends
|
|
||||||
func Table(t string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Table = t
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReadOptions fills ReadOptions struct with opts slice
|
// NewReadOptions fills ReadOptions struct with opts slice
|
||||||
@@ -136,29 +136,35 @@ func NewReadOptions(opts ...ReadOption) ReadOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadOptions configures an individual Read operation
|
|
||||||
type ReadOptions struct {
|
|
||||||
// Context holds external options
|
|
||||||
Context context.Context
|
|
||||||
// Database holds the database name
|
|
||||||
Database string
|
|
||||||
// Table holds table name
|
|
||||||
Table string
|
|
||||||
// Namespace holds namespace
|
|
||||||
Namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadOption sets values in ReadOptions
|
// ReadOption sets values in ReadOptions
|
||||||
type ReadOption func(r *ReadOptions)
|
type ReadOption func(r *ReadOptions)
|
||||||
|
|
||||||
// ReadFrom the database and table
|
// ReadContext pass context.Context to ReadOptions
|
||||||
func ReadFrom(database, table string) ReadOption {
|
func ReadContext(ctx context.Context) ReadOption {
|
||||||
return func(r *ReadOptions) {
|
return func(o *ReadOptions) {
|
||||||
r.Database = database
|
o.Context = ctx
|
||||||
r.Table = table
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadNamespace pass namespace to ReadOptions
|
||||||
|
func ReadNamespace(ns string) ReadOption {
|
||||||
|
return func(o *ReadOptions) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteOptions configures an individual Write operation
|
||||||
|
type WriteOptions struct {
|
||||||
|
// Context holds external options
|
||||||
|
Context context.Context
|
||||||
|
// Metadata contains additional metadata
|
||||||
|
Metadata metadata.Metadata
|
||||||
|
// Namespace holds namespace
|
||||||
|
Namespace string
|
||||||
|
// TTL specifies key TTL
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// NewWriteOptions fills WriteOptions struct with opts slice
|
// NewWriteOptions fills WriteOptions struct with opts slice
|
||||||
func NewWriteOptions(opts ...WriteOption) WriteOptions {
|
func NewWriteOptions(opts ...WriteOption) WriteOptions {
|
||||||
options := WriteOptions{}
|
options := WriteOptions{}
|
||||||
@@ -168,47 +174,45 @@ func NewWriteOptions(opts ...WriteOption) WriteOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteOptions configures an individual Write operation
|
|
||||||
type WriteOptions struct {
|
|
||||||
// Context holds external options
|
|
||||||
Context context.Context
|
|
||||||
// Metadata contains additional metadata
|
|
||||||
Metadata metadata.Metadata
|
|
||||||
// Database holds database name
|
|
||||||
Database string
|
|
||||||
// Table holds table name
|
|
||||||
Table string
|
|
||||||
// Namespace holds namespace
|
|
||||||
Namespace string
|
|
||||||
// TTL specifies key TTL
|
|
||||||
TTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteOption sets values in WriteOptions
|
// WriteOption sets values in WriteOptions
|
||||||
type WriteOption func(w *WriteOptions)
|
type WriteOption func(w *WriteOptions)
|
||||||
|
|
||||||
// WriteTo the database and table
|
// WriteContext pass context.Context to wirte options
|
||||||
func WriteTo(database, table string) WriteOption {
|
func WriteContext(ctx context.Context) WriteOption {
|
||||||
return func(w *WriteOptions) {
|
return func(o *WriteOptions) {
|
||||||
w.Database = database
|
o.Context = ctx
|
||||||
w.Table = table
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTTL is the time the record expires
|
|
||||||
func WriteTTL(d time.Duration) WriteOption {
|
|
||||||
return func(w *WriteOptions) {
|
|
||||||
w.TTL = d
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteMetadata add metadata.Metadata
|
// WriteMetadata add metadata.Metadata
|
||||||
func WriteMetadata(md metadata.Metadata) WriteOption {
|
func WriteMetadata(md metadata.Metadata) WriteOption {
|
||||||
return func(w *WriteOptions) {
|
return func(o *WriteOptions) {
|
||||||
w.Metadata = metadata.Copy(md)
|
o.Metadata = metadata.Copy(md)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteTTL is the time the record expires
|
||||||
|
func WriteTTL(d time.Duration) WriteOption {
|
||||||
|
return func(o *WriteOptions) {
|
||||||
|
o.TTL = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteNamespace pass namespace to write options
|
||||||
|
func WriteNamespace(ns string) WriteOption {
|
||||||
|
return func(o *WriteOptions) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOptions configures an individual Delete operation
|
||||||
|
type DeleteOptions struct {
|
||||||
|
// Context holds external options
|
||||||
|
Context context.Context
|
||||||
|
// Namespace holds namespace
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
// NewDeleteOptions fills DeleteOptions struct with opts slice
|
// NewDeleteOptions fills DeleteOptions struct with opts slice
|
||||||
func NewDeleteOptions(opts ...DeleteOption) DeleteOptions {
|
func NewDeleteOptions(opts ...DeleteOption) DeleteOptions {
|
||||||
options := DeleteOptions{}
|
options := DeleteOptions{}
|
||||||
@@ -218,29 +222,33 @@ func NewDeleteOptions(opts ...DeleteOption) DeleteOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteOptions configures an individual Delete operation
|
|
||||||
type DeleteOptions struct {
|
|
||||||
// Context holds external options
|
|
||||||
Context context.Context
|
|
||||||
// Database holds database name
|
|
||||||
Database string
|
|
||||||
// Table holds table name
|
|
||||||
Table string
|
|
||||||
// Namespace holds namespace
|
|
||||||
Namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOption sets values in DeleteOptions
|
// DeleteOption sets values in DeleteOptions
|
||||||
type DeleteOption func(d *DeleteOptions)
|
type DeleteOption func(d *DeleteOptions)
|
||||||
|
|
||||||
// DeleteFrom the database and table
|
// DeleteContext pass context.Context to delete options
|
||||||
func DeleteFrom(database, table string) DeleteOption {
|
func DeleteContext(ctx context.Context) DeleteOption {
|
||||||
return func(d *DeleteOptions) {
|
return func(o *DeleteOptions) {
|
||||||
d.Database = database
|
o.Context = ctx
|
||||||
d.Table = table
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteNamespace pass namespace to delete options
|
||||||
|
func DeleteNamespace(ns string) DeleteOption {
|
||||||
|
return func(o *DeleteOptions) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOptions configures an individual List operation
|
||||||
|
type ListOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
Prefix string
|
||||||
|
Suffix string
|
||||||
|
Namespace string
|
||||||
|
Limit uint
|
||||||
|
Offset uint
|
||||||
|
}
|
||||||
|
|
||||||
// NewListOptions fills ListOptions struct with opts slice
|
// NewListOptions fills ListOptions struct with opts slice
|
||||||
func NewListOptions(opts ...ListOption) ListOptions {
|
func NewListOptions(opts ...ListOption) ListOptions {
|
||||||
options := ListOptions{}
|
options := ListOptions{}
|
||||||
@@ -250,59 +258,50 @@ func NewListOptions(opts ...ListOption) ListOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListOptions configures an individual List operation
|
|
||||||
type ListOptions struct {
|
|
||||||
Context context.Context
|
|
||||||
Database string
|
|
||||||
Prefix string
|
|
||||||
Suffix string
|
|
||||||
Namespace string
|
|
||||||
Table string
|
|
||||||
Limit uint
|
|
||||||
Offset uint
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListOption sets values in ListOptions
|
// ListOption sets values in ListOptions
|
||||||
type ListOption func(l *ListOptions)
|
type ListOption func(l *ListOptions)
|
||||||
|
|
||||||
// ListFrom the database and table
|
// ListContext pass context.Context to list options
|
||||||
func ListFrom(database, table string) ListOption {
|
func ListContext(ctx context.Context) ListOption {
|
||||||
return func(l *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
l.Database = database
|
o.Context = ctx
|
||||||
l.Table = table
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPrefix returns all keys that are prefixed with key
|
// ListPrefix returns all keys that are prefixed with key
|
||||||
func ListPrefix(p string) ListOption {
|
func ListPrefix(s string) ListOption {
|
||||||
return func(l *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
l.Prefix = p
|
o.Prefix = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSuffix returns all keys that end with key
|
// ListSuffix returns all keys that end with key
|
||||||
func ListSuffix(s string) ListOption {
|
func ListSuffix(s string) ListOption {
|
||||||
return func(l *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
l.Suffix = s
|
o.Suffix = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLimit limits the number of returned keys to l
|
// ListLimit limits the number of returned keys
|
||||||
func ListLimit(l uint) ListOption {
|
func ListLimit(n uint) ListOption {
|
||||||
return func(lo *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
lo.Limit = l
|
o.Limit = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListOffset starts returning responses from o. Use in conjunction with Limit for pagination.
|
// ListOffset use with Limit for pagination
|
||||||
func ListOffset(o uint) ListOption {
|
func ListOffset(n uint) ListOption {
|
||||||
return func(l *ListOptions) {
|
return func(o *ListOptions) {
|
||||||
l.Offset = o
|
o.Offset = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExistsOption specifies Exists call options
|
// ListNamespace pass namespace to list options
|
||||||
type ExistsOption func(*ExistsOptions)
|
func ListNamespace(ns string) ListOption {
|
||||||
|
return func(o *ListOptions) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ExistsOptions holds options for Exists method
|
// ExistsOptions holds options for Exists method
|
||||||
type ExistsOptions struct {
|
type ExistsOptions struct {
|
||||||
@@ -312,6 +311,9 @@ type ExistsOptions struct {
|
|||||||
Namespace string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExistsOption specifies Exists call options
|
||||||
|
type ExistsOption func(*ExistsOptions)
|
||||||
|
|
||||||
// NewExistsOptions helper for Exists method
|
// NewExistsOptions helper for Exists method
|
||||||
func NewExistsOptions(opts ...ExistsOption) ExistsOptions {
|
func NewExistsOptions(opts ...ExistsOption) ExistsOptions {
|
||||||
options := ExistsOptions{
|
options := ExistsOptions{
|
||||||
@@ -322,3 +324,26 @@ func NewExistsOptions(opts ...ExistsOption) ExistsOptions {
|
|||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExistsContext pass context.Context to exist options
|
||||||
|
func ExistsContext(ctx context.Context) ExistsOption {
|
||||||
|
return func(o *ExistsOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExistsNamespace pass namespace to exist options
|
||||||
|
func ExistsNamespace(ns string) ExistsOption {
|
||||||
|
return func(o *ExistsOptions) {
|
||||||
|
o.Namespace = ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// WrapStore adds a store Wrapper to a list of options passed into the store
|
||||||
|
func WrapStore(w Wrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Wrappers = append(o.Wrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
82
store/wrapper.go
Normal file
82
store/wrapper.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogfFunc function used for Logf method
|
||||||
|
// type LogfFunc func(ctx context.Context, level Level, msg string, args ...interface{})
|
||||||
|
// type Wrapper interface {
|
||||||
|
// Logf logs message with needed level
|
||||||
|
// Logf(LogfFunc) LogfFunc
|
||||||
|
// }
|
||||||
|
|
||||||
|
// NamespaceStore wrap store with namespace
|
||||||
|
type NamespaceStore struct {
|
||||||
|
s Store
|
||||||
|
ns string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Store = &NamespaceStore{}
|
||||||
|
|
||||||
|
func NewNamespaceStore(s Store, ns string) Store {
|
||||||
|
return &NamespaceStore{s: s, ns: ns}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Init(opts ...Option) error {
|
||||||
|
return w.s.Init(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Connect(ctx context.Context) error {
|
||||||
|
return w.s.Connect(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Disconnect(ctx context.Context) error {
|
||||||
|
return w.s.Disconnect(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Read(ctx context.Context, key string, val interface{}, opts ...ReadOption) error {
|
||||||
|
return w.s.Read(ctx, key, val, append(opts, ReadNamespace(w.ns))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Write(ctx context.Context, key string, val interface{}, opts ...WriteOption) error {
|
||||||
|
return w.s.Write(ctx, key, val, append(opts, WriteNamespace(w.ns))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Delete(ctx context.Context, key string, opts ...DeleteOption) error {
|
||||||
|
return w.s.Delete(ctx, key, append(opts, DeleteNamespace(w.ns))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Exists(ctx context.Context, key string, opts ...ExistsOption) error {
|
||||||
|
return w.s.Exists(ctx, key, append(opts, ExistsNamespace(w.ns))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) List(ctx context.Context, opts ...ListOption) ([]string, error) {
|
||||||
|
return w.s.List(ctx, append(opts, ListNamespace(w.ns))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Options() Options {
|
||||||
|
return w.s.Options()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) Name() string {
|
||||||
|
return w.s.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *NamespaceStore) String() string {
|
||||||
|
return w.s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// type NamespaceWrapper struct{}
|
||||||
|
|
||||||
|
// func NewNamespaceWrapper() Wrapper {
|
||||||
|
// return &NamespaceWrapper{}
|
||||||
|
// }
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (w *OmitWrapper) Logf(fn LogfFunc) LogfFunc {
|
||||||
|
return func(ctx context.Context, level Level, msg string, args ...interface{}) {
|
||||||
|
fn(ctx, level, msg, getArgs(args)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
@@ -51,7 +51,7 @@ func (m *memorySync) Leader(id string, opts ...LeaderOption) (Leader, error) {
|
|||||||
id: id,
|
id: id,
|
||||||
resign: func(id string) error {
|
resign: func(id string) error {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
m.Unlock(id)
|
_ = m.Unlock(id)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/auth"
|
"github.com/unistack-org/micro/v3/auth"
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify the auth credentials and refresh the auth token periodically
|
// Verify the auth credentials and refresh the auth token periodically
|
||||||
@@ -22,7 +22,11 @@ func Verify(a auth.Auth) error {
|
|||||||
auth.WithScopes("service"),
|
auth.WithScopes("service"),
|
||||||
}
|
}
|
||||||
|
|
||||||
acc, err := a.Generate(uuid.New().String(), opts...)
|
id, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
acc, err := a.Generate(id, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -44,7 +48,7 @@ func Verify(a auth.Auth) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the credentials and token in auth options
|
// set the credentials and token in auth options
|
||||||
a.Init(
|
_ = a.Init(
|
||||||
auth.ClientToken(token),
|
auth.ClientToken(token),
|
||||||
auth.Credentials(accID, accSecret),
|
auth.Credentials(accID, accSecret),
|
||||||
)
|
)
|
||||||
@@ -75,7 +79,7 @@ func Verify(a auth.Auth) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the token
|
// set the token
|
||||||
a.Init(auth.ClientToken(tok))
|
_ = a.Init(auth.ClientToken(tok))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -80,5 +81,4 @@ func TestRoundTripper(t *testing.T) {
|
|||||||
if string(b) != "hello world" {
|
if string(b) != "hello world" {
|
||||||
t.Fatal("response is", string(b))
|
t.Fatal("response is", string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
239
util/http/trie.go
Normal file
239
util/http/trie.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrieOptions contains search options
|
||||||
|
type TrieOptions struct {
|
||||||
|
IgnoreCase bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrieOption func signature
|
||||||
|
type TrieOption func(*TrieOptions)
|
||||||
|
|
||||||
|
// IgnoreCase says that search must be case insensitive
|
||||||
|
func IgnoreCase(b bool) TrieOption {
|
||||||
|
return func(o *TrieOptions) {
|
||||||
|
o.IgnoreCase = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tree is a trie tree.
|
||||||
|
type Trie struct {
|
||||||
|
node *node
|
||||||
|
rcache map[string]*regexp.Regexp
|
||||||
|
rmu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// node is a node of tree
|
||||||
|
type node struct {
|
||||||
|
actions map[string]interface{} // key is method, val is handler interface
|
||||||
|
children map[string]*node // key is label of next nodes
|
||||||
|
label string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathRoot string = "/"
|
||||||
|
pathDelimiter string = "/"
|
||||||
|
paramDelimiter string = ":"
|
||||||
|
leftPtnDelimiter string = "{"
|
||||||
|
rightPtnDelimiter string = "}"
|
||||||
|
ptnWildcard string = "(.+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTree creates a new trie tree.
|
||||||
|
func NewTrie() *Trie {
|
||||||
|
return &Trie{
|
||||||
|
node: &node{
|
||||||
|
label: pathRoot,
|
||||||
|
actions: make(map[string]interface{}),
|
||||||
|
children: make(map[string]*node),
|
||||||
|
},
|
||||||
|
rcache: make(map[string]*regexp.Regexp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts a route definition to tree.
|
||||||
|
func (t *Trie) Insert(methods []string, path string, handler interface{}) {
|
||||||
|
curNode := t.node
|
||||||
|
if path == pathRoot {
|
||||||
|
curNode.label = path
|
||||||
|
for _, method := range methods {
|
||||||
|
curNode.actions[method] = handler
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ep := splitPath(path)
|
||||||
|
for i, p := range ep {
|
||||||
|
nextNode, ok := curNode.children[p]
|
||||||
|
if ok {
|
||||||
|
curNode = nextNode
|
||||||
|
}
|
||||||
|
// Create a new node.
|
||||||
|
if !ok {
|
||||||
|
curNode.children[p] = &node{
|
||||||
|
label: p,
|
||||||
|
actions: make(map[string]interface{}),
|
||||||
|
children: make(map[string]*node),
|
||||||
|
}
|
||||||
|
curNode = curNode.children[p]
|
||||||
|
}
|
||||||
|
// last loop.
|
||||||
|
// If there is already registered data, overwrite it.
|
||||||
|
if i == len(ep)-1 {
|
||||||
|
curNode.label = p
|
||||||
|
for _, method := range methods {
|
||||||
|
curNode.actions[method] = handler
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search searches a path from a tree.
|
||||||
|
func (t *Trie) Search(method string, path string, opts ...TrieOption) (interface{}, map[string]string, bool) {
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
options := TrieOptions{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
curNode := t.node
|
||||||
|
|
||||||
|
nodeLoop:
|
||||||
|
for _, p := range splitPath(path) {
|
||||||
|
nextNode, ok := curNode.children[p]
|
||||||
|
if ok {
|
||||||
|
curNode = nextNode
|
||||||
|
continue nodeLoop
|
||||||
|
}
|
||||||
|
if options.IgnoreCase {
|
||||||
|
// additional loop for case insensitive matching
|
||||||
|
for k, v := range curNode.children {
|
||||||
|
if literalEqual(k, p, true) {
|
||||||
|
curNode = v
|
||||||
|
continue nodeLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(curNode.children) == 0 {
|
||||||
|
if !literalEqual(curNode.label, p, options.IgnoreCase) {
|
||||||
|
// no matching path was found
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isParamMatch := false
|
||||||
|
for c := range curNode.children {
|
||||||
|
if string([]rune(c)[0]) == leftPtnDelimiter {
|
||||||
|
ptn := getPattern(c)
|
||||||
|
t.rmu.RLock()
|
||||||
|
reg, ok := t.rcache[ptn]
|
||||||
|
t.rmu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
reg, err = regexp.Compile(ptn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
t.rmu.Lock()
|
||||||
|
t.rcache[ptn] = reg
|
||||||
|
t.rmu.Unlock()
|
||||||
|
}
|
||||||
|
if reg.Match([]byte(p)) {
|
||||||
|
pn := getParamName(c)
|
||||||
|
params[pn] = p
|
||||||
|
curNode = curNode.children[c]
|
||||||
|
isParamMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// no matching param was found.
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isParamMatch {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path == pathRoot {
|
||||||
|
if len(curNode.actions) == 0 {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, ok := curNode.actions[method]
|
||||||
|
if !ok || handler == nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return handler, params, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPattern gets a pattern from a label
|
||||||
|
// {id:[^\d+$]} -> ^\d+$
|
||||||
|
// {id} -> (.+)
|
||||||
|
func getPattern(label string) string {
|
||||||
|
leftI := strings.Index(label, leftPtnDelimiter)
|
||||||
|
rightI := strings.Index(label, paramDelimiter)
|
||||||
|
// if label doesn't have any pattern, return wild card pattern as default.
|
||||||
|
if leftI == -1 || rightI == -1 {
|
||||||
|
return ptnWildcard
|
||||||
|
}
|
||||||
|
return label[rightI+1 : len(label)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getParamName gets a parameter from a label
|
||||||
|
// {id:[^\d+$]} -> id
|
||||||
|
// {id} -> id
|
||||||
|
func getParamName(label string) string {
|
||||||
|
leftI := strings.Index(label, leftPtnDelimiter)
|
||||||
|
rightI := func(l string) int {
|
||||||
|
r := []rune(l)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i := 0; i < len(r); i++ {
|
||||||
|
n = i
|
||||||
|
switch string(r[i]) {
|
||||||
|
case paramDelimiter:
|
||||||
|
n = i
|
||||||
|
break loop
|
||||||
|
case rightPtnDelimiter:
|
||||||
|
n = i
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(r)-1 {
|
||||||
|
n = i + 1
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}(label)
|
||||||
|
|
||||||
|
return label[leftI+1 : rightI]
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPath removes an empty value in slice.
|
||||||
|
func splitPath(path string) []string {
|
||||||
|
s := strings.Split(path, pathDelimiter)
|
||||||
|
var r []string
|
||||||
|
for _, str := range s {
|
||||||
|
if str != "" {
|
||||||
|
r = append(r, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func literalEqual(component, literal string, ignoreCase bool) bool {
|
||||||
|
if ignoreCase {
|
||||||
|
return strings.EqualFold(component, literal)
|
||||||
|
}
|
||||||
|
return component == literal
|
||||||
|
}
|
95
util/http/trie_test.go
Normal file
95
util/http/trie_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrieIgnoreCase(t *testing.T) {
|
||||||
|
type handler struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", &handler{name: "test"})
|
||||||
|
|
||||||
|
_, _, ok := tr.Search(http.MethodPut, "/v1/CREATE/12", IgnoreCase(true))
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrieContentType(t *testing.T) {
|
||||||
|
type handler struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{"application/json"}, "/v1/create/{id}", &handler{name: "test"})
|
||||||
|
|
||||||
|
h, _, ok := tr.Search("application/json", "/v1/create/12")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("must be found error")
|
||||||
|
}
|
||||||
|
if h.(*handler).name != "test" {
|
||||||
|
t.Fatalf("invalid handler %v", h)
|
||||||
|
}
|
||||||
|
_, _, ok = tr.Search("text/xml", "/v1/create/12")
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("must be not found error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrieNoMatchMethod(t *testing.T) {
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", nil)
|
||||||
|
|
||||||
|
_, _, ok := tr.Search(http.MethodPost, "/v1/create")
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("must be not found error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrieMatchRegexp(t *testing.T) {
|
||||||
|
type handler struct{}
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create/{category}/{id:[0-9]+}", &handler{})
|
||||||
|
|
||||||
|
_, params, ok := tr.Search(http.MethodPut, "/v1/create/test_cat/12345")
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
t.Fatalf("route not found")
|
||||||
|
case len(params) != 2:
|
||||||
|
t.Fatalf("param matching error %v", params)
|
||||||
|
case params["category"] != "test_cat":
|
||||||
|
t.Fatalf("param matching error %v", params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrieMatchRegexpFail(t *testing.T) {
|
||||||
|
type handler struct{}
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[a-z]+}", &handler{})
|
||||||
|
|
||||||
|
_, _, ok := tr.Search(http.MethodPut, "/v1/create/12345")
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("route must not be not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrieMatchLongest(t *testing.T) {
|
||||||
|
type handler struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tr := NewTrie()
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create", &handler{name: "first"})
|
||||||
|
tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[0-9]+}", &handler{name: "second"})
|
||||||
|
if h, _, ok := tr.Search(http.MethodPut, "/v1/create/12345"); !ok {
|
||||||
|
t.Fatalf("route must be found")
|
||||||
|
} else if h.(*handler).name != "second" {
|
||||||
|
t.Fatalf("invalid handler found: %s != %s", h.(*handler).name, "second")
|
||||||
|
}
|
||||||
|
if h, _, ok := tr.Search(http.MethodPut, "/v1/create"); !ok {
|
||||||
|
t.Fatalf("route must be found")
|
||||||
|
} else if h.(*handler).name != "first" {
|
||||||
|
t.Fatalf("invalid handler found: %s != %s", h.(*handler).name, "first")
|
||||||
|
}
|
||||||
|
}
|
22
util/id/LICENSE
Normal file
22
util/id/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2018-2021 Matous Dzivjak <matousdzivjak@gmail.com>
|
||||||
|
Copyright (c) 2021 Unistack LLC <v.tolstov@unistack.org>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
112
util/id/id.go
Normal file
112
util/id/id.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package id
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultAlphabet is the alphabet used for ID characters by default
|
||||||
|
var DefaultAlphabet = []rune("6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz")
|
||||||
|
|
||||||
|
// DefaultSize is the size used for ID by default
|
||||||
|
// To get uuid like collision specify 21
|
||||||
|
var DefaultSize = 16
|
||||||
|
|
||||||
|
// getMask generates bit mask used to obtain bits from the random bytes that are used to get index of random character
|
||||||
|
// from the alphabet. Example: if the alphabet has 6 = (110)_2 characters it is sufficient to use mask 7 = (111)_2
|
||||||
|
func getMask(alphabetSize int) int {
|
||||||
|
for i := 1; i <= 8; i++ {
|
||||||
|
mask := (2 << uint(i)) - 1
|
||||||
|
if mask >= alphabetSize-1 {
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns new id or error
|
||||||
|
func New(opts ...Option) (string, error) {
|
||||||
|
options := NewOptions(opts...)
|
||||||
|
|
||||||
|
if len(options.Alphabet) == 0 || len(options.Alphabet) > 255 {
|
||||||
|
return "", errors.New("alphabet must not be empty and contain no more than 255 chars")
|
||||||
|
}
|
||||||
|
if options.Size <= 0 {
|
||||||
|
return "", errors.New("size must be positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
chars := options.Alphabet
|
||||||
|
|
||||||
|
mask := getMask(len(chars))
|
||||||
|
// estimate how many random bytes we will need for the ID, we might actually need more but this is tradeoff
|
||||||
|
// between average case and worst case
|
||||||
|
ceilArg := 1.6 * float64(mask*options.Size) / float64(len(options.Alphabet))
|
||||||
|
step := int(math.Ceil(ceilArg))
|
||||||
|
|
||||||
|
id := make([]rune, options.Size)
|
||||||
|
bytes := make([]byte, step)
|
||||||
|
for j := 0; ; {
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for i := 0; i < step; i++ {
|
||||||
|
currByte := bytes[i] & byte(mask)
|
||||||
|
if currByte < byte(len(chars)) {
|
||||||
|
id[j] = chars[currByte]
|
||||||
|
j++
|
||||||
|
if j == options.Size {
|
||||||
|
return string(id[:options.Size]), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must is the same as New but fatals on error
|
||||||
|
func Must(opts ...Option) string {
|
||||||
|
id, err := New(opts...)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(context.TODO(), err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options contains id deneration options
|
||||||
|
type Options struct {
|
||||||
|
Alphabet []rune
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option func signature
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// Alphabet specifies alphabet to use
|
||||||
|
func Alphabet(alphabet string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Alphabet = []rune(alphabet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size specifies id size
|
||||||
|
func Size(size int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns new Options struct filled by opts
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
options := Options{
|
||||||
|
Alphabet: DefaultAlphabet,
|
||||||
|
Size: DefaultSize,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/unistack-org/micro/v3/util/rand"
|
"github.com/unistack-org/micro/v3/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Do returns a random time to jitter with max cap specified
|
// Random returns a random time to jitter with max cap specified
|
||||||
func Do(d time.Duration) time.Duration {
|
func Random(d time.Duration) time.Duration {
|
||||||
var rng rand.Rand
|
var rng rand.Rand
|
||||||
v := rng.Float64() * float64(d.Nanoseconds())
|
v := rng.Float64() * float64(d.Nanoseconds())
|
||||||
return time.Duration(v)
|
return time.Duration(v)
|
65
util/jitter/ticker.go
Normal file
65
util/jitter/ticker.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package jitter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/unistack-org/micro/v3/util/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ticker is similar to time.Ticker but ticks at random intervals between
|
||||||
|
// the min and max duration values (stored internally as int64 nanosecond
|
||||||
|
// counts).
|
||||||
|
type Ticker struct {
|
||||||
|
C chan time.Time
|
||||||
|
done chan chan struct{}
|
||||||
|
min int64
|
||||||
|
max int64
|
||||||
|
rng rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTicker returns a pointer to an initialized instance of the Ticker.
|
||||||
|
// Min and max are durations of the shortest and longest allowed
|
||||||
|
// ticks. Ticker will run in a goroutine until explicitly stopped.
|
||||||
|
func NewTicker(min, max time.Duration) *Ticker {
|
||||||
|
ticker := &Ticker{
|
||||||
|
C: make(chan time.Time),
|
||||||
|
done: make(chan chan struct{}),
|
||||||
|
min: min.Nanoseconds(),
|
||||||
|
max: max.Nanoseconds(),
|
||||||
|
}
|
||||||
|
go ticker.run()
|
||||||
|
return ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop terminates the ticker goroutine and closes the C channel.
|
||||||
|
func (ticker *Ticker) Stop() {
|
||||||
|
c := make(chan struct{})
|
||||||
|
ticker.done <- c
|
||||||
|
<-c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ticker *Ticker) run() {
|
||||||
|
defer close(ticker.C)
|
||||||
|
t := time.NewTimer(ticker.nextInterval())
|
||||||
|
for {
|
||||||
|
// either a stop signal or a timeout
|
||||||
|
select {
|
||||||
|
case c := <-ticker.done:
|
||||||
|
t.Stop()
|
||||||
|
close(c)
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
select {
|
||||||
|
case ticker.C <- time.Now():
|
||||||
|
t.Stop()
|
||||||
|
t = time.NewTimer(ticker.nextInterval())
|
||||||
|
default:
|
||||||
|
// there could be noone receiving...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ticker *Ticker) nextInterval() time.Duration {
|
||||||
|
return time.Duration(ticker.rng.Int63n(ticker.max-ticker.min)+ticker.min) * time.Nanosecond
|
||||||
|
}
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/unistack-org/micro/v3/network/transport"
|
"github.com/unistack-org/micro/v3/network/transport"
|
||||||
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pool struct {
|
type pool struct {
|
||||||
@@ -49,7 +49,7 @@ func (p *poolConn) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *poolConn) Id() string {
|
func (p *poolConn) ID() string {
|
||||||
return p.id
|
return p.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +87,13 @@ func (p *pool) Get(ctx context.Context, addr string, opts ...transport.DialOptio
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
id, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &poolConn{
|
return &poolConn{
|
||||||
Client: c,
|
Client: c,
|
||||||
id: uuid.New().String(),
|
id: id,
|
||||||
created: time.Now(),
|
created: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ type Pool interface {
|
|||||||
// Conn conn pool interface
|
// Conn conn pool interface
|
||||||
type Conn interface {
|
type Conn interface {
|
||||||
// unique id of connection
|
// unique id of connection
|
||||||
Id() string
|
ID() string
|
||||||
// time it was created
|
// time it was created
|
||||||
Created() time.Time
|
Created() time.Time
|
||||||
// embedded connection
|
// embedded connection
|
||||||
|
@@ -16,7 +16,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
// ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function.
|
// ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function.
|
||||||
// Specifically, this will be returned when there is no equals sign present in the URL query parameter.
|
// Specifically, this will be returned when there is no equals sign present in the URL query parameter.
|
||||||
ErrInvalidParam error = errors.New("qson: invalid url query param provided")
|
ErrInvalidParam = errors.New("qson: invalid url query param provided")
|
||||||
|
|
||||||
bracketSplitter *regexp.Regexp
|
bracketSplitter *regexp.Regexp
|
||||||
)
|
)
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
package rand
|
package rand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rand is a wrapper around crypto/rand that adds some convenience functions known from math/rand.
|
// Rand is a wrapper around crypto/rand that adds some convenience functions known from math/rand
|
||||||
type Rand struct {
|
type Rand struct {
|
||||||
buf [8]byte
|
buf [8]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rand) Int31() int32 {
|
func (r *Rand) Int31() int32 {
|
||||||
_, _ = rand.Read(r.buf[:4])
|
_, _ = crand.Read(r.buf[:4])
|
||||||
return int32(binary.BigEndian.Uint32(r.buf[:4]) & ^uint32(1<<31))
|
return int32(binary.BigEndian.Uint32(r.buf[:4]) & ^uint32(1<<31))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ func (r *Rand) Intn(n int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rand) Int63() int64 {
|
func (r *Rand) Int63() int64 {
|
||||||
_, _ = rand.Read(r.buf[:])
|
_, _ = crand.Read(r.buf[:])
|
||||||
return int64(binary.BigEndian.Uint64(r.buf[:]) & ^uint64(1<<63))
|
return int64(binary.BigEndian.Uint64(r.buf[:]) & ^uint64(1<<63))
|
||||||
}
|
}
|
||||||
|
|
||||||
// copied from the standard library math/rand implementation of Int63n
|
// Int31n copied from the standard library math/rand implementation of Int31n
|
||||||
func (r *Rand) Int31n(n int32) int32 {
|
func (r *Rand) Int31n(n int32) int32 {
|
||||||
if n&(n-1) == 0 { // n is power of two, can mask
|
if n&(n-1) == 0 { // n is power of two, can mask
|
||||||
return r.Int31() & (n - 1)
|
return r.Int31() & (n - 1)
|
||||||
@@ -71,6 +71,7 @@ func (r *Rand) Int31n(n int32) int32 {
|
|||||||
return v % n
|
return v % n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Int63n copied from the standard library math/rand implementation of Int63n
|
||||||
func (r *Rand) Int63n(n int64) int64 {
|
func (r *Rand) Int63n(n int64) int64 {
|
||||||
if n&(n-1) == 0 { // n is power of two, can mask
|
if n&(n-1) == 0 { // n is power of two, can mask
|
||||||
return r.Int63() & (n - 1)
|
return r.Int63() & (n - 1)
|
||||||
@@ -82,3 +83,26 @@ func (r *Rand) Int63n(n int64) int64 {
|
|||||||
}
|
}
|
||||||
return v % n
|
return v % n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shuffle copied from the standard library math/rand implementation of Shuffle
|
||||||
|
func (r *Rand) Shuffle(n int, swap func(i, j int)) {
|
||||||
|
if n < 0 {
|
||||||
|
panic("invalid argument to Shuffle")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
||||||
|
// Shuffle really ought not be called with n that doesn't fit in 32 bits.
|
||||||
|
// Not only will it take a very long time, but with 2³¹! possible permutations,
|
||||||
|
// there's no way that any PRNG can have a big enough internal state to
|
||||||
|
// generate even a minuscule percentage of the possible permutations.
|
||||||
|
// Nevertheless, the right API signature accepts an int n, so handle it as best we can.
|
||||||
|
i := n - 1
|
||||||
|
for ; i > 1<<31-1-1; i-- {
|
||||||
|
j := int(r.Int63n(int64(i + 1)))
|
||||||
|
swap(i, j)
|
||||||
|
}
|
||||||
|
for ; i > 0; i-- {
|
||||||
|
j := int(r.Int31n(int32(i + 1)))
|
||||||
|
swap(i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -82,9 +82,9 @@ func getValueByName(v reflect.Value, key string) (reflect.Value, error) {
|
|||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
value = v.FieldByName(key)
|
value = v.FieldByName(key)
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
kValue := reflect.Indirect(reflect.New(v.Type().Key()))
|
kvalue := reflect.Indirect(reflect.New(v.Type().Key()))
|
||||||
kValue.SetString(key)
|
kvalue.SetString(key)
|
||||||
value = v.MapIndex(kValue)
|
value = v.MapIndex(kvalue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !value.IsValid() {
|
if !value.IsValid() {
|
||||||
|
@@ -7,21 +7,24 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidParam specifies invalid url query params
|
// ErrInvalidParam specifies invalid url query params
|
||||||
var ErrInvalidParam = errors.New("invalid url query param provided")
|
var ErrInvalidParam = errors.New("invalid url query param provided")
|
||||||
|
|
||||||
|
// var timeKind = reflect.ValueOf(time.Time{}).Kind()
|
||||||
|
|
||||||
|
// bracketSplitter
|
||||||
var bracketSplitter = regexp.MustCompile(`\[|\]`)
|
var bracketSplitter = regexp.MustCompile(`\[|\]`)
|
||||||
|
|
||||||
var timeKind = reflect.TypeOf(time.Time{}).Kind()
|
// StructField contains struct field path its value and field
|
||||||
|
|
||||||
type StructField struct {
|
type StructField struct {
|
||||||
Field reflect.StructField
|
Path string
|
||||||
Value reflect.Value
|
Value reflect.Value
|
||||||
|
Field reflect.StructField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StructFieldByTag get struct field by tag key and its value
|
||||||
func StructFieldByTag(src interface{}, tkey string, tval string) (interface{}, error) {
|
func StructFieldByTag(src interface{}, tkey string, tval string) (interface{}, error) {
|
||||||
sv := reflect.ValueOf(src)
|
sv := reflect.ValueOf(src)
|
||||||
if sv.Kind() == reflect.Ptr {
|
if sv.Kind() == reflect.Ptr {
|
||||||
@@ -35,7 +38,7 @@ func StructFieldByTag(src interface{}, tkey string, tval string) (interface{}, e
|
|||||||
for idx := 0; idx < typ.NumField(); idx++ {
|
for idx := 0; idx < typ.NumField(); idx++ {
|
||||||
fld := typ.Field(idx)
|
fld := typ.Field(idx)
|
||||||
val := sv.Field(idx)
|
val := sv.Field(idx)
|
||||||
if !val.CanSet() || len(fld.PkgPath) != 0 {
|
if len(fld.PkgPath) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +69,19 @@ func StructFieldByTag(src interface{}, tkey string, tval string) (interface{}, e
|
|||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StructFieldByPath get struct field by its path
|
||||||
|
func StructFieldByPath(src interface{}, path string) (interface{}, error) {
|
||||||
|
var err error
|
||||||
|
for _, p := range strings.Split(path, ".") {
|
||||||
|
src, err = StructFieldByName(src, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructFieldByName get struct field by its name
|
||||||
func StructFieldByName(src interface{}, tkey string) (interface{}, error) {
|
func StructFieldByName(src interface{}, tkey string) (interface{}, error) {
|
||||||
sv := reflect.ValueOf(src)
|
sv := reflect.ValueOf(src)
|
||||||
if sv.Kind() == reflect.Ptr {
|
if sv.Kind() == reflect.Ptr {
|
||||||
@@ -79,7 +95,7 @@ func StructFieldByName(src interface{}, tkey string) (interface{}, error) {
|
|||||||
for idx := 0; idx < typ.NumField(); idx++ {
|
for idx := 0; idx < typ.NumField(); idx++ {
|
||||||
fld := typ.Field(idx)
|
fld := typ.Field(idx)
|
||||||
val := sv.Field(idx)
|
val := sv.Field(idx)
|
||||||
if !val.CanSet() || len(fld.PkgPath) != 0 {
|
if len(fld.PkgPath) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fld.Name == tkey {
|
if fld.Name == tkey {
|
||||||
@@ -105,6 +121,19 @@ func StructFieldByName(src interface{}, tkey string) (interface{}, error) {
|
|||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StructFieldsMap returns struct map[string]interface{} or error
|
||||||
|
func StructFieldsMap(src interface{}) (map[string]interface{}, error) {
|
||||||
|
fields, err := StructFields(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mp := make(map[string]interface{}, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
mp[field.Path] = field.Value.Interface()
|
||||||
|
}
|
||||||
|
return mp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StructFields returns slice of struct fields
|
// StructFields returns slice of struct fields
|
||||||
func StructFields(src interface{}) ([]StructField, error) {
|
func StructFields(src interface{}) ([]StructField, error) {
|
||||||
var fields []StructField
|
var fields []StructField
|
||||||
@@ -116,25 +145,29 @@ func StructFields(src interface{}) ([]StructField, error) {
|
|||||||
if sv.Kind() != reflect.Struct {
|
if sv.Kind() != reflect.Struct {
|
||||||
return nil, ErrInvalidStruct
|
return nil, ErrInvalidStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
typ := sv.Type()
|
typ := sv.Type()
|
||||||
for idx := 0; idx < typ.NumField(); idx++ {
|
for idx := 0; idx < typ.NumField(); idx++ {
|
||||||
fld := typ.Field(idx)
|
fld := typ.Field(idx)
|
||||||
val := sv.Field(idx)
|
val := sv.Field(idx)
|
||||||
if !val.CanSet() || len(fld.PkgPath) != 0 {
|
if !val.IsValid() || len(fld.PkgPath) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case timeKind:
|
// case timeKind:
|
||||||
fields = append(fields, StructField{Field: fld, Value: val})
|
// fmt.Printf("GGG\n")
|
||||||
|
// fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
infields, err := StructFields(val.Interface())
|
infields, err := StructFields(val.Interface())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fields = append(fields, infields...)
|
for _, infield := range infields {
|
||||||
|
infield.Path = fmt.Sprintf("%s.%s", fld.Name, infield.Path)
|
||||||
|
fields = append(fields, infield)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fields = append(fields, StructField{Field: fld, Value: val})
|
fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +212,7 @@ func CopyFrom(a, b interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StructURLValues get struct fields via url.Values
|
||||||
func StructURLValues(src interface{}, pref string, tags []string) (url.Values, error) {
|
func StructURLValues(src interface{}, pref string, tags []string) (url.Values, error) {
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
|
|
||||||
@@ -194,7 +228,7 @@ func StructURLValues(src interface{}, pref string, tags []string) (url.Values, e
|
|||||||
for idx := 0; idx < typ.NumField(); idx++ {
|
for idx := 0; idx < typ.NumField(); idx++ {
|
||||||
fld := typ.Field(idx)
|
fld := typ.Field(idx)
|
||||||
val := sv.Field(idx)
|
val := sv.Field(idx)
|
||||||
if !val.CanSet() || len(fld.PkgPath) != 0 || !val.IsValid() {
|
if len(fld.PkgPath) != 0 || !val.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +1,82 @@
|
|||||||
package reflect
|
package reflect_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
rutil "github.com/unistack-org/micro/v3/util/reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestStructFieldsMap(t *testing.T) {
|
||||||
|
type NestedStr struct {
|
||||||
|
BBB string
|
||||||
|
CCC int
|
||||||
|
}
|
||||||
|
type Str struct {
|
||||||
|
Name []string `json:"name" codec:"flatten"`
|
||||||
|
XXX string `json:"xxx"`
|
||||||
|
Nested NestedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
val := &Str{Name: []string{"first", "second"}, XXX: "ttt", Nested: NestedStr{BBB: "ddd", CCC: 9}}
|
||||||
|
fields, err := rutil.StructFieldsMap(val)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v, ok := fields["Nested.BBB"]; !ok || v != "ddd" {
|
||||||
|
t.Fatalf("invalid field from %v", fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructFields(t *testing.T) {
|
||||||
|
type NestedStr struct {
|
||||||
|
BBB string
|
||||||
|
CCC int
|
||||||
|
}
|
||||||
|
type Str struct {
|
||||||
|
Name []string `json:"name" codec:"flatten"`
|
||||||
|
XXX string `json:"xxx"`
|
||||||
|
Nested NestedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
val := &Str{Name: []string{"first", "second"}, XXX: "ttt", Nested: NestedStr{BBB: "ddd", CCC: 9}}
|
||||||
|
fields, err := rutil.StructFields(val)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
for _, field := range fields {
|
||||||
|
if field.Path == "Nested.CCC" {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("struct fields returns invalid path: %v", fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructByPath(t *testing.T) {
|
||||||
|
type NestedStr struct {
|
||||||
|
BBB string
|
||||||
|
CCC int
|
||||||
|
}
|
||||||
|
type Str struct {
|
||||||
|
Name []string `json:"name" codec:"flatten"`
|
||||||
|
XXX string `json:"xxx"`
|
||||||
|
Nested NestedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
val := &Str{Name: []string{"first", "second"}, XXX: "ttt", Nested: NestedStr{BBB: "ddd", CCC: 9}}
|
||||||
|
field, err := rutil.StructFieldByPath(val, "Nested.CCC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if reflect.Indirect(reflect.ValueOf(field)).Int() != 9 {
|
||||||
|
t.Fatalf("invalid elem returned: %v", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructByTag(t *testing.T) {
|
func TestStructByTag(t *testing.T) {
|
||||||
type Str struct {
|
type Str struct {
|
||||||
Name []string `json:"name" codec:"flatten"`
|
Name []string `json:"name" codec:"flatten"`
|
||||||
@@ -12,7 +84,7 @@ func TestStructByTag(t *testing.T) {
|
|||||||
|
|
||||||
val := &Str{Name: []string{"first", "second"}}
|
val := &Str{Name: []string{"first", "second"}}
|
||||||
|
|
||||||
iface, err := StructFieldByTag(val, "codec", "flatten")
|
iface, err := rutil.StructFieldByTag(val, "codec", "flatten")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -31,7 +103,7 @@ func TestStructByName(t *testing.T) {
|
|||||||
|
|
||||||
val := &Str{Name: []string{"first", "second"}}
|
val := &Str{Name: []string{"first", "second"}}
|
||||||
|
|
||||||
iface, err := StructFieldByName(val, "Name")
|
iface, err := rutil.StructFieldByName(val, "Name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -51,7 +123,7 @@ func TestStructURLValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val := &Str{Name: "test_name", Args: []int{1, 2, 3}, Str: &Str{Name: "nested_name"}}
|
val := &Str{Name: "test_name", Args: []int{1, 2, 3}, Str: &Str{Name: "nested_name"}}
|
||||||
data, err := StructURLValues(val, "", []string{"json"})
|
data, err := rutil.StructURLValues(val, "", []string{"json"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -67,7 +139,7 @@ func TestURLSliceVars(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mp, err := URLMap(u.RawQuery)
|
mp, err := rutil.URLMap(u.RawQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -93,7 +165,7 @@ func TestURLVars(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mp, err := URLMap(u.RawQuery)
|
mp, err := rutil.URLMap(u.RawQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -113,14 +185,14 @@ func TestIsZero(t *testing.T) {
|
|||||||
}
|
}
|
||||||
testStr1.Nested.NestedName = "nested_name"
|
testStr1.Nested.NestedName = "nested_name"
|
||||||
|
|
||||||
if ok := IsZero(testStr1); ok {
|
if ok := rutil.IsZero(testStr1); ok {
|
||||||
t.Fatalf("zero ret on non zero struct: %#+v", testStr1)
|
t.Fatalf("zero ret on non zero struct: %#+v", testStr1)
|
||||||
}
|
}
|
||||||
|
|
||||||
testStr1.Name = ""
|
testStr1.Name = ""
|
||||||
testStr1.Value = ""
|
testStr1.Value = ""
|
||||||
testStr1.Nested.NestedName = ""
|
testStr1.Nested.NestedName = ""
|
||||||
if ok := IsZero(testStr1); !ok {
|
if ok := rutil.IsZero(testStr1); !ok {
|
||||||
t.Fatalf("non zero ret on zero struct: %#+v", testStr1)
|
t.Fatalf("non zero ret on zero struct: %#+v", testStr1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +207,12 @@ func TestIsZero(t *testing.T) {
|
|||||||
Name: "test_name",
|
Name: "test_name",
|
||||||
Nested: &testStr3{Nested: "nested_name"},
|
Nested: &testStr3{Nested: "nested_name"},
|
||||||
}
|
}
|
||||||
if ok := IsZero(vtest); ok {
|
if ok := rutil.IsZero(vtest); ok {
|
||||||
t.Fatalf("zero ret on non zero struct: %#+v", vtest)
|
t.Fatalf("zero ret on non zero struct: %#+v", vtest)
|
||||||
}
|
}
|
||||||
vtest.Nested = nil
|
vtest.Nested = nil
|
||||||
vtest.Name = ""
|
vtest.Name = ""
|
||||||
if ok := IsZero(vtest); !ok {
|
if ok := rutil.IsZero(vtest); !ok {
|
||||||
t.Fatalf("non zero ret on zero struct: %#+v", vtest)
|
t.Fatalf("non zero ret on zero struct: %#+v", vtest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/unistack-org/micro/v3/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Buffer is ring buffer
|
// Buffer is ring buffer
|
||||||
@@ -28,8 +28,8 @@ type Stream struct {
|
|||||||
Entries chan *Entry
|
Entries chan *Entry
|
||||||
// Stop channel
|
// Stop channel
|
||||||
Stop chan bool
|
Stop chan bool
|
||||||
// Id of the stream
|
// ID of the stream
|
||||||
Id string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put adds a new value to ring buffer
|
// Put adds a new value to ring buffer
|
||||||
@@ -53,7 +53,7 @@ func (b *Buffer) Put(v interface{}) {
|
|||||||
for _, stream := range b.streams {
|
for _, stream := range b.streams {
|
||||||
select {
|
select {
|
||||||
case <-stream.Stop:
|
case <-stream.Stop:
|
||||||
delete(b.streams, stream.Id)
|
delete(b.streams, stream.ID)
|
||||||
close(stream.Entries)
|
close(stream.Entries)
|
||||||
case stream.Entries <- entry:
|
case stream.Entries <- entry:
|
||||||
}
|
}
|
||||||
@@ -112,11 +112,11 @@ func (b *Buffer) Stream() (<-chan *Entry, chan bool) {
|
|||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
entries := make(chan *Entry, 128)
|
entries := make(chan *Entry, 128)
|
||||||
id := uuid.New().String()
|
id := id.Must()
|
||||||
stop := make(chan bool)
|
stop := make(chan bool)
|
||||||
|
|
||||||
b.streams[id] = &Stream{
|
b.streams[id] = &Stream{
|
||||||
Id: id,
|
ID: id,
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
Stop: stop,
|
Stop: stop,
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2015, Gengo, Inc.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Gengo, Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from this
|
|
||||||
software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
||||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,110 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile.go
|
|
||||||
|
|
||||||
const (
|
|
||||||
opcodeVersion = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Template is a compiled representation of path templates.
|
|
||||||
type Template struct {
|
|
||||||
// Verb is a VERB part in the template
|
|
||||||
Verb string
|
|
||||||
// Original template (example: /v1/a_bit_of_everything)
|
|
||||||
Template string
|
|
||||||
// OpCodes is a sequence of operations
|
|
||||||
OpCodes []int
|
|
||||||
// Pool is a constant pool
|
|
||||||
Pool []string
|
|
||||||
// Fields is a list of field paths bound in this template
|
|
||||||
Fields []string
|
|
||||||
// Version is the version number of the format
|
|
||||||
Version int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compiler compiles utilities representation of path templates into marshallable operations.
|
|
||||||
// They can be unmarshalled by runtime.NewPattern.
|
|
||||||
type Compiler interface {
|
|
||||||
Compile() Template
|
|
||||||
}
|
|
||||||
|
|
||||||
type op struct {
|
|
||||||
str string
|
|
||||||
code OpCode
|
|
||||||
operand int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w wildcard) compile() []op {
|
|
||||||
return []op{
|
|
||||||
{code: OpPush},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w deepWildcard) compile() []op {
|
|
||||||
return []op{
|
|
||||||
{code: OpPushM},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l literal) compile() []op {
|
|
||||||
return []op{
|
|
||||||
{
|
|
||||||
code: OpLitPush,
|
|
||||||
str: string(l),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v variable) compile() []op {
|
|
||||||
ops := make([]op, 0, len(v.segments))
|
|
||||||
for _, s := range v.segments {
|
|
||||||
ops = append(ops, s.compile()...)
|
|
||||||
}
|
|
||||||
ops = append(ops, op{
|
|
||||||
code: OpConcatN,
|
|
||||||
operand: len(v.segments),
|
|
||||||
}, op{
|
|
||||||
code: OpCapture,
|
|
||||||
str: v.path,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ops
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t template) Compile() Template {
|
|
||||||
rawOps := make([]op, 0, len(t.segments))
|
|
||||||
for _, s := range t.segments {
|
|
||||||
rawOps = append(rawOps, s.compile()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ops := make([]int, 0, len(rawOps))
|
|
||||||
var (
|
|
||||||
ops []int
|
|
||||||
pool []string
|
|
||||||
fields []string
|
|
||||||
)
|
|
||||||
consts := make(map[string]int)
|
|
||||||
for _, op := range rawOps {
|
|
||||||
ops = append(ops, int(op.code))
|
|
||||||
if op.str == "" {
|
|
||||||
ops = append(ops, op.operand)
|
|
||||||
} else {
|
|
||||||
if _, ok := consts[op.str]; !ok {
|
|
||||||
consts[op.str] = len(pool)
|
|
||||||
pool = append(pool, op.str)
|
|
||||||
}
|
|
||||||
ops = append(ops, consts[op.str])
|
|
||||||
}
|
|
||||||
if op.code == OpCapture {
|
|
||||||
fields = append(fields, op.str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Template{
|
|
||||||
Version: opcodeVersion,
|
|
||||||
OpCodes: ops,
|
|
||||||
Pool: pool,
|
|
||||||
Verb: t.verb,
|
|
||||||
Fields: fields,
|
|
||||||
Template: t.template,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,122 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile_test.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
operandFiller = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCompile(t *testing.T) {
|
|
||||||
for _, spec := range []struct {
|
|
||||||
segs []segment
|
|
||||||
verb string
|
|
||||||
|
|
||||||
ops []int
|
|
||||||
pool []string
|
|
||||||
fields []string
|
|
||||||
}{
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
ops: []int{int(OpPush), operandFiller},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
deepWildcard{},
|
|
||||||
},
|
|
||||||
ops: []int{int(OpPushM), operandFiller},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
},
|
|
||||||
ops: []int{int(OpLitPush), 0},
|
|
||||||
pool: []string{"v1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
},
|
|
||||||
verb: "LOCK",
|
|
||||||
ops: []int{int(OpLitPush), 0},
|
|
||||||
pool: []string{"v1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
variable{
|
|
||||||
path: "name.nested",
|
|
||||||
segments: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ops: []int{
|
|
||||||
int(OpPush), operandFiller,
|
|
||||||
int(OpConcatN), 1,
|
|
||||||
int(OpCapture), 0,
|
|
||||||
},
|
|
||||||
pool: []string{"name.nested"},
|
|
||||||
fields: []string{"name.nested"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
segs: []segment{
|
|
||||||
literal("obj"),
|
|
||||||
variable{
|
|
||||||
path: "name.nested",
|
|
||||||
segments: []segment{
|
|
||||||
literal("a"),
|
|
||||||
wildcard{},
|
|
||||||
literal("b"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variable{
|
|
||||||
path: "obj",
|
|
||||||
segments: []segment{
|
|
||||||
deepWildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ops: []int{
|
|
||||||
int(OpLitPush), 0,
|
|
||||||
int(OpLitPush), 1,
|
|
||||||
int(OpPush), operandFiller,
|
|
||||||
int(OpLitPush), 2,
|
|
||||||
int(OpConcatN), 3,
|
|
||||||
int(OpCapture), 3,
|
|
||||||
int(OpPushM), operandFiller,
|
|
||||||
int(OpConcatN), 1,
|
|
||||||
int(OpCapture), 0,
|
|
||||||
},
|
|
||||||
pool: []string{"obj", "a", "b", "name.nested"},
|
|
||||||
fields: []string{"name.nested", "obj"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
tmpl := template{
|
|
||||||
segments: spec.segs,
|
|
||||||
verb: spec.verb,
|
|
||||||
}
|
|
||||||
compiled := tmpl.Compile()
|
|
||||||
if got, want := compiled.Version, opcodeVersion; got != want {
|
|
||||||
t.Errorf("tmpl.Compile().Version = %d; want %d; segs=%#v, verb=%q", got, want, spec.segs, spec.verb)
|
|
||||||
}
|
|
||||||
if got, want := compiled.OpCodes, spec.ops; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("tmpl.Compile().OpCodes = %v; want %v; segs=%#v, verb=%q", got, want, spec.segs, spec.verb)
|
|
||||||
}
|
|
||||||
if got, want := compiled.Pool, spec.pool; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("tmpl.Compile().Pool = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb)
|
|
||||||
}
|
|
||||||
if got, want := compiled.Verb, spec.verb; got != want {
|
|
||||||
t.Errorf("tmpl.Compile().Verb = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb)
|
|
||||||
}
|
|
||||||
if got, want := compiled.Fields, spec.fields; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("tmpl.Compile().Fields = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,363 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InvalidTemplateError indicates that the path template is not valid.
|
|
||||||
type InvalidTemplateError struct {
|
|
||||||
tmpl string
|
|
||||||
msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e InvalidTemplateError) Error() string {
|
|
||||||
return fmt.Sprintf("%s: %s", e.msg, e.tmpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses the string representation of path template
|
|
||||||
func Parse(tmpl string) (Compiler, error) {
|
|
||||||
if !strings.HasPrefix(tmpl, "/") {
|
|
||||||
return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"}
|
|
||||||
}
|
|
||||||
tokens, verb := tokenize(tmpl[1:])
|
|
||||||
|
|
||||||
p := parser{tokens: tokens}
|
|
||||||
segs, err := p.topLevelSegments()
|
|
||||||
if err != nil {
|
|
||||||
return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()}
|
|
||||||
}
|
|
||||||
|
|
||||||
return template{
|
|
||||||
segments: segs,
|
|
||||||
verb: verb,
|
|
||||||
template: tmpl,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenize(path string) (tokens []string, verb string) {
|
|
||||||
if path == "" {
|
|
||||||
return []string{eof}, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
init = iota
|
|
||||||
field
|
|
||||||
nested
|
|
||||||
)
|
|
||||||
st := init
|
|
||||||
for path != "" {
|
|
||||||
var idx int
|
|
||||||
switch st {
|
|
||||||
case init:
|
|
||||||
idx = strings.IndexAny(path, "/{")
|
|
||||||
case field:
|
|
||||||
idx = strings.IndexAny(path, ".=}")
|
|
||||||
case nested:
|
|
||||||
idx = strings.IndexAny(path, "/}")
|
|
||||||
}
|
|
||||||
if idx < 0 {
|
|
||||||
tokens = append(tokens, path)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch r := path[idx]; r {
|
|
||||||
case '/', '.':
|
|
||||||
case '{':
|
|
||||||
st = field
|
|
||||||
case '=':
|
|
||||||
st = nested
|
|
||||||
case '}':
|
|
||||||
st = init
|
|
||||||
}
|
|
||||||
if idx == 0 {
|
|
||||||
tokens = append(tokens, path[idx:idx+1])
|
|
||||||
} else {
|
|
||||||
tokens = append(tokens, path[:idx], path[idx:idx+1])
|
|
||||||
}
|
|
||||||
path = path[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
l := len(tokens)
|
|
||||||
t := tokens[l-1]
|
|
||||||
if idx := strings.LastIndex(t, ":"); idx == 0 {
|
|
||||||
tokens, verb = tokens[:l-1], t[1:]
|
|
||||||
} else if idx > 0 {
|
|
||||||
tokens[l-1], verb = t[:idx], t[idx+1:]
|
|
||||||
}
|
|
||||||
tokens = append(tokens, eof)
|
|
||||||
return tokens, verb
|
|
||||||
}
|
|
||||||
|
|
||||||
// parser is a parser of the template syntax defined in github.com/googleapis/googleapis/google/api/http.proto.
|
|
||||||
type parser struct {
|
|
||||||
tokens []string
|
|
||||||
accepted []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// topLevelSegments is the target of this parser.
|
|
||||||
func (p *parser) topLevelSegments() ([]segment, error) {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Debug(context.TODO(), "Parsing %q", p.tokens)
|
|
||||||
}
|
|
||||||
segs, err := p.segments()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "accept segments: %q; %q", p.accepted, p.tokens)
|
|
||||||
}
|
|
||||||
if _, err := p.accept(typeEOF); err != nil {
|
|
||||||
return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, ""))
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "accept eof: %q; %q", p.accepted, p.tokens)
|
|
||||||
}
|
|
||||||
return segs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) segments() ([]segment, error) {
|
|
||||||
s, err := p.segment()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "accept segment: %q; %q", p.accepted, p.tokens)
|
|
||||||
}
|
|
||||||
segs := []segment{s}
|
|
||||||
for {
|
|
||||||
if _, err := p.accept("/"); err != nil {
|
|
||||||
return segs, nil
|
|
||||||
}
|
|
||||||
s, err := p.segment()
|
|
||||||
if err != nil {
|
|
||||||
return segs, err
|
|
||||||
}
|
|
||||||
segs = append(segs, s)
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "accept segment: %q; %q", p.accepted, p.tokens)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) segment() (segment, error) {
|
|
||||||
if _, err := p.accept("*"); err == nil {
|
|
||||||
return wildcard{}, nil
|
|
||||||
}
|
|
||||||
if _, err := p.accept("**"); err == nil {
|
|
||||||
return deepWildcard{}, nil
|
|
||||||
}
|
|
||||||
if l, err := p.literal(); err == nil {
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := p.variable()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("segment neither wildcards, literal or variable: %v", err)
|
|
||||||
}
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) literal() (segment, error) {
|
|
||||||
lit, err := p.accept(typeLiteral)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return literal(lit), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) variable() (segment, error) {
|
|
||||||
if _, err := p.accept("{"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := p.fieldPath()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var segs []segment
|
|
||||||
if _, err := p.accept("="); err == nil {
|
|
||||||
segs, err = p.segments()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid segment in variable %q: %v", path, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
segs = []segment{wildcard{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := p.accept("}"); err != nil {
|
|
||||||
return nil, fmt.Errorf("unterminated variable segment: %s", path)
|
|
||||||
}
|
|
||||||
return variable{
|
|
||||||
path: path,
|
|
||||||
segments: segs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) fieldPath() (string, error) {
|
|
||||||
c, err := p.accept(typeIdent)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
components := []string{c}
|
|
||||||
for {
|
|
||||||
if _, err = p.accept("."); err != nil {
|
|
||||||
return strings.Join(components, "."), nil
|
|
||||||
}
|
|
||||||
c, err := p.accept(typeIdent)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid field path component: %v", err)
|
|
||||||
}
|
|
||||||
components = append(components, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A termType is a type of terminal symbols.
|
|
||||||
type termType string
|
|
||||||
|
|
||||||
// These constants define some of valid values of termType.
|
|
||||||
// They improve readability of parse functions.
|
|
||||||
//
|
|
||||||
// You can also use "/", "*", "**", "." or "=" as valid values.
|
|
||||||
const (
|
|
||||||
typeIdent = termType("ident")
|
|
||||||
typeLiteral = termType("literal")
|
|
||||||
typeEOF = termType("$")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// eof is the terminal symbol which always appears at the end of token sequence.
|
|
||||||
eof = "\u0000"
|
|
||||||
)
|
|
||||||
|
|
||||||
// accept tries to accept a token in "p".
|
|
||||||
// This function consumes a token and returns it if it matches to the specified "term".
|
|
||||||
// If it doesn't match, the function does not consume any tokens and return an error.
|
|
||||||
func (p *parser) accept(term termType) (string, error) {
|
|
||||||
t := p.tokens[0]
|
|
||||||
switch term {
|
|
||||||
case "/", "*", "**", ".", "=", "{", "}":
|
|
||||||
if t != string(term) && t != "/" {
|
|
||||||
return "", fmt.Errorf("expected %q but got %q", term, t)
|
|
||||||
}
|
|
||||||
case typeEOF:
|
|
||||||
if t != eof {
|
|
||||||
return "", fmt.Errorf("expected EOF but got %q", t)
|
|
||||||
}
|
|
||||||
case typeIdent:
|
|
||||||
if err := expectIdent(t); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
case typeLiteral:
|
|
||||||
if err := expectPChars(t); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unknown termType %q", term)
|
|
||||||
}
|
|
||||||
p.tokens = p.tokens[1:]
|
|
||||||
p.accepted = append(p.accepted, t)
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// expectPChars determines if "t" consists of only pchars defined in RFC3986.
|
|
||||||
//
|
|
||||||
// https://www.ietf.org/rfc/rfc3986.txt, P.49
|
|
||||||
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
|
||||||
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
|
||||||
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
|
||||||
// / "*" / "+" / "," / ";" / "="
|
|
||||||
// pct-encoded = "%" HEXDIG HEXDIG
|
|
||||||
//nolint:gocyclo
|
|
||||||
func expectPChars(t string) error {
|
|
||||||
const (
|
|
||||||
init = iota
|
|
||||||
pct1
|
|
||||||
pct2
|
|
||||||
)
|
|
||||||
st := init
|
|
||||||
for _, r := range t {
|
|
||||||
if st != init {
|
|
||||||
if !isHexDigit(r) {
|
|
||||||
return fmt.Errorf("invalid hexdigit: %c(%U)", r, r)
|
|
||||||
}
|
|
||||||
switch st {
|
|
||||||
case pct1:
|
|
||||||
st = pct2
|
|
||||||
case pct2:
|
|
||||||
st = init
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// unreserved
|
|
||||||
switch {
|
|
||||||
case 'A' <= r && r <= 'Z':
|
|
||||||
continue
|
|
||||||
case 'a' <= r && r <= 'z':
|
|
||||||
continue
|
|
||||||
case '0' <= r && r <= '9':
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch r {
|
|
||||||
case '-', '.', '_', '~':
|
|
||||||
// unreserved
|
|
||||||
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=':
|
|
||||||
// sub-delims
|
|
||||||
case ':', '@':
|
|
||||||
// rest of pchar
|
|
||||||
case '%':
|
|
||||||
// pct-encoded
|
|
||||||
st = pct1
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid character in path segment: %q(%U)", r, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if st != init {
|
|
||||||
return fmt.Errorf("invalid percent-encoding in %q", t)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*).
|
|
||||||
func expectIdent(ident string) error {
|
|
||||||
if ident == "" {
|
|
||||||
return fmt.Errorf("empty identifier")
|
|
||||||
}
|
|
||||||
for pos, r := range ident {
|
|
||||||
switch {
|
|
||||||
case '0' <= r && r <= '9':
|
|
||||||
if pos == 0 {
|
|
||||||
return fmt.Errorf("identifier starting with digit: %s", ident)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
case 'A' <= r && r <= 'Z':
|
|
||||||
continue
|
|
||||||
case 'a' <= r && r <= 'z':
|
|
||||||
continue
|
|
||||||
case r == '_':
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHexDigit(r rune) bool {
|
|
||||||
switch {
|
|
||||||
case '0' <= r && r <= '9':
|
|
||||||
return true
|
|
||||||
case 'A' <= r && r <= 'F':
|
|
||||||
return true
|
|
||||||
case 'a' <= r && r <= 'f':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@@ -1,323 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse_test.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTokenize(t *testing.T) {
|
|
||||||
for _, spec := range []struct {
|
|
||||||
src string
|
|
||||||
tokens []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
src: "",
|
|
||||||
tokens: []string{eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1",
|
|
||||||
tokens: []string{"v1", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/b",
|
|
||||||
tokens: []string{"v1", "/", "b", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/endpoint/*",
|
|
||||||
tokens: []string{"v1", "/", "endpoint", "/", "*", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/endpoint/**",
|
|
||||||
tokens: []string{"v1", "/", "endpoint", "/", "**", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/b/{bucket_name=*}",
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"b", "/",
|
|
||||||
"{", "bucket_name", "=", "*", "}",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/b/{bucket_name=buckets/*}",
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"b", "/",
|
|
||||||
"{", "bucket_name", "=", "buckets", "/", "*", "}",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/b/{bucket_name=buckets/*}/o",
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"b", "/",
|
|
||||||
"{", "bucket_name", "=", "buckets", "/", "*", "}", "/",
|
|
||||||
"o",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/b/{bucket_name=buckets/*}/o/{name}",
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"b", "/",
|
|
||||||
"{", "bucket_name", "=", "buckets", "/", "*", "}", "/",
|
|
||||||
"o", "/", "{", "name", "}",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "v1/a=b&c=d;e=f:g/endpoint.rdf",
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"a=b&c=d;e=f:g", "/",
|
|
||||||
"endpoint.rdf",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
tokens, verb := tokenize(spec.src)
|
|
||||||
if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want)
|
|
||||||
}
|
|
||||||
if got, want := verb, ""; got != want {
|
|
||||||
t.Errorf("tokenize(%q) = _, %q; want _, %q", spec.src, got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
src := fmt.Sprintf("%s:%s", spec.src, "LOCK")
|
|
||||||
tokens, verb = tokenize(src)
|
|
||||||
if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("tokenize(%q) = %q, _; want %q, _", src, got, want)
|
|
||||||
}
|
|
||||||
if got, want := verb, "LOCK"; got != want {
|
|
||||||
t.Errorf("tokenize(%q) = _, %q; want _, %q", src, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSegments(t *testing.T) {
|
|
||||||
flag.Set("v", "3")
|
|
||||||
for _, spec := range []struct {
|
|
||||||
tokens []string
|
|
||||||
want []segment
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
tokens: []string{"v1", eof},
|
|
||||||
want: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"/", eof},
|
|
||||||
want: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"-._~!$&'()*+,;=:@", eof},
|
|
||||||
want: []segment{
|
|
||||||
literal("-._~!$&'()*+,;=:@"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"%e7%ac%ac%e4%b8%80%e7%89%88", eof},
|
|
||||||
want: []segment{
|
|
||||||
literal("%e7%ac%ac%e4%b8%80%e7%89%88"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"v1", "/", "*", eof},
|
|
||||||
want: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"v1", "/", "**", eof},
|
|
||||||
want: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
deepWildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"{", "name", "}", eof},
|
|
||||||
want: []segment{
|
|
||||||
variable{
|
|
||||||
path: "name",
|
|
||||||
segments: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"{", "name", "=", "*", "}", eof},
|
|
||||||
want: []segment{
|
|
||||||
variable{
|
|
||||||
path: "name",
|
|
||||||
segments: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"{", "field", ".", "nested", ".", "nested2", "=", "*", "}", eof},
|
|
||||||
want: []segment{
|
|
||||||
variable{
|
|
||||||
path: "field.nested.nested2",
|
|
||||||
segments: []segment{
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{"{", "name", "=", "a", "/", "b", "/", "*", "}", eof},
|
|
||||||
want: []segment{
|
|
||||||
variable{
|
|
||||||
path: "name",
|
|
||||||
segments: []segment{
|
|
||||||
literal("a"),
|
|
||||||
literal("b"),
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokens: []string{
|
|
||||||
"v1", "/",
|
|
||||||
"{",
|
|
||||||
"name", ".", "nested", ".", "nested2",
|
|
||||||
"=",
|
|
||||||
"a", "/", "b", "/", "*",
|
|
||||||
"}", "/",
|
|
||||||
"o", "/",
|
|
||||||
"{",
|
|
||||||
"another_name",
|
|
||||||
"=",
|
|
||||||
"a", "/", "b", "/", "*", "/", "c",
|
|
||||||
"}", "/",
|
|
||||||
"**",
|
|
||||||
eof,
|
|
||||||
},
|
|
||||||
want: []segment{
|
|
||||||
literal("v1"),
|
|
||||||
variable{
|
|
||||||
path: "name.nested.nested2",
|
|
||||||
segments: []segment{
|
|
||||||
literal("a"),
|
|
||||||
literal("b"),
|
|
||||||
wildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
literal("o"),
|
|
||||||
variable{
|
|
||||||
path: "another_name",
|
|
||||||
segments: []segment{
|
|
||||||
literal("a"),
|
|
||||||
literal("b"),
|
|
||||||
wildcard{},
|
|
||||||
literal("c"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deepWildcard{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
p := parser{tokens: spec.tokens}
|
|
||||||
segs, err := p.topLevelSegments()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("parser{%q}.segments() failed with %v; want success", spec.tokens, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if got, want := segs, spec.want; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("parser{%q}.segments() = %#v; want %#v", spec.tokens, got, want)
|
|
||||||
}
|
|
||||||
if got := p.tokens; len(got) > 0 {
|
|
||||||
t.Errorf("p.tokens = %q; want []; spec.tokens=%q", got, spec.tokens)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSegmentsWithErrors(t *testing.T) {
|
|
||||||
flag.Set("v", "3")
|
|
||||||
for _, spec := range []struct {
|
|
||||||
tokens []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// double slash
|
|
||||||
tokens: []string{"//", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// invalid literal
|
|
||||||
tokens: []string{"a?b", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// invalid percent-encoding
|
|
||||||
tokens: []string{"%", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// invalid percent-encoding
|
|
||||||
tokens: []string{"%2", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// invalid percent-encoding
|
|
||||||
tokens: []string{"a%2z", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// empty segments
|
|
||||||
tokens: []string{eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// unterminated variable
|
|
||||||
tokens: []string{"{", "name", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// unterminated variable
|
|
||||||
tokens: []string{"{", "name", "=", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// unterminated variable
|
|
||||||
tokens: []string{"{", "name", "=", "*", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// empty component in field path
|
|
||||||
tokens: []string{"{", "name", ".", "}", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// empty component in field path
|
|
||||||
tokens: []string{"{", "name", ".", ".", "nested", "}", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// invalid character in identifier
|
|
||||||
tokens: []string{"{", "field-name", "}", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// no slash between segments
|
|
||||||
tokens: []string{"v1", "endpoint", eof},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// no slash between segments
|
|
||||||
tokens: []string{"v1", "{", "name", "}", eof},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
p := parser{tokens: spec.tokens}
|
|
||||||
segs, err := p.topLevelSegments()
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("parser{%q}.segments() succeeded; want InvalidTemplateError; accepted %#v", spec.tokens, segs)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Info(context.TODO(), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/utilities/pattern.go
|
|
||||||
|
|
||||||
// An OpCode is a opcode of compiled path patterns.
|
|
||||||
type OpCode int
|
|
||||||
|
|
||||||
// These constants are the valid values of OpCode.
|
|
||||||
const (
|
|
||||||
// OpNop does nothing
|
|
||||||
OpNop = OpCode(iota)
|
|
||||||
// OpPush pushes a component to stack
|
|
||||||
OpPush
|
|
||||||
// OpLitPush pushes a component to stack if it matches to the literal
|
|
||||||
OpLitPush
|
|
||||||
// OpPushM concatenates the remaining components and pushes it to stack
|
|
||||||
OpPushM
|
|
||||||
// OpConcatN pops N items from stack, concatenates them and pushes it back to stack
|
|
||||||
OpConcatN
|
|
||||||
// OpCapture pops an item and binds it to the variable
|
|
||||||
OpCapture
|
|
||||||
// OpEnd is the least positive invalid opcode.
|
|
||||||
OpEnd
|
|
||||||
)
|
|
@@ -1,32 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/unistack-org/micro/v3/register"
|
|
||||||
"github.com/unistack-org/micro/v3/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type apiRouter struct {
|
|
||||||
router.Router
|
|
||||||
routes []router.Route
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *apiRouter) Lookup(...router.QueryOption) ([]router.Route, error) {
|
|
||||||
return r.routes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *apiRouter) String() string {
|
|
||||||
return "api"
|
|
||||||
}
|
|
||||||
|
|
||||||
// New router is a hack for API routing
|
|
||||||
func New(srvs []*register.Service) router.Router {
|
|
||||||
var routes []router.Route
|
|
||||||
|
|
||||||
for _, srv := range srvs {
|
|
||||||
for _, n := range srv.Nodes {
|
|
||||||
routes = append(routes, router.Route{Address: n.Address, Metadata: n.Metadata})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiRouter{routes: routes}
|
|
||||||
}
|
|
@@ -1,284 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/runtime/pattern.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNotMatch indicates that the given HTTP request path does not match to the pattern.
|
|
||||||
ErrNotMatch = errors.New("not match to the path pattern")
|
|
||||||
// ErrInvalidPattern indicates that the given definition of Pattern is not valid.
|
|
||||||
ErrInvalidPattern = errors.New("invalid pattern")
|
|
||||||
)
|
|
||||||
|
|
||||||
type rop struct {
|
|
||||||
code OpCode
|
|
||||||
operand int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern is a template pattern of http request paths defined in github.com/googleapis/googleapis/google/api/http.proto.
|
|
||||||
type Pattern struct {
|
|
||||||
verb string
|
|
||||||
// ops is a list of operations
|
|
||||||
ops []rop
|
|
||||||
// pool is a constant pool indexed by the operands or vars
|
|
||||||
pool []string
|
|
||||||
// vars is a list of variables names to be bound by this pattern
|
|
||||||
vars []string
|
|
||||||
// stacksize is the max depth of the stack
|
|
||||||
stacksize int
|
|
||||||
// tailLen is the length of the fixed-size segments after a deep wildcard
|
|
||||||
tailLen int
|
|
||||||
// assumeColonVerb indicates whether a path suffix after a final
|
|
||||||
// colon may only be interpreted as a verb.
|
|
||||||
assumeColonVerb bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type patternOptions struct {
|
|
||||||
assumeColonVerb bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// PatternOpt is an option for creating Patterns.
|
|
||||||
type PatternOpt func(*patternOptions)
|
|
||||||
|
|
||||||
// NewPattern returns a new Pattern from the given definition values.
|
|
||||||
// "ops" is a sequence of op codes. "pool" is a constant pool.
|
|
||||||
// "verb" is the verb part of the pattern. It is empty if the pattern does not have the part.
|
|
||||||
// "version" must be 1 for now.
|
|
||||||
// It returns an error if the given definition is invalid.
|
|
||||||
//nolint:gocyclo
|
|
||||||
func NewPattern(version int, ops []int, pool []string, verb string, opts ...PatternOpt) (Pattern, error) {
|
|
||||||
options := patternOptions{
|
|
||||||
assumeColonVerb: true,
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
if version != 1 {
|
|
||||||
if logger.V(logger.DebugLevel) {
|
|
||||||
logger.Debug(context.TODO(), "unsupported version: %d", version)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
l := len(ops)
|
|
||||||
if l%2 != 0 {
|
|
||||||
if logger.V(logger.DebugLevel) {
|
|
||||||
logger.Debug(context.TODO(), "odd number of ops codes: %d", l)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
typedOps []rop
|
|
||||||
stack, maxstack int
|
|
||||||
tailLen int
|
|
||||||
pushMSeen bool
|
|
||||||
vars []string
|
|
||||||
)
|
|
||||||
for i := 0; i < l; i += 2 {
|
|
||||||
op := rop{code: OpCode(ops[i]), operand: ops[i+1]}
|
|
||||||
switch op.code {
|
|
||||||
case OpNop:
|
|
||||||
continue
|
|
||||||
case OpPush:
|
|
||||||
if pushMSeen {
|
|
||||||
tailLen++
|
|
||||||
}
|
|
||||||
stack++
|
|
||||||
case OpPushM:
|
|
||||||
if pushMSeen {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "pushM appears twice")
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
pushMSeen = true
|
|
||||||
stack++
|
|
||||||
case OpLitPush:
|
|
||||||
if op.operand < 0 || len(pool) <= op.operand {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "negative literal index: %d", op.operand)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
if pushMSeen {
|
|
||||||
tailLen++
|
|
||||||
}
|
|
||||||
stack++
|
|
||||||
case OpConcatN:
|
|
||||||
if op.operand <= 0 {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "negative concat size: %d", op.operand)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
stack -= op.operand
|
|
||||||
if stack < 0 {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "stack underflow")
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
stack++
|
|
||||||
case OpCapture:
|
|
||||||
if op.operand < 0 || len(pool) <= op.operand {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Trace(context.TODO(), "variable name index out of bound: %d", op.operand)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
v := pool[op.operand]
|
|
||||||
op.operand = len(vars)
|
|
||||||
vars = append(vars, v)
|
|
||||||
stack--
|
|
||||||
if stack < 0 {
|
|
||||||
if logger.V(logger.DebugLevel) {
|
|
||||||
logger.Trace(context.TODO(), "stack underflow")
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if logger.V(logger.DebugLevel) {
|
|
||||||
logger.Trace(context.TODO(), "invalid opcode: %d", op.code)
|
|
||||||
}
|
|
||||||
return Pattern{}, ErrInvalidPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxstack < stack {
|
|
||||||
maxstack = stack
|
|
||||||
}
|
|
||||||
typedOps = append(typedOps, op)
|
|
||||||
}
|
|
||||||
return Pattern{
|
|
||||||
ops: typedOps,
|
|
||||||
pool: pool,
|
|
||||||
vars: vars,
|
|
||||||
stacksize: maxstack,
|
|
||||||
tailLen: tailLen,
|
|
||||||
verb: verb,
|
|
||||||
assumeColonVerb: options.assumeColonVerb,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustPattern is a helper function which makes it easier to call NewPattern in variable initialization.
|
|
||||||
func MustPattern(p Pattern, err error) Pattern {
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.FatalLevel) {
|
|
||||||
logger.Fatal(context.TODO(), "Pattern initialization failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match examines components if it matches to the Pattern.
|
|
||||||
// If it matches, the function returns a mapping from field paths to their captured values.
|
|
||||||
// If otherwise, the function returns an error.
|
|
||||||
//nolint:gocyclo
|
|
||||||
func (p Pattern) Match(components []string, verb string) (map[string]string, error) {
|
|
||||||
if p.verb != verb {
|
|
||||||
if p.assumeColonVerb || p.verb != "" {
|
|
||||||
return nil, ErrNotMatch
|
|
||||||
}
|
|
||||||
if len(components) == 0 {
|
|
||||||
components = []string{":" + verb}
|
|
||||||
} else {
|
|
||||||
components = append([]string{}, components...)
|
|
||||||
components[len(components)-1] += ":" + verb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pos int
|
|
||||||
stack := make([]string, 0, p.stacksize)
|
|
||||||
captured := make([]string, len(p.vars))
|
|
||||||
l := len(components)
|
|
||||||
for _, op := range p.ops {
|
|
||||||
switch op.code {
|
|
||||||
case OpNop:
|
|
||||||
continue
|
|
||||||
case OpPush, OpLitPush:
|
|
||||||
if pos >= l {
|
|
||||||
return nil, ErrNotMatch
|
|
||||||
}
|
|
||||||
c := components[pos]
|
|
||||||
if op.code == OpLitPush {
|
|
||||||
if lit := p.pool[op.operand]; c != lit {
|
|
||||||
return nil, ErrNotMatch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stack = append(stack, c)
|
|
||||||
pos++
|
|
||||||
case OpPushM:
|
|
||||||
end := len(components)
|
|
||||||
if end < pos+p.tailLen {
|
|
||||||
return nil, ErrNotMatch
|
|
||||||
}
|
|
||||||
end -= p.tailLen
|
|
||||||
stack = append(stack, strings.Join(components[pos:end], "/"))
|
|
||||||
pos = end
|
|
||||||
case OpConcatN:
|
|
||||||
n := op.operand
|
|
||||||
l := len(stack) - n
|
|
||||||
stack = append(stack[:l], strings.Join(stack[l:], "/"))
|
|
||||||
case OpCapture:
|
|
||||||
n := len(stack) - 1
|
|
||||||
captured[op.operand] = stack[n]
|
|
||||||
stack = stack[:n]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pos < l {
|
|
||||||
return nil, ErrNotMatch
|
|
||||||
}
|
|
||||||
bindings := make(map[string]string, len(captured))
|
|
||||||
for i, val := range captured {
|
|
||||||
bindings[p.vars[i]] = val
|
|
||||||
}
|
|
||||||
return bindings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verb returns the verb part of the Pattern.
|
|
||||||
func (p Pattern) Verb() string { return p.verb }
|
|
||||||
|
|
||||||
func (p Pattern) String() string {
|
|
||||||
var stack []string
|
|
||||||
for _, op := range p.ops {
|
|
||||||
switch op.code {
|
|
||||||
case OpNop:
|
|
||||||
continue
|
|
||||||
case OpPush:
|
|
||||||
stack = append(stack, "*")
|
|
||||||
case OpLitPush:
|
|
||||||
stack = append(stack, p.pool[op.operand])
|
|
||||||
case OpPushM:
|
|
||||||
stack = append(stack, "**")
|
|
||||||
case OpConcatN:
|
|
||||||
n := op.operand
|
|
||||||
l := len(stack) - n
|
|
||||||
stack = append(stack[:l], strings.Join(stack[l:], "/"))
|
|
||||||
case OpCapture:
|
|
||||||
n := len(stack) - 1
|
|
||||||
stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segs := strings.Join(stack, "/")
|
|
||||||
if p.verb != "" {
|
|
||||||
return fmt.Sprintf("/%s:%s", segs, p.verb)
|
|
||||||
}
|
|
||||||
return "/" + segs
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssumeColonVerbOpt indicates whether a path suffix after a final
|
|
||||||
// colon may only be interpreted as a verb.
|
|
||||||
func AssumeColonVerbOpt(val bool) PatternOpt {
|
|
||||||
return PatternOpt(func(o *patternOptions) {
|
|
||||||
o.assumeColonVerb = val
|
|
||||||
})
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user