Compare commits

..

No commits in common. "master" and "v3.5.9" have entirely different histories.

323 changed files with 13551 additions and 12931 deletions

View File

@ -1,18 +0,0 @@
---
name: Bug report
about: For reporting bugs in micro
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
1. What are you trying to do?
2. What did you expect to happen?
3. What happens instead?
**How to reproduce the bug:**
If possible, please include a minimal code snippet here.

View File

@ -1,17 +0,0 @@
---
name: Feature request / Enhancement
about: If you have a need not served by micro
title: "[FEATURE]"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,8 +0,0 @@
---
name: Question
about: Ask a question about micro
title: ''
labels: ''
assignees: ''
---

View File

@ -1,28 +0,0 @@
name: "autoapprove"
on:
pull_request_target:
types: [assigned, opened, synchronize, reopened]
workflow_run:
workflows: ["prbuild"]
types:
- completed
permissions:
pull-requests: write
contents: write
jobs:
autoapprove:
runs-on: ubuntu-latest
steps:
- name: approve
run: [ "curl -o tea https://dl.gitea.com/tea/main/tea-main-linux-amd64",
"chmod +x ./tea",
"./tea login add --name unistack --token ${{ secrets.GITHUB_TOKEN }} --url https://git.unistack.org",
"./tea pr --repo ${{ github.event.repository.name }}"
]
if: github.actor == 'vtolstov'
id: approve
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,3 +0,0 @@
branches:
- master
- v3

View File

@ -1,24 +0,0 @@
on:
push:
branches:
- 'main'
- 'master'
- 'v3'
schedule:
#- cron: '* * * * *'
- cron: '@hourly'
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- name: setup-go
uses: https://gitea.com/actions/setup-go@v3
with:
go-version: 1.21
- name: checkout
uses: https://gitea.com/actions/checkout@v3
- name: get pkgdashcli
run: GOPROXY=direct GONOSUMDB="git.unistack.org/*" GONOPROXY="git.unistack.org/*" GOBIN=/bin go install git.unistack.org/unistack-org/pkgdash/cmd/pkgdashcli@latest
- name: pkgdashcli check
run: /bin/pkgdashcli check

View File

@ -1,30 +0,0 @@
name: Go
on:
push:
branches: [ master, v3 ]
pull_request:
branches: [ master, v3 ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup
uses: actions/setup-go@v4
with:
go-version: stable
- name: coverage
run: go test -v -coverprofile coverage.out ./...
- name: badge
uses: ncruces/go-coverage-report@main
with:
coverage-file: coverage.out
reuse-go: true
amend: true

View File

@ -1,24 +0,0 @@
name: lint
on:
pull_request:
branches:
- master
- v3
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- name: setup-go
uses: https://gitea.com/actions/setup-go@v3
with:
go-version: 1.21
- name: checkout
uses: https://gitea.com/actions/checkout@v3
- name: deps
run: go get -v -d ./...
- name: lint
uses: https://github.com/golangci/golangci-lint-action@v3.4.0
continue-on-error: true
with:
version: v1.52

View File

@ -1,23 +0,0 @@
name: pr
on:
pull_request:
branches:
- master
- v3
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: checkout
uses: https://gitea.com/actions/checkout@v3
- name: setup-go
uses: https://gitea.com/actions/setup-go@v3
with:
go-version: 1.21
- name: deps
run: go get -v -t -d ./...
- name: test
env:
INTEGRATION_TESTS: yes
run: go test -v -mod readonly -race -coverprofile=coverage.txt -covermode=atomic ./...

View File

@ -1,9 +0,0 @@
## Pull Request template
Please, go through these steps before clicking submit on this PR.
1. Give a descriptive title to your PR.
2. Provide a description of your changes.
3. Make sure you have some relevant tests.
4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).
**PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**

View File

@ -1,24 +0,0 @@
name: "autoapprove"
on:
pull_request_target:
types: [assigned, opened, synchronize, reopened]
workflow_run:
workflows: ["prbuild"]
types:
- completed
permissions:
pull-requests: write
contents: write
jobs:
autoapprove:
runs-on: ubuntu-latest
steps:
- name: approve
uses: hmarr/auto-approve-action@v3
if: github.actor == 'vtolstov' || github.actor == 'dependabot[bot]'
id: approve
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,21 +0,0 @@
name: "automerge"
on:
pull_request_target:
types: [assigned, opened, synchronize, reopened]
permissions:
pull-requests: write
contents: write
jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'vtolstov'
steps:
- name: merge
id: merge
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.TOKEN}}

View File

@ -1,39 +0,0 @@
name: "codecov"
on:
workflow_run:
workflows: ["build"]
types:
- completed
push:
branches: [ v3 ]
pull_request:
branches: [ v3 ]
schedule:
- cron: '34 1 * * 0'
jobs:
codecov:
name: codecov
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: checkout
uses: actions/checkout@v3
- name: setup
uses: actions/setup-go@v3
with:
go-version: 1.17
- name: Run coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
- name: codecov
uses: codecov/codecov-action@v3.1.1

View File

@ -1,78 +0,0 @@
# 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, v3 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master, v3 ]
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
uses: actions/checkout@v3
- name: setup
uses: actions/setup-go@v3
with:
go-version: 1.17
# Initializes the CodeQL tools for scanning.
- name: init
uses: github/codeql-action/init@v2
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@v2
# 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: analyze
uses: github/codeql-action/analyze@v2

View File

@ -1,27 +0,0 @@
name: "dependabot-automerge"
on:
pull_request_target:
types: [assigned, opened, synchronize, reopened]
permissions:
pull-requests: write
contents: write
jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: metadata
id: metadata
uses: dependabot/fetch-metadata@v1.3.6
with:
github-token: "${{ secrets.TOKEN }}"
- name: merge
id: merge
if: ${{contains(steps.metadata.outputs.dependency-names, 'go.unistack.org')}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.TOKEN}}

View File

@ -1,40 +0,0 @@
name: prbuild
on:
pull_request:
branches:
- master
- v3
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: setup
uses: actions/setup-go@v3
with:
go-version: 1.17
- name: checkout
uses: actions/checkout@v3
- name: cache
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: deps
run: go get -v -t -d ./...
- name: test
env:
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
lint:
name: lint
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: lint
uses: golangci/golangci-lint-action@v3.4.0
continue-on-error: true
with:
version: v1.30

View File

@ -11,16 +11,9 @@ 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"

View File

@ -3,42 +3,57 @@ on:
push: push:
branches: branches:
- master - master
- v3
jobs: jobs:
test: test:
name: test name: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: setup - name: setup
uses: actions/setup-go@v3 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.16
- name: checkout
uses: actions/checkout@v3
- name: cache - name: cache
uses: actions/cache@v3 uses: actions/cache@v2
with: with:
path: ~/go/pkg/mod path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go- restore-keys: ${{ runner.os }}-go-
- name: deps - name: sdk checkout
uses: actions/checkout@v2
- name: sdk deps
run: go get -v -t -d ./... run: go get -v -t -d ./...
- name: test - name: sdk test
env: env:
INTEGRATION_TESTS: yes INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./... run: go test -mod readonly -v ./...
- name: tests checkout
uses: actions/checkout@v2
with:
repository: unistack-org/micro-tests
ref: refs/heads/master
path: micro-tests
fetch-depth: 1
- name: tests deps
run: |
cd micro-tests
go mod edit -replace="github.com/unistack-org/micro/v3=../"
go get -v -t -d ./...
- name: tests test
env:
INTEGRATION_TESTS: yes
run: cd micro-tests && go test -mod readonly -v ./...
lint: lint:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: lint - name: lint
uses: golangci/golangci-lint-action@v3.4.0 uses: golangci/golangci-lint-action@v2
continue-on-error: true continue-on-error: true
with: with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.30 version: v1.39
# Optional: working directory, useful for monorepos # Optional: working directory, useful for monorepos
# working-directory: somedir # working-directory: somedir
# Optional: golangci-lint command line arguments. # Optional: golangci-lint command line arguments.

62
.github/workflows/pr.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: prbuild
on:
pull_request:
branches:
- master
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: cache
uses: actions/cache@v2
with:
path: ~/go/pkg
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: sdk checkout
uses: actions/checkout@v2
- name: sdk deps
run: go get -v -t -d ./...
- name: sdk test
env:
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
- name: tests checkout
uses: actions/checkout@v2
with:
repository: unistack-org/micro-tests
ref: refs/heads/master
path: micro-tests
fetch-depth: 1
- name: tests deps
run: |
cd micro-tests
go mod edit -replace="github.com/unistack-org/micro/v3=../"
go get -v -t -d ./...
- name: tests test
env:
INTEGRATION_TESTS: yes
run: cd micro-tests && go test -mod readonly -v ./...
lint:
name: lint
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: lint
uses: golangci/golangci-lint-action@v2
continue-on-error: true
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.39
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

3
.gitignore vendored
View File

@ -1,8 +1,6 @@
# Develop tools # Develop tools
/.vscode/ /.vscode/
/.idea/ /.idea/
.idea
.vscode
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
@ -15,7 +13,6 @@
_obj _obj
_test _test
_build _build
.DS_Store
# Architecture specific extensions/prefixes # Architecture specific extensions/prefixes
*.[568vq] *.[568vq]

View File

@ -1,5 +1,6 @@
run: run:
concurrency: 4 concurrency: 4
deadline: 5m
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
@ -12,13 +13,15 @@ linters-settings:
linters: linters:
enable: enable:
- govet - govet
- deadcode
- errcheck - errcheck
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- spancheck - varcheck
- bodyclose - bodyclose
- gci - gci
- goconst - goconst
@ -27,7 +30,7 @@ linters:
- gofmt - gofmt
- gofumpt - gofumpt
- goimports - goimports
- revive - golint
- gosec - gosec
- makezero - makezero
- misspell - misspell
@ -38,5 +41,4 @@ linters:
- prealloc - prealloc
- unconvert - unconvert
- unparam - unparam
- unused
disable-all: false disable-all: false

View File

@ -1,4 +1,4 @@
# Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Doc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go.unistack.org/micro/v4?tab=overview) [![Status](https://github.com/unistack-org/micro/workflows/build/badge.svg?branch=master)](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [![Lint](https://goreportcard.com/badge/go.unistack.org/micro/v4)](https://goreportcard.com/report/go.unistack.org/micro/v4) [![Coverage](https://codecov.io/gh/unistack-org/micro/branch/v4/graph/badge.svg?token=OZPO2LP7VS)](https://codecov.io/gh/unistack-org/micro) # Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Doc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [![Status](https://github.com/unistack-org/micro/workflows/build/badge.svg?branch=master)](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [![Lint](https://goreportcard.com/badge/github.com/unistack-org/micro)](https://goreportcard.com/report/github.com/unistack-org/micro) [![Slack](https://img.shields.io/static/v1?label=micro&message=slack&color=blueviolet)](https://unistack-org.slack.com/messages/default)
Micro is a standard library for microservices. Micro is a standard library for microservices.

View File

@ -1,15 +0,0 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 3.7.x | :white_check_mark: |
| < 3.7.0 | :x: |
## Reporting a Vulnerability
If you find any issue, please create github issue in this repo

181
api/api.go Normal file
View File

@ -0,0 +1,181 @@
package api
import (
"errors"
"regexp"
"strings"
"github.com/unistack-org/micro/v3/metadata"
"github.com/unistack-org/micro/v3/register"
"github.com/unistack-org/micro/v3/server"
)
// Api interface
type Api interface {
// Initialise options
Init(...Option) error
// Get the options
Options() Options
// Register a http handler
Register(*Endpoint) error
// Register a route
Deregister(*Endpoint) error
// Implementation of api
String() string
}
// Options holds the options
type Options struct{}
// Option func signature
type Option func(*Options) error
// Endpoint is a mapping between an RPC method and HTTP endpoint
type Endpoint struct {
// Name Greeter.Hello
Name string
// Desciption for endpoint
Description string
// Handler e.g rpc, proxy
Handler string
// Body destination
// "*" or "" - top level message value
// "string" - inner message value
Body string
// Host e.g example.com
Host []string
// Method e.g GET, POST
Method []string
// Path e.g /greeter. Expect POSIX regex
Path []string
// Stream flag
Stream bool
}
// Service represents an API service
type Service struct {
// Name of service
Name string
// Endpoint for this service
Endpoint *Endpoint
// Services that provides service
Services []*register.Service
}
// Encode encodes an endpoint to endpoint metadata
func Encode(e *Endpoint) map[string]string {
if e == nil {
return nil
}
// endpoint map
ep := make(map[string]string)
// set vals only if they exist
set := func(k, v string) {
if len(v) == 0 {
return
}
ep[k] = v
}
set("endpoint", e.Name)
set("description", e.Description)
set("handler", e.Handler)
set("method", strings.Join(e.Method, ","))
set("path", strings.Join(e.Path, ","))
set("host", strings.Join(e.Host, ","))
set("body", e.Body)
return ep
}
// Decode decodes endpoint metadata into an endpoint
func Decode(e metadata.Metadata) *Endpoint {
if e == nil {
return nil
}
ep := &Endpoint{}
ep.Name, _ = e.Get("endpoint")
ep.Description, _ = e.Get("description")
epmethod, _ := e.Get("method")
ep.Method = []string{epmethod}
eppath, _ := e.Get("path")
ep.Path = []string{eppath}
ephost, _ := e.Get("host")
ep.Host = []string{ephost}
ep.Handler, _ = e.Get("handler")
ep.Body, _ = e.Get("body")
return ep
}
// Validate validates an endpoint to guarantee it won't blow up when being served
func Validate(e *Endpoint) error {
if e == nil {
return errors.New("endpoint is nil")
}
if len(e.Name) == 0 {
return errors.New("name required")
}
for _, p := range e.Path {
ps := p[0]
pe := p[len(p)-1]
if ps == '^' && pe == '$' {
_, err := regexp.CompilePOSIX(p)
if err != nil {
return err
}
} else if ps == '^' && pe != '$' {
return errors.New("invalid path")
} else if ps != '^' && pe == '$' {
return errors.New("invalid path")
}
}
if len(e.Handler) == 0 {
return errors.New("invalid handler")
}
return nil
}
/*
Design ideas
// Gateway is an api gateway interface
type Gateway interface {
// Register a http handler
Handle(pattern string, http.Handler)
// Register a route
RegisterRoute(r Route)
// Init initialises the command line.
// It also parses further options.
Init(...Option) error
// Run the gateway
Run() error
}
// NewGateway returns a new api gateway
func NewGateway() Gateway {
return newGateway()
}
*/
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
//
// Usage:
//
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
// &api.Endpoint{
// Name: "Greeter.Hello",
// Path: []string{"/greeter"},
// },
// ))
func WithEndpoint(e *Endpoint) server.HandlerOption {
return server.EndpointMetadata(e.Name, Encode(e))
}

152
api/api_test.go Normal file
View File

@ -0,0 +1,152 @@
package api
import (
"strings"
"testing"
)
//nolint:gocyclo
func TestEncoding(t *testing.T) {
testData := []*Endpoint{
nil,
{
Name: "Foo.Bar",
Description: "A test endpoint",
Handler: "meta",
Host: []string{"foo.com"},
Method: []string{"GET"},
Path: []string{"/test"},
},
}
compare := func(expect, got []string) bool {
// no data to compare, return true
if len(expect) == 0 && len(got) == 0 {
return true
}
// no data expected but got some return false
if len(expect) == 0 && len(got) > 0 {
return false
}
// compare expected with what we got
for _, e := range expect {
var seen bool
for _, g := range got {
if e == g {
seen = true
break
}
}
if !seen {
return false
}
}
// we're done, return true
return true
}
for _, d := range testData {
// encode
e := Encode(d)
// decode
de := Decode(e)
// nil endpoint returns nil
if d == nil {
if e != nil {
t.Fatalf("expected nil got %v", e)
}
if de != nil {
t.Fatalf("expected nil got %v", de)
}
continue
}
// check encoded map
name := e["endpoint"]
desc := e["description"]
method := strings.Split(e["method"], ",")
path := strings.Split(e["path"], ",")
host := strings.Split(e["host"], ",")
handler := e["handler"]
if name != d.Name {
t.Fatalf("expected %v got %v", d.Name, name)
}
if desc != d.Description {
t.Fatalf("expected %v got %v", d.Description, desc)
}
if handler != d.Handler {
t.Fatalf("expected %v got %v", d.Handler, handler)
}
if ok := compare(d.Method, method); !ok {
t.Fatalf("expected %v got %v", d.Method, method)
}
if ok := compare(d.Path, path); !ok {
t.Fatalf("expected %v got %v", d.Path, path)
}
if ok := compare(d.Host, host); !ok {
t.Fatalf("expected %v got %v", d.Host, host)
}
if de.Name != d.Name {
t.Fatalf("expected %v got %v", d.Name, de.Name)
}
if de.Description != d.Description {
t.Fatalf("expected %v got %v", d.Description, de.Description)
}
if de.Handler != d.Handler {
t.Fatalf("expected %v got %v", d.Handler, de.Handler)
}
if ok := compare(d.Method, de.Method); !ok {
t.Fatalf("expected %v got %v", d.Method, de.Method)
}
if ok := compare(d.Path, de.Path); !ok {
t.Fatalf("expected %v got %v", d.Path, de.Path)
}
if ok := compare(d.Host, de.Host); !ok {
t.Fatalf("expected %v got %v", d.Host, de.Host)
}
}
}
func TestValidate(t *testing.T) {
epPcre := &Endpoint{
Name: "Foo.Bar",
Description: "A test endpoint",
Handler: "meta",
Host: []string{"foo.com"},
Method: []string{"GET"},
Path: []string{"^/test/?$"},
}
if err := Validate(epPcre); err != nil {
t.Fatal(err)
}
epGpath := &Endpoint{
Name: "Foo.Bar",
Description: "A test endpoint",
Handler: "meta",
Host: []string{"foo.com"},
Method: []string{"GET"},
Path: []string{"/test/{id}"},
}
if err := Validate(epGpath); err != nil {
t.Fatal(err)
}
epPcreInvalid := &Endpoint{
Name: "Foo.Bar",
Description: "A test endpoint",
Handler: "meta",
Host: []string{"foo.com"},
Method: []string{"GET"},
Path: []string{"/test/?$"},
}
if err := Validate(epPcreInvalid); err == nil {
t.Fatalf("invalid pcre %v", epPcreInvalid.Path[0])
}
}

14
api/handler/handler.go Normal file
View File

@ -0,0 +1,14 @@
// Package handler provides http handlers
package handler
import (
"net/http"
)
// Handler represents a HTTP handler that manages a request
type Handler interface {
// standard http handler
http.Handler
// name of handler
String() string
}

70
api/handler/options.go Normal file
View File

@ -0,0 +1,70 @@
package handler
import (
"github.com/unistack-org/micro/v3/api/router"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/logger"
)
// DefaultMaxRecvSize specifies max recv size for handler
var DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
// Options struct holds handler options
type Options struct {
Router router.Router
Client client.Client
Logger logger.Logger
Namespace string
MaxRecvSize int64
}
// Option func signature
type Option func(o *Options)
// NewOptions creates new options struct and fills it
func NewOptions(opts ...Option) Options {
options := Options{
Client: client.DefaultClient,
Router: router.DefaultRouter,
Logger: logger.DefaultLogger,
MaxRecvSize: DefaultMaxRecvSize,
}
for _, o := range opts {
o(&options)
}
// set namespace if blank
if len(options.Namespace) == 0 {
WithNamespace("go.micro.api")(&options)
}
return options
}
// WithNamespace specifies the namespace for the handler
func WithNamespace(s string) Option {
return func(o *Options) {
o.Namespace = s
}
}
// WithRouter specifies a router to be used by the handler
func WithRouter(r router.Router) Option {
return func(o *Options) {
o.Router = r
}
}
// WithClient specifies client to be used by the handler
func WithClient(c client.Client) Option {
return func(o *Options) {
o.Client = c
}
}
// WithMaxRecvSize specifies max body size
func WithMaxRecvSize(size int64) Option {
return func(o *Options) {
o.MaxRecvSize = size
}
}

47
api/resolver/grpc/grpc.go Normal file
View File

@ -0,0 +1,47 @@
// Package grpc resolves a grpc service like /greeter.Say/Hello to greeter service
package grpc
import (
"errors"
"net/http"
"strings"
"github.com/unistack-org/micro/v3/api/resolver"
)
// Resolver struct
type Resolver struct {
opts resolver.Options
}
// Resolve func to resolve enndpoint
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
// /foo.Bar/Service
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
// [foo.Bar, Service]
parts := strings.Split(req.URL.Path[1:], "/")
// [foo, Bar]
name := strings.Split(parts[0], ".")
// foo
return &resolver.Endpoint{
Name: strings.Join(name[:len(name)-1], "."),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
func (r *Resolver) String() string {
return "grpc"
}
// NewResolver is used to create new Resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
}

35
api/resolver/host/host.go Normal file
View File

@ -0,0 +1,35 @@
// Package host resolves using http host
package host
import (
"net/http"
"github.com/unistack-org/micro/v3/api/resolver"
)
type hostResolver struct {
opts resolver.Options
}
// Resolve endpoint
func (r *hostResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
return &resolver.Endpoint{
Name: req.Host,
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
func (r *hostResolver) String() string {
return "host"
}
// NewResolver creates new host api resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &hostResolver{opts: resolver.NewOptions(opts...)}
}

70
api/resolver/options.go Normal file
View File

@ -0,0 +1,70 @@
package resolver
import (
"context"
"github.com/unistack-org/micro/v3/register"
)
// Options struct
type Options struct {
// Context is for external defined options
Context context.Context
// Handler name
Handler string
// ServicePrefix is the prefix
ServicePrefix string
}
// Option func
type Option func(o *Options)
// WithHandler sets the handler being used
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
// WithServicePrefix sets the ServicePrefix option
func WithServicePrefix(p string) Option {
return func(o *Options) {
o.ServicePrefix = p
}
}
// NewOptions returns new initialised options
func NewOptions(opts ...Option) Options {
options := Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// ResolveOptions are used when resolving a request
type ResolveOptions struct {
Domain string
}
// ResolveOption sets an option
type ResolveOption func(*ResolveOptions)
// Domain sets the resolve Domain option
func Domain(n string) ResolveOption {
return func(o *ResolveOptions) {
o.Domain = n
}
}
// NewResolveOptions returns new initialised resolve options
func NewResolveOptions(opts ...ResolveOption) ResolveOptions {
options := ResolveOptions{Domain: register.DefaultDomain}
for _, o := range opts {
o(&options)
}
return options
}

44
api/resolver/path/path.go Normal file
View File

@ -0,0 +1,44 @@
// Package path resolves using http path
package path
import (
"net/http"
"strings"
"github.com/unistack-org/micro/v3/api/resolver"
)
// Resolver the path resolver
type Resolver struct {
opts resolver.Options
}
// Resolve resolves endpoint
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
if req.URL.Path == "/" {
return nil, resolver.ErrNotFound
}
parts := strings.Split(req.URL.Path[1:], "/")
return &resolver.Endpoint{
Name: r.opts.ServicePrefix + "." + parts[0],
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
// String retruns the string representation
func (r *Resolver) String() string {
return "path"
}
// NewResolver returns new path resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
}

34
api/resolver/resolver.go Normal file
View File

@ -0,0 +1,34 @@
// Package resolver resolves a http request to an endpoint
package resolver
import (
"errors"
"net/http"
)
var (
// ErrNotFound returned when endpoint is not found
ErrNotFound = errors.New("not found")
// ErrInvalidPath returned on invalid path
ErrInvalidPath = errors.New("invalid path")
)
// Resolver resolves requests to endpoints
type Resolver interface {
Resolve(r *http.Request, opts ...ResolveOption) (*Endpoint, error)
String() string
}
// Endpoint is the endpoint for a http request
type Endpoint struct {
// Endpoint name e.g greeter
Name string
// HTTP Host e.g example.com
Host string
// HTTP Methods e.g GET, POST
Method string
// HTTP Path e.g /greeter.
Path string
// Domain endpoint exists within
Domain string
}

View File

@ -0,0 +1,90 @@
// Package subdomain is a resolver which uses the subdomain to determine the domain to route to. It
// offloads the endpoint resolution to a child resolver which is provided in New.
package subdomain
import (
"net"
"net/http"
"strings"
"github.com/unistack-org/micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/logger"
"golang.org/x/net/publicsuffix"
)
// NewResolver creates new subdomain api resolver
func NewResolver(parent resolver.Resolver, opts ...resolver.Option) resolver.Resolver {
options := resolver.NewOptions(opts...)
return &subdomainResolver{opts: options, Resolver: parent}
}
type subdomainResolver struct {
resolver.Resolver
opts resolver.Options
}
// Resolve resolve endpoint based on subdomain
func (r *subdomainResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
if dom := r.Domain(req); len(dom) > 0 {
opts = append(opts, resolver.Domain(dom))
}
return r.Resolver.Resolve(req, opts...)
}
// Domain returns domain
func (r *subdomainResolver) Domain(req *http.Request) string {
// determine the host, e.g. foobar.m3o.app
host := req.URL.Hostname()
if len(host) == 0 {
if h, _, err := net.SplitHostPort(req.Host); err == nil {
host = h // host does contain a port
} else if strings.Contains(err.Error(), "missing port in address") {
host = req.Host // host does not contain a port
}
}
// check for an ip address
if net.ParseIP(host) != nil {
return ""
}
// check for dev environment
if host == "localhost" || host == "127.0.0.1" {
return ""
}
// extract the top level domain plus one (e.g. 'myapp.com')
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
if logger.V(logger.DebugLevel) {
logger.Debug(r.opts.Context, "Unable to extract domain from %v", host)
}
return ""
}
// there was no subdomain
if host == domain {
return ""
}
// remove the domain from the host, leaving the subdomain, e.g. "staging.foo.myapp.com" => "staging.foo"
subdomain := strings.TrimSuffix(host, "."+domain)
// ignore the API subdomain
if subdomain == "api" {
return ""
}
// return the reversed subdomain as the namespace, e.g. "staging.foo" => "foo-staging"
comps := strings.Split(subdomain, ".")
for i := len(comps)/2 - 1; i >= 0; i-- {
opp := len(comps) - 1 - i
comps[i], comps[opp] = comps[opp], comps[i]
}
return strings.Join(comps, "-")
}
func (r *subdomainResolver) String() string {
return "subdomain"
}

View File

@ -0,0 +1,73 @@
package subdomain
import (
"net/http"
"net/url"
"testing"
"github.com/unistack-org/micro/v3/api/resolver/vpath"
)
func TestResolve(t *testing.T) {
tt := []struct {
Name string
Host string
Result string
}{
{
Name: "Top level domain",
Host: "micro.mu",
Result: "micro",
},
{
Name: "Effective top level domain",
Host: "micro.com.au",
Result: "micro",
},
{
Name: "Subdomain dev",
Host: "dev.micro.mu",
Result: "dev",
},
{
Name: "Subdomain foo",
Host: "foo.micro.mu",
Result: "foo",
},
{
Name: "Multi-level subdomain",
Host: "staging.myapp.m3o.app",
Result: "myapp-staging",
},
{
Name: "Dev host",
Host: "127.0.0.1",
Result: "micro",
},
{
Name: "Localhost",
Host: "localhost",
Result: "micro",
},
{
Name: "IP host",
Host: "81.151.101.146",
Result: "micro",
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
r := NewResolver(vpath.NewResolver())
result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}})
if err != nil {
t.Fatal(err)
}
if result != nil {
if tc.Result != result.Domain {
t.Fatalf("Expected %v but got %v", tc.Result, result.Domain)
}
}
})
}
}

View File

@ -0,0 +1,75 @@
// Package vpath resolves using http path and recognised versioned urls
package vpath
import (
"errors"
"net/http"
"regexp"
"strings"
"github.com/unistack-org/micro/v3/api/resolver"
)
// NewResolver creates new vpath api resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &vpathResolver{opts: resolver.NewOptions(opts...)}
}
type vpathResolver struct {
opts resolver.Options
}
var re = regexp.MustCompile("^v[0-9]+$")
// Resolve endpoint
func (r *vpathResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
options := resolver.NewResolveOptions(opts...)
parts := strings.Split(req.URL.Path[1:], "/")
if len(parts) == 1 {
return &resolver.Endpoint{
Name: r.withPrefix(parts...),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
// /v1/foo
if re.MatchString(parts[0]) {
return &resolver.Endpoint{
Name: r.withPrefix(parts[0:2]...),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
return &resolver.Endpoint{
Name: r.withPrefix(parts[0]),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
Domain: options.Domain,
}, nil
}
func (r *vpathResolver) String() string {
return "vpath"
}
// withPrefix transforms "foo" into "go.micro.api.foo"
func (r *vpathResolver) withPrefix(parts ...string) string {
p := r.opts.ServicePrefix
if len(p) > 0 {
parts = append([]string{p}, parts...)
}
return strings.Join(parts, ".")
}

75
api/router/options.go Normal file
View File

@ -0,0 +1,75 @@
package router
import (
"context"
"github.com/unistack-org/micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver/vpath"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/register"
)
// Options holds the options for api router
type Options struct {
// Register for service lookup
Register register.Register
// Resolver to use
Resolver resolver.Resolver
// Logger micro logger
Logger logger.Logger
// Context is for external options
Context context.Context
// Handler name
Handler string
}
// Option func signature
type Option func(o *Options)
// NewOptions returns options struct filled by opts
func NewOptions(opts ...Option) Options {
options := Options{
Context: context.Background(),
Handler: "meta",
}
for _, o := range opts {
o(&options)
}
if options.Resolver == nil {
options.Resolver = vpath.NewResolver(
resolver.WithHandler(options.Handler),
)
}
return options
}
// WithContext sets the context
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// WithHandler sets the handler
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
// WithRegister sets the register
func WithRegister(r register.Register) Option {
return func(o *Options) {
o.Register = r
}
}
// WithResolver sets the resolver
func WithResolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r
}
}

31
api/router/router.go Normal file
View File

@ -0,0 +1,31 @@
// Package router provides api service routing
package router
import (
"net/http"
"github.com/unistack-org/micro/v3/api"
)
// DefaultRouter contains default router implementation
var DefaultRouter Router
// Router is used to determine an endpoint for a request
type Router interface {
// Returns options
Options() Options
// Init initialize router
Init(...Option) error
// Stop the router
Close() error
// Endpoint returns an api.Service endpoint or an error if it does not exist
Endpoint(r *http.Request) (*api.Service, error)
// Register endpoint in router
Register(ep *api.Endpoint) error
// Deregister endpoint from router
Deregister(ep *api.Endpoint) error
// Route returns an api.Service route
Route(r *http.Request) (*api.Service, error)
// String representation of router
String() string
}

141
auth/auth.go Normal file
View File

@ -0,0 +1,141 @@
// Package auth provides authentication and authorization capability
package auth
import (
"context"
"errors"
"time"
"github.com/unistack-org/micro/v3/metadata"
)
const (
// BearerScheme used for Authorization header
BearerScheme = "Bearer "
// ScopePublic is the scope applied to a rule to allow access to the public
ScopePublic = ""
// ScopeAccount is the scope applied to a rule to limit to users with any valid account
ScopeAccount = "*"
)
var (
// DefaultAuth holds default auth implementation
DefaultAuth Auth = NewAuth()
// ErrInvalidToken is when the token provided is not valid
ErrInvalidToken = errors.New("invalid token provided")
// ErrForbidden is when a user does not have the necessary scope to access a resource
ErrForbidden = errors.New("resource forbidden")
)
// Auth provides authentication and authorization
type Auth interface {
// Init the auth
Init(opts ...Option) error
// Options set for auth
Options() Options
// Generate a new account
Generate(id string, opts ...GenerateOption) (*Account, error)
// Verify an account has access to a resource using the rules
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
// Inspect a token
Inspect(token string) (*Account, error)
// Token generated using refresh token or credentials
Token(opts ...TokenOption) (*Token, error)
// Grant access to a resource
Grant(rule *Rule) error
// Revoke access to a resource
Revoke(rule *Rule) error
// Rules returns all the rules used to verify requests
Rules(...RulesOption) ([]*Rule, error)
// String returns the name of the implementation
String() string
}
// Account provided by an auth provider
type Account struct {
// Metadata any other associated metadata
Metadata metadata.Metadata `json:"metadata"`
// ID of the account e.g. email or uuid
ID string `json:"id"`
// Type of the account, e.g. service
Type string `json:"type"`
// Issuer of the account
Issuer string `json:"issuer"`
// Secret for the account, e.g. the password
Secret string `json:"secret"`
// Scopes the account has access to
Scopes []string `json:"scopes"`
}
// Token can be short or long lived
type Token struct {
// Time of token creation
Created time.Time `json:"created"`
// Time of token expiry
Expiry time.Time `json:"expiry"`
// The token to be used for accessing resources
AccessToken string `json:"access_token"`
// RefreshToken to be used to generate a new token
RefreshToken string `json:"refresh_token"`
}
// Expired returns a boolean indicating if the token needs to be refreshed
func (t *Token) Expired() bool {
return t.Expiry.Unix() < time.Now().Unix()
}
// Resource is an entity such as a user or
type Resource struct {
// Name of the resource, e.g. go.micro.service.notes
Name string `json:"name"`
// Type of resource, e.g. service
Type string `json:"type"`
// Endpoint resource e.g NotesService.Create
Endpoint string `json:"endpoint"`
}
// Access defines the type of access a rule grants
type Access int
const (
// AccessGranted to a resource
AccessGranted Access = iota
// AccessDenied to a resource
AccessDenied
)
// Rule is used to verify access to a resource
type Rule struct {
// Resource that rule belongs to
Resource *Resource
// ID of the rule
ID string
// Scope of the rule
Scope string
// Access flag allow/deny
Access Access
// Priority holds the rule priority
Priority int32
}
type accountKey struct{}
// AccountFromContext gets the account from the context, which
// is set by the auth wrapper at the start of a call. If the account
// is not set, a nil account will be returned. The error is only returned
// when there was a problem retrieving an account
func AccountFromContext(ctx context.Context) (*Account, bool) {
if ctx == nil {
return nil, false
}
acc, ok := ctx.Value(accountKey{}).(*Account)
return acc, ok
}
// ContextWithAccount sets the account in the context
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, accountKey{}, account)
}

79
auth/noop.go Normal file
View File

@ -0,0 +1,79 @@
package auth
import (
"github.com/google/uuid"
)
type noopAuth struct {
opts Options
}
// String returns the name of the implementation
func (n *noopAuth) String() string {
return "noop"
}
// Init the auth
func (n *noopAuth) Init(opts ...Option) error {
for _, o := range opts {
o(&n.opts)
}
return nil
}
// Options set for auth
func (n *noopAuth) Options() Options {
return n.opts
}
// Generate a new account
func (n *noopAuth) Generate(id string, opts ...GenerateOption) (*Account, error) {
options := NewGenerateOptions(opts...)
return &Account{
ID: id,
Secret: options.Secret,
Metadata: options.Metadata,
Scopes: options.Scopes,
Issuer: n.Options().Issuer,
}, nil
}
// Grant access to a resource
func (n *noopAuth) Grant(rule *Rule) error {
return nil
}
// Revoke access to a resource
func (n *noopAuth) Revoke(rule *Rule) error {
return nil
}
// Rules used to verify requests
func (n *noopAuth) Rules(opts ...RulesOption) ([]*Rule, error) {
return []*Rule{}, nil
}
// Verify an account has access to a resource
func (n *noopAuth) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
return nil
}
// Inspect a token
func (n *noopAuth) Inspect(token string) (*Account, error) {
uid, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &Account{ID: uid.String(), Issuer: n.Options().Issuer}, nil
}
// Token generation using an account id and secret
func (n *noopAuth) Token(opts ...TokenOption) (*Token, error) {
return &Token{}, nil
}
// NewAuth returns new noop auth
func NewAuth(opts ...Option) Auth {
return &noopAuth{opts: NewOptions(opts...)}
}

311
auth/options.go Normal file
View File

@ -0,0 +1,311 @@
package auth
import (
"context"
"time"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/metadata"
"github.com/unistack-org/micro/v3/meter"
"github.com/unistack-org/micro/v3/store"
"github.com/unistack-org/micro/v3/tracer"
)
// NewOptions creates Options struct from slice of options
func NewOptions(opts ...Option) Options {
options := Options{
Tracer: tracer.DefaultTracer,
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
}
for _, o := range opts {
o(&options)
}
return options
}
// Options struct holds auth options
type Options struct {
// Context holds the external options
Context context.Context
// Meter used for metrics
Meter meter.Meter
// Logger used for logging
Logger logger.Logger
// Tracer used for tracing
Tracer tracer.Tracer
// Store used for stre data
Store store.Store
// Token is the services token used to authenticate itself
Token *Token
// LoginURL is the relative url path where a user can login
LoginURL string
// PrivateKey for encoding JWTs
PrivateKey string
// PublicKey for decoding JWTs
PublicKey string
// Secret is used to authenticate the service
Secret string
// ID is the services auth ID
ID string
// Issuer of the service's account
Issuer string
// Name holds the auth name
Name string
// Addrs sets the addresses of auth
Addrs []string
}
// Option func
type Option func(o *Options)
// Addrs is the auth addresses to use
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// Issuer of the services account
func Issuer(i string) Option {
return func(o *Options) {
o.Issuer = i
}
}
// Store to back auth
func Store(s store.Store) Option {
return func(o *Options) {
o.Store = s
}
}
// PublicKey is the JWT public key
func PublicKey(key string) Option {
return func(o *Options) {
o.PublicKey = key
}
}
// PrivateKey is the JWT private key
func PrivateKey(key string) Option {
return func(o *Options) {
o.PrivateKey = key
}
}
// Credentials sets the auth credentials
func Credentials(id, secret string) Option {
return func(o *Options) {
o.ID = id
o.Secret = secret
}
}
// ClientToken sets the auth token to use when making requests
func ClientToken(token *Token) Option {
return func(o *Options) {
o.Token = token
}
}
// LoginURL sets the auth LoginURL
func LoginURL(url string) Option {
return func(o *Options) {
o.LoginURL = url
}
}
// GenerateOptions struct
type GenerateOptions struct {
Metadata metadata.Metadata
Provider string
Type string
Secret string
Issuer string
Scopes []string
}
// GenerateOption func
type GenerateOption func(o *GenerateOptions)
// WithSecret for the generated account
func WithSecret(s string) GenerateOption {
return func(o *GenerateOptions) {
o.Secret = s
}
}
// WithType for the generated account
func WithType(t string) GenerateOption {
return func(o *GenerateOptions) {
o.Type = t
}
}
// WithMetadata for the generated account
func WithMetadata(md metadata.Metadata) GenerateOption {
return func(o *GenerateOptions) {
o.Metadata = metadata.Copy(md)
}
}
// WithProvider for the generated account
func WithProvider(p string) GenerateOption {
return func(o *GenerateOptions) {
o.Provider = p
}
}
// WithScopes for the generated account
func WithScopes(s ...string) GenerateOption {
return func(o *GenerateOptions) {
o.Scopes = s
}
}
// WithIssuer for the generated account
func WithIssuer(i string) GenerateOption {
return func(o *GenerateOptions) {
o.Issuer = i
}
}
// NewGenerateOptions from a slice of options
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
var options GenerateOptions
for _, o := range opts {
o(&options)
}
return options
}
// TokenOptions struct
type TokenOptions struct {
ID string
Secret string
RefreshToken string
Issuer string
Expiry time.Duration
}
// TokenOption func
type TokenOption func(o *TokenOptions)
// WithExpiry for the token
func WithExpiry(ex time.Duration) TokenOption {
return func(o *TokenOptions) {
o.Expiry = ex
}
}
// WithCredentials sets tye id and secret
func WithCredentials(id, secret string) TokenOption {
return func(o *TokenOptions) {
o.ID = id
o.Secret = secret
}
}
// WithToken sets the refresh token
func WithToken(rt string) TokenOption {
return func(o *TokenOptions) {
o.RefreshToken = rt
}
}
// WithTokenIssuer sets the token issuer option
func WithTokenIssuer(iss string) TokenOption {
return func(o *TokenOptions) {
o.Issuer = iss
}
}
// NewTokenOptions from a slice of options
func NewTokenOptions(opts ...TokenOption) TokenOptions {
var options TokenOptions
for _, o := range opts {
o(&options)
}
// set default expiry of token
if options.Expiry == 0 {
options.Expiry = time.Minute
}
return options
}
// VerifyOptions struct
type VerifyOptions struct {
Context context.Context
Namespace string
}
// VerifyOption func
type VerifyOption func(o *VerifyOptions)
// VerifyContext pass context to verify
func VerifyContext(ctx context.Context) VerifyOption {
return func(o *VerifyOptions) {
o.Context = ctx
}
}
// VerifyNamespace sets thhe namespace for verify
func VerifyNamespace(ns string) VerifyOption {
return func(o *VerifyOptions) {
o.Namespace = ns
}
}
// RulesOptions struct
type RulesOptions struct {
Context context.Context
Namespace string
}
// RulesOption func
type RulesOption func(o *RulesOptions)
// RulesContext pass rules context
func RulesContext(ctx context.Context) RulesOption {
return func(o *RulesOptions) {
o.Context = ctx
}
}
// RulesNamespace sets the rule namespace
func RulesNamespace(ns string) RulesOption {
return func(o *RulesOptions) {
o.Namespace = ns
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Tracer sets the meter
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}

92
auth/rules.go Normal file
View File

@ -0,0 +1,92 @@
package auth
import (
"fmt"
"sort"
"strings"
)
// VerifyAccess an account has access to a resource using the rules provided. If the account does not have
// access an error will be returned. If there are no rules provided which match the resource, an error
// will be returned
//nolint:gocyclo
func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
// the rule is only to be applied if the type matches the resource or is catch-all (*)
validTypes := []string{"*", res.Type}
// the rule is only to be applied if the name matches the resource or is catch-all (*)
validNames := []string{"*", res.Name}
// rules can have wildcard excludes on endpoints since this can also be a path for web services,
// e.g. /foo/* would include /foo/bar. We also want to check for wildcards and the exact endpoint
validEndpoints := []string{"*", res.Endpoint}
if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 {
for i := 1; i < len(comps)+1; i++ {
wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/"))
validEndpoints = append(validEndpoints, wildcard)
}
}
// filter the rules to the ones which match the criteria above
filteredRules := make([]*Rule, 0)
for _, rule := range rules {
if !include(validTypes, rule.Resource.Type) {
continue
}
if !include(validNames, rule.Resource.Name) {
continue
}
if !include(validEndpoints, rule.Resource.Endpoint) {
continue
}
filteredRules = append(filteredRules, rule)
}
// sort the filtered rules by priority, highest to lowest
sort.SliceStable(filteredRules, func(i, j int) bool {
return filteredRules[i].Priority > filteredRules[j].Priority
})
// loop through the rules and check for a rule which applies to this account
for _, rule := range filteredRules {
// a blank scope indicates the rule applies to everyone, even nil accounts
if rule.Scope == ScopePublic && rule.Access == AccessDenied {
return ErrForbidden
} else if rule.Scope == ScopePublic && rule.Access == AccessGranted {
return nil
}
// all further checks require an account
if acc == nil {
continue
}
// this rule applies to any account
if rule.Scope == ScopeAccount && rule.Access == AccessDenied {
return ErrForbidden
} else if rule.Scope == ScopeAccount && rule.Access == AccessGranted {
return nil
}
// if the account has the necessary scope
if include(acc.Scopes, rule.Scope) && rule.Access == AccessDenied {
return ErrForbidden
} else if include(acc.Scopes, rule.Scope) && rule.Access == AccessGranted {
return nil
}
}
// if no rules matched then return forbidden
return ErrForbidden
}
// include is a helper function which checks to see if the slice contains the value. includes is
// not case sensitive.
func include(slice []string, val string) bool {
for _, s := range slice {
if strings.EqualFold(s, val) {
return true
}
}
return false
}

288
auth/rules_test.go Normal file
View File

@ -0,0 +1,288 @@
package auth
import (
"testing"
)
func TestVerify(t *testing.T) {
srvResource := &Resource{
Type: "service",
Name: "go.micro.service.foo",
Endpoint: "Foo.Bar",
}
webResource := &Resource{
Type: "service",
Name: "go.micro.web.foo",
Endpoint: "/foo/bar",
}
catchallResource := &Resource{
Type: "*",
Name: "*",
Endpoint: "*",
}
tt := []struct {
Error error
Account *Account
Resource *Resource
Name string
Rules []*Rule
}{
{
Name: "NoRules",
Rules: []*Rule{},
Account: nil,
Resource: srvResource,
Error: ErrForbidden,
},
{
Name: "CatchallPublicAccount",
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
{
Scope: "",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPublicNoAccount",
Resource: srvResource,
Rules: []*Rule{
{
Scope: "",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPrivateAccount",
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
},
},
},
{
Name: "CatchallPrivateNoAccount",
Resource: srvResource,
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallServiceRuleMatch",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
Name: srvResource.Name,
Endpoint: "*",
},
},
},
},
{
Name: "CatchallServiceRuleNoMatch",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
Name: "wrongname",
Endpoint: "*",
},
},
},
Error: ErrForbidden,
},
{
Name: "ExactRuleValidScope",
Resource: srvResource,
Account: &Account{
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
{
Scope: "neededscope",
Resource: srvResource,
},
},
},
{
Name: "ExactRuleInvalidScope",
Resource: srvResource,
Account: &Account{
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
{
Scope: "invalidscope",
Resource: srvResource,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallDenyWithAccount",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
},
},
Error: ErrForbidden,
},
{
Name: "CatchallDenyWithNoAccount",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
},
},
Error: ErrForbidden,
},
{
Name: "RulePriorityGrantFirst",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 1,
},
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
Priority: 0,
},
},
},
{
Name: "RulePriorityDenyFirst",
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 0,
},
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
Priority: 1,
},
},
Error: ErrForbidden,
},
{
Name: "WebExactEndpointValid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: webResource,
},
},
},
{
Name: "WebExactEndpointInalid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "invalidendpoint",
},
},
},
Error: ErrForbidden,
},
{
Name: "WebWildcardEndpoint",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "*",
},
},
},
},
{
Name: "WebWildcardPathEndpointValid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "/foo/*",
},
},
},
},
{
Name: "WebWildcardPathEndpointInvalid",
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
Name: webResource.Name,
Endpoint: "/bar/*",
},
},
},
Error: ErrForbidden,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if err := VerifyAccess(tc.Rules, tc.Account, tc.Resource); err != tc.Error {
t.Errorf("Expected %v but got %v", tc.Error, err)
}
})
}
}

View File

@ -1,13 +1,11 @@
// Package broker is an interface used for asynchronous messaging // Package broker is an interface used for asynchronous messaging
package broker // import "go.unistack.org/micro/v4/broker" package broker
import ( import (
"context" "context"
"errors" "errors"
"time"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
"go.unistack.org/micro/v4/options"
) )
// DefaultBroker default memory broker // DefaultBroker default memory broker
@ -18,10 +16,6 @@ var (
ErrNotConnected = errors.New("broker not connected") ErrNotConnected = errors.New("broker not connected")
// ErrDisconnected returns when broker disconnected // ErrDisconnected returns when broker disconnected
ErrDisconnected = errors.New("broker disconnected") ErrDisconnected = errors.New("broker disconnected")
// ErrInvalidMessage returns when message has nvalid format
ErrInvalidMessage = errors.New("broker message has invalid format")
// DefaultGracefulTimeout
DefaultGracefulTimeout = 5 * time.Second
) )
// Broker is an interface used for asynchronous messaging. // Broker is an interface used for asynchronous messaging.
@ -29,7 +23,7 @@ type Broker interface {
// Name returns broker instance name // Name returns broker instance name
Name() string Name() string
// Init initilize broker // Init initilize broker
Init(opts ...options.Option) error Init(opts ...Option) error
// Options returns broker options // Options returns broker options
Options() Options Options() Options
// Address return configured address // Address return configured address
@ -38,29 +32,91 @@ type Broker interface {
Connect(ctx context.Context) error Connect(ctx context.Context) error
// Disconnect disconnect from broker // Disconnect disconnect from broker
Disconnect(ctx context.Context) error Disconnect(ctx context.Context) error
// Publish message, msg can be single broker.Message or []broker.Message // Publish message to broker topic
Publish(ctx context.Context, msg interface{}, opts ...options.Option) error Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
// Subscribe subscribes to topic message via handler // Subscribe subscribes to topic message via handler
Subscribe(ctx context.Context, topic string, handler interface{}, opts ...options.Option) (Subscriber, error) Subscribe(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
// BatchPublish messages to broker with multiple topics
BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error
// BatchSubscribe subscribes to topic messages via handler
BatchSubscribe(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
// String type of broker // String type of broker
String() string String() string
} }
// Message is given to a subscription handler for processing // Handler is used to process messages via a subscription of a topic.
type Message interface { type Handler func(Event) error
// Context for the message
Context() context.Context // Events contains multiple events
// Topic 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
type Event interface {
// Topic returns event topic
Topic() string Topic() string
// Header returns message headers // Message returns broker message
Header() metadata.Metadata Message() *Message
// Body returns broker message may be []byte slice or some go struct
Body() interface{}
// Ack acknowledge message // Ack acknowledge message
Ack() error Ack() error
// Error returns message error (like decoding errors or some other) // Error returns message error (like decoding errors or some other)
// In this case Body contains raw []byte from broker
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
type Message struct {
// Header contains message metadata
Header metadata.Metadata
// Body contains message body
Body RawMessage
}
// NewMessage create broker message with topic filled
func NewMessage(topic string) *Message {
m := &Message{Header: metadata.New(2)}
m.Header.Set(metadata.HeaderTopic, topic)
return m
} }
// Subscriber is a convenience return type for the Subscribe method // Subscriber is a convenience return type for the Subscribe method
@ -72,9 +128,3 @@ type Subscriber interface {
// Unsubscribe from topic // Unsubscribe from topic
Unsubscribe(ctx context.Context) error Unsubscribe(ctx context.Context) error
} }
// MessageHandler func signature for single message processing
type MessageHandler func(Message) error
// MessagesHandler func signature for batch message processing
type MessagesHandler func([]Message) error

View File

@ -22,3 +22,33 @@ func NewContext(ctx context.Context, s Broker) context.Context {
} }
return context.WithValue(ctx, brokerKey{}, s) return context.WithValue(ctx, brokerKey{}, s)
} }
// SetSubscribeOption returns a function to setup a context with given value
func SetSubscribeOption(k, v interface{}) SubscribeOption {
return func(o *SubscribeOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetOption returns a function to setup a context with given value
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)
}
}
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@ -1,39 +0,0 @@
package broker
import (
"context"
"testing"
)
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), brokerKey{}, NewBroker())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestFromNilContext(t *testing.T) {
// nolint: staticcheck
c, ok := FromContext(nil)
if ok || c != nil {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewBroker())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestNewNilContext(t *testing.T) {
// nolint: staticcheck
ctx := NewContext(nil, NewBroker())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}

View File

@ -2,22 +2,17 @@ package broker
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time"
"go.unistack.org/micro/v4/codec" "github.com/google/uuid"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
"go.unistack.org/micro/v4/options" maddr "github.com/unistack-org/micro/v3/util/addr"
"go.unistack.org/micro/v4/semconv" mnet "github.com/unistack-org/micro/v3/util/net"
maddr "go.unistack.org/micro/v4/util/addr" "github.com/unistack-org/micro/v3/util/rand"
"go.unistack.org/micro/v4/util/id"
mnet "go.unistack.org/micro/v4/util/net"
"go.unistack.org/micro/v4/util/rand"
) )
type MemoryBroker struct { type memoryBroker struct {
subscribers map[string][]*memorySubscriber subscribers map[string][]*memorySubscriber
addr string addr string
opts Options opts Options
@ -25,15 +20,32 @@ type MemoryBroker struct {
connected bool connected bool
} }
func (m *MemoryBroker) Options() Options { type memoryEvent struct {
err error
message interface{}
topic string
opts Options
}
type memorySubscriber struct {
ctx context.Context
exit chan bool
handler Handler
batchhandler BatchHandler
id string
topic string
opts SubscribeOptions
}
func (m *memoryBroker) Options() Options {
return m.opts return m.opts
} }
func (m *MemoryBroker) Address() string { func (m *memoryBroker) Address() string {
return m.addr return m.addr
} }
func (m *MemoryBroker) Connect(ctx context.Context) error { func (m *memoryBroker) Connect(ctx context.Context) error {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
@ -57,33 +69,26 @@ func (m *MemoryBroker) Connect(ctx context.Context) error {
return nil return nil
} }
func (m *MemoryBroker) Disconnect(ctx context.Context) error { func (m *memoryBroker) Disconnect(ctx context.Context) error {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
select { if !m.connected {
case <-ctx.Done(): return nil
return ctx.Err() }
default:
if m.connected {
m.connected = false m.connected = false
}
}
return nil return nil
} }
func (m *MemoryBroker) Init(opts ...options.Option) error { func (m *memoryBroker) Init(opts ...Option) error {
var err error
for _, o := range opts { for _, o := range opts {
if err = o(&m.opts); err != nil { o(&m.opts)
return err
}
} }
return nil return nil
} }
func (m *MemoryBroker) Publish(ctx context.Context, message interface{}, opts ...options.Option) error { func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
m.RLock() m.RLock()
if !m.connected { if !m.connected {
m.RUnlock() m.RUnlock()
@ -91,133 +96,127 @@ func (m *MemoryBroker) Publish(ctx context.Context, message interface{}, opts ..
} }
m.RUnlock() m.RUnlock()
var err error
select {
case <-ctx.Done():
return ctx.Err()
default:
options := NewPublishOptions(opts...) options := NewPublishOptions(opts...)
var msgs []Message vs := make([]msgWrapper, 0, 1)
switch v := message.(type) { if m.opts.Codec == nil || options.BodyOnly {
case []Message: topic, _ := msg.Header.Get(metadata.HeaderTopic)
msgs = v vs = append(vs, msgWrapper{topic: topic, body: msg})
case Message:
msgs = append(msgs, v)
default:
return ErrInvalidMessage
}
msgTopicMap := make(map[string][]*memoryMessage)
for _, msg := range msgs {
p := &memoryMessage{opts: options}
p.topic, _ = msg.Header().Get(metadata.HeaderTopic)
if v, ok := msg.Body().(*codec.Frame); ok {
p.body = msg.Body()
} else if len(m.opts.Codecs) == 0 {
p.body = msg.Body()
} else { } else {
cf, ok := m.opts.Codecs[options.ContentType] topic, _ := msg.Header.Get(metadata.HeaderTopic)
if !ok { buf, err := m.opts.Codec.Marshal(msg)
return fmt.Errorf("%s: %s", codec.ErrUnknownContentType, options.ContentType)
}
p.body, err = cf.Marshal(v)
if err != nil { if err != nil {
return err return err
} }
vs = append(vs, msgWrapper{topic: topic, body: buf})
}
return m.publish(ctx, vs, opts...)
}
type msgWrapper struct {
topic string
body interface{}
}
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
m.RLock()
if !m.connected {
m.RUnlock()
return ErrNotConnected
}
m.RUnlock()
options := NewPublishOptions(opts...)
vs := make([]msgWrapper, 0, len(msgs))
if m.opts.Codec == nil || options.BodyOnly {
for _, msg := range msgs {
topic, _ := msg.Header.Get(metadata.HeaderTopic)
vs = append(vs, msgWrapper{topic: topic, body: msg})
}
} else {
for _, msg := range msgs {
topic, _ := msg.Header.Get(metadata.HeaderTopic)
buf, err := m.opts.Codec.Marshal(msg)
if err != nil {
return err
}
vs = append(vs, msgWrapper{topic: topic, body: buf})
}
}
return m.publish(ctx, vs, opts...)
}
func (m *memoryBroker) publish(ctx context.Context, vs []msgWrapper, opts ...PublishOption) error {
var err error
msgTopicMap := make(map[string]Events)
for _, v := range vs {
p := &memoryEvent{
topic: v.topic,
message: v.body,
opts: m.opts,
} }
msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p) msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p)
} }
beh := m.opts.BatchErrorHandler
eh := m.opts.ErrorHandler eh := m.opts.ErrorHandler
for t, ms := range msgTopicMap { for t, ms := range msgTopicMap {
ts := time.Now()
m.opts.Meter.Counter(semconv.PublishMessageInflight, "endpoint", t).Add(len(ms))
m.opts.Meter.Counter(semconv.SubscribeMessageInflight, "endpoint", t).Add(len(ms))
m.RLock() m.RLock()
subs, ok := m.subscribers[t] subs, ok := m.subscribers[t]
m.RUnlock() m.RUnlock()
if !ok { if !ok {
m.opts.Meter.Counter(semconv.PublishMessageTotal, "endpoint", t, "status", "failure").Add(len(ms))
m.opts.Meter.Counter(semconv.PublishMessageInflight, "endpoint", t).Add(-len(ms))
m.opts.Meter.Counter(semconv.SubscribeMessageInflight, "endpoint", t).Add(-len(ms))
continue continue
} }
m.opts.Meter.Counter(semconv.PublishMessageTotal, "endpoint", t, "status", "success").Add(len(ms))
for _, sub := range subs { for _, sub := range subs {
// batch processing
if sub.batchhandler != nil {
if err = sub.batchhandler(ms); err != nil {
ms.SetError(err)
if sub.opts.BatchErrorHandler != nil {
beh = sub.opts.BatchErrorHandler
}
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
if sub.handler != nil {
for _, p := range ms {
if err = sub.handler(p); err != nil {
p.SetError(err)
if sub.opts.ErrorHandler != nil { if sub.opts.ErrorHandler != nil {
eh = sub.opts.ErrorHandler eh = sub.opts.ErrorHandler
} }
switch mh := sub.handler.(type) {
case MessagesHandler:
mhs := make([]Message, 0, len(ms))
for _, m := range ms {
mhs = append(mhs, m)
}
if err = mh(mhs); err != nil {
m.opts.Meter.Counter(semconv.SubscribeMessageTotal, "endpoint", t, "status", "failure").Add(len(ms))
if eh != nil { if eh != nil {
switch meh := eh.(type) { eh(p)
case MessagesHandler:
_ = meh(mhs)
case MessageHandler:
for _, me := range mhs {
_ = meh(me)
}
}
} else if m.opts.Logger.V(logger.ErrorLevel) { } else if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, err.Error()) m.opts.Logger.Error(m.opts.Context, err.Error())
} }
} } else if sub.opts.AutoAck {
case MessageHandler:
for _, p := range ms {
if err = mh(p); err != nil {
m.opts.Meter.Counter(semconv.SubscribeMessageTotal, "endpoint", t, "status", "failure").Inc()
if eh != nil {
switch meh := eh.(type) {
case MessageHandler:
_ = meh(p)
case MessagesHandler:
_ = meh([]Message{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 { if err = p.Ack(); err != nil {
m.opts.Logger.Error(m.opts.Context, "ack failed: "+err.Error()) m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
m.opts.Meter.Counter(semconv.SubscribeMessageTotal, "endpoint", t, "status", "failure").Inc() }
} else {
m.opts.Meter.Counter(semconv.SubscribeMessageTotal, "endpoint", t, "status", "success").Inc()
}
} else {
m.opts.Meter.Counter(semconv.SubscribeMessageTotal, "endpoint", t, "status", "success").Inc()
}
}
m.opts.Meter.Counter(semconv.PublishMessageInflight, "endpoint", t).Add(-1)
m.opts.Meter.Counter(semconv.SubscribeMessageInflight, "endpoint", t).Add(-1)
} }
} }
} }
te := time.Since(ts)
m.opts.Meter.Summary(semconv.PublishMessageLatencyMicroseconds, "endpoint", t).Update(te.Seconds())
m.opts.Meter.Histogram(semconv.PublishMessageDurationSeconds, "endpoint", t).Update(te.Seconds())
m.opts.Meter.Summary(semconv.SubscribeMessageLatencyMicroseconds, "endpoint", t).Update(te.Seconds())
m.opts.Meter.Histogram(semconv.SubscribeMessageDurationSeconds, "endpoint", t).Update(te.Seconds())
} }
} }
return nil return nil
} }
func (m *MemoryBroker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...options.Option) (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()
@ -225,7 +224,7 @@ func (m *MemoryBroker) Subscribe(ctx context.Context, topic string, handler inte
} }
m.RUnlock() m.RUnlock()
sid, err := id.New() id, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -234,7 +233,52 @@ func (m *MemoryBroker) Subscribe(ctx context.Context, topic string, handler inte
sub := &memorySubscriber{ sub := &memorySubscriber{
exit: make(chan bool, 1), exit: make(chan bool, 1),
id: sid, id: id.String(),
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()
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
options := NewSubscribeOptions(opts...)
sub := &memorySubscriber{
exit: make(chan bool, 1),
id: id.String(),
topic: topic, topic: topic,
handler: handler, handler: handler,
opts: options, opts: options,
@ -262,54 +306,46 @@ func (m *MemoryBroker) Subscribe(ctx context.Context, topic string, handler inte
return sub, nil return sub, nil
} }
func (m *MemoryBroker) String() string { func (m *memoryBroker) String() string {
return "memory" return "memory"
} }
func (m *MemoryBroker) Name() string { func (m *memoryBroker) Name() string {
return m.opts.Name return m.opts.Name
} }
type memoryMessage struct { func (m *memoryEvent) Topic() string {
err error
body interface{}
topic string
header metadata.Metadata
opts PublishOptions
ctx context.Context
}
func (m *memoryMessage) Topic() string {
return m.topic return m.topic
} }
func (m *memoryMessage) Header() metadata.Metadata { func (m *memoryEvent) Message() *Message {
return m.header switch v := m.message.(type) {
} case *Message:
return v
case []byte:
msg := &Message{}
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
}
return nil
}
return msg
}
func (m *memoryMessage) Body() interface{} {
return m.body
}
func (m *memoryMessage) Ack() error {
return nil return nil
} }
func (m *memoryMessage) Error() error { func (m *memoryEvent) Ack() error {
return nil
}
func (m *memoryEvent) Error() error {
return m.err return m.err
} }
func (m *memoryMessage) Context() context.Context { func (m *memoryEvent) SetError(err error) {
return m.ctx m.err = err
}
type memorySubscriber struct {
ctx context.Context
exit chan bool
handler interface{}
id string
topic string
opts SubscribeOptions
} }
func (m *memorySubscriber) Options() SubscribeOptions { func (m *memorySubscriber) Options() SubscribeOptions {
@ -326,8 +362,8 @@ func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
} }
// NewBroker return new memory broker // NewBroker return new memory broker
func NewBroker(opts ...options.Option) *MemoryBroker { 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),
} }

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
) )
func TestMemoryBatchBroker(t *testing.T) { func TestMemoryBatchBroker(t *testing.T) {
@ -19,35 +19,29 @@ func TestMemoryBatchBroker(t *testing.T) {
topic := "test" topic := "test"
count := 10 count := 10
fn := func(evts []Message) error { fn := func(evts Events) error {
var err error return evts.Ack()
for _, evt := range evts {
if err = evt.Ack(); err != nil {
return err
}
}
return nil
} }
sub, err := b.Subscribe(ctx, topic, fn) sub, err := b.BatchSubscribe(ctx, topic, fn)
if err != nil { if err != nil {
t.Fatalf("Unexpected error subscribing %v", err) t.Fatalf("Unexpected error subscribing %v", err)
} }
msgs := make([]Message, 0, count) msgs := make([]*Message, 0, 0)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
message := &memoryMessage{ message := &Message{
header: metadata.Metadata{ Header: map[string]string{
metadata.HeaderTopic: []string{topic}, metadata.HeaderTopic: topic,
"foo": []string{"bar"}, "foo": "bar",
"id": []string{fmt.Sprintf("%d", i)}, "id": fmt.Sprintf("%d", i),
}, },
body: []byte(`"hello world"`), Body: []byte(`"hello world"`),
} }
msgs = append(msgs, message) msgs = append(msgs, message)
} }
if err := b.Publish(ctx, msgs); err != nil { if err := b.BatchPublish(ctx, msgs); err != nil {
t.Fatalf("Unexpected error publishing %v", err) t.Fatalf("Unexpected error publishing %v", err)
} }
@ -59,7 +53,6 @@ func TestMemoryBatchBroker(t *testing.T) {
t.Fatalf("Unexpected connect error %v", err) 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()
@ -71,8 +64,8 @@ func TestMemoryBroker(t *testing.T) {
topic := "test" topic := "test"
count := 10 count := 10
fn := func(p Message) error { fn := func(p Event) error {
return p.Ack() return nil
} }
sub, err := b.Subscribe(ctx, topic, fn) sub, err := b.Subscribe(ctx, topic, fn)
@ -80,20 +73,24 @@ func TestMemoryBroker(t *testing.T) {
t.Fatalf("Unexpected error subscribing %v", err) t.Fatalf("Unexpected error subscribing %v", err)
} }
msgs := make([]Message, 0, count) msgs := make([]*Message, 0, 0)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
message := &memoryMessage{ message := &Message{
header: metadata.Metadata{ Header: map[string]string{
metadata.HeaderTopic: []string{topic}, metadata.HeaderTopic: topic,
"foo": []string{"bar"}, "foo": "bar",
"id": []string{fmt.Sprintf("%d", i)}, "id": fmt.Sprintf("%d", i),
}, },
body: []byte(`"hello world"`), Body: []byte(`"hello world"`),
} }
msgs = append(msgs, message) msgs = append(msgs, message)
if err := b.Publish(ctx, topic, message); err != nil {
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
}
} }
if err := b.Publish(ctx, msgs); err != nil { if err := b.BatchPublish(ctx, msgs); err != nil {
t.Fatalf("Unexpected error publishing %v", err) t.Fatalf("Unexpected error publishing %v", err)
} }

View File

@ -5,14 +5,11 @@ import (
"crypto/tls" "crypto/tls"
"time" "time"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/meter"
"go.unistack.org/micro/v4/meter" "github.com/unistack-org/micro/v3/register"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/tracer"
"go.unistack.org/micro/v4/register"
"go.unistack.org/micro/v4/sync"
"go.unistack.org/micro/v4/tracer"
) )
// Options struct // Options struct
@ -21,8 +18,8 @@ type Options struct {
Tracer tracer.Tracer Tracer tracer.Tracer
// Register can be used for clustering // Register can be used for clustering
Register register.Register Register register.Register
// Codecs holds the codec for marshal/unmarshal // Codec holds the codec for marshal/unmarshal
Codecs map[string]codec.Codec Codec codec.Codec
// Logger used for logging // Logger used for logging
Logger logger.Logger Logger logger.Logger
// Meter used for metrics // Meter used for metrics
@ -31,51 +28,49 @@ type Options struct {
Context context.Context Context context.Context
// TLSConfig holds tls.TLSConfig options // TLSConfig holds tls.TLSConfig options
TLSConfig *tls.Config TLSConfig *tls.Config
// ErrorHandler used when broker have error while processing message // ErrorHandler used when broker can't unmarshal incoming message
ErrorHandler interface{} 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
// Address holds the broker address // Addrs holds the broker address
Address []string Addrs []string
Wait *sync.WaitGroup
GracefulTimeout time.Duration
} }
// NewOptions create new Options // NewOptions create new Options
func NewOptions(opts ...options.Option) Options { func NewOptions(opts ...Option) Options {
newOpts := Options{ options := Options{
Register: register.DefaultRegister, Register: register.DefaultRegister,
Logger: logger.DefaultLogger, Logger: logger.DefaultLogger,
Context: context.Background(), Context: context.Background(),
Meter: meter.DefaultMeter, Meter: meter.DefaultMeter,
Codecs: make(map[string]codec.Codec), Codec: codec.DefaultCodec,
Tracer: tracer.DefaultTracer, Tracer: tracer.DefaultTracer,
GracefulTimeout: DefaultGracefulTimeout,
} }
for _, o := range opts { for _, o := range opts {
o(&newOpts) o(&options)
}
return options
}
// Context sets the context option
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
} }
return newOpts
} }
// PublishOptions struct // PublishOptions struct
type PublishOptions struct { type PublishOptions struct {
// Context holds external options // Context holds external options
Context context.Context Context context.Context
// Message metadata usually passed as message headers
Metadata metadata.Metadata
// Content-Type of message for marshal
ContentType string
// Topic destination
Topic string
// BodyOnly flag says the message contains raw body bytes // BodyOnly flag says the message contains raw body bytes
BodyOnly bool BodyOnly bool
} }
// NewPublishOptions creates PublishOptions struct // NewPublishOptions creates PublishOptions struct
func NewPublishOptions(opts ...options.Option) PublishOptions { func NewPublishOptions(opts ...PublishOption) PublishOptions {
options := PublishOptions{ options := PublishOptions{
Context: context.Background(), Context: context.Background(),
} }
@ -85,21 +80,16 @@ func NewPublishOptions(opts ...options.Option) PublishOptions {
return options return options
} }
// PublishTopic pass topic for messages
func PublishTopic(t string) options.Option {
return func(src interface{}) error {
return options.Set(src, t, ".Topic")
}
}
// SubscribeOptions struct // SubscribeOptions struct
type SubscribeOptions struct { type SubscribeOptions struct {
// Context holds external options // Context holds external options
Context context.Context Context context.Context
// ErrorHandler used when broker have error while processing message // ErrorHandler used when broker can't unmarshal incoming message
ErrorHandler interface{} ErrorHandler Handler
// QueueGroup holds consumer group // BatchErrorHandler used when broker can't unmashal incoming messages
QueueGroup string BatchErrorHandler BatchHandler
// Group holds consumer group
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
@ -110,16 +100,179 @@ type SubscribeOptions struct {
BatchWait time.Duration BatchWait time.Duration
} }
// ErrorHandler will catch all broker errors that cant be handled // Option func
// in normal way, for example Codec errors type Option func(*Options)
func ErrorHandler(h interface{}) options.Option {
return func(src interface{}) error { // PublishOption func
return options.Set(src, h, ".ErrorHandler") type PublishOption func(*PublishOptions)
// PublishBodyOnly publish only body of the message
func PublishBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
} }
} }
// PublishContext sets the context
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
// Addrs sets the host addresses to be used by the broker
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
}
}
// Codec sets the codec used for encoding/decoding used where
// a broker does not support headers
func Codec(c codec.Codec) Option {
return func(o *Options) {
o.Codec = c
}
}
// ErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func ErrorHandler(h Handler) Option {
return func(o *Options) {
o.ErrorHandler = h
}
}
// BatchErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func BatchErrorHandler(h BatchHandler) Option {
return func(o *Options) {
o.BatchErrorHandler = h
}
}
// SubscribeErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func SubscribeErrorHandler(h Handler) SubscribeOption {
return func(o *SubscribeOptions) {
o.ErrorHandler = h
}
}
// SubscribeBatchErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func SubscribeBatchErrorHandler(h BatchHandler) SubscribeOption {
return func(o *SubscribeOptions) {
o.BatchErrorHandler = h
}
}
// Queue sets the subscribers queue
// Deprecated
func Queue(name string) SubscribeOption {
return func(o *SubscribeOptions) {
o.Group = name
}
}
// SubscribeGroup sets the name of the queue to share messages on
func SubscribeGroup(name string) SubscribeOption {
return func(o *SubscribeOptions) {
o.Group = name
}
}
// Register sets register option
func Register(r register.Register) Option {
return func(o *Options) {
o.Register = r
}
}
// TLSConfig sets the TLS Config
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// SubscribeContext set context
func SubscribeContext(ctx context.Context) SubscribeOption {
return func(o *SubscribeOptions) {
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 // NewSubscribeOptions creates new SubscribeOptions
func NewSubscribeOptions(opts ...options.Option) SubscribeOptions { func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
options := SubscribeOptions{ options := SubscribeOptions{
AutoAck: true, AutoAck: true,
Context: context.Background(), Context: context.Background(),
@ -129,39 +282,3 @@ func NewSubscribeOptions(opts ...options.Option) SubscribeOptions {
} }
return options return options
} }
// SubscribeAutoAck contol auto acking of messages
// after they have been handled.
func SubscribeAutoAck(b bool) options.Option {
return func(src interface{}) error {
return options.Set(src, b, ".AutoAck")
}
}
// BodyOnly transfer only body without
func BodyOnly(b bool) options.Option {
return func(src interface{}) error {
return options.Set(src, b, ".BodyOnly")
}
}
// SubscribeBatchSize specifies max batch size
func SubscribeBatchSize(n int) options.Option {
return func(src interface{}) error {
return options.Set(src, n, ".BatchSize")
}
}
// SubscribeBatchWait specifies max batch wait time
func SubscribeBatchWait(td time.Duration) options.Option {
return func(src interface{}) error {
return options.Set(src, td, ".BatchWait")
}
}
// SubscribeQueueGroup sets the shared queue name distributed messages across subscribers
func SubscribeQueueGroup(n string) options.Option {
return func(src interface{}) error {
return options.Set(src, n, ".QueueGroup")
}
}

View File

@ -1,98 +0,0 @@
package broker
import (
"fmt"
"reflect"
"strings"
"unicode"
"unicode/utf8"
)
const (
subSig = "func(context.Context, interface{}) error"
batchSubSig = "func([]context.Context, []interface{}) error"
)
// Precompute the reflect type for error. Can't use error directly
// because Typeof takes an empty interface value. This is annoying.
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
// Is this an exported - upper case - name?
func isExported(name string) bool {
r, _ := utf8.DecodeRuneInString(name)
return unicode.IsUpper(r)
}
// Is this type exported or a builtin?
func isExportedOrBuiltinType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
// PkgPath will be non-empty even for an exported type,
// so we need to check the type name as well.
return isExported(t.Name()) || t.PkgPath() == ""
}
// ValidateSubscriber func signature
func ValidateSubscriber(sub interface{}) error {
typ := reflect.TypeOf(sub)
var argType reflect.Type
switch typ.Kind() {
case reflect.Func:
name := "Func"
switch typ.NumIn() {
case 1: // func(Message) error
case 2: // func(context.Context, Message) error or func(context.Context, []Message) error
argType = typ.In(2)
// 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:
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s or %s", name, typ.NumIn(), subSig, batchSubSig)
}
if !isExportedOrBuiltinType(argType) {
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
}
if typ.NumOut() != 1 {
return fmt.Errorf("subscriber %v has wrong number of return values: %v require signature %s or %s",
name, typ.NumOut(), subSig, batchSubSig)
}
if returnType := typ.Out(0); returnType != typeOfError {
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
}
default:
hdlr := reflect.ValueOf(sub)
name := reflect.Indirect(hdlr).Type().Name()
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
switch method.Type.NumIn() {
case 3:
argType = method.Type.In(2)
default:
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s or %s",
name, method.Name, method.Type.NumIn(), subSig, batchSubSig)
}
if !isExportedOrBuiltinType(argType) {
return fmt.Errorf("%v argument type not exported: %v", name, argType)
}
if method.Type.NumOut() != 1 {
return fmt.Errorf(
"subscriber %v.%v has wrong number of return values: %v require signature %s or %s",
name, method.Name, method.Type.NumOut(), subSig, batchSubSig)
}
if returnType := method.Type.Out(0); returnType != typeOfError {
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
}
}
}
return nil
}

32
build/build.go Normal file
View File

@ -0,0 +1,32 @@
// Package build is for building source into a package
package build
// Build is an interface for building packages
type Build interface {
// Package builds a package
Package(name string, src *Source) (*Package, error)
// Remove removes the package
Remove(*Package) error
}
// Source is the source of a build
type Source struct {
// Path to the source if local
Path string
// Language is the language of code
Language string
// Location of the source
Repository string
}
// Package is packaged format for source
type Package struct {
// Source of the package
Source *Source
// Name of the package
Name string
// Location of the package
Path string
// Type of package e.g tarball, binary, docker
Type string
}

17
build/options.go Normal file
View File

@ -0,0 +1,17 @@
package build
// Options struct
type Options struct {
// local path to download source
Path string
}
// Option func
type Option func(o *Options)
// Path is the Local path for repository
func Path(p string) Option {
return func(o *Options) {
o.Path = p
}
}

View File

@ -2,29 +2,14 @@ package client
import ( import (
"context" "context"
"math"
"time" "time"
"go.unistack.org/micro/v4/util/backoff" "github.com/unistack-org/micro/v3/util/backoff"
) )
// BackoffFunc is the backoff call func // BackoffFunc is the backoff call func
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error) type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
// BackoffExp using exponential backoff func func exponentialBackoff(ctx context.Context, req Request, attempts int) (time.Duration, error) {
func BackoffExp(_ context.Context, _ Request, attempts int) (time.Duration, error) {
return backoff.Do(attempts), nil return backoff.Do(attempts), nil
} }
// BackoffInterval specifies randomization interval for backoff func
func BackoffInterval(min time.Duration, max time.Duration) BackoffFunc {
return func(_ context.Context, _ Request, attempts int) (time.Duration, error) {
td := time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100
if td < min {
return min, nil
} else if td > max {
return max, nil
}
return td, nil
}
}

View File

@ -6,7 +6,7 @@ import (
"time" "time"
) )
func TestBackoffExp(t *testing.T) { func TestBackoff(t *testing.T) {
results := []time.Duration{ results := []time.Duration{
0 * time.Second, 0 * time.Second,
100 * time.Millisecond, 100 * time.Millisecond,
@ -22,7 +22,7 @@ func TestBackoffExp(t *testing.T) {
} }
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
d, err := BackoffExp(context.TODO(), r, i) d, err := exponentialBackoff(context.TODO(), r, i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -32,25 +32,3 @@ func TestBackoffExp(t *testing.T) {
} }
} }
} }
func TestBackoffInterval(t *testing.T) {
min := 100 * time.Millisecond
max := 300 * time.Millisecond
r := &testRequest{
service: "test",
method: "test",
}
fn := BackoffInterval(min, max)
for i := 0; i < 5; i++ {
d, err := fn(context.TODO(), r, i)
if err != nil {
t.Fatal(err)
}
if d < min || d > max {
t.Fatalf("Expected %v < %v < %v", min, d, max)
}
}
}

View File

@ -1,29 +1,27 @@
// Package client is an interface for an RPC client // Package client is an interface for an RPC client
package client // import "go.unistack.org/micro/v4/client" package client
import ( import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/metadata"
) )
var ( var (
// DefaultClient is the global default client // DefaultClient is the global default client
DefaultClient = NewClient() DefaultClient Client = NewClient()
// DefaultContentType is the default content-type if not specified // DefaultContentType is the default content-type if not specified
DefaultContentType = "" DefaultContentType = "application/json"
// DefaultBackoff is the default backoff function for retries (minimum 10 millisecond and maximum 5 second) // DefaultBackoff is the default backoff function for retries
DefaultBackoff = BackoffInterval(10*time.Millisecond, 5*time.Second) DefaultBackoff = exponentialBackoff
// DefaultRetry is the default check-for-retry function for retries // DefaultRetry is the default check-for-retry function for retries
DefaultRetry = RetryNever DefaultRetry = RetryNever
// DefaultRetries is the default number of times a request is tried // DefaultRetries is the default number of times a request is tried
DefaultRetries = 0 DefaultRetries = 0
// DefaultRequestTimeout is the default request timeout // DefaultRequestTimeout is the default request timeout
DefaultRequestTimeout = time.Second * 5 DefaultRequestTimeout = time.Second * 5
// DefaultDialTimeout the default dial timeout
DefaultDialTimeout = time.Second * 5
// DefaultPoolSize sets the connection pool size // DefaultPoolSize sets the connection pool size
DefaultPoolSize = 100 DefaultPoolSize = 100
// DefaultPoolTTL sets the connection pool ttl // DefaultPoolTTL sets the connection pool ttl
@ -35,14 +33,24 @@ var (
// It also supports bidirectional streaming of requests. // It also supports bidirectional streaming of requests.
type Client interface { type Client interface {
Name() string Name() string
Init(opts ...options.Option) error Init(opts ...Option) error
Options() Options Options() Options
NewRequest(service string, endpoint string, req interface{}, opts ...options.Option) Request NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
Call(ctx context.Context, req Request, rsp interface{}, opts ...options.Option) error NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
Stream(ctx context.Context, req Request, opts ...options.Option) (Stream, error) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
BatchPublish(ctx context.Context, msg []Message, opts ...PublishOption) error
String() string String() string
} }
// Message is the interface for publishing asynchronously
type Message interface {
Topic() string
Payload() interface{}
ContentType() string
}
// Request is the interface for a synchronous request used by Call or Stream // Request is the interface for a synchronous request used by Call or Stream
type Request interface { type Request interface {
// The service to call // The service to call
@ -59,22 +67,16 @@ type Request interface {
Codec() codec.Codec Codec() codec.Codec
// indicates whether the request will be a streaming one rather than unary // indicates whether the request will be a streaming one rather than unary
Stream() bool Stream() bool
// Header data
// Header() metadata.Metadata
} }
// Response is the response received from a service // Response is the response received from a service
type Response interface { type Response interface {
// Read the response // Read the response
Codec() codec.Codec Codec() codec.Codec
// The content type // read the header
// ContentType() string Header() metadata.Metadata
// Header data
// Header() metadata.Metadata
// Read the undecoded response // Read the undecoded response
Read() ([]byte, error) Read() ([]byte, error)
// The unencoded request body
// Body() interface{}
} }
// Stream is the interface for a bidirectional synchronous stream // Stream is the interface for a bidirectional synchronous stream
@ -89,14 +91,23 @@ type Stream interface {
Send(msg interface{}) error Send(msg interface{}) error
// Recv will decode and read a response // Recv will decode and read a response
Recv(msg interface{}) error Recv(msg interface{}) error
// SendMsg will encode and send a request
SendMsg(msg interface{}) error
// RecvMsg will decode and read a response
RecvMsg(msg interface{}) error
// Error returns the stream error // Error returns the stream error
Error() error Error() error
// Close closes the stream // Close closes the stream
Close() error Close() error
// CloseSend closes the send direction of the stream
CloseSend() error
} }
// Option used by the Client
type Option func(*Options)
// CallOption used by Call or Stream
type CallOption func(*CallOptions)
// PublishOption used by Publish
type PublishOption func(*PublishOptions)
// MessageOption used by NewMessage
type MessageOption func(*MessageOptions)
// RequestOption used by NewRequest
type RequestOption func(*RequestOptions)

View File

@ -2,24 +2,22 @@ package client
import ( import (
"context" "context"
"go.unistack.org/micro/v4/options"
) )
type clientCallOptions struct { type clientCallOptions struct {
Client Client
opts []options.Option opts []CallOption
} }
func (s *clientCallOptions) Call(ctx context.Context, req Request, rsp interface{}, opts ...options.Option) error { func (s *clientCallOptions) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
return s.Client.Call(ctx, req, rsp, append(s.opts, opts...)...) return s.Client.Call(ctx, req, rsp, append(s.opts, opts...)...)
} }
func (s *clientCallOptions) Stream(ctx context.Context, req Request, opts ...options.Option) (Stream, error) { func (s *clientCallOptions) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
return s.Client.Stream(ctx, req, append(s.opts, opts...)...) return s.Client.Stream(ctx, req, append(s.opts, opts...)...)
} }
// NewClientCallOptions add CallOption to every call // NewClientCallOptions add CallOption to every call
func NewClientCallOptions(c Client, opts ...options.Option) Client { func NewClientCallOptions(c Client, opts ...CallOption) Client {
return &clientCallOptions{c, opts} return &clientCallOptions{c, opts}
} }

View File

@ -1,28 +0,0 @@
package client
import (
"context"
"testing"
"time"
"go.unistack.org/micro/v4/options"
)
func TestNewClientCallOptions(t *testing.T) {
var flag bool
w := func(fn CallFunc) CallFunc {
flag = true
return fn
}
c := NewClientCallOptions(NewClient(),
options.Address("127.0.0.1"),
WithCallWrapper(w),
RequestTimeout(1*time.Millisecond),
Retries(0),
Backoff(BackoffInterval(10*time.Millisecond, 100*time.Millisecond)),
)
_ = c.Call(context.TODO(), c.NewRequest("service", "endpoint", nil), nil)
if !flag {
t.Fatalf("NewClientCallOptions not works")
}
}

View File

@ -22,3 +22,33 @@ func NewContext(ctx context.Context, c Client) context.Context {
} }
return context.WithValue(ctx, clientKey{}, c) return context.WithValue(ctx, clientKey{}, c)
} }
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetCallOption returns a function to setup a context with given value
func SetCallOption(k, v interface{}) CallOption {
return func(o *CallOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetOption returns a function to setup a context with given value
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)
}
}

View File

@ -1,40 +0,0 @@
package client
import (
"context"
"testing"
)
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), clientKey{}, NewClient())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestFromNilContext(t *testing.T) {
// nolint: staticcheck
c, ok := FromContext(nil)
if ok || c != nil {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewClient())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestNewNilContext(t *testing.T) {
// nolint: staticcheck
ctx := NewContext(nil, NewClient())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}

View File

@ -4,15 +4,15 @@ import (
"context" "context"
"sort" "sort"
"go.unistack.org/micro/v4/errors" "github.com/unistack-org/micro/v3/errors"
"go.unistack.org/micro/v4/router" "github.com/unistack-org/micro/v3/router"
) )
// LookupFunc is used to lookup routes for a service // LookupFunc is used to lookup routes for a service
type LookupFunc func(context.Context, Request, CallOptions) ([]string, error) type LookupFunc func(context.Context, Request, CallOptions) ([]string, error)
// LookupRoute for a request using the router and then choose one using the selector // LookupRoute for a request using the router and then choose one using the selector
func LookupRoute(_ context.Context, req Request, opts CallOptions) ([]string, error) { func LookupRoute(ctx context.Context, req Request, opts CallOptions) ([]string, error) {
// check to see if an address was provided as a call option // check to see if an address was provided as a call option
if len(opts.Address) > 0 { if len(opts.Address) > 0 {
return opts.Address, nil return opts.Address, nil

View File

@ -2,15 +2,11 @@ package client
import ( import (
"context" "context"
"fmt"
"time"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/broker"
"go.unistack.org/micro/v4/errors" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/errors"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/metadata"
"go.unistack.org/micro/v4/selector"
"go.unistack.org/micro/v4/semconv"
) )
// DefaultCodecs will be used to encode/decode data // DefaultCodecs will be used to encode/decode data
@ -22,6 +18,12 @@ type noopClient struct {
opts Options opts Options
} }
type noopMessage struct {
topic string
payload interface{}
opts MessageOptions
}
type noopRequest struct { type noopRequest struct {
body interface{} body interface{}
codec codec.Codec codec codec.Codec
@ -33,12 +35,16 @@ type noopRequest struct {
} }
// NewClient returns new noop client // NewClient returns new noop client
func NewClient(opts ...options.Option) Client { func NewClient(opts ...Option) Client {
nc := &noopClient{opts: NewOptions(opts...)} nc := &noopClient{opts: NewOptions(opts...)}
// wrap in reverse // wrap in reverse
c := Client(nc) c := Client(nc)
for i := len(nc.opts.Wrappers); i > 0; i-- {
c = nc.opts.Wrappers[i-1](c)
}
return c return c
} }
@ -113,14 +119,6 @@ func (n *noopStream) Recv(interface{}) error {
return nil return nil
} }
func (n *noopStream) SendMsg(interface{}) error {
return nil
}
func (n *noopStream) RecvMsg(interface{}) error {
return nil
}
func (n *noopStream) Error() error { func (n *noopStream) Error() error {
return nil return nil
} }
@ -129,8 +127,16 @@ func (n *noopStream) Close() error {
return nil return nil
} }
func (n *noopStream) CloseSend() error { func (n *noopMessage) Topic() string {
return nil return n.topic
}
func (n *noopMessage) Payload() interface{} {
return n.payload
}
func (n *noopMessage) ContentType() string {
return n.opts.ContentType
} }
func (n *noopClient) newCodec(contentType string) (codec.Codec, error) { func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
@ -143,7 +149,7 @@ func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
return nil, codec.ErrUnknownContentType return nil, codec.ErrUnknownContentType
} }
func (n *noopClient) Init(opts ...options.Option) error { func (n *noopClient) Init(opts ...Option) error {
for _, o := range opts { for _, o := range opts {
o(&n.opts) o(&n.opts)
} }
@ -158,307 +164,77 @@ func (n *noopClient) String() string {
return "noop" return "noop"
} }
func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...options.Option) error { func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
// make a copy of call opts
callOpts := n.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// check if we already have a deadline
d, ok := ctx.Deadline()
if !ok {
var cancel context.CancelFunc
// no deadline so we create a new one
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
defer cancel()
} else {
// got a deadline so no need to setup context
// but we need to set the timeout we pass along
opt := RequestTimeout(time.Until(d))
opt(&callOpts)
}
// should we noop right here?
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make copy of call method
hcall := n.call
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
hcall = callOpts.CallWrappers[i-1](hcall)
}
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = n.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = n.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(n.opts.Proxy) > 0 {
callOpts.Address = []string{n.opts.Proxy}
}
var next selector.Next
// return errors.New("go.micro.client", "request timeout", 408)
call := func(i int) error {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
if next == nil {
var routes []string
// lookup the route to send the reques to
// TODO apply any filtering here
routes, err = n.opts.Lookup(ctx, req, callOpts)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err = callOpts.Selector.Select(routes)
if err != nil {
return err
}
}
node := next()
// make the call
err = hcall(ctx, node, req, rsp, callOpts)
// record the result of the call to inform future routing decisions
if verr := n.opts.Selector.Record(node, err); verr != nil {
return verr
}
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return verr
}
return err
}
ch := make(chan error, callOpts.Retries)
var gerr error
ts := time.Now()
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", endpoint).Inc()
for i := 0; i <= callOpts.Retries; i++ {
go func() {
ch <- call(i)
}()
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case err := <-ch:
// if the call succeeded lets bail early
if err == nil {
return nil
}
retry, rerr := callOpts.Retry(ctx, req, i, err)
if rerr != nil {
return rerr
}
if !retry {
return err
}
gerr = err
}
}
if gerr != nil {
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", endpoint, "status", "failure").Inc()
} else {
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", endpoint, "status", "success").Inc()
}
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", endpoint).Dec()
te := time.Since(ts)
n.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", endpoint).Update(te.Seconds())
n.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", endpoint).Update(te.Seconds())
return gerr
}
func (n *noopClient) call(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
return nil return nil
} }
func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts ...options.Option) Request { func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts ...RequestOption) Request {
return &noopRequest{service: service, endpoint: endpoint} return &noopRequest{service: service, endpoint: endpoint}
} }
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...options.Option) (Stream, error) { func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
var err error options := NewMessageOptions(append([]MessageOption{MessageContentType(n.opts.ContentType)}, opts...)...)
return &noopMessage{topic: topic, payload: msg, opts: options}
// make a copy of call opts
callOpts := n.opts.CallOptions
for _, o := range opts {
o(&callOpts)
}
// check if we already have a deadline
d, ok := ctx.Deadline()
if !ok && callOpts.StreamTimeout > time.Duration(0) {
var cancel context.CancelFunc
// no deadline so we create a new one
ctx, cancel = context.WithTimeout(ctx, callOpts.StreamTimeout)
defer cancel()
} else {
// got a deadline so no need to setup context
// but we need to set the timeout we pass along
o := StreamTimeout(time.Until(d))
o(&callOpts)
}
// should we noop right here?
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
/*
// make copy of call method
hstream := h.stream
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
hstream = callOpts.CallWrappers[i-1](hstream)
}
*/
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = n.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = n.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(n.opts.Proxy) > 0 {
callOpts.Address = []string{n.opts.Proxy}
}
var next selector.Next
call := func(i int) (Stream, error) {
// call backoff first. Someone may want an initial start delay
t, cerr := callOpts.Backoff(ctx, req, i)
if cerr != nil {
return nil, errors.InternalServerError("go.micro.client", cerr.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
if next == nil {
var routes []string
// lookup the route to send the reques to
// TODO apply any filtering here
routes, err = n.opts.Lookup(ctx, req, callOpts)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err = callOpts.Selector.Select(routes)
if err != nil {
return nil, err
}
}
node := next()
// ts := time.Now()
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", endpoint).Inc()
stream, cerr := n.stream(ctx, node, req, callOpts)
if cerr != nil {
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", endpoint, "status", "failure").Inc()
} else {
n.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", endpoint, "status", "success").Inc()
}
// record the result of the call to inform future routing decisions
if verr := n.opts.Selector.Record(node, cerr); verr != nil {
return nil, verr
}
// try and transform the error to a go-micro error
if verr, ok := cerr.(*errors.Error); ok {
return nil, verr
}
return stream, cerr
}
type response struct {
stream Stream
err error
}
ch := make(chan response, callOpts.Retries)
var grr error
for i := 0; i <= callOpts.Retries; i++ {
go func() {
s, cerr := call(i)
ch <- response{s, cerr}
}()
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case rsp := <-ch:
// if the call succeeded lets bail early
if rsp.err == nil {
return rsp.stream, nil
}
retry, rerr := callOpts.Retry(ctx, req, i, err)
if rerr != nil {
return nil, rerr
}
if !retry {
return nil, rsp.err
}
grr = rsp.err
}
}
return nil, grr
} }
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (*noopStream, error) { func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
return &noopStream{}, nil return &noopStream{}, nil
} }
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
return n.publish(ctx, ps, opts...)
}
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
return n.publish(ctx, []Message{p}, opts...)
}
func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishOption) error {
options := NewPublishOptions(opts...)
msgs := make([]*broker.Message, 0, len(ps))
for _, p := range ps {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.New(0)
}
md[metadata.HeaderContentType] = p.ContentType()
topic := p.Topic()
// get the exchange
if len(options.Exchange) > 0 {
topic = options.Exchange
}
md[metadata.HeaderTopic] = topic
var body []byte
// passed in raw data
if d, ok := p.Payload().(*codec.Frame); ok {
body = d.Data
} else {
// use codec for payload
cf, err := n.newCodec(p.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// set the body
b, err := cf.Marshal(p.Payload())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
body = b
}
msgs = append(msgs, &broker.Message{Header: md, Body: body})
}
return n.opts.Broker.BatchPublish(ctx, msgs,
broker.PublishContext(options.Context),
broker.PublishBodyOnly(options.BodyOnly),
)
}

View File

@ -3,18 +3,18 @@ package client
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"net"
"time" "time"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/broker"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "github.com/unistack-org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/network/transport"
"go.unistack.org/micro/v4/router" "github.com/unistack-org/micro/v3/register"
"go.unistack.org/micro/v4/selector" "github.com/unistack-org/micro/v3/router"
"go.unistack.org/micro/v4/selector/random" "github.com/unistack-org/micro/v3/selector"
"go.unistack.org/micro/v4/tracer" "github.com/unistack-org/micro/v3/selector/random"
"github.com/unistack-org/micro/v3/tracer"
) )
// Options holds client options // Options holds client options
@ -25,38 +25,40 @@ type Options struct {
Logger logger.Logger Logger logger.Logger
// Tracer used for tracing // Tracer used for tracing
Tracer tracer.Tracer Tracer tracer.Tracer
// Broker used to publish messages
Broker broker.Broker
// Meter used for metrics // Meter used for metrics
Meter meter.Meter Meter meter.Meter
// Context is used for external options
Context context.Context
// Router used to get route // Router used to get route
Router router.Router Router router.Router
// TLSConfig specifies tls.Config for secure connection // Transport used for transfer messages
TLSConfig *tls.Config Transport transport.Transport
// Codecs map // Context is used for external options
Codecs map[string]codec.Codec Context context.Context
// Lookup func used to get destination addr // Lookup func used to get destination addr
Lookup LookupFunc Lookup LookupFunc
// Codecs map
Codecs map[string]codec.Codec
// TLSConfig specifies tls.Config for secure connection
TLSConfig *tls.Config
// 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
ContentType string ContentType string
// Name is the client name // Name is the client name
Name string Name string
// Wrappers contains wrappers
Wrappers []Wrapper
// CallOptions contains default CallOptions // CallOptions contains default CallOptions
CallOptions CallOptions CallOptions CallOptions
// PoolSize connection pool size // PoolSize connection pool size
PoolSize int PoolSize int
// PoolTTL connection pool ttl // PoolTTL connection pool ttl
PoolTTL time.Duration PoolTTL time.Duration
// ContextDialer used to connect
ContextDialer func(context.Context, string) (net.Conn, error)
// Hooks may contains Client func wrapper
Hooks options.Hooks
} }
// NewCallOptions creates new call options struct // NewCallOptions creates new call options struct
func NewCallOptions(opts ...options.Option) CallOptions { func NewCallOptions(opts ...CallOption) CallOptions {
options := CallOptions{} options := CallOptions{}
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@ -66,12 +68,12 @@ func NewCallOptions(opts ...options.Option) 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
@ -80,39 +82,67 @@ type CallOptions struct {
Network string Network string
// Content-Type // Content-Type
ContentType string ContentType string
// AuthToken string
AuthToken string
// Address specifies static addr list
Address []string
// SelectOptions selector options
SelectOptions []selector.SelectOption
// CallWrappers call wrappers // CallWrappers call wrappers
CallWrappers []CallWrapper CallWrappers []CallWrapper
// SelectOptions selector options
SelectOptions []selector.SelectOption
// Address specifies static addr list
Address []string
// Retries specifies retries num
Retries int
// StreamTimeout stream timeout // StreamTimeout stream timeout
StreamTimeout time.Duration StreamTimeout time.Duration
// RequestTimeout request timeout // RequestTimeout request timeout
RequestTimeout time.Duration RequestTimeout time.Duration
// RequestMetadata holds additional metadata for call
RequestMetadata metadata.Metadata
// ResponseMetadata holds additional metadata from call
ResponseMetadata *metadata.Metadata
// DialTimeout dial timeout // DialTimeout dial timeout
DialTimeout time.Duration DialTimeout time.Duration
// Retries specifies retries num // AuthToken string
Retries int AuthToken string
// ContextDialer used to connect
ContextDialer func(context.Context, string) (net.Conn, error)
} }
// ContextDialer pass ContextDialer to client // Context pass context to client
func ContextDialer(fn func(context.Context, string) (net.Conn, error)) options.Option { func Context(ctx context.Context) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".ContextDialer") o.Context = ctx
} }
} }
// NewPublishOptions create new PublishOptions struct from option
func NewPublishOptions(opts ...PublishOption) PublishOptions {
options := PublishOptions{}
for _, o := range opts {
o(&options)
}
return options
}
// PublishOptions holds publish options
type PublishOptions struct {
// BodyOnly will publish only message body
BodyOnly bool
// Context used for external options
Context context.Context
// Exchange topic exchange name
Exchange string
}
// NewMessageOptions creates message options struct
func NewMessageOptions(opts ...MessageOption) MessageOptions {
options := MessageOptions{}
for _, o := range opts {
o(&options)
}
return options
}
// MessageOptions holds client message options
type MessageOptions struct {
// ContentType specify content-type of message
ContentType string
}
// NewRequestOptions creates new RequestOptions struct // NewRequestOptions creates new RequestOptions struct
func NewRequestOptions(opts ...options.Option) RequestOptions { func NewRequestOptions(opts ...RequestOption) RequestOptions {
options := RequestOptions{} options := RequestOptions{}
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@ -131,7 +161,7 @@ type RequestOptions struct {
} }
// NewOptions creates new options struct // NewOptions creates new options struct
func NewOptions(opts ...options.Option) Options { func NewOptions(opts ...Option) Options {
options := Options{ options := Options{
Context: context.Background(), Context: context.Background(),
ContentType: DefaultContentType, ContentType: DefaultContentType,
@ -142,16 +172,18 @@ func NewOptions(opts ...options.Option) Options {
Retry: DefaultRetry, Retry: DefaultRetry,
Retries: DefaultRetries, Retries: DefaultRetries,
RequestTimeout: DefaultRequestTimeout, RequestTimeout: DefaultRequestTimeout,
DialTimeout: DefaultDialTimeout, DialTimeout: transport.DefaultDialTimeout,
}, },
Lookup: LookupRoute, Lookup: LookupRoute,
PoolSize: DefaultPoolSize, PoolSize: DefaultPoolSize,
PoolTTL: DefaultPoolTTL, PoolTTL: DefaultPoolTTL,
Selector: random.NewSelector(), Selector: random.NewSelector(),
Logger: logger.DefaultLogger, Logger: logger.DefaultLogger,
Broker: broker.DefaultBroker,
Meter: meter.DefaultMeter, Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer, Tracer: tracer.DefaultTracer,
Router: router.DefaultRouter, Router: router.DefaultRouter,
Transport: transport.DefaultTransport,
} }
for _, o := range opts { for _, o := range opts {
@ -161,131 +193,351 @@ func NewOptions(opts ...options.Option) Options {
return options return options
} }
// Broker to be used for pub/sub
func Broker(b broker.Broker) Option {
return func(o *Options) {
o.Broker = b
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Logger to be used for log mesages
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter to be used for metrics
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Codec to be used to encode/decode requests for a given content type
func Codec(contentType string, c codec.Codec) Option {
return func(o *Options) {
o.Codecs[contentType] = c
}
}
// ContentType used by default if not specified
func ContentType(ct string) Option {
return func(o *Options) {
o.ContentType = ct
}
}
// Proxy sets the proxy address // Proxy sets the proxy address
func Proxy(addr string) options.Option { func Proxy(addr string) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, addr, ".Proxy") o.Proxy = addr
} }
} }
// PoolSize sets the connection pool size // PoolSize sets the connection pool size
func PoolSize(d int) options.Option { func PoolSize(d int) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, d, ".PoolSize") o.PoolSize = d
} }
} }
// PoolTTL sets the connection pool ttl // PoolTTL sets the connection pool ttl
func PoolTTL(td time.Duration) options.Option { func PoolTTL(d time.Duration) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, td, ".PoolTTL") o.PoolTTL = d
}
}
// Transport to use for communication e.g http, rabbitmq, etc
func Transport(t transport.Transport) Option {
return func(o *Options) {
o.Transport = t
}
}
// Register sets the routers register
func Register(r register.Register) Option {
return func(o *Options) {
if o.Router != nil {
o.Router.Init(router.Register(r))
}
}
}
// Router is used to lookup routes for a service
func Router(r router.Router) Option {
return func(o *Options) {
o.Router = r
} }
} }
// Selector is used to select a route // Selector is used to select a route
func Selector(s selector.Selector) options.Option { func Selector(s selector.Selector) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, s, ".Selector") o.Selector = s
}
}
// Wrap adds a wrapper to the list of options passed into the client
func Wrap(w Wrapper) Option {
return func(o *Options) {
o.Wrappers = append(o.Wrappers, w)
}
}
// WrapCall adds a wrapper to the list of CallFunc wrappers
func WrapCall(cw ...CallWrapper) Option {
return func(o *Options) {
o.CallOptions.CallWrappers = append(o.CallOptions.CallWrappers, cw...)
} }
} }
// Backoff is used to set the backoff function used when retrying Calls // Backoff is used to set the backoff function used when retrying Calls
func Backoff(fn BackoffFunc) options.Option { func Backoff(fn BackoffFunc) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".Backoff") o.CallOptions.Backoff = fn
}
}
// Name sets the client name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
} }
} }
// Lookup sets the lookup function to use for resolving service names // Lookup sets the lookup function to use for resolving service names
func Lookup(fn LookupFunc) options.Option { func Lookup(l LookupFunc) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".Lookup") o.Lookup = l
} }
} }
// WithCallWrapper sets the retry function to be used when re-trying. // TLSConfig specifies a *tls.Config
func WithCallWrapper(fn CallWrapper) options.Option { func TLSConfig(t *tls.Config) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".CallWrappers") // set the internal tls
o.TLSConfig = t
// set the default transport if one is not
// already set. Required for Init call below.
// set the transport tls
o.Transport.Init(
transport.TLSConfig(t),
)
} }
} }
// Retries sets the retry count when making the request. // Retries sets the retry count when making the request.
func Retries(n int) options.Option { func Retries(i int) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, n, ".Retries") o.CallOptions.Retries = i
} }
} }
// Retry sets the retry function to be used when re-trying. // Retry sets the retry function to be used when re-trying.
func Retry(fn RetryFunc) options.Option { func Retry(fn RetryFunc) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".Retry") o.CallOptions.Retry = fn
} }
} }
// RequestTimeout is the request timeout. // RequestTimeout is the request timeout.
func RequestTimeout(td time.Duration) options.Option { func RequestTimeout(d time.Duration) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, td, ".RequestTimeout") o.CallOptions.RequestTimeout = d
} }
} }
// StreamTimeout sets the stream timeout // StreamTimeout sets the stream timeout
func StreamTimeout(td time.Duration) options.Option { func StreamTimeout(d time.Duration) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, td, ".StreamTimeout") o.CallOptions.StreamTimeout = d
} }
} }
// DialTimeout sets the dial timeout // DialTimeout sets the dial timeout
func DialTimeout(td time.Duration) options.Option { func DialTimeout(d time.Duration) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, td, ".DialTimeout") o.CallOptions.DialTimeout = d
} }
} }
// WithResponseMetadata is a CallOption which adds metadata.Metadata to Options.CallOptions // WithExchange sets the exchange to route a message through
func ResponseMetadata(md *metadata.Metadata) options.Option { // Deprecated
return func(src interface{}) error { func WithExchange(e string) PublishOption {
return options.Set(src, md, ".ResponseMetadata") return func(o *PublishOptions) {
o.Exchange = e
} }
} }
// WithRequestMetadata is a CallOption which adds metadata.Metadata to Options.CallOptions // PublishExchange sets the exchange to route a message through
func RequestMetadata(md metadata.Metadata) options.Option { func PublishExchange(e string) PublishOption {
return func(src interface{}) error { return func(o *PublishOptions) {
return options.Set(src, metadata.Copy(md), ".RequestMetadata") o.Exchange = e
} }
} }
// AuthToken is a CallOption which overrides the // WithBodyOnly publish only message body
// DERECATED
func WithBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
}
}
// PublishBodyOnly publish only message body
func PublishBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
}
}
// PublishContext sets the context in publish options
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
// WithContentType specifies call content type
func WithContentType(ct string) CallOption {
return func(o *CallOptions) {
o.ContentType = ct
}
}
// WithAddress sets the remote addresses to use rather than using service discovery
func WithAddress(a ...string) CallOption {
return func(o *CallOptions) {
o.Address = a
}
}
// WithCallWrapper is a CallOption which adds to the existing CallFunc wrappers
func WithCallWrapper(cw ...CallWrapper) CallOption {
return func(o *CallOptions) {
o.CallWrappers = append(o.CallWrappers, cw...)
}
}
// WithBackoff is a CallOption which overrides that which
// set in Options.CallOptions
func WithBackoff(fn BackoffFunc) CallOption {
return func(o *CallOptions) {
o.Backoff = fn
}
}
// WithRetry is a CallOption which overrides that which
// set in Options.CallOptions
func WithRetry(fn RetryFunc) CallOption {
return func(o *CallOptions) {
o.Retry = fn
}
}
// WithRetries is a CallOption which overrides that which
// set in Options.CallOptions
func WithRetries(i int) CallOption {
return func(o *CallOptions) {
o.Retries = i
}
}
// WithRequestTimeout is a CallOption which overrides that which
// set in Options.CallOptions
func WithRequestTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.RequestTimeout = d
}
}
// WithStreamTimeout sets the stream timeout
func WithStreamTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.StreamTimeout = d
}
}
// WithDialTimeout is a CallOption which overrides that which
// set in Options.CallOptions
func WithDialTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.DialTimeout = d
}
}
// WithAuthToken is a CallOption which overrides the
// authorization header with the services own auth token // authorization header with the services own auth token
func AuthToken(t string) options.Option { func WithAuthToken(t string) CallOption {
return func(src interface{}) error { return func(o *CallOptions) {
return options.Set(src, t, ".AuthToken") o.AuthToken = t
} }
} }
// Network is a CallOption which sets the network attribute // WithNetwork is a CallOption which sets the network attribute
func Network(n string) options.Option { func WithNetwork(n string) CallOption {
return func(src interface{}) error { return func(o *CallOptions) {
return options.Set(src, n, ".Network") o.Network = n
}
}
// WithRouter sets the router to use for this call
func WithRouter(r router.Router) CallOption {
return func(o *CallOptions) {
o.Router = r
}
}
// WithSelector sets the selector to use for this call
func WithSelector(s selector.Selector) CallOption {
return func(o *CallOptions) {
o.Selector = s
} }
} }
/*
// WithSelectOptions sets the options to pass to the selector for this call // WithSelectOptions sets the options to pass to the selector for this call
func WithSelectOptions(sops ...selector.SelectOption) options.Option { func WithSelectOptions(sops ...selector.SelectOption) CallOption {
return func(o *CallOptions) { return func(o *CallOptions) {
o.SelectOptions = sops o.SelectOptions = sops
} }
} }
*/
// StreamingRequest specifies that request is streaming // WithMessageContentType sets the message content type
func StreamingRequest(b bool) options.Option { // Deprecated
return func(src interface{}) error { func WithMessageContentType(ct string) MessageOption {
return options.Set(src, b, ".Stream") return func(o *MessageOptions) {
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
func StreamingRequest(b bool) RequestOption {
return func(o *RequestOptions) {
o.Stream = b
}
}
// RequestContentType specifies request content type
func RequestContentType(ct string) RequestOption {
return func(o *RequestOptions) {
o.ContentType = ct
} }
} }

View File

@ -3,7 +3,7 @@ package client
import ( import (
"context" "context"
"go.unistack.org/micro/v4/errors" "github.com/unistack-org/micro/v3/errors"
) )
// RetryFunc that returning either false or a non-nil error will result in the call not being retried // RetryFunc that returning either false or a non-nil error will result in the call not being retried
@ -19,32 +19,18 @@ func RetryNever(ctx context.Context, req Request, retryCount int, err error) (bo
return false, nil return false, nil
} }
// RetryOnError retries a request on a 500 or 408 (timeout) error // RetryOnError retries a request on a 500 or timeout error
func RetryOnError(_ context.Context, _ Request, _ int, err error) (bool, error) { func RetryOnError(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
if err == nil { if err == nil {
return false, nil return false, nil
} }
me := errors.FromError(err) me := errors.FromError(err)
switch me.Code { switch me.Code {
// retry on timeout or internal server error // retry on timeout or internal server error
case 408, 500: case 408, 500:
return true, nil return true, nil
} }
return false, nil
}
// RetryOnErrors retries a request on specified error codes
func RetryOnErrors(codes ...int32) RetryFunc {
return func(_ context.Context, _ Request, _ int, err error) (bool, error) {
if err == nil {
return false, nil return false, nil
}
me := errors.FromError(err)
for _, code := range codes {
if me.Code == code {
return true, nil
}
}
return false, nil
}
} }

View File

@ -1,70 +0,0 @@
package client
import (
"context"
"fmt"
"testing"
"go.unistack.org/micro/v4/errors"
)
func TestRetryAlways(t *testing.T) {
tests := []error{
nil,
errors.InternalServerError("test", "%s", "test"),
fmt.Errorf("test"),
}
for _, e := range tests {
ok, er := RetryAlways(context.TODO(), nil, 1, e)
if !ok || er != nil {
t.Fatal("RetryAlways not works properly")
}
}
}
func TestRetryNever(t *testing.T) {
tests := []error{
nil,
errors.InternalServerError("test", "%s", "test"),
fmt.Errorf("test"),
}
for _, e := range tests {
ok, er := RetryNever(context.TODO(), nil, 1, e)
if ok || er != nil {
t.Fatal("RetryNever not works properly")
}
}
}
func TestRetryOnError(t *testing.T) {
tests := []error{
fmt.Errorf("test"),
errors.NotFound("test", "%s", "test"),
errors.Timeout("test", "%s", "test"),
}
for i, e := range tests {
ok, er := RetryOnError(context.TODO(), nil, 1, e)
if i == 2 && (!ok || er != nil) {
t.Fatal("RetryOnError not works properly")
}
}
}
func TestRetryOnErrors(t *testing.T) {
tests := []error{
fmt.Errorf("test"),
errors.NotFound("test", "%s", "test"),
errors.Timeout("test", "%s", "test"),
}
fn := RetryOnErrors(404)
for i, e := range tests {
ok, er := fn(context.TODO(), nil, 1, e)
if i == 1 && (!ok || er != nil) {
t.Fatal("RetryOnErrors not works properly")
}
}
}

View File

@ -1,7 +1,7 @@
package client package client
import ( import (
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/codec"
) )
type testRequest struct { type testRequest struct {

View File

@ -1,11 +1,11 @@
// Package codec is an interface for encoding messages // Package codec is an interface for encoding messages
package codec // import "go.unistack.org/micro/v4/codec" package codec
import ( import (
"errors" "errors"
"io" "io"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
) )
// Message types // Message types
@ -25,9 +25,9 @@ var (
var ( var (
// DefaultMaxMsgSize specifies how much data codec can handle // DefaultMaxMsgSize specifies how much data codec can handle
DefaultMaxMsgSize = 1024 * 1024 * 4 // 4Mb DefaultMaxMsgSize int = 1024 * 1024 * 4 // 4Mb
// DefaultCodec is the global default codec // DefaultCodec is the global default codec
DefaultCodec = NewCodec() DefaultCodec Codec = NewCodec()
// DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal // DefaultTagName specifies struct tag name to control codec Marshal/Unmarshal
DefaultTagName = "codec" DefaultTagName = "codec"
) )
@ -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(r io.Reader, m *Message, mt MessageType) error ReadHeader(io.Reader, *Message, MessageType) error
ReadBody(r io.Reader, v interface{}) error ReadBody(io.Reader, interface{}) error
Write(w io.Writer, m *Message, v interface{}) error Write(io.Writer, *Message, interface{}) error
Marshal(v interface{}, opts ...Option) ([]byte, error) Marshal(interface{}) ([]byte, error)
Unmarshal(b []byte, v interface{}, opts ...Option) error Unmarshal([]byte, interface{}) 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,41 +67,3 @@ 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
}
// 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
}

View File

@ -1,34 +0,0 @@
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)
}
}

View File

@ -1,35 +0,0 @@
package codec
import (
"context"
"testing"
)
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), codecKey{}, NewCodec())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewCodec())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestSetOption(t *testing.T) {
type key struct{}
o := SetOption(key{}, "test")
opts := &Options{}
o(opts)
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
t.Fatal("SetOption not works")
}
}

View File

@ -4,42 +4,3 @@ package codec
type Frame struct { type Frame struct {
Data []byte Data []byte
} }
// NewFrame returns new frame with data
func NewFrame(data []byte) *Frame {
return &Frame{Data: data}
}
// MarshalJSON returns frame data
func (m *Frame) MarshalJSON() ([]byte, error) {
return m.Marshal()
}
// UnmarshalJSON set frame data
func (m *Frame) UnmarshalJSON(data []byte) error {
return m.Unmarshal(data)
}
// ProtoMessage noop func
func (m *Frame) ProtoMessage() {}
// Reset resets frame
func (m *Frame) Reset() {
*m = Frame{}
}
// String returns frame as string
func (m *Frame) String() string {
return string(m.Data)
}
// Marshal returns frame data
func (m *Frame) Marshal() ([]byte, error) {
return m.Data, nil
}
// Unmarshal set frame data
func (m *Frame) Unmarshal(data []byte) error {
m.Data = data
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2021-2023 Unistack LLC // Copyright 2021 Unistack LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -17,7 +17,7 @@ syntax = "proto3";
package micro.codec; package micro.codec;
option cc_enable_arenas = true; option cc_enable_arenas = true;
option go_package = "go.unistack.org/micro/v4/codec;codec"; option go_package = "github.com/unistack-org/micro/v3/codec;codec";
option java_multiple_files = true; option java_multiple_files = true;
option java_outer_classname = "MicroCodec"; option java_outer_classname = "MicroCodec";
option java_package = "micro.codec"; option java_package = "micro.codec";

View File

@ -5,9 +5,7 @@ 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
@ -71,11 +69,11 @@ func (c *noopCodec) String() string {
} }
// NewCodec returns new noop codec // NewCodec returns new noop codec
func NewCodec(opts ...Option) Codec { func NewCodec() Codec {
return &noopCodec{opts: NewOptions(opts...)} return &noopCodec{}
} }
func (c *noopCodec) Marshal(v interface{}, opts ...Option) ([]byte, error) { func (c *noopCodec) Marshal(v interface{}) ([]byte, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil
} }
@ -98,7 +96,7 @@ func (c *noopCodec) Marshal(v interface{}, opts ...Option) ([]byte, error) {
return json.Marshal(v) return json.Marshal(v)
} }
func (c *noopCodec) Unmarshal(d []byte, v interface{}, opts ...Option) error { func (c *noopCodec) Unmarshal(d []byte, v interface{}) error {
if v == nil { if v == nil {
return nil return nil
} }
@ -106,9 +104,6 @@ func (c *noopCodec) Unmarshal(d []byte, v interface{}, opts ...Option) error {
case *string: case *string:
*ve = string(d) *ve = string(d)
return nil return nil
case []byte:
copy(ve, d)
return nil
case *[]byte: case *[]byte:
*ve = d *ve = d
return nil return nil

View File

@ -5,23 +5,9 @@ import (
"testing" "testing"
) )
func TestNoopBytesPtr(t *testing.T) {
req := []byte("test req")
rsp := make([]byte, len(req))
nc := NewCodec()
if err := nc.Unmarshal(req, &rsp); err != nil {
t.Fatal(err)
}
if !bytes.Equal(req, rsp) {
t.Fatalf("req not eq rsp: %s != %s", req, rsp)
}
}
func TestNoopBytes(t *testing.T) { func TestNoopBytes(t *testing.T) {
req := []byte("test req") req := []byte("test req")
var rsp []byte rsp := make([]byte, len(req))
nc := NewCodec() nc := NewCodec()
if err := nc.Unmarshal(req, &rsp); err != nil { if err := nc.Unmarshal(req, &rsp); err != nil {

View File

@ -1,11 +1,9 @@
package codec package codec
import ( import (
"context" "github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/meter"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/tracer"
"go.unistack.org/micro/v4/meter"
"go.unistack.org/micro/v4/tracer"
) )
// Option func // Option func
@ -19,10 +17,6 @@ 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
} }
@ -34,13 +28,6 @@ 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) {
@ -65,12 +52,10 @@ 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 {

View File

@ -1,26 +1,19 @@
// Package config is an interface for dynamic configuration. // Package config is an interface for dynamic configuration.
package config // import "go.unistack.org/micro/v4/config" package config
import ( import (
"context" "context"
"errors" "errors"
"reflect"
"time" "time"
"go.unistack.org/micro/v4/options"
) )
type Validator interface {
Validate() error
}
// DefaultConfig default config // DefaultConfig default config
var DefaultConfig = NewConfig() var DefaultConfig Config = NewConfig()
// DefaultWatcherMinInterval default min interval for poll changes // DefaultWatcherMinInterval default min interval for poll changes
var DefaultWatcherMinInterval = 5 * time.Second var DefaultWatcherMinInterval = 5 * time.Second
// DefaultWatcherMaxInterval default max interval for poll changes // DefaultWatcherMinInterval default max interval for poll changes
var DefaultWatcherMaxInterval = 9 * time.Second var DefaultWatcherMaxInterval = 9 * time.Second
var ( var (
@ -30,8 +23,6 @@ var (
ErrInvalidStruct = errors.New("invalid struct specified") ErrInvalidStruct = errors.New("invalid struct specified")
// ErrWatcherStopped is returned when source watcher has been stopped // ErrWatcherStopped is returned when source watcher has been stopped
ErrWatcherStopped = errors.New("watcher stopped") ErrWatcherStopped = errors.New("watcher stopped")
// ErrWatcherNotImplemented returned when config does not implement watch
ErrWatcherNotImplemented = errors.New("watcher not implemented")
) )
// Config is an interface abstraction for dynamic configuration // Config is an interface abstraction for dynamic configuration
@ -39,15 +30,15 @@ type Config interface {
// Name returns name of config // Name returns name of config
Name() string Name() string
// Init the config // Init the config
Init(opts ...options.Option) error Init(opts ...Option) error
// Options in the config // Options in the config
Options() Options Options() Options
// Load config from sources // Load config from sources
Load(context.Context, ...options.Option) error Load(context.Context, ...LoadOption) error
// Save config to sources // Save config to sources
Save(context.Context, ...options.Option) error Save(context.Context, ...SaveOption) error
// Watch a config for changes // Watch a config for changes
Watch(context.Context, ...options.Option) (Watcher, error) Watch(context.Context, ...WatchOption) (Watcher, error)
// String returns config type name // String returns config type name
String() string String() string
} }
@ -61,7 +52,7 @@ type Watcher interface {
} }
// Load loads config from config sources // Load loads config from config sources
func Load(ctx context.Context, cs []Config, opts ...options.Option) 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 {
@ -73,147 +64,3 @@ func Load(ctx context.Context, cs []Config, opts ...options.Option) error {
} }
return nil return nil
} }
// Validate runs Validate() error func for each struct field
func Validate(ctx context.Context, cfg interface{}) error {
if cfg == nil {
return nil
}
if v, ok := cfg.(Validator); ok {
if err := v.Validate(); err != nil {
return err
}
}
sv := reflect.ValueOf(cfg)
if sv.Kind() == reflect.Ptr {
sv = sv.Elem()
}
if sv.Kind() != reflect.Struct {
return nil
}
typ := sv.Type()
for idx := 0; idx < typ.NumField(); idx++ {
fld := typ.Field(idx)
val := sv.Field(idx)
if !val.IsValid() || len(fld.PkgPath) != 0 {
continue
}
if v, ok := val.Interface().(Validator); ok {
if err := v.Validate(); err != nil {
return err
}
}
switch val.Kind() {
case reflect.Ptr:
if reflect.Indirect(val).Kind() == reflect.Struct {
if err := Validate(ctx, val.Interface()); err != nil {
return err
}
}
case reflect.Struct:
if err := Validate(ctx, val.Interface()); err != nil {
return err
}
}
}
return nil
}
var (
// DefaultBeforeLoad default func that runs before config Load
DefaultBeforeLoad = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().BeforeLoad {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" BeforeLoad error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
// DefaultAfterLoad default func that runs after config Load
DefaultAfterLoad = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().AfterLoad {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" AfterLoad error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
// DefaultBeforeSave default func that runs befora config Save
DefaultBeforeSave = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().BeforeSave {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" BeforeSave error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
// DefaultAfterSave default func that runs after config Save
DefaultAfterSave = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().AfterSave {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" AfterSave error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
// DefaultBeforeInit default func that runs befora config Init
DefaultBeforeInit = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().BeforeInit {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" BeforeInit error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
// DefaultAfterInit default func that runs after config Init
DefaultAfterInit = func(ctx context.Context, c Config) error {
for _, fn := range c.Options().AfterSave {
if fn == nil {
return nil
}
if err := fn(ctx, c); err != nil {
c.Options().Logger.Error(ctx, c.String()+" AfterInit error "+err.Error())
if !c.Options().AllowFail {
return err
}
}
}
return nil
}
)

View File

@ -22,3 +22,13 @@ func NewContext(ctx context.Context, c Config) context.Context {
} }
return context.WithValue(ctx, configKey{}, c) return context.WithValue(ctx, configKey{}, 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)
}
}

View File

@ -1,42 +0,0 @@
package config
import (
"context"
"testing"
)
func TestFromNilContext(t *testing.T) {
// nolint: staticcheck
c, ok := FromContext(nil)
if ok || c != nil {
t.Fatal("FromContext not works")
}
}
func TestNewNilContext(t *testing.T) {
// nolint: staticcheck
ctx := NewContext(nil, NewConfig())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), configKey{}, NewConfig())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewConfig())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}

View File

@ -5,14 +5,10 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"dario.cat/mergo" "github.com/imdario/mergo"
"github.com/google/uuid" "github.com/unistack-org/micro/v3/util/jitter"
"go.unistack.org/micro/v4/options" rutil "github.com/unistack-org/micro/v3/util/reflect"
mid "go.unistack.org/micro/v4/util/id"
rutil "go.unistack.org/micro/v4/util/reflect"
mtime "go.unistack.org/micro/v4/util/time"
) )
type defaultConfig struct { type defaultConfig struct {
@ -23,30 +19,19 @@ func (c *defaultConfig) Options() Options {
return c.opts return c.opts
} }
func (c *defaultConfig) Init(opts ...options.Option) error { func (c *defaultConfig) Init(opts ...Option) error {
for _, o := range opts { for _, o := range opts {
o(&c.opts) o(&c.opts)
} }
if err := DefaultBeforeInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
return err
}
if err := DefaultAfterInit(c.opts.Context, c); err != nil && !c.opts.AllowFail {
return err
}
return nil return nil
} }
func (c *defaultConfig) Load(ctx context.Context, opts ...options.Option) error { func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
if c.opts.SkipLoad != nil && c.opts.SkipLoad(ctx, c) { for _, fn := range c.opts.BeforeLoad {
return nil if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
}
if err := DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
return err return err
} }
}
options := NewLoadOptions(opts...) options := NewLoadOptions(opts...)
mopts := []func(*mergo.Config){mergo.WithTypeCheck} mopts := []func(*mergo.Config){mergo.WithTypeCheck}
@ -63,26 +48,21 @@ func (c *defaultConfig) Load(ctx context.Context, opts ...options.Option) error
} }
src, err := rutil.Zero(dst) src, err := rutil.Zero(dst)
if err != nil { if err == nil {
if !c.opts.AllowFail {
return err
}
if err = DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail {
return err
}
}
if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil { if err = fillValues(reflect.ValueOf(src), c.opts.StructTag); err == nil {
err = mergo.Merge(dst, src, mopts...) err = mergo.Merge(dst, src, mopts...)
} }
}
if err != nil && !c.opts.AllowFail { if err != nil && !c.opts.AllowFail {
return err return err
} }
if err := DefaultAfterLoad(ctx, c); err != nil && !c.opts.AllowFail { for _, fn := range c.opts.AfterLoad {
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
return err return err
} }
}
return nil return nil
} }
@ -92,7 +72,6 @@ func fillValue(value reflect.Value, val string) error {
if !rutil.IsEmpty(value) { if !rutil.IsEmpty(value) {
return nil return nil
} }
switch value.Kind() { switch value.Kind() {
case reflect.Map: case reflect.Map:
t := value.Type() t := value.Type()
@ -131,20 +110,6 @@ func fillValue(value reflect.Value, val string) error {
} }
value.Set(reflect.ValueOf(v)) value.Set(reflect.ValueOf(v))
case reflect.String: case reflect.String:
switch val {
case "micro:generate uuid":
uid, err := uuid.NewRandom()
if err != nil {
return err
}
val = uid.String()
case "micro:generate id":
uid, err := mid.New()
if err != nil {
return err
}
val = uid
}
value.Set(reflect.ValueOf(val)) value.Set(reflect.ValueOf(val))
case reflect.Float32: case reflect.Float32:
v, err := strconv.ParseFloat(val, 32) v, err := strconv.ParseFloat(val, 32)
@ -183,26 +148,11 @@ func fillValue(value reflect.Value, val string) error {
} }
value.Set(reflect.ValueOf(int32(v))) value.Set(reflect.ValueOf(int32(v)))
case reflect.Int64: case reflect.Int64:
switch {
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "time":
v, err := time.ParseDuration(val)
if err != nil {
return err
}
value.Set(reflect.ValueOf(v))
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v4/util/time":
v, err := mtime.ParseDuration(val)
if err != nil {
return err
}
value.SetInt(int64(v))
default:
v, err := strconv.ParseInt(val, 10, 64) v, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }
value.Set(reflect.ValueOf(v)) value.Set(reflect.ValueOf(v))
}
case reflect.Uint: case reflect.Uint:
v, err := strconv.ParseUint(val, 10, 0) v, err := strconv.ParseUint(val, 10, 0)
if err != nil { if err != nil {
@ -296,18 +246,18 @@ func fillValues(valueOf reflect.Value, tname string) error {
return nil return nil
} }
func (c *defaultConfig) Save(ctx context.Context, _ ...options.Option) error { func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
if c.opts.SkipSave != nil && c.opts.SkipSave(ctx, c) { for _, fn := range c.opts.BeforeSave {
return nil if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
}
if err := DefaultBeforeSave(ctx, c); err != nil {
return err return err
} }
}
if err := DefaultAfterSave(ctx, c); err != nil { for _, fn := range c.opts.AfterSave {
if err := fn(ctx, c); err != nil && !c.opts.AllowFail {
return err return err
} }
}
return nil return nil
} }
@ -320,15 +270,98 @@ func (c *defaultConfig) Name() string {
return c.opts.Name return c.opts.Name
} }
func (c *defaultConfig) Watch(ctx context.Context, opts ...options.Option) (Watcher, error) { func (c *defaultConfig) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
return nil, ErrWatcherNotImplemented w := &defaultWatcher{
opts: c.opts,
wopts: NewWatchOptions(opts...),
done: make(chan struct{}),
vchan: make(chan map[string]interface{}),
echan: make(chan error),
}
go w.run()
return w, nil
} }
// NewConfig returns new default config source // NewConfig returns new default config source
func NewConfig(opts ...options.Option) Config { func NewConfig(opts ...Option) Config {
options := NewOptions(opts...) options := NewOptions(opts...)
if len(options.StructTag) == 0 { if len(options.StructTag) == 0 {
options.StructTag = "default" options.StructTag = "default"
} }
return &defaultConfig{opts: options} return &defaultConfig{opts: options}
} }
type defaultWatcher struct {
opts Options
wopts WatchOptions
done chan struct{}
vchan chan map[string]interface{}
echan chan error
}
func (w *defaultWatcher) run() {
ticker := jitter.NewTicker(w.wopts.MinInterval, w.wopts.MaxInterval)
defer ticker.Stop()
src := w.opts.Struct
if w.wopts.Struct != nil {
src = w.wopts.Struct
}
for {
select {
case <-w.done:
return
case <-ticker.C:
dst, err := rutil.Zero(src)
if err == nil {
err = fillValues(reflect.ValueOf(dst), w.opts.StructTag)
}
if err != nil {
w.echan <- err
return
}
srcmp, err := rutil.StructFieldsMap(src)
if err != nil {
w.echan <- err
return
}
dstmp, err := rutil.StructFieldsMap(dst)
if err != nil {
w.echan <- err
return
}
for sk, sv := range srcmp {
if reflect.DeepEqual(dstmp[sk], sv) {
delete(dstmp, sk)
}
}
if len(dstmp) > 0 {
w.vchan <- dstmp
src = dst
}
}
}
}
func (w *defaultWatcher) Next() (map[string]interface{}, error) {
select {
case <-w.done:
break
case err := <-w.echan:
return nil, err
case v, ok := <-w.vchan:
if !ok {
break
}
return v, nil
}
return nil, ErrWatcherStopped
}
func (w *defaultWatcher) Stop() error {
close(w.done)
return nil
}

View File

@ -6,59 +6,84 @@ import (
"testing" "testing"
"time" "time"
"go.unistack.org/micro/v4/config" "github.com/unistack-org/micro/v3/config"
mid "go.unistack.org/micro/v4/util/id"
mtime "go.unistack.org/micro/v4/util/time"
) )
type cfg struct { type Cfg struct {
StringValue string `default:"string_value"` StringValue string `default:"string_value"`
IgnoreValue string `json:"-"` IgnoreValue string `json:"-"`
StructValue *cfgStructValue StructValue struct {
IntValue int `default:"99"`
DurationValue time.Duration `default:"10s"`
MDurationValue mtime.Duration `default:"10s"`
MapValue map[string]bool `default:"key1=true,key2=false"`
UUIDValue string `default:"micro:generate uuid"`
IDValue string `default:"micro:generate id"`
}
type cfgStructValue struct {
StringValue string `default:"string_value"` StringValue string `default:"string_value"`
}
IntValue int `default:"99"`
} }
func (c *cfg) Validate() error { func TestWatch(t *testing.T) {
if c.IntValue != 10 { ctx := context.Background()
return fmt.Errorf("invalid IntValue %d != %d", 10, c.IntValue)
}
if c.MapValue["key1"] != true {
return fmt.Errorf("invalid MapValue %t != %t", true, c.MapValue["key1"])
}
return nil
}
func (c *cfgStructValue) Validate() error { conf := &Cfg{IntValue: 10}
if c.StringValue != "string_value" {
return fmt.Errorf("invalid StringValue %s != %s", "string_value", c.StringValue) cfg := config.NewConfig(config.Struct(conf))
if err := cfg.Init(); err != nil {
t.Fatal(err)
} }
return nil if err := cfg.Load(ctx); err != nil {
t.Fatal(err)
}
w, err := cfg.Watch(ctx, config.WatchInterval(200*time.Millisecond, 500*time.Millisecond))
if err != nil {
t.Fatal(err)
}
defer func() {
_ = w.Stop()
}()
done := make(chan struct{})
go func() {
for {
mp, err := w.Next()
if err != nil && err != config.ErrWatcherStopped {
t.Fatal(err)
} else if err == config.ErrWatcherStopped {
return
}
if len(mp) != 1 {
t.Fatal(fmt.Errorf("default watcher err: %v", mp))
}
v, ok := mp["IntValue"]
if !ok {
t.Fatal(fmt.Errorf("default watcher err: %v", v))
}
if nv, ok := v.(int); !ok || nv != 99 {
t.Fatal(fmt.Errorf("default watcher err: %v", v))
}
close(done)
return
}
}()
<-done
} }
func TestDefault(t *testing.T) { func TestDefault(t *testing.T) {
ctx := context.Background() ctx := context.Background()
conf := &cfg{IntValue: 10} conf := &Cfg{IntValue: 10}
blfn := func(_ context.Context, c config.Config) error { blfn := func(ctx context.Context, cfg config.Config) error {
nconf, ok := c.Options().Struct.(*cfg) nconf, ok := cfg.Options().Struct.(*Cfg)
if !ok { if !ok {
return fmt.Errorf("failed to get Struct from options: %v", c.Options()) return fmt.Errorf("failed to get Struct from options: %v", cfg.Options())
} }
nconf.StringValue = "before_load" nconf.StringValue = "before_load"
return nil return nil
} }
alfn := func(_ context.Context, c config.Config) error { alfn := func(ctx context.Context, cfg config.Config) error {
nconf, ok := c.Options().Struct.(*cfg) nconf, ok := cfg.Options().Struct.(*Cfg)
if !ok { if !ok {
return fmt.Errorf("failed to get Struct from options: %v", c.Options()) return fmt.Errorf("failed to get Struct from options: %v", cfg.Options())
} }
nconf.StringValue = "after_load" nconf.StringValue = "after_load"
return nil return nil
@ -74,53 +99,6 @@ func TestDefault(t *testing.T) {
if conf.StringValue != "after_load" { if conf.StringValue != "after_load" {
t.Fatal("AfterLoad option not working") t.Fatal("AfterLoad option not working")
} }
if len(conf.MapValue) != 2 {
t.Fatalf("map value invalid: %#+v\n", conf.MapValue)
}
if conf.UUIDValue == "" {
t.Fatalf("uuid value empty")
} else if len(conf.UUIDValue) != 36 {
t.Fatalf("uuid value invalid: %s", conf.UUIDValue)
}
if conf.IDValue == "" {
t.Fatalf("id value empty")
} else if len(conf.IDValue) != mid.DefaultSize {
t.Fatalf("id value invalid: %s", conf.IDValue)
}
_ = conf _ = conf
// t.Logf("%#+v\n", conf) //t.Logf("%#+v\n", conf)
}
func TestValidate(t *testing.T) {
ctx := context.Background()
conf := &cfg{IntValue: 10}
cfg := config.NewConfig(config.Struct(conf))
if err := cfg.Init(); err != nil {
t.Fatal(err)
}
if err := cfg.Load(ctx); err != nil {
t.Fatal(err)
}
if err := config.Validate(ctx, conf); err != nil {
t.Fatal(err)
}
}
func TestString(t *testing.T) {
cfg := config.NewConfig()
res := cfg.String()
if res != "default" {
t.Fatalf("string value invalid: %s", res)
}
}
func TestName(t *testing.T) {
cfg := config.NewConfig()
res := cfg.Name()
if res != "" {
t.Fatal("name value not empty")
}
} }

View File

@ -4,11 +4,10 @@ import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "github.com/unistack-org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/tracer"
"go.unistack.org/micro/v4/tracer"
) )
// Options hold the config options // Options hold the config options
@ -29,28 +28,23 @@ type Options struct {
Name string Name string
// StructTag name // StructTag name
StructTag string StructTag string
// BeforeSave contains slice of funcs that runs before Save // BeforeSave contains slice of funcs that runs before save
BeforeSave []func(context.Context, Config) error BeforeSave []func(context.Context, Config) error
// AfterSave contains slice of funcs that runs after Save // AfterLoad contains slice of funcs that runs after load
AfterSave []func(context.Context, Config) error
// BeforeLoad contains slice of funcs that runs before Load
BeforeLoad []func(context.Context, Config) error
// AfterLoad contains slice of funcs that runs after Load
AfterLoad []func(context.Context, Config) error AfterLoad []func(context.Context, Config) error
// BeforeInit contains slice of funcs that runs before Init // BeforeLoad contains slice of funcs that runs before load
BeforeInit []func(context.Context, Config) error BeforeLoad []func(context.Context, Config) error
// AfterInit contains slice of funcs that runs after Init // AfterSave contains slice of funcs that runs after save
AfterInit []func(context.Context, Config) error AfterSave []func(context.Context, Config) error
// AllowFail flag to allow fail in config source // AllowFail flag to allow fail in config source
AllowFail bool AllowFail bool
// SkipLoad runs only if condition returns true
SkipLoad func(context.Context, Config) bool
// SkipSave runs only if condition returns true
SkipSave func(context.Context, Config) bool
} }
// Option function signature
type Option func(o *Options)
// NewOptions new options struct with filed values // NewOptions new options struct with filed values
func NewOptions(opts ...options.Option) Options { func NewOptions(opts ...Option) Options {
options := Options{ options := Options{
Logger: logger.DefaultLogger, Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter, Meter: meter.DefaultMeter,
@ -64,16 +58,17 @@ func NewOptions(opts ...options.Option) Options {
return options return options
} }
// LoadOption function signature
type LoadOption func(o *LoadOptions)
// LoadOptions struct // LoadOptions struct
type LoadOptions struct { type LoadOptions struct {
Struct interface{} Struct interface{}
Context context.Context
Override bool Override bool
Append bool Append bool
} }
// NewLoadOptions create LoadOptions struct with provided opts func NewLoadOptions(opts ...LoadOption) LoadOptions {
func NewLoadOptions(opts ...options.Option) LoadOptions {
options := LoadOptions{} options := LoadOptions{}
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@ -82,27 +77,43 @@ func NewLoadOptions(opts ...options.Option) LoadOptions {
} }
// LoadOverride override values when load // LoadOverride override values when load
func LoadOverride(b bool) options.Option { func LoadOverride(b bool) LoadOption {
return func(src interface{}) error { return func(o *LoadOptions) {
return options.Set(src, b, ".Override") o.Override = b
} }
} }
// LoadAppend override values when load // LoadAppend override values when load
func LoadAppend(b bool) options.Option { func LoadAppend(b bool) LoadOption {
return func(src interface{}) error { return func(o *LoadOptions) {
return options.Set(src, b, ".Append") o.Append = b
} }
} }
// LoadStruct override struct for loading
func LoadStruct(src interface{}) LoadOption {
return func(o *LoadOptions) {
o.Struct = src
}
}
// SaveOption function signature
type SaveOption func(o *SaveOptions)
// SaveOptions struct // SaveOptions struct
type SaveOptions struct { type SaveOptions struct {
Struct interface{} Struct interface{}
Context context.Context }
// SaveStruct override struct for save to config
func SaveStruct(src interface{}) SaveOption {
return func(o *SaveOptions) {
o.Struct = src
}
} }
// NewSaveOptions fill SaveOptions struct // NewSaveOptions fill SaveOptions struct
func NewSaveOptions(opts ...options.Option) SaveOptions { func NewSaveOptions(opts ...SaveOption) SaveOptions {
options := SaveOptions{} options := SaveOptions{}
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@ -111,65 +122,86 @@ func NewSaveOptions(opts ...options.Option) SaveOptions {
} }
// AllowFail allows config source to fail // AllowFail allows config source to fail
func AllowFail(b bool) options.Option { func AllowFail(b bool) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, b, ".AllowFail") o.AllowFail = b
}
}
// BeforeInit run funcs before config Init
func BeforeInit(fn ...func(context.Context, Config) error) options.Option {
return func(src interface{}) error {
return options.Set(src, fn, ".BeforeInit")
}
}
// AfterInit run funcs after config Init
func AfterInit(fn ...func(context.Context, Config) error) options.Option {
return func(src interface{}) error {
return options.Set(src, fn, ".AfterInit")
} }
} }
// BeforeLoad run funcs before config load // BeforeLoad run funcs before config load
func BeforeLoad(fn ...func(context.Context, Config) error) options.Option { func BeforeLoad(fn ...func(context.Context, Config) error) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".BeforeLoad") o.BeforeLoad = fn
} }
} }
// AfterLoad run funcs after config load // AfterLoad run funcs after config load
func AfterLoad(fn ...func(context.Context, Config) error) options.Option { func AfterLoad(fn ...func(context.Context, Config) error) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".AfterLoad") o.AfterLoad = fn
} }
} }
// BeforeSave run funcs before save // BeforeSave run funcs before save
func BeforeSave(fn ...func(context.Context, Config) error) options.Option { func BeforeSave(fn ...func(context.Context, Config) error) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".BeforeSave") o.BeforeSave = fn
} }
} }
// AfterSave run fncs after save // AfterSave run fncs after save
func AfterSave(fn ...func(context.Context, Config) error) options.Option { func AfterSave(fn ...func(context.Context, Config) error) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, fn, ".AfterSave") o.AfterSave = fn
}
}
// Context pass context
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// Codec sets the source codec
func Codec(c codec.Codec) Option {
return func(o *Options) {
o.Codec = c
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
} }
} }
// Struct used as config // Struct used as config
func Struct(v interface{}) options.Option { func Struct(v interface{}) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, v, ".Struct") o.Struct = v
} }
} }
// StructTag sets the struct tag that used for filling // StructTag sets the struct tag that used for filling
func StructTag(name string) options.Option { func StructTag(name string) Option {
return func(src interface{}) error { return func(o *Options) {
return options.Set(src, name, ".StructTag") o.StructTag = name
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
} }
} }
@ -177,18 +209,19 @@ func StructTag(name string) options.Option {
type WatchOptions struct { type WatchOptions struct {
// Context used by non default options // Context used by non default options
Context context.Context Context context.Context
// Struct for filling // Coalesce multiple events to one
Struct interface{} Coalesce bool
// MinInterval specifies the min time.Duration interval for poll changes // MinInterval specifies the min time.Duration interval for poll changes
MinInterval time.Duration MinInterval time.Duration
// MaxInterval specifies the max time.Duration interval for poll changes // MaxInterval specifies the max time.Duration interval for poll changes
MaxInterval time.Duration MaxInterval time.Duration
// Coalesce multiple events to one // Struct for filling
Coalesce bool Struct interface{}
} }
// NewWatchOptions create WatchOptions struct with provided opts type WatchOption func(*WatchOptions)
func NewWatchOptions(opts ...options.Option) WatchOptions {
func NewWatchOptions(opts ...WatchOption) WatchOptions {
options := WatchOptions{ options := WatchOptions{
Context: context.Background(), Context: context.Background(),
MinInterval: DefaultWatcherMinInterval, MinInterval: DefaultWatcherMinInterval,
@ -200,20 +233,31 @@ func NewWatchOptions(opts ...options.Option) WatchOptions {
return options return options
} }
// Coalesce controls watch event combining // WatchContext pass context
func WatchCoalesce(b bool) options.Option { func WatchContext(ctx context.Context) WatchOption {
return func(src interface{}) error { return func(o *WatchOptions) {
return options.Set(src, b, ".Coalesce") 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 // WatchInterval specifies min and max time.Duration for pulling changes
func WatchInterval(min, max time.Duration) options.Option { func WatchInterval(min, max time.Duration) WatchOption {
return func(src interface{}) error { return func(o *WatchOptions) {
var err error o.MinInterval = min
if err = options.Set(src, min, ".MinInterval"); err == nil { o.MaxInterval = max
err = options.Set(src, max, ".MaxInterval") }
} }
return err
// WatchStruct overrides struct for fill
func WatchStruct(src interface{}) WatchOption {
return func(o *WatchOptions) {
o.Struct = src
} }
} }

View File

@ -1,24 +0,0 @@
package micro
import (
"context"
"testing"
)
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), serviceKey{}, NewService())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewService())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}

View File

@ -1,159 +0,0 @@
package database
import (
"crypto/tls"
"errors"
"fmt"
"net/url"
"strings"
)
var (
ErrInvalidDSNAddr = errors.New("invalid dsn addr")
ErrInvalidDSNUnescaped = errors.New("dsn must be escaped")
ErrInvalidDSNNoSlash = errors.New("dsn must contains slash")
)
type Config struct {
TLSConfig *tls.Config
Username string
Password string
Scheme string
Host string
Port string
Database string
Params []string
}
func (cfg *Config) FormatDSN() string {
var s strings.Builder
if len(cfg.Scheme) > 0 {
s.WriteString(cfg.Scheme + "://")
}
// [username[:password]@]
if len(cfg.Username) > 0 {
s.WriteString(cfg.Username)
if len(cfg.Password) > 0 {
s.WriteByte(':')
s.WriteString(url.PathEscape(cfg.Password))
}
s.WriteByte('@')
}
// [host:port]
if len(cfg.Host) > 0 {
s.WriteString(cfg.Host)
if len(cfg.Port) > 0 {
s.WriteByte(':')
s.WriteString(cfg.Port)
}
}
// /dbname
s.WriteByte('/')
s.WriteString(url.PathEscape(cfg.Database))
for i := 0; i < len(cfg.Params); i += 2 {
if i == 0 {
s.WriteString("?")
} else {
s.WriteString("&")
}
s.WriteString(cfg.Params[i])
s.WriteString("=")
s.WriteString(cfg.Params[i+1])
}
return s.String()
}
func ParseDSN(dsn string) (*Config, error) {
cfg := &Config{}
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find last '/' that goes before dbname
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] != '/' {
continue
}
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// Find the first ':' in dsn
for j = i; j >= 0; j-- {
if dsn[j] == ':' {
cfg.Scheme = dsn[0:j]
}
}
// [username[:password]@][host]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the second ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
if cfg.Scheme == dsn[:k] {
continue
}
var err error
cfg.Password, err = url.PathUnescape(dsn[k+1 : j])
if err != nil {
return nil, err
}
break
}
}
cfg.Username = dsn[len(cfg.Scheme)+3 : k]
break
}
}
for k = j + 1; k < i; k++ {
if dsn[k] == ':' {
cfg.Host = dsn[j+1 : k]
cfg.Port = dsn[k+1 : i]
break
}
}
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
parts := strings.Split(dsn[j+1:], "&")
cfg.Params = make([]string, 0, len(parts)*2)
for _, p := range parts {
k, v, found := strings.Cut(p, "=")
if !found {
continue
}
cfg.Params = append(cfg.Params, k, v)
}
break
}
}
var err error
dbname := dsn[i+1 : j]
if cfg.Database, err = url.PathUnescape(dbname); err != nil {
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
}
break
}
if !foundSlash && len(dsn) > 0 {
return nil, ErrInvalidDSNNoSlash
}
return cfg, nil
}

View File

@ -1,31 +0,0 @@
package database
import (
"net/url"
"testing"
)
func TestParseDSN(t *testing.T) {
cfg, err := ParseDSN("postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2")
if err != nil {
t.Fatal(err)
}
if cfg.Password != "p@ssword#" {
t.Fatalf("parsing error")
}
}
func TestFormatDSN(t *testing.T) {
src := "postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2"
cfg, err := ParseDSN(src)
if err != nil {
t.Fatal(err)
}
dst, err := url.PathUnescape(cfg.FormatDSN())
if err != nil {
t.Fatal(err)
}
if src != dst {
t.Fatalf("\n%s\n%s", src, dst)
}
}

View File

@ -1,14 +1,11 @@
// Package errors provides a way to return detailed information // Package errors provides a way to return detailed information
// for an RPC request error. The error is normally JSON encoded. // for an RPC request error. The error is normally JSON encoded.
package errors // import "go.unistack.org/micro/v4/errors" package errors
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings"
) )
var ( var (
@ -40,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 id // Id holds error id or service, usually someting like my_service or uuid
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,26 +53,10 @@ func (e *Error) Error() string {
return string(b) return string(b)
} }
/*
// Generator struct holds id of error
type Generator struct {
id string
}
// Generator can emit new error with static id
func NewGenerator(id string) *Generator {
return &Generator{id: id}
}
func (g *Generator) BadRequest(format string, args ...interface{}) error {
return BadRequest(g.id, format, args...)
}
*/
// 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)),
@ -85,130 +66,130 @@ func New(id, detail string, code int32) error {
// Parse tries to parse a JSON string into an error. If that // Parse tries to parse a JSON string into an error. If that
// fails, it will set the given string as the error detail. // fails, it will set the given string as the error detail.
func Parse(err string) *Error { func Parse(err string) *Error {
e := &Error{} e := new(Error)
nerr := json.Unmarshal([]byte(err), e) errr := json.Unmarshal([]byte(err), e)
if nerr != nil { if errr != nil {
e.Detail = err e.Detail = err
} }
return e return e
} }
// BadRequest generates a 400 error. // BadRequest generates a 400 error.
func BadRequest(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(400), Status: http.StatusText(400),
} }
} }
// Unauthorized generates a 401 error. // Unauthorized generates a 401 error.
func Unauthorized(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(401), Status: http.StatusText(401),
} }
} }
// Forbidden generates a 403 error. // Forbidden generates a 403 error.
func Forbidden(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(403), Status: http.StatusText(403),
} }
} }
// NotFound generates a 404 error. // NotFound generates a 404 error.
func NotFound(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(404), Status: http.StatusText(404),
} }
} }
// MethodNotAllowed generates a 405 error. // MethodNotAllowed generates a 405 error.
func MethodNotAllowed(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(405), Status: http.StatusText(405),
} }
} }
// Timeout generates a 408 error. // Timeout generates a 408 error.
func Timeout(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(408), Status: http.StatusText(408),
} }
} }
// Conflict generates a 409 error. // Conflict generates a 409 error.
func Conflict(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(409), Status: http.StatusText(409),
} }
} }
// InternalServerError generates a 500 error. // InternalServerError generates a 500 error.
func InternalServerError(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(500), Status: http.StatusText(500),
} }
} }
// NotImplemented generates a 501 error // NotImplemented generates a 501 error
func NotImplemented(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(501), Status: http.StatusText(501),
} }
} }
// BadGateway generates a 502 error // BadGateway generates a 502 error
func BadGateway(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(502), Status: http.StatusText(502),
} }
} }
// ServiceUnavailable generates a 503 error // ServiceUnavailable generates a 503 error
func ServiceUnavailable(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(503), Status: http.StatusText(503),
} }
} }
// GatewayTimeout generates a 504 error // GatewayTimeout generates a 504 error
func GatewayTimeout(id, format string, args ...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, args...), Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(504), Status: http.StatusText(504),
} }
} }
@ -233,27 +214,6 @@ func Equal(err1 error, err2 error) bool {
return true return true
} }
// CodeIn return true if err has specified code
func CodeIn(err interface{}, codes ...int32) bool {
var code int32
switch verr := err.(type) {
case *Error:
code = verr.Code
case int32:
code = verr
default:
return false
}
for _, check := range codes {
if code == check {
return true
}
}
return false
}
// FromError try to convert go error to *Error // FromError try to convert go error to *Error
func FromError(err error) *Error { func FromError(err error) *Error {
if verr, ok := err.(*Error); ok && verr != nil { if verr, ok := err.(*Error); ok && verr != nil {
@ -262,81 +222,3 @@ func FromError(err error) *Error {
return Parse(err.Error()) return Parse(err.Error())
} }
// MarshalJSON returns error data
func (e *Error) MarshalJSON() ([]byte, error) {
return e.Marshal()
}
// UnmarshalJSON set error data
func (e *Error) UnmarshalJSON(data []byte) error {
return e.Unmarshal(data)
}
// ProtoMessage noop func
func (e *Error) ProtoMessage() {}
// Reset resets error
func (e *Error) Reset() {
*e = Error{}
}
// String returns error as string
func (e *Error) String() string {
return fmt.Sprintf(`{"id":"%s","detail":"%s","status":"%s","code":%d}`, addslashes(e.ID), addslashes(e.Detail), addslashes(e.Status), e.Code)
}
// Marshal returns error data
func (e *Error) Marshal() ([]byte, error) {
return []byte(e.String()), nil
}
// Unmarshal set error data
func (e *Error) Unmarshal(data []byte) error {
str := string(data)
if len(data) < 41 {
return fmt.Errorf("invalid data")
}
parts := strings.FieldsFunc(str[1:len(str)-1], func(r rune) bool {
return r == ','
})
for _, part := range parts {
nparts := strings.FieldsFunc(part, func(r rune) bool {
return r == ':'
})
for idx := 0; idx < len(nparts)/2; idx += 2 {
val := strings.Trim(nparts[idx+1], `"`)
if len(val) == 0 {
continue
}
switch {
case nparts[idx] == `"id"`:
e.ID = val
case nparts[idx] == `"detail"`:
e.Detail = val
case nparts[idx] == `"status"`:
e.Status = val
case nparts[idx] == `"code"`:
c, err := strconv.ParseInt(val, 10, 32)
if err != nil {
return err
}
e.Code = int32(c)
}
idx++
}
}
return nil
}
func addslashes(str string) string {
var buf bytes.Buffer
for _, char := range str {
switch char {
case '\'', '"', '\\':
buf.WriteRune('\\')
}
buf.WriteRune(char)
}
return buf.String()
}

View File

@ -1,31 +0,0 @@
// Copyright 2021-2023 Unistack LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package micro.errors;
option cc_enable_arenas = true;
option go_package = "go.unistack.org/micro/v4/errors;errors";
option java_multiple_files = true;
option java_outer_classname = "MicroErrors";
option java_package = "micro.errors";
option objc_class_prefix = "MERRORS";
message Error {
string id = 1;
string detail = 2;
string status = 3;
uint32 code = 4;
}

View File

@ -1,43 +1,20 @@
package errors package errors
import ( import (
"encoding/json"
er "errors" er "errors"
"fmt"
"net/http" "net/http"
"testing" "testing"
) )
func TestMarshalJSON(t *testing.T) {
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
_, err := json.Marshal(e)
if err != nil {
t.Fatal(err)
}
}
func TestEmpty(t *testing.T) {
msg := "test"
var err *Error
err = FromError(fmt.Errorf(msg))
if err.Detail != msg {
t.Fatalf("invalid error %v", err)
}
err = FromError(fmt.Errorf(`{"id":"","detail":"%s","status":"%s","code":0}`, msg, msg))
if err.Detail != msg || err.Status != msg {
t.Fatalf("invalid error %#+v", err)
}
}
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)
} }
} }
@ -59,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),
@ -67,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())
@ -79,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 {
@ -96,19 +73,3 @@ func TestErrors(t *testing.T) {
} }
} }
} }
func TestCodeIn(t *testing.T) {
err := InternalServerError("id", "%s", "msg")
if ok := CodeIn(err, 400, 500); !ok {
t.Fatalf("CodeIn not works: %v", err)
}
if ok := CodeIn(err.(*Error).Code, 500); !ok {
t.Fatalf("CodeIn not works: %v", err)
}
if ok := CodeIn(err, 100); ok {
t.Fatalf("CodeIn not works: %v", err)
}
}

27
event.go Normal file
View File

@ -0,0 +1,27 @@
package micro
import (
"context"
"github.com/unistack-org/micro/v3/client"
)
// Event is used to publish messages to a topic
type Event interface {
// Publish publishes a message to the event topic
Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error
}
type event struct {
c client.Client
topic string
}
// NewEvent creates a new event publisher
func NewEvent(topic string, c client.Client) Event {
return &event{c, topic}
}
func (e *event) Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error {
return e.c.Publish(ctx, e.c.NewMessage(e.topic, msg), opts...)
}

View File

@ -22,3 +22,13 @@ func NewContext(ctx context.Context, f Flow) context.Context {
} }
return context.WithValue(ctx, flowKey{}, f) 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)
}
}

View File

@ -1,42 +0,0 @@
package flow
import (
"context"
"testing"
)
func TestFromNilContext(t *testing.T) {
// nolint: staticcheck
c, ok := FromContext(nil)
if ok || c != nil {
t.Fatal("FromContext not works")
}
}
func TestNewNilContext(t *testing.T) {
// nolint: staticcheck
ctx := NewContext(nil, NewFlow())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), flowKey{}, NewFlow())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), NewFlow())
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}

View File

@ -7,56 +7,6 @@ import (
"github.com/silas/dag" "github.com/silas/dag"
) )
func TestDeps(t *testing.T) {
t.Skip()
d := &dag.AcyclicGraph{}
v0 := d.Add(&node{"v0"})
v1 := d.Add(&node{"v1"})
v2 := d.Add(&node{"v2"})
v3 := d.Add(&node{"v3"})
v4 := d.Add(&node{"v4"})
d.Connect(dag.BasicEdge(v0, v1))
d.Connect(dag.BasicEdge(v1, v2))
d.Connect(dag.BasicEdge(v2, v4))
d.Connect(dag.BasicEdge(v0, v3))
d.Connect(dag.BasicEdge(v3, v4))
if err := d.Validate(); err != nil {
t.Fatal(err)
}
d.TransitiveReduction()
var steps [][]string
fn := func(n dag.Vertex, idx int) error {
if idx == 0 {
steps = make([][]string, 1)
steps[0] = make([]string, 0, 1)
} else if idx >= len(steps) {
tsteps := make([][]string, idx+1)
copy(tsteps, steps)
steps = tsteps
steps[idx] = make([]string, 0, 1)
}
steps[idx] = append(steps[idx], fmt.Sprintf("%s", n))
return nil
}
start := &node{"v0"}
err := d.SortedDepthFirstWalk([]dag.Vertex{start}, fn)
checkErr(t, err)
for idx, steps := range steps {
fmt.Printf("level %d steps %#+v\n", idx, steps)
}
if len(steps[2]) != 1 {
t.Logf("invalid steps %#+v", steps[2])
}
}
func checkErr(t *testing.T, err error) { func checkErr(t *testing.T, err error) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -3,17 +3,16 @@ package flow
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath"
"sync" "sync"
"github.com/google/uuid"
"github.com/silas/dag" "github.com/silas/dag"
"go.unistack.org/micro/v4/client" "github.com/unistack-org/micro/v3/client"
"go.unistack.org/micro/v4/codec" "github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/store"
moptions "go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/store"
"go.unistack.org/micro/v4/util/id"
) )
type microFlow struct { type microFlow struct {
@ -21,13 +20,13 @@ type microFlow struct {
} }
type microWorkflow struct { type microWorkflow struct {
opts Options
g *dag.AcyclicGraph
steps map[string]Step
id string id string
status Status g *dag.AcyclicGraph
sync.RWMutex
init bool init bool
sync.RWMutex
opts Options
steps map[string]Step
status Status
} }
func (w *microWorkflow) ID() string { func (w *microWorkflow) ID() string {
@ -150,22 +149,22 @@ func (w *microWorkflow) getSteps(start string, reverse bool) ([][]Step, error) {
return steps, nil return steps, nil
} }
func (w *microWorkflow) Abort(ctx context.Context, id string) error { func (w *microWorkflow) Abort(ctx context.Context, eid string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id) workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
} }
func (w *microWorkflow) Suspend(ctx context.Context, id string) error { func (w *microWorkflow) Suspend(ctx context.Context, eid string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id) workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())})
} }
func (w *microWorkflow) Resume(ctx context.Context, id string) error { func (w *microWorkflow) Resume(ctx context.Context, eid string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id) workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())})
} }
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...options.Option) (string, error) { func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
w.Lock() w.Lock()
if !w.init { if !w.init {
if err := w.g.Validate(); err != nil { if err := w.g.Validate(); err != nil {
@ -177,20 +176,21 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...optio
} }
w.Unlock() w.Unlock()
eid, err := id.New() uid, err := uuid.NewRandom()
if err != nil { if err != nil {
return "", err return "", err
} }
eid := uid.String()
stepStore := store.NewNamespaceStore(w.opts.Store, "steps"+w.opts.Store.Options().Separator+eid) stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+eid) workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
options := NewExecuteOptions(opts...) options := NewExecuteOptions(opts...)
steps, err := w.getSteps(options.Start, options.Reverse) steps, err := w.getSteps(options.Start, options.Reverse)
if err != nil { if err != nil {
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
} }
return "", err return "", err
} }
@ -202,25 +202,25 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...optio
nctx, cancel := context.WithCancel(ctx) nctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
nopts := make([]moptions.Option, 0, len(opts)+5) nopts := make([]ExecuteOption, 0, len(opts)+5)
nopts = append(nopts, nopts = append(nopts,
moptions.Client(w.opts.Client), ExecuteClient(w.opts.Client),
moptions.Tracer(w.opts.Tracer), ExecuteTracer(w.opts.Tracer),
moptions.Logger(w.opts.Logger), ExecuteLogger(w.opts.Logger),
moptions.Meter(w.opts.Meter), ExecuteMeter(w.opts.Meter),
) )
nopts = append(nopts, opts...) nopts = append(nopts, opts...)
done := make(chan struct{}) done := make(chan struct{})
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
return eid, werr return eid, werr
} }
for idx := range steps { for idx := range steps {
for nidx := range steps[idx] { for nidx := range steps[idx] {
cstep := steps[idx][nidx] cstep := steps[idx][nidx]
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
return eid, werr return eid, werr
} }
} }
@ -239,79 +239,80 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...optio
return return
} }
if w.opts.Logger.V(logger.TraceLevel) { if w.opts.Logger.V(logger.TraceLevel) {
w.opts.Logger.Trace(nctx, fmt.Sprintf("step will be executed %v", steps[idx][nidx])) w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx])
} }
cstep := steps[idx][nidx] cstep := steps[idx][nidx]
// nolint: nestif
if len(cstep.Requires()) == 0 { if len(cstep.Requires()) == 0 {
wg.Add(1) wg.Add(1)
go func(step Step) { go func(step Step) {
defer wg.Done() defer wg.Done()
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil {
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
rsp, serr := step.Execute(nctx, req, nopts...) rsp, serr := step.Execute(nctx, req, nopts...)
if serr != nil { if serr != nil {
step.SetStatus(StatusFailure) step.SetStatus(StatusFailure)
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
} }
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { 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.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
} }
cherr <- serr cherr <- serr
return return
} } else {
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil {
w.opts.Logger.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
w.opts.Logger.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
cherr <- werr cherr <- werr
return return
} }
}
}(cstep) }(cstep)
wg.Wait() wg.Wait()
} else { } else {
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil {
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
rsp, serr := cstep.Execute(nctx, req, nopts...) rsp, serr := cstep.Execute(nctx, req, nopts...)
if serr != nil { if serr != nil {
cstep.SetStatus(StatusFailure) cstep.SetStatus(StatusFailure)
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
} }
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { 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.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
} }
cherr <- serr cherr <- serr
return return
} } else {
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil {
w.opts.Logger.Error(ctx, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(ctx, "store write error: %v", werr)
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
} }
} }
} }
}
close(done) close(done)
}() }()
@ -319,7 +320,7 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...optio
return eid, nil return eid, nil
} }
w.opts.Logger.Trace(ctx, "wait for finish or error") logger.Tracef(ctx, "wait for finish or error")
select { select {
case <-nctx.Done(): case <-nctx.Done():
err = nctx.Err() err = nctx.Err()
@ -329,29 +330,31 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...optio
close(cherr) close(cherr)
case <-chstatus: case <-chstatus:
close(chstatus) close(chstatus)
return eid, nil return uid.String(), nil
} }
switch { switch {
case nctx.Err() != nil: case nctx.Err() != nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
} }
break
case err == nil: case err == nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
} }
break
case err != nil: case err != nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", "error", werr.Error()) w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr)
} }
break
} }
return eid, err return uid.String(), err
} }
// NewFlow create new flow func NewFlow(opts ...Option) Flow {
func NewFlow(opts ...options.Option) Flow {
options := NewOptions(opts...) options := NewOptions(opts...)
return &microFlow{opts: options} return &microFlow{opts: options}
} }
@ -360,7 +363,7 @@ func (f *microFlow) Options() Options {
return f.opts return f.opts
} }
func (f *microFlow) Init(opts ...options.Option) error { func (f *microFlow) Init(opts ...Option) error {
for _, o := range opts { for _, o := range opts {
o(&f.opts) o(&f.opts)
} }
@ -427,11 +430,11 @@ func (f *microFlow) WorkflowLoad(ctx context.Context, id string) (Workflow, erro
} }
type microCallStep struct { type microCallStep struct {
rsp *Message opts StepOptions
req *Message
service string service string
method string method string
opts StepOptions rsp *Message
req *Message
status Status status Status
} }
@ -489,20 +492,18 @@ func (s *microCallStep) SetStatus(status Status) {
s.status = status s.status = status
} }
func (s *microCallStep) Execute(ctx context.Context, req *Message, opts ...options.Option) (*Message, error) { func (s *microCallStep) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error) {
options := NewExecuteOptions(opts...) options := NewExecuteOptions(opts...)
if options.Client == nil { if options.Client == nil {
return nil, ErrMissingClient return nil, ErrMissingClient
} }
rsp := &codec.Frame{} rsp := &codec.Frame{}
copts := []moptions.Option{client.Retries(0)} copts := []client.CallOption{client.WithRetries(0)}
if options.Timeout > 0 { if options.Timeout > 0 {
copts = append(copts, copts = append(copts, client.WithRequestTimeout(options.Timeout), client.WithDialTimeout(options.Timeout))
client.RequestTimeout(options.Timeout),
client.DialTimeout(options.Timeout))
} }
nctx := metadata.NewOutgoingContext(ctx, req.Header) 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...) err := options.Client.Call(nctx, options.Client.NewRequest(s.service, s.method, &codec.Frame{Data: req.Body}), rsp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -511,10 +512,10 @@ func (s *microCallStep) Execute(ctx context.Context, req *Message, opts ...optio
} }
type microPublishStep struct { type microPublishStep struct {
opts StepOptions
topic string
req *Message req *Message
rsp *Message rsp *Message
topic string
opts StepOptions
status Status status Status
} }
@ -553,7 +554,7 @@ func (s *microPublishStep) String() string {
if s.opts.ID != "" { if s.opts.ID != "" {
return s.opts.ID return s.opts.ID
} }
return s.topic return fmt.Sprintf("%s", s.topic)
} }
func (s *microPublishStep) Name() string { func (s *microPublishStep) Name() string {
@ -572,18 +573,16 @@ func (s *microPublishStep) SetStatus(status Status) {
s.status = status s.status = status
} }
func (s *microPublishStep) Execute(ctx context.Context, req *Message, opts ...options.Option) (*Message, error) { func (s *microPublishStep) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error) {
return nil, nil return nil, nil
} }
// NewCallStep create new step with client.Call func NewCallStep(service string, name string, method string, opts ...StepOption) Step {
func NewCallStep(service string, name string, method string, opts ...options.Option) Step {
options := NewStepOptions(opts...) options := NewStepOptions(opts...)
return &microCallStep{service: service, method: name + "." + method, opts: options} return &microCallStep{service: service, method: name + "." + method, opts: options}
} }
// NewPublishStep create new step with client.Publish func NewPublishStep(topic string, opts ...StepOption) Step {
func NewPublishStep(topic string, opts ...options.Option) Step {
options := NewStepOptions(opts...) options := NewStepOptions(opts...)
return &microPublishStep{topic: topic, opts: options} return &microPublishStep{topic: topic, opts: options}
} }

View File

@ -1,5 +1,5 @@
// Package flow is an interface used for saga pattern microservice workflow // Package flow is an interface used for saga pattern microservice workflow
package flow // import "go.unistack.org/micro/v4/flow" package flow
import ( import (
"context" "context"
@ -7,14 +7,11 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"go.unistack.org/micro/v4/metadata" "github.com/unistack-org/micro/v3/metadata"
"go.unistack.org/micro/v4/options"
) )
var ( var (
// ErrStepNotExists returns when step not found
ErrStepNotExists = errors.New("step not exists") ErrStepNotExists = errors.New("step not exists")
// ErrMissingClient returns when client.Client is missing
ErrMissingClient = errors.New("client not set") ErrMissingClient = errors.New("client not set")
) )
@ -39,7 +36,6 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// Message used to transfer data between steps
type Message struct { type Message struct {
Header metadata.Metadata Header metadata.Metadata
Body RawMessage Body RawMessage
@ -52,7 +48,7 @@ type Step interface {
// 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 step run
Execute(ctx context.Context, req *Message, opts ...options.Option) (*Message, error) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (*Message, error)
// Requires returns dependent steps // Requires returns dependent steps
Requires() []string Requires() []string
// Options returns step options // Options returns step options
@ -71,7 +67,6 @@ type Step interface {
Response() *Message Response() *Message
} }
// Status contains step current status
type Status int type Status int
func (status Status) String() string { func (status Status) String() string {
@ -79,22 +74,15 @@ func (status Status) String() string {
} }
const ( const (
// StatusPending step waiting to start
StatusPending Status = iota StatusPending Status = iota
// StatusRunning step is running
StatusRunning StatusRunning
// StatusFailure step competed with error
StatusFailure StatusFailure
// StatusSuccess step completed without error
StatusSuccess StatusSuccess
// StatusAborted step aborted while it running
StatusAborted StatusAborted
// StatusSuspend step suspended
StatusSuspend StatusSuspend
) )
var ( var (
// StatusString contains map status => string
StatusString = map[Status]string{ StatusString = map[Status]string{
StatusPending: "StatusPending", StatusPending: "StatusPending",
StatusRunning: "StatusRunning", StatusRunning: "StatusRunning",
@ -103,7 +91,6 @@ var (
StatusAborted: "StatusAborted", StatusAborted: "StatusAborted",
StatusSuspend: "StatusSuspend", StatusSuspend: "StatusSuspend",
} }
// StringStatus contains map string => status
StringStatus = map[string]Status{ StringStatus = map[string]Status{
"StatusPending": StatusPending, "StatusPending": StatusPending,
"StatusRunning": StatusRunning, "StatusRunning": StatusRunning,
@ -119,7 +106,7 @@ type Workflow interface {
// ID returns id of the workflow // ID returns id of the workflow
ID() string ID() string
// Execute workflow with args, return execution id and error // Execute workflow with args, return execution id and error
Execute(ctx context.Context, req *Message, opts ...options.Option) (string, error) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error)
// RemoveSteps remove steps from workflow // RemoveSteps remove steps from workflow
RemoveSteps(steps ...Step) error RemoveSteps(steps ...Step) error
// AppendSteps append steps to workflow // AppendSteps append steps to workflow
@ -129,11 +116,11 @@ type Workflow interface {
// Steps returns steps slice where parallel steps returned on the same level // Steps returns steps slice where parallel steps returned on the same level
Steps() ([][]Step, error) Steps() ([][]Step, error)
// Suspend suspends execution // Suspend suspends execution
Suspend(ctx context.Context, id string) error Suspend(ctx context.Context, eid string) error
// Resume resumes execution // Resume resumes execution
Resume(ctx context.Context, id string) error Resume(ctx context.Context, eid string) error
// Abort abort execution // Abort abort execution
Abort(ctx context.Context, id string) error Abort(ctx context.Context, eid string) error
} }
// Flow the base interface to interact with workflows // Flow the base interface to interact with workflows
@ -141,7 +128,7 @@ type Flow interface {
// Options returns options // Options returns options
Options() Options Options() Options
// Init initialize // Init initialize
Init(...options.Option) error Init(...Option) error
// WorkflowCreate creates new workflow with specific id and steps // WorkflowCreate creates new workflow with specific id and steps
WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error)
// WorkflowSave saves workflow // WorkflowSave saves workflow
@ -157,7 +144,6 @@ var (
atomicSteps atomic.Value atomicSteps atomic.Value
) )
// RegisterStep register own step with workflow
func RegisterStep(step Step) { func RegisterStep(step Step) {
flowMu.Lock() flowMu.Lock()
steps, _ := atomicSteps.Load().([]Step) steps, _ := atomicSteps.Load().([]Step)

View File

@ -1,36 +0,0 @@
package flow
import (
"reflect"
"testing"
)
func FuzzMarshall(f *testing.F) {
f.Fuzz(func(t *testing.T, ref []byte) {
rm := RawMessage(ref)
b, err := rm.MarshalJSON()
if err != nil {
t.Errorf("Error MarshalJSON: %s", err)
}
if !reflect.DeepEqual(ref, b) {
t.Errorf("Error. Expected '%s', was '%s'", ref, b)
}
})
}
func FuzzUnmarshall(f *testing.F) {
f.Fuzz(func(t *testing.T, ref string) {
b := []byte(ref)
rm := RawMessage(b)
if err := rm.UnmarshalJSON(b); err != nil {
t.Errorf("Error UnmarshalJSON: %s", err)
}
if ref != string(rm) {
t.Errorf("Error. Expected '%s', was '%s'", ref, rm)
}
})
}

View File

@ -4,14 +4,16 @@ import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/client" "github.com/unistack-org/micro/v3/client"
"go.unistack.org/micro/v4/logger" "github.com/unistack-org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "github.com/unistack-org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "github.com/unistack-org/micro/v3/store"
"go.unistack.org/micro/v4/store" "github.com/unistack-org/micro/v3/tracer"
"go.unistack.org/micro/v4/tracer"
) )
// Option func
type Option func(*Options)
// Options server struct // Options server struct
type Options struct { type Options struct {
// Context holds the external options and can be used for flow shutdown // Context holds the external options and can be used for flow shutdown
@ -29,7 +31,7 @@ type Options struct {
} }
// NewOptions returns new options struct with default or passed values // NewOptions returns new options struct with default or passed values
func NewOptions(opts ...options.Option) Options { func NewOptions(opts ...Option) Options {
options := Options{ options := Options{
Context: context.Background(), Context: context.Background(),
Logger: logger.DefaultLogger, Logger: logger.DefaultLogger,
@ -45,13 +47,66 @@ func NewOptions(opts ...options.Option) Options {
return options return options
} }
// WorkflowOptions holds workflow options // Logger sets the logger option
type WorkflowOptions struct { func Logger(l logger.Logger) Option {
Context context.Context return func(o *Options) {
ID string 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 {
ID string
Context context.Context
}
// WorkflowID set workflow id
func WorkflowID(id string) WorkflowOption {
return func(o *WorkflowOptions) {
o.ID = id
}
} }
// ExecuteOptions holds execute options
type ExecuteOptions struct { type ExecuteOptions struct {
// Client holds the client.Client // Client holds the client.Client
Client client.Client Client client.Client
@ -65,30 +120,65 @@ type ExecuteOptions struct {
Context context.Context Context context.Context
// Start step // Start step
Start string Start string
// Timeout for execution
Timeout time.Duration
// Reverse execution // Reverse execution
Reverse bool Reverse bool
// Timeout for execution
Timeout time.Duration
// Async enables async execution // Async enables async execution
Async bool Async bool
} }
// Reverse says that dag must be run in reverse order type ExecuteOption func(*ExecuteOptions)
func Reverse(b bool) options.Option {
return func(src interface{}) error { func ExecuteClient(c client.Client) ExecuteOption {
return options.Set(src, b, ".Reverse") return func(o *ExecuteOptions) {
o.Client = c
} }
} }
// Async says that caller does not wait for execution complete func ExecuteTracer(t tracer.Tracer) ExecuteOption {
func Async(b bool) options.Option { return func(o *ExecuteOptions) {
return func(src interface{}) error { o.Tracer = t
return options.Set(src, b, ".Async")
} }
} }
// NewExecuteOptions create new ExecuteOptions struct func ExecuteLogger(l logger.Logger) ExecuteOption {
func NewExecuteOptions(opts ...options.Option) ExecuteOptions { 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{ options := ExecuteOptions{
Client: client.DefaultClient, Client: client.DefaultClient,
Logger: logger.DefaultLogger, Logger: logger.DefaultLogger,
@ -102,16 +192,16 @@ func NewExecuteOptions(opts ...options.Option) ExecuteOptions {
return options return options
} }
// StepOptions holds step options
type StepOptions struct { type StepOptions struct {
Context context.Context
Fallback string
ID string ID string
Context context.Context
Requires []string Requires []string
Fallback string
} }
// NewStepOptions create new StepOptions struct type StepOption func(*StepOptions)
func NewStepOptions(opts ...options.Option) StepOptions {
func NewStepOptions(opts ...StepOption) StepOptions {
options := StepOptions{ options := StepOptions{
Context: context.Background(), Context: context.Background(),
} }
@ -121,23 +211,20 @@ func NewStepOptions(opts ...options.Option) StepOptions {
return options return options
} }
// Requires specifies required steps func StepID(id string) StepOption {
func Requires(steps ...string) options.Option { return func(o *StepOptions) {
return func(src interface{}) error { o.ID = id
return options.Set(src, steps, ".Requires")
} }
} }
// Fallback set the step to run on error func StepRequires(steps ...string) StepOption {
func Fallback(step string) options.Option { return func(o *StepOptions) {
return func(src interface{}) error { o.Requires = steps
return options.Set(src, step, ".Fallback")
} }
} }
// ID sets the step ID func StepFallback(step string) StepOption {
func StepID(id string) options.Option { return func(o *StepOptions) {
return func(src interface{}) error { o.Fallback = step
return options.Set(src, id, ".ID")
} }
} }

View File

@ -1,126 +0,0 @@
package fsm
import (
"context"
"fmt"
"sync"
)
type state struct {
body interface{}
name string
}
var _ State = &state{}
func (s *state) Name() string {
return s.name
}
func (s *state) Body() interface{} {
return s.body
}
// fsm is a finite state machine
type fsm struct {
statesMap map[string]StateFunc
current string
statesOrder []string
opts Options
mu sync.Mutex
}
// NewFSM creates a new finite state machine having the specified initial state
// with specified options
func NewFSM(opts ...Option) *fsm {
return &fsm{
statesMap: map[string]StateFunc{},
opts: NewOptions(opts...),
}
}
// Current returns the current state
func (f *fsm) Current() string {
f.mu.Lock()
s := f.current
f.mu.Unlock()
return s
}
// Current returns the current state
func (f *fsm) Reset() {
f.mu.Lock()
f.current = f.opts.Initial
f.mu.Unlock()
}
// State adds state to fsm
func (f *fsm) State(state string, fn StateFunc) {
f.mu.Lock()
f.statesMap[state] = fn
f.statesOrder = append(f.statesOrder, state)
f.mu.Unlock()
}
// Start runs state machine with provided data
func (f *fsm) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
var err error
f.mu.Lock()
options := f.opts
for _, opt := range opts {
opt(&options)
}
sopts := []StateOption{StateDryRun(options.DryRun)}
cstate := options.Initial
states := make(map[string]StateFunc, len(f.statesMap))
for k, v := range f.statesMap {
states[k] = v
}
f.current = cstate
f.mu.Unlock()
var s State
s = &state{name: cstate, body: args}
nstate := s.Name()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
fn, ok := states[nstate]
if !ok {
return nil, fmt.Errorf(`state "%s" %w`, nstate, ErrInvalidState)
}
f.mu.Lock()
f.current = nstate
f.mu.Unlock()
// wrap the handler func
for i := len(options.Wrappers); i > 0; i-- {
fn = options.Wrappers[i-1](fn)
}
s, err = fn(ctx, s, sopts...)
switch {
case err != nil:
return s.Body(), err
case s.Name() == StateEnd:
return s.Body(), nil
case s.Name() == "":
for idx := range f.statesOrder {
if f.statesOrder[idx] == nstate && len(f.statesOrder) > idx+1 {
nstate = f.statesOrder[idx+1]
}
}
default:
nstate = s.Name()
}
}
}
}

View File

@ -1,29 +0,0 @@
package fsm // import "go.unistack.org/micro/v4/fsm"
import (
"context"
"errors"
)
var (
ErrInvalidState = errors.New("does not exists")
StateEnd = "end"
)
type State interface {
Name() string
Body() interface{}
}
// StateWrapper wraps the StateFunc and returns the equivalent
type StateWrapper func(StateFunc) StateFunc
// StateFunc called on state transition and return next step and error
type StateFunc func(ctx context.Context, state State, opts ...StateOption) (State, error)
type FSM interface {
Start(context.Context, interface{}, ...Option) (interface{}, error)
Current() string
Reset()
State(string, StateFunc)
}

View File

@ -1,72 +0,0 @@
package fsm
import (
"context"
"fmt"
"testing"
"go.unistack.org/micro/v4/logger"
)
func TestFSMStart(t *testing.T) {
ctx := context.TODO()
if err := logger.DefaultLogger.Init(); err != nil {
t.Fatal(err)
}
wrapper := func(next StateFunc) StateFunc {
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
sctx = logger.NewContext(sctx, logger.Attrs("state", s.Name()))
return next(sctx, s, opts...)
}
}
f := NewFSM(InitialState("1"), WrapState(wrapper))
f1 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
_, ok := logger.FromContext(sctx)
if !ok {
t.Fatal("f1 context does not have logger")
}
args := s.Body().(map[string]interface{})
if v, ok := args["request"].(string); !ok || v == "" {
return nil, fmt.Errorf("empty request")
}
return &state{name: "", body: map[string]interface{}{"response": "state1"}}, nil
}
f2 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
_, ok := logger.FromContext(sctx)
if !ok {
t.Fatal("f2 context does not have logger")
}
args := s.Body().(map[string]interface{})
if v, ok := args["response"].(string); !ok || v == "" {
return nil, fmt.Errorf("empty response")
}
return &state{name: "", body: map[string]interface{}{"response": "state2"}}, nil
}
f3 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
_, ok := logger.FromContext(sctx)
if !ok {
t.Fatal("f3 context does not have logger")
}
args := s.Body().(map[string]interface{})
if v, ok := args["response"].(string); !ok || v == "" {
return nil, fmt.Errorf("empty response")
}
return &state{name: StateEnd, body: map[string]interface{}{"response": "state3"}}, nil
}
f.State("1", f1)
f.State("2", f2)
f.State("3", f3)
rsp, err := f.Start(ctx, map[string]interface{}{"request": "state"})
if err != nil {
t.Fatal(err)
}
args := rsp.(map[string]interface{})
if v, ok := args["response"].(string); !ok || v == "" {
t.Fatalf("nil rsp: %#+v", args)
} else if v != "state3" {
t.Fatalf("invalid rsp %#+v", args)
}
}

View File

@ -1,52 +0,0 @@
package fsm
// Options struct holding fsm options
type Options struct {
// Initial state
Initial string
// Wrappers runs before state
Wrappers []StateWrapper
// DryRun mode
DryRun bool
}
// Option func signature
type Option func(*Options)
// StateOptions holds state options
type StateOptions struct {
DryRun bool
}
// StateDryRun says that state executes in dry run mode
func StateDryRun(b bool) StateOption {
return func(o *StateOptions) {
o.DryRun = b
}
}
// StateOption func signature
type StateOption func(*StateOptions)
// InitialState sets init state for state machine
func InitialState(initial string) Option {
return func(o *Options) {
o.Initial = initial
}
}
// WrapState adds a state Wrapper to a list of options passed into the fsm
func WrapState(w StateWrapper) Option {
return func(o *Options) {
o.Wrappers = append(o.Wrappers, w)
}
}
// NewOptions returns new Options struct filled by passed Option
func NewOptions(opts ...Option) Options {
options := Options{}
for _, o := range opts {
o(&options)
}
return options
}

Some files were not shown because too many files have changed in this diff Show More